phase: 161_reasoning_bubble
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:
@@ -247,6 +247,33 @@ function addToolLines(wrap, tools) {
|
||||
body.insertBefore(disclosure, body.querySelector(".bubble"));
|
||||
}
|
||||
|
||||
/* Phase 161 (task 02, D1/D6): the SHARED page's parity copy of
|
||||
* app.js's createReasoningWrap — the SAME details.reasoning
|
||||
* container (summary.reasoning-summary with the "Reasoning" literal
|
||||
* — D6's ONLY new static literal — + the EMPTY span.reasoning-status
|
||||
* the at-rest count fills, and the .reasoning-body window), built
|
||||
* with createElement only (innerHTML ABSENT — the house rule; the
|
||||
* "Reasoning" literal is a static text node, no data flows through
|
||||
* it). The caller owns open/fold + insertion: the shared page
|
||||
* renders it COLLAPSED around the per-run timeline (D5 — the
|
||||
* pure-render convention) with the D6 "N steps" summary. A parity
|
||||
* copy, not an import (the shared page has no import of app.js). */
|
||||
function createReasoningWrap() {
|
||||
const cont = document.createElement("details");
|
||||
cont.className = "reasoning";
|
||||
const summary = document.createElement("summary");
|
||||
summary.className = "reasoning-summary";
|
||||
summary.textContent = "Reasoning"; // D6: the ONLY new static literal (parity with app.js)
|
||||
const status = document.createElement("span");
|
||||
status.className = "reasoning-status"; // empty until the at-rest count fills it
|
||||
summary.appendChild(status);
|
||||
cont.appendChild(summary);
|
||||
const body = document.createElement("div");
|
||||
body.className = "reasoning-body";
|
||||
cont.appendChild(body);
|
||||
return cont;
|
||||
}
|
||||
|
||||
/* Phase 158 (task 03, D4/D5): the SHARED page's per-run timeline —
|
||||
* the parity copy of the chat page's restoreTimeline: the record's
|
||||
* `segments` array (the per-run order, task 01's ChatMessage field)
|
||||
@@ -255,7 +282,18 @@ function addToolLines(wrap, tools) {
|
||||
* details.tool-calls-disclosure ("Tool call(s) (N)" per run), in
|
||||
* arrival order, ABOVE the answer — a pure render, so everything is
|
||||
* created CLOSED (D5; the same collapsed convention as
|
||||
* addThinkingBlock / addToolLines). The structure is
|
||||
* addThinkingBlock / addToolLines). Phase 161 (task 02, D5/D6): the
|
||||
* per-run timeline renders inside ONE details.reasoning container —
|
||||
* the local parity builder above, created CLOSED (the pure-render
|
||||
* convention) and inserted ONCE before the bubble (the shared
|
||||
* page's existing insertion convention); every per-run block
|
||||
* appends into the container's .reasoning-body (the per-run bubble
|
||||
* anchor is superseded — arrival order preserved). The D6 at-rest
|
||||
* step count uses the SAME two-increment semantics as app.js's
|
||||
* restoreTimeline (a parity copy, not an import — contiguous runs:
|
||||
* each thinking entry is ONE run; the first entry of a new
|
||||
* contiguous tool group is ONE run), with the same uniform
|
||||
* `${runs} steps` literal. The structure is
|
||||
* createElement/textContent; the line bytes mirror addToolLines
|
||||
* EXACTLY (the four pinned label literals + the phase-70 legacy
|
||||
* names + the phase-139 readRangeSuffix + the phase-95 truncation
|
||||
@@ -270,7 +308,17 @@ function addSegments(wrap, segments) {
|
||||
const body = wrap.querySelector(".msg-body");
|
||||
if (!body) return;
|
||||
const bubble = body.querySelector(".bubble");
|
||||
// Phase 161 (task 02, D5/D6): the container — the local parity
|
||||
// builder, created CLOSED (D5 — the pure-render convention;
|
||||
// explicit for the pin) and inserted ONCE before the bubble (the
|
||||
// shared page's existing insertion convention — the per-run blocks
|
||||
// nest into its .reasoning-body, arrival order preserved).
|
||||
const cont = createReasoningWrap();
|
||||
cont.open = false; // D5: collapsed (the pure-render convention; explicit for the pin)
|
||||
const runsEl = cont.querySelector(".reasoning-body");
|
||||
body.insertBefore(cont, bubble); // the shared page's existing insertion convention
|
||||
let toolDisc = null; // the CURRENT run of consecutive tool entries (one disclosure per run, D1)
|
||||
let runs = 0; // D6: the at-rest step count — contiguous runs, not entry count
|
||||
for (const entry of segments || []) {
|
||||
// a malformed entry (or one with an unknown `kind`) is SKIPPED —
|
||||
// never render from storage (textContent only, the house rule).
|
||||
@@ -278,6 +326,7 @@ function addSegments(wrap, segments) {
|
||||
if (entry.kind === "thinking") {
|
||||
if (typeof entry.text !== "string") continue; // malformed: skipped
|
||||
if (toolDisc) toolDisc = null; // the run is sealed — the count is final
|
||||
runs += 1; // D6: each thinking entry is ONE run (parity with app.js — a thinking entry after a tool group starts a run; two adjacent thinking entries are TWO runs)
|
||||
const block = document.createElement("details");
|
||||
block.className = "thinking";
|
||||
block.open = false; // D5: collapsed (the addThinkingBlock convention)
|
||||
@@ -287,13 +336,14 @@ function addSegments(wrap, segments) {
|
||||
textEl.className = "thinking-text";
|
||||
textEl.innerHTML = renderMarkdown(entry.text); // escape-first, XSS-safe (parity with addThinkingBlock)
|
||||
block.append(summary, textEl);
|
||||
body.insertBefore(block, bubble); // D2: above the bubble, in arrival order
|
||||
runsEl.appendChild(block); // Phase 161 D5: inside the container, in arrival order
|
||||
} else if (entry.kind === "tool") {
|
||||
if (typeof entry.name !== "string") continue; // malformed: skipped
|
||||
if (!toolDisc) {
|
||||
toolDisc = document.createElement("details");
|
||||
toolDisc.className = "tool-calls-disclosure";
|
||||
toolDisc.open = false; // D5: collapsed (parity with addToolLines — phase 117 D3)
|
||||
runs += 1; // D6: the first entry of a NEW contiguous group is ONE run (parity with app.js — a group of N entries is ONE run)
|
||||
const summary = document.createElement("summary");
|
||||
summary.className = "tool-calls-summary";
|
||||
toolDisc.appendChild(summary);
|
||||
@@ -302,7 +352,7 @@ function addSegments(wrap, segments) {
|
||||
container.setAttribute("role", "list");
|
||||
container.setAttribute("aria-label", "Tool calls");
|
||||
toolDisc.appendChild(container);
|
||||
body.insertBefore(toolDisc, bubble); // D2: above the bubble, in arrival order
|
||||
runsEl.appendChild(toolDisc); // Phase 161 D5: inside the container, in arrival order
|
||||
}
|
||||
const container = toolDisc.querySelector(".tool-calls");
|
||||
const line = document.createElement("span");
|
||||
@@ -364,6 +414,9 @@ function addSegments(wrap, segments) {
|
||||
// an entry with an unknown `kind` (or a malformed shape) is
|
||||
// SKIPPED — never render from storage (textContent only, the
|
||||
// house rule); a guard comment names the skip.
|
||||
// Phase 161 (task 02, D6): the at-rest step count — the UNIFORM
|
||||
// literal (parity with app.js — no singular/plural special-case).
|
||||
cont.querySelector(".reasoning-status").textContent = `${runs} steps`;
|
||||
}
|
||||
|
||||
/* Phase 139 (task 01): the shared page's parity copy of app.js's
|
||||
|
||||
Reference in New Issue
Block a user