Public: anyone can read this; the crew can change it.
The terminal's New forge screen picks a template, Blank or Starter app, as MCP spawn's `template` does #8035
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/7934-the-terminal-s-new-forge-screen-picks-a → main 5 commits fork 4 threads · 4 unresolved
2 files
changed +38
−2
src/tui/screens/newforge.ts +10 −2
| @@ -35,6 +35,8 @@ const SIZE_CHOICES = Object.keys(SIZES) as Size[]; | ||
| 35 | 35 | |
| 36 | 36 | const TEMPLATE_CHOICES = TEMPLATES.map((t) => t.id); |
| 37 | 37 | |
| 38 | const TEMPLATE_HINT = "blank (default) or starter: left and right choose, or type it."; | |
| 39 | ||
| 38 | 40 | const SPECS: readonly Spec[] = [ |
| 39 | 41 | { key: "name", label: "Name", hint: "Lowercase letters, digits and dashes." }, |
| 40 | 42 | { key: "for", label: "For whom", hint: "A person or client, for your list of forges sent. Optional." }, |
| @@ -46,9 +48,15 @@ const SPECS: readonly Spec[] = [ | ||
| 46 | 48 | { key: "budgetUsd", label: "Budget in dollars", hint: `${MIN_BUDGET_USD}–${MAX_BUDGET_USD}, for the key's whole life. Needed with a key.` }, |
| 47 | 49 | { key: "followMode", label: "Follows this forge's code", hint: FOLLOW_LINE }, |
| 48 | 50 | { key: "size", label: "Size", hint: "small (default), medium or large: left and right choose, or type it.", choices: SIZE_CHOICES }, |
| 49 | { key: "template", label: "Template", hint: "blank (default) or starter: left and right choose, or type it.", choices: TEMPLATE_CHOICES }, | |
| 51 | { key: "template", label: "Template", hint: TEMPLATE_HINT, choices: TEMPLATE_CHOICES }, | |
| 50 | 52 | ]; |
| 51 | 53 | |
| 54 | /** The chosen template's own sentence; a typed id that is no template keeps the general hint. */ | |
| 55 | function templateHint(id: string): string { | |
| 56 | const t = TEMPLATES.find((x) => x.id === id); | |
| 57 | return t === undefined ? TEMPLATE_HINT : t.says; | |
| 58 | } | |
| 59 | ||
| 52 | 60 | /** The door's "field: reason" refusal, as the field it names and the reason. */ |
| 53 | 61 | function refusal(body: Record<string, unknown>): { key?: string; text: string } { |
| 54 | 62 | const text = String(body["message"] ?? "refused"); |
| @@ -199,7 +207,7 @@ export function newForgeScreen(f: Session, ctx: Ctx): Screen { | ||
| 199 | 207 | ...(focus === i ? { cursor: [...shown].length } : {}), |
| 200 | 208 | ...(error !== undefined && (errorKey === spec.key || (errorKey === undefined && focus === i)) |
| 201 | 209 | ? { error } |
| 202 | : { hint: spec.hint }), | |
| 210 | : { hint: spec.key === "template" ? templateHint(values["template"] || DEFAULT_TEMPLATE) : spec.hint }), | |
| 203 | 211 | }); |
| 204 | 212 | if (focus === i) cursor = at; |
| 205 | 213 | }); |
test/tui-newforge.test.ts +28
| @@ -207,6 +207,34 @@ describe("the new forge screen", () => { | ||
| 207 | 207 | expect(docs.find((d) => d?.name === "tuisize")?.size).toBe("medium"); |
| 208 | 208 | })); |
| 209 | 209 | |
| 210 | test("Blank is the default: the plan names no template", () => | |
outdated
The name says the default is sent as nothing, but the test never inspects what was sent. Assert on the body. |
||
| 211 | withScreen(admin, async ({ tui, terminal }) => { | |
| 212 | terminal.feed("tuiblank" + "\t".repeat(10)); | |
| 213 | await settleApp(tui); | |
| 214 | expect(tui.screen.toText()).toContain("Only the apps you fork"); | |
| 215 | terminal.feed("\x10"); | |
| 216 | await settleApp(tui); | |
| 217 | expect(tui.screen.toText()).toContain("The plan"); | |
| 218 | expect(tui.screen.toText()).not.toContain("Template:"); | |
| 219 | })); | |
| 220 | ||
| 221 | test("choosing starter shows its sentence and the plan names it", () => | |
| 222 | withScreen(admin, async ({ tui, terminal }) => { | |
| 223 | terminal.feed("tuistart" + "\t".repeat(10) + "\x1b[C"); // right arrow | |
| 224 | await settleApp(tui); | |
| 225 | expect(tui.screen.toText()).toContain("A server, a page, tests and a forge.json"); | |
| 226 | terminal.feed("\x10"); | |
| 227 | await settleApp(tui); | |
| 228 | expect(tui.screen.toText()).toContain("Template: Starter app."); | |
| 229 | })); | |
| 230 | ||
| 231 | test("an unknown template is refused beside the field", () => | |
| 232 | withScreen(admin, async ({ tui, terminal }) => { | |
| 233 | terminal.feed("tuitpl" + "\t".repeat(10) + "nope\x10"); | |
| 234 | await settleApp(tui); | |
| 235 | expect(tui.screen.toText()).toContain("template must be blank or starter"); | |
| 236 | })); | |
| 237 | ||
| 210 | 238 | test("an unknown size is refused beside the field", () => |
| 211 | 239 | withScreen(admin, async ({ tui, terminal }) => { |
| 212 | 240 | terminal.feed("tuibig" + "\t".repeat(9) + "huge\x10"); |
new above
offline
This repeats the string from SPECS line 49. Take the fallback from the spec so there is one source.