diff --git a/src/app/hooks/useThreads.running.test.ts b/src/app/hooks/useThreads.running.test.ts index e10398f..dad873b 100644 --- a/src/app/hooks/useThreads.running.test.ts +++ b/src/app/hooks/useThreads.running.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest"; import { getRunningThreadIds, + mergeRunningIds, setThreadRunning, subscribeRunningThreads, } from "./useThreads"; @@ -36,3 +37,22 @@ describe("running thread store", () => { setThreadRunning("thread-c", false); }); }); + +describe("mergeRunningIds", () => { + it("returns busy-only ids", () => { + const merged = mergeRunningIds(["a"], []); + expect(merged.has("a")).toBe(true); + expect(merged.size).toBe(1); + }); + + it("returns local-only ids", () => { + const merged = mergeRunningIds([], ["b"]); + expect(merged.has("b")).toBe(true); + expect(merged.size).toBe(1); + }); + + it("merges both sources without duplicates", () => { + const merged = mergeRunningIds(["a", "b"], ["b", "c"]); + expect([...merged].sort()).toEqual(["a", "b", "c"]); + }); +}); diff --git a/src/app/hooks/useThreads.ts b/src/app/hooks/useThreads.ts index 139e9b6..e94a611 100644 --- a/src/app/hooks/useThreads.ts +++ b/src/app/hooks/useThreads.ts @@ -125,7 +125,24 @@ export function useLocalRunningThreadIds(): ReadonlySet { return useSyncExternalStore(subscribeRunningThreads, getRunningThreadIds); } -export async function deleteThread(id: string): Promise { const response = await fetch(`/api/conversations/${encodeURIComponent(id)}`, { +/** Merged view of "this thread has an active run": the zero-latency local + * store (this client's own runs) plus the 10s busy heartbeat (runs started by + * other tabs or scheduled tasks). */ +export function mergeRunningIds( + busy: Iterable, + local: Iterable +): ReadonlySet { + return new Set([...busy, ...local]); +} + +export function useRunningThreadIds(): ReadonlySet { + const local = useLocalRunningThreadIds(); + const busy = useBusyThreadIds(); + return useMemo(() => mergeRunningIds(busy, local), [busy, local]); +} + +export async function deleteThread(id: string): Promise { + const response = await fetch(`/api/conversations/${encodeURIComponent(id)}`, { method: "DELETE", }); if (!response.ok) { @@ -157,12 +174,14 @@ export async function setThreadModelSelection( } function slugifyForFilename(input: string): string { - return input - // eslint-disable-next-line no-control-regex - .replace(/[\\/:*?"<>|\u0000-\u001F]+/g, "") - .trim() - .replace(/\s+/g, "-") - .slice(0, 80); + return ( + input + // eslint-disable-next-line no-control-regex + .replace(/[\\/:*?"<>|\u0000-\u001F]+/g, "") + .trim() + .replace(/\s+/g, "-") + .slice(0, 80) + ); } export async function exportThread(