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

A finished check shard is announced on /events, and the pull page's checks line stops beating every 5 seconds #2893

Merged opened this

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

Merged

scout/forge:scout/2889-a-finished-check-shard-is-announced-on-e → main 3 commits fork 4 threads · 4 unresolved

  • Merged. scout/2889-a-finished-check-shard-is-announced-on-e was merged into main as 3f49e79.

Review

Sign in to review this change.
21 files changed +157 −35
src/checks/service.ts +4 −1
@@ -17,6 +17,7 @@ import { tmpdir } from "node:os";
1717 import { join } from "node:path";
1818 import type { App } from "../app.ts";
1919 import { announceRun } from "../actions/runs.ts";
20 import { announceShard } from "./shard_event.ts";
2021 import type { ForgeEvent } from "../core/events.ts";
2122 import { getPull, listPulls, type Pull } from "../core/pulls.ts";
2223 import { issueMayRun } from "../core/pull_trust.ts";
@@ -476,8 +477,10 @@ export function startChecks(app: App, options: ChecksOptions): ChecksHandle {
476477 if (!claimed) return;
477478 const lease = holdLease(app.db, run.id);
478479 announceRun(app, claimed);
479 const write = (line: string): void =>
480 const write = (line: string): void => {
480481 appendRunLog(app.config.dataDir, run.id, line);
482 announceShard(app, run, line);

This call is untested: remove it and the suite stays green. Drive a run through the callback and assert a run.shard is appended.

483 };
481484 if (options.label !== undefined)
482485 write(`forge ${options.label} — checked in its parent's lane`);
483486 const started = Date.now();
src/checks/shard_event.ts added +37
@@ -0,0 +1,37 @@
1 /**
2 * A check's shard finishing, said on the outbox. The shard runner
3 * (`scripts/test-shards.ts`) writes a line when a shard ends; this reads that
4 * line and announces it as `run.shard`, so a pull's checks line, which listens
5 * to `run.*`, patches itself instead of beating on a timer. One line, one
6 * event: the live form ends at `done`, the batched form at its `(exit` header,
7 * and neither line is the other's.
8 */
9 import type { App } from "../app.ts";
10 import type { Run } from "../core/runs.ts";
11
12 const LIVE_DONE = /^── shard (\d+)\/(\d+) done \(exit \d+, [0-9.]+s\)$/;

These regexes are coupled to the runner output wording. With the beat removed, a format drift fails silently: the line stops updating until the run finishes. Add a test that feeds real test-shards.ts output through finishedShard.

13 const BATCHED = /^── shard (\d+)\/(\d+) \(exit \d+, [0-9.]+s\): /;
14
15 /** Which shard a log line says has finished, or nothing. */
16 export function finishedShard(line: string): { shard: number; of: number } | undefined {
17 const m = LIVE_DONE.exec(line) ?? BATCHED.exec(line);
18 return m === null ? undefined : { shard: Number(m[1]), of: Number(m[2]) };
19 }
20
21 /** Announce the shard `line` finishes, if it finishes one. */
22 export function announceShard(app: App, run: Run, line: string): void {
23 const done = finishedShard(line);
24 if (done === undefined) return;
25 app.bus.append({
26 kind: "run.shard",
27 repoId: run.repoId,
28 payload: {
29 runId: run.id,
30 kind: run.kind,
31 env: run.env,
32 commitOid: run.commitOid,
33 shard: done.shard,
34 of: done.of,
35 },
36 });
37 }
src/web/pages/board.ts +6 −6
@@ -338,7 +338,7 @@ function forgeStrip(ctx: Ctx): Html | null {
338338 return html`<div
339339 class="board-forge"
340340 id="forge"
341 data-live="run.*,app.*"
341 data-live="run.queued,run.started,run.finished,app.*"
342342 data-ok="${ok ? "yes" : "no"}"
343343 data-rollout="${fh.rollout.state}"
344344 role="status"
@@ -491,7 +491,7 @@ function declinedStrip(ctx: Ctx, scope: BoardScope): Html | null {
491491 return html`<div
492492 class="board-declined"
493493 id="declined"
494 data-live="run.*,issue.*,pull.*"
494 data-live="run.queued,run.started,run.finished,issue.*,pull.*"
495495 role="status"
496496 >
497497 ${declines.map((d) => {
@@ -611,7 +611,7 @@ function windowsStrip(ctx: Ctx): Html | null {
611611 return html`<div
612612 class="board-windows"
613613 id="windows"
614 data-live="run.*,usage.*"
614 data-live="run.queued,run.started,run.finished,usage.*"
615615 aria-label="Subscription windows"
616616 >
617617 <div class="board-subs" data-group="crew">
@@ -700,7 +700,7 @@ function working(ctx: Ctx, scope: BoardScope): Html {
700700 "working",
701701 "1",
702702 "Working now",
703 "run.*",
703 "run.queued,run.started,run.finished",
704704 rows,
705705 scope.repos.length === 0
706706 ? "No apps to watch."
@@ -921,7 +921,7 @@ function decided(ctx: Ctx, scope: BoardScope): Html {
921921 "decided",
922922 "3",
923923 "Decided",
924 "run.*,pull.*",
924 "run.queued,run.started,run.finished,pull.*",
925925 rows,
926926 pile === null ? "Nothing decided yet." : PILE_EMPTY[pile],
927927 pile === null
@@ -1041,7 +1041,7 @@ function today(ctx: Ctx, scope: BoardScope): Html {
10411041 "today",
10421042 "4",
10431043 "Today",
1044 "run.*,pull.merged",
1044 "run.queued,run.started,run.finished,pull.merged",
10451045 rows,
10461046 scope.repos.length === 0
10471047 ? "No apps to watch."
src/web/pages/boards-view.ts +1 −1
@@ -23,7 +23,7 @@ import { cellsOf, chartBox, paramsOf, queryFields, readParams, runFor } from "./
2323
2424 const RANGE_MS = { "1h": 3_600_000, "24h": 86_400_000, "7d": 604_800_000, "30d": 2_592_000_000 } as const;
2525 /** The events the panels' queries read (runs, pulls, checks, issues, usage): any of them may change a chart. */
26 const READS = "run.*,pull.*,check.*,issue.*,usage.*";
26 const READS = "run.queued,run.started,run.finished,pull.*,check.*,issue.*,usage.*";
2727
2828 const option = (value: string, selected: boolean) =>
2929 html`<option value="${value}" ${selected ? raw("selected") : null}>${value}</option>`;
src/web/pages/conflicts.ts +1 −1
@@ -124,7 +124,7 @@ async function proposalCard(
124124 </form>`);
125125 if (parts.length === 0) return html``;
126126 // A live region: the run's events re-read the card, so nobody reloads.
127 return html`<div id="crew-proposal" data-live="run.*,pull.*,comment.*">${card(html`${parts}`, { head: html`<h2>The crew</h2>` })}</div>`;
127 return html`<div id="crew-proposal" data-live="run.queued,run.started,run.finished,pull.*,comment.*">${card(html`${parts}`, { head: html`<h2>The crew</h2>` })}</div>`;
128128 }
129129
130130 const CSS = `
src/web/pages/map.ts +1 −1
@@ -1301,7 +1301,7 @@ export const mapViewJs = /* js */ `
13011301 })();
13021302 `;
13031303
1304 const LIVE_KINDS = "run.*,issue.*,pull.*";
1304 const LIVE_KINDS = "run.queued,run.started,run.finished,issue.*,pull.*";

The same run.queued,run.started,run.finished string is repeated in about nine places. Export one constant and use it, so the next run.* kind does not need nine edits.

13051305
13061306 /** The empty state in the studio's own words, pointing at the app page's
13071307 * own box for telling the crew (`#tell`, `src/web/studio/app.ts`). */
src/web/pages/pull.ts +3 −6
@@ -272,15 +272,12 @@ export function checksLine(
272272 : waiting
273273 ? html`<span>${icon("lock")}${WAITING_WORDS}</span>`
274274 : html`<span>${icon("clock")}No checks on this head yet</span>`;
275 // Its own address: an event or the beat fetches this line, not the page.
276 // While a check runs there is no event for a finished shard, so it also
277 // asks on a beat.
275 // Its own address: an event fetches this line, not the page. A finished
276 // shard is a `run.shard` event (`checks/shard_event.ts`), so no beat.
278277 const live =
279278 liveSrc === ""
280279 ? null
281 : html`id="live-checks" data-live="run.*" data-live-src="${liveSrc}"${
282 check.state === "running" || check.state === "queued" ? html` data-live-every="5"` : null
283 }`;
280 : html`id="live-checks" data-live="run.*" data-live-src="${liveSrc}"`;
284281 return html`<p class="checks-line" ${live}>
285282 ${body}${against}${when(
286283 tier !== "",
src/web/pages/spend-tab.ts +1 −1
@@ -16,7 +16,7 @@ import { usageShell } from "./usage-tabs.ts";
1616 export function spendPage(ctx: Ctx): Response {
1717 if (!ctx.auth.user) return signInFirst(ctx);
1818 if (!ctx.auth.user.isAdmin) return notFoundPage(ctx);
19 const body = html`<div id="spend" data-live="run.*,usage.*">${card(
19 const body = html`<div id="spend" data-live="run.queued,run.started,run.finished,usage.*">${card(
2020 spendView(readSpend(ctx.app, new Date())),
2121 { head: html`${icon("clock")}<h2>Spend</h2>` },
2222 )}</div>`;
src/web/studio/answers.ts +1 −1
@@ -22,7 +22,7 @@ import { time } from "../ui.ts";
2222 import { tellHref } from "./nouns.ts";
2323
2424 /** The events that can change an answer. */
25 export const ANSWERS_LIVE = "run.*,issue.*";
25 export const ANSWERS_LIVE = "run.queued,run.started,run.finished,issue.*";
2626
2727 /** How many tells the region shows, and the longest answer it quotes. */
2828 const SHOWN = 8;
src/web/studio/now.ts +1 −1
@@ -40,7 +40,7 @@ import {
4040 } from "./words.ts";
4141
4242 /** The events that can change what Now says. */
43 export const NOW_LIVE = "run.*,pull.*,issue.*";
43 export const NOW_LIVE = "run.queued,run.started,run.finished,pull.*,issue.*";
4444
4545 /**
4646 * The card in its live region: the kinds it listens for, the repository
test/board.test.ts +5 −5
@@ -136,12 +136,12 @@ describe("the board", () => {
136136 expect(html).not.toContain('class="topbar"');
137137 expect(html).not.toContain('class="foot"');
138138 expect(html).toMatch(/<main\s+class="bare"\s+id="main"\s*>/);
139 expect(html).toMatch(/id="working"\s+data-live="run\.\*"/);
139 expect(html).toMatch(/id="working"\s+data-live="run\.queued,run\.started,run\.finished"/);
140140 expect(html).toMatch(
141141 /id="landed"\s+data-live="pull\.merged,run\.finished"/,
142142 );
143 expect(html).toMatch(/id="decided"\s+data-live="run\.\*,pull\.\*"/);
144 expect(html).toMatch(/id="today"\s+data-live="run\.\*,pull\.merged"/);
143 expect(html).toMatch(/id="decided"\s+data-live="run\.queued,run\.started,run\.finished,pull\.\*"/);
144 expect(html).toMatch(/id="today"\s+data-live="run\.queued,run\.started,run\.finished,pull\.merged"/);
145145 expect(html).toContain("Nobody is working right now.");
146146 expect(html).toContain("Nothing has shipped yet.");
147147 expect(html).toContain("Nothing decided yet.");
@@ -348,7 +348,7 @@ describe("the board", () => {
348348 expect(res.status).toBe(200);
349349 const html = await res.text();
350350 expect(html).toMatch(/<div class="board"\s+data-wall="">/);
351 expect(html).toMatch(/id="working"\s+data-live="run\.\*"/);
351 expect(html).toMatch(/id="working"\s+data-live="run\.queued,run\.started,run\.finished"/);
352352 expect(html).toMatch(/class="board-scope"\s*>notes</);
353353 });
354354
@@ -449,7 +449,7 @@ describe("the board's forge strip", () => {
449449
450450 test("an admin reads All fine; a stuck run becomes a linked sentence; a non-admin sees nothing", async () => {
451451 const fine = await board("root");
452 expect(fine).toMatch(/id="forge"\s+data-live="run\.\*,app\.\*"/);
452 expect(fine).toMatch(/id="forge"\s+data-live="run\.queued,run\.started,run\.finished,app\.\*"/);
453453 expect(fine).toContain("All fine");
454454
455455 const made = await createRepo(app, admin, { name: "stuck" });
test/boards-store.test.ts +1 −1
@@ -175,7 +175,7 @@ describe("/boards", () => {
175175 const grid = body.match(/<section[^>]*id="board-grid"[^>]*>/)?.[0] ?? "";
176176 expect(grid).not.toContain("data-live-every");
177177 expect(grid).toMatch(/data-live="[^"]+"/);
178 expect(grid).toContain('data-live="run.*,pull.*,check.*,issue.*,usage.*"');
178 expect(grid).toContain('data-live="run.queued,run.started,run.finished,pull.*,check.*,issue.*,usage.*"');
179179 expect(body).toContain('data-live-src="/boards/fleet-health/live"');
180180 expect(body).toMatch(/<option value="10s" selected>on: redraw when events arrive<\/option>/);
181181 expect(body).not.toContain(">1m</option>");
test/check-shard-event.test.ts added +32
@@ -0,0 +1,32 @@
1 import { expect, test } from "bun:test";
2 import { announceShard, finishedShard } from "../src/checks/shard_event.ts";
3 import { checksLine } from "../src/web/pages/pull.ts";
4
5 const run = { id: "r1", repoId: "repo1", kind: "check", env: "pull-7", commitOid: "abc" } as never;
6
7 function fakeApp() {
8 const events: { kind: string; repoId: string; payload: Record<string, unknown> }[] = [];
9 return { events, app: { bus: { append: (e: never) => events.push(e) } } as never };
10 }
11
12 test("a finished shard, live or batched, is one line and one event", () => {
13 expect(finishedShard("── shard 2/4 done (exit 0, 3.0s)")).toEqual({ shard: 2, of: 4 });
14 expect(finishedShard("── shard 3/4 (exit 1, 2.0s): a.ts b.ts")).toEqual({ shard: 3, of: 4 });
15 expect(finishedShard("── shard 2/4: a.ts b.ts")).toBeUndefined();
16 expect(finishedShard(" 5 pass")).toBeUndefined();
17
18 const { app, events } = fakeApp();
19 for (const line of ["── shard 1/2: a.ts", " 5 pass", "── shard 1/2 done (exit 0, 1.0s)"])
20 announceShard(app, run, line);
21 expect(events).toHaveLength(1);
22 expect(events[0]!.kind).toBe("run.shard");
23 expect(events[0]!.repoId).toBe("repo1");
24 expect(events[0]!.payload).toEqual({ runId: "r1", kind: "check", env: "pull-7", commitOid: "abc", shard: 1, of: 2 });
25 });
26
27 test("the checks line of a running check listens for run.* and never beats", () => {
28 const run = { id: "r1", kind: "check", env: "pull-7", commitOid: "abcdef1234567", state: "running", summary: "", createdAt: "2026-10-16T10:00:00Z" } as never;
29 const html = checksLine("/a/b", { state: "running", run }, false, true, false, "", "", 0, "", false, "", "/a/b/pulls/7/checks-line").text;
30 expect(html).toContain('data-live="run.*"');
31 expect(html).not.toContain("data-live-every");
32 });
test/checks.test.ts +53
@@ -46,6 +46,7 @@ import { pullWord, servingHistory } from "../src/web/studio/words.ts";
4646 import { createUser, type User } from "../src/core/users.ts";
4747 import type { Repo } from "../src/core/repos.ts";
4848 import { serve, type Servers } from "../src/http/server.ts";
49 import { listEvents } from "../src/core/events.ts";
4950 import { readRunLog } from "../src/runs/log.ts";
5051 import { promote } from "./helpers/members.ts";
5152
@@ -1210,3 +1211,55 @@ describe("startChecks: a booting forge removes only checkouts nobody is running
12101211 }
12111212 });
12121213 });
1214
1215 describe("checks: a finished shard is announced as the runner writes it (#2889)", () => {
1216 test("each shard-done line the check prints puts one run.shard on the bus, and nothing else does", async () => {
1217 const dir = await mkdtemp(join(tmpdir(), "forge-shards-"));
1218 const shardApp = createApp({ dataDir: dir, port: 0 });
1219 const shardServers = await serve(shardApp);
1220 const handle = startChecks(shardApp, { forgeUrl: shardServers.url, concurrency: 1 });
1221 try {
1222 const u = await createUser(shardApp.db, {
1223 origin: "seed",
1224 trusted: true,
1225 username: "alice",
1226 email: "[email protected]",
1227 password: "a-good-passphrase",
1228 adminIfFirst: true,
1229 });
1230 if (!u.ok) throw new Error(u.error);
1231 const made = await createRepo(shardApp, u.value, { name: "lib", visibility: "public" });
1232 if (!made.ok) throw new Error(String(made.error));
1233 const lines = [
1234 "test-shards: 3 files in 2 shards",
1235 "── shard 1/2: a.ts b.ts",
1236 " 5 pass",
1237 "── shard 1/2 done (exit 0, 1.0s)",
1238 "── shard 2/2: c.ts",
1239 "── shard 2/2 done (exit 0, 1.0s)",
1240 ];
1241 const script = lines.map((l) => `echo '${l}'`).join("; ");
1242 const pushed = await commitToBranch(shardApp, u.value, made.value, {
1243 message: "Declare a check",
1244 files: [{ path: "forge.json", content: checkJson(script) }],
1245 });
1246 if (!pushed.ok) throw new Error(JSON.stringify(pushed.error));
1247 const deadline = Date.now() + 30_000;
1248 let run: Run | undefined;
1249 while (Date.now() < deadline) {
1250 run = listRuns(shardApp.db, made.value.id, { kind: "check", limit: 5 })[0];
1251 if (run && (run.state === "ok" || run.state === "failed")) break;
1252 await Bun.sleep(50);
1253 }
1254 expect(run?.state).toBe("ok");
1255 const shards = listEvents(shardApp.db, { repoId: made.value.id, kind: "run.shard" });
1256 expect(shards.map((e) => e.payload["shard"]).sort()).toEqual([1, 2]);
1257 expect(shards.every((e) => e.payload["runId"] === run!.id && e.payload["of"] === 2)).toBe(true);
1258 } finally {
1259 await handle.stop();
1260 shardServers.stop();
1261 shardApp.close();
1262 await rm(dir, { recursive: true, force: true });
1263 }
1264 });
1265 });
test/credentials.test.ts +1 −1
@@ -324,7 +324,7 @@ describe("the board", () => {
324324 expect(page.slice(crewAt, apartAt)).toContain("platform (env)");
325325 expect(page.slice(crewAt, apartAt)).not.toContain("trevato-espo");
326326 expect(page.slice(apartAt)).toContain("trevato-espo");
327 expect(page).toContain('data-live="run.*,usage.*"');
327 expect(page).toContain('data-live="run.queued,run.started,run.finished,usage.*"');
328328 expect(page).toContain('data-by="byKind"');
329329 expect(page).toContain("The 10 costliest runs");
330330
test/crew-proposal.test.ts +1 −1
@@ -225,7 +225,7 @@ echo '{"type":"result","subtype":"success","is_error":false,"result":"Wrote note
225225 ).then((r) => r.text());
226226 expect(page).toContain("The crew proposed a resolution");
227227 expect(page).toContain("Kept both lines");
228 expect(page).toContain('id="crew-proposal" data-live="run.*');
228 expect(page).toContain('id="crew-proposal" data-live="run.queued,run.started,run.finished');
229229 expect(page).toContain(`/edit/`);
230230 expect(page).toContain(now.headOid.slice(0, 7));
231231 });
test/map.test.ts +2 −2
@@ -366,7 +366,7 @@ describe("the map, per app and across the forge", () => {
366366 await fetch(`${base}/alice/engine/map`, { headers: { cookie } })
367367 ).text();
368368 const region = page.match(/<div[^>]*id="map-live"[^>]*>/)?.[0] ?? "";
369 expect(region).toContain('data-live="run.*,issue.*,pull.*"');
369 expect(region).toContain('data-live="run.queued,run.started,run.finished,issue.*,pull.*"');
370370 expect(region).toContain('data-live-repo="alice/engine"');
371371 // The canvas carries its size for the fit, and the tools for the rest.
372372 expect(page).toMatch(
@@ -392,7 +392,7 @@ describe("the map, per app and across the forge", () => {
392392 ).text();
393393 expect(page).toContain("<h1>Map</h1>");
394394 expect(page).toMatch(
395 /<div[^>]*id="map-lanes"[^>]*data-live="run\.\*,issue\.\*,pull\.\*"/,
395 /<div[^>]*id="map-lanes"[^>]*data-live="run\.queued,run\.started,run\.finished,issue\.\*,pull\.\*"/,
396396 );
397397 const lane = (id: string) =>
398398 page.match(new RegExp(`<details[^>]*id="${id}"[^>]*>`))?.[0] ?? "";
test/no-polling.test.ts −1
@@ -14,7 +14,6 @@ const ALLOWED: Record<string, { allow: string[]; why: string }> = {
1414 "src/web/pages/migrate.ts": { allow: ["refresh: {", 'http-equiv="refresh"'], why: "migrate progress runs in a process that emits no events" },
1515 "src/web/pages/browse.ts": { allow: ["refresh: {"], why: "mirror progress runs in a process that emits no events" },
1616 "src/web/pages/backups.ts": { allow: ["refresh: {"], why: "a backup moving emits no event" },
17 "src/web/pages/pull.ts": { allow: ["data-live-every"], why: "a shard finishing emits no event, so a running check row beats" },
1817 "src/web/client.ts": { allow: ["data-live-every", "setInterval"], why: "the data-live-every mechanism itself, and clocks that only redraw text" },
1918 "src/web/fleet-graph.ts": { allow: ["setInterval"], why: "no fleet event yet; remove once it has one" },
2019 };
test/pull-checks-line-route.test.ts +4 −3
@@ -110,7 +110,7 @@ test("a viewer who can gets the line, without counts until the log has them", as
110110 const text = await res.text();
111111 expect(text).toContain('id="live-checks"');
112112 expect(text).toContain("Check running");
113 expect(text).toContain('data-live-every="5"');
113 expect(text).not.toContain("data-live-every");
114114 expect(text).not.toContain(" files");
115115 });
116116
@@ -134,7 +134,8 @@ test("the page that draws the line also ships the script that drives it", async
134134 expect(res.status).toBe(200);
135135 const text = await res.text();
136136 expect(text).toContain('id="live-checks"');
137 expect(text).toContain('data-live-every="5"');
137 // The line itself carries no beat attribute; the script still knows the mechanism.
138 expect(text).not.toMatch(/id="live-checks"[^>]*data-live-every/);
138139 // liveRegionsJs reads data-live-src and arms the beat; liveClientJs alone does not.
139140 expect(text).toContain("data-live-src");
140141 expect(text).toContain("function arm(");
@@ -161,7 +162,7 @@ test("the change page the owner lands on draws the same live line, with the coun
161162 expect(res.status).toBe(200);
162163 const text = await res.text();
163164 expect(text).toContain('id="live-checks"');
164 expect(text).toContain('data-live-every="5"');
165 expect(text).not.toMatch(/id="live-checks"[^>]*data-live-every/);
165166 expect(text).toContain("2 of 170 files · 500 passed, 0 failed");
166167 expect(text).toContain("function arm(");
167168 });
test/studio-door.test.ts +1 −1
@@ -1332,7 +1332,7 @@ describe("the app's live fragment: /:owner/:name/app/state", () => {
13321332 expect(text).toMatch(/^\s*<div data-etag="[0-9a-f]{16}"/);
13331333 expect(`"${/data-etag="([0-9a-f]{16})"/.exec(text)![1]}"`).toBe(etag!);
13341334 expect(text).toContain('id="live-now"');
1335 expect(text).toContain('data-live="run.*,pull.*,issue.*"');
1335 expect(text).toContain('data-live="run.queued,run.started,run.finished,pull.*,issue.*"');
13361336 expect(text).toContain('data-live-src="/alice/vault/app/now"');
13371337 expect(text).toContain("<h2>Now</h2>");
13381338
test/studio-runs.test.ts +1 −1
@@ -1021,7 +1021,7 @@ describe("the app's page tails the crew's log while it works", () => {
10211021 // for this repository's runs, and the tail rides the one /events stream.
10221022 expect(page).not.toMatch(/<meta\s+http-equiv="refresh"/);
10231023 expect(page).toMatch(
1024 /id="live-now"\s+data-live="run\.\*[^"]*"\s+data-live-repo="alice\/engine"/,
1024 /id="live-now"\s+data-live="run\.queued,run\.started,run\.finished[^"]*"\s+data-live-repo="alice\/engine"/,
10251025 );
10261026
10271027 const stream = await get(

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs