Public: anyone can read this; the crew can change it.
The studio's app and change pages wear a git chip no studio reader can act on: ● workbench · ○ this machine #317
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/281-the-studio-s-app-and-change-pages-wear-a → main 1 commit fork
4 files
changed +73
−3
src/web/chrome.ts +3 −1
| @@ -20,6 +20,7 @@ import { | ||
| 20 | 20 | browseActions, |
| 21 | 21 | hostOf, |
| 22 | 22 | localScope, |
| 23 | studioOriginNote, | |
| 23 | 24 | upstreamHref, |
| 24 | 25 | whereRow, |
| 25 | 26 | type RepoScope, |
| @@ -281,7 +282,8 @@ export function scopeHeader( | ||
| 281 | 282 | <span class="mono">${scope.upstream}</span> |
| 282 | 283 | </p>`, |
| 283 | 284 | )} |
| 284 | ${whereRow(ctx, scope)} ${tabs(items, "Repository")} | |
| 285 | ${studio ? studioOriginNote(repo) : whereRow(ctx, scope)} | |
| 286 | ${tabs(items, "Repository")} | |
| 285 | 287 | </header>`; |
| 286 | 288 | } |
| 287 | 289 |
src/web/scope.ts +25
| @@ -475,6 +475,31 @@ export function whereRow(ctx: Ctx, scope: RepoScope): Html { | ||
| 475 | 475 | </div>`; |
| 476 | 476 | } |
| 477 | 477 | |
| 478 | /** | |
| 479 | * The studio's own answer to "where does this app's bytes live", for a | |
| 480 | * repository the studio owns end to end. A native app is the studio's alone | |
| 481 | * — there is nothing to say, so a caller in studio mode draws no strip at | |
| 482 | * all rather than dev's three-place diagram of a git remote nobody here | |
| 483 | * reads dev's words for. A mirrored app still owes a reader the one fact | |
| 484 | * the strip carried — where it reads from, and how fresh — said as a | |
| 485 | * sentence instead of dots. | |
| 486 | */ | |
| 487 | export function studioOriginNote(repo: Repo | undefined): Html | null { | |
| 488 | const at = repo?.origin; | |
| 489 | if (at == null) return null; | |
| 490 | const host = at.htmlUrl === "" ? at.fullName : hostOf(at.htmlUrl); | |
| 491 | return html`<p class="repo-lineage"> | |
| 492 | Reads from | |
| 493 | ${at.htmlUrl === "" | |
| 494 | ? host | |
| 495 | : html`<a href="${at.htmlUrl}" rel="noreferrer noopener" target="_blank" | |
| 496 | >${host}</a | |
| 497 | >`}${at.lastFetchAt === null | |
| 498 | ? "" | |
| 499 | : html`, synced ${timeAgo(at.lastFetchAt)}`} | |
| 500 | </p>`; | |
| 501 | } | |
| 502 | ||
| 478 | 503 | /** |
| 479 | 504 | * The banner a live browse carries above its content. Deliberately quiet and |
| 480 | 505 | * deliberately unmissable: it says what you are looking at, that you cannot |
test/studio-words.test.ts +16 −2
| @@ -93,6 +93,16 @@ const DEV_WORDS: readonly { pattern: RegExp; because: string }[] = [ | ||
| 93 | 93 | pattern: /Close issue\b/, |
| 94 | 94 | because: "the studio's own word for a tell is tell", |
| 95 | 95 | }, |
| 96 | { | |
| 97 | pattern: /\bworkbench\b/i, | |
| 98 | because: | |
| 99 | "#281: a native app is the studio's alone, so its header draws no where strip at all", | |
| 100 | }, | |
| 101 | { | |
| 102 | pattern: /this machine/i, | |
| 103 | because: | |
| 104 | "#281: dev's git chip (workbench · this machine) never reaches a studio header", | |
| 105 | }, | |
| 96 | 106 | { |
| 97 | 107 | // Scoped to a badge's own content — "failed" also rides inside the |
| 98 | 108 | // legitimate `serving · tests failed` phrase (`envPhrase`), which does |
| @@ -505,7 +515,10 @@ describe("dev's vocabulary never reaches a studio page", () => { | ||
| 505 | 515 | path: repoPath, |
| 506 | 516 | title: "vocab", |
| 507 | 517 | shows: [`href="#i-pull"/></svg>Changes`], |
| 508 | hides: [`href="#i-eye"/></svg>Look`, `>Look<`], | |
| 518 | hides: [`href="#i-eye"/></svg>Look`, `>Look<`, 'class="where"'], | |
| 519 | // #281: `?mode=dev` on the same address is unaffected — the where | |
| 520 | // strip renders exactly as today, dev's own dots and words. | |
| 521 | dev: ["workbench", "this machine"], | |
| 509 | 522 | }, |
| 510 | 523 | { |
| 511 | 524 | word: "tell", |
| @@ -539,7 +552,8 @@ describe("dev's vocabulary never reaches a studio page", () => { | ||
| 539 | 552 | // #171: the composer's close button used to say "Close pull |
| 540 | 553 | // request" here, dev's own noun on the studio's own page. |
| 541 | 554 | shows: ["Close change"], |
| 542 | dev: ["diff-table", openBranch, "Close pull request"], | |
| 555 | hides: ['class="where"'], | |
| 556 | dev: ["diff-table", openBranch, "Close pull request", "workbench"], | |
| 543 | 557 | devTitle: `${openPullTitle} · vocab`, |
| 544 | 558 | }, |
| 545 | 559 | { |
test/web-origins.test.ts +29
| @@ -16,6 +16,7 @@ import { tmpdir } from "node:os"; | ||
| 16 | 16 | import { join } from "node:path"; |
| 17 | 17 | import { createApp, type App } from "../src/app.ts"; |
| 18 | 18 | import { serve, type Servers } from "../src/http/server.ts"; |
| 19 | import { enableApp } from "../src/actions/apps.ts"; | |
| 19 | 20 | import { resetConnectLimit } from "../src/actions/origins.ts"; |
| 20 | 21 | import { createToken } from "../src/actions/users.ts"; |
| 21 | 22 | import { listOrigins } from "../src/core/origins.ts"; |
| @@ -735,6 +736,34 @@ describe("origins on the web", () => { | ||
| 735 | 736 | expect(write.status).toBe(403); |
| 736 | 737 | }); |
| 737 | 738 | |
| 739 | // ---- studio mode on a mirrored app (#281) ---- | |
| 740 | ||
| 741 | test("in studio mode, a mirrored app says where it reads from in one line, never dev's where strip", async () => { | |
| 742 | const aliceUser = getUserByName(app.db, "alice")!; | |
| 743 | const repo = getRepo(app.db, "alice", "widgets")!; | |
| 744 | const enabled = enableApp(app, aliceUser, repo, { slug: "widgets" }); | |
| 745 | expect(enabled.ok).toBe(true); | |
| 746 | ||
| 747 | const studioHtml = await ( | |
| 748 | await asUser(`${alice}; forge_mode=studio`, "/alice/widgets") | |
| 749 | ).text(); | |
| 750 | expect(studioHtml).not.toContain('class="where"'); | |
| 751 | expect(studioHtml).not.toContain("workbench"); | |
| 752 | expect(studioHtml).not.toContain("this machine"); | |
| 753 | // Still says where it is read from, and when it last synced — in one | |
| 754 | // line, the studio's own words rather than dev's three-place strip. | |
| 755 | expect(studioHtml).toContain(`${originServers.url}/octo/widgets`); | |
| 756 | expect(studioHtml).toContain("synced"); | |
| 757 | ||
| 758 | // `?mode=dev` on the same address is unaffected: same dots, same words. | |
| 759 | const devHtml = await ( | |
| 760 | await asUser(`${alice}; forge_mode=dev`, "/alice/widgets") | |
| 761 | ).text(); | |
| 762 | expect(devHtml).toContain('class="where"'); | |
| 763 | expect(devHtml).toContain("workbench"); | |
| 764 | expect(devHtml).toContain("this machine"); | |
| 765 | }); | |
| 766 | ||
| 738 | 767 | test("disconnecting is confirmed on a page that says what it costs", async () => { |
| 739 | 768 | const id = listOrigins(app.db, getUserByName(app.db, "alice")!.id)[0]!.id; |
| 740 | 769 | const confirm = await asUser( |
new above
offline