Files
brain-of-reese/.agents/phases/complete/158_thinking_timeline/02_live_timeline.md
T
ducoterra 31bd903a93
Build and Push Containers / build-and-push-app (push) Successful in 8m38s
Build and Push Containers / build-and-push-db (push) Successful in 32s
phase: 158_thinking_timeline
All verification complete. Every gate is green and the docs check out. Final report:

## Phase 158 final verification pass — COMPLETE

**Verified (all tasks pre-completed; this pass re-ran the full gate):**
- Unit + integration: `uv run pytest --cov=app --cov-report=term-missing` → **3618 passed, 99% coverage on `app/`** (>90% gate)
- Phase E2E in isolation: `uv run pytest tests/e2e/test_thinking_timeline.py -v --no-cov` → **6/6 passed** (order pin, fold-at-rest, live-open, restore+`segments` array, shared page, no-thinking single disclosure)
- Regression E2E, each in isolation: `test_thinking_display` 5✓, `test_tool_call_compact` 4✓, `test_turn_progress_loader` 3✓ (per-run pin updated), `test_big_read_progress` 4✓, `test_agent_document_tools` 4✓, `test_share_chat` 4✓, `test_chat_persistence` 4✓, `test_smoke` 3✓
- Lint/types: `uv run ruff check . && uv run pyright` → **clean (0 errors)**

**Completion criteria:** all 9 met — timeline E2E pins ✓, restore/shared `segments` ✓, no-thinking single-disclosure + the three UNCHANGED regression suites green ✓, pre-phase byte-identical round-trip (integration pin: key absent, never null; >50 / >32 000 / empty text / unknown kind all 422) ✓, full suite + coverage + E2E + lint ✓, `git diff --stat` footprint limited to `app/schemas.py`, `frontend/assets/{app,shared}.js`, `tests/`, README, phase files (a pre-existing owner `TODO.md` edit is out of scope, not staged by the phase commit) ✓, D15→per-run supersession recorded in the updated pin ✓. (Commit + phase move are the harness's job; task dirs already in `complete/`.)

**Documentation:** README.md — Thinking section correctly describes the phase-158 timeline; config reference matches `.env.example` exactly; page table matches registered routes; no stale single-block wording. AGENTS.md — rules and quick-reference commands all accurate. **Verified consistent — no changes needed.**

**Decisions/deviations:** none. `test_tool_call_compact.py` carries a one-line docstring-only update (referenced the removed `closeToolCalls`); its test logic is byte-identical to the phase-117 contract and green.

**Next pending phase:** none — `todo/` contains only 158.
2026-10-05 02:52:55 -04:00

12 KiB
Raw Blame History

Task 02 — the per-run live timeline in app.js

Phase: 158_thinking_timeline · Story: n/a

Objective

Replace the single-block/single-disclosure turn rendering with the per-run timeline: each contiguous thinking run streams into its own details.thinking block, each contiguous tool-call run into its own details.tool-calls-disclosure, all segments ABOVE the answer bubble in arrival order (D2); the runs open live and fold at rest (D5); the phase-87 clock is re-targeted to a tracked line reference; the save payloads persist the segments array (D4).

Work

  1. frontend/assets/app.js — turn state (the runTurn locals / module scope, ~L3065–L3080):
    • segRuns = [] — MODULE scope (beside thinkingAcc, which the pagehide partial reads), reset to [] in runTurn with the other turn locals. Entries: { kind: "thinking", text: "", records: [] } / { kind: "tool", text: "", records: [] } (the tool entry's records holds the toolAcc record objects — the SAME objects toolAcc holds, so the phase-95 tool_result stamp (t.truncated = true; t.chars_shown = …; t.chars_total = …, ~L3398–L3404) flows into both automatically — NO new wiring).
    • currentRun = null — turn-local (the active run's element + kind).
  2. frontend/assets/app.js — the run builders (replace ensureThinkingBlock
    • the single-container logic in appendToolLine):
    • ensureThinkingRun(wrap) — returns the CURRENT thinking run's details.thinking if currentRun?.kind === "thinking"; else closes currentRun (if any — open = false), creates a NEW details.thinking (open = true, <summary>Thinking</summary> — the phase-17 literal, D3 — built via the existing innerHTML template that ensureThinkingBlock uses, OR createElement/textContent — either is fine, but the RESTORE path's builder stays createElement/textContent (the house rule pins innerHTML ABSENT from the restore/shared builders — see task 03)), inserts it before body.querySelector(".bubble") (the bubble get-or-created first), pushes a fresh {kind:"thinking",…} to segRuns, sets currentRun.
    • ensureToolRun(wrap) — the same, for details.tool-calls-disclosure (open = true, summary.tool-calls-summary + the div.tool-calls list role=list aria-label="Tool calls" — the phase-117 structure, now per run).
    • appendToolLine — the signature becomes appendToolLine(container, name, argument, offset = null, limit = null, scope = null) (the CALLER passes the run's disclosure — no more body.querySelector(".tool-calls-disclosure") single-container derivation). EVERYTHING else in the function is BYTE-IDENTICAL: the .tool-calls list get-or-create, the line bytes, the four pinned line.textContent label literals, the phase-139 range suffix, the phase-146 scope span, the Tool call(s) (N) count.
    • closeCurrentRun() — folds currentRun (if any, open = false); idempotent; no-op without a run.
    • closeAllRuns(wrap) — closes EVERY open details.thinking + details.tool-calls-disclosure in the wrap (defensive; the done + stop/abort settle sites).
  3. frontend/assets/app.js — the turn-handler branches:
    • the thinking branch (~L3148): keep settleToolLine(), thinkingAcc += ev.text || "", sawThinking, clearTurnTimeout(), if (!wrap) wrap = addMessage("brain", ""), removeTyping() — all unchanged. REPLACE the ensureThinkingBlock + block.open = true (phase-109 D15 re-open — SUPERSEDED by D1) with const block = ensureThinkingRun(wrap); + the run's text extension: the run's text accumulates ev.text; the pre-update pin-state / bottom-follow logic (the isThinkingNearBottom capture + scrollTop = scrollHeight, ~L3166–L3189) moves VERBATIM into this branch, now operating on THIS block (per-block — the existing comment block stays, updated to name the per-run placement).
    • the tool branch (~L3193): keep the phase-139/146 guards, toolAcc.push(toolRecord), clearTurnTimeout(), the wrap get-or-create, and the toolStatus label logic UNCHANGED. REPLACE appendToolLine(wrap, …) + armToolLineClock(wrap) with: const disc = ensureToolRun(wrap); const line = appendToolLine(disc, name, argument, frameOffset, frameLimit, framePath); (the function returns the appended line element — a one-line change, the function already has it in scope) + armToolLineClock(wrap, line). The run record is the SAME toolRecord object pushed to toolAcc (push it into the run's records too — or derive the run's records from segRuns at save time via the call order — the executor picks ONE, but the phase-95 stamp must reach the segments payload: the simplest is the run's records.push(toolRecord) alongside the toolAcc.push(toolRecord)).
    • the delta branch (~L3300): settleToolLine() + acc += … + the thinking→streaming transition UNCHANGED; replace closeThinkingBlock(wrap) + closeToolCalls(wrap) with closeCurrentRun().
    • the THREE remaining settle-pair sites — finalizeFailedTurn's if (wrap) block (L2990–L2991), the STOP path (L3477–L3478), and any other closeThinkingBlock(wrap); closeToolCalls(wrap); pair (grep for the pair — the redesign removes both functions): replace the pair with closeAllRuns(wrap);.
    • removeTyping() / the typing-indicator / the #send-status copy / the UI state machine / the 300 s guard / the stream-drop guard / the thinking-without-answer fallback: ALL UNCHANGED (§7.4 intact).
  4. frontend/assets/app.js — the clock (the phase-87 re-target, ~L1479):
    • armToolLineClock(wrap, lineEl) — the NEW second param: store lineEl in a module-scope toolLineEl (reset to null with toolLineWrap in stopToolLineClock and on re-arm); the interval body uses toolLineEl (null-guarded — the wrap-reset no-op stays) instead of toolLineWrap?.querySelector?.(".tool-calls .tool-call:last-child"). The suffix element + the (Ns) text + the TOOL_LINE_ELAPSED_AFTER_MS gate are UNCHANGED.
    • settleToolLine() — the querySelectorAll(".tool-elapsed") removal stays (it clears every line in the wrap — correct with multiple disclosures); add toolLineEl = null alongside.
    • the comment block updated to name the re-target (the multi-disclosure fix, phase 158 D1).
  5. frontend/assets/app.js — the save payloads (D4) — the FIVE sites:
    • buildSegments(segRuns) — a helper (beside the turn state): maps the runs IN ORDER — a thinking run → { kind: "thinking", text: run.text } ONLY when run.text is non-empty (an empty run stores nothing); a tool run → one { kind: "tool", …record } per record in order (spread the record object — the additive offset/limit/path + the phase-95 stamp fields ride through; NO re-validation on the client — the server's ChatMessage boundary validates).
    • the done save point (~L3355, the rememberBrainTurn meta): segments: buildSegments(segRuns).length ? buildSegments(segRuns) : undefined (the undefined-drops-the-key house pattern, D4) — the thinking: thinkingAcc || undefined + tools: toolAcc.length ? toolAcc : undefined lines are UNCHANGED.
    • finalizeFailedTurn (L2983): the parameter object gains segments (the TWO call sites — the stream-drop site ~L3431 and the network-error site ~L3519 — pass buildSegments(segRuns)); the function's if (wrap) saved record includes segments ONLY when the passed value is a non-empty array (segments: segments?.length ? segments : undefined); the existing acc/thinking/tools fields are UNCHANGED.
    • the STOP path's save point (L3487–L3491, the stopped: true meta): gains the same segments field (non-empty only) — a stopped partial restores its timeline.
    • the pagehide partial (L3642–L3650, the module-scope read — segRuns is module scope, so it is readable there): gains the same segments field (non-empty only). The zero-frame fallback save (L3456–L3462) needs NOTHING (zero frames → no runs → no key).
    • the thinking/tools keys are UNCHANGED at every site.
  6. tests/unit/test_thinking_timeline.py (NEW — the source-level house pattern, the js[fn : js.find("\n}\n", fn)] body-extraction the existing pins use):
    • ensureThinkingRun body: creates details + className = "thinking" + open = true + the plain Thinking summary literal (D3 — the phase-17 literal, NOT a numbered variant) + inserts before .bubble (D2 — the anchor is the bubble, NOT the old .tool-calls-disclosure ?? … chain); ensureToolRun body: className = "tool-calls-disclosure" + open = true + the summary/list structure.
    • appendToolLine signature: the first param is the container (no body.querySelector(".tool-calls-disclosure") in the body — the single-container derivation is GONE); the four label literals STILL present (mirror the guard); returns the line.
    • armToolLineClock: the :last-child query is GONE; a toolLineEl reference is used; settleToolLine resets it.
    • closeCurrentRun / closeAllRuns defined; the delta branch calls closeCurrentRun (pin: appears in the delta-branch region); done + the stop/abort settle call closeAllRuns.
    • buildSegments present; the done save + the finalizeFailedTurn call site + the pagehide partial all reference it (≥ 3 call sites).
    • innerHTML NOT in ensureThinkingRun… — pin per the builder choice made in Work item 2 (if the live builder reuses the phase-17 innerHTML template, pin that; the RESTORE/SHARED builders (task 03) are pinned innerHTML-free there).
    • segRuns is module scope + reset in runTurn.
    • Builder style: MIRROR the function it replaces (the live thinking builder keeps the phase-17 static-structure convention — a static template with NO data — the data flows through renderMarkdown / textContent exactly as today; the live tool builder keeps the phase-117 createElement/textContent style — innerHTML ABSENT from appendToolLine, as pinned today). Pin the STRUCTURE + the no-data- through-innerHTML rule, not the mechanism.

Testing & Quality

  • Unit: tests/unit/test_thinking_timeline.py (this task's pins) + UPDATE the existing source pins THIS task's change breaks (inspect, in order: test_frontend_feedback.py, test_frontend_turn_loader.py — the phase-109 D15 re-open pin becomes the per-run pin (a NEW block, not a re-open), test_frontend_tool_states.py — the appendToolLine single-container pin, test_big_read_progress.py — the clock-query pin, test_frontend_failed_turn.py — the finalizeFailedTurn payload pin, test_thinking_scroll.py, test_chat_persistence.py — ONLY the pins this task touches; the restore-path pins are task 03's, the shared-page pins are task 03's). Every pin this task does NOT break (the tool-line label literals, the phase-117 deboxed CSS, the phase-139/146 suffix pins, the emoji guard) stays green UNCHANGED.
  • Coverage: >90% on this task's modified code (the pins).

Completion Criteria

  • a live turn renders per-run blocks in arrival order (the code paths land exactly as designed — the unit pins)
  • the clock targets the tracked line reference (the :last-child query is gone — the unit pin)
  • the done / failed / pagehide save payloads carry segments (non-empty only) — the unit pins
  • uv run pytest tests/unit -q green (every pin this task breaks is updated; every pin it doesn't touch stays green); uv run ruff check . && uv run pyright clean
  • git diff --stat limited to frontend/assets/app.js, tests/unit/test_thinking_timeline.py, and the unit pin files updated in this task