From 7479dce8ef5e9bb32b36a010d9902aadd9c09b7b Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Wed, 23 Sep 2026 16:16:51 +0300 Subject: [PATCH] Wave 7 interlude: prober tool-call chrome + live rename cluster labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Graph tool-call visibility — the prober's list_sessions / list_propositions rendered like nothing happened in the transcript (the registry default is silence for unregistered tools). They now join the EXACT read/ls/find/grep discipline: artifactRenderers maps both to the ToolCallCard — collapsed one-liner, expandable Input/Output card. The results ride the card's string path verbatim (the tools' own compact name/level/date line format), exactly how read results render. - list_propositions gets a headline arg: the session id is the call's subject — the collapsed line reads "list_propositions chat_…". Shown as given, never resolved (the same no-resolution rule as read's path). list_sessions carries no arg (empty input — the ls-with-no-path discipline). - No bespoke row-layout card: the outputs are pre-formatted strings (the model-facing format IS the learner-facing format here), and the dispatch's "SAME look" directive settles the shape question. Live rename labels — renaming a session anywhere (sidebar inline row, chat header) already announces lh:chat-renamed {id, title}; the graph view now listens. The birth-session partition means the title IS the cluster label, so the patch is targeted and in-place, no refetch: the snapshot's sessions map (the detail card's provenance line) and the one matching cluster. The new snapshot object flows the existing setData → geoVersion → rebuild chain, so the wrapped label (wave 7's wrapRegionLabel) re-wraps for the new text. Unlabeled clusters keep their hull-only silence — a null label is a GONE record, the guard holds the line. Tests: registry pins for both tools + the prober-card render (collapsed headline, verbatim rows, no-arg discipline). 855/855; live-verified on :3001 with fillText instrumentation: both cards render collapsed and expandable in the transcript, and a sidebar rename flipped the canvas cluster label old→new with the old label's draws stopping cold. --- components/artifacts/index.ts | 7 +++++ components/artifacts/tool-call-card.tsx | 6 +++++ components/graph-view.tsx | 26 ++++++++++++++++++ tests/demos.test.tsx | 36 +++++++++++++++++++++++++ 4 files changed, 75 insertions(+) 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", () => {