Skip to content

Commit fc75f47

Browse files
authored
Automatically refresh Artifacts panel (#468)
This PR triggers an automatic refresh of the Artifacts panel after every completed tool call message and completed session so that users don't need to click the Refresh button manually. It also adds a badge to the panel's header whenever artifacts exist, which should make it easier to spot that artifacts have been created when the panel is collapsed (like it is by default). cc @Daniel-Warner-X ## Demo https://github.com/user-attachments/assets/f430bfeb-660b-4d20-a0ea-b12c6b3d1156
1 parent 43ea458 commit fc75f47

File tree

2 files changed

+135
-4
lines changed

2 files changed

+135
-4
lines changed

components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/accordions/artifacts-accordion.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"use client";
22

3+
import { useMemo } from "react";
34
import { Folder, NotepadText, Download, FolderSync, Loader2 } from "lucide-react";
45
import { AccordionItem, AccordionTrigger, AccordionContent } from "@/components/ui/accordion";
56
import { Button } from "@/components/ui/button";
7+
import { Badge } from "@/components/ui/badge";
68
import { FileTree, type FileTreeNode } from "@/components/file-tree";
79

810
type WorkspaceFile = {
@@ -33,12 +35,26 @@ export function ArtifactsAccordion({
3335
onDownloadFile,
3436
onNavigateBack,
3537
}: ArtifactsAccordionProps) {
38+
// Count total files (not directories) - memoized to avoid recalculation on every render
39+
const fileCount = useMemo(() => files.filter((f) => !f.isDir).length, [files]);
40+
3641
return (
3742
<AccordionItem value="artifacts" className="border rounded-lg px-3 bg-card">
3843
<AccordionTrigger className="text-base font-semibold hover:no-underline py-3">
39-
<div className="flex items-center gap-2">
44+
<div className="flex items-center gap-2 w-full">
4045
<NotepadText className="h-4 w-4" />
4146
<span>Artifacts</span>
47+
{fileCount > 0 && (
48+
<Badge
49+
variant="secondary"
50+
className="ml-auto mr-2"
51+
aria-live="polite"
52+
aria-atomic="true"
53+
>
54+
<span className="sr-only">{fileCount} {fileCount === 1 ? 'file' : 'files'}</span>
55+
{fileCount}
56+
</Badge>
57+
)}
4258
</div>
4359
</AccordionTrigger>
4460
<AccordionContent className="pt-2 pb-3">

components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useState, useEffect, useMemo, useRef } from "react";
3+
import { useState, useEffect, useMemo, useRef, useCallback } from "react";
44
import {
55
Loader2,
66
FolderTree,
@@ -106,6 +106,40 @@ import {
106106
} from "@/services/queries/use-workflows";
107107
import { useMutation } from "@tanstack/react-query";
108108

109+
// Constants for artifact auto-refresh timing
110+
// Moved outside component to avoid unnecessary effect re-runs
111+
//
112+
// Wait 1 second after last tool completion to batch rapid writes together
113+
// Prevents excessive API calls during burst writes (e.g., when Claude creates multiple files in quick succession)
114+
// Testing: 500ms was too aggressive (hit API rate limits), 2000ms felt sluggish to users
115+
const ARTIFACTS_DEBOUNCE_MS = 1000;
116+
117+
// Wait 2 seconds after session completes before final artifact refresh
118+
// Backend can take 1-2 seconds to flush final artifacts to storage
119+
// Ensures users see all artifacts even if final writes occur after status transition
120+
const COMPLETION_DELAY_MS = 2000;
121+
122+
/**
123+
* Type guard to check if a message is a completed ToolUseMessages with result.
124+
* Extracted for testability and proper validation.
125+
* Uses proper type assertion and validation.
126+
*/
127+
function isCompletedToolUseMessage(msg: MessageObject | ToolUseMessages): msg is ToolUseMessages {
128+
if (msg.type !== "tool_use_messages") {
129+
return false;
130+
}
131+
132+
// Cast to ToolUseMessages for proper type checking
133+
const toolMsg = msg as ToolUseMessages;
134+
135+
return (
136+
toolMsg.resultBlock !== undefined &&
137+
toolMsg.resultBlock !== null &&
138+
typeof toolMsg.resultBlock === "object" &&
139+
toolMsg.resultBlock.content !== null
140+
);
141+
}
142+
109143
export default function ProjectSessionDetailPage({
110144
params,
111145
}: {
@@ -391,16 +425,26 @@ export default function ProjectSessionDetailPage({
391425
basePath: "artifacts",
392426
});
393427

394-
const { data: artifactsFiles = [], refetch: refetchArtifactsFiles } =
428+
const { data: artifactsFiles = [], refetch: refetchArtifactsFilesRaw } =
395429
useWorkspaceList(
396430
projectName,
397431
sessionName,
398432
artifactsOps.currentSubPath
399433
? `artifacts/${artifactsOps.currentSubPath}`
400434
: "artifacts",
401-
{ enabled: openAccordionItems.includes("artifacts") },
402435
);
403436

437+
// Stabilize refetchArtifactsFiles with useCallback to prevent infinite re-renders
438+
// React Query's refetch is already stable, but this ensures proper dependency tracking
439+
const refetchArtifactsFiles = useCallback(async () => {
440+
try {
441+
await refetchArtifactsFilesRaw();
442+
} catch (error) {
443+
console.error('Failed to refresh artifacts:', error);
444+
// Silent fail - don't interrupt user experience
445+
}
446+
}, [refetchArtifactsFilesRaw]);
447+
404448
// File uploads list (for Context accordion)
405449
const { data: fileUploadsList = [], refetch: refetchFileUploadsList } =
406450
useWorkspaceList(
@@ -511,6 +555,77 @@ export default function ProjectSessionDetailPage({
511555
);
512556
}, [messages, session?.spec?.interactive]);
513557

558+
// Auto-refresh artifacts when messages complete
559+
// UX improvement: Automatically refresh the artifacts panel when Claude writes new files,
560+
// so users can see their changes immediately without manually clicking the refresh button
561+
const previousToolResultCount = useRef(0);
562+
const artifactsRefreshTimeoutRef = useRef<NodeJS.Timeout | null>(null);
563+
const completionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
564+
const hasRefreshedOnCompletionRef = useRef(false);
565+
566+
// Memoize the completed tool count to avoid redundant filtering
567+
// Uses extracted type guard for testability and proper validation
568+
const completedToolCount = useMemo(() => {
569+
return streamMessages.filter(isCompletedToolUseMessage).length;
570+
}, [streamMessages]);
571+
572+
useEffect(() => {
573+
// Initialize on first mount to avoid triggering refresh for existing tools
574+
if (previousToolResultCount.current === 0 && completedToolCount > 0) {
575+
previousToolResultCount.current = completedToolCount;
576+
return;
577+
}
578+
579+
// If we have new completed tools, refresh artifacts after a short delay
580+
if (completedToolCount > previousToolResultCount.current && completedToolCount > 0) {
581+
// Clear any pending refresh timeout
582+
if (artifactsRefreshTimeoutRef.current) {
583+
clearTimeout(artifactsRefreshTimeoutRef.current);
584+
}
585+
586+
// Debounce refresh to avoid excessive calls during rapid tool completions
587+
artifactsRefreshTimeoutRef.current = setTimeout(() => {
588+
refetchArtifactsFiles();
589+
}, ARTIFACTS_DEBOUNCE_MS);
590+
591+
previousToolResultCount.current = completedToolCount;
592+
}
593+
594+
// Cleanup timeout on unmount or effect re-run
595+
return () => {
596+
if (artifactsRefreshTimeoutRef.current) {
597+
clearTimeout(artifactsRefreshTimeoutRef.current);
598+
}
599+
};
600+
}, [completedToolCount, refetchArtifactsFiles]);
601+
602+
// Also refresh artifacts when session completes (catch any final artifacts)
603+
useEffect(() => {
604+
const phase = session?.status?.phase;
605+
if (phase === "Completed" && !hasRefreshedOnCompletionRef.current) {
606+
// Refresh after a short delay to ensure all final writes are complete
607+
completionTimeoutRef.current = setTimeout(() => {
608+
refetchArtifactsFiles();
609+
}, COMPLETION_DELAY_MS);
610+
hasRefreshedOnCompletionRef.current = true;
611+
} else if (phase !== "Completed") {
612+
// Clear any pending completion refresh to avoid race conditions
613+
if (completionTimeoutRef.current) {
614+
clearTimeout(completionTimeoutRef.current);
615+
completionTimeoutRef.current = null;
616+
}
617+
// Reset flag whenever leaving Completed state (handles Running, Error, Cancelled, etc.)
618+
hasRefreshedOnCompletionRef.current = false;
619+
}
620+
621+
// Cleanup timeout on unmount or phase change
622+
return () => {
623+
if (completionTimeoutRef.current) {
624+
clearTimeout(completionTimeoutRef.current);
625+
}
626+
};
627+
}, [session?.status?.phase, refetchArtifactsFiles]);
628+
514629
// Session action handlers
515630
const handleStop = () => {
516631
stopMutation.mutate(

0 commit comments

Comments
 (0)