phase: 117_tool_call_compact
All verification complete — every gate green. Final report: **Phase 117 final verification pass — all criteria verified** (tasks 01–04 were already in `complete/`; I re-verified the full gate myself, no code changes needed) - **Verified:** `appendToolLine` wraps lines in `details.tool-calls-disclosure` (open live, `closeToolCalls` at delta/done/stop + restore-closed, count via textContent); `addToolLines` parity (closed); `.tool-call` deboxed / `code` dechipped; new disclosure/summary CSS; only the one pinned CSS assert updated - `uv run pytest` → **2466 passed** - `uv run pytest --cov=app --cov-report=term-missing` → **99%** on `app/` (>90% floor) - E2E in isolation: `test_tool_call_compact.py` **4 passed** (fold/expand/deboxed-flow/restored-folded); `test_agent_document_tools.py` **4 passed**; `test_big_read_progress.py` **4 passed** (live disclosure open + `.tool-elapsed` visible); `test_thinking_display.py` **5 passed**; `test_smoke.py` **3 passed** - `uv run ruff check . && uv run pyright` → clean (pyright 0 errors, 0 warnings) - Completion criteria: pins 1–4 ✅ (E2E above); live "calling tool" state byte-identical ✅ (`test_frontend_tool_states.py` 10 passed incl. `test_calling_tool_label_strings`); scope ✅ — `git diff` limited to the 3 frontend assets + 2 unit tests + E2E story + phase files, **0 changes in `app/`** - Notable: `TODO.md` carries a **pre-existing uncommitted owner edit** (new retrieval-context idea) already in the tree before this pass — left untouched, will ride along in the harness commit; pre-existing committed quirk: the brain-wave commit's styles.css comment mislabels itself "Phase 117" (cosmetic, out of this diff) - No commit made (harness commits + moves the phase); **next pending phase: none** — `todo/` holds only phase 117
This commit is contained in:
+47
-6
@@ -942,7 +942,10 @@ function ensureThinkingBlock(wrap) {
|
||||
// Phase 37: the scratchpad stays the TOP row of the wrap — if tool
|
||||
// lines are already there (a `tool` frame preceded the first
|
||||
// `thinking` frame), the block lands above them, not below.
|
||||
// Phase 117: the anchor is the WHOLE disclosure (the bare .tool-calls
|
||||
// list term stays as the pre-117 fallback).
|
||||
const anchor =
|
||||
body.querySelector(".tool-calls-disclosure") ??
|
||||
body.querySelector(".tool-calls") ?? body.querySelector(".bubble");
|
||||
body.insertBefore(block, anchor);
|
||||
}
|
||||
@@ -954,6 +957,17 @@ function closeThinkingBlock(wrap) {
|
||||
if (block) block.open = false; // idempotent; no-op without a block
|
||||
}
|
||||
|
||||
/* Phase 117 (D3): the tool-calls disclosure settles FOLDED at rest —
|
||||
* the same sites that settle the Thinking block: the answer began
|
||||
* (`delta`), the turn ended (`done`), or the turn was stopped (the
|
||||
* abort settle). The N-line record collapses to one compact
|
||||
* "Tool call(s) (N)" line. Idempotent; a no-op without a disclosure
|
||||
* (deflected answers, pre-tool turns, …). */
|
||||
function closeToolCalls(wrap) {
|
||||
const disc = wrap?.querySelector?.(".tool-calls-disclosure");
|
||||
if (disc) disc.open = false; // idempotent; no-op without a disclosure
|
||||
}
|
||||
|
||||
/* ---------- tool-call lines (phase 37, PLAN §4 extension) ----------
|
||||
* One visible "calling tool" row per `tool` SSE frame, in the same wrap
|
||||
* the Thinking block uses — above the answer, below the Thinking
|
||||
@@ -967,6 +981,14 @@ function closeThinkingBlock(wrap) {
|
||||
* HTML-shaped can come from storage. Lines are not interactive (no
|
||||
* focus targets).
|
||||
*
|
||||
* Phase 117 (owner report 2026-09-15): the list rides in a native
|
||||
* <details> disclosure (details.tool-calls-disclosure + a plain-text
|
||||
* "Tool call(s) (N)" summary) — OPEN while the turn is live, folded by
|
||||
* closeToolCalls (delta/done/stop) and by the restore path (a restored
|
||||
* turn is one compact line, not N stacked cards). The line bytes, the
|
||||
* four label literals, and the <code> textContent arguments are
|
||||
* untouched (phase-87 clock + emoji-guard pins stay green).
|
||||
*
|
||||
* Phase 70 (owner permission 2026-09-03): the server tools were remapped
|
||||
* to the harness-aligned surface — ls / read(path) / grep(pattern,
|
||||
* path?) — so the NEW names get their own lines (read → the Reading
|
||||
@@ -981,16 +1003,26 @@ function closeThinkingBlock(wrap) {
|
||||
function appendToolLine(wrap, name, argument) {
|
||||
const body = wrap?.querySelector?.(".msg-body");
|
||||
if (!body) return;
|
||||
let container = body.querySelector(".tool-calls");
|
||||
// Phase 117 (D2): the lines ride in a native <details> disclosure —
|
||||
// one compact "Tool call(s) (N)" line instead of N stacked cards.
|
||||
let container = body.querySelector(".tool-calls-disclosure");
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.className = "tool-calls";
|
||||
container.setAttribute("role", "list");
|
||||
container.setAttribute("aria-label", "Tool calls");
|
||||
container = document.createElement("details");
|
||||
container.className = "tool-calls-disclosure";
|
||||
container.open = true; // D3: open while the turn is live; closeToolCalls folds it
|
||||
const summary = document.createElement("summary");
|
||||
summary.className = "tool-calls-summary";
|
||||
container.appendChild(summary);
|
||||
const listEl = document.createElement("div");
|
||||
listEl.className = "tool-calls";
|
||||
listEl.setAttribute("role", "list");
|
||||
listEl.setAttribute("aria-label", "Tool calls");
|
||||
container.appendChild(listEl);
|
||||
// Before the bubble; below an existing Thinking block (both insert
|
||||
// before the bubble, so document order is preserved).
|
||||
body.insertBefore(container, body.querySelector(".bubble"));
|
||||
}
|
||||
const list = container.querySelector(".tool-calls");
|
||||
const line = document.createElement("span");
|
||||
line.className = "tool-call";
|
||||
line.setAttribute("role", "listitem");
|
||||
@@ -1015,7 +1047,12 @@ function appendToolLine(wrap, name, argument) {
|
||||
} else {
|
||||
line.textContent = "🔎 Listing documents";
|
||||
}
|
||||
container.appendChild(line);
|
||||
list.appendChild(line);
|
||||
// Phase 117 (D5): the count rides the summary — every append (live
|
||||
// frames and the restore path) re-renders through here.
|
||||
const n = list.children.length;
|
||||
container.querySelector("summary").textContent =
|
||||
`Tool call${n === 1 ? "" : "s"} (${n})`;
|
||||
}
|
||||
|
||||
/* Phase 95 (task 02): the truncation marker on a Reading line. The
|
||||
@@ -1636,6 +1673,7 @@ function renderStoredMessage(m) {
|
||||
appendTruncatedNote(wrap, arg, Number(t.chars_shown) || 0, Number(t.chars_total) || 0);
|
||||
}
|
||||
}
|
||||
closeToolCalls(wrap); // phase 117 (D3): a restored turn renders folded
|
||||
}
|
||||
if (m.deflected) {
|
||||
wrap.classList.add("is-deflected");
|
||||
@@ -2512,11 +2550,13 @@ async function runTurn(text, { reask = false } = {}) {
|
||||
if (uiState === UI_STATE.thinking) setUiState(UI_STATE.streaming);
|
||||
if (!wrap) wrap = addMessage("brain", ""); // first token: live bubble in
|
||||
closeThinkingBlock(wrap); // auto-collapse; idempotent — the next `thinking` frame re-opens it (phase 109, D15)
|
||||
closeToolCalls(wrap); // the answer began — fold the tool record (phase 117, D3)
|
||||
wrap.querySelector(".bubble").innerHTML = renderMarkdown(acc);
|
||||
// No page scroll (phase 42): the answer never follows the viewport.
|
||||
} else if (ev.type === "done") {
|
||||
sawDone = true;
|
||||
closeThinkingBlock(wrap); // the turn is over: settle the block closed
|
||||
closeToolCalls(wrap); // the turn ended — settle the record folded (phase 117, D3)
|
||||
if (!wrap) {
|
||||
setUiState(UI_STATE.streaming);
|
||||
wrap = addMessage("brain", "…");
|
||||
@@ -2644,6 +2684,7 @@ async function runTurn(text, { reask = false } = {}) {
|
||||
// which aborts the same fetch — from saving the partial twice.
|
||||
if (wrap && acc && !persistedOnLeave) {
|
||||
closeThinkingBlock(wrap); // settle the block closed, like `done`
|
||||
closeToolCalls(wrap); // the turn was stopped — fold the record (phase 117, D3)
|
||||
persistedOnLeave = true;
|
||||
appendTuneButton(wrap); // admin-only; parity with the restore path
|
||||
appendStoppedNote(wrap);
|
||||
|
||||
Reference in New Issue
Block a user