Public: anyone can read this; the crew can change it.
The command palette still says Code, Commits and New issue on every studio page #266
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/246-the-command-palette-still-says-code-comm → main 2 commits fork
3 files
changed +60
−13
src/web/chrome.ts +17 −11
| @@ -289,24 +289,30 @@ export { hostOf }; | ||
| 289 | 289 | |
| 290 | 290 | /** This repo's own rows for the command palette — the studio's nouns for a |
| 291 | 291 | * studio-mode viewer of an app, so the palette never sends someone to a |
| 292 | * list under a name the address bar would not use. */ | |
| 292 | * list under a name the address bar would not use. Commits is dev's own | |
| 293 | * page (no studio tab reaches it, `scopeHeader` above), so it drops out of | |
| 294 | * a studio app's rows rather than sit there under a dev word; New issue | |
| 295 | * gives way to the studio's own first verb, telling the crew, which posts | |
| 296 | * from the app page itself (`${base}#tell`, `studio/app.ts`). */ | |
| 293 | 297 | export function repoPalette(ctx: Ctx, repo: Repo): PaletteItem[] { |
| 294 | 298 | const base = repoBase(repo); |
| 295 | 299 | const name = `${repo.ownerName}/${repo.name}`; |
| 296 | 300 | const studio = |
| 297 | 301 | ctx.mode === "studio" && getAppByRepo(ctx.app.db, repo.id) !== undefined; |
| 302 | if (studio) { | |
| 303 | return [ | |
| 304 | { t: `${name} — Studio`, h: base, c: "This repo" }, | |
| 305 | { t: `${name} — Told`, h: tellsHref(base), c: "This repo" }, | |
| 306 | { t: `${name} — Changes`, h: changesHref(base), c: "This repo" }, | |
| 307 | { t: `${name} — Tell the crew`, h: `${base}#tell`, c: "This repo" }, | |
| 308 | { t: `${name} — Runs`, h: `${base}/runs`, c: "This repo" }, | |
| 309 | { t: `${name} — Settings`, h: `${base}/settings`, c: "This repo" }, | |
| 310 | ]; | |
| 311 | } | |
| 298 | 312 | return [ |
| 299 | 313 | { t: `${name} — Code`, h: base, c: "This repo" }, |
| 300 | { | |
| 301 | t: `${name} — ${studio ? "Told" : "Issues"}`, | |
| 302 | h: studio ? tellsHref(base) : `${base}/issues`, | |
| 303 | c: "This repo", | |
| 304 | }, | |
| 305 | { | |
| 306 | t: `${name} — ${studio ? "Changes" : "Pull requests"}`, | |
| 307 | h: studio ? changesHref(base) : `${base}/pulls`, | |
| 308 | c: "This repo", | |
| 309 | }, | |
| 314 | { t: `${name} — Issues`, h: `${base}/issues`, c: "This repo" }, | |
| 315 | { t: `${name} — Pull requests`, h: `${base}/pulls`, c: "This repo" }, | |
| 310 | 316 | { t: `${name} — Commits`, h: `${base}/commits`, c: "This repo" }, |
| 311 | 317 | { t: `${name} — New issue`, h: `${base}/issues/new`, c: "This repo" }, |
| 312 | 318 | { t: `${name} — Runs`, h: `${base}/runs`, c: "This repo" }, |
test/fork-flow.test.ts +5 −1
| @@ -793,7 +793,11 @@ describe("the fork flow", () => { | ||
| 793 | 793 | body: JSON.stringify({ fullName: `${UPSTREAM_OWNER}/late` }), |
| 794 | 794 | }); |
| 795 | 795 | expect(started.status).toBe(202); |
| 796 | setTimeout(() => void seedUpstream("late"), 1_200); | |
| 796 | // Seeded well inside the first retry's own wait (`firstFetchRetry.ms`, | |
| 797 | // `src/origins/mirror.ts`, 2s by default): the immediate first fetch | |
| 798 | // still finds nothing and fails, same as it would on a slower machine, | |
| 799 | // but the retry has margin to spare rather than racing it. | |
| 800 | setTimeout(() => void seedUpstream("late"), 300); | |
| 797 | 801 | const job = await mirrorAndWait(started); |
| 798 | 802 | expect(job?.stage).toBe("done"); |
| 799 | 803 | expect(job?.repo?.name).toBe("late"); |
test/studio-words.test.ts +38 −1
| @@ -183,6 +183,7 @@ describe("dev's vocabulary never reaches a studio page", () => { | ||
| 183 | 183 | let mergedPullNumber: number; |
| 184 | 184 | let openBranch: string; |
| 185 | 185 | let mergedBranch: string; |
| 186 | let repoPath: string; | |
| 186 | 187 | /** Every branch this fixture made. No studio page names one of them. */ |
| 187 | 188 | const branches: string[] = []; |
| 188 | 189 | /** The table every test below walks. Filled at the end of `beforeAll`. */ |
| @@ -477,7 +478,7 @@ describe("dev's vocabulary never reaches a studio page", () => { | ||
| 477 | 478 | }); |
| 478 | 479 | if (!blankEnabled.ok) throw new Error(String(blankEnabled.error)); |
| 479 | 480 | |
| 480 | const repoPath = "/alice/vocab"; | |
| 481 | repoPath = "/alice/vocab"; | |
| 481 | 482 | // Every studio page, one row each. A new studio page goes here — the |
| 482 | 483 | // path it answers at, the word it speaks, and dev's own words at the |
| 483 | 484 | // same path — and nothing else in this file changes. |
| @@ -723,6 +724,42 @@ describe("dev's vocabulary never reaches a studio page", () => { | ||
| 723 | 724 | expect(h1).not.toContain(runId); |
| 724 | 725 | }); |
| 725 | 726 | |
| 727 | test("the command palette speaks the studio's own nouns for an app (#246) — it rides a script tag the vocabulary walk above never opens", async () => { | |
| 728 | const res = await fetch(`${base}${repoPath}`, { | |
| 729 | headers: { cookie: `${cookie}; forge_mode=studio` }, | |
| 730 | redirect: "manual", | |
| 731 | }); | |
| 732 | expect(res.status).toBe(200); | |
| 733 | const html = await res.text(); | |
| 734 | const block = html.match( | |
| 735 | /<script type="application\/json" id="palette-index">([\s\S]*?)<\/script>/, | |
| 736 | )?.[1]; | |
| 737 | if (block === undefined) throw new Error("no palette-index on the app page"); | |
| 738 | const index = JSON.parse(block) as { items: { t: string; h: string }[] }; | |
| 739 | const rows = new Map(index.items.map((i) => [i.t, i.h])); | |
| 740 | ||
| 741 | // Dev's nouns and dev's pages: gone from a studio app's rows. | |
| 742 | for (const t of [ | |
| 743 | "alice/vocab — Code", | |
| 744 | "alice/vocab — Commits", | |
| 745 | "alice/vocab — New issue", | |
| 746 | "alice/vocab — Issues", | |
| 747 | "alice/vocab — Pull requests", | |
| 748 | ]) { | |
| 749 | expect(rows.has(t), `palette still offers "${t}" on a studio app page`).toBe( | |
| 750 | false, | |
| 751 | ); | |
| 752 | } | |
| 753 | ||
| 754 | // The studio's own nouns, at the studio's own addresses. | |
| 755 | expect(rows.get("alice/vocab — Studio")).toBe(repoPath); | |
| 756 | expect(rows.get("alice/vocab — Told")).toBe(`${repoPath}/tells`); | |
| 757 | expect(rows.get("alice/vocab — Changes")).toBe(`${repoPath}/changes`); | |
| 758 | // The studio's first verb — telling the crew — reachable from the | |
| 759 | // palette, landing on the app page where the tell box lives. | |
| 760 | expect(rows.get("alice/vocab — Tell the crew")).toBe(`${repoPath}#tell`); | |
| 761 | }); | |
| 762 | ||
| 726 | 763 | test("?mode=dev on the same routes still carries dev's own words — the assertions above are real, not vacuous", async () => { |
| 727 | 764 | let proved = 0; |
| 728 | 765 | for (const p of pages) { |
new above
offline