phase: 161_reasoning_bubble
Build and Push Containers / build-and-push-app (push) Successful in 1m55s
Build and Push Containers / build-and-push-db (push) Successful in 14s

All gates verified. Final check — AGENTS.md rules against the project (commands, gates, protocols) — the quick-reference commands all exist and the rules (PLAN §2/§7/§9 references, phase protocol, No CDN, Debugpy, git gates) accurately describe the project; the reasoning bubble is a pure UI change with no new commands or env vars, so no AGENTS.md change is needed.

**Verification complete — full results:**

- **Verified** (implemented by prior task passes, all in working tree): `createReasoningWrap`/`ensureReasoningWrap` live container (D1), `settleReasoning` fold at the 3 settle sites + not at delta (D2), 320px `.reasoning-body` window + pinned-tail follow (D3), zero persistence change (D4), collapsed restore + shared-page parity with D6 step counts, `styles.css` family treatment + reduced-motion still, mock `REASONING_MIDROUND_TRIGGER` flow, 6-test story suite, phase-158 pin updates
- **Full suite:** `uv run pytest` → **3738 passed, 0 failed** · `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99% (>90%)** · `uv run ruff check .` → all checks passed · `uv run pyright` → **0 errors, 0 warnings**
- **E2E in isolation** (DB up; port-8123 collision with an unrelated `undercooked` service → `E2E_APP_PORT=8125`/`E2E_MOCK_PORT=8902`, no live change made): `test_reasoning_bubble.py` **6/6** · `test_thinking_timeline.py` ✓ · `test_thinking_display.py` ✓ · `test_tool_call_compact.py` ✓ · `test_big_read_progress.py` ✓ · `test_turn_progress_loader.py` ✓ · `test_share_chat.py` ✓ · `test_chat_persistence.py` ✓ · `test_smoke.py` ✓ (all exit 0, zero failures)
- **Completion criteria:** all 9 checked against the code + test outcomes — container open/live/nested ✓, stays open past mid-round delta ✓, folds at done ✓, restore + shared collapsed with "3 steps" ✓, plain turn = no container ✓, regression suites green ✓, suite/coverage/lint green ✓, diff scope limited to `frontend/assets/{app.js,shared.js,styles.css}` + `tests/` + `README.md` + phase files (no `app/` — D4) ✓
- **Documentation:** README.md "Thinking" section names the Reasoning bubble accurately (320px window, open-until-settle incl. mid-round deltas, "N steps", byte-identical no-container, collapsed on restore/shared) — verified consistent, no changes needed; AGENTS.md — verified consistent, no changes needed; all `.env.example` vars documented
- **Decisions:** commit + phase-dir move left to the harness (per pass rules — no git index/history commands executed); `undercooked` dev server on port 8123 untouched, E2E used the env port overrides
- **Next pending phase:** none — `161_reasoning_bubble` is the last in `todo/`
This commit is contained in:
2026-10-06 06:36:09 -04:00
parent 8172956c15
commit 051f62deb7
48 changed files with 4194 additions and 166 deletions
+46 -4
View File
@@ -32,8 +32,10 @@ And — the phase's explicit "NOT touched" contract — ``retryLastTurn``
is byte-unchanged, ``runTurn`` is pinned at exactly the state the
phase left it (its sanctioned additions — the task-01
``lastUserWrap`` assignment + the idle-settle ``markLastEditable()``
mark pass, and the task-03 aborted-settle mark pair — included;
no task may restructure it), and — the task-04 regression pins —
mark pass, the task-03 aborted-settle mark pair, the phase-160
done-settle pair, and the phase-161 live-container additions —
included; no task may restructure it), and — the task-04 regression
pins —
``handleSend`` and ``rememberBrainTurn`` are byte-unchanged (the
plain send's pre-work and the brain-record funnel the re-ask's
settle rides are touched by no phase-159 task). Task 03 (the
@@ -852,6 +854,7 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
thinkingAcc = "";
segRuns = []; // phase 158: a fresh turn starts with no runs (the pagehide partial reads the current turn's runs)
currentRun = null;
reasoningWrapEl = null; // phase 161: a fresh turn starts with no container (the pagehide partial reads the live container, like segRuns)
persistedOnLeave = false;
leftThePage = false;
leavePartialIndex = -1;
@@ -954,6 +957,11 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
if (!wrap) wrap = addMessage("brain", "");
removeTyping(); // the live block replaces the dots as feedback
const block = ensureThinkingRun(wrap);
// Phase 161 (D6): the container's live status — "Thinking…"
// while a thinking frame streams (a tool frame overwrites it
// with "Calling <name>…"; the settle sites overwrite it with
// "N steps").
setReasoningStatus("Thinking…");
currentRun.run.text += ev.text || ""; // the RUN's text — the per-run block renders it
const textEl = block.querySelector(".thinking-text");
// Pin state is measured BEFORE the re-render: a chunk taller
@@ -967,6 +975,13 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
// THIS block (per-run — each run's block pins its own tail;
// the pre-update capture is verbatim).
const pinned = block.open && isThinkingNearBottom(textEl);
// Phase 161 (D3): the container window's PRE-update pin
// capture — the phase-17 pre-measure convention, BEFORE the
// re-render grows the content (the inner per-block follow
// above stays — inside the container the .thinking-text is
// no longer scrollable, so it no-ops there; task 03).
const rbody = reasoningWrapEl?.querySelector(".reasoning-body");
const rPinned = !!rbody && isReasoningPinned(rbody);
textEl.innerHTML = renderMarkdown(currentRun.run.text); // escape-first, XSS-safe
if (pinned) {
// Follow the live tail only while the user was pinned to the
@@ -975,6 +990,7 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
// returning to the bottom re-arms the pin on the next chunk.
textEl.scrollTop = textEl.scrollHeight;
}
if (rPinned && rbody) scrollReasoningToBottom(rbody); // post-append apply
} else if (ev.type === "tool") {
// Phase 37 (PLAN §4 extension): an agent tool call. The UI
// state stays "thinking" — the button stays the enabled "Stop"
@@ -1057,6 +1073,14 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
// wiring.
const disc = ensureToolRun(wrap);
currentRun.run.records.push(toolRecord);
// Phase 161 (D6): "Calling <name>…" — the display name the
// tool line already shows (the phase-70 legacy-name mapping
// produced `name` before this point).
setReasoningStatus(`Calling ${name}…`);
// Phase 161 (D3): the container window's PRE-update pin
// capture — before the line append grows the content.
const tbody = reasoningWrapEl?.querySelector(".reasoning-body");
const tPinned = !!tbody && isReasoningPinned(tbody);
const line = appendToolLine(
disc,
name,
@@ -1065,6 +1089,7 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
frameLimit,
framePath
);
if (tPinned && tbody) scrollReasoningToBottom(tbody); // post-append apply
// Phase 87 (TODO.md L5): arm this line's elapsed clock — the
// baseline resets per line, so each line counts its OWN
// frameless silence. This live branch is the ONLY arm call site
@@ -1121,6 +1146,11 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
// late double-close is a no-op). Replaces the phase-117
// closeToolCalls fold + the D15 block close.
closeAllRuns(wrap);
// Phase 161 (D2): the turn settled — fold the container too
// (the final response has been returned); the summary reads
// the live run count (D6 — segRuns, module scope).
settleReasoning(wrap);
setReasoningStatus(`${segRuns.length} steps`);
if (!wrap) {
setUiState(UI_STATE.streaming);
wrap = addMessage("brain", "…");
@@ -1332,6 +1362,11 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
// (replaces the phase-117 closeToolCalls fold + the D15 block
// close, like `done`).
closeAllRuns(wrap);
// Phase 161 (D2): the session stopped — fold the container
// alongside the runs; the summary reads the live run count
// (D6 — segRuns, module scope).
settleReasoning(wrap);
setReasoningStatus(`${segRuns.length} steps`);
persistedOnLeave = true;
appendTuneButton(wrap); // admin-only; parity with the restore path
appendStoppedNote(wrap);
@@ -1427,12 +1462,19 @@ def test_run_turn_is_pinned_at_phase_159_state() -> None:
plus phase 160 (task 04)'s sanctioned additions (task 02 consumes
its ``reask`` path AS-IS — the phase's "NOT touched" contract;
the phase-160 additions are the done settle's additive
``budget_exhausted`` meta key + the flag-gated Continue button):
``budget_exhausted`` meta key + the flag-gated Continue button)
and phase 161 (task 01)'s sanctioned additions (the
``reasoningWrapEl`` turn-local reset — the pagehide partial reads
the live container, like segRuns; the D6 status writes + the D3
pre/post follow in the thinking/tool frame branches; the
``settleReasoning(wrap)`` fold at the done / stop / failed
settle sites — the container's live-path contract, D1/D2/D3/D6):
the full source must match the pin — the ``reask`` path, the
turn-local resets, the SSE contract, the save points, and the
sanctioned additions (the task-01 ``lastUserWrap`` assignment +
the idle-settle mark pass, the task-03 aborted-settle mark pair,
and the phase-160 done-settle pair) included."""
the phase-160 done-settle pair, and the phase-161 live-container
additions) included."""
assert _fn_source(_js(), "runTurn") == PINNED_RUN_TURN