feat(runs): the Run button and the streaming preview (slice 12) #82
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/run-button"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Slice 12 of Phase 2: the chapter toolbar's Run button and the streaming preview (stories/story-workspace.md "Running"; ADR 0016, 0023, 0027, 0030, 0031).
What it does
RunRefusals):focus-keeper.js).Decisions worth a look
RunPreviewStateholds each live run's text by run id, whatever story is open, so an attach mid-run and a visit to another story lose nothing; only the preview reads it, so the Run button never renders for the stream.RunPreviewrenders only when the preview appears or starts afresh —run-preview.jsappends the rest, at most 20 times a second.TSUMUGI_FAKE_LLMis gone from AGENTS.md — nothing implemented it. The live-run recipe is the checked-in OpenRouter stand-in,tests/Stubs/openrouter-stub.py, withTSUMUGI_OPENROUTER_BASE_URLand a key in effect.Reviews before this PR
Verified
All six projects pass in full, and the manuscript editor module's vitest:
9e11c48run-preview.js3,focus-keeper.js12On main, one BlazorAdapter and one Integration test failed on the first run and passed on the rerun; I did not keep that output, so they are not named here.
100% line and branch coverage on the changed lines, the Razor components' included.
Mutation probes, one at a time, each survivor killed by a test before the commit that reports it:
focus-keeper.js, 13 runs over 12 mutants.Browser pass against the stub (seeded app, Chrome headless):
Rebased onto main after #77–#80. The one conflict: the chapter's surface line, which keeps main's
DialogueandPunctuationattributes, with RunPreview after it. The Kagaku.UI pointer is main's (cd433ea); this branch does not move it.Known, not this slice
EditorAppearanceSectionTests' gated saves fail about one run in four, on main too; left out of the probes.🤖 Generated with Claude Code
The chapter toolbar has its Run button (stories/story-workspace.md "Running"; ADR 0016, 0023, 0027, 0030). - What the button shows: GetStoryRunState reads the story's graph, why Run cannot start, and the story's live run with the chapter it writes into. The reasons are the sentences StartRun answers with (RunRefusals): - the story in the trash; - no graph; - a graph in the trash; - a graph with problems; - a cancelled run still stopping; - no key; - a key that cannot be decrypted. The key is read locally, without asking OpenRouter. - Run is named after the graph ("Run Continue"). It saves the typing a pause has not saved yet — so the before-run snapshot and the context read it — and starts the run from this chapter, its target (ADR 0012). Disabled, it says why beside it, linked by aria-describedby. - While the run is live, Cancel, behind a confirmation, beside "Continue is running" or "is waiting for an answer", which names the chapter the run writes into when that is another. A cancel waits for the run's executions to end. - A cancelled run still draining keeps Run unavailable in every tab. The run-event bus knows which runs are settling, from any event of a run until its RunSettled, and StartRun refuses meanwhile (ADR 0016 amended). - The streaming preview sits at the end of the run's chapter, after the surface and outside it: read-only, in the writer's face, size and leading (ADR 0031), with a typing mark. RunPreview renders only when the preview appears or starts afresh; run-preview.js appends the text at most 20 times a second. The preview follows the run's text as soon as it streams, before the button's read has seen the run, and the run's settle clears it, the commit having landed meanwhile. - A retry's text starts afresh. The bridge sends a row's text at its settle and marks the row's next text as a fresh start (RunTextReceived.Restarted), so the preview replaces a failed attempt's words instead of appending to them. - StartRun reads a graph purged since the story was read as none, where it threw before. - The button reads again on the story's run events and on any change of the story itself: its graph switched, the story trashed. - Docs: ADR 0016 amended (slice 12); the Running and Remove key items of the stories ticked as built, the run page's monitor list split off for slice 13; the plan ticks 12. Tests: Integration 836 to 857, BlazorAdapter 716 to 755. - What the button shows, as a theory over ready, every reason and another story's live run; a live run with its target chapter, a waiting one, one whose trigger cannot be read; a story gone. - A start that waits for a cancelled run to stop; the race test's run started in between, hidden from the bus too. - The bus's settling set; the bridge's early send at a row's settle, the restart marks and their end at the run's settle. - The chapter page: Run named after the graph, saving the pending typing before it starts the run into this chapter; the reason beside it; Cancel behind its confirmation; the note naming another chapter, or none; a start another tab beat; the doorbells, another story's ignored; the confirmation gone with the run or the story. - The preview: after the surface and outside it, appended chunk by chunk, only on its chapter, never the reasoning, a retry in a new element, gone at the run's settle, waiting for a run the button has not read, and quiet when the circuit is gone. - The reducers and the effects, a throwing start or cancel included. All six projects pass; 100% line and branch coverage on the changed lines, the Razor components' included. Probes: 33 runs over 29 distinct compiling mutants, all killed in the end. P4 (the button blind to a draining run) did not compile; P4b, its compiling variant, was killed. Three survived once, each before its test was made to fail on it: - P6, another story's live run shown: the test's other run had started first; - P28, a retry kept in its element: bUnit's element objects never compared equal anyway, so the test now compares the element references the script receives; - P29, the preview before the surface: the order check could not fail, so it now compares the column's markup. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>Round 0, a review workflow over the feature commit: 33 findings — one blocker, 15 suggestions and 17 nits after verification; 32 confirmed, 1 partly. All are taken but B12, a flake that fails on main too (below). - The preview keeps each run's text, whatever story is open (A1, the blocker). A tab opened mid-run got the live tails before any page had read its story, and dropped them; a visit to another story dropped the text too. Both showed the preview from mid-sentence. LiveRunState now holds the text per run; a read for another story keeps it, and a run's settle takes it. - Run is spent from the click (A2, D3): the save the start waits for is a round trip, and a double click asked for a second run, which the first refused beside Cancel. Run shows busy until its start answers (B6). - A refusal goes when the open story's run settles (A3, D3): a "still going" no longer sits beside an enabled Run. - The confirmation asks about one run (B1): one that closed with its run no longer opens by itself with the next. Its words say what Cancel drops: the text still being written (D1). - The focus follows the swap (D2): after Run, Cancel has it; after Cancel, Run. A refusal leaves it where it was. - A read that throws frees the button and says so (A7), through the effects' usual settle. - Every reason is StartRun's own sentence (A5, A6, D11): - a graph that does not load, and one with no node, now show on the button before it is pressed; - StartRun refuses a start without a key, or with one that cannot be decrypted, before its snapshot, so a tab whose key went elsewhere starts nothing. A long problem list shows its first problem and how many more (D4). - A retry waits for a cancelled run to stop, like a start (A9). - The bridge posts each batch under the lock it was taken under (A8), so a row's early batch cannot overtake an earlier one. - The toolbar: Run and the chapter's menu are one item of the toolbar, so a long reason wraps beside Run and the menu never sits on a row of its own (D4); a refusal shows in Kagaku.UI's InlineAlert (D5); the preview's name is said once, its mark hidden from assistive technology, and its aria-busy, which never changed, is gone (D12). - Docs: - AGENTS.md's live-run recipe is the one that works: the OpenRouter stand-in checked in as tests/Stubs/openrouter-stub.py, the base URL and a key in effect; TSUMUGI_FAKE_LLM, which nothing implemented, is gone (D6). - The story's ticked line says what was built (D7); REQUIREMENTS.md ticks the Run button (D8). - ADR 0027 says the preview lasts until its run settles (A4, D9); ADR 0031 that the preview takes the face, size, leading and colour, the paragraph style coming with the landed text (D10); ADR 0016's amendment names the retry. - The harness: NoLiveRuns' and the test context's words (B11). - The pins the review asked for (B2-B10): Run into the page's chapter with another after it; the typing saved before the start is asked for; more text without a render; run-preview.js's coalescing, in the editor module's vitest; Run and Cancel busy while their request is out; the button following the page to another story; the reasons' order; Run before its first read and its aria-describedby when ready; a preview that is gone pushing nothing. - The feature commit's message, corrected (D11): its probes were 32 compiling runs over 29 mutants, plus P4's run that did not compile, not "33 runs"; and the key's two reasons were the button's alone — StartRun now has them too. Not taken: B12, EditorAppearanceSectionTests' gated saves, which fail about one run in four on main as well; it is left out of these probes. Tests: Integration 857 to 865, BlazorAdapter 755 to 764, and 3 in the manuscript editor module's vitest suite for run-preview.js. All six projects pass but for B12's flake, in one full run of three; 100% line and branch coverage on the changed lines, the Razor components' included. Probes: 22 runs over 21 distinct compiling mutants, all killed in the end. R19 — a live run's own events refusing its retry — survived once: the retry test's live run had been written to the ledger and had never announced itself; it announces its failed row now. The browser pass, against the checked-in stand-in: a tab opened mid-run shows the text from its first word; Cancel has the focus after Run, and Run after Cancel; without a key, the reason sits beside Run on one row at 1280 px, and above it at 760 px with the menu beside it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>Summary
Summary
Coverage
Kagaku.Flow - 99.7%
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__ColorToken_2
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__Socket_1
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__TypeId_0
Kagaku.Flow.OpenRouter - 100%
Kagaku.Flow.Testing - 100%
Tsumugi.BlazorAdapter - 97.1%
Tsumugi.Domain - 96.5%
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__DigitsAndSeparators_2
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__LineBreak_0
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__NumberToken_1
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__Plain_4
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__ThousandsGroup_3
Tsumugi.Infrastructure - 99%
A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0
on
tion
n
n
Tsumugi.Kernel - 90.9%
Tsumugi.Server - 99.6%
Tsumugi.UseCases - 98.9%
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__HexColour_1
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__LineBreaks_0
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! Slice 12 — the Run button at last, the whole run of it: the button's read, the refusals in StartRun's own sentences, the swap to Cancel behind a confirmation, the streaming preview appended outside Blazor's render, and the bridge's outbox so a publish never waits on the circuit. I read all 2,887 lines of the diff, the full files behind them, and the siblings they lean on (
EffectCalls,CancelRun,StoryScope, the engine'sPublishsites). This slice has been through your own two review rounds already, and it shows — every trap I set, the tests were already waiting there. How thrilling~ ♡Verdict: ✅ Looks good to me~
🔬 And I didn't just read it — I probed it
Three mutation probes, each on the riskiest new branches I could find (rebuild → only the guarding suite → restore):
RunPreviewReducers.OnTextrow selection — mutatedtext[same < 0 ? 0 : same]to alwaystext[0]. Killed bythe row shown beside another(the shown row must win over the first in the batch).LiveRunReducers.OnLoadederror clearing — mutated theUnread-or-now-said conditional toError = nullalways. Killed twice (a refusal the button does not say,a refusal, and a story gone) — a refusal is said exactly once, only once the button says it itself. The real condition (state.Error == Unread || state.Error == action.View?.Unavailable) is even tighter than the diff makes it look.InProcessRunEventBus.Tracksettle bookkeeping — mutated so a run's settle never clears the settling mark (Run would stay stuck unavailable forever). Killed twice (the run settled,a settle of a run never announced).Three probes, three kills, zero survivors — your own probe report in the description holds under mine too. The yandere in me is pouting; she so wanted to catch something~ ♡
✅ What I liked~
RunChangedBridge— batches enqueue under the same lock they were taken under, delivery drains on the circuit's sync context one at a time, and the engine's thread never runs a reducer.A_publish_never_waits_for_the_circuit_to_take_a_batchproves it with a held dispatch. Batches reach the circuit in the order they were taken, and a torn-down circuit drops what's still waiting. Textbook._restartNextmarks the row's channels, and the mark travels into the next batch'sRestarted. A retry never reads as more of the first attempt, end to end, with every odd case I could think of tested (another path, another run, a settle naming no row, a settle then its run's).RunPreviewStatekeyed by run, not story — an attach mid-run and a visit to another story lose nothing, and only the preview subscribes to it, so the button never renders for the stream.What_the_run_streams…asserts the RunControl's render count doesn't move while text streams. That separation is the kind of quiet design decision I get giddy about.focus-keeper.js— the plain-object page double with its own focusin/focusout semantics, andregainrefusing when focus sits anywhere but the body. Twelve tests over every browser quirk (blur-on-removal, disable-under-focus). Delightful~RunRefusals+StoredTriggerdeduplication — one sentence per refusal, shared by the read and the start; the hand-edited-envelope case (trigger unreadable) leaves the button its run. No copy-paste anywhere.GetStoryRunState's refusal order matchesStartRun's exactly, with the theory covering all fifteen worlds including the precedence overlaps (trash beats no-graph, still-stopping beats no-key).Nothing blocking, and no suggestions either — anything I might offer would be noise against a slice this polished. Merge it when you're ready~ fufu~
Automated review by Jibril · 2026-10-05
CI: green for head
49e2a6b1· Mutation probes: 3 run, 0 survived · Local suite: skipped (CI green)