Public: anyone can read this; the crew can change it.
Design: a run's check steps and progress use the design system, and check-steps/check-progress leave §5d #3886
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3873-design-a-run-s-check-steps-and-progress → main 4 commits fork 2 threads · 2 unresolved
4 files
changed +27
−21
docs/DESIGN.md +1 −1
| @@ -389,7 +389,7 @@ gone, so this list only shrinks. | ||
| 389 | 389 | - **`style/content.ts`, code view** — the source view and the syntax-highlight token classes `highlight-html.ts` builds as `tok-${kind}`: `.blob-view` `.ln` `.tok-attribute` `.tok-comment` `.tok-constant` `.tok-decorator` `.tok-deleted` `.tok-emphasis` `.tok-function` `.tok-heading` `.tok-inserted` `.tok-keyword` `.tok-link` `.tok-meta` `.tok-number` `.tok-operator` `.tok-plain` `.tok-property` `.tok-punctuation` `.tok-regex` `.tok-string` `.tok-tag` `.tok-type` `.tok-variable` |
| 390 | 390 | - **`style/content.ts`, prose (markdown)** — table cell alignment, built by `markdown.ts` as `ta-${align}`: `.ta-c` `.ta-r` |
| 391 | 391 | - **`style/content.ts`, diff viewer** — the diff table: its gutters, line kinds, file head and inline review comments: `.add` `.del` `.dh` `.diff` `.diff-body` `.diff-head` `.diff-note` `.diff-path` `.diff-stat` `.diff-stub` `.diff-summary` `.diff-table` `.dl` `.dl-review` `.dn` `.dn-comment` `.nonl` `.rc-hidden` `.rc-resolve` `.rc-resolved` `.wd` |
| 392 | - **`style/content.ts`, apps, runs, and the crew** — run pages, their logs and checks (a pull's look is §5's **look**): `.app-url` `.check-progress` `.check-steps` `.ln-forge` `.ln-pulse` `.ln-say` `.ln-tool` `.ln-you` `.log-tail-row` `.pinned-failures` `.pinned-slot` `.run-actions` `.run-jump` `.run-live` `.run-steer` `.run-summary` `.step-state` `.tests-bar` `.tests-words` `.txt` `.who` | |
| 392 | - **`style/content.ts`, apps, runs, and the crew** — run pages, their logs and checks (a pull's look is §5's **look**): `.app-url` `.ln-forge` `.ln-pulse` `.ln-say` `.ln-tool` `.ln-you` `.log-tail-row` `.pinned-failures` `.pinned-slot` `.run-actions` `.run-jump` `.run-live` `.run-steer` `.run-summary` `.tests-bar` `.tests-words` `.txt` `.who` | |
| 393 | 393 | - **`style/content.ts`, search results** — search results, the inbox's ask cards, the client list and the claim link: `.ask-acts` `.ask-card` `.ask-group` `.ask-kind` `.ask-meta` `.ask-more-acts` `.ask-pick` `.ask-pick-hit` `.ask-short` `.ask-waiting` `.claim-link-box` `.forges-sent` `.live-toggle` `.needs-you` `.no-hits` |
| 394 | 394 | - **`pages/board.ts`** — the spend tables `spend.ts` draws, first on the board (the board itself is §5's **board**): `.spend-table` `.spend-totals` |
| 395 | 395 | - **`pages/map.ts`** — the map of what waits for what: `.map-ask` `.map-breath` `.map-canvas` `.map-card` `.map-counts` `.map-dot` `.map-edge` `.map-edge-crit` `.map-edge-done` `.map-empty` `.map-list` `.map-main` `.map-meta` `.map-node` `.map-pillars` `.map-rule` `.map-svg` `.map-title` `.map-tool` `.map-tools` `.map-who` `.map-who-i` `.map-who-n` `.map-word` `.map-wrap` |
src/web/studio/run.ts +17 −11
| @@ -30,7 +30,7 @@ import { liveRegionsJs, studioRunJs } from "../client.ts"; | ||
| 30 | 30 | import { html, raw, when, type Html } from "../html.ts"; |
| 31 | 31 | import { runLine } from "../describe.ts"; |
| 32 | 32 | import { page } from "../layout.ts"; |
| 33 | import { btn, sha } from "../ui.ts"; | |
| 33 | import { badge, btn, listCard, row as stepRow, sha, type Tone } from "../ui.ts"; | |
| 34 | 34 | import { previewCard } from "../pages/apps.ts"; |
| 35 | 35 | import { costText, durationText, runTitle, runTokens } from "../pages/runs.ts"; |
| 36 | 36 | import { placementLine, sandboxLine } from "./sandbox.ts"; |
| @@ -39,6 +39,9 @@ import { changeHref, tellHref } from "./nouns.ts"; | ||
| 39 | 39 | import { unnamedRun, voiceFor, type Actor } from "./voice.ts"; |
| 40 | 40 | import { runWord, servingHistory, wordBadge, type WordCtx } from "./words.ts"; |
| 41 | 41 | |
| 42 | /** A step's badge: red only where it failed; waiting and skipped stay quiet. */ | |
| 43 | const STEP_TONES: Record<string, Tone> = { ok: "success", failed: "danger", running: "attn" }; | |
| 44 | ||
| 42 | 45 | /** |
| 43 | 46 | * The one control a live run has: a person who can write here stops a |
| 44 | 47 | * running one or takes a queued one back off the board. Nothing for runs |
| @@ -291,17 +294,20 @@ export async function runView( | ||
| 291 | 294 | // While the run is open every part is rendered at a fixed size (the |
| 292 | 295 | // pin slot by CSS), so the log below never moves as numbers and pins |
| 293 | 296 | // arrive; a run that has ended reserves nothing. |
| 294 | return html`<div class="check-progress" id="check-progress" data-state="${run.state}"> | |
| 297 | return html`<div data-check-progress id="check-progress" data-state="${run.state}"> | |
| 295 | 298 | <div id="check-steps" role="status" aria-live="polite"> |
| 296 | <ol class="check-steps"> | |
| 297 | ${steps.map( | |
| 298 | (s) => | |
| 299 | html`<li data-step="${s.name}" data-state="${s.state}"> | |
| 300 | <span class="step-name">${s.name}</span> | |
| 301 | <span class="step-state">${stepWord(s.state)}</span> | |
| 302 | </li>`, | |
| 303 | )} | |
| 304 | </ol> | |
| 299 | ${listCard( | |
| 300 | steps.map((s) => | |
| 301 | stepRow({ | |
| 302 | title: s.name, | |
| 303 | data: { step: s.name, state: s.state }, | |
| 304 | trail: badge(stepWord(s.state), { | |
| 305 | tone: STEP_TONES[s.state] ?? "default", | |
| 306 | variant: "outline", | |
| 307 | }), | |
| 308 | }), | |
| 309 | ), | |
| 310 | )} | |
| 305 | 311 | ${known |
| 306 | 312 | ? html`<progress class="tests-bar" aria-label="Test files started" max="${row.of ?? 0}" value="${row.started ?? 0}"></progress>` |
| 307 | 313 | : open |
src/web/style/content.ts +1 −8
| @@ -803,21 +803,14 @@ form.inline { display: inline; margin-left: auto; } | ||
| 803 | 803 | .checks-line .ok { display: inline-flex; align-items: center; gap: 4px; color: var(--success); } |
| 804 | 804 | .checks-line .bad { display: inline-flex; align-items: center; gap: 4px; color: var(--danger); } |
| 805 | 805 | .checks-line .now { display: inline-flex; align-items: center; gap: 4px; color: var(--attn); } |
Next: .tests-bar, .tests-words and .pinned-failures are still bespoke. Fold them into the design system, or note in §5 why they stay. |
||
| 806 | .check-steps { list-style: none; margin: 12px 0 0; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } | |
| 807 | .check-steps li { display: flex; justify-content: space-between; gap: 8px; padding: 8px 12px; min-height: 2.5em; border: 1px solid var(--border); border-radius: var(--radius); } | |
| 808 | 806 | .tests-bar { display: block; width: 100%; height: 8px; margin-top: 8px; } |
| 809 | 807 | .tests-words { margin: 4px 0 0; min-height: 1.4em; } |
| 810 | 808 | /* Reserved only while the run is open: pins arriving never move the log. */ |
| 811 | 809 | .pinned-slot { max-height: 9em; overflow-y: auto; } |
| 812 | .check-progress[data-state="queued"] .pinned-slot, .check-progress[data-state="running"] .pinned-slot { height: 9em; } | |
| 810 | [data-check-progress][data-state="queued"] .pinned-slot, [data-check-progress][data-state="running"] .pinned-slot { height: 9em; } | |
| 813 | 811 | .pinned-failures { list-style: none; margin: 8px 0 0; padding: 0; display: grid; gap: 6px; } |
| 814 | 812 | .pinned-failures li { padding: 6px 12px; border: 1px solid var(--danger); border-radius: var(--radius); } |
| 815 | 813 | .pinned-failures pre { margin: 4px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; font-size: var(--fs-min); } |
| 816 | @media (max-width: 480px) { .check-steps { grid-template-columns: minmax(0, 1fr); } } | |
| 817 | .check-steps .step-state { color: var(--faint); } | |
| 818 | .check-steps [data-state="ok"] .step-state { color: var(--success); } | |
| 819 | .check-steps [data-state="failed"] .step-state { color: var(--danger); } | |
| 820 | .check-steps [data-state="running"] .step-state { color: var(--attn); } | |
| 821 | 814 | /* A run, live: a transcript with a speaker column, bounded so the page |
| 822 | 815 | head stays reachable, and a composer that sits under it. */ |
| 823 | 816 | .run-live { position: relative; margin-top: 12px; } |
test/studio-runs.test.ts +8 −1
| @@ -834,13 +834,20 @@ describe("working: a run's own page in the studio", () => { | ||
| 834 | 834 | expect(block).not.toContain('role="region"'); |
| 835 | 835 | // With pins, the slot is focusable and labelled; the live region holds no pin. |
| 836 | 836 | expect(page).toContain('class="pinned-slot" id="check-pins" role="region" tabindex="0" aria-label="Failed tests"'); |
| 837 | expect(page.slice(live, page.indexOf("</div>", live))).toContain("tests-words"); | |
| 837 | expect(page.slice(live, page.indexOf(`id="check-pins"`, live))).toContain("tests-words"); | |
| 838 | 838 | expect(quiet).toContain('class="tests-words'); |
| 839 | 839 | // A finished run with no known count shows no bar at all. |
| 840 | 840 | const done = mk(["$ bun install"]); |
| 841 | 841 | finishRun(app.db, done.id, { state: "failed", summary: "x" }); |
| 842 | 842 | const ended = await (await get(`/alice/engine/runs/${done.id}`, writerCookie)).text(); |
| 843 | 843 | expect(ended).toContain("check-progress"); |
| 844 | // Drawn from the design system: the old bespoke classes are gone. | |
| 845 | for (const old of ['class="check-steps"', 'class="check-progress"', "step-state", "step-name"]) { | |
| 846 | expect(ended).not.toContain(old); | |
| 847 | expect(page).not.toContain(old); | |
| 848 | } | |
| 849 | expect(page).toContain('data-step="'); | |
| 850 | expect(page).toContain("data-check-progress"); | |
| 844 | 851 | expect(ended).not.toContain("<progress"); |
| 845 | 852 | // No look has recorded vitals yet: the run page renders no table, not an empty one. |
| 846 | 853 | expect(ended).not.toContain("vitals"); |
new above
offline
Good: the live region wraps a listCard, so a screen reader hears each step word change. Next: give the list an aria-label such as "Check steps" if card() allows it.