Public: anyone can read this; the crew can change it.
A run page's jump, live and steer controls draw §5's bar and btn, and .run-jump, .run-live, .run-steer leave the markup #4367
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4263-a-run-page-s-jump-live-and-steer-control → main 4 commits fork
4 files
changed +45
−16
docs/DESIGN.md +1 −1
| @@ -398,7 +398,7 @@ gone, so this list only shrinks. | ||
| 398 | 398 | - **`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` |
| 399 | 399 | - **`style/content.ts`, prose (markdown)** — table cell alignment, built by `markdown.ts` as `ta-${align}`: `.ta-c` `.ta-r` |
| 400 | 400 | - **`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` `.log-tail-row` `.pinned-failures` `.pinned-slot` `.run-jump` `.run-live` `.run-steer` `.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` `.log-tail-row` `.pinned-failures` `.pinned-slot` `.txt` `.who` | |
| 402 | 402 | - **`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` |
| 403 | 403 | - **`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 |
| 404 | 404 | - **`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 +11 −4
| @@ -332,7 +332,7 @@ export async function runView( | ||
| 332 | 332 | }, |
| 333 | 333 | )} |
| 334 | 334 | ${vitalsBlock(run, logHidden, looks)} |
| 335 | <div class="run-live" id="run-live" data-state="${run.state}"> | |
| 335 | <div class="mt" id="run-live" data-state="${run.state}"> | |
| 336 | 336 | ${when( |
| 337 | 337 | // The head above is what makes \`liveRegionsJs\` open the one |
| 338 | 338 | // /events connection this log and its finish ride on. |
| @@ -363,7 +363,14 @@ export async function runView( | ||
| 363 | 363 | } |
| 364 | 364 | : {}), |
| 365 | 365 | pulse: run.state === "running", |
| 366 | extra: html`<button type="button" class="run-jump" id="run-jump" hidden> | |
| 366 | extra: html`<button | |
| 367 | type="button" | |
| 368 | class="btn" | |
| 369 | data-size="sm" | |
| 370 | data-variant="primary" | |
| 371 | id="run-jump" | |
| 372 | hidden | |
| 373 | > | |
| 367 | 374 | newer below |
| 368 | 375 | </button>`, |
| 369 | 376 | }))} |
| @@ -375,7 +382,7 @@ export async function runView( | ||
| 375 | 382 | writer && run.kind === "agent" && issue !== undefined, |
| 376 | 383 | () => |
| 377 | 384 | html`<form |
| 378 | class="run-steer" | |
| 385 | class="card stack" | |
| 379 | 386 | id="run-steer" |
| 380 | 387 | method="post" |
| 381 | 388 | action="${base}/runs/${run.id}/steer" |
| @@ -386,7 +393,7 @@ export async function runView( | ||
| 386 | 393 | >Say something to ${who}</label |
| 387 | 394 | > |
| 388 | 395 | <textarea |
| 389 | class="input" | |
| 396 | class="input break" | |
| 390 | 397 | id="steer-text" |
| 391 | 398 | name="text" |
| 392 | 399 | rows="1" |
src/web/style/content.ts +9 −11
| @@ -737,13 +737,13 @@ form.inline { display: inline; margin-left: auto; } | ||
| 737 | 737 | .pinned-failures pre { margin: 4px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; font-size: var(--fs-min); } |
| 738 | 738 | /* A run, live: a transcript with a speaker column, bounded so the page |
| 739 | 739 | head stays reachable, and a composer that sits under it. */ |
| 740 | .run-live { position: relative; margin-top: 12px; } | |
| 740 | #run-live { position: relative; } | |
| 741 | 741 | .run-log { |
| 742 | 742 | margin: 0; padding: 10px 14px 8px; max-height: calc(100dvh - 300px); min-height: 12em; overflow: auto; |
| 743 | 743 | background: var(--muted); border: 1px solid var(--border); border-radius: var(--radius); |
| 744 | 744 | font-size: 14px; line-height: 1.5; overflow-wrap: anywhere; tab-size: 4; |
| 745 | 745 | } |
| 746 | .run-live:has(.run-steer) .run-log { border-radius: var(--radius) var(--radius) 0 0; } | |
| 746 | #run-live:has(#run-steer) .run-log { border-radius: var(--radius) var(--radius) 0 0; } | |
| 747 | 747 | .run-log .ln { display: grid; grid-template-columns: 64px minmax(0, 1fr); gap: 10px; padding: 2px 0; } |
| 748 | 748 | .run-log .who { color: var(--faint); font-size: 12.5px; font-weight: 600; padding-top: 2px; user-select: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| 749 | 749 | .run-log .txt { white-space: pre-wrap; } |
| @@ -762,17 +762,15 @@ form.inline { display: inline; margin-left: auto; } | ||
| 762 | 762 | .run-log .ln-pulse i:nth-child(2) { animation-delay: .2s; } |
| 763 | 763 | .run-log .ln-pulse i:nth-child(3) { animation-delay: .4s; } |
| 764 | 764 | @keyframes run-pulse { 0%, 80%, 100% { opacity: .25; } 40% { opacity: 1; } } |
| 765 | .run-jump { | |
| 766 | position: sticky; bottom: 6px; float: right; margin-top: 6px; padding: 4px 10px; font: inherit; font-size: 12.5px; | |
| 767 | color: var(--bg); background: var(--fg); border: 0; border-radius: 999px; cursor: pointer; box-shadow: var(--shadow-2); | |
| 765 | #run-jump { | |
| 766 | position: sticky; bottom: 6px; float: right; margin-top: 6px; border-radius: 999px; box-shadow: var(--shadow-2); | |
| 768 | 767 | } |
| 769 | .run-steer { | |
| 770 | position: sticky; bottom: 0; display: grid; gap: 6px; padding: 10px 12px; background: var(--bg); | |
| 771 | border: 1px solid var(--border); border-top: 0; border-radius: 0 0 var(--radius) var(--radius); | |
| 768 | #run-steer { | |
| 769 | position: sticky; bottom: 0; gap: 6px; padding: 10px 12px; background: var(--bg); | |
| 770 | border-top: 0; border-radius: 0 0 var(--radius) var(--radius); box-shadow: none; | |
| 772 | 771 | } |
| 773 | .run-steer textarea { min-height: 38px; max-height: 160px; resize: none; } | |
| 774 | .run-steer .bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } | |
| 775 | .run-steer .bar .muted { flex: 1 1 200px; } | |
| 772 | #run-steer textarea { min-height: 38px; max-height: 160px; resize: none; } | |
| 773 | #run-steer .bar .muted { flex: 1 1 200px; } | |
| 776 | 774 | /* A tail beside a running row (studio/app.ts): the same log, a peek at it |
| 777 | 775 | — fixed height so nothing around it shifts as lines arrive, and it |
| 778 | 776 | scrolls itself rather than the page. */ |
test/studio-runs.test.ts +24
| @@ -517,6 +517,17 @@ describe("working: a run's own page in the studio", () => { | ||
| 517 | 517 | expect(page).toContain('data-who="scout"'); |
| 518 | 518 | expect(page).toContain('id="run-steer"'); |
| 519 | 519 | expect(page).toContain("Steer scout"); |
| 520 | // Jump, live and steer draw §5's btn, bar and card (#4263). | |
| 521 | for (const old of ["run-jump", "run-live", "run-steer"]) { | |
| 522 | expect(page).not.toMatch(new RegExp(`class="[^"]*\\b${old}\\b`)); | |
| 523 | } | |
| 524 | expect(page).toContain("newer below"); | |
| 525 | expect(page).toMatch(/<button[^>]*class="btn"[^>]*id="run-jump"/); | |
| 526 | expect(page).toMatch( | |
| 527 | /<form[^>]*class="card stack"[^>]*id="run-steer"[^>]*action="\/alice\/engine\/runs\/\w+\/steer"/s, | |
| 528 | ); | |
| 529 | expect(page).toContain('name="text"'); | |
| 530 | expect(page).toContain('formaction="/alice/engine/runs/'); | |
| 520 | 531 | expect(page).toContain('id="run-stop"'); |
| 521 | 532 | // The transcript is on the page, and there is no second reading of it. |
| 522 | 533 | expect(page).toContain('id="run-log"'); |
| @@ -2295,3 +2306,16 @@ describe("a deploy that just landed is news on the front door", () => { | ||
| 2295 | 2306 | * as `canWrite` today — that every surface carrying money, the crew or the |
| 2296 | 2307 | * decision ledger must ask before it computes or renders one of those. |
| 2297 | 2308 | */ |
| 2309 | ||
| 2310 | describe("run page controls leave the old classes behind (#4263)", () => { | |
| 2311 | test("no run-jump, run-live or run-steer class selector is defined, and [hidden] still beats .btn", async () => { | |
| 2312 | const { css } = await import("../src/web/style.ts"); | |
| 2313 | for (const old of ["run-jump", "run-live", "run-steer"]) { | |
| 2314 | expect(css).not.toMatch(new RegExp(`\\.${old}(?![\\w-])`)); | |
| 2315 | } | |
| 2316 | // The layout is kept under the ids the client script already uses. | |
| 2317 | expect(css).toMatch(/#run-steer \{[^}]*position: sticky/); | |
| 2318 | expect(css).toMatch(/#run-jump \{[^}]*position: sticky/); | |
| 2319 | expect(css).toMatch(/\[hidden\] \{ display: none !important; \}/); | |
| 2320 | }); | |
| 2321 | }); |
new above
offline