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

The runs list shows agent runs, checks and deploys together by default, with the three as filters #2400

Merged opened this

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

Merged

scout/forge:scout/2381-the-runs-list-shows-agent-runs-checks-an → main 2 commits fork 1 thread · 1 unresolved

  • Merged. scout/2381-the-runs-list-shows-agent-runs-checks-an was merged into main as b1043b9.

Review

Sign in to review this change.
2 files changed +84 −25
src/web/studio/lists.ts +44 −25
@@ -52,7 +52,8 @@ import {
5252 seg,
5353 time,
5454 } from "../ui.ts";
55 import { actorsFor, issuesFor, runTitleFrom } from "../pages/runs.ts";
55 import type { Run } from "../../core/runs.ts";
56 import { actorsFor, durationText, issuesFor, runTitleFrom } from "../pages/runs.ts";
5657 import { originGlyph } from "../pages/thread.ts";
5758 import { changeHref, PAGE_TITLES, pageTitle, tellHref } from "./nouns.ts";
5859 import { tellLine, tellsAnswered } from "./branch_tell.ts";
@@ -553,43 +554,49 @@ export async function changesList(ctx: Ctx): Promise<Response> {
553554 });
554555 }
555556
556 /** Which runs a list shows: the crew's by default, or the deploys or the
557 * checks that `?kind=` asks for — the narrowing dev's list offered, kept
558 * so a link written to it keeps meaning what it said. */
557 /** Which runs a list shows: all three kinds by default, or the one that
558 * `?kind=` asks for — the narrowing dev's list offered, kept so a link
559 * written to it keeps meaning what it said. */
559560 type RunsKind = "agent" | "deploy" | "check";
560561
561 function readRunsKind(ctx: Ctx): RunsKind {
562 function readRunsKind(ctx: Ctx): RunsKind | undefined {
562563 const raw = ctx.url.searchParams.get("kind");
563 return raw === "deploy" || raw === "check" ? raw : "agent";
564 return raw === "agent" || raw === "deploy" || raw === "check"
565 ? raw
566 : undefined;
564567 }
565568
566569 /** The runs list's own address with a word, a kind and a cursor. */
567570 function runsHref(
568571 base: string,
569 kind: RunsKind,
572 kind: RunsKind | undefined,
570573 word: WordFilter | undefined,
571574 cursor?: string,
572575 ): string {
573576 const params = new URLSearchParams();
574 if (kind !== "agent") params.set("kind", kind);
577 if (kind !== undefined) params.set("kind", kind);
575578 if (word !== undefined) params.set("word", word);
576579 if (cursor !== undefined) params.set("cursor", cursor);
577580 const text = params.toString();
578581 return text === "" ? `${base}/runs` : `${base}/runs?${text}`;
579582 }
580583
584 /** A check's state in the words its row names it by. */
585 const checkState = (run: Run): string =>
586 run.state === "ok" ? "passed" : run.state;
587
581588 const RUNS_KIND_WORDS: Record<RunsKind, string> = {
582 agent: "the crew's runs",
583 deploy: "deploys",
584 check: "checks",
589 agent: "Agent runs",
590 deploy: "Deploys",
591 check: "Checks",
585592 };
586593
587594 /**
588595 * GET /:owner/:name/runs: every run, for every repository and every viewer,
589596 * one row per run in its studio word — who ran it, when it last moved, and
590 * the transcript as the row's own link. The crew's runs by default;
591 * `?kind=deploy|check` reads the deploys or the checks instead, from one
592 * quiet line under the list. `?word=` narrows any of them.
597 * the transcript as the row's own link. Agent runs, checks and deploys
598 * together, newest first, by default; `?kind=agent|deploy|check` narrows to
599 * one, from the three links under the list. `?word=` narrows any of them.
593600 */
594601 export async function runsListPage(ctx: Ctx, repo: Repo): Promise<Response> {
595602 const db = ctx.app.db;
@@ -620,7 +627,13 @@ export async function runsListPage(ctx: Ctx, repo: Repo): Promise<Response> {
620627 const words = await Promise.all(fetched.map((run) => runWord(wordCtx, run)));
621628 const rows = fetched
622629 .map((run, i) => ({ run, info: words[i]! }))
623 .filter((r) => inWordFilter(r.info.word, filter));
630 .filter(
631 // With no word chosen a check or a deploy that finished is news too:
632 // the default list is the whole story, newest first.
633 (r) =>
634 (filter === undefined && r.run.kind !== "agent") ||
635 inWordFilter(r.info.word, filter),
636 );
624637
625638 const list =
626639 rows.length === 0
@@ -630,7 +643,7 @@ export async function runsListPage(ctx: Ctx, repo: Repo): Promise<Response> {
630643 note: emptyNote(
631644 filter,
632645 "No run",
633 kind === "agent"
646 kind === undefined || kind === "agent"
634647 ? "Nothing open. A run is one go the crew takes at a job; each one shows up here."
635648 : `Nothing open. Every ${kind === "deploy" ? "deploy" : "check"} shows up here while it runs.`,
636649 ),
@@ -649,16 +662,18 @@ export async function runsListPage(ctx: Ctx, repo: Repo): Promise<Response> {
649662 ? avatar(who)
650663 : icon(run.kind === "check" ? "check" : "rocket"),
651664 title: runTitleFrom(run, actors, issues),
652 meta: html`${run.kind === "agent" ? html`${who} · ` : null}${info.note}
665 meta:
666 run.kind === "check"
667 ? html`${run.commitOid.slice(0, 7)} · ${checkState(run)}
668 ${when(durationText(run), (d) => html` · took ${d}`)} ·
669 ${time(run.finishedAt ?? run.createdAt)}`
670 : html`${run.kind === "agent" ? html`${who} · ` : null}${info.note}
653671 · ${time(run.finishedAt ?? run.createdAt)}`,
654672 trail: wordBadge(info.word, undefined, record === undefined),
655673 });
656674 }),
657675 );
658676 const last = fetched[fetched.length - 1];
659 const others = (["agent", "deploy", "check"] as const).filter(
660 (k) => k !== kind,
661 );
662677
663678 const body = html`${repoHeader(ctx, repo, "runs", { compact: true })}
664679 <div class="mt">
@@ -675,12 +690,16 @@ export async function runsListPage(ctx: Ctx, repo: Repo): Promise<Response> {
675690 )}
676691 </div>
677692 <p class="muted small mt">
678 Also ${others.map(
693 Show ${(["agent", "deploy", "check"] as const).map(

Small tap targets in a muted line below the list. Move above the list as padded chips and mark the current one with aria-current="page" (bold alone is not announced).

679694 (k, i) =>
680 html`${i > 0 ? " · " : ""}<a href="${runsHref(base, k, undefined)}"
681 >${RUNS_KIND_WORDS[k]}</a
682 >`,
683 )}
695 html`${i > 0 ? " · " : ""}${k === kind
696 ? html`<strong>${RUNS_KIND_WORDS[k]}</strong>`
697 : html`<a href="${runsHref(base, k, undefined)}"
698 >${RUNS_KIND_WORDS[k]}</a
699 >`}`,
700 )}${kind === undefined
701 ? null
702 : html` · <a href="${runsHref(base, undefined, undefined)}">All</a>`}
684703 </p>
685704 ${liveMount({ repo: base })}`;
686705
test/studio-changes.test.ts +40
@@ -303,6 +303,46 @@ describe("the studio, continued", () => {
303303 expect(deploys).toContain('href="/alice/notes/runs?kind=deploy&amp;word=told"');
304304 });
305305
306 test("the runs list shows agent runs, checks and deploys together, and each kind link narrows it", async () => {
307 const scout = getUserByName(app.db, "scout")!;
308 const mk = (kind: "agent" | "check" | "deploy") => {
309 const run = createRun(app.db, {
310 repoId: repo.id,
311 kind,
312 actorId: kind === "agent" ? scout.id : undefined,
313 reason: "push",
314 commitOid: "abcdef1234567890abcdef1234567890abcdef12",
315 });
316 claimRun(app.db, run.id);
317 finishRun(app.db, run.id, { state: "ok", summary: "fine" });
318 return `href="/alice/notes/runs/${run.id}"`;
319 };
320 const agent = mk("agent");
321 const check = mk("check");
322 const deploy = mk("deploy");
323
324 const all = await (await get("/alice/notes/runs")).text();
325 for (const link of [agent, check, deploy]) expect(all).toContain(link);
326 expect(all).toContain("Check of abcdef1");
327 expect(all).toContain("abcdef1 · passed");
328 expect(all).toContain("took ");
329
330 const only = async (kind: string) =>
331 (await get(`/alice/notes/runs?kind=${kind}`)).text();
332 const a = await only("agent");
333 expect(a).toContain(agent);
334 expect(a).not.toContain(check);
335 expect(a).not.toContain(deploy);
336 const c = await only("check");
337 expect(c).toContain(check);
338 expect(c).not.toContain(agent);
339 expect(c).not.toContain(deploy);
340 const d = await only("deploy");
341 expect(d).toContain(deploy);
342 expect(d).not.toContain(agent);
343 expect(d).not.toContain(check);
344 });
345
306346 test("issues, pulls and runs on a repository that is not an app are the same one list", async () => {
307347 const plain = await createRepo(app, alice, {
308348 name: "plainlists",

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs