feat(runner): DEV-2159 preview pane chrome + boot / refresh / error states (T5) - #97
Merged
Merged
Conversation
demtario
added a commit
that referenced
this pull request
Jul 31, 2026
Addresses bugbot on PR #97. The pattern was `/\[[0-9;]*m/`, which omits the ESC byte: it left an orphan `\x1b` wherever it removed a colour code, and it matched a literal `[31m` in ordinary prose. `container.ts:228` has always used the correct `\x1b\[…m` form. The line is unchanged from before this branch, so the PR did not introduce it — but it is newly load-bearing. `emitProgress` streams the log raw (container's own strip is on the failure path only), and T5 promotes the newest line from a collapsed block to an always-visible single row, where the artefacts show. Widened to full CSI rather than colour alone: pnpm and vite redraw progress with erase-line and cursor-move codes (`\x1b[2K`, `\x1b[1G`), which a boot log is full of. Carriage returns get the same treatment — a line rewritten with `\r` should read as whatever it ended up as, not as every frame concatenated. Before / after on a realistic pnpm+vite fragment: old: "\x1bProgress\x1b: resolved 12\r\x1b[2K\x1b[1GProgress: resolved 340" new: "Progress: resolved 340" Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tates T5. The preview column gets the bottom status bar the design puts there (`48:6701`): `● ready` on the left, the project label and Handsontable version on the right, on the same 28px band as the editor's status bar. T2 removed the old full-width accent strip on the grounds that `● ready` belonged here, so until now a running preview reported nothing at all. The bar renders as a sibling of `PreviewPane`, not inside it. Every overlay in that section is `inset: 0`, so a bar inside would be painted over by the boot, error and refresh overlays — which is why the pre-T2 strip had to buy its way out with `inset: "28px 0 0 0"` on each one. As a sibling it also lands in the same band as `EditorStatusBar` with no arithmetic, and `s.paneStatusBar` is shared by both so they cannot drift. `data-preview-status` and `aria-label` stay on the section: the starter matrix polls the first and the docs suite selects on the second. Both bars sit on `editorBg` (#ffffff / #19191c, measured). `previewBg` is a step darker in dark mode — the frames paint this band lighter than the preview surround, not level with it. See open item 7. `reload()` now returns a promise that settles when the refresh has landed, which is what lets the pane blank behind a spinner and recover (`72:26445`). Container resolves on the reloaded page's `load`; Sandpack on the bundler's next `done`, settled at the top of that branch so a compile error settles it too rather than waiting out the timeout. Both also settle on a 10s timeout and on `dispose()`, so no spinner can outlive what it was waiting on. It never rejects: failure already has `onError`. The Tier-2 boot log stays — it is the only signal when a container is slow or stuck, and the design omitting it is a gap, not a removal. The overlay leads with the design's spinner and "Loading data …", then adds the wait explanation, the newest log line, and the tail behind a `Details` disclosure. Gated on the engine rather than on the log being non-empty: the session POST is what stalls when the container pool is full, so the window where the explanation matters most is exactly the window with no log to infer it from. `syncing` stays a non-blocking corner badge. It fires on Tier-2 keystroke bursts, and blanking the grid on every keystroke would be worse than a badge in the corner. Error state has no frame, so T9's rule applies: a tokenized card with a `danger` heading and the message in a mono block. Also here: `Spinner` extracted from `PreviewPane` (four callers now) with its keyframes hoisted into `THEME_CSS`, since `editor-shell` cannot reach the app's global stylesheet; `Splash` rebuilt to `72:14610`; and the orphaned `s.statusBar()` deleted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Addresses bugbot on PR #97. The pattern was `/\[[0-9;]*m/`, which omits the ESC byte: it left an orphan `\x1b` wherever it removed a colour code, and it matched a literal `[31m` in ordinary prose. `container.ts:228` has always used the correct `\x1b\[…m` form. The line is unchanged from before this branch, so the PR did not introduce it — but it is newly load-bearing. `emitProgress` streams the log raw (container's own strip is on the failure path only), and T5 promotes the newest line from a collapsed block to an always-visible single row, where the artefacts show. Widened to full CSI rather than colour alone: pnpm and vite redraw progress with erase-line and cursor-move codes (`\x1b[2K`, `\x1b[1G`), which a boot log is full of. Carriage returns get the same treatment — a line rewritten with `\r` should read as whatever it ended up as, not as every frame concatenated. Before / after on a realistic pnpm+vite fragment: old: "\x1bProgress\x1b: resolved 12\r\x1b[2K\x1b[1GProgress: resolved 340" new: "Progress: resolved 340" Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
demtario
force-pushed
the
feat/DEV-2159-preview-status
branch
from
July 31, 2026 08:12
50f1e0b to
4b5664b
Compare
Addresses bugbot on PR #97. `ContainerRuntime.reload()`'s settler deletes itself from the set; Sandpack's did not, so one that fired from the timeout stayed registered. No behaviour change today: `settleReload()` empties the set wholesale and calling a settler twice resolves an already-resolved promise. What it fixes is the set growing for as long as reloads keep timing out with no `done` behind them, and the fact that leaving stale entries in there quietly depended on `settleReload()` never becoming selective about which waiters it settles. `settle` also becomes an arrow so it can reach the set — a `function` declaration's `this` is not the instance — with the timer handle hoisted above it rather than relying on declaration hoisting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a6b130d. Configure here.
…t on `done` Addresses bugbot on PR #97, and in chasing it found the bigger problem the report was pointing at. Bugbot's case was real: `reload()` armed its settler before `pushUpdate`, which awaits a transpile before dispatching, so a `done` from a compile already in flight — a keystroke a moment earlier — could settle a refresh whose files had not been sent. Arming after dispatch instead does not work, and measuring it is what exposed the real issue. The `done` for our own update can land in the same turn the dispatch promise resolves, so arming late loses the race: the refresh then sat out the full 10s timeout (measured 10.07s to clear). Gating a registered-early waiter on a `dispatched` flag fixed the ordering and *still* timed out at 10.02s — because no `done` ever arrives at all. Verified in a browser: after a refresh click the parent window sees no messages whatsoever for 11s, while a `done` on mount arrives normally and drives emitReady(). So the listener is fine and this specific call goes unanswered, which means the promise never resolved early and the shell blanked the pane and held a spinner over it for a full 10 seconds on *every* Tier-1 refresh. My earlier "cleared at ~3.1s off the real done" was a bad measurement, taken across two tool round-trips. So Sandpack now settles on the work we actually do and can time — transpile plus handing the update to the bundler. Measured 48/50/62ms on a warm React starter, three consecutive runs, no timeouts. The waiter set, the `settleReload()` hook and the timeout all go with it; Container keeps its real `load` signal and its backstops. Open item 26 rewritten to record the measurement, and to name what is still unknown: whether `updateSandbox(setup, true)` recompiles at all on an unchanged file set. If it no-ops then the Tier-1 refresh button does nothing and the silence is a symptom rather than the bug — which would make T2's "isInitializationCompile re-runs the sandbox" claim wrong. That predates T5 and wants its own ticket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

T5 of the DEV-2027 redesign. DEV-2159 · plan:
runner/docs/dev-2027-redesign-plan.md· ADR-0023.Frames:
48:6560(ready),72:26445(refresh in flight),72:14610(loading),48:6701/72:15699(the bar itself).Why
T2 deleted
PreviewPane's full-width accent strip on the grounds that● readybelonged in T5's bottom bar — so until now a running preview reported nothing at all, ands.statusBar()sat instyles.tsstyling nothing. T2 also addedDemoRuntime.reload()and the row-2 refresh button, but pressing it produced no feedback whatsoever. This closes both.The bar is a sibling of
PreviewPane, not a childThe load-bearing decision. Every overlay in that section is
position: absolute; inset: 0, so a bar rendered inside it is painted over by the boot, error and refresh overlays — which is exactly why the pre-T2 top strip had to buy its way out withinset: "28px 0 0 0"on every overlay. As the preview column's last child it also lands in the editor's band with no arithmetic: measured flush attop: 734, height: 28, with T6's 1px splitter track between them.48:6701and48:6740are both y=800 h=28 inside their own column frames, which is that band.Both bars share
s.paneStatusBar— sharing the object is what stops them drifting apart. It sits oneditorBg(#ffffff / #19191c, exact).previewBgis wrong here: #070604 in dark, a step darker, where the frames paint this band lighter than the preview surround. Extends open item 7.data-preview-statusandaria-label="Preview"stay on the<section>and the iframe stays a descendant —e2e/starter-matrix.spec.ts:144polls the first,e2e/docs-examples.spec.ts:305selects on the second. Both re-verified in the DOM after the restructure. Please don't tidy them away.reload()now returnsPromise<void>types.ts,container.ts,sandpack.ts. It is what the refresh spinner waits on, and it never rejects — failure already hasonError.The two tiers settle on genuinely different things, and the difference is not cosmetic:
load.srcnavigation is what a refresh is, andloadfires on the frame element regardless of origin. 10s timeout anddispose()as backstops.done, because the bundler sends nothing back for anupdateSandbox(setup, true).The boot log stays, gated on the engine
The design shows only a spinner and "Loading data …" and omits the live install/dev-server log. That log is the only signal when a container is slow or stuck, so it is a gap in the design, not a removal.
My first cut inferred the tier from
bootLogbeing non-empty. That read correctly until a wedged container pool madePOST /api/sessionhang for 100s+ — and the log only starts arriving after that request returns, so the window where the user most needs to be told the wait is expected is the window with no log to infer it from.containerBootis now an explicit prop. Tier 1 gets the designed spinner alone; Tier 2 adds the wait explanation, the newest log line (Preparing container…until the first arrives), and the tail behind aDetailsdisclosure.Detailsis a button plus a chevron rather than<details>/<summary>: hiding the native marker needslist-style: noneand::-webkit-details-marker { display: none }, both pseudo-element rules inline styles cannot express, andeditor-shellmay not reach the app's global block — the same constraint that movedhot-spin's keyframes intoTHEME_CSS.syncingstays the non-blocking corner badge, restyled. It fires on Tier-2 keystroke bursts; blanking the grid per keystroke is worse than a badge in the corner.refreshingsuppresses it, since the pane is already blank behind the refresh spinner.Error state has no frame, so T9's rule applies: a tokenized card with a
dangerheading and the message in a mono block.Also here
Spinnerextracted fromPreviewPane(four callers now) with its keyframes hoisted intoTHEME_CSS;Splashrebuilt to72:14610;s.statusBar()deleted.frameworkNameis its own prop rather than a repurposedframeworkLabel— for a docs exampleentry.displayNameis the long `"Columns ▸ … · Standard example · React (TS)"` breadcrumb, so the short label resolves through the starter catalog by framework key (.find, notgetEntry, which throws).Verification
pnpm typecheckclean across all four workspace projects.Browser (
runner:verify), both light and dark:● ready·React (Vite, TS)·Handsontable 18.0.0; bars flushPreparing container…Detailsexpands the tailpreviewBg, bar still● ready, clears in 48–62ms (3 runs)● errorFull suite: 25 passed / 80 skipped with
E2E_LIVE=1, against this branch's build on a dedicated port. Re-run after rebasing onto T7 (#96), whosedocs-examples.spec.tsrewrite this branch now sits on top of. The 80 skips arestarter-matrix.spec.ts, gated onE2E_STARTER_MATRIX=1. Open item 17 notes a default run skips everylive:spec, soE2E_LIVE=1is the only setting that proves anything about preview mount/teardown.Not run:
starter-matrix.spec.ts(gated onE2E_STARTER_MATRIX=1, iterates every framework × major). Itsdata-preview-statuscontract was asserted directly in the DOM instead.Open items logged, not resolved
Renumbered 26–29 in the rebase onto T7, which had taken 20–25:
26 — Tier 1 has no refresh-completion signal, and may not recompile at all (includes what a follow-up should not re-derive) · 27 — the version renders twice in
playmode (no frame shows the collision;48:6560is a saved demo with no version menu) · 28 —72:14610draws chrome above a splash that renders before the shell exists · 29 —README.mdlistsrunner/apps/viewer/, which does not exist.🤖 Generated with Claude Code
Note
Medium Risk
Touches preview lifecycle and runtime reload semantics (Sandpack vs container); behavior changes are UX-visible but isolated from auth and persistence.
Overview
T5 (DEV-2159) adds the designed preview bottom bar and rebuilds boot, refresh, and error UI. Readiness moves from the removed top accent strip to
PreviewStatusBar(●status, short framework label,Handsontable {version}), placed as a sibling ofPreviewPaneso full-pane overlays do not cover it.PreviewPanenow uses sharedSpinner, tier-aware boot (containerBoot+ optional install log behind Details), apreviewBgrefresh overlay, restyled syncing pill, and a tokenized error card. Boot log parsing strips full ANSI/\rprogress lines.s.paneStatusBaris shared with the editor status bar;s.statusBar()is deleted.DemoRuntime.reload()returnsPromise<void>(never rejects). Authoring drives arefreshingspinner with a sequence guard; remounts cancel in-flight refresh. Container reload settles on iframeload(10s timeout /dispose). Sandpack settles on transpile +updateSandboxdispatch because refresh does not emitdone.Authoring
Splashmatches the loading frame;frameworkNameis resolved from the starter catalog for docs examples. Plan docs log open items 26–29.Reviewed by Cursor Bugbot for commit c6c4631. Bugbot is set up for automated code reviews on this repo. Configure here.