phase: 117_tool_call_compact
Build and Push Containers / build-and-push-app (push) Successful in 2m2s
Build and Push Containers / build-and-push-db (push) Successful in 12s

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:
2026-09-15 18:26:38 -04:00
parent 2ac3fc89c2
commit c851d1a1c5
27 changed files with 1682 additions and 34 deletions
@@ -0,0 +1,90 @@
# Phase 117 — Compact, well-wrapped tool-call lines
**Source:** owner visual-glitch report (live chat, 2026-09-15) — "how much space the tool calls take up, and the tool call text is spit and wrapped poorly."
**Story:** n/a (owner bug report — mobile viewport, `https://brain.reeseapps.com`, reproduced 2026-09-15)
**Context:** `frontend/assets/app.js` (`appendToolLine` — renders one `.tool-call` line per `tool` SSE frame into a `.tool-calls` list above the answer bubble; `ensureThinkingBlock` — places the Thinking `<details>` above the `.tool-calls` list via `anchor = body.querySelector(".tool-calls") ?? body.querySelector(".bubble")`; the delta/done/stop handlers each call `closeThinkingBlock(wrap)`; `renderStoredMessage` re-renders persisted tools through the SAME `appendToolLine`); `frontend/assets/shared.js` (`addToolLines` — the shared page's local copy of the same renderer, pinned byte-parity with `appendToolLine`); `frontend/assets/styles.css` (`.tool-calls` = flex column + gap; `.tool-call` = `display:flex; align-items:baseline` + a full card: `background` + `border` + `border-left:3px var(--accent-line)` + `border-radius` + `padding`; `.tool-call code` = a chip: `var(--brand-soft)` background + padding + radius on top of `var(--mono)`/`var(--ink)`); `tests/unit/test_frontend_tool_states.py` (pins the exact tool-line template literals + the `.tool-calls`/`.tool-call` DOM shape + the `.tool-call` CSS — `test_tool_call_style_is_accent_and_contrast_safe` asserts `display: flex` + `var(--accent-line)` on `.tool-call`); `tests/unit/test_big_read_progress.py` (pins phase 87's `.tool-elapsed` clock, which queries `.tool-calls .tool-call:last-child` and appends a SIBLING suffix — the line's own text/literals stay byte-identical); `tests/e2e/test_agent_document_tools.py` + `tests/e2e/test_big_read_progress.py` (the tool-line E2E — the former asserts `.tool-call` count + `to_contain_text` after a completed turn, the latter asserts the FIRST `.tool-call` line and the `.tool-elapsed` suffix are `to_be_visible` DURING the live frameless gap, before the answer's delta).
## Bug basis (code-traced + reproduced live, 2026-09-15)
Reproduced at 390×844 (mobile) on the live site. One answer to "Generate a change log … last 5 phases" rendered **6+ stacked full-width cards** — one per `tool` frame — each a complete bordered card (accent left border + surface background + radius) holding a mono path *chip* inside it. Two distinct defects:
- **Space:** each `tool` frame appends a full-width bordered `.tool-call` card spanning the whole chat column. The agent loop is round-capped and fires several calls per turn (`ls → read → read …`), so a single answer stacks N cards above it. On a phone the tool process visually swamps the answer.
- **Wrapping:** `.tool-call` is `display:flex; align-items:baseline`, so the label text node (`"📄 Reading "`) and the `<code>` path are **two separate flex items**. The long mono path squeezes the label flex-item, and `overflow-wrap: anywhere` breaks the label **mid-word** (`Reading` → `Rea` / `ding`); the path wraps to 3 lines indented to the right of that narrow broken label. The "spit and wrapped poorly" look.
- **The live-feedback that must be preserved:** the "calling tool" live state lives in `#send-status` (aria-live) + the typing-indicator aria-label (pinned by `test_calling_tool_label_strings`), NOT the tool lines — so folding the tool lines does not remove any live feedback. Phase 87's ticking `.tool-elapsed` suffix, however, IS on a `.tool-call` line and its E2E asserts it is **visible during the live gap** — so the lines must stay visible while the turn is in flight and fold only once the answer begins.
## Objective
Turn the per-call tool "cards" into a single collapsible disclosure — one compact "Tool calls (N)" line by default, expandable to the individual calls — and restyle the individual lines as deboxed, inline-flowing text so the path wraps to the left edge like a normal sentence and the label never breaks mid-word. Collapsed by default for completed/restored turns (the space fix), open during a live turn (keeps phase 87's live suffix visible), with the "calling tool" live state unchanged.
## Owner decisions (chat, 2026-09-15 — recorded per AGENTS.md rule 3)
- **D1 — Frontend-only.** No `app/` change, no new SSE frame, no persistence-format change. The stored record stays `{name, argument}` (+ `truncated`); only the *rendering* changes. The server and the SSE event set are byte-identical.
- **D2 — Reuse the Thinking-block convention.** The tool calls become a native `<details>/<summary>` disclosure in the same `.msg-body` wrap, mirroring `details.thinking` (open-while-active, closed-when-done). No new component, no new JS dependency.
- **D3 — Open live, folded at rest.** The disclosure is created **open** on the first live `tool` frame (the calls + phase 87's `.tool-elapsed` suffix stay visible during the turn); it folds when the answer's first `delta` arrives, on `done`, and on stop/abort (`closeToolCalls`, idempotent — the exact sites that call `closeThinkingBlock`). The restore path (`renderStoredMessage`) and the shared page (`addToolLines`) render it **closed**. This is what makes the user's screenshot (a completed turn) collapse to one line while keeping the live behavior green.
- **D4 — Debox + inline flow.** The `.tool-call` line loses its card (no `display:flex` / background / border / left border / radius / padding) so the label + inline `<code>` flow as one continuous run (fixes the mid-word label break + indented wrap); the `<code>` loses its chip background but keeps `var(--mono)` + `var(--ink)`. The accent is carried by the line's `color` (`var(--accent-ink)`), not a border.
- **D5 — Plain-text summary, no emoji.** The summary reads `Tool call (1)` / `Tool calls (N)` — plain text, matching the emoji-free chrome (the Thinking summary is just "Thinking") and the phase-08 emoji-free-chrome lean. The existing `📄`/`🔎` line glyphs stay (they are pinned literals + the emoji-guard strip set).
- **D6 — Keep the pinned literals + DOM shape.** The four `line.textContent = "…"` label literals, the `.tool-calls` list (role=list, aria-label "Tool calls"), the `.tool-call` listitems, and the `<code>` `textContent` arguments stay **byte-identical** — so `test_frontend_tool_states.py` (all but the one CSS assert), `test_big_read_progress.py`, the emoji guard, and phase 87's `.tool-calls .tool-call:last-child` query all stay green unchanged.
## Design (shared by all tasks — the executor reads this, not the chat)
### `frontend/assets/app.js`
- **`appendToolLine(wrap, name, argument)`** — wrap the list in a disclosure; keep everything else byte-identical:
- `let container = body.querySelector(".tool-calls-disclosure");` (idempotent per wrap — was `.tool-calls`).
- On first frame: `container = document.createElement("details"); container.className = "tool-calls-disclosure"; container.open = true;` then a `<summary class="tool-calls-summary">` (createElement, textContent only) appended first, then the existing list — `const listEl = document.createElement("div"); listEl.className = "tool-calls"; listEl.setAttribute("role","list"); listEl.setAttribute("aria-label","Tool calls");` — appended second; then `body.insertBefore(container, body.querySelector(".bubble"))` (the disclosure goes where the list did: above the answer, below an existing Thinking block).
- `const list = container.querySelector(".tool-calls");` — build the line EXACTLY as today (`line.className="tool-call"`, `role="listitem"`, the four pinned `line.textContent` label literals, the `<code>` with `code.textContent = argument`), `list.appendChild(line)`.
- Update the summary count on every append (live + restore): `const n = list.children.length; container.querySelector("summary").textContent = \`Tool call${n === 1 ? "" : "s"} (${n})\`;`.
- **No `innerHTML` anywhere in the function** (house rule; pinned). The function stays flat (no nested function declaration) so the `js[fn : js.find("\n}\n", fn)]` body-extraction pins keep working.
- **`closeToolCalls(wrap)`** (new, next to `closeThinkingBlock`): `const disc = wrap?.querySelector?.(".tool-calls-disclosure"); if (disc) disc.open = false;` — idempotent, no-op without a disclosure. Called at **every** existing `closeThinkingBlock(wrap)` site: the `delta` handler (≈L2514), the `done` handler (≈L2519), and the stop/abort settle (≈L2646).
- **`renderStoredMessage`** — after the existing `if (Array.isArray(m.tools)) { …appendToolLine(wrap, t.name, arg)… }` loop, add `closeToolCalls(wrap);` so a restored turn renders folded (the space fix; mirrors the thinking restore rendering collapsed).
- **`ensureThinkingBlock`** — the anchor becomes `body.querySelector(".tool-calls-disclosure") ?? body.querySelector(".tool-calls") ?? body.querySelector(".bubble")` so the Thinking block lands above the WHOLE disclosure (not inside it). `block.open = true` unchanged.
### `frontend/assets/shared.js`
- **`addToolLines(wrap, tools)`** — parity with `appendToolLine`, but **created closed** (pure render, always folded): build the `details.tool-calls-disclosure` (`container.open = false`) + `summary.tool-calls-summary` + the existing `.tool-calls` list (role=list, aria-label) exactly as today; render each line byte-identical (the same four label literals + `<code>` + the phase-95 `truncated-note`); set the summary count once at the end (`Tool call (N)` / `Tool calls (N)`). Keep the `code.textContent = argument` count at 3 and no `innerHTML` (the `test_shared_page_tool_lines_…` / `test_frontend_tool_states.py` shared pins).
### `frontend/assets/styles.css`
- **`.tool-call`** — DEBOX: remove `display:flex`, `align-items:baseline`, `gap`, `background`, `border`, `border-left`, `border-radius`, `padding`. KEEP `color: var(--accent-ink)`, `font-size: 0.8rem`, `line-height: 1.4`, `overflow-wrap: anywhere`. As a flex item of the `.tool-calls` column it stays block-level per line, but its label + inline `<code>` now flow as one continuous run → the path wraps to the left edge and the label no longer breaks mid-word.
- **`.tool-call code`** — DECHIP: remove `background: var(--brand-soft)`, `padding`, `border-radius`. KEEP `font-family: var(--mono)`, `font-size: 0.95em`, `color: var(--ink)` (≈11.5:1, AA).
- **NEW `.tool-calls-disclosure` + `.tool-calls-summary`** — model the disclosure on the existing `details.thinking` styling (house AA palette, no new hue): the summary is a native focusable toggle that carries the house 3px `:focus-visible` ring (the `details.thinking summary` already has it — reuse that language), small status font, `color: var(--accent-ink)` (≈10.4:1 on the surface — same pairing the deboxed line uses). No new color literal (phase-92 zero-literal invariant; B5 text+color, never color alone — the count is text).
- **`.tool-calls`** (the list) — UNCHANGED: stays `display:flex; flex-direction:column; gap: 0.25rem` (spacing between the now-deboxed lines).
- **`.tool-elapsed` / `.truncated-note`** — UNCHANGED (phase 87 / phase 95).
### `tests/unit/test_tool_call_compact.py` (NEW — source-level house pattern)
- **app.js:** `appendToolLine` body contains `document.createElement("details")` + `className = "tool-calls-disclosure"` + `document.createElement("summary")`; `container.open = true` (the live default, D3); the count literal `Tool call${n === 1 ? "" : "s"} (${n})` (or the equivalent template) is built with `textContent` (no `innerHTML`); `closeToolCalls` is defined and called at **3** handler sites (delta/done/stop) + once in `renderStoredMessage` (pin: `closeToolCalls(wrap)` appears ≥4×); `ensureThinkingBlock`'s anchor includes `.tool-calls-disclosure`; the four pinned `line.textContent = "…"` label literals are STILL present (mirror the guard); `innerHTML` is NOT in the `appendToolLine` body.
- **shared.js:** `addToolLines` body contains `document.createElement("details")` + `className = "tool-calls-disclosure"` + `document.createElement("summary")` + `open = false` (closed on the shared page, D3); the four label literals are still present; `body.count("code.textContent = argument") == 3`; no `innerHTML`.
- **styles.css:** the `.tool-call` rule has **NO** `display: flex` and **NO** `var(--accent-line)` (deboxed, D4) but still has `var(--accent-ink)`; the `.tool-call code` rule still has `var(--mono)` + `var(--ink)` and has **NO** `background`; `.tool-calls-disclosure` and `.tool-calls-summary` rules exist.
### `tests/unit/test_frontend_tool_states.py` (UPDATE — the only existing test that changes)
- **`test_tool_call_style_is_accent_and_contrast_safe`** — the `.tool-call` rule is deboxed: REMOVE the `assert "display: flex" in row` and `assert "var(--accent-line)" in row` lines; KEEP `assert "var(--accent-ink)" in row` (the accent is now the line's color) and the `.tool-call code` `var(--mono)` + `var(--ink)` asserts. Update the docstring to describe the deboxed, inline-flow line (phase 117). Every OTHER test in this file stays green unchanged (the DOM-shape, literal, branch-order, persist, restore, shared-parity, and no-CDN pins are all preserved by D6).
### `tests/e2e/test_tool_call_compact.py` (NEW story suite — mock "use your tools" flow, 3 tool calls)
The conftest `app_server`/`page` fixtures + the phase-37 admin login + the marker question that drives the mock's `ls → ls(scoped) → read` flow (mirror `test_agent_document_tools.py`). Four tests:
1. **Folds at rest:** after the turn completes, the `.tool-calls-summary` is visible with text `Tool calls (3)`; the disclosure is **not** open (`.tool-calls-disclosure` has no `[open]`); the `.tool-call` lines are present in the DOM (count 3) but hidden; the answer bubble is present.
2. **Expands on tap:** clicking the summary opens the disclosure; the three `.tool-call` lines become visible with the correct text (nth 0 "Listing documents", nth 1 "Listing documents in", nth 2 "Reading ").
3. **Deboxed inline flow:** on a visible (expanded) `.tool-call` line, `getComputedStyle(line).display !== "flex"` (the label + path are one inline run, not two flex items) and the line `to_contain_text("Reading ")` (label immediately followed by the path in the same run).
4. **Restored folded:** RELOAD (same context — the phase-14/50 persisted conversation restores); the restored brain message's `.tool-calls-disclosure` is present, closed, summary `Tool calls (3)`, `.tool-call` count 3 in the DOM — no auto-expand on load.
## Dependencies
- `95_read_truncation_cap`, `87_big_read_progress`, `70_harness_aligned_tools`, `37` (the tool-line rendering this restyles — complete). NO code dependency beyond the shared frontend files; the only pipeline predecessor is execution order (todo/ is empty — this is the next phase).
## Tasks
1. `01_disclosure_wrapper.md` — the `<details>/<summary>` wrapper + count + open-live/close-at-rest + restore-closed + shared.js parity (app.js + shared.js + the new unit module's JS pins).
2. `02_debox_and_inline_flow.md` — the CSS debox/dechip + disclosure/summary rules, and the one `test_frontend_tool_states.py` CSS-pin update (styles.css + unit CSS pins + the existing CSS test).
3. `03_e2e_story_suite.md` — `tests/e2e/test_tool_call_compact.py` (fold / expand / deboxed-flow / restored-folded).
4. `04_verify_and_commit.md` — full gate (unit + integration, coverage >90%, the new E2E story in isolation, `test_agent_document_tools.py` + `test_big_read_progress.py` + `test_thinking_display.py` + smoke in isolation, ruff + pyright) + atomic commit + move to complete/.
## Testing & Quality
- Unit — `tests/unit/test_tool_call_compact.py` (new): the JS/CSS pins listed above (disclosure + summary + count + open-live, close-at-4-sites, restore-closed, shared parity, deboxed CSS, disclosure/summary rules, the pinned literals still present, no innerHTML).
- Existing unit suites MUST stay green: `tests/unit/test_big_read_progress.py` (phase-87 clock + the byte-identical tool-line literals), `tests/unit/test_shared_page.py` (shared-page tool-line parity), and `tests/unit/test_frontend_tool_states.py` (all tests except the ONE deboxed CSS assert updated in task 02).
- E2E — `tests/e2e/test_tool_call_compact.py` (new; isolation gate per AGENTS.md rule 9): the four tests above, mock LLM (no slow proxy needed — the fold is about a completed turn).
- Regression E2E (run in isolation by task 04): `test_agent_document_tools.py` (tool-line count + text after a completed turn — the lines stay in the DOM inside the folded disclosure), `test_big_read_progress.py` (the FIRST `.tool-call` line + the `.tool-elapsed` suffix are visible DURING the live gap — the disclosure is still open then), `test_thinking_display.py` (the Thinking block ordering vs. the disclosure), `test_smoke.py`.
- Coverage: **>90%** on `app/` — no `app/` code changes (the floor is held by the untouched suite).
## Completion Criteria
- [ ] A completed tool turn renders as ONE collapsed "Tool calls (N)" line (E2E pin 1); tapping it reveals the individual calls (E2E pin 2); the restored/shared view is folded on load (E2E pin 4). The 6+ stacked cards from the screenshot are gone.
- [ ] An expanded `.tool-call` line is deboxed inline-flow text (E2E pin 3 + the CSS unit pins): the path wraps to the left edge and the label never breaks mid-word — the "spit and wrapped poorly" glitch is fixed at 390×844 and at desktop.
- [ ] Live behavior unchanged: during a live frameless gap the calls + phase-87 `.tool-elapsed` suffix stay visible (the disclosure is open until the first delta) — `test_big_read_progress.py` green in isolation; the "calling tool" `#send-status`/aria state is byte-identical (`test_calling_tool_label_strings` green).
- [ ] `uv run pytest` green (including `test_big_read_progress.py`, `test_shared_page.py`, and the updated `test_frontend_tool_states.py`); `uv run pytest --cov=app --cov-report=term-missing` >90%; the new E2E story + `test_agent_document_tools.py` + `test_big_read_progress.py` + `test_thinking_display.py` + `test_smoke.py` green in isolation; `uv run ruff check . && uv run pyright` clean.
- [ ] `git diff --stat` limited to `frontend/assets/app.js`, `frontend/assets/shared.js`, `frontend/assets/styles.css`, `tests/unit/test_tool_call_compact.py`, `tests/unit/test_frontend_tool_states.py`, `tests/e2e/test_tool_call_compact.py`, and the phase files — nothing in `app/`.
- [ ] One atomic `--no-gpg-sign` commit (e.g. `feat(ui): fold tool calls into a compact collapsible disclosure`); phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **Frontend-only (D1)** — `app/`, the SSE event set, and the persistence format are byte-identical; only the rendering changes.
- **Reuse the native `<details>` Thinking-block convention (D2/D3)** — open while the turn is live, folded at rest and on restore; no new component or dependency. This is what keeps phase 87's live-suffix E2E and the tool-line count/text E2E green.
- **Pinned literals + DOM shape stay (D6)** — the four `line.textContent` labels, the `.tool-calls` list, the `.tool-call` listitems, and the `<code>` `textContent` arguments are byte-identical; the emoji guard and phase 87's `.tool-calls .tool-call:last-child` query are unaffected. Only the ONE CSS assert in `test_frontend_tool_states.py` changes (the debox).
- **Debox, don't restyle the accent away (D4)** — the line keeps `var(--accent-ink)` as its text color (≈10.4:1 AA on the surface); the accent moves off the border onto the text, so the line still reads as distinct from the brand-ink Thinking summary.
@@ -0,0 +1,63 @@
# Task 01 — The `<details>/<summary>` disclosure wrapper + count + open-live/close-at-rest
**Phase:** `117_tool_call_compact` · **Source:** owner visual-glitch report (2026-09-15) — tool-call cards take too much space and wrap poorly.
**Story:** n/a (owner bug report)
## Objective
Wrap the existing `.tool-calls` list in a native `details.tool-calls-disclosure` with a `summary.tool-calls-summary` that carries a `Tool call (N)` / `Tool calls (N)` count. The disclosure is created **open** on the first live `tool` frame, **folds** when the answer starts / the turn ends / is stopped (`closeToolCalls`), and renders **closed** on the restore path and the shared page. The `.tool-calls` list, the `.tool-call` lines, the four label literals, and the `<code>` arguments stay byte-identical (D6) — only the wrapper is added.
## Work
1. `frontend/assets/app.js` — `appendToolLine` (house comment style, citing phase 117):
- `let container = body.querySelector(".tool-calls-disclosure");` (was `.tool-calls`).
- First-frame branch (the `if (!container)` block): create the disclosure + summary + the existing list, in this order:
```js
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);
body.insertBefore(container, body.querySelector(".bubble"));
```
- After the `if`: `const list = container.querySelector(".tool-calls");` then build the line EXACTLY as today (`line.className="tool-call"`, `role="listitem"`, the four pinned `line.textContent` label literals, the `<code>` with `code.textContent = argument`) and `list.appendChild(line)`.
- Summary count on every append (live + restore): `const n = list.children.length; container.querySelector("summary").textContent = \`Tool call${n === 1 ? "" : "s"} (${n})\`;`.
- **No `innerHTML`** anywhere in the function; keep it flat (no nested function declaration) so the `js[fn : js.find("\n}\n", fn)]` body-extraction pins keep working.
2. `frontend/assets/app.js` — `closeToolCalls(wrap)` (new, defined next to `closeThinkingBlock`):
```js
function closeToolCalls(wrap) {
const disc = wrap?.querySelector?.(".tool-calls-disclosure");
if (disc) disc.open = false; // idempotent; no-op without a disclosure
}
```
Add `closeToolCalls(wrap);` on the line next to each existing `closeThinkingBlock(wrap);` — the `delta` handler (≈L2514), the `done` handler (≈L2519), and the stop/abort settle (≈L2646). Comment each: the answer began / the turn ended / the turn was stopped — fold the record (mirrors the Thinking block settling closed).
3. `frontend/assets/app.js` — `renderStoredMessage`: immediately after the existing `if (Array.isArray(m.tools)) { …appendToolLine(wrap, t.name, arg)… }` loop, add `closeToolCalls(wrap);` (a restored turn renders folded — the space fix; mirrors the thinking restore rendering collapsed).
4. `frontend/assets/app.js` — `ensureThinkingBlock`: change the anchor to
`const anchor = body.querySelector(".tool-calls-disclosure") ?? body.querySelector(".tool-calls") ?? body.querySelector(".bubble");`
so the Thinking block lands above the whole disclosure. `block.open = true` and everything else unchanged.
5. `frontend/assets/shared.js` — `addToolLines` (parity, but **created closed** — pure render, always folded):
- Build the same `details.tool-calls-disclosure` with `container.open = false`, a `summary.tool-calls-summary` (first child), and the existing `.tool-calls` list (role=list, aria-label "Tool calls") — appended to the same `.msg-body` anchor as today.
- Render each line byte-identical (the same four label literals + `<code>` + the phase-95 `truncated-note`), appending to the list.
- Set the summary count once at the end: `const n = list.children.length; summary.textContent = \`Tool call${n === 1 ? "" : "s"} (${n})\`;`.
- Keep `body.count("code.textContent = argument") == 3` and no `innerHTML` (the shared-parity pins).
6. `tests/unit/test_tool_call_compact.py` (NEW module) — the source-level JS pins (house pattern, `_js()` / `_shared_js()` readers like `test_frontend_tool_states.py`):
- **app.js `appendToolLine`:** body contains `document.createElement("details")`, `className = "tool-calls-disclosure"`, `document.createElement("summary")`; `container.open = true`; the count template `Tool call${n === 1 ? "" : "s"} (${n})` (assert the template fragment, not the rendered value); `innerHTML` NOT in the body; the four pinned `line.textContent = "…"` label literals are still present.
- **app.js `closeToolCalls`:** defined (`function closeToolCalls`); `closeToolCalls(wrap)` appears **≥4×** (the 3 handler sites + `renderStoredMessage`).
- **app.js `ensureThinkingBlock`:** the body contains `.tool-calls-disclosure` (the new anchor term) AND still contains `querySelector(".tool-calls")` + `querySelector(".bubble")` + `block.open = true`.
- **shared.js `addToolLines`:** body contains `document.createElement("details")`, `className = "tool-calls-disclosure"`, `document.createElement("summary")`, `open = false` (closed on the shared page); the four label literals present; `body.count("code.textContent = argument") == 3`; no `innerHTML`.
## Testing & Quality
- Unit: `uv run pytest tests/unit/test_tool_call_compact.py -v` green (all JS pins). `uv run pytest tests/unit/test_frontend_tool_states.py tests/unit/test_big_read_progress.py tests/unit/test_shared_page.py -v` green **unchanged** (the CSS pin is task 02; the JS/DOM pins are preserved by D6).
- Quick live sanity (session log): dev server, trigger a tool turn — during the turn the disclosure is open (calls + any `.tool-elapsed` suffix visible); when the answer starts it folds to "Tool calls (N)"; a reload shows it folded. (The deterministic E2E is task 03 — this is a wiring smoke only.)
- Coverage: **>90%** on `app/` unaffected (no `app/` change).
## Completion Criteria
- [ ] `git diff frontend/assets/app.js` shows: the disclosure/summary/list creation in `appendToolLine`, the summary count line, the new `closeToolCalls`, the three handler call sites + the `renderStoredMessage` call, and the `ensureThinkingBlock` anchor — and NOTHING else; the four `line.textContent` label literals and the `<code>` `textContent` bytes byte-identical.
- [ ] `git diff frontend/assets/shared.js` shows the parallel closed-disclosure build + count + the unchanged line/literal bytes.
- [ ] `uv run pytest tests/unit/test_tool_call_compact.py -v` green; `tests/unit/test_frontend_tool_states.py` + `tests/unit/test_big_read_progress.py` + `tests/unit/test_shared_page.py` green unchanged (CSS test still asserts the OLD flex/border — that flips in task 02, so run it here to confirm only that one assert is the pending delta).
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] No behavior change in completed work (the fold/expand proof is task 03; the live-suffix regression is task 04).
@@ -0,0 +1,53 @@
# Task 02 — Debox + inline-flow CSS, and the one CSS-pin update
**Phase:** `117_tool_call_compact` · **Source:** owner visual-glitch report (2026-09-15) — the tool-call "cards" are too heavy and the label breaks mid-word.
**Story:** n/a (owner bug report)
## Objective
Debox the `.tool-call` line (no card, no flex) so the label + inline `<code>` flow as one continuous run (fixes the `Rea`/`ding` mid-word break and the indented 3-line wrap), dechip the `.tool-call code` (no background chip, keep mono + ink), and add the `.tool-calls-disclosure` / `.tool-calls-summary` rules modeled on the Thinking disclosure. Update the ONE existing CSS assert in `test_frontend_tool_states.py` to the deboxed contract, and add the CSS pins to the new unit module.
## Work
1. `frontend/assets/styles.css` — **`.tool-call`** (the debox, D4). Remove `display: flex;`, `align-items: baseline;`, `gap: 0.45rem;`, `background: var(--surface);`, `border: 1px solid var(--line);`, `border-left: 3px solid var(--accent-line);`, `border-radius: var(--radius-sm);`, `padding: 0.3rem 0.75rem;`. KEEP `color: var(--accent-ink);`, `font-size: 0.8rem;`, `line-height: 1.4;`, `overflow-wrap: anywhere;`. Update the rule's comment: phase 117 deboxed the line — the accent now rides the text color (≈10.4:1 on the surface) instead of a border; the label + inline `code` flow as one run so the path wraps to the left edge (no more mid-word label break).
2. `frontend/assets/styles.css` — **`.tool-call code`** (the dechip). Remove `background: var(--brand-soft);`, `padding: 0.05em 0.35em;`, `border-radius: 5px;`. KEEP `font-family: var(--mono);`, `font-size: 0.95em;`, `color: var(--ink);`, `overflow-wrap: anywhere;`. Comment: the path keeps the mono + ink treatment (≈11.5:1 AA) but loses the chip background — it is inline text in the line's run now.
3. `frontend/assets/styles.css` — **NEW `.tool-calls-disclosure` + `.tool-calls-summary`** rules, placed next to the `details.thinking` rules and modeled on them (house AA palette, no new hue — the phase-92 zero-literal invariant):
```css
/* Phase 117: the tool-call record folds into a single disclosure line,
reusing the Thinking block's native <details> convention. Open while
the turn is live (phase 87's elapsed suffix stays visible), folded at
rest / on restore. The summary is a native focusable toggle — it
inherits the house 3px :focus-visible ring (see details.thinking
summary). Accent rides the TEXT (var(--accent-ink), ≈10.4:1 on the
surface) — text + color, never color alone (B5). No new literal. */
.tool-calls-disclosure { margin: 0.25rem 0; }
.tool-calls-summary {
cursor: pointer;
color: var(--accent-ink);
font-size: 0.8rem;
line-height: 1.4;
padding: 0.15rem 0;
list-style: none; /* the native marker is redundant with the count text */
}
.tool-calls-summary::-webkit-details-marker { display: none; }
```
(If the `details.thinking summary` uses a chevron/marker rather than the native one, match that treatment so the two disclosures read as one family — the count text is the accessible label either way.)
4. `frontend/assets/styles.css` — **`.tool-calls`** (the list) — UNCHANGED: keep `display: flex; flex-direction: column; gap: 0.25rem;` (spacing between the now-deboxed lines). **`.tool-elapsed`** and **`.tool-call .truncated-note`** — UNCHANGED (phase 87 / phase 95).
5. `tests/unit/test_tool_call_compact.py` (extend the module from task 01) — the CSS pins:
- the `.tool-call` rule (regex `\.tool-call \{([^}]*)\}`) has **NO** `display: flex` and **NO** `var(--accent-line)` (deboxed), but still has `var(--accent-ink)`;
- the `.tool-call code` rule still has `var(--mono)` + `var(--ink)` and has **NO** `background`;
- `.tool-calls-disclosure` and `.tool-calls-summary` rules exist (the summary carries `var(--accent-ink)`).
6. `tests/unit/test_frontend_tool_states.py` — **`test_tool_call_style_is_accent_and_contrast_safe`** (the ONE existing test that changes, D4/D6):
- REMOVE the two now-false asserts: `assert "display: flex" in row` and `assert "var(--accent-line)" in row`.
- KEEP `assert "var(--accent-ink)" in row` (the accent is now the line's color) and the `.tool-call code` `var(--mono)` + `var(--ink)` asserts and the `.tool-calls` `gap` assert.
- Update the docstring: the line is now a deboxed inline-flow row (phase 117) — the accent rides the text color; the `code` is inline mono text (no chip). Everything else in this file is untouched.
## Testing & Quality
- Unit: `uv run pytest tests/unit/test_tool_call_compact.py -v` green (JS pins from task 01 + the new CSS pins); `uv run pytest tests/unit/test_frontend_tool_states.py -v` green with the updated CSS test; `tests/unit/test_big_read_progress.py` + `tests/unit/test_shared_page.py` green unchanged.
- Quick live sanity (session log): dev server, expand a completed tool turn at 390×844 — the lines are plain text rows (no card border/background), the path wraps to the left edge, and "Reading" never breaks as "Rea/ding". (The deterministic E2E is task 03.)
- Coverage: **>90%** on `app/` unaffected.
## Completion Criteria
- [ ] `git diff frontend/assets/styles.css` shows: the deboxed `.tool-call`, the dechipped `.tool-call code`, the two new disclosure/summary rules — and NOTHING else (`.tool-calls` gap, `.tool-elapsed`, `.truncated-note` unchanged).
- [ ] `git diff tests/unit/test_frontend_tool_states.py` shows ONLY the two removed asserts + the docstring in `test_tool_call_style_is_accent_and_contrast_safe`.
- [ ] `uv run pytest tests/unit/test_tool_call_compact.py tests/unit/test_frontend_tool_states.py tests/unit/test_big_read_progress.py tests/unit/test_shared_page.py -v` all green.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] No behavior change in completed work (E2E proof in task 03/04).
@@ -0,0 +1,41 @@
# Task 03 — The E2E story suite (fold / expand / deboxed-flow / restored-folded)
**Phase:** `117_tool_call_compact` · **Source:** owner visual-glitch report (2026-09-15).
**Story:** n/a (owner bug report)
## Objective
Prove the new behavior end-to-end against the deterministic mock: a completed 3-call tool turn folds to one "Tool calls (3)" line, expands on tap to the deboxed inline-flow lines, and renders folded on reload. Reuses the phase-37 admin login + the mock "use your tools" flow (exactly the 3 tool calls `test_agent_document_tools.py` drives: `ls` → `ls(scoped)` → `read`).
## Work
1. `tests/e2e/test_tool_call_compact.py` (NEW module) — header docstring cites phase 117 + the owner report; the conftest `app_url` + `page` fixtures; `from e2e.auth_helpers import login`. Constants:
- `MARKER_QUESTION = "Use your tools: what is the exact JSON shape of reeselink.json for my aws route53 hosted zone?"` (the SAME marker that drives the mock's 3-tool flow — mirror `test_agent_document_tools.py` so the flow is deterministic).
- `SUMMARY = "#messages .msg.brain .tool-calls-summary"` (the disclosure toggle).
- `DISCLOSURE = "#messages .msg.brain .tool-calls-disclosure"`.
- `LINES = "#messages .msg.brain .tool-call"`.
- A `_submit_tools_turn(page, app_url)` helper: `login(page, app_url, next="/")`; `page.fill("#message-input", MARKER_QUESTION)`; `page.click("#send-btn")`; wait for the answer bubble (`MOCK_ANSWER_MARKER = "Deterministic mock answer for E2E"`) — i.e. the turn is COMPLETE (the disclosure has already folded on the first delta).
2. **`test_tool_calls_fold_to_one_line_after_turn`** (pin 1): after `_submit_tools_turn`:
- `expect(page.locator(SUMMARY)).to_be_visible()` and `to_contain_text("Tool calls (3)")`.
- the disclosure is NOT open: `page.get_attribute(DISCLOSURE, "open")` is `None` (a closed `<details>` has no `open` attribute).
- the lines are present in the DOM but folded: `expect(page.locator(LINES)).to_have_count(3)` (DOM count) and the FIRST line is NOT visible (`expect(page.locator(LINES).first).not_to_be_visible()`).
- the answer is present: `expect(page.locator(".msg.brain .bubble").last).to_contain_text(MOCK_ANSWER_MARKER)`.
3. **`test_summary_click_expands_the_calls`** (pin 2): after `_submit_tools_turn`:
- `page.locator(SUMMARY).click()` (native `<summary>` — a real focusable toggle, AA).
- `page.get_attribute(DISCLOSURE, "open")` is now `"true"` (or the attribute present).
- `expect(page.locator(LINES)).to_have_count(3)` and every line is now visible; assert the text (mirror `test_agent_document_tools.py`): nth 0 `to_contain_text("Listing documents")`, nth 1 `to_contain_text("Listing documents in")`, nth 2 `to_contain_text("Reading ")`.
4. **`test_expanded_line_is_deboxed_inline_flow`** (pin 3): after expanding (reuse the click from pin 2 in a fresh turn):
- take the visible `.tool-call` "Reading" line; assert `page.evaluate` on its computed style: `getComputedStyle(el).display !== "flex"` (the label + path are one inline run, not two flex items — the debox).
- `expect(line).to_contain_text("Reading ")` (the label is immediately followed by the path in the same run — no separate indented code column).
5. **`test_restored_turn_renders_folded`** (pin 4): after `_submit_tools_turn`:
- `page.reload()` (same context — the phase-14/50 persisted conversation restores).
- wait for the restored brain message's answer; then `expect(page.locator(SUMMARY)).to_be_visible()` + `to_contain_text("Tool calls (3)")`; the disclosure is NOT open (`get_attribute(...) is None`); `expect(page.locator(LINES)).to_have_count(3)` in the DOM; the FIRST line is NOT visible (folded on load — no auto-expand).
## Testing & Quality
- E2E: `uv run pytest tests/e2e/test_tool_call_compact.py -v --no-cov` green **in isolation** (DB up: `podman compose up -d db`). Mock LLM by default (no slow proxy — the fold is about a completed turn, not a live gap).
- No `app/` change — the conftest app-server + mock flow are used as-is.
- Coverage: **>90%** on `app/` unaffected.
## Completion Criteria
- [ ] `uv run pytest tests/e2e/test_tool_call_compact.py -v --no-cov` green in isolation (all four pins).
- [ ] The suite proves: folded-after-turn (pin 1), expand-on-tap (pin 2), deboxed inline-flow (pin 3), restored-folded (pin 4).
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] No behavior change in completed work (the adjacent-suite regression is task 04).
@@ -0,0 +1,33 @@
# Task 04 — Full gate + atomic commit
**Phase:** `117_tool_call_compact` · **Source:** owner visual-glitch report (2026-09-15).
**Story:** n/a (owner bug report)
## Objective
Run the complete phase gate, land the phase as one atomic commit, and move the phase directory to `complete/`.
## Work
1. **Full regression gate** (AGENTS.md rule 9):
- `uv run pytest` — unit + integration green (including `tests/unit/test_tool_call_compact.py`, the updated `tests/unit/test_frontend_tool_states.py`, and the untouched `tests/unit/test_big_read_progress.py` + `tests/unit/test_shared_page.py`).
- `uv run pytest --cov=app --cov-report=term-missing` — `app/` coverage **>90%** (no `app/` change this phase — confirm the floor is held).
- `uv run pytest tests/e2e/test_tool_call_compact.py -v --no-cov` — green **in isolation** (this phase's E2E story — fold / expand / deboxed-flow / restored-folded).
- `uv run pytest tests/e2e/test_agent_document_tools.py -v --no-cov` — green in isolation (the tool-line count + text after a completed turn — the lines stay in the DOM inside the folded disclosure).
- `uv run pytest tests/e2e/test_big_read_progress.py -v --no-cov` — green in isolation (the FIRST `.tool-call` line + the `.tool-elapsed` suffix are visible DURING the live gap — the disclosure is still open then; this is the key live-behavior regression proof for D3).
- `uv run pytest tests/e2e/test_thinking_display.py -v --no-cov` — green in isolation (the Thinking block ordering vs. the new disclosure anchor).
- `uv run pytest tests/e2e/test_smoke.py -v --no-cov` — green in isolation.
- `uv run ruff check . && uv run pyright` — clean.
2. **Manual live check** (keep the output in the session log): the live/dev app at a **mobile viewport (390×844)** — trigger a multi-call tool turn (e.g. "Generate a change log … last 5 phases"). Confirm: the 6+ stacked cards from the original report are now ONE collapsed "Tool calls (N)" line; tapping it reveals deboxed text rows whose paths wrap to the left edge with the label never breaking mid-word (`Reading` stays intact); during a live frameless gap the calls + any "(Ns)" suffix are visible (the disclosure is open); on reload the turn is folded.
3. **Commit** (AGENTS.md rule 8 — one atomic, Conventional-Commits commit, always `--no-gpg-sign`), staging `frontend/assets/app.js`, `frontend/assets/shared.js`, `frontend/assets/styles.css`, `tests/unit/test_tool_call_compact.py`, `tests/unit/test_frontend_tool_states.py`, `tests/e2e/test_tool_call_compact.py`, and the phase files:
`feat(ui): fold tool calls into a compact collapsible disclosure`
— body: owner visual-glitch report (2026-09-15) — each tool call rendered as a full-width bordered card, so a multi-call turn stacked N cards over the answer, and the flex label+code layout broke the label mid-word ("Reading" → "Rea/ding") with the path indented to 3 lines. Two changes, frontend-only: (1) the tool-call record now folds into a single native `<details>` "Tool calls (N)" line reusing the Thinking-block convention — open while the turn is live (phase 87's elapsed suffix stays visible), folded when the answer starts / on done / on stop / on restore, closed by default on the shared page; (2) the individual lines are deboxed (no card, no flex) and dechipped (no code background) so the label + path flow as one inline run that wraps to the left edge. The four pinned line literals, the `.tool-calls` list, the `<code>` textContent arguments, the emoji-guard set, the persistence format, and the server are byte-identical; only the ONE deboxed CSS assert in `test_frontend_tool_states.py` changes. New source-level unit pins + a dedicated fold/expand/debox/restore E2E story.
4. Move the phase directory: `mv .agents/phases/todo/117_tool_call_compact .agents/phases/complete/` and include the move in the same commit.
## Testing & Quality
- This task IS the phase-level gate — the commands above are the completion evidence.
- Coverage: >90% held.
## Completion Criteria
- [ ] All seven gate commands green (unit + integration, coverage >90%, the new E2E story + `test_agent_document_tools.py` + `test_big_read_progress.py` + `test_thinking_display.py` + smoke in isolation, ruff + pyright).
- [ ] The live mobile check shows the stacked cards replaced by one collapsed "Tool calls (N)" line that expands to deboxed inline-flow rows (path wraps to the left edge, label intact), live calls still visible during a gap, and restored turns folded (output kept in the session log).
- [ ] `git show --stat HEAD` lists the staged files above + the phase files (todo → complete move) — nothing else (in particular `app/`, `pyproject.toml` / `uv.lock`, `conftest.py`, `mock_llm.py`, `slow_llm.py` untouched).
- [ ] `.agents/phases/complete/117_tool_call_compact/` exists; `todo/` no longer contains it.
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,103 @@
........................................................................ [ 2%]
........................................................................ [ 5%]
........................................................................ [ 8%]
........................................................................ [ 11%]
........................................................................ [ 14%]
........................................................................ [ 17%]
........................................................................ [ 20%]
........................................................................ [ 23%]
........................................................................ [ 26%]
........................................................................ [ 29%]
........................................................................ [ 32%]
........................................................................ [ 35%]
........................................................................ [ 37%]
........................................................................ [ 40%]
........................................................................ [ 43%]
........................................................................ [ 46%]
........................................................................ [ 49%]
........................................................................ [ 52%]
........................................................................ [ 55%]
........................................................................ [ 58%]
........................................................................ [ 61%]
........................................................................ [ 64%]
........................................................................ [ 67%]
........................................................................ [ 70%]
........................................................................ [ 72%]
........................................................................ [ 75%]
........................................................................ [ 78%]
........................................................................ [ 81%]
........................................................................ [ 84%]
........................................................................ [ 87%]
........................................................................ [ 90%]
........................................................................ [ 93%]
........................................................................ [ 96%]
........................................................................ [ 99%]
.................. [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain-of-reese/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.7-final-0 ________________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 224 1 99%
app/api/chats.py 110 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 99 0 100%
app/api/docs.py 156 1 99%
app/api/git_sources.py 232 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 33 0 100%
app/api/sync.py 139 0 100%
app/api/tokens.py 40 0 100%
app/api/ui_settings.py 55 0 100%
app/config.py 210 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 124 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 20 0 100%
app/core/theming.py 38 0 100%
app/core/tokens.py 44 0 100%
app/db.py 22 0 100%
app/main.py 66 0 100%
app/models.py 128 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 317 1 99%
app/rag/archive_upload.py 134 0 100%
app/rag/chunker.py 206 4 98%
app/rag/doc_dates.py 18 0 100%
app/rag/folder_summaries.py 123 0 100%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 215 3 99%
app/rag/llm.py 244 1 99%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 192 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 329 0 100%
--------------------------------------------------
TOTAL 4147 16 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
validation OK
@@ -0,0 +1,22 @@
All completion criteria verified. Task 01 is complete.
---
**Phase 117 · Task 01 — disclosure wrapper: DONE**
- `app.js` `appendToolLine`: `.tool-calls` list now wrapped in `details.tool-calls-disclosure` + `summary.tool-calls-summary`; created **open** on first live frame; count `Tool call(s) (N)` via textContent on every append; no innerHTML; function stays flat
- New `closeToolCalls(wrap)` next to `closeThinkingBlock`; called at 4 sites: delta (answer began), done (turn ended), stop/abort settle, `renderStoredMessage` (restore renders folded)
- `ensureThinkingBlock` anchor → `.tool-calls-disclosure` ?? `.tool-calls` ?? `.bubble`
- `shared.js` `addToolLines`: parallel disclosure, created **closed** (pure render), count set once at end
- New `tests/unit/test_tool_call_compact.py`: 6 source-level JS pins (disclosure/summary/count, close-at-4-sites, restore-fold, anchor, shared parity, pinned literals, no innerHTML)
**Results:**
- `uv run pytest tests/unit/test_tool_call_compact.py -v` → 6 passed; `test_frontend_tool_states.py` + `test_big_read_progress.py` + `test_shared_page.py` (+`test_read_truncation_frontend.py`) → 43 passed unchanged
- `uv run pytest` → 2463 passed (started the down `brain-of-reese_db_1` container — initial 6 failures were environment-only, confirmed pre-existing via stash)
- `uv run pytest --cov=app` → **99%** (>90%); `uv run ruff check .` clean; `uv run pyright` → 0 errors
- E2E smoke (temp, deleted): at rest one folded "Tool calls (3)" disclosure, tap expands, reload restores folded — passed
- Regression E2E in isolation: `test_agent_document_tools.py` 4 ✓, `test_big_read_progress.py` 4 ✓ (live suffix still visible — disclosure open mid-turn), `test_thinking_display.py` 5 ✓, `test_smoke.py` 3 ✓
**Decisions:** D6 held — all four label literals + `<code>` textContent bytes byte-identical (diff-verified); nothing in `app/`. Note: `TODO.md` carries a new uncommitted owner note added mid-session — not mine, left untouched.
**Next pending task:** `02_debox_and_inline_flow.md` (CSS debox/dechip + disclosure/summary rules + the one CSS-pin update).
@@ -0,0 +1,103 @@
........................................................................ [ 2%]
........................................................................ [ 5%]
........................................................................ [ 8%]
........................................................................ [ 11%]
........................................................................ [ 14%]
........................................................................ [ 17%]
........................................................................ [ 20%]
........................................................................ [ 23%]
........................................................................ [ 26%]
........................................................................ [ 29%]
........................................................................ [ 32%]
........................................................................ [ 35%]
........................................................................ [ 38%]
........................................................................ [ 40%]
........................................................................ [ 43%]
........................................................................ [ 46%]
........................................................................ [ 49%]
........................................................................ [ 52%]
........................................................................ [ 55%]
........................................................................ [ 58%]
........................................................................ [ 61%]
........................................................................ [ 64%]
........................................................................ [ 67%]
........................................................................ [ 70%]
........................................................................ [ 73%]
........................................................................ [ 76%]
........................................................................ [ 78%]
........................................................................ [ 81%]
........................................................................ [ 84%]
........................................................................ [ 87%]
........................................................................ [ 90%]
........................................................................ [ 93%]
........................................................................ [ 96%]
........................................................................ [ 99%]
............... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain-of-reese/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.7-final-0 ________________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 224 1 99%
app/api/chats.py 110 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 99 0 100%
app/api/docs.py 156 1 99%
app/api/git_sources.py 232 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 33 0 100%
app/api/sync.py 139 0 100%
app/api/tokens.py 40 0 100%
app/api/ui_settings.py 55 0 100%
app/config.py 210 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 124 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 20 0 100%
app/core/theming.py 38 0 100%
app/core/tokens.py 44 0 100%
app/db.py 22 0 100%
app/main.py 66 0 100%
app/models.py 128 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 317 1 99%
app/rag/archive_upload.py 134 0 100%
app/rag/chunker.py 206 4 98%
app/rag/doc_dates.py 18 0 100%
app/rag/folder_summaries.py 123 0 100%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 215 3 99%
app/rag/llm.py 244 1 99%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 192 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 329 0 100%
--------------------------------------------------
TOTAL 4147 16 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
validation OK
@@ -0,0 +1,19 @@
All green. Final verification of the targeted suites was already done (46 passed). Summary:
## Task 02 complete — Debox + inline-flow CSS, one CSS-pin update
**Implemented**
- `styles.css`: `.tool-call` deboxed (removed flex/align/gap/background/borders/radius/padding; kept accent-ink, 0.8rem, 1.4, overflow-wrap) with the phase-117 comment
- `styles.css`: `.tool-call code` dechipped (kept `var(--mono)` + `var(--ink)` + overflow-wrap; removed chip bg/padding/radius)
- `styles.css`: new `.tool-calls-disclosure` / `.tool-calls-summary` rules next to `details.thinking`; since the Thinking summary uses a CSS chevron, the summary carries the matching `▸` chevron (rotates on `[open]`, stills in the reduced-motion block) per the task's family-match parenthetical — no new literals, focus ring via the global rule
- `tests/unit/test_tool_call_compact.py`: 3 new CSS pins (deboxed row, dechipped code, disclosure/summary rules) + docstring/reader updates
- `tests/unit/test_frontend_tool_states.py`: exactly the two removed asserts + docstring in `test_tool_call_style_is_accent_and_contrast_safe`
**Test / lint / coverage**
- `uv run pytest tests/unit/test_tool_call_compact.py tests/unit/test_frontend_tool_states.py tests/unit/test_big_read_progress.py tests/unit/test_shared_page.py -v` → 46 passed
- `uv run pytest` (full) → 2466 passed; `uv run pytest --cov=app` → 99% (>90%)
- `uv run ruff check . && uv run pyright` → clean, 0 errors
**Decisions** — chevron + reduced-motion still added to the new summary (task step 3's parenthetical requires matching the Thinking chevron treatment); `.tool-calls` gap / `.tool-elapsed` / `.truncated-note` untouched; no `app/` changes.
**Next pending task:** `.agents/phases/todo/117_tool_call_compact/03_e2e_story_suite.md`
@@ -0,0 +1,103 @@
........................................................................ [ 2%]
........................................................................ [ 5%]
........................................................................ [ 8%]
........................................................................ [ 11%]
........................................................................ [ 14%]
........................................................................ [ 17%]
........................................................................ [ 20%]
........................................................................ [ 23%]
........................................................................ [ 26%]
........................................................................ [ 29%]
........................................................................ [ 32%]
........................................................................ [ 35%]
........................................................................ [ 37%]
........................................................................ [ 40%]
........................................................................ [ 43%]
........................................................................ [ 46%]
........................................................................ [ 49%]
........................................................................ [ 52%]
........................................................................ [ 55%]
........................................................................ [ 58%]
........................................................................ [ 61%]
........................................................................ [ 64%]
........................................................................ [ 67%]
........................................................................ [ 70%]
........................................................................ [ 72%]
........................................................................ [ 75%]
........................................................................ [ 78%]
........................................................................ [ 81%]
........................................................................ [ 84%]
........................................................................ [ 87%]
........................................................................ [ 90%]
........................................................................ [ 93%]
........................................................................ [ 96%]
........................................................................ [ 99%]
.................. [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain-of-reese/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.7-final-0 ________________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 224 1 99%
app/api/chats.py 110 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 99 0 100%
app/api/docs.py 156 1 99%
app/api/git_sources.py 232 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 33 0 100%
app/api/sync.py 139 0 100%
app/api/tokens.py 40 0 100%
app/api/ui_settings.py 55 0 100%
app/config.py 210 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 124 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 20 0 100%
app/core/theming.py 38 0 100%
app/core/tokens.py 44 0 100%
app/db.py 22 0 100%
app/main.py 66 0 100%
app/models.py 128 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 317 1 99%
app/rag/archive_upload.py 134 0 100%
app/rag/chunker.py 206 4 98%
app/rag/doc_dates.py 18 0 100%
app/rag/folder_summaries.py 123 0 100%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 215 3 99%
app/rag/llm.py 244 1 99%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 192 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 329 0 100%
--------------------------------------------------
TOTAL 4147 16 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
validation OK
@@ -0,0 +1,14 @@
All gates green and deterministic. Final verification of the completion criteria is done:
**Report — task 03 (`117_tool_call_compact` / E2E story suite)**
- Implemented `tests/e2e/test_tool_call_compact.py` (new, mock-only): conftest `app_url`/`page` fixtures, phase-37 admin `login(next="/")`, `_submit_tools_turn` helper (fill → click → wait for answer bubble + settle), and all four pins:
- pin 1: completed 3-call turn → visible "Tool calls (3)" summary, disclosure closed (`open` attr `None`), 3 `.tool-call` lines in DOM with first not visible, answer present
- pin 2: summary click opens the disclosure; 3 lines visible with pinned text (Listing documents / Listing documents in / Reading)
- pin 3: expanded "Reading" line's computed `display != "flex"` (deboxed inline run, via in-page `getComputedStyle`)
- pin 4: same-context reload restores the turn folded (no auto-expand), answer intact
- KB seed byte-identical mirror of `test_agent_document_tools.py` (2-doc pair + phase-94 registry ordering) so the mock runs the deterministic `ls → ls(scoped) → read` flow.
- Deviation: the task's `MOCK_ANSWER_MARKER = "Deterministic mock answer for E2E"` is the *plain grounded* answer tail — the tool flow's answer is `Read <source/path>. <first 80 chars>` and never contains it (a literal wait would time out). Kept the constant name/role, set it to `Read Deployments/example-record-file.json.`; rationale documented in the module docstring.
- Results: `uv run pytest tests/e2e/test_tool_call_compact.py -v --no-cov` → 4 passed (×2 runs, ~8 s each, in isolation, DB up); `uv run pytest` → 2466 passed; `uv run pytest --cov=app --cov-report=term-missing` → 99% on `app/` (>90%); `uv run ruff check . && uv run pyright` → clean (0 errors).
- Footprint: only the new test file; no `app/` change. Adjacent-suite regression E2E (`test_agent_document_tools`, `test_big_read_progress`, `test_thinking_display`, smoke) is task 04's gate, per the task.
- Next pending task: `.agents/phases/todo/117_tool_call_compact/04_verify_and_commit.md`.
@@ -0,0 +1,103 @@
........................................................................ [ 2%]
........................................................................ [ 5%]
........................................................................ [ 8%]
........................................................................ [ 11%]
........................................................................ [ 14%]
........................................................................ [ 17%]
........................................................................ [ 20%]
........................................................................ [ 23%]
........................................................................ [ 26%]
........................................................................ [ 29%]
........................................................................ [ 32%]
........................................................................ [ 35%]
........................................................................ [ 37%]
........................................................................ [ 40%]
........................................................................ [ 43%]
........................................................................ [ 46%]
........................................................................ [ 49%]
........................................................................ [ 52%]
........................................................................ [ 55%]
........................................................................ [ 58%]
........................................................................ [ 61%]
........................................................................ [ 64%]
........................................................................ [ 67%]
........................................................................ [ 70%]
........................................................................ [ 72%]
........................................................................ [ 75%]
........................................................................ [ 78%]
........................................................................ [ 81%]
........................................................................ [ 84%]
........................................................................ [ 87%]
........................................................................ [ 90%]
........................................................................ [ 93%]
........................................................................ [ 96%]
........................................................................ [ 99%]
.................. [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain-of-reese/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.7-final-0 ________________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 224 1 99%
app/api/chats.py 110 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 99 0 100%
app/api/docs.py 156 1 99%
app/api/git_sources.py 232 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 33 0 100%
app/api/sync.py 139 0 100%
app/api/tokens.py 40 0 100%
app/api/ui_settings.py 55 0 100%
app/config.py 210 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 124 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 20 0 100%
app/core/theming.py 38 0 100%
app/core/tokens.py 44 0 100%
app/db.py 22 0 100%
app/main.py 66 0 100%
app/models.py 128 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 317 1 99%
app/rag/archive_upload.py 134 0 100%
app/rag/chunker.py 206 4 98%
app/rag/doc_dates.py 18 0 100%
app/rag/folder_summaries.py 123 0 100%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 215 3 99%
app/rag/llm.py 244 1 99%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 192 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 329 0 100%
--------------------------------------------------
TOTAL 4147 16 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
validation OK
@@ -0,0 +1,16 @@
Task 04 complete — the full phase gate is green. Report:
**Phase 117 (tasks 01–03 already landed; task 04 = full gate):**
- Ran the complete regression gate; every command green:
- `uv run pytest` — **2466 passed** (unit + integration)
- `uv run pytest --cov=app --cov-report=term-missing` — **99%** on `app/` (>90% floor held; no `app/` change)
- `uv run pytest tests/e2e/test_tool_call_compact.py -v --no-cov` — **4 passed** (fold / expand / deboxed-flow / restored-folded)
- `uv run pytest tests/e2e/test_agent_document_tools.py -v --no-cov` — **4 passed**
- `uv run pytest tests/e2e/test_big_read_progress.py -v --no-cov` — **4 passed** (live-gap: disclosure still open, `.tool-elapsed` visible)
- `uv run pytest tests/e2e/test_thinking_display.py -v --no-cov` — **5 passed**
- `uv run pytest tests/e2e/test_smoke.py -v --no-cov` — **3 passed**
- `uv run ruff check . && uv run pyright` — clean (pyright: 0 errors, 0 warnings)
- Manual live check at 390×844 (dev app + mock LLM, screenshots verified): in-flight turn → disclosure **open** (D3); completed turn → one collapsed **"Tool calls (3)"** line; expand → deboxed inline-flow rows, **"Reading" on a single line box** (no "Rea/ding"), path wraps to the **left edge** (wrapped line x=58.0 ≤ label x=77.3); reload → **folded** again. The 6+ stacked cards are gone.
- Working tree scope verified: only `frontend/assets/{app.js,shared.js,styles.css}`, the two test files, and phase/report files — nothing in `app/`, `pyproject.toml`/`uv.lock`, `conftest.py`, or the mock LLM files.
- **Notable:** per harness rules I did NOT commit or move the phase dir — all changes left in the working tree for the harness's atomic commit. `TODO.md` shows a pre-existing owner edit (new retrieval-context idea) already in the tree before this task; I left it untouched (it will ride along in the harness commit). Pre-existing quirk: the earlier brain-wave commit also labels itself "Phase 117" in styles.css — cosmetic, not part of this diff.
- Next pending task: **none** — task 04 is the last task of phase 117; `todo/` holds nothing else.
@@ -0,0 +1,103 @@
........................................................................ [ 2%]
........................................................................ [ 5%]
........................................................................ [ 8%]
........................................................................ [ 11%]
........................................................................ [ 14%]
........................................................................ [ 17%]
........................................................................ [ 20%]
........................................................................ [ 23%]
........................................................................ [ 26%]
........................................................................ [ 29%]
........................................................................ [ 32%]
........................................................................ [ 35%]
........................................................................ [ 37%]
........................................................................ [ 40%]
........................................................................ [ 43%]
........................................................................ [ 46%]
........................................................................ [ 49%]
........................................................................ [ 52%]
........................................................................ [ 55%]
........................................................................ [ 58%]
........................................................................ [ 61%]
........................................................................ [ 64%]
........................................................................ [ 67%]
........................................................................ [ 70%]
........................................................................ [ 72%]
........................................................................ [ 75%]
........................................................................ [ 78%]
........................................................................ [ 81%]
........................................................................ [ 84%]
........................................................................ [ 87%]
........................................................................ [ 90%]
........................................................................ [ 93%]
........................................................................ [ 96%]
........................................................................ [ 99%]
.................. [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain-of-reese/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.7-final-0 ________________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 224 1 99%
app/api/chats.py 110 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 99 0 100%
app/api/docs.py 156 1 99%
app/api/git_sources.py 232 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 33 0 100%
app/api/sync.py 139 0 100%
app/api/tokens.py 40 0 100%
app/api/ui_settings.py 55 0 100%
app/config.py 210 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 124 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 20 0 100%
app/core/theming.py 38 0 100%
app/core/tokens.py 44 0 100%
app/db.py 22 0 100%
app/main.py 66 0 100%
app/models.py 128 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 317 1 99%
app/rag/archive_upload.py 134 0 100%
app/rag/chunker.py 206 4 98%
app/rag/doc_dates.py 18 0 100%
app/rag/folder_summaries.py 123 0 100%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 215 3 99%
app/rag/llm.py 244 1 99%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 192 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 329 0 100%
--------------------------------------------------
TOTAL 4147 16 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
validation OK