Public: anyone can read this; the crew can change it.
The studio says an app is Autonomous when nobody is on its crew #519
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/227-the-studio-says-an-app-is-autonomous-whe → main 1 commit fork
4 files
changed +57
−3
src/web/studio/app.ts +3 −2
| @@ -43,7 +43,7 @@ import { | ||
| 43 | 43 | import { changesHref, tellsHref } from "./nouns.ts"; |
| 44 | 44 | import { nowCard, nowRegion } from "./now.ts"; |
| 45 | 45 | import { appPanels } from "./previews.ts"; |
| 46 | import { servingHistory, type WordCtx } from "./words.ts"; | |
| 46 | import { noCrewLine, servingHistory, type WordCtx } from "./words.ts"; | |
| 47 | 47 | |
| 48 | 48 | /** |
| 49 | 49 | * The week's decisions in one line: how many, how many the decider took |
| @@ -400,7 +400,8 @@ export async function studioApp( | ||
| 400 | 400 | : autonomy.paused} |
| 401 | 401 | </dd>` |
| 402 | 402 | : null} |
| 403 | </dl>`, | |
| 403 | </dl> | |
| 404 | ${when(crew.length === 0, () => noCrewLine(`${base}/settings`))}`, | |
| 404 | 405 | { |
| 405 | 406 | head: html`${icon("play")} |
| 406 | 407 | <h2>On its own</h2> |
src/web/studio/shelf.ts +2 −1
| @@ -49,6 +49,7 @@ import { | ||
| 49 | 49 | type ProductionFact, |
| 50 | 50 | type ServingHistory, |
| 51 | 51 | type WordCtx, |
| 52 | NOBODY_ON_CREW, | |
| 52 | 53 | } from "./words.ts"; |
| 53 | 54 | |
| 54 | 55 | /** Reads the caller has already made, so the shelf does not make them again. */ |
| @@ -237,7 +238,7 @@ export async function shelfRows( | ||
| 237 | 238 | word === undefined |
| 238 | 239 | ? stuck.has(repo.id) |
| 239 | 240 | ? html`<span class="muted" |
| 240 | >told — nobody on the crew to take it</span | |
| 241 | >told — ${NOBODY_ON_CREW} to take it</span | |
| 241 | 242 | >` |
| 242 | 243 | : canTell |
| 243 | 244 | ? html`<span class="muted">quiet — tell the crew something</span>` |
src/web/studio/words.ts +14
| @@ -27,6 +27,20 @@ import { | ||
| 27 | 27 | import { html, type Html } from "../html.ts"; |
| 28 | 28 | import { icon } from "../ui.ts"; |
| 29 | 29 | |
| 30 | /** | |
| 31 | * The empty crew, said once: the shelf's stuck tell, the app page's autonomy | |
| 32 | * card and the settings card all take it from here. | |
| 33 | */ | |
| 34 | export const NOBODY_ON_CREW = "nobody on the crew"; | |
| 35 | ||
| 36 | /** The quiet line under a mode nothing can honour yet. `href` is the crew panel. */ | |
| 37 | export function noCrewLine(href: string): Html { | |
| 38 | return html`<p class="muted small"> | |
| 39 | Mode is a promise with ${NOBODY_ON_CREW} to keep it. | |
| 40 | <a href="${href}">Put someone on the crew</a>. | |
| 41 | </p>`; | |
| 42 | } | |
| 43 | ||
| 30 | 44 | export type Word = |
| 31 | 45 | | "working" |
| 32 | 46 | | "look" |
test/studio.test.ts +38
| @@ -50,6 +50,7 @@ import type { Decider } from "../src/crew/decide.ts"; | ||
| 50 | 50 | import { CHECK_FAILED_LIMIT } from "../src/crew/dispatch.ts"; |
| 51 | 51 | import { serve, type Servers } from "../src/http/server.ts"; |
| 52 | 52 | import { |
| 53 | NOBODY_ON_CREW, | |
| 53 | 54 | pullWord, |
| 54 | 55 | runWord, |
| 55 | 56 | servingHistory, |
| @@ -5325,6 +5326,43 @@ describe("a stranger sees the app, not the owner's books", () => { | ||
| 5325 | 5326 | redirect: "manual", |
| 5326 | 5327 | }); |
| 5327 | 5328 | |
| 5329 | test("an autonomous app with nobody on its crew says so beside the mode, and stops saying it once a builder joins", async () => { | |
| 5330 | const made = await createRepo(app, alice, { name: "lamp", visibility: "public" }); | |
| 5331 | if (!made.ok) throw new Error(String(made.error)); | |
| 5332 | const lamp = made.value; | |
| 5333 | const seeded = await commitToBranch(app, alice, lamp, { | |
| 5334 | message: "Start", | |
| 5335 | files: [{ path: "README.md", content: "lamp\n" }], | |
| 5336 | }); | |
| 5337 | if (!seeded.ok) throw new Error(JSON.stringify(seeded.error)); | |
| 5338 | const enabled = await enableApp(app, alice, lamp, { slug: "lamp" }); | |
| 5339 | if (!enabled.ok) throw new Error(String(enabled.error)); | |
| 5340 | const on = await updateRepoSettings(app, alice, lamp, { | |
| 5341 | autonomy: "autonomous", | |
| 5342 | autonomyDirection: "", | |
| 5343 | autonomyCeilingUsd: 5, | |
| 5344 | autonomySlots: 1, | |
| 5345 | autonomyBy: alice.id, | |
| 5346 | }); | |
| 5347 | expect(on.ok).toBe(true); | |
| 5348 | ||
| 5349 | const empty = await (await get("/alice/lamp", ownerCookie)).text(); | |
| 5350 | expect(empty).toContain("Autonomous"); | |
| 5351 | expect(empty).toContain("nobody on the crew"); | |
| 5352 | expect(empty).toContain('href="/alice/lamp/settings">Put someone on the crew'); | |
| 5353 | // The shelf's stuck word and this card share one phrase. | |
| 5354 | expect(NOBODY_ON_CREW).toBe("nobody on the crew"); | |
| 5355 | ||
| 5356 | const crewed = await (await get("/alice/beacon", ownerCookie)).text(); | |
| 5357 | expect(crewed).not.toContain("nobody on the crew"); | |
| 5358 | ||
| 5359 | const agent = await createAgent(app, alice, { username: "wick" }); | |
| 5360 | if (!agent.ok) throw new Error(String(agent.error)); | |
| 5361 | expect(addToCrew(app, alice, lamp, "wick").ok).toBe(true); | |
| 5362 | const joined = await (await get("/alice/lamp", ownerCookie)).text(); | |
| 5363 | expect(joined).not.toContain("nobody on the crew"); | |
| 5364 | }); | |
| 5365 | ||
| 5328 | 5366 | test("the studio's settings page renders the crew and the ceiling for a writer, with the same numbers the app page shows", async () => { |
| 5329 | 5367 | const owner = await ( |
| 5330 | 5368 | await get("/alice/beacon/settings", ownerCookie) |
new above
offline