From 0b863148cdc3226de07239b8c76db3394199db02 Mon Sep 17 00:00:00 2001 From: Nayan Date: Sat, 15 Aug 2026 10:25:27 +0530 Subject: [PATCH] docs: explain the two catalogues, and gate the files they generate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents what the preceding commits added, so it lands after them rather than describing things that are not there yet. Both new generators are wired up here too, because a generated file that nothing checks is a file that drifts. The controls table in `README.md` is now generated from the command catalogue, which is the point of having one — the six hand-written rows it replaces had already drifted in the copy under `apps/cli/`. `make controls` rewrites it and `CONTROLS.md`, then syncs the CLI's README, and it has to run in that order because `sync-readme.mjs` copies the file the first step writes into. Both `make preflight` and the PR gate fail on a stale one. `make models:refresh` is deliberately not in either. Every other generated file in this repo derives from something committed beside it, so its drift gate can only fire when a human changed the input. This one derives from models.dev, which changes whenever a vendor ships a model, so gating on it would need network to pass and would turn PRs that touched nothing red. Treat a refresh like a lockfile bump. Both generated tables spell each chord for both platforms. A single column was Mac-only exactly where it mattered, which is the bug the catalogue exists to end, and `README.md` is the highest-traffic place to make it. The model flags get their own rows, the config file keys are documented as per-backend and why, and the two capability matrices gain a line for what each backend can enumerate about itself — the asymmetry the seed list exists for. --- .github/workflows/checks.yml | 12 +++++ CONTRIBUTING.md | 69 +++++++++++++++++++++++++++++ Makefile | 23 +++++++++- README.md | 65 +++++++++++++++++++++------ apps/cli/README.md | 85 +++++++++++++++++++++++++++++++----- 5 files changed, 229 insertions(+), 25 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index f75fe9f..fadd8e4 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -73,6 +73,18 @@ jobs: # generated file matches what the generator emits. That is a property of # the repo, not of the platform, so running it twice doubles the runtime # and the flake surface for no extra signal. + # CONTROLS.md and the short table in README.md are generated from the + # overlay's command catalog. Before the README check, because that one + # copies the root README.md that this one writes into. + - name: Controls reference is not stale + if: matrix.os == 'ubuntu-latest' + shell: bash + run: | + if ! node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/gen-controls.mjs --check; then + echo "::error file=CONTROLS.md::The controls reference is stale. Run 'make controls' and commit the result." + exit 1 + fi + - name: README is not stale if: matrix.os == 'ubuntu-latest' shell: bash diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9101288..b6a435a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,6 +49,8 @@ wrapper either way — every recipe is one `pnpm` or `node` call — so use thos | `make doctor` | `node apps/cli/dist/index.js doctor --cwd apps/web` | | `make check` | `pnpm lint && pnpm typecheck && pnpm test` | | `make readme` | `node scripts/sync-readme.mjs` | +| `make controls` | `node --experimental-strip-types scripts/gen-controls.mjs`, then `make readme` | +| `make models:refresh` | `node scripts/gen-models.mjs` — refetches the seed model list | | `make storybook`| `pnpm turbo run storybook --filter=@airship/overlay` | `pnpm dev:web` rather than a bare `vite dev`: the site cannot start until @@ -163,6 +165,8 @@ the adapter is explicit about each gap rather than faking it: | Cost | `total_cost_usd` | tokens only | | Fork a session | `forkSession` | starts a fresh thread, and says so | | `maxTurns` / `maxBudgetUsd` | enforced | unsupported; warned about at startup | +| Model selection | `options.model` | `ThreadOptions.model` | +| Listing its models | `query.supportedModels()`, account-scoped | **nothing** — no subcommand, no RPC | Codex items are normalized into the same tool vocabulary Claude uses (`command_execution` → `Bash`, `file_change` → `Edit`/`Write`/`Delete`), so one copy of the summarization rules serves @@ -186,6 +190,8 @@ It is the most capable of the three in several places, and the adapter uses all | Native undo | none | `session.revert`, snapshot-backed | | Cost | tokens only | real cost per message | | Reasoning effort | `modelReasoningEffort` | **none** — `--effort` is ignored | +| Model selection | `ThreadOptions.model` | `{providerID, modelID}` on the prompt | +| Listing its models | **nothing** | `client.config.providers()`, only what is authed | Gaps handled explicitly rather than faked: @@ -242,6 +248,69 @@ into a real value. That indirection is why the install command appears once, in why `resolve.ts` throws at module load on an unknown token rather than shipping the literal `{{installCommand}}` to a visitor. +### The controls reference + +`CONTROLS.md`, and the short table between the `` markers in +`README.md`, are generated: + +```bash +make controls # rewrites both, then syncs apps/cli/README.md, then commit them +``` + +The source is `packages/overlay/src/keys/catalog.ts` — the same table the runtime binds +from, the shortcuts panel and the ⌘K palette render from, and every tooltip chip resolves +against. Nothing else in the editor is allowed to spell a chord: `MenuItem.command` renders +one from the catalog, and `keys/catalog.test.ts` fails any `hint:` literal that looks like a +keystroke. + +That is not tidiness. Chords used to be string literals at each `keys.bind` call site, so +twenty-seven of the thirty-three shortcuts appeared nowhere in the product or the docs, five +menu rows showed Mac glyphs to Windows users, one advertised `⌘Z` for a feature ⌘Z has never +run, and the only reference — six hand-written rows in `README.md` — had already drifted in +its own copy under `apps/cli/`. + +`gen-controls.mjs` imports the `.ts` catalog directly under `--experimental-strip-types`, +which is why that module may contain **no value imports**; type imports are erased before +resolution and are free. `keys/catalog.test.ts` enforces it, and +`keys/controls-doc.test.ts` byte-compares the committed files against the same renderers the +script uses — so drift fails the suite even where the script itself cannot run. + +Run it before `make readme`, never after: it writes into the root `README.md` that +`sync-readme.mjs` copies. `make controls` does both in that order. + +### The seed model list + +`packages/protocol/src/models.ts` is generated from [models.dev](https://models.dev): + +```bash +make models:refresh # refetches, rewrites the module, then commit it +``` + +It exists because of an asymmetry between the backends. Claude answers +`query.supportedModels()` and OpenCode answers `client.config.providers()`, both live and both +scoped to what you are actually signed in to — so for those two this is only what the picker +paints before the answer arrives, and what it falls back to offline. **Codex can enumerate +nothing**, at any layer, so for that backend this file *is* the list. + +Two things about it are deliberate. + +**It is not in `make preflight`.** `gen-controls.mjs --check` belongs there because it derives +from a file committed beside it, so it can only drift when someone edits that file. This one +derives from a remote registry that changes whenever a vendor ships a model — gating on it +would need network to pass and would turn PRs that touched nothing red. `reference/NEXT-STEPS.md` +§7 describes what that costs. Treat a refresh like a lockfile bump: deliberate, and reviewed as +a diff. + +**Judgement lives in `scripts/models.curation.json`, not in the output.** The mechanical filter +— `tool_call`, `reasoning`, a release-date floor — admits things like `gpt-realtime-2.1`, which +is not a coding model. The deny list, the cap, and the Claude CLI aliases models.dev cannot know +about are all in that file, so the generated module stays purely derived and the taste is what +gets reviewed. + +The generator formats its own output through the repo's biome before writing. Without that, +`ultracite fix` would reformat the file the first time anyone linted and `--check` would report +stale against a file nobody touched. + ### The social card `public/og.png` is generated, not drawn: diff --git a/Makefile b/Makefile index 1bf5364..b94a2cf 100644 --- a/Makefile +++ b/Makefile @@ -151,6 +151,9 @@ preflight: ## Run the full CI gate locally (lint + typecheck + test + route drif @$(MAKE) typecheck @printf "$(CYAN)» Test$(RESET)\n" @$(MAKE) test + @printf "$(CYAN)» Controls reference$(RESET)\n" + @node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/gen-controls.mjs --check + @node scripts/sync-readme.mjs --check @printf "$(CYAN)» Route tree$(RESET)\n" @$(MAKE) "web:build" @if ! git diff --quiet -- $(GENERATED); then \ @@ -167,6 +170,8 @@ preflight: ## Run the full CI gate locally (lint + typecheck + test + route drif preflight\:fix: ## preflight, but autofix first (ultracite fix + regenerate routes) @printf "$(CYAN)» Fix (ultracite)$(RESET)\n" @$(MAKE) fix + @printf "$(CYAN)» Controls reference$(RESET)\n" + @$(MAKE) controls @printf "$(CYAN)» Route tree$(RESET)\n" @$(MAKE) "web:build" @printf "$(CYAN)» Typecheck$(RESET)\n" @@ -283,7 +288,23 @@ demo: ## One-shot: install + build, then print the two-terminal recipe ##@ Release (@airshiplabs/cli) -.PHONY: readme release release\:ci release\:retry release\:version +.PHONY: controls models\:refresh readme release release\:ci release\:retry release\:version + +# Before `readme`, always: this writes the short table into the root README.md, +# and `readme` copies the root README.md to apps/cli. Run the other way round +# and the CLI's copy is one generation behind. +controls: ## Regenerate CONTROLS.md and the README table from the command catalog + @node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/gen-controls.mjs + @$(MAKE) readme + +# Deliberately NOT in `preflight`, unlike `controls`. Every other generated file +# here derives from something committed beside it, so its drift gate can only +# fire when a human changed the input. This one derives from models.dev, which +# changes whenever a vendor ships a model — gating on it would need network to +# pass and would go red on PRs that touched nothing. See the header of +# scripts/gen-models.mjs, and NEXT-STEPS §7 for what that costs. +models\:refresh: ## Refresh the seed model catalogue from models.dev + @node scripts/gen-models.mjs readme: ## Regenerate apps/cli/README.md from the root README.md @node scripts/sync-readme.mjs diff --git a/README.md b/README.md index f75866e..c8cd562 100644 --- a/README.md +++ b/README.md @@ -106,16 +106,37 @@ to the canvas — the parameter takes the internal mode name, so it is `shell`, Open any route of your app in Airship — `/pricing`, `/settings` — and every frame opens there. -On the canvas: +The controls, in short — the table below is generated from the editor's own command +catalog, so it cannot drift from what the keys actually do: -| Gesture | | -| --- | --- | -| wheel / two-finger | pan | -| ⌘/ctrl-wheel, pinch | zoom at cursor | -| space-drag, middle-drag | pan | -| ⇧1 / ⇧2 / ⇧0 | fit / zoom to selection / 100% | -| `H` | hand tool (view mode) | -| `F` | add a frame | + +| | macOS | Windows / Linux | +| --- | --- | --- | +| pan the canvas | Wheel / two-finger | Wheel / two-finger | +| zoom at the cursor | ⌘-wheel / pinch | Ctrl-wheel / pinch | +| pan without the hand | Space-drag | Space-drag | +| select an element | Click | Click | +| edit text in place | Double-click | Double-click | +| open the element menu | Right-click | Right-click | +| scrub a number | Drag a field's glyph | Drag a field's glyph | +| undo | ⌘Z | Ctrl+Z | +| delete element | ⌫ or Del | Backspace or Del | +| duplicate | ⌘D | Ctrl+D | +| edit text | ↩ or T | Enter or T | +| move | V | V | +| inspect | I | I | +| zoom in | ⌘= or = | Ctrl+= or = | +| zoom out | ⌘- or - | Ctrl+- or - | +| zoom to 100% | ⌘0 or ⇧0 | Ctrl+0 or Shift+0 | +| zoom to fit | ⇧1 | Shift+1 | +| hand tool | H | H | +| add a frame | F | F | +| send | ⌘↩ | Ctrl+Enter | +| keyboard shortcuts | ? | ? | +| command palette | ⌘K | Ctrl+K | + +Press `?` in the editor for all of them, or see [CONTROLS.md](./CONTROLS.md). + ## Edit and View @@ -166,6 +187,7 @@ you what you're giving up at startup. | `--effort` | yes | yes | **ignored** | | `--max-turns`, `--max-budget` | yes | **ignored** | **ignored** | | `--model` | a model name | a model name | needs the `provider/model` form | +| Lists its own models | yes | **no** — Airship ships a list | yes, the ones you are signed in to | | `--safe` | checks each edit and command | **real sandbox** | asks before each edit and command | | Install | included | included | **you install it yourself** | @@ -255,7 +277,7 @@ forwarded. | Flag | | Default | | --- | --- | --- | | `-a, --agent ` | Coding agent: `claude`, `codex`, `opencode`. | `claude` | -| `-m, --model ` | Model id. | the agent's own default | +| `-m, --model ` | Model for whichever agent runs. Per-backend flags below outrank it. | the agent's own default | | `--effort ` | Reasoning effort: `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. | | | `--max-turns ` | Cap agent turns per edit (claude only). | `24` | | `--max-budget ` | Stop an edit if it exceeds this cost in USD (claude only). | | @@ -263,6 +285,12 @@ forwarded. `-m` is `--model`, not `--mode`. `--mode` has no short alias. +You can also pick the model from the editor. The agent button in the chat header opens a +group per backend; choosing a row picks the backend **and** its model in one go, and a box +at the bottom takes any id the list does not offer. That choice is per backend and is +remembered, so switching between them does not carry one backend's model to another. The +flags below are the resting default it starts from. + ### Sandbox | Flag | | Default | @@ -273,6 +301,9 @@ forwarded. | Flag | | Default | | --- | --- | --- | +| `--claude-model ` | Model for the `claude` backend. Takes an alias or a full id. | `--model`, then the agent's own | +| `--codex-model ` | Model for the `codex` backend. | `--model`, then the agent's own | +| `--opencode-model ` | Model for the `opencode` backend. Needs the `provider/model` form. | `--model`, then the agent's own | | `--codex-path ` | Path to the `codex` binary. | bundled | | `--codex-config ` | Extra `codex --config` pair; repeatable. | | | `--opencode-path ` | Path to the `opencode` binary. | found on PATH | @@ -339,10 +370,17 @@ one. Every key is a flag name, in either kebab or camel case: "agent": "claude", "mode": "canvas", "target": 3000, - "safe": true + "safe": true, + "claudeModel": "opus", + "codexModel": "gpt-5.3-codex" } ``` +Models are keyed per backend — `claudeModel`, `codexModel`, `opencodeModel` — because the +editor's picker can switch backends mid-session, and one shared `model` would follow it and +hand Codex an id only Claude answers to. A plain `"model"` still works and applies to +whichever backend runs, with the per-backend keys taking precedence. + Airship looks for it from `--cwd` upwards and **stops at your repository root**, so a stray config file somewhere above your repo won't affect you. Misspell a key and it says so, with a suggestion — it never quietly ignores one. @@ -359,8 +397,9 @@ AIRSHIP_CWD AIRSHIP_EFFORT AIRSHIP_OPENCODE_PATH AIRSHIP_MODE AIRSHIP_MAX_TURNS AIRSHIP_OPENCODE_URL AIRSHIP_EXEC AIRSHIP_MAX_BUDGET AIRSHIP_OPENCODE_AGENT AIRSHIP_OPEN AIRSHIP_COMMIT AIRSHIP_OPENCODE_CONFIG -AIRSHIP_SAFE AIRSHIP_JSON AIRSHIP_QUIET -AIRSHIP_DEBUG +AIRSHIP_SAFE AIRSHIP_JSON AIRSHIP_OPENCODE_MODEL +AIRSHIP_DEBUG AIRSHIP_QUIET AIRSHIP_CLAUDE_MODEL + AIRSHIP_CODEX_MODEL ``` `AIRSHIP_HELP` and `AIRSHIP_VERSION` are deliberately not read — exporting one would leave the diff --git a/apps/cli/README.md b/apps/cli/README.md index 9146a0f..fc8a7d8 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -108,14 +108,59 @@ to the canvas — the parameter takes the internal mode name, so it is `shell`, Open any route of your app in Airship — `/pricing`, `/settings` — and every frame opens there. -On the canvas: +The controls, in short — the table below is generated from the editor's own command +catalog, so it cannot drift from what the keys actually do: -| Gesture | | -| --- | --- | -| wheel / two-finger | pan | -| ⌘/ctrl-wheel, pinch | zoom at cursor | -| space-drag, middle-drag | pan | -| ⇧1 / ⇧2 / ⇧0 | fit / zoom to selection / 100% | + +| | macOS | Windows / Linux | +| --- | --- | --- | +| pan the canvas | Wheel / two-finger | Wheel / two-finger | +| zoom at the cursor | ⌘-wheel / pinch | Ctrl-wheel / pinch | +| pan without the hand | Space-drag | Space-drag | +| select an element | Click | Click | +| edit text in place | Double-click | Double-click | +| open the element menu | Right-click | Right-click | +| scrub a number | Drag a field's glyph | Drag a field's glyph | +| undo | ⌘Z | Ctrl+Z | +| delete element | ⌫ or Del | Backspace or Del | +| duplicate | ⌘D | Ctrl+D | +| edit text | ↩ or T | Enter or T | +| move | V | V | +| inspect | I | I | +| zoom in | ⌘= or = | Ctrl+= or = | +| zoom out | ⌘- or - | Ctrl+- or - | +| zoom to 100% | ⌘0 or ⇧0 | Ctrl+0 or Shift+0 | +| zoom to fit | ⇧1 | Shift+1 | +| hand tool | H | H | +| add a frame | F | F | +| send | ⌘↩ | Ctrl+Enter | +| keyboard shortcuts | ? | ? | +| command palette | ⌘K | Ctrl+K | + +Press `?` in the editor for all of them, or see [CONTROLS.md](https://github.com/0xnyn/airship/blob/main/CONTROLS.md). + + +## Edit and View + +The two modes point the editor at different things, and the panels follow. + +**Edit** is about an element: hover to highlight, click to select, and the agent panel and +inspector are open on either side of it. + +**View** is about your frames. The page underneath is fully interactive — click through it, +fill in forms, scroll — so there is no element selection, and the two panels that depend on +one step aside. In their place the left panel lists every frame, and a minimap appears in +the bottom-right corner: + +- Click a frame in the list to go to it without changing your zoom; double-click to zoom + to it. Rename it in place, and use `⋯` for its device size, rotate, duplicate and delete. +- The list is stacking order, front-most at the top. Drag a row anywhere along it — or + press ↑↓ on its handle — to restack frames that overlap on the canvas; up is forward. +- Drag the minimap's indicator and the canvas travels with it; press anywhere else on the + map to jump there. Pan far off your frames and it keeps pointing back at them. + +Your panel arrangement is remembered per mode, so switching back returns the inspector +exactly as you left it. ## Inspector @@ -144,6 +189,7 @@ you what you're giving up at startup. | `--effort` | yes | yes | **ignored** | | `--max-turns`, `--max-budget` | yes | **ignored** | **ignored** | | `--model` | a model name | a model name | needs the `provider/model` form | +| Lists its own models | yes | **no** — Airship ships a list | yes, the ones you are signed in to | | `--safe` | checks each edit and command | **real sandbox** | asks before each edit and command | | Install | included | included | **you install it yourself** | @@ -233,7 +279,7 @@ forwarded. | Flag | | Default | | --- | --- | --- | | `-a, --agent ` | Coding agent: `claude`, `codex`, `opencode`. | `claude` | -| `-m, --model ` | Model id. | the agent's own default | +| `-m, --model ` | Model for whichever agent runs. Per-backend flags below outrank it. | the agent's own default | | `--effort ` | Reasoning effort: `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. | | | `--max-turns ` | Cap agent turns per edit (claude only). | `24` | | `--max-budget ` | Stop an edit if it exceeds this cost in USD (claude only). | | @@ -241,6 +287,12 @@ forwarded. `-m` is `--model`, not `--mode`. `--mode` has no short alias. +You can also pick the model from the editor. The agent button in the chat header opens a +group per backend; choosing a row picks the backend **and** its model in one go, and a box +at the bottom takes any id the list does not offer. That choice is per backend and is +remembered, so switching between them does not carry one backend's model to another. The +flags below are the resting default it starts from. + ### Sandbox | Flag | | Default | @@ -251,6 +303,9 @@ forwarded. | Flag | | Default | | --- | --- | --- | +| `--claude-model ` | Model for the `claude` backend. Takes an alias or a full id. | `--model`, then the agent's own | +| `--codex-model ` | Model for the `codex` backend. | `--model`, then the agent's own | +| `--opencode-model ` | Model for the `opencode` backend. Needs the `provider/model` form. | `--model`, then the agent's own | | `--codex-path ` | Path to the `codex` binary. | bundled | | `--codex-config ` | Extra `codex --config` pair; repeatable. | | | `--opencode-path ` | Path to the `opencode` binary. | found on PATH | @@ -317,10 +372,17 @@ one. Every key is a flag name, in either kebab or camel case: "agent": "claude", "mode": "canvas", "target": 3000, - "safe": true + "safe": true, + "claudeModel": "opus", + "codexModel": "gpt-5.3-codex" } ``` +Models are keyed per backend — `claudeModel`, `codexModel`, `opencodeModel` — because the +editor's picker can switch backends mid-session, and one shared `model` would follow it and +hand Codex an id only Claude answers to. A plain `"model"` still works and applies to +whichever backend runs, with the per-backend keys taking precedence. + Airship looks for it from `--cwd` upwards and **stops at your repository root**, so a stray config file somewhere above your repo won't affect you. Misspell a key and it says so, with a suggestion — it never quietly ignores one. @@ -337,8 +399,9 @@ AIRSHIP_CWD AIRSHIP_EFFORT AIRSHIP_OPENCODE_PATH AIRSHIP_MODE AIRSHIP_MAX_TURNS AIRSHIP_OPENCODE_URL AIRSHIP_EXEC AIRSHIP_MAX_BUDGET AIRSHIP_OPENCODE_AGENT AIRSHIP_OPEN AIRSHIP_COMMIT AIRSHIP_OPENCODE_CONFIG -AIRSHIP_SAFE AIRSHIP_JSON AIRSHIP_QUIET -AIRSHIP_DEBUG +AIRSHIP_SAFE AIRSHIP_JSON AIRSHIP_OPENCODE_MODEL +AIRSHIP_DEBUG AIRSHIP_QUIET AIRSHIP_CLAUDE_MODEL + AIRSHIP_CODEX_MODEL ``` `AIRSHIP_HELP` and `AIRSHIP_VERSION` are deliberately not read — exporting one would leave the