feat(webui): add useRunningThreadIds merging local + heartbeat busy state
This commit is contained in:
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user