Wave 7 interlude: prober tool-call chrome + live rename cluster labels

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.
This commit is contained in:
amosblomqvist
2026-09-23 16:16:51 +03:00
parent 7a17230f26
commit 7479dce8ef
4 changed files with 75 additions and 0 deletions
+7
View File
@@ -37,4 +37,11 @@ export const artifactRenderers: Record<string, ComponentType<ArtifactPartProps>>
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,
};
+6
View File
@@ -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 "";
}
+26
View File
@@ -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
+36
View File
@@ -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(<ToolCallCard part={propPart as never} />);
assert.match(html, /list_propositions/);
assert.match(html, /chat_mtiudjao_776beada/); // the collapsed headline
assert.match(html, /Kepler&#x27;s laws/); // the row output, verbatim (apostrophe HTML-escaped)
assert.ok(html.includes("<details"), "collapsed by default");
// list_sessions: no headline arg (empty input) — the tool name alone,
// exactly the ls-with-no-path discipline
const sessPart = {
type: "tool-list_sessions",
state: "output-available",
toolCallId: "g2",
input: {},
output: "3 session(s) in the knowledge graph, most recent first:\n1. Orbital mechanics (chat, 8 propositions, born 2026-09-18)",
};
const html2 = renderToStaticMarkup(<ToolCallCard part={sessPart as never} />);
assert.match(html2, /list_sessions/);
assert.match(html2, /Orbital mechanics/);
const headline = html2.split("<summary")[1]?.split("</summary>")[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", () => {