Public: anyone can read this; the crew can change it.

A run page's pinned failures draw §5's card and row, and .pinned-failures, .pinned-slot leave the markup #4312

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/4264-a-run-page-s-pinned-failures-draw-5-s-ca → main 7 commits fork 5 threads · 5 unresolved

  • Merged. scout/4264-a-run-page-s-pinned-failures-draw-5-s-ca was merged into main as 42d30b1.

Review

Sign in to review this change.
4 files changed +41 −22
docs/DESIGN.md +1 −1
@@ -398,7 +398,7 @@ gone, so this list only shrinks.
398398 - **`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`
399399 - **`style/content.ts`, prose (markdown)** — table cell alignment, built by `markdown.ts` as `ta-${align}`: `.ta-c` `.ta-r`
400400 - **`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`
401 - **`style/content.ts`, apps, runs, and the crew** — run pages, their logs and checks (a pull's look is §5's **look**): `.ln-forge` `.ln-pulse` `.ln-say` `.ln-tool` `.ln-you` `.pinned-failures` `.pinned-slot` `.txt` `.who`
401 - **`style/content.ts`, apps, runs, and the crew** — run pages, their logs and checks (a pull's look is §5's **look**): `.ln-forge` `.ln-pulse` `.ln-say` `.ln-tool` `.ln-you` `.txt` `.who`
402402 - **`style/content.ts`, search results** — search results, the inbox's ask cards: `.ask-acts` `.ask-card` `.ask-group` `.ask-kind` `.ask-meta` `.ask-more-acts` `.ask-pick` `.ask-pick-hit` `.ask-short` `.ask-waiting` `.live-toggle`
403403 - **`pages/board.ts`** — the board itself is §5's **board**; the spend tables `spend.ts` draws wear §5's `.table` and `.stack`, no class of their own
404404 - **`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-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 +10 −12
@@ -30,7 +30,7 @@ import { liveRegionsJs, studioRunJs } from "../client.ts";
3030 import { html, raw, when, type Html } from "../html.ts";
3131 import { runLine } from "../describe.ts";
3232 import { page } from "../layout.ts";
33 import { badge, btn, listCard, row as stepRow, sha, type Tone } from "../ui.ts";
33 import { badge, btn, card, listCard, row as stepRow, sha, type Tone } from "../ui.ts";
3434 import { previewCard } from "../pages/apps.ts";
3535 import { costText, durationText, runTitle, runTokens } from "../pages/runs.ts";
3636 import { placementLine, sandboxLine } from "./sandbox.ts";
@@ -315,17 +315,15 @@ export async function runView(
315315 : ""}
316316 <p class="check-words small muted break">${[words, runningWords].filter((w) => w !== "").join(" · ")}</p>
317317 </div>
318 <div class="pinned-slot" id="check-pins"${row.failures.length > 0 ? raw(' role="region" tabindex="0" aria-label="Failed tests"') : ""}>
319 ${when(
320 row.failures.length > 0,
321 () => html`<ul class="pinned-failures">
322 ${row.failures.map(
323 (f) => html`<li><strong>${f.name}</strong>${when(
324 f.lines.length > 0,
325 () => html`<pre>${f.lines.join("\n")}</pre>`,
326 )}</li>`,
327 )}
328 </ul>`,
318 <div data-pins id="check-pins"${row.failures.length > 0 ? raw(' role="region" tabindex="0" aria-label="Failed tests"') : ""}>
outdated

data-pins and .stack have no height rule yet. The old .pinned-slot reserved 9em while the check ran and capped it with scroll afterwards. Add that on [data-pins] in this pull, and remove the dead .pinned-* rules.

319 ${row.failures.map((f) =>
320 card(
321 html`<strong class="break">${f.name}</strong>${when(
322 f.lines.length > 0,
323 () => html`<pre class="break">${f.lines.join("\n")}</pre>`,
324 )}`,
325 { tone: "danger" },
326 ),
329327 )}
330328 </div>
331329 </div>`;
src/web/style/content.ts +5 −5
@@ -712,11 +712,11 @@ form.inline { display: inline; margin-left: auto; }
712712 .checks-line .now { display: inline-flex; align-items: center; gap: 4px; color: var(--attn); }
713713 .check-words { margin: 4px 0 0; min-height: 1.4em; }
714714 /* Reserved only while the run is open: pins arriving never move the log. */
715 .pinned-slot { max-height: 9em; overflow-y: auto; }
716 [data-check-progress][data-state="queued"] .pinned-slot, [data-check-progress][data-state="running"] .pinned-slot { height: 9em; }
717 .pinned-failures { list-style: none; margin: 8px 0 0; padding: 0; display: grid; gap: 6px; }
718 .pinned-failures li { padding: 6px 12px; border: 1px solid var(--danger); border-radius: var(--radius); }
719 .pinned-failures pre { margin: 4px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; font-size: var(--fs-min); }
715 /* A plain block, not .stack: a flex column would shrink the clipped cards to fit 9em. */
716 [data-pins] { max-height: 9em; overflow-y: auto; }
717 [data-check-progress][data-state="queued"] [data-pins], [data-check-progress][data-state="running"] [data-pins] { height: 9em; }
718 [data-pins] .card + .card { margin-top: 12px; }

Local spacing rule on a card sibling; later move into a shared §5 stack variant that does not shrink children.

719 [data-pins] pre { margin: 4px 0 0; white-space: pre-wrap; font-size: var(--fs-min); }
720720 /* A run, live: a transcript with a speaker column, bounded so the page
721721 head stays reachable, and a composer that sits under it. */
722722 #run-live { position: relative; }
test/studio-runs.test.ts +25 −4
@@ -879,13 +879,13 @@ describe("working: a run's own page in the studio", () => {
879879 const page = await (await get(`/alice/engine/runs/${run.id}`, writerCookie)).text();
880880 expect(page).toContain('<progress aria-label="Test files started" max="9" value="2"');
881881 expect(page).toContain("2 of 9 files started · 4 passed, 1 failed · running test/b.test.ts");
882 expect(page).toContain("<strong>a breaks</strong>");
882 expect(page).toContain('<strong class="break">a breaks</strong>');
883883 expect(page).toContain("error: &lt;b&gt;boom&lt;/b&gt;");
884884 // The pins sit after the live region closes.
885885 const live = page.indexOf('role="status"');
886886 expect(live).toBeGreaterThan(-1);
887 expect(page.indexOf("pinned-failures")).toBeGreaterThan(page.indexOf("Test files"));
888 expect(page.slice(live, page.indexOf("pinned-slot"))).not.toContain("a breaks");
887 expect(page.indexOf('id="check-pins"')).toBeGreaterThan(page.indexOf("Test files"));
888 expect(page.slice(live, page.indexOf('id="check-pins"'))).not.toContain("a breaks");
889889 // The replaced element (#check-steps) closes before the pin slot, which is
890890 // patched in place so focus and scroll survive a refetch.
891891 const at = page.indexOf('id="check-steps"');
@@ -902,7 +902,11 @@ describe("working: a run's own page in the studio", () => {
902902 expect(block).not.toContain("tabindex");
903903 expect(block).not.toContain('role="region"');
904904 // With pins, the slot is focusable and labelled; the live region holds no pin.
905 expect(page).toContain('class="pinned-slot" id="check-pins" role="region" tabindex="0" aria-label="Failed tests"');
905 expect(page).toContain('data-pins id="check-pins" role="region" tabindex="0" aria-label="Failed tests"');
906 expect(page).not.toMatch(/data-pins[^>]*class="[^"]*stack/);
907 expect(page).not.toContain("pinned-failures");
908 expect(page).not.toContain("pinned-slot");
909 expect(page.slice(page.indexOf('id="check-pins"'))).toMatch(/class="card"\s+data-tone="danger"/);
906910 expect(page.slice(live, page.indexOf(`id="check-pins"`, live))).toContain("2 of 9 files started");
907911 expect(quiet).toContain('<p class="check-words small muted break"></p>'); // empty, but still rendered
908912 for (const old of ["tests-bar", "tests-words"]) {
@@ -2333,6 +2337,23 @@ describe("a deploy that just landed is news on the front door", () => {
23332337 * decision ledger must ask before it computes or renders one of those.
23342338 */
23352339
2340 describe("the pin slot's stylesheet", () => {
2341 test("caps [data-pins] with scroll and reserves it while a check is open", async () => {
2342 const { css } = await import("../src/web/style.ts");
2343 expect(css).toContain("[data-pins] { max-height: 9em; overflow-y: auto; }");
2344 expect(css).toContain('[data-check-progress][data-state="running"] [data-pins] { height: 9em; }');
2345 expect(css).toContain('[data-check-progress][data-state="queued"] [data-pins]');
2346 expect(css).not.toContain(".pinned-");
2347 // Cards stack as blocks (a flex column would squash them into 9em), and output wraps.
2348 expect(css).toContain("[data-pins] .card + .card { margin-top: 12px; }");
2349 expect(css).toContain("[data-pins] pre { margin: 4px 0 0; white-space: pre-wrap; font-size: var(--fs-min); }");
2350 });
2351 test("the slot is not a flex column", async () => {
2352 const { css } = await import("../src/web/style.ts");
2353 expect(css).not.toMatch(/\[data-pins\][^{]*\{[^}]*display:\s*(flex|grid)/);
2354 });
2355 });
2356
23362357 describe("run page controls leave the old classes behind (#4263)", () => {
23372358 test("no run-jump, run-live or run-steer class selector is defined, and [hidden] still beats .btn", async () => {
23382359 const { css } = await import("../src/web/style.ts");

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs