feat(webui): add useRunningThreadIds merging local + heartbeat busy state

This commit is contained in:
m4
2026-08-12 12:13:39 +08:00
parent 93500c3020
commit 8fb1f299e6
2 changed files with 46 additions and 7 deletions
+20
View File
@@ -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"]);
});
});
+26 -7
View File
@@ -125,7 +125,24 @@ export function useLocalRunningThreadIds(): ReadonlySet<string> {
return useSyncExternalStore(subscribeRunningThreads, getRunningThreadIds);
}
export async function deleteThread(id: string): Promise<void> { 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<string>,
local: Iterable<string>
): ReadonlySet<string> {
return new Set([...busy, ...local]);
}
export function useRunningThreadIds(): ReadonlySet<string> {
const local = useLocalRunningThreadIds();
const busy = useBusyThreadIds();
return useMemo(() => mergeRunningIds(busy, local), [busy, local]);
}
export async function deleteThread(id: string): Promise<void> {
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(