From 9b491bb9be44cb7914a188cf66fb2a567b2994ba Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sat, 7 Nov 2026 08:53:52 -0800 Subject: [PATCH] fix(render): offer the Aside remedy only on macOS (#3735); fix docs/skills.md vision-model line (#3375) (#4131) * fix(render): offer the Aside remedy only on macOS (#3735) NO_BROWSER_HELP told Linux and Windows hosts to open the Aside app, which ships only for macOS. noBrowserHelp(platform) now adds the Aside remedy on darwin only and keeps the ./setup and GSTACK_BROWSE_BIN remedies everywhere. Also corrects docs/skills.md's /design-html refinement loop, which still said "Uses GPT-4o vision", to match the design-html template (#3375). Rewritten on main from outside PR #4122. Co-authored-by: szsunyuan <3579304+szsunyuan@users.noreply.github.com> Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore(changelog): name the #3735 fragment after the branch Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: szsunyuan <3579304+szsunyuan@users.noreply.github.com> Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- changelog.d/capy-offer-the-aside-remedy.md | 1 + docs/skills.md | 2 +- lib/aside-render.ts | 7 ++++++- make-pdf/test/asideClient.test.ts | 2 +- make-pdf/test/setup-smoke.test.ts | 4 ++-- test/aside-render.test.ts | 19 ++++++++++++++++++- 6 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 changelog.d/capy-offer-the-aside-remedy.md diff --git a/changelog.d/capy-offer-the-aside-remedy.md b/changelog.d/capy-offer-the-aside-remedy.md new file mode 100644 index 000000000..03a38a55a --- /dev/null +++ b/changelog.d/capy-offer-the-aside-remedy.md @@ -0,0 +1 @@ +- **Missing-browser errors stop telling Linux and Windows hosts to open the Aside app.** `NO_BROWSER_HELP`, used by `/make-pdf` and the renderer's no-browser error, now offers the Aside remedy only on macOS, where Aside ships, and keeps the `./setup` and `GSTACK_BROWSE_BIN` remedies on every platform. The docs/skills.md `/design-html` refinement loop also stops naming GPT-4o and describes the configured vision model, matching the template. (#3735, #3375; contributed by @szsunyuan in #4122) diff --git a/docs/skills.md b/docs/skills.md index 1b343d681..10c8a1187 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -467,7 +467,7 @@ Not every page needs the full Pretext engine. The skill reads the design and pic ### The refinement loop 1. Reads the approved mockup from `approved.json`, plus any page archetype and section outline you picked on the board, which it builds as given instead of asking again -2. Uses GPT-4o vision to extract implementation spec (colors, typography, layout) +2. Extracts an implementation spec (colors, typography, layout) with the provider's vision model (OpenAI gpt-5.5, or Gemini when only a Gemini key is set; `GSTACK_DESIGN_MODEL` overrides) 3. Generates self-contained HTML with Pretext inlined (15KB, zero network dependency) 4. Spins up a live-reload server so you see changes instantly 5. Screenshots at 3 viewports (mobile, tablet, desktop) to verify layout diff --git a/lib/aside-render.ts b/lib/aside-render.ts index adef341e6..4cc47dab0 100644 --- a/lib/aside-render.ts +++ b/lib/aside-render.ts @@ -678,7 +678,12 @@ export async function renderWithBrowse(spec: RenderSpec, bin: string | null = re // ─── Engine choice ─────────────────────────────────────────────────────────── export const NO_BROWSER = 'no browser available'; -export const NO_BROWSER_HELP = "open the Aside app (macOS 15+, aside.com), or run ./setup in the gstack repo to build gstack's own headless browser (or point GSTACK_BROWSE_BIN at a browse binary)"; +/** Recovery advice for a missing browser. Aside ships only for macOS, so other platforms get only the bundled-browser remedies. */ +export function noBrowserHelp(platform: NodeJS.Platform = process.platform): string { + const aside = platform === 'darwin' ? 'open the Aside app (macOS 15+, aside.com), or ' : ''; + return `${aside}run ./setup in the gstack repo to build gstack's own headless browser (or point GSTACK_BROWSE_BIN at a browse binary)`; +} +export const NO_BROWSER_HELP = noBrowserHelp(); export type EngineChoice = | { engine: 'aside'; version: string } diff --git a/make-pdf/test/asideClient.test.ts b/make-pdf/test/asideClient.test.ts index d2ae96622..451732b6f 100644 --- a/make-pdf/test/asideClient.test.ts +++ b/make-pdf/test/asideClient.test.ts @@ -126,7 +126,7 @@ describe("renderPdf", () => { const none = async (): Promise => ({ ok: false, outputs: [], evals: {}, stdout: "", error: `${NO_BROWSER}: ${NO_BROWSER_HELP} (NEEDS_ASIDE: aside not on PATH)` }); const err = await renderPdf("

", { output: "/tmp/never.pdf" }, none).catch((e: Error) => e); expect(err).toBeInstanceOf(BrowserUnavailableError); - expect((err as Error).message).toContain("aside.com"); + expect((err as Error).message).toContain(NO_BROWSER_HELP); expect((err as Error).message).toContain("./setup"); expect((err as Error).message).toContain("GSTACK_BROWSE_BIN"); }); diff --git a/make-pdf/test/setup-smoke.test.ts b/make-pdf/test/setup-smoke.test.ts index ce48b7462..88e0a0d56 100644 --- a/make-pdf/test/setup-smoke.test.ts +++ b/make-pdf/test/setup-smoke.test.ts @@ -124,7 +124,7 @@ const smokeFixture = () => path.join(OUTPUT_TMP_DIR, `make-pdf-smoke-${process.p // ─── [1/5] browser check ────────────────────────────────────────────────────── describe("runSetup [1/5]: no browser at all", () => { - test("Aside absent and no browse binary: FAIL, the NO_BROWSER text with both remedies, exit 4, never reaches [2/5]", async () => { + test("Aside absent and no browse binary: FAIL, the NO_BROWSER text with this platform's remedies, exit 4, never reaches [2/5]", async () => { pickEngine(true, { probe: noAside, resolveBin: () => null }); const run = await captureRun(() => runSetup()); expect(run.error).toBeUndefined(); @@ -156,7 +156,7 @@ describe.skipIf(isWin)("runSetup [2/5]: render smoke through gstack's own browse expect(run.stderr).toContain("[2/5] Rendering through gstack browser... FAIL"); expect(run.stderr).toContain("gstack browser could not render a page: unexpected page text: not-the-smoke-page"); expect(run.stderr).toContain(`To fix: ${NO_BROWSER_HELP}`); - expect(run.stderr).toContain("open the Aside app"); + expect(run.stderr.includes("open the Aside app")).toBe(process.platform === "darwin"); expect(run.stderr).toContain("./setup"); expect(run.stderr).not.toContain("[3/5]"); expect(smokeDirs(stage)).toEqual(before); // finally { rmSync(smokeDir) } ran despite the exit diff --git a/test/aside-render.test.ts b/test/aside-render.test.ts index 88bdc6880..d9139b356 100644 --- a/test/aside-render.test.ts +++ b/test/aside-render.test.ts @@ -14,7 +14,7 @@ import { spawnSync } from 'child_process'; import { buildRenderScript, lengthToInches, paperInches, renderWithAside, RENDER_SENTINEL, resolveBrowseBin, browsePdfPayload, browseScreenshotArgs, renderWithBrowse, NO_BROWSER, - serveDir, pickEngine, SAFE_TMP_DIR, + serveDir, pickEngine, SAFE_TMP_DIR, noBrowserHelp, NO_BROWSER_HELP, type RenderSpec, type RenderResult, type AsideProbe, type EngineChoice, } from '../lib/aside-render'; import { asideAvailable } from './helpers/aside-available'; @@ -950,6 +950,23 @@ describe.skipIf(!HERMETIC)('aside-render: renderWithBrowse — daemon CLI contra }, 20_000); }); +describe('aside-render: missing-browser advice (#3735)', () => { + // Value: protects=Aside recovery advice only on macOS, bundled-browser advice everywhere; fails_when=Linux or Windows hosts are told to open the macOS-only Aside app, or a host loses the ./setup remedy; why_new=no test covered the advice per platform; seam=none + test.each(['linux', 'win32', 'darwin'] as const)('%s gets only the remedies it can use', (platform) => { + const help = noBrowserHelp(platform); + expect(help).toContain('./setup'); + expect(help).toContain('GSTACK_BROWSE_BIN'); + expect(help.includes('open the Aside app')).toBe(platform === 'darwin'); + expect(help.includes('aside.com')).toBe(platform === 'darwin'); + }); + + test('NO_BROWSER_HELP and the no-engine error use this host\'s advice', () => { + expect(NO_BROWSER_HELP).toBe(noBrowserHelp(process.platform)); + const choice = pickEngine(true, { probe: () => ({ ok: false, reason: 'NEEDS_ASIDE', detail: 'stub' }), resolveBin: () => null }); + expect(choice.engine === null && choice.error).toBe(`${NO_BROWSER}: ${NO_BROWSER_HELP} (NEEDS_ASIDE: stub)`); + }); +}); + describe('aside-render: pickEngine — cached engine choice through the probe/resolver seam', () => { const notRunning: AsideProbe = { ok: false, reason: 'ASIDE_NOT_RUNNING', detail: 'the app is closed' }; const noBin = (): null => null;