diff --git a/src/story-mode.ts b/src/story-mode.ts index db95cf5e..2f9d4c1b 100644 --- a/src/story-mode.ts +++ b/src/story-mode.ts @@ -2086,7 +2086,7 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde ? `Build and test, ${plural(n, 'round')}. Every check passes.` : panel.ok === true && fails === lintFails ? `Build and test, ${plural(n, 'round')}. A check fails ${fails === 1 ? 'once' : `${fails} times`}, then everything passes.` - : panel.ok === true && n === 2 + : panel.ok === true && checks.length === 2 && fails === 1 ? 'Fix and test: the first run fails, the second passes.' : panel.ok === true ? `Fix and test, ${plural(n, 'round')}: ${fails === 1 ? 'one run fails' : `${fails} runs fail`}, then it passes.` diff --git a/src/web/public/story-mode.css b/src/web/public/story-mode.css index be815cef..666e58fa 100644 --- a/src/web/public/story-mode.css +++ b/src/web/public/story-mode.css @@ -218,6 +218,37 @@ body.story-open { } } +/* A phone: three rows (title and close, the turn, the view and Save image), not four with the + close button alone at the bottom. Tab order stays the DOM's; focus starts on close anyway. */ +@media (max-width: 600px) { + .story-bar { + gap: 8px 10px; + padding-block: 8px; + } + .story-brand { + order: 0; + flex: 1 1 0; + } + .story-title { + font-size: 24px; + } + .story-close { + order: 1; + } + .story-nav { + order: 2; + flex: 1 1 100%; + } + .story-turns { + max-width: none; + } + .story-seg, + .story-live, + .story-save { + order: 3; + } +} + /* Main: the sheet and the wire */ .story-main { overflow: auto; @@ -731,7 +762,7 @@ body.story-open .toast-container { padding-right: max(16px, env(safe-area-inset-right, 0px)); } .story-drawer { - top: calc(var(--story-bar-h, 18vh) + 6px); + top: var(--story-bar-h, 18vh); left: 0; width: auto; height: auto; diff --git a/src/web/public/story-mode.js b/src/web/public/story-mode.js index 9d6a3c86..dcb6c226 100644 --- a/src/web/public/story-mode.js +++ b/src/web/public/story-mode.js @@ -1894,12 +1894,16 @@ Object.assign(CodemanApp.prototype, { _storyStartRuntime() { const st = this._story; + const bar = st.root.querySelector('.story-bar'); st.ro = new ResizeObserver((entries) => { for (const entry of entries) { + // The bar's height places the phone drawer below it, and changes when the phone turns. + if (entry.target === bar) st.root.style.setProperty('--story-bar-h', `${bar.offsetHeight}px`); const rt = st.panels.find((p) => p.el === entry.target); if (rt) this._storySizePanel(rt); } }); + if (bar) st.ro.observe(bar); st.io = new IntersectionObserver((entries) => { for (const entry of entries) { const rt = st.panels.find((p) => p.el === entry.target); @@ -1947,6 +1951,16 @@ Object.assign(CodemanApp.prototype, { drawn++; } st.drawCursor = n ? ((st.drawCursor || 0) + 1) % n : 0; + // A quiet frame draws a panel out of view (one band of it), so its buttons exist for the + // keyboard and a screen reader before anyone scrolls to it. Its clock waits, so the entrance + // still plays when it comes into view. + if (!drawn) { + const off = st.panels.find((p) => !p.visible && p.pending); + if (off) { + off.pending = false; + this._storyDrawPanel(off); + } + } } st.raf = requestAnimationFrame(loop); }; diff --git a/test/fixtures/story-corpus/bash-script-background-helper.expected.json b/test/fixtures/story-corpus/bash-script-background-helper.expected.json index 7f381c33..7ddc3766 100644 --- a/test/fixtures/story-corpus/bash-script-background-helper.expected.json +++ b/test/fixtures/story-corpus/bash-script-background-helper.expected.json @@ -19,6 +19,7 @@ "director": "Prompt · 65 words", "balloon": "First write a short todo list for this job, then build a bash script rename_by_date.sh in this folder that renames *.jpg files to their modification time as YYYY-MM-DD_HHMMSS.jpg with a --dry-run flag, and while you do that use the Agent tool to have one helper agent write README.md for the script; finally test it on a few sample files you create in a samples folder.", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -28,6 +29,7 @@ "director": "written as text · 5 steps", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -37,6 +39,7 @@ "director": "Bash ×2 · ls -la && git log --oneline | head; which shellcheck; bash…", "balloon": null, "tests": null, + "roundsOk": null, "events": 2 }, { @@ -46,6 +49,7 @@ "director": "Advisor · a second opinion", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -55,6 +59,7 @@ "director": "Agent · general-purpose · 6 calls inside", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -64,6 +69,7 @@ "director": "Write · Edit ×3 · Bash · rename_by_date.sh +95 −3 · -dash.jpg +1", "balloon": "Now I'll create the samples and run the dry run.", "tests": null, + "roundsOk": null, "events": 5 }, { @@ -73,6 +79,7 @@ "director": "Bash ×5 · rm -- ./-- && S=/tmp/claude/-work-cases-story-review-c/740e…", "balloon": "My mk -- ... helper call left a stray file named --. I'll remove it, then run the dry run and save a listing to confirm nothing changed.", "tests": null, + "roundsOk": null, "events": 5 }, { @@ -82,6 +89,7 @@ "director": "turn_duration · 2 min 1 s · 14 tool calls · 2 files changed · 1 helper", "balloon": "All the script tests pass.", "tests": null, + "roundsOk": null, "events": 0 } ] @@ -106,6 +114,7 @@ "director": "Prompt · 7 words · task", "balloon": "Agent \"Write README for rename script\" finished", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -115,6 +124,7 @@ "director": "Read", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -124,6 +134,7 @@ "director": "Bash · mkdir -p \"$S/ro\" && printf x > \"$S/ro/q.jpg\" && chmod a-w \"…", "balloon": "The README matches the script.", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -133,6 +144,7 @@ "director": "turn_duration · 21 s · 2 tool calls · 1 helper", "balloon": "I built rename_by_date.sh, the helper agent wrote README.md, and the script passed every test I ran on sample files in samples/. Nothing is committed because this folder isn't a git repository, even though the session context said it was.", "tests": null, + "roundsOk": null, "events": 0 } ] @@ -157,6 +169,7 @@ "director": "Prompt · 26 words", "balloon": "Add a --prefix option to rename_by_date.sh that puts a given text in front of the new names, and show me a dry run on the samples.", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -166,6 +179,7 @@ "director": "Read", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -175,6 +189,7 @@ "director": "Edit ×3 · rename_by_date.sh +21 −6", "balloon": null, "tests": null, + "roundsOk": null, "events": 3 }, { @@ -184,6 +199,7 @@ "director": "Bash ×2 · bash -n \"$P/rename_by_date.sh\" && echo \"syntax ok\"", "balloon": "Here's the dry run you asked for on the samples, plus a check that nothing on disk changed:", "tests": null, + "roundsOk": null, "events": 2 }, { @@ -193,6 +209,7 @@ "director": "Edit ×3 · README.md +18 −2", "balloon": "All passing.", "tests": null, + "roundsOk": null, "events": 3 }, { @@ -202,6 +219,7 @@ "director": "turn_duration · 1 min 14 s · 9 tool calls · 2 files changed", "balloon": "I added a --prefix option to rename_by_date.sh.", "tests": null, + "roundsOk": null, "events": 0 } ] diff --git a/test/fixtures/story-corpus/compact-then-plan-mode.expected.json b/test/fixtures/story-corpus/compact-then-plan-mode.expected.json index 2f2b4995..c1b3d7ca 100644 --- a/test/fixtures/story-corpus/compact-then-plan-mode.expected.json +++ b/test/fixtures/story-corpus/compact-then-plan-mode.expected.json @@ -19,6 +19,7 @@ "director": "Prompt · 41 words", "balloon": "Write mathx.py with a function clamp(value, low, high) that returns value limited to the range low..high and raises ValueError when low > high, then write test_mathx.py with exactly 5 pytest tests for it, and run them with python3 -m pytest -q.", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -28,6 +29,7 @@ "director": "Bash ×2 · ls -la && git status && git log --oneline -5 2>&1; python3… · 1 error", "balloon": null, "tests": null, + "roundsOk": null, "events": 2 }, { @@ -37,6 +39,7 @@ "director": "Advisor · a second opinion", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -46,6 +49,7 @@ "director": "Write ×2 · mathx.py +11 · test_mathx.py +26", "balloon": null, "tests": null, + "roundsOk": null, "events": 2 }, { @@ -58,6 +62,7 @@ "passed": 5, "failed": 0 }, + "roundsOk": null, "events": 1 }, { @@ -67,6 +72,7 @@ "director": "git commit -q -F -", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -76,6 +82,7 @@ "director": "turn_duration · 1 min 12 s · 7 tool calls · 2 files changed", "balloon": "I wrote clamp and its five tests, and all five pass.", "tests": null, + "roundsOk": null, "events": 0 } ] @@ -100,6 +107,7 @@ "director": "Command · /compact", "balloon": "/compact", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -109,6 +117,7 @@ "director": "manual · 46.9k → 3.6k tokens · 24 s", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -118,6 +127,7 @@ "director": "local command · 24 s · 0 tool calls", "balloon": "Compacted", "tests": null, + "roundsOk": null, "events": 0 } ] @@ -142,6 +152,7 @@ "director": "Prompt · 50 words", "balloon": "Call your EnterPlanMode tool to enter plan mode, then write a short 3-step plan for adding a wrap(value, low, high) function (modular wrap into the range) to mathx.py with 3 more pytest tests, and present that plan with your ExitPlanMode tool. After I approve, implement it and run the tests.", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -151,6 +162,7 @@ "director": "ToolSearch", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -160,6 +172,7 @@ "director": "EnterPlanMode", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -169,6 +182,7 @@ "director": "Advisor · a second opinion", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -178,6 +192,7 @@ "director": "Write · plan file · 3 steps", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -187,6 +202,7 @@ "director": "ExitPlanMode · answer: Approved", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -196,6 +212,7 @@ "director": "Edit ×3 · mathx.py +11 · test_mathx.py +20 −1", "balloon": null, "tests": null, + "roundsOk": null, "events": 3 }, { @@ -208,6 +225,7 @@ "passed": 8, "failed": 0 }, + "roundsOk": null, "events": 1 }, { @@ -217,6 +235,7 @@ "director": "git commit -q -F -", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -226,6 +245,7 @@ "director": "turn_duration · 1 min 7 s · 10 tool calls · 2 files changed", "balloon": "I added wrap() as planned, with 3 new tests.", "tests": null, + "roundsOk": null, "events": 0 } ] diff --git a/test/fixtures/story-corpus/failing-test-on-purpose.expected.json b/test/fixtures/story-corpus/failing-test-on-purpose.expected.json index 928414ac..d1993218 100644 --- a/test/fixtures/story-corpus/failing-test-on-purpose.expected.json +++ b/test/fixtures/story-corpus/failing-test-on-purpose.expected.json @@ -19,6 +19,7 @@ "director": "Prompt · 67 words", "balloon": "Create a small JavaScript ES module slugify.mjs exporting slugify(str) (lowercase, trim, spaces and punctuation to single hyphens, no leading or trailing hyphens) plus slugify.test.mjs with 4 node:test tests; on purpose make ONE test expectation wrong so the first run of node --test fails, run it, then fix that expectation, rerun until all pass, then git init if needed and git commit both files w…", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -28,6 +29,7 @@ "director": "Bash ×2 · ls -la && git status && git log --oneline -5 2>&1; node --v…", "balloon": null, "tests": null, + "roundsOk": null, "events": 2 }, { @@ -37,6 +39,7 @@ "director": "Advisor · a second opinion", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -49,6 +52,10 @@ "passed": 4, "failed": 0 }, + "roundsOk": [ + false, + true + ], "events": 5 }, { @@ -58,6 +65,7 @@ "director": "Bash · git init -b main && echo \"name=$(git config user.name) emai…", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -67,6 +75,7 @@ "director": "git commit -q -F -", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -76,6 +85,7 @@ "director": "turn_duration · 1 min 24 s · 10 tool calls · 2 files changed", "balloon": "slugify.mjs and its test file are written, all 4 tests pass, and both files are committed.", "tests": null, + "roundsOk": null, "events": 0 } ] @@ -100,6 +110,7 @@ "director": "Prompt · 38 words", "balloon": "Add a fifth node:test test for slugify('') to slugify.test.mjs that on purpose expects 'x' so the first run fails, run node --test, then fix that expectation to the empty string, rerun until all pass, and commit the change.", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -112,6 +123,10 @@ "passed": 5, "failed": 0 }, + "roundsOk": [ + false, + true + ], "events": 4 }, { @@ -121,6 +136,7 @@ "director": "git commit -q -F -", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -130,6 +146,7 @@ "director": "turn_duration · 21 s · 5 tool calls · 1 file changed", "balloon": "I added a fifth test for slugify('').", "tests": null, + "roundsOk": null, "events": 0 } ] diff --git a/test/fixtures/story-corpus/long-turn-prompt-added.expected.json b/test/fixtures/story-corpus/long-turn-prompt-added.expected.json index 62c4e4da..b2c893cd 100644 --- a/test/fixtures/story-corpus/long-turn-prompt-added.expected.json +++ b/test/fixtures/story-corpus/long-turn-prompt-added.expected.json @@ -19,6 +19,7 @@ "director": "Prompt · 96 words", "balloon": "Build a small Python package inventory/ (a CLI to add, list and remove items kept in a JSON file) split into at least 4 modules (for example storage.py, models.py, commands.py, cli.py), with a pytest suite of at least 25 tests in total; build it one module at a time, and after EACH module write its tests and run pytest, fixing any failures before moving on to the next module; then git init the fo…", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -28,6 +29,7 @@ "director": "Bash ×4", "balloon": null, "tests": null, + "roundsOk": null, "events": 4 }, { @@ -37,6 +39,7 @@ "director": "Advisor · a second opinion", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -46,6 +49,7 @@ "director": "Bash · pyproject.toml +11 · .gitignore +11 · __init__.py +3", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -55,6 +59,7 @@ "director": "Prompt · 11 words", "balloon": "Also add a --csv option to the list command, with tests.", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -67,6 +72,13 @@ "passed": 63, "failed": 0 }, + "roundsOk": [ + true, + true, + true, + false, + true + ], "events": 14 }, { @@ -76,6 +88,7 @@ "director": "Write · README.md +79", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -85,6 +98,7 @@ "director": "Bash ×2 · D=/tmp/claude/-work-cases-story-r2-a/94066c8e-0855-4c6b-a14…", "balloon": "README examples match the real output.", "tests": null, + "roundsOk": null, "events": 2 }, { @@ -94,6 +108,7 @@ "director": "git commit -q -m \"chore: scaffold inventory package\" -m \"Ad…", "balloon": "Commits are in.", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -106,6 +121,7 @@ "passed": 63, "failed": 0 }, + "roundsOk": null, "events": 1 }, { @@ -115,6 +131,7 @@ "director": "Advisor · a second opinion", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -124,6 +141,7 @@ "director": "turn_duration · 6 min 25 s · 26 tool calls · 13 files changed", "balloon": "The inventory/ package is built and committed.", "tests": null, + "roundsOk": null, "events": 0 } ] @@ -148,6 +166,7 @@ "director": "Prompt · 188 words", "balloon": "Now do these 15 small steps strictly one at a time, each as its own separate tool call, in exactly this order, never combining two steps into one call and without extra steps in between: 1) Read inventory/models.py with the Read tool; 2) with the Edit tool add the line 'Version 0.1.0.' at the end of README.md; 3) run 'python3 -m pytest -q' in Bash; 4) run 'uname -s' in Bash; 5) run 'git add -A &&…", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -157,6 +176,7 @@ "director": "Read · Edit · Bash ×4", "balloon": "Step 1:", "tests": null, + "roundsOk": null, "events": 6 }, { @@ -166,6 +186,7 @@ "director": "Prompt · 15 words", "balloon": "Also, as a 16th step at the very end, run git log --oneline in Bash.", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -175,6 +196,7 @@ "director": "Write · Bash ×2", "balloon": "Noted, I'll add step 16 at the end.", "tests": null, + "roundsOk": null, "events": 3 }, { @@ -184,6 +206,7 @@ "director": "git commit -m \"docs: changelog\" -m \"Co-Authored-By: Claude…", "balloon": "Step 10:", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -193,6 +216,7 @@ "director": "Read", "balloon": "Step 11:", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -202,6 +226,7 @@ "director": "Edit · CHANGELOG.md +2", "balloon": "Step 12:", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -214,6 +239,7 @@ "passed": 63, "failed": 0 }, + "roundsOk": null, "events": 1 }, { @@ -223,6 +249,7 @@ "director": "Bash · uname -m", "balloon": "Step 14:", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -232,6 +259,7 @@ "director": "git commit -am \"docs: changelog entry\" -m \"Co-Authored-By:…", "balloon": "Step 15:", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -241,6 +269,7 @@ "director": "Bash · git log --oneline", "balloon": "Step 16:", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -250,6 +279,7 @@ "director": "turn_duration · 1 min 16 s · 16 tool calls · 2 files changed", "balloon": "I ran all 16 steps in order, one tool call each.", "tests": null, + "roundsOk": null, "events": 0 } ] diff --git a/test/fixtures/story-corpus/python-cli-fix-loop.expected.json b/test/fixtures/story-corpus/python-cli-fix-loop.expected.json index 0e1d6be8..e54a8983 100644 --- a/test/fixtures/story-corpus/python-cli-fix-loop.expected.json +++ b/test/fixtures/story-corpus/python-cli-fix-loop.expected.json @@ -19,6 +19,7 @@ "director": "Prompt · 52 words", "balloon": "Build a small Python command-line tool wcount.py in this folder that prints the line, word and character counts of the files given as arguments, like wc, with a --json flag that prints the counts as JSON, plus pytest tests in test_wcount.py; run pytest and fix anything that fails until all tests pass.", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -28,6 +29,7 @@ "director": "Bash ×2 · ls -la && git log --oneline -5 2>&1; git branch -a; python3… · 1 error", "balloon": null, "tests": null, + "roundsOk": null, "events": 2 }, { @@ -37,18 +39,24 @@ "director": "Advisor · a second opinion", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { "kind": "loop", "ok": true, - "story": "Fix and test: the first run fails, the second passes.", + "story": "Fix and test, 2 rounds: 2 runs fail, then it passes.", "director": "3 checks · 5 edits · +218 −1 · last: ok", "balloon": "All 19 tests pass, ruff check is clean, and the output matches real wc column for column.", "tests": { "passed": 19, "failed": 0 }, + "roundsOk": [ + false, + false, + true + ], "events": 7 }, { @@ -58,6 +66,7 @@ "director": "git commit -q -m \"chore: ignore Python and pytest caches · writes 1 file", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -67,6 +76,7 @@ "director": "Advisor · a second opinion", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -76,6 +86,7 @@ "director": "Bash · python3 wcount.py nope nope2; echo \"exit=$?\"; wc nope nope2…", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -85,6 +96,7 @@ "director": "turn_duration · 3 min 13 s · 13 tool calls · 3 files changed", "balloon": "I built wcount.py and test_wcount.py.", "tests": null, + "roundsOk": null, "events": 0 } ] diff --git a/test/fixtures/story-corpus/question-tests-first-push.expected.json b/test/fixtures/story-corpus/question-tests-first-push.expected.json index 85f50eb0..5a2a701e 100644 --- a/test/fixtures/story-corpus/question-tests-first-push.expected.json +++ b/test/fixtures/story-corpus/question-tests-first-push.expected.json @@ -19,6 +19,7 @@ "director": "Prompt · 121 words", "balloon": "Build a tiny Python command-line tool in this folder that turns text into URL slugs. Before doing anything else, ask me with your AskUserQuestion tool whether the tool should be named slugify or kebab (offer exactly those two options). After my answer, first write me your plan as a short numbered list, then write the pytest tests FIRST (at least 8 tests in tests/), run them with python3 -m pytest…", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -28,6 +29,7 @@ "director": "AskUserQuestion · answer: kebab", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -37,6 +39,7 @@ "director": "Bash ×2 · ls -la && git status 2>&1 | head -5; git rev-parse --show-t…", "balloon": null, "tests": null, + "roundsOk": null, "events": 2 }, { @@ -46,6 +49,7 @@ "director": "Advisor · a second opinion", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -58,6 +62,10 @@ "passed": 19, "failed": 0 }, + "roundsOk": [ + false, + true + ], "events": 6 }, { @@ -67,6 +75,7 @@ "director": "Write ×2 · Edit · README.md +54 · .gitignore +11 · CLAUDE.md +5 −1", "balloon": null, "tests": null, + "roundsOk": null, "events": 3 }, { @@ -76,6 +85,7 @@ "director": "Bash ×3 · python3 src/kebab.py \"Hello, World!\" && python3 src/kebab.p…", "balloon": "Now I'll run each documented README command by hand and check the git identity:", "tests": null, + "roundsOk": null, "events": 3 }, { @@ -85,6 +95,7 @@ "director": "git commit -q -F - · git push -u origin main · 1 error", "balloon": null, "tests": null, + "roundsOk": null, "events": 2 }, { @@ -97,6 +108,7 @@ "passed": 19, "failed": 0 }, + "roundsOk": null, "events": 1 }, { @@ -106,6 +118,7 @@ "director": "Advisor · a second opinion", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -115,6 +128,7 @@ "director": "turn_duration · 4 min 57 s · 20 tool calls · 6 files changed", "balloon": "I've built the kebab slug tool in this folder, committed it, and pushed it to the new bare repo remote.git.", "tests": null, + "roundsOk": null, "events": 0 } ] diff --git a/test/fixtures/story-corpus/teammates-then-interrupt.expected.json b/test/fixtures/story-corpus/teammates-then-interrupt.expected.json index ea174129..6d342f89 100644 --- a/test/fixtures/story-corpus/teammates-then-interrupt.expected.json +++ b/test/fixtures/story-corpus/teammates-then-interrupt.expected.json @@ -19,6 +19,7 @@ "director": "Prompt · 77 words", "balloon": "Create a tiny node project here: package.json (type module) and index.js exporting two functions add(a,b) and slugify(s). Then start TWO helpers with the Agent tool IN THE BACKGROUND (run_in_background: true): one writes README.md describing the project, the other writes CONTRIBUTING.md. While they work, write index.test.mjs using node:test with a few tests for both functions and run it with node…", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -28,6 +29,7 @@ "director": "Bash ×2 · ls -la && git log --oneline -3 2>&1; node --version · 1 error", "balloon": null, "tests": null, + "roundsOk": null, "events": 2 }, { @@ -37,6 +39,7 @@ "director": "Advisor · a second opinion", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -46,6 +49,7 @@ "director": "git commit -q -m \"feat: add package.json and index.js with… · writes 2 files", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -55,6 +59,7 @@ "director": "Agent ×2 · general-purpose", "balloon": null, "tests": null, + "roundsOk": null, "events": 2 }, { @@ -67,6 +72,7 @@ "passed": 8, "failed": 0 }, + "roundsOk": null, "events": 1 }, { @@ -76,6 +82,7 @@ "director": "git commit -q -m \"test: add node:test suite for add and slu…", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -85,6 +92,7 @@ "director": "stop_hook_summary · 1 min 44 s · 8 tool calls · 3 files changed · 2 helpers", "balloon": "I've set up the project, started both helpers, and the tests pass: node --test index.test.mjs ran 8 tests, 8 passed, 0 failed.", "tests": null, + "roundsOk": null, "events": 0 } ] @@ -109,6 +117,7 @@ "director": "Prompt · 26 words · task", "balloon": "contributing-writer: I wrote /work/cases/story-r2-b/CONTRIBUTING.md covering setup, tests, code style, commits, README sync and PRs; it isn't committed because git was off-limits, and index.test.mjs and README.md, which…", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -118,6 +127,7 @@ "director": "Bash · ls -la && git status --short && cat CONTRIBUTING.md", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -127,6 +137,7 @@ "director": "git commit -q -m \"docs: add CONTRIBUTING.md", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -136,6 +147,7 @@ "director": "stop_hook_summary · 15 s · 2 tool calls", "balloon": "The CONTRIBUTING.md helper has finished, and I've committed its file (docs: add CONTRIBUTING.md).", "tests": null, + "roundsOk": null, "events": 0 } ] @@ -160,6 +172,7 @@ "director": "Prompt · 6 words · task", "balloon": "readme-writer: README done, test file missing", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -169,6 +182,7 @@ "director": "Bash · cat README.md && ls index.test.mjs", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -181,6 +195,7 @@ "passed": 8, "failed": 0 }, + "roundsOk": null, "events": 1 }, { @@ -190,6 +205,7 @@ "director": "stop_hook_summary · 16 s · 2 tool calls", "balloon": "The project is done: both helpers finished, and I checked their files and committed them.", "tests": null, + "roundsOk": null, "events": 0 } ] @@ -214,6 +230,7 @@ "director": "Prompt · 56 words", "balloon": "Big refactor: split index.js into three modules under lib/ (lib/add.js, lib/slugify.js, lib/util.js with shared helpers), keep index.js as a re-export barrel, add full JSDoc to every function, add at least 15 more node:test tests in separate files under test/, update README and CONTRIBUTING to match, run the tests after each step and commit each step separately.", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -223,6 +240,7 @@ "director": "Advisor · a second opinion", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -235,6 +253,7 @@ "passed": 8, "failed": 0 }, + "roundsOk": null, "events": 1 }, { @@ -244,6 +263,7 @@ "director": "git commit -q -m \"refactor: split index.js into lib/ module…", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -256,6 +276,7 @@ "passed": 8, "failed": 0 }, + "roundsOk": null, "events": 1 }, { @@ -265,6 +286,7 @@ "director": "git commit -q -m \"docs: add full JSDoc to every function an…", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -274,6 +296,7 @@ "director": "interrupted · 2 min 41 s · 5 tool calls · 4 files changed", "balloon": "Step 3: new tests under test/.", "tests": null, + "roundsOk": null, "events": 0 } ] @@ -298,6 +321,7 @@ "director": "Prompt · 23 words", "balloon": "Probe turn, no other work: run the shell command sleep 65 with Bash (foreground, not background), then reply with just the word done.", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -307,6 +331,7 @@ "director": "Bash · sleep 65 · 1 error", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -316,6 +341,7 @@ "director": "stop_hook_summary · 6 s · 1 tool call", "balloon": "Not done. The harness blocked sleep 65: a sleep on its own in the foreground isn't allowed.", "tests": null, + "roundsOk": null, "events": 0 } ] diff --git a/test/fixtures/story-corpus/text-plan-dry-run-helper.expected.json b/test/fixtures/story-corpus/text-plan-dry-run-helper.expected.json index c8121ccb..10aed630 100644 --- a/test/fixtures/story-corpus/text-plan-dry-run-helper.expected.json +++ b/test/fixtures/story-corpus/text-plan-dry-run-helper.expected.json @@ -19,6 +19,7 @@ "director": "Prompt · 81 words", "balloon": "First reply with a numbered plan of exactly 4 steps (as text, not a tool). Then build csvstat.py, a Python CLI that prints row count and per-column min/max/mean of a CSV, with pytest tests. Do a dry run on a sample CSV and save its log with `| tee /tmp/csvstat-dryrun.log`. Start ONE helper with the Agent tool, run_in_background: true and WITHOUT a name parameter, to write README.md; do not wait f…", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -28,6 +29,7 @@ "director": "written as text · 4 steps", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -37,6 +39,7 @@ "director": "Bash ×2 · ls -la && git status && git branch -a && git log --oneline… · 1 error", "balloon": null, "tests": null, + "roundsOk": null, "events": 2 }, { @@ -46,6 +49,7 @@ "director": "Advisor · a second opinion", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -55,6 +59,7 @@ "director": "Write ×4 · csvstat.py +115 · sample.csv +6 · test_csvstat.py +129", "balloon": null, "tests": null, + "roundsOk": null, "events": 4 }, { @@ -67,6 +72,7 @@ "passed": 17, "failed": 0 }, + "roundsOk": null, "events": 2 }, { @@ -76,6 +82,7 @@ "director": "Agent · general-purpose · 7 calls inside", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -85,6 +92,7 @@ "director": "Bash · set -o pipefail && python3 csvstat.py sample.csv | tee /tmp…", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -94,6 +102,7 @@ "director": "turn_duration · 2 min 44 s · 11 tool calls · 5 files changed · 1 helper", "balloon": "I've built csvstat.py, a command-line tool that prints a CSV's row count and the min, max and mean of each column.", "tests": null, + "roundsOk": null, "events": 0 } ] @@ -118,6 +127,7 @@ "director": "Prompt · 6 words · task", "balloon": "Agent \"Write README for csvstat\" finished", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -127,6 +137,7 @@ "director": "Bash · git log --oneline && git status --short && git show --stat…", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -136,6 +147,7 @@ "director": "turn_duration · 9 s · 1 tool call · 1 helper", "balloon": "The background helper has finished README.md, and I've checked its work.", "tests": null, + "roundsOk": null, "events": 0 } ] diff --git a/test/fixtures/story-corpus/web-page-node-test.expected.json b/test/fixtures/story-corpus/web-page-node-test.expected.json index 7735f903..e1e51e8d 100644 --- a/test/fixtures/story-corpus/web-page-node-test.expected.json +++ b/test/fixtures/story-corpus/web-page-node-test.expected.json @@ -19,6 +19,7 @@ "director": "Prompt · 56 words", "balloon": "Build a tiny single-page pomodoro timer in this folder: index.html with inline CSS, timer.js exporting the timer logic as plain functions, and timer.test.mjs using node:test that covers start, pause and reset; run the tests with node --test until they pass, then git init and commit everything with a short message, with no remote and no push.", "tests": null, + "roundsOk": null, "events": 1 }, { @@ -28,6 +29,7 @@ "director": "Bash ×2 · ls -la && git status 2>&1 | head; git log --oneline 2>&1 |…", "balloon": null, "tests": null, + "roundsOk": null, "events": 2 }, { @@ -37,6 +39,7 @@ "director": "Bash · grep -oE '\"(permissions|env|allow|deny)\"|[A-Za-z_]*(TOKEN|K… · 1 error", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -46,6 +49,7 @@ "director": "Advisor · a second opinion", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -55,6 +59,7 @@ "director": "Write ×3 · timer.js +42 · timer.test.mjs +74 · index.html +120", "balloon": null, "tests": null, + "roundsOk": null, "events": 3 }, { @@ -67,6 +72,7 @@ "passed": 7, "failed": 0 }, + "roundsOk": null, "events": 1 }, { @@ -76,6 +82,7 @@ "director": "git commit -q -m \"feat: add pomodoro timer with tests\" -m \"…", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -85,6 +92,7 @@ "director": "Advisor · a second opinion", "balloon": null, "tests": null, + "roundsOk": null, "events": 1 }, { @@ -94,6 +102,7 @@ "director": "turn_duration · 2 min 39 s · 10 tool calls · 3 files changed", "balloon": "I built the pomodoro timer.", "tests": null, + "roundsOk": null, "events": 0 } ] diff --git a/test/story-corpus.test.ts b/test/story-corpus.test.ts index db2059b6..1eee78d0 100644 --- a/test/story-corpus.test.ts +++ b/test/story-corpus.test.ts @@ -46,6 +46,7 @@ function pagesOf(name: string) { director: x.caption.director, balloon: x.balloon?.text ?? null, tests: x.tests ?? null, + roundsOk: x.roundsOk ?? null, events: x.events.length, })), }; @@ -68,9 +69,11 @@ describe('Story Mode on real sessions', () => { const panel = (name: string, turn: number, kind: string) => pagesOf(name)[turn].panels.find((p) => p.kind === kind); it('a fix, a failing test (exit code hidden by a pipe), a fix and a pass fold into one loop', () => { - expect(panel('python-cli-fix-loop', 0, 'loop')?.story).toBe( - 'Fix and test: the first run fails, the second passes.' - ); + // Three runs in two rounds: the failing test asked for, then ruff, then a pass (the marks: ✗ ✗ ✓). + expect(panel('python-cli-fix-loop', 0, 'loop')).toMatchObject({ + story: 'Fix and test, 2 rounds: 2 runs fail, then it passes.', + roundsOk: [false, false, true], + }); }); it('a prompt typed mid-turn (a queued_command attachment) is drawn where Claude took it in', () => { diff --git a/test/story-mode.browser.test.ts b/test/story-mode.browser.test.ts index d7e5ad35..61bcfabd 100644 --- a/test/story-mode.browser.test.ts +++ b/test/story-mode.browser.test.ts @@ -138,6 +138,19 @@ describe('Story Mode in a real browser', () => { return main.scrollWidth - main.clientWidth; }); expect(overflow).toBe(0); + // The bar keeps to three rows, and a panel below the fold gets its buttons without a scroll. + expect(await page.evaluate(() => (document.querySelector('.story-bar') as HTMLElement).offsetHeight)).toBeLessThan( + 190 + ); + await page.waitForFunction( + () => { + const p = document.querySelector('.story-panel[data-kind="loop"]') as HTMLElement; + const main = document.querySelector('.story-main') as HTMLElement; + return p.getBoundingClientRect().top > main.getBoundingClientRect().bottom && !!p.querySelector('.story-hit'); + }, + null, + { timeout: 10000 } + ); await page.evaluate(() => (window as any).app.closeStoryMode()); await page.setViewportSize({ width: 1400, height: 950 }); });