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:
@@ -248,20 +248,21 @@ def test_thinking_block_stays_on_top_of_tool_lines() -> None:
|
||||
|
||||
|
||||
def test_tool_call_style_is_accent_and_contrast_safe() -> None:
|
||||
"""styles.css: .tool-call is an inline row with the accent palette
|
||||
(distinct from the brand-ink Thinking block) and mono `code` styling
|
||||
for the path; the wrapper stacks lines without shifting the column."""
|
||||
"""styles.css (phase 117 debox): .tool-call is a deboxed
|
||||
inline-flow row — the label + the mono `code` path are ONE
|
||||
continuous wrapping run (no flex, no card), the accent rides the
|
||||
TEXT color (distinct from the brand-ink Thinking block), and the
|
||||
`code` is inline mono text with no chip; the wrapper stacks lines
|
||||
without shifting the column."""
|
||||
css = _css()
|
||||
assert ".tool-calls" in css
|
||||
assert ".tool-call" in css
|
||||
m = re.search(r"\.tool-call \{([^}]*)\}", css)
|
||||
assert m, "the .tool-call rule must exist"
|
||||
row = m.group(1)
|
||||
assert "display: flex" in row, "inline row: icon + text"
|
||||
assert "var(--accent-ink)" in row, (
|
||||
"accent color distinguishes it from the thinking block (≈10.4:1 on surface)"
|
||||
)
|
||||
assert "var(--accent-line)" in row, "accent left border"
|
||||
code = re.search(r"\.tool-call code \{([^}]*)\}", css)
|
||||
assert code, "the path `code` must be styled"
|
||||
assert "var(--mono)" in code.group(1)
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
"""Unit: the phase-117 tool-calls DISCLOSURE contract (task 01).
|
||||
|
||||
The owner's visual-glitch report (2026-09-15): one per-call tool "card"
|
||||
per ``tool`` frame — 6+ stacked full-width bordered cards on a phone —
|
||||
swamped a completed answer, and the flex row broke the label
|
||||
mid-word. The fix (phase 117, owner decisions D1–D6) wraps the
|
||||
existing ``.tool-calls`` list in a native ``details`` disclosure with a
|
||||
plain-text "Tool call(s) (N)" summary — created OPEN on the first live
|
||||
``tool`` frame, folded at rest (delta/done/stop via ``closeToolCalls``),
|
||||
and rendered CLOSED on the restore path and the shared page (D3). The
|
||||
line bytes, the four label literals, and the ``<code>`` textContent
|
||||
arguments stay byte-identical (D6), so the phase-37/70/87/95 pins
|
||||
stay green.
|
||||
|
||||
No ``app/`` logic (frontend-only, D1) — like
|
||||
``test_frontend_tool_states.py``, this module pins the JS/CSS markers
|
||||
at source level: the disclosure/summary build (task 01) and the
|
||||
debox/dechip + disclosure CSS contract (task 02). The E2E story suite
|
||||
(task 03) completes the phase.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
|
||||
APP_JS = FRONTEND / "assets" / "app.js"
|
||||
SHARED_JS = FRONTEND / "assets" / "shared.js"
|
||||
STYLES_CSS = FRONTEND / "assets" / "styles.css"
|
||||
|
||||
#: The four pinned tool-line label literals (D6) — the emoji guard and
|
||||
#: the phase-37/70 pins key off these exact bytes.
|
||||
PINNED_LABELS = (
|
||||
'line.textContent = "📄 Reading "',
|
||||
'line.textContent = "🔎 Searching for "',
|
||||
'line.textContent = "🔎 Listing documents in "',
|
||||
'line.textContent = "🔎 Listing documents"',
|
||||
)
|
||||
|
||||
|
||||
def _js() -> str:
|
||||
return APP_JS.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _shared_js() -> str:
|
||||
return SHARED_JS.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _css() -> str:
|
||||
return STYLES_CSS.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _fn(src: str, name: str) -> str:
|
||||
"""The flat function body (house extraction: first ``\n}\n`` after
|
||||
the definition — the function must stay flat, no nested
|
||||
declarations)."""
|
||||
fn = src.find(f"function {name}")
|
||||
assert fn != -1, f"{name} must exist"
|
||||
return src[fn : src.find("\n}\n", fn)]
|
||||
|
||||
|
||||
def test_append_tool_line_builds_an_open_details_disclosure() -> None:
|
||||
"""appendToolLine (D2/D3/D5): the FIRST frame creates a native
|
||||
``details.tool-calls-disclosure`` — a ``summary.tool-calls-summary``
|
||||
(first child) + the existing ``.tool-calls`` list (role=list +
|
||||
aria-label "Tool calls", second child) — inserted before the bubble
|
||||
where the list used to be. It starts OPEN while the turn is live;
|
||||
every append re-renders the plain-text "Tool call(s) (N)" count on
|
||||
the summary through textContent (no innerHTML anywhere)."""
|
||||
body = _fn(_js(), "appendToolLine")
|
||||
assert "document.createElement(\"details\")" in body
|
||||
assert "className = \"tool-calls-disclosure\"" in body
|
||||
assert "container.open = true", (
|
||||
"D3: created OPEN on the first live tool frame — the calls and "
|
||||
"the phase-87 .tool-elapsed suffix stay visible during the turn"
|
||||
)
|
||||
assert "document.createElement(\"summary\")" in body
|
||||
assert "className = \"tool-calls-summary\"" in body
|
||||
# The list keeps its exact phase-37 shape, nested in the disclosure.
|
||||
assert "className = \"tool-calls\"" in body
|
||||
assert 'setAttribute("role", "list")' in body
|
||||
assert 'setAttribute("aria-label", "Tool calls")' in body
|
||||
assert 'insertBefore(container, body.querySelector(".bubble"))' in body, (
|
||||
"the disclosure sits ABOVE the answer, below an existing Thinking block"
|
||||
)
|
||||
# D5: the count rides the summary — the template fragment itself
|
||||
# (singular for one call, plural otherwise), textContent-built.
|
||||
assert "Tool call${n === 1 ? \"\" : \"s\"} (${n})" in body, (
|
||||
"the plain-text count template (D5) on every append"
|
||||
)
|
||||
assert "innerHTML" not in body, (
|
||||
"no HTML injection surface on tool lines — textContent only"
|
||||
)
|
||||
|
||||
|
||||
def test_append_tool_line_keeps_the_four_pinned_labels() -> None:
|
||||
"""D6: the four ``line.textContent`` label literals and the
|
||||
``<code>`` textContent arguments stay byte-identical — the emoji
|
||||
guard, the phase-37/70 unit pins, and the E2E text assertions all
|
||||
key off these exact bytes."""
|
||||
body = _fn(_js(), "appendToolLine")
|
||||
for label in PINNED_LABELS:
|
||||
assert label in body, f"pinned label literal changed: {label!r}"
|
||||
assert "code.textContent = argument" in body, (
|
||||
"the path/pattern/scope is data — textContent, never innerHTML"
|
||||
)
|
||||
|
||||
|
||||
def test_close_tool_calls_defined_and_called_at_every_rest_site() -> None:
|
||||
"""closeToolCalls (D3): defined next to closeThinkingBlock,
|
||||
idempotent (optional-chaining query → open=false), and called at
|
||||
ALL rest sites — the delta handler (the answer began), the done
|
||||
handler (the turn ended), the stop/abort settle (the turn was
|
||||
stopped), and the restore path — so the definition + ≥4 calls."""
|
||||
js = _js()
|
||||
body = _fn(js, "closeToolCalls")
|
||||
assert 'querySelector?.(".tool-calls-disclosure")' in body, (
|
||||
"targets the disclosure of the given wrap only"
|
||||
)
|
||||
assert "disc.open = false" in body, "fold: open=false (idempotent)"
|
||||
assert js.count("closeToolCalls(wrap)") >= 4, (
|
||||
"the definition + the delta/done/stop handler sites + the "
|
||||
"renderStoredMessage restore fold — the disclosure settles "
|
||||
"exactly where the Thinking block settles"
|
||||
)
|
||||
|
||||
|
||||
def test_restore_path_folds_the_disclosure() -> None:
|
||||
"""renderStoredMessage (D3): a restored brain record with `tools`
|
||||
re-renders through the SAME appendToolLine (byte-identical), and
|
||||
the disclosure is FOLDED right after the loop — a completed turn
|
||||
from storage is one compact "Tool calls (N)" line, not N stacked
|
||||
cards (the space fix; mirrors the thinking restore rendering
|
||||
collapsed)."""
|
||||
js = _js()
|
||||
fn = js.find("function renderStoredMessage")
|
||||
assert fn != -1
|
||||
body = js[fn : js.find("function restoreConversation", fn)]
|
||||
assert "appendToolLine(wrap, t.name, arg)" in body
|
||||
assert "closeToolCalls(wrap)" in body
|
||||
assert (
|
||||
body.find("appendToolLine(wrap, t.name, arg)")
|
||||
< body.find("closeToolCalls(wrap)")
|
||||
), "the fold sits AFTER the tool-line restore loop"
|
||||
|
||||
|
||||
def test_thinking_anchor_accounts_for_the_disclosure() -> None:
|
||||
"""ensureThinkingBlock: the anchor is the WHOLE disclosure (a
|
||||
`thinking` frame after the first `tool` frame lands the scratchpad
|
||||
ABOVE the disclosure, not inside it); the bare .tool-calls and
|
||||
.bubble terms stay as fallbacks, and the block still opens while
|
||||
active (phase-109 toggle behavior untouched)."""
|
||||
body = _fn(_js(), "ensureThinkingBlock")
|
||||
assert ".tool-calls-disclosure" in body, "the new anchor term"
|
||||
assert 'querySelector(".tool-calls")' in body, "the pre-117 fallback"
|
||||
assert 'querySelector(".bubble")' in body
|
||||
assert "block.open = true" in body
|
||||
|
||||
|
||||
def test_shared_page_add_tool_lines_builds_a_closed_disclosure() -> None:
|
||||
"""shared.js addToolLines (D3 parity): the SAME
|
||||
``details.tool-calls-disclosure`` + summary + ``.tool-calls`` list —
|
||||
but created CLOSED (a pure render has no live turn to open it),
|
||||
with the count set once at the end. The four label literals, the
|
||||
three textContent-only argument lines, and the phase-95 marker stay
|
||||
byte-identical (no innerHTML anywhere)."""
|
||||
body = _fn(_shared_js(), "addToolLines")
|
||||
assert "document.createElement(\"details\")" in body
|
||||
assert "className = \"tool-calls-disclosure\"" in body
|
||||
assert "document.createElement(\"summary\")" in body
|
||||
assert "open = false" in body, (
|
||||
"D3: the shared page renders the disclosure CLOSED — never an "
|
||||
"auto-expanded record"
|
||||
)
|
||||
assert "className = \"tool-calls\"" in body
|
||||
for label in PINNED_LABELS:
|
||||
assert label in body, f"pinned label literal changed: {label!r}"
|
||||
assert "Tool call${n === 1 ? \"\" : \"s\"} (${n})" in body, (
|
||||
"the same plain-text count template (D5), set once at the end"
|
||||
)
|
||||
assert body.count("code.textContent = argument") == 3, (
|
||||
"all three argument-bearing lines (read / grep / ls) stay "
|
||||
"textContent-only"
|
||||
)
|
||||
assert "innerHTML" not in body, (
|
||||
"no HTML injection surface on shared tool lines — textContent only"
|
||||
)
|
||||
|
||||
|
||||
def test_tool_call_rule_is_deboxed_inline_flow() -> None:
|
||||
"""D4: the ``.tool-call`` rule is DEBOXED — no ``display: flex``,
|
||||
no card (no background / border / radius / padding), and no
|
||||
``var(--accent-line)`` left border. The accent now rides the TEXT
|
||||
(``var(--accent-ink)``, ≈10.4:1 on the surface), and the line keeps
|
||||
the compact status metrics + ``overflow-wrap: anywhere`` so the
|
||||
label + inline ``code`` flow as ONE continuous run — a long path
|
||||
wraps to the left edge and the label never breaks mid-word (the
|
||||
"Rea/ding" glitch is gone)."""
|
||||
m = re.search(r"\.tool-call \{([^}]*)\}", _css())
|
||||
assert m, "the .tool-call rule must exist"
|
||||
row = m.group(1)
|
||||
assert "display: flex" not in row, (
|
||||
"deboxed: no flex — the label + code are one inline run"
|
||||
)
|
||||
assert "var(--accent-line)" not in row, "deboxed: no accent left border"
|
||||
assert "border" not in row, "deboxed: no card borders at all"
|
||||
assert "background" not in row, "deboxed: no card background"
|
||||
assert "padding" not in row, "deboxed: no card padding"
|
||||
assert "var(--accent-ink)" in row, (
|
||||
"the accent now rides the text color (≈10.4:1 on the surface)"
|
||||
)
|
||||
assert "font-size: 0.8rem" in row, "the compact status metrics stay"
|
||||
assert "overflow-wrap: anywhere" in row, "the path wraps inside the run"
|
||||
|
||||
|
||||
def test_tool_call_code_rule_is_dechipped() -> None:
|
||||
"""D4: the ``.tool-call code`` rule KEEPS ``var(--mono)`` +
|
||||
``var(--ink)`` (≈11.5:1 on the surface — AA) but LOSES the chip
|
||||
(no background, no padding, no radius) — the path is inline text
|
||||
in the line's run now."""
|
||||
m = re.search(r"\.tool-call code \{([^}]*)\}", _css())
|
||||
assert m, "the .tool-call code rule must exist"
|
||||
code = m.group(1)
|
||||
assert "var(--mono)" in code
|
||||
assert "var(--ink)" in code
|
||||
assert "background" not in code, "dechipped: no chip background"
|
||||
assert "padding" not in code, "dechipped: no chip padding"
|
||||
assert "border-radius" not in code, "dechipped: no chip radius"
|
||||
assert "overflow-wrap: anywhere" in code
|
||||
|
||||
|
||||
def test_tool_calls_disclosure_and_summary_rules_exist() -> None:
|
||||
"""Phase 117 (D2/D4): the NEW ``.tool-calls-disclosure`` and
|
||||
``.tool-calls-summary`` rules exist — the summary is a native
|
||||
focusable toggle carrying the accent TEXT (``var(--accent-ink)``,
|
||||
≈10.4:1 on the surface; text + color, never color alone, B5),
|
||||
modeled on the details.thinking family, with no new color literal
|
||||
(the phase-92 zero-literal invariant)."""
|
||||
css = _css()
|
||||
assert re.search(r"\.tool-calls-disclosure \{", css), (
|
||||
"the .tool-calls-disclosure rule must exist"
|
||||
)
|
||||
m = re.search(r"\.tool-calls-summary \{([^}]*)\}", css)
|
||||
assert m, "the .tool-calls-summary rule must exist"
|
||||
summary = m.group(1)
|
||||
assert "var(--accent-ink)" in summary, (
|
||||
"the accent rides the summary text (≈10.4:1 on the surface)"
|
||||
)
|
||||
assert "cursor: pointer" in summary, "a toggle affordance"
|
||||
Reference in New Issue
Block a user