diff --git a/components/artifacts/index.ts b/components/artifacts/index.ts index 90867f0..6db5414 100644 --- a/components/artifacts/index.ts +++ b/components/artifacts/index.ts @@ -37,4 +37,11 @@ export const artifactRenderers: Record> ls: ToolCallCard, find: ToolCallCard, grep: ToolCallCard, + // The prober's graph tools (wave 7): same quiet expandable card — + // collapsed one-liner (tool name + the session id for + // list_propositions), the tool's own compact row-per-line output + // (name / level / date) verbatim on expand, exactly the read/ls + // discipline (their results are pre-formatted strings too). + list_sessions: ToolCallCard, + list_propositions: ToolCallCard, }; diff --git a/components/artifacts/tool-call-card.tsx b/components/artifacts/tool-call-card.tsx index 15143a1..f704016 100644 --- a/components/artifacts/tool-call-card.tsx +++ b/components/artifacts/tool-call-card.tsx @@ -33,6 +33,12 @@ function headlineArg(tool: string, input: unknown): string { return p; } if (typeof inp.pattern === "string") return inp.pattern; + // list_propositions: the session id is the call's subject — the + // collapsed line reads "list_propositions chat_…". Shown as given + // (the id is exactly what list_sessions reported to the model). + if (tool === "list_propositions" && typeof inp.session === "string" && inp.session.length > 0) { + return inp.session; + } return ""; } diff --git a/components/graph-view.tsx b/components/graph-view.tsx index ed57497..370ca34 100644 --- a/components/graph-view.tsx +++ b/components/graph-view.tsx @@ -919,6 +919,32 @@ export function GraphView() { return () => window.removeEventListener("lh:graph-revision", onRevision); }, [refreshCosts]); + /* Live rename labels (wave 7 interlude): a session rename anywhere + (sidebar inline row, chat header) announces itself as lh:chat-renamed + {id, title} — and the birth-session partition means that title IS + the cluster label. Patch the snapshot in place: the sessions map + (the detail card's provenance line) and the one matching cluster. + No refetch — the server derives the same join on its next read, this + just keeps the two views from disagreeing until then. Unlabeled + clusters keep their hull-only silence (a null label is a GONE + record — a rename can't reach one, and the guard holds the line). */ + useEffect(() => { + const onRenamed = (e: Event) => { + const { id, title } = (e as CustomEvent<{ id?: string; title?: string }>).detail ?? {}; + if (typeof id !== "string" || typeof title !== "string" || id === "" || title === "") return; + setSnapshot((snap) => { + if (snap == null) return snap; + const sessions = { ...snap.sessions, [id]: title }; + const clusters = snap.clusters.map((c) => + c.id === id && c.label != null ? { ...c, label: title } : c, + ); + return { ...snap, sessions, clusters }; + }); + }; + window.addEventListener("lh:chat-renamed", onRenamed); + return () => window.removeEventListener("lh:chat-renamed", onRenamed); + }, []); + // Layout (Option A): the positions ledger (written by the server's // incremental settle after every merge) IS the layout — the renderer // receives it as frozen data. The local one-shot forceAtlas2 pass runs diff --git a/tests/demos.test.tsx b/tests/demos.test.tsx index a7aac25..2c80f6d 100644 --- a/tests/demos.test.tsx +++ b/tests/demos.test.tsx @@ -855,6 +855,42 @@ describe("tool + renderer registry wiring", () => { for (const t of ["read", "ls", "find", "grep"]) { assert.equal(artifactRenderers[t], ToolCallCard, `${t} renders the tool-call card`); } + // wave 7 — the prober's graph tools join the SAME discipline (they + // rendered like nothing happened before this interlude) + for (const t of ["list_sessions", "list_propositions"]) { + assert.equal(artifactRenderers[t], ToolCallCard, `${t} renders the tool-call card`); + } + }); + + it("the tool-call card: the prober's graph tools — collapsed headline + their compact row output", () => { + // list_propositions: the session id is the call's subject + const propPart = { + type: "tool-list_propositions", + state: "output-available", + toolCallId: "g1", + input: { session: "chat_mtiudjao_776beada" }, + output: "2 proposition(s) born in this session:\n- Kepler's laws — solid (as of 2026-09-20)\n- Tidal locking — shaky (as of 2026-09-21)", + }; + const html = renderToStaticMarkup(); + assert.match(html, /list_propositions/); + assert.match(html, /chat_mtiudjao_776beada/); // the collapsed headline + assert.match(html, /Kepler's laws/); // the row output, verbatim (apostrophe HTML-escaped) + assert.ok(html.includes("); + assert.match(html2, /list_sessions/); + assert.match(html2, /Orbital mechanics/); + const headline = html2.split("")[0] ?? ""; + assert.doesNotMatch(headline, /Orbital/, "the collapsed line shows the tool name only — no noise"); }); it("the tool-call card: collapsed one-liner with the tool name + headline arg; expand shows input and output", () => {