@@ -83,7 +51,7 @@ export function ArtifactsRail({
void load()}
+ onClick={onRefresh}
disabled={refreshing}
aria-label="Refresh session files"
title="Refresh"
@@ -117,3 +85,63 @@ export function ArtifactsRail({
);
}
+
+export function ArtifactsRail({
+ threadId,
+ live,
+}: {
+ readonly threadId: string;
+ readonly live: boolean;
+}) {
+ const [artifacts, setArtifacts] = useState
([]);
+ const [available, setAvailable] = useState(null);
+ const [refreshing, setRefreshing] = useState(false);
+
+ const load = useCallback(
+ async ({ manual = false, signal }: { manual?: boolean; signal?: AbortSignal } = {}) => {
+ // Only a viewer's click spins the icon; the 5-second poll and org-change
+ // reloads refresh quietly so the control is not in constant motion.
+ if (manual) setRefreshing(true);
+ try {
+ const response = await backendFetch(artifactQueryForThread(threadId), {
+ cache: "no-store",
+ signal,
+ });
+ if (!response.ok) throw new Error(`artifact request failed (${response.status})`);
+ const parsed = extractArtifacts(await response.json());
+ if (!parsed) throw new Error("artifact response was invalid");
+ setArtifacts(parsed);
+ setAvailable(true);
+ } catch {
+ if (!signal?.aborted) setAvailable(false);
+ } finally {
+ if (manual) setRefreshing(false);
+ }
+ },
+ [threadId],
+ );
+
+ useOrgChanges((change) => {
+ if (change.type === "artifact" && change.threadId === threadId) void load();
+ });
+
+ useEffect(() => {
+ const controller = new AbortController();
+ void load({ signal: controller.signal });
+ if (!live) return () => controller.abort();
+ const interval = setInterval(() => void load({ signal: controller.signal }), LIVE_REFRESH_MS);
+ return () => {
+ controller.abort();
+ clearInterval(interval);
+ };
+ }, [live, load]);
+
+ return (
+ void load({ manual: true })}
+ />
+ );
+}