gearbox-code 0.1.36 → 0.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.mjs +434 -104
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -142793,6 +142793,206 @@ class RoutingSelector {
142793
142793
  init_reasoning();
142794
142794
  init_providers();
142795
142795
 
142796
+ // src/ui/panel.ts
142797
+ var clamp2 = (n, lo, hi) => Math.max(lo, Math.min(n, hi));
142798
+ var clampIndex = (i2, count) => count <= 0 ? 0 : clamp2(i2, 0, count - 1);
142799
+ var clampScroll = (s2, max2) => clamp2(s2, 0, Math.max(0, max2));
142800
+ var panelBodyHeight = (height) => Math.max(1, height - 2);
142801
+ function windowStart(index, count, viewH) {
142802
+ if (count <= viewH)
142803
+ return 0;
142804
+ const half = Math.floor(viewH / 2);
142805
+ return clamp2(index - half, 0, count - viewH);
142806
+ }
142807
+ function filterModelRows(rows, filter7) {
142808
+ const q = filter7.trim().toLowerCase();
142809
+ if (!q)
142810
+ return rows;
142811
+ return rows.filter((r2) => r2.label.toLowerCase().includes(q) || r2.id.toLowerCase().includes(q) || r2.provider.toLowerCase().includes(q));
142812
+ }
142813
+ function appendFilter(panel, ch) {
142814
+ return { ...panel, filter: panel.filter + ch, index: 0 };
142815
+ }
142816
+ function backspaceFilter(panel) {
142817
+ return { ...panel, filter: panel.filter.slice(0, -1), index: 0 };
142818
+ }
142819
+
142820
+ // src/ui/components/Panel.tsx
142821
+ var jsx_dev_runtime12 = __toESM(require_jsx_dev_runtime(), 1);
142822
+ function accountStateColor3(status) {
142823
+ if (status === "active")
142824
+ return color.ok;
142825
+ if (/not signed in/i.test(status))
142826
+ return color.err;
142827
+ return color.faint;
142828
+ }
142829
+ function Panel({
142830
+ panel,
142831
+ width,
142832
+ height,
142833
+ accounts,
142834
+ models,
142835
+ currentModelId,
142836
+ staticLines
142837
+ }) {
142838
+ const bodyH = panelBodyHeight(height);
142839
+ const innerW = Math.max(4, width - 2);
142840
+ let body = null;
142841
+ let hint = "esc close";
142842
+ if (panel.kind === "static") {
142843
+ const lines = staticLines ?? itemsToLines(panel.items, innerW);
142844
+ const maxScroll = Math.max(0, lines.length - bodyH);
142845
+ const scroll = Math.min(panel.scroll, maxScroll);
142846
+ body = /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
142847
+ paddingX: 1,
142848
+ children: /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Viewport, {
142849
+ lines,
142850
+ scrollTop: scroll,
142851
+ height: bodyH,
142852
+ width: innerW
142853
+ }, undefined, false, undefined, this)
142854
+ }, undefined, false, undefined, this);
142855
+ hint = lines.length > bodyH ? "↑↓ / PgUp PgDn scroll · esc close" : "esc close";
142856
+ } else if (panel.kind === "accounts") {
142857
+ const rows = accounts?.rows ?? [];
142858
+ const idx = clampIndex(panel.index, rows.length);
142859
+ const start = windowStart(idx, rows.length, bodyH);
142860
+ const slice2 = rows.slice(start, start + bodyH);
142861
+ const labelPad = accounts?.labelPad ?? 0;
142862
+ body = /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
142863
+ flexDirection: "column",
142864
+ paddingX: 1,
142865
+ children: rows.length === 0 ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
142866
+ color: color.faint,
142867
+ children: "no accounts yet — /account add to add one"
142868
+ }, undefined, false, undefined, this) : slice2.map((r2, i2) => {
142869
+ const sel = start + i2 === idx;
142870
+ return /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
142871
+ backgroundColor: sel ? color.accentBg : undefined,
142872
+ children: [
142873
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
142874
+ color: sel ? color.accent : color.faint,
142875
+ children: sel ? "▶ " : " "
142876
+ }, undefined, false, undefined, this),
142877
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
142878
+ color: color.text,
142879
+ bold: r2.active,
142880
+ children: r2.name.padEnd(labelPad)
142881
+ }, undefined, false, undefined, this),
142882
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
142883
+ color: color.faint,
142884
+ children: [
142885
+ " ",
142886
+ r2.type
142887
+ ]
142888
+ }, undefined, true, undefined, this),
142889
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
142890
+ color: accountStateColor3(r2.status),
142891
+ children: [
142892
+ " ",
142893
+ r2.status
142894
+ ]
142895
+ }, undefined, true, undefined, this),
142896
+ r2.active ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
142897
+ color: color.ok,
142898
+ children: [
142899
+ " ",
142900
+ glyph.on,
142901
+ " current"
142902
+ ]
142903
+ }, undefined, true, undefined, this) : null
142904
+ ]
142905
+ }, r2.alias, true, undefined, this);
142906
+ })
142907
+ }, undefined, false, undefined, this);
142908
+ hint = "↑↓ move · ⏎ switch · 1–9 jump · esc close";
142909
+ } else {
142910
+ const rows = filterModelRows(models ?? [], panel.filter);
142911
+ const idx = clampIndex(panel.index, rows.length);
142912
+ const start = windowStart(idx, rows.length, bodyH);
142913
+ const slice2 = rows.slice(start, start + bodyH);
142914
+ body = /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
142915
+ flexDirection: "column",
142916
+ paddingX: 1,
142917
+ children: rows.length === 0 ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
142918
+ color: color.faint,
142919
+ children: [
142920
+ "no models match “",
142921
+ panel.filter,
142922
+ "”"
142923
+ ]
142924
+ }, undefined, true, undefined, this) : slice2.map((r2, i2) => {
142925
+ const sel = start + i2 === idx;
142926
+ const pinned = r2.id === currentModelId;
142927
+ return /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
142928
+ backgroundColor: sel ? color.accentBg : undefined,
142929
+ children: [
142930
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
142931
+ color: sel ? color.accent : color.faint,
142932
+ children: sel ? "▶ " : " "
142933
+ }, undefined, false, undefined, this),
142934
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
142935
+ color: pinned ? color.ok : color.text,
142936
+ bold: pinned,
142937
+ children: r2.label.padEnd(22)
142938
+ }, undefined, false, undefined, this),
142939
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
142940
+ color: color.faint,
142941
+ children: r2.provider
142942
+ }, undefined, false, undefined, this),
142943
+ pinned ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
142944
+ color: color.ok,
142945
+ children: [
142946
+ " ",
142947
+ glyph.on,
142948
+ " pinned"
142949
+ ]
142950
+ }, undefined, true, undefined, this) : null
142951
+ ]
142952
+ }, r2.id, true, undefined, this);
142953
+ })
142954
+ }, undefined, false, undefined, this);
142955
+ hint = `filter: ${panel.filter || "(type to filter)"} · ↑↓ · ⏎ pin · esc close`;
142956
+ }
142957
+ return /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
142958
+ flexDirection: "column",
142959
+ width,
142960
+ height,
142961
+ children: [
142962
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
142963
+ width,
142964
+ paddingX: 1,
142965
+ justifyContent: "space-between",
142966
+ children: [
142967
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
142968
+ color: color.accent,
142969
+ bold: true,
142970
+ children: panel.title
142971
+ }, undefined, false, undefined, this),
142972
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
142973
+ color: color.faint,
142974
+ children: "esc to close"
142975
+ }, undefined, false, undefined, this)
142976
+ ]
142977
+ }, undefined, true, undefined, this),
142978
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
142979
+ flexDirection: "column",
142980
+ width,
142981
+ height: bodyH,
142982
+ children: body
142983
+ }, undefined, false, undefined, this),
142984
+ /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
142985
+ width,
142986
+ paddingX: 1,
142987
+ children: /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
142988
+ color: color.faint,
142989
+ children: hint
142990
+ }, undefined, false, undefined, this)
142991
+ }, undefined, false, undefined, this)
142992
+ ]
142993
+ }, undefined, true, undefined, this);
142994
+ }
142995
+
142796
142996
  // src/agent/run.ts
142797
142997
  init_dist26();
142798
142998
  init_providers();
@@ -144700,7 +144900,7 @@ bun run typecheck
144700
144900
  },
144701
144901
  {
144702
144902
  file: "CLAUDE.md",
144703
- text: "# Gearbox — project guide\n\nGearbox is a multi-provider coding harness for the terminal: a beautiful, simple terminal agent that reads/writes code and runs commands, talking to any provider (Anthropic, OpenAI, Google, DeepSeek) through one clean loop.\n\n**The point of the project:** intelligent per-task *model routing* — automatically picking the right model for each task across every provider and account you pay for. Basic routing is live (`RoutingSelector` — classify → quality bar → cheapest winner); the richer engine (shadow-eval, credit/limit penalties, confidence display) layers on top of the same seam. See `DESIGN.md` for the full vision and `experiments/FINDINGS.md` for the validation behind it.\n\n## The one rule that matters\n\n**Keep the routing seam clean.** The agent must never hardcode a model. It asks a `ModelSelector` for the model to use. `RoutingSelector` is the live default (classify task → filter by quality bar → cheapest winner); `FixedSelector` is used only when a model is explicitly pinned (`--model` flag or `/model <name>`). Concretely:\n\n- `src/model/selector.ts` — the seam. `select(task) => ModelChoice`. Do not bypass it.\n- `src/model/router.ts` — `RoutingSelector`: classify prompt → quality bar → cost-sort candidates → respect `/prefer` preferences.\n- `src/model/profiles.ts` — the data corpus: quality, cost, latency, tokenizer calibration per model. Routing reads this.\n- `src/providers.ts` — maps a provider+model id to an AI SDK model instance. Already multi-provider. Adding a model is data, not code.\n- Every model call captures token usage (`src/agent/run.ts`) so the cost engine has data. Do not drop usage.\n- The UI consumes a normalized `AgentEvent` stream (`src/agent/events.ts`), never the AI SDK's raw types. This decouples the UI from the provider layer and from routing.\n\nIf you find yourself writing `anthropic('claude-...')` anywhere outside `providers.ts`, stop — route it through the selector.\n\n## Layout\n\n```\nsrc/\n cli.tsx entry point; renders the Ink app; picks RoutingSelector by default\n config.ts minimal config (default model, provider from env)\n providers.ts provider+model id -> AI SDK model (multi-provider; contextWindow per model)\n commands.ts slash-command metadata + pure helpers (fuzzy model match, /help, model list)\n tools.ts read / write / edit / list / search / glob / run_shell (AI SDK tools)\n model/\n selector.ts THE ROUTING SEAM — ModelSelector interface + FixedSelector (pinned model)\n router.ts RoutingSelector: classify → quality bar → cost-sort → preferences (the live default)\n profiles.ts model corpus: quality (SWE-bench), cost ($/Mtok), latency, tokenizer calibration\n tokens.ts calibrated token counting (js-tiktoken × per-model calibration factor)\n preferences.ts persist /prefer kind model choices to ~/.gearbox/routing-preferences.json\n reasoning.ts reasoning/thinking config helpers\n context/\n builder.ts context engine: system + memory + repo map + retrieved files + curated history\n retrieve.ts BM25 lexical retrieval — top-K relevant files for a prompt (no model call)\n repomap.ts repo structure summary for the system prompt\n memory.ts project memory (GEARBOX.md / CLAUDE.md loaded into context)\n compact.ts context compaction (/compact)\n accounts/\n types.ts Account + AuthMethod types (API key, AWS, Azure, Vertex, CLI, OpenAI-compat)\n store.ts accounts.json persistence (~/.gearbox/accounts.json)\n catalog.ts provider catalog (known providers, env vars, labels)\n detect.ts auto-detect env creds + cloud credentials\n onboard.ts interactive add/test account flows\n resolve.ts credential resolution (Account → ResolvedCreds, fetching secrets on demand)\n discover.ts per-account model discovery (Azure deployments / Foundry / gateway /models) → account.models; catalog defaultModels are seeds, not callable ids\n usage.ts per-account spend ledger + rate-limit snapshots + balance tracking\n balance.ts provider balance fetch helpers\n help/\n ask.ts /ask corpus: bundled docs + generated command reference, system prompt, meta-question auto-detect\n agent/\n events.ts AgentEvent — normalized stream the UI consumes\n run.ts real agent loop (AI SDK streamText -> AgentEvent), abort-aware; runCompletion = tool-less grounded answer (used by /ask)\n cli-backend.ts claude/codex CLI subprocess backend (for Pro/Max subscriptions)\n mock.ts scripted demo stream (runs with no API key; used by tests)\n ui/\n theme.ts colors + glyphs (the look)\n input.ts pure key→action reducer for the composer (tested)\n history.ts pure ↑/↓ prompt-history nav (tested)\n net.ts background online probe; status bar shows ⚠ offline when down\n useTerminalSize.ts reactive width on resize (everything reflows)\n git.ts current branch for the status line\n App.tsx the Ink app: state, useInput dispatch, commands, turns\n components/ Banner, Transcript, Composer, CommandPalette, StatusBar, PermissionPrompt\ntest/ pure-logic + render tests (ink-testing-library); no keys\nDESIGN.md full product vision (routing, requirements, UX)\nexperiments/ prototypes that validated the architecture\n```\n\nThe composer is custom (Ink `useInput` + `src/ui/input.ts`), not a third-party widget — full control over the cursor, ↑/↓ history, and esc-to-interrupt, with no focus/remount fragility. **Multi-line**: ⌃J (or shift/alt+⏎) inserts a newline, ⏎ submits; ↑/↓ move between lines and fall through to history at the top/bottom line; bracketed paste (enabled in `cli.tsx`) inserts multi-line text literally (CR normalized, paste markers stripped) instead of submitting per line. `caretPos()` is the shared line/col helper. **Readline editing** (all pure in `input.ts`, tested): ⌃U/⌃K kill to line start/end, ⌃W / ⌥⌫ kill word, ⌃D forward-delete, ⌥/⌃ + ←→ word-jump, ⌃A/⌃E line home/end. Keys: ⏎ send · ⌃J newline · ↑↓ line/history · ← → cursor · ⌥←→ word · tab complete @file · **shift+tab cycles mode (normal · auto-accept · plan)** · ⌃Y copy last reply · esc interrupt · ⌃c quit. `/keys` shows the cheatsheet.\n\n**Modes & effort.** Three input modes cycled by shift+tab (`App.tsx` `cycleMode`): **normal** (asks before writes/edits/shell), **auto-accept** (file writes/edits apply without asking — the permission broker auto-resolves `write`/`edit`; shell still gated; diffs still render), **plan** (read-only). Plus **yolo** (auto-approve everything) via `/yolo`. **Effort tiers** (`/effort fast|balanced|max`, or `setEffort`) pin the model through the routing seam (fast→haiku, balanced/max→sonnet) — the active mode + `⚡effort` show as badges in the `StatusBar`. **Click pickers** (fullscreen only): clicking the **model** or **effort** label in the status bar opens a floating picker above it (↑↓ select · ⏎ apply · esc close), reusing the same `/model`/`/effort` command path. The slash commands remain the keyboard path. The fragile row+column hit-test lives in pure, tested `statusBarHit`/`statusBarLayout` (`StatusBar.tsx`); `App.tsx` only supplies live layout (composer line count, `PALETTE_ROWS`, the rendered model/effort/mode) and toggles `quickPicker` state. Inline mode has no mouse grab, so the labels stay informational there. **Copy**: ⌃Y / `/copy` copies the last reply via OSC 52 (`src/ui/clipboard.ts`, works over SSH); `/export [file]` writes the transcript to Markdown. **Terminal integration** (`src/ui/terminal.ts`): the tab title (OSC 2) reflects working/idle, and a long turn (>8s) rings the bell + fires a desktop notification (macOS) so you can step away.\n\n**More UX affordances.** **Type-ahead**: prompts submitted while busy are queued (`queueRef`, shown as chips) and sent when the turn ends. **⌃C** interrupts a turn → clears the composer → \"press again to quit\" (`cli.tsx` renders with `exitOnCtrlC:false`). **Large pastes** collapse to a `[Pasted N lines]` chip (`pasteStoreRef`), expanded back on submit. **Fuzzy** `@file`/`/command` pickers (`src/ui/fuzzy.ts` — substring-first, then subsequence scored by boundary+contiguity; tested). **Cost**: live `$` estimate in the status bar from per-turn model+tokens (`estimateCost` + per-model pricing in `providers.ts`). **Syntax highlighting** for code blocks (`src/ui/highlight.ts` — lightweight per-line tokenizer → Ink spans, NEVER raw ANSI; used by both `lines.ts` `clipSpans` and `Markdown.tsx`). `?` on an empty composer shows the cheatsheet (`KEYS_HELP`).\n\n**Sessions** (`src/session.ts`): conversations persist per-project under `~/.gearbox/sessions/<slug>/` (`GEARBOX_HOME` overrides). Each record holds provider-neutral `messages` + the UI `items` + **per-turn `{model, usage, at}`** (routing/cost data — the record is deliberately not single-model). `gearbox --continue`/`-c` resumes the latest; `/resume [n]` lists/loads in-app; `/clear` starts a fresh session. Prompt history persists across runs (`history.json`). Saving is best-effort (never crashes the app); skipped in demo mode.\n\nFeatures: full markdown via **marked** (parse, `marked.lexer`) + **Ink** (render) in `Markdown.tsx` — headings, bold/italic/inline-code, tables, ordered+nested lists, blockquotes, code blocks. NO foreign ANSI in Ink (cli-highlight/marked-terminal were tried and removed — they corrupt Ink's width/wrapping; render marked's token tree as Ink elements instead). Markdown gets a `width` prop (threaded App→Transcript→Markdown) for table/rule sizing. Colored diffs under edits (`src/diff.ts`, edit/write tools return `{summary,diff}`), plan mode (read-only tools + plan prompt; `/plan` or shift+tab), `!cmd` runs a shell command directly (`src/shell.ts`), `@file` mentions (fuzzy picker `src/ui/mention.ts`+`files.ts`; expanded into the model message on send), live \"working · Ns\" timer.\n\n**Boo (the mascot).** A pixel ghost, now **parametric** (`src/ui/ghost/engine.ts`, ported from a Claude Design handoff). A 20×20 pixel sprite composited from composable layers — body (palette) + face (eyes/mouth) + accessory + persona + a frame-driven overlay (tears/dots/confetti/Z's/sparkle/hearts) — then FOLDED into half-block cells (`▀`/`▄`, top px → `t`/glyph color, bottom px → `b`/bg). `renderGhost(cfg)` is the source of truth for the **default blocks path**; it's pure + memoized. The data: 13 faces (`FACES`), 9 palettes (`PALETTES`), 6 accessories, 9 personas (personas/accessories ported but not yet surfaced in the live UI). Ink `color`/`backgroundColor` props only, NEVER raw ANSI (corrupts Ink's width math). PNG paths are **opt-in** via `GEARBOX_GHOST`:\n\n- `GEARBOX_GHOST=kitty` — real PNG via kitty graphics Unicode placeholders (`U+10EEEE`, fg encodes image id, diacritics encode row/col; PNGs transmitted once in `cli.tsx`). NOTE: the placeholder protocol is young and mis-rendered (squished) in Ghostty during testing — kept opt-in until that's solved.\n- `GEARBOX_GHOST=iterm` — OSC 1337 splash banner (iTerm2/WezTerm).\n\n`detectImageMode()` returns `blocks` unless `GEARBOX_GHOST` opts in. Baked PNGs live in `src/ui/mascot-png.ts`; `bun run scripts/ghost-preview.ts` previews the parametric engine (splash + all faces + the in-flow state crops). **Boo is animated but deliberately calm** on the blocks path (`AnimatedGhost` in `Mascot.tsx`): one shared, unhurried 240ms tick (leaf-local `useTick`, never lifted to App root); talk + overlays advance at half that (~480ms). There is NO idle bob/float and NO splash sparkle — motion is a quiet sign of life, not fidgeting (the splash just blinks every ~6s; in-flow only the state-meaningful overlay/talk moves). `GEARBOX_NO_MOTION=1` freezes to frame 0. `/ghost [mood]` cycles the skin (`skinToCfg` maps it to a cfg; `shades` is the cool face + shades accessory).\n\n**Layout: fullscreen by default; inline is opt-in.** **Fullscreen is the default** (alt-screen frame + virtualized scroll region + scrollbar + mouse wheel scroll); `--inline`, `GEARBOX_INLINE=1`, or `/config inline on` (pref `fullscreen: false`) opts into inline mode. `GEARBOX_FULLSCREEN=1` or `--fullscreen` forces fullscreen explicitly. The decision lives in `cli.tsx` (`wantsFullscreen`). Grabbing the mouse for wheel-scroll is exactly what disables native terminal selection, so in fullscreen mode text selection requires the terminal's modifier (e.g. Option-drag in Ghostty). **Inline mode** (the plain `Transcript` component): no alt-screen, no mouse grab — native click-drag selection / scrollback / copy all work with no modifier. The transcript is a **virtualized line buffer**: `src/ui/lines.ts` (`itemsToLines`) flattens items into styled `Line`s (markdown→lines, wrapping, diffs) — INVARIANT: every line ≤ width (tested), so nothing overflows. **Streaming perf**: flattening the markdown-heavy `assistant`/`user` items is super-linear with their length, so `staticItemLines` memoizes per item in a `WeakMap` keyed by object reference (unchanged items keep identity across renders, so only the changing tail re-parses — history is free; running tools are not cached since their spinner animates). On the producer side, assistant **text deltas are coalesced** on a ~45ms flush timer in `App.tsx`'s `onEvent` (mirroring the tool-stream coalescer), so streaming re-renders at ~22fps instead of per-token — both together stop the auto-scroll jitter that grew with reply length. `finishAssistant`/the turn `finally` flush any buffered text before marking done or on interrupt. In fullscreen, `App` renders only the visible window via `Viewport` (`src/ui/components/Viewport.tsx`) at a computed `transcriptHeight = rows − header − footer` (footer over-estimated so the frame never exceeds the screen; alt-screen clips, so under-filling is safe). Fullscreen scroll: mouse wheel (SGR mouse reporting enabled in `cli.tsx`; parsed off raw stdin in `App` since Ink doesn't model mouse — buttons 64/65) and PgUp/PgDn; new output re-pins to the bottom (`atBottomRef`); a scrollbar sits on the right. (In fullscreen, mouse reporting means text selection needs the terminal's modifier, e.g. Option-drag in Ghostty — which is why inline is now the default.) The virtualized buffer replaced an earlier flex/overflow fullscreen that corrupted on tall output. Chrome spans full width; prose wraps ≤100 cols. The plain `Transcript` component is the inline-fallback renderer. `scripts/gen-mascot.ts` still bakes the PNGs + baked sprites (`mascot-sprite.ts` `GHOSTS`) — but those now feed **only the opt-in kitty/iTerm image path** (`image.ts`); the default blocks path renders the parametric engine instead. The splash scales to the terminal (big=2×/mini=1×/none by rows×cols, in `App.tsx`). The inline/working presence is the compact **state ghost** (see below) — a native-resolution head crop so Boo never dominates the transcript.\n\nCommands are grouped in `/help` (models · conversation · accounts · save · modes · settings · other) and `src/commands.ts` carries plain-language descriptions: /model [name] (fuzzy — \"haiku\"; `/model auto` routes, `/model all` lists every provider) /effort [fast|balanced|max] /prefer [kind model] (remember a confirmed routing preference for a task type) /clear /resume /retry /compact /context /memory /ask &lt;q&gt; (answer questions about Gearbox itself from its bundled docs via a cheap routed model; plain meta-questions auto-route here with a visible affordance) /account (unified: list/add/login/use/rm/refresh — `/accounts` and `/login` are hidden aliases; `/account refresh` re-discovers each account's real callable models) /cost /copy /export [file] /plan /yolo /theme /config (theme·vim·notify·inline; `/vim` is a hidden alias) /init /keys /help /exit. **Hidden** (work but not listed): /accounts /login /vim /ghost. **Removed:** /cwd (the working dir now shows in `/context`). `formatModelList` shows usable models first and collapses no-key providers to a one-line count.\n\n**Permission gate:** `write_file`/`edit_file`/`run_shell` block on a confirm before mutating. Broker: `src/permission.ts` (`requestPermission` in the tools; `setPermissionHandler` installed by `App`; no handler → allow, so tests/headless are unchanged). Decisions: **once** (1), **always** (2, grants that kind for the session), **all/yolo** (a, auto-approves everything until toggled), **deny** (3/esc). YOLO is also toggled by `/yolo` or started with `--yolo`; a `⚡ yolo` badge shows in the status. The `!` prefix is user-initiated so it is NOT gated. Search/nav tools: `search` (ripgrep, Bun-walk fallback) and `glob` (`Bun.Glob`), both read-only (also in plan mode). The working indicator IS Boo now (`components/Working.tsx`): a compact head-crop ghost whose face follows the agent state — thinking (dots) → streaming (talk) → tool (loading dots) → a clean-finish celebrate (party hat + confetti) → error (crying with falling tears). `App.tsx` derives `mascotState` from the `onEvent` stream; the success/error beat **lingers ~1.5s** after the turn (`linger` state — the working line gates on `busy || linger`, since it would otherwise unmount the instant `busy` goes false). Crops are per-state (`stateView`): head (rows 4–14), head+dots (2–14), head+hat (0–14) so overlays outside the head still read. This deliberately supersedes the earlier \"Boo stays on the welcome splash only / in-flow movement reads as noise\" decision — the compact, state-bearing ghost is the point of the design port.\n\n## Conventions\n\n- Runtime: **Bun**. TypeScript + TSX. Run with `bun run src/cli.tsx`.\n- UI: **Ink** (React for terminals) + **@inkjs/ui**. Keep it calm and beautiful: restrained palette (one accent), generous spacing, consistent glyphs. The look lives in `src/ui/theme.ts` — change colors/glyphs there, not inline.\n- Open + free: MIT, no paid dependencies, no hosted backend, no telemetry. The only cost is the user's own model calls on their own keys.\n- Tools must be safe by default: confirm or sandbox anything destructive; never `rm -rf` or write outside the workspace without intent.\n\n## Run it\n\n```bash\nbun install\n# set at least one key:\nexport ANTHROPIC_API_KEY=... # or OPENAI_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY / DEEPSEEK_API_KEY\nbun run src/cli.tsx # or: bun start\n```\n\nWith no key it launches in demo mode (a scripted transcript) so the UI still runs.\n\n## Test\n\n```bash\nbun test # render tests + agent-loop tests; no API key needed\nbun run typecheck # tsc --noEmit\n```"
144903
+ text: "# Gearbox — project guide\n\nGearbox is a multi-provider coding harness for the terminal: a beautiful, simple terminal agent that reads/writes code and runs commands, talking to any provider (Anthropic, OpenAI, Google, DeepSeek) through one clean loop.\n\n**The point of the project:** intelligent per-task *model routing* — automatically picking the right model for each task across every provider and account you pay for. Basic routing is live (`RoutingSelector` — classify → quality bar → cheapest winner); the richer engine (shadow-eval, credit/limit penalties, confidence display) layers on top of the same seam. See `DESIGN.md` for the full vision and `experiments/FINDINGS.md` for the validation behind it.\n\n## The one rule that matters\n\n**Keep the routing seam clean.** The agent must never hardcode a model. It asks a `ModelSelector` for the model to use. `RoutingSelector` is the live default (classify task → filter by quality bar → cheapest winner); `FixedSelector` is used only when a model is explicitly pinned (`--model` flag or `/model <name>`). Concretely:\n\n- `src/model/selector.ts` — the seam. `select(task) => ModelChoice`. Do not bypass it.\n- `src/model/router.ts` — `RoutingSelector`: classify prompt → quality bar → cost-sort candidates → respect `/prefer` preferences.\n- `src/model/profiles.ts` — the data corpus: quality, cost, latency, tokenizer calibration per model. Routing reads this.\n- `src/providers.ts` — maps a provider+model id to an AI SDK model instance. Already multi-provider. Adding a model is data, not code.\n- Every model call captures token usage (`src/agent/run.ts`) so the cost engine has data. Do not drop usage.\n- The UI consumes a normalized `AgentEvent` stream (`src/agent/events.ts`), never the AI SDK's raw types. This decouples the UI from the provider layer and from routing.\n\nIf you find yourself writing `anthropic('claude-...')` anywhere outside `providers.ts`, stop — route it through the selector.\n\n## Layout\n\n```\nsrc/\n cli.tsx entry point; renders the Ink app; picks RoutingSelector by default\n config.ts minimal config (default model, provider from env)\n providers.ts provider+model id -> AI SDK model (multi-provider; contextWindow per model)\n commands.ts slash-command metadata + pure helpers (fuzzy model match, /help, model list)\n tools.ts read / write / edit / list / search / glob / run_shell (AI SDK tools)\n model/\n selector.ts THE ROUTING SEAM — ModelSelector interface + FixedSelector (pinned model)\n router.ts RoutingSelector: classify → quality bar → cost-sort → preferences (the live default)\n profiles.ts model corpus: quality (SWE-bench), cost ($/Mtok), latency, tokenizer calibration\n tokens.ts calibrated token counting (js-tiktoken × per-model calibration factor)\n preferences.ts persist /prefer kind model choices to ~/.gearbox/routing-preferences.json\n reasoning.ts reasoning/thinking config helpers\n context/\n builder.ts context engine: system + memory + repo map + retrieved files + curated history\n retrieve.ts BM25 lexical retrieval — top-K relevant files for a prompt (no model call)\n repomap.ts repo structure summary for the system prompt\n memory.ts project memory (GEARBOX.md / CLAUDE.md loaded into context)\n compact.ts context compaction (/compact)\n accounts/\n types.ts Account + AuthMethod types (API key, AWS, Azure, Vertex, CLI, OpenAI-compat)\n store.ts accounts.json persistence (~/.gearbox/accounts.json)\n catalog.ts provider catalog (known providers, env vars, labels)\n detect.ts auto-detect env creds + cloud credentials\n onboard.ts interactive add/test account flows\n resolve.ts credential resolution (Account → ResolvedCreds, fetching secrets on demand)\n discover.ts per-account model discovery (Azure deployments / Foundry / gateway /models) → account.models; catalog defaultModels are seeds, not callable ids\n usage.ts per-account spend ledger + rate-limit snapshots + balance tracking\n balance.ts provider balance fetch helpers\n help/\n ask.ts /ask corpus: bundled docs + generated command reference, system prompt, meta-question auto-detect\n agent/\n events.ts AgentEvent — normalized stream the UI consumes\n run.ts real agent loop (AI SDK streamText -> AgentEvent), abort-aware; runCompletion = tool-less grounded answer (used by /ask)\n cli-backend.ts claude/codex CLI subprocess backend (for Pro/Max subscriptions)\n mock.ts scripted demo stream (runs with no API key; used by tests)\n ui/\n theme.ts colors + glyphs (the look)\n input.ts pure key→action reducer for the composer (tested)\n history.ts pure ↑/↓ prompt-history nav (tested)\n net.ts background online probe; status bar shows ⚠ offline when down\n useTerminalSize.ts reactive width on resize (everything reflows)\n git.ts current branch for the status line\n App.tsx the Ink app: state, useInput dispatch, commands, turns\n components/ Banner, Transcript, Composer, CommandPalette, StatusBar, PermissionPrompt, Panel\n panel.ts dismissable command-panel model + pure helpers (clamp/window/filter); tested\ntest/ pure-logic + render tests (ink-testing-library); no keys\nDESIGN.md full product vision (routing, requirements, UX)\nexperiments/ prototypes that validated the architecture\n```\n\nThe composer is custom (Ink `useInput` + `src/ui/input.ts`), not a third-party widget — full control over the cursor, ↑/↓ history, and esc-to-interrupt, with no focus/remount fragility. **Multi-line**: ⌃J (or shift/alt+⏎) inserts a newline, ⏎ submits; ↑/↓ move between lines and fall through to history at the top/bottom line; bracketed paste (enabled in `cli.tsx`) inserts multi-line text literally (CR normalized, paste markers stripped) instead of submitting per line. `caretPos()` is the shared line/col helper. **Readline editing** (all pure in `input.ts`, tested): ⌃U/⌃K kill to line start/end, ⌃W / ⌥⌫ kill word, ⌃D forward-delete, ⌥/⌃ + ←→ word-jump, ⌃A/⌃E line home/end. Keys: ⏎ send · ⌃J newline · ↑↓ line/history · ← → cursor · ⌥←→ word · tab complete @file · **shift+tab cycles mode (normal · auto-accept · plan)** · ⌃Y copy last reply · esc interrupt · ⌃c quit. `/keys` shows the cheatsheet.\n\n**Modes & effort.** Three input modes cycled by shift+tab (`App.tsx` `cycleMode`): **normal** (asks before writes/edits/shell), **auto-accept** (file writes/edits apply without asking — the permission broker auto-resolves `write`/`edit`; shell still gated; diffs still render), **plan** (read-only). Plus **yolo** (auto-approve everything) via `/yolo`. **Effort tiers** (`/effort fast|balanced|max`, or `setEffort`) pin the model through the routing seam (fast→haiku, balanced/max→sonnet) — the active mode + `⚡effort` show as badges in the `StatusBar`. **Click pickers** (fullscreen only): clicking the **model** or **effort** label in the status bar opens a floating picker above it (↑↓ select · ⏎ apply · esc close), reusing the same `/model`/`/effort` command path. The slash commands remain the keyboard path. The fragile row+column hit-test lives in pure, tested `statusBarHit`/`statusBarLayout` (`StatusBar.tsx`); `App.tsx` only supplies live layout (composer line count, `PALETTE_ROWS`, the rendered model/effort/mode) and toggles `quickPicker` state. Inline mode has no mouse grab, so the labels stay informational there. **Copy**: ⌃Y / `/copy` copies the last reply via OSC 52 (`src/ui/clipboard.ts`, works over SSH); `/export [file]` writes the transcript to Markdown. **Terminal integration** (`src/ui/terminal.ts`): the tab title (OSC 2) reflects working/idle, and a long turn (>8s) rings the bell + fires a desktop notification (macOS) so you can step away.\n\n**More UX affordances.** **Type-ahead**: prompts submitted while busy are queued (`queueRef`, shown as chips) and sent when the turn ends. **⌃C** interrupts a turn → clears the composer → \"press again to quit\" (`cli.tsx` renders with `exitOnCtrlC:false`). **Large pastes** collapse to a `[Pasted N lines]` chip (`pasteStoreRef`), expanded back on submit. **Fuzzy** `@file`/`/command` pickers (`src/ui/fuzzy.ts` — substring-first, then subsequence scored by boundary+contiguity; tested). **Cost**: live `$` estimate in the status bar from per-turn model+tokens (`estimateCost` + per-model pricing in `providers.ts`). **Syntax highlighting** for code blocks (`src/ui/highlight.ts` — lightweight per-line tokenizer → Ink spans, NEVER raw ANSI; used by both `lines.ts` `clipSpans` and `Markdown.tsx`). `?` on an empty composer shows the cheatsheet (`KEYS_HELP`).\n\n**Sessions** (`src/session.ts`): conversations persist per-project under `~/.gearbox/sessions/<slug>/` (`GEARBOX_HOME` overrides). Each record holds provider-neutral `messages` + the UI `items` + **per-turn `{model, usage, at}`** (routing/cost data — the record is deliberately not single-model). `gearbox --continue`/`-c` resumes the latest; `/resume [n]` lists/loads in-app; `/clear` starts a fresh session. Prompt history persists across runs (`history.json`). Saving is best-effort (never crashes the app); skipped in demo mode.\n\nFeatures: full markdown via **marked** (parse, `marked.lexer`) + **Ink** (render) in `Markdown.tsx` — headings, bold/italic/inline-code, tables, ordered+nested lists, blockquotes, code blocks. NO foreign ANSI in Ink (cli-highlight/marked-terminal were tried and removed — they corrupt Ink's width/wrapping; render marked's token tree as Ink elements instead). Markdown gets a `width` prop (threaded App→Transcript→Markdown) for table/rule sizing. Colored diffs under edits (`src/diff.ts`, edit/write tools return `{summary,diff}`), plan mode (read-only tools + plan prompt; `/plan` or shift+tab), `!cmd` runs a shell command directly (`src/shell.ts`), `@file` mentions (fuzzy picker `src/ui/mention.ts`+`files.ts`; expanded into the model message on send), live \"working · Ns\" timer.\n\n**Boo (the mascot).** A pixel ghost, now **parametric** (`src/ui/ghost/engine.ts`, ported from a Claude Design handoff). A 20×20 pixel sprite composited from composable layers — body (palette) + face (eyes/mouth) + accessory + persona + a frame-driven overlay (tears/dots/confetti/Z's/sparkle/hearts) — then FOLDED into half-block cells (`▀`/`▄`, top px → `t`/glyph color, bottom px → `b`/bg). `renderGhost(cfg)` is the source of truth for the **default blocks path**; it's pure + memoized. The data: 13 faces (`FACES`), 9 palettes (`PALETTES`), 6 accessories, 9 personas (personas/accessories ported but not yet surfaced in the live UI). Ink `color`/`backgroundColor` props only, NEVER raw ANSI (corrupts Ink's width math). PNG paths are **opt-in** via `GEARBOX_GHOST`:\n\n- `GEARBOX_GHOST=kitty` — real PNG via kitty graphics Unicode placeholders (`U+10EEEE`, fg encodes image id, diacritics encode row/col; PNGs transmitted once in `cli.tsx`). NOTE: the placeholder protocol is young and mis-rendered (squished) in Ghostty during testing — kept opt-in until that's solved.\n- `GEARBOX_GHOST=iterm` — OSC 1337 splash banner (iTerm2/WezTerm).\n\n`detectImageMode()` returns `blocks` unless `GEARBOX_GHOST` opts in. Baked PNGs live in `src/ui/mascot-png.ts`; `bun run scripts/ghost-preview.ts` previews the parametric engine (splash + all faces + the in-flow state crops). **Boo is animated but deliberately calm** on the blocks path (`AnimatedGhost` in `Mascot.tsx`): one shared, unhurried 240ms tick (leaf-local `useTick`, never lifted to App root); talk + overlays advance at half that (~480ms). There is NO idle bob/float and NO splash sparkle — motion is a quiet sign of life, not fidgeting (the splash just blinks every ~6s; in-flow only the state-meaningful overlay/talk moves). `GEARBOX_NO_MOTION=1` freezes to frame 0. `/ghost [mood]` cycles the skin (`skinToCfg` maps it to a cfg; `shades` is the cool face + shades accessory).\n\n**Layout: fullscreen by default; inline is opt-in.** **Fullscreen is the default** (alt-screen frame + virtualized scroll region + scrollbar + mouse wheel scroll); `--inline`, `GEARBOX_INLINE=1`, or `/config inline on` (pref `fullscreen: false`) opts into inline mode. `GEARBOX_FULLSCREEN=1` or `--fullscreen` forces fullscreen explicitly. The decision lives in `cli.tsx` (`wantsFullscreen`). Grabbing the mouse for wheel-scroll is exactly what disables native terminal selection, so in fullscreen mode text selection requires the terminal's modifier (e.g. Option-drag in Ghostty). **Inline mode** (the plain `Transcript` component): no alt-screen, no mouse grab — native click-drag selection / scrollback / copy all work with no modifier. The transcript is a **virtualized line buffer**: `src/ui/lines.ts` (`itemsToLines`) flattens items into styled `Line`s (markdown→lines, wrapping, diffs) — INVARIANT: every line ≤ width (tested), so nothing overflows. **Streaming perf**: flattening the markdown-heavy `assistant`/`user` items is super-linear with their length, so `staticItemLines` memoizes per item in a `WeakMap` keyed by object reference (unchanged items keep identity across renders, so only the changing tail re-parses — history is free; running tools are not cached since their spinner animates). On the producer side, assistant **text deltas are coalesced** on a ~45ms flush timer in `App.tsx`'s `onEvent` (mirroring the tool-stream coalescer), so streaming re-renders at ~22fps instead of per-token — both together stop the auto-scroll jitter that grew with reply length. `finishAssistant`/the turn `finally` flush any buffered text before marking done or on interrupt. In fullscreen, `App` renders only the visible window via `Viewport` (`src/ui/components/Viewport.tsx`) at a computed `transcriptHeight = rows − header − footer` (footer over-estimated so the frame never exceeds the screen; alt-screen clips, so under-filling is safe). Fullscreen scroll: mouse wheel (SGR mouse reporting enabled in `cli.tsx`; parsed off raw stdin in `App` since Ink doesn't model mouse — buttons 64/65) and PgUp/PgDn; new output re-pins to the bottom (`atBottomRef`); a scrollbar sits on the right. (In fullscreen, mouse reporting means text selection needs the terminal's modifier, e.g. Option-drag in Ghostty — which is why inline is now the default.) The virtualized buffer replaced an earlier flex/overflow fullscreen that corrupted on tall output. Chrome spans full width; prose wraps ≤100 cols. The plain `Transcript` component is the inline-fallback renderer. `scripts/gen-mascot.ts` still bakes the PNGs + baked sprites (`mascot-sprite.ts` `GHOSTS`) — but those now feed **only the opt-in kitty/iTerm image path** (`image.ts`); the default blocks path renders the parametric engine instead. The splash scales to the terminal (big=2×/mini=1×/none by rows×cols, in `App.tsx`). The inline/working presence is the compact **state ghost** (see below) — a native-resolution head crop so Boo never dominates the transcript.\n\nCommands are grouped in `/help` (models · conversation · accounts · save · modes · settings · other) and `src/commands.ts` carries plain-language descriptions: /model [name] (fuzzy — \"haiku\"; `/model auto` routes, `/model all` lists every provider) /effort [fast|balanced|max] /prefer [kind model] (remember a confirmed routing preference for a task type) /clear /resume /retry /compact /context /memory /ask &lt;q&gt; (answer questions about Gearbox itself from its bundled docs via a cheap routed model; plain meta-questions auto-route here with a visible affordance) /account (unified: list/add/login/use/rm/refresh — `/accounts` and `/login` are hidden aliases; `/account refresh` re-discovers each account's real callable models) /cost /copy /export [file] /plan /yolo /theme /config (theme·vim·notify·inline; `/vim` is a hidden alias) /init /keys /help /exit. **Hidden** (work but not listed): /accounts /login /vim /ghost. **Removed:** /cwd (the working dir now shows in `/context`). `formatModelList` shows usable models first and collapses no-key providers to a one-line count.\n\n**Command panel (fullscreen only).** Big info-dump commands open a dismissable, Esc-closable overlay instead of dumping into the transcript (`Panel.tsx` + pure `panel.ts`, wired in `App.tsx`): `/help` `/keys` `/context` `/cost` `/memory` are scrollable static dumps (reuse `itemsToLines` + `Viewport`); `/account` and `/model` are interactive lists (↑↓ select · ⏎ acts — they just dispatch the equivalent `/account <n>` / `/model <id>` command and close), and `/model` has type-to-filter (127 Foundry models). The panel replaces the transcript Viewport region while open and takes precedence over `welcome`; the key handler is a branch in `useInput` placed after ⌃C so Esc closes the panel rather than interrupting a turn. Short confirmations (`model → haiku`, `remembered`, `✓ added`, errors) stay inline. Inline mode keeps the old inline printing (no alt-screen to overlay). `openInfoPanel` returns false inline so callers fall back to `push`.\n\n**Permission gate:** `write_file`/`edit_file`/`run_shell` block on a confirm before mutating. Broker: `src/permission.ts` (`requestPermission` in the tools; `setPermissionHandler` installed by `App`; no handler → allow, so tests/headless are unchanged). Decisions: **once** (1), **always** (2, grants that kind for the session), **all/yolo** (a, auto-approves everything until toggled), **deny** (3/esc). YOLO is also toggled by `/yolo` or started with `--yolo`; a `⚡ yolo` badge shows in the status. The `!` prefix is user-initiated so it is NOT gated. Search/nav tools: `search` (ripgrep, Bun-walk fallback) and `glob` (`Bun.Glob`), both read-only (also in plan mode). The working indicator IS Boo now (`components/Working.tsx`): a compact head-crop ghost whose face follows the agent state — thinking (dots) → streaming (talk) → tool (loading dots) → a clean-finish celebrate (party hat + confetti) → error (crying with falling tears). `App.tsx` derives `mascotState` from the `onEvent` stream; the success/error beat **lingers ~1.5s** after the turn (`linger` state — the working line gates on `busy || linger`, since it would otherwise unmount the instant `busy` goes false). Crops are per-state (`stateView`): head (rows 4–14), head+dots (2–14), head+hat (0–14) so overlays outside the head still read. This deliberately supersedes the earlier \"Boo stays on the welcome splash only / in-flow movement reads as noise\" decision — the compact, state-bearing ghost is the point of the design port.\n\n## Conventions\n\n- Runtime: **Bun**. TypeScript + TSX. Run with `bun run src/cli.tsx`.\n- UI: **Ink** (React for terminals) + **@inkjs/ui**. Keep it calm and beautiful: restrained palette (one accent), generous spacing, consistent glyphs. The look lives in `src/ui/theme.ts` — change colors/glyphs there, not inline.\n- Open + free: MIT, no paid dependencies, no hosted backend, no telemetry. The only cost is the user's own model calls on their own keys.\n- Tools must be safe by default: confirm or sandbox anything destructive; never `rm -rf` or write outside the workspace without intent.\n\n## Run it\n\n```bash\nbun install\n# set at least one key:\nexport ANTHROPIC_API_KEY=... # or OPENAI_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY / DEEPSEEK_API_KEY\nbun run src/cli.tsx # or: bun start\n```\n\nWith no key it launches in demo mode (a scripted transcript) so the UI still runs.\n\n## Test\n\n```bash\nbun test # render tests + agent-loop tests; no API key needed\nbun run typecheck # tsc --noEmit\n```"
144704
144904
  },
144705
144905
  {
144706
144906
  file: "DESIGN.md",
@@ -145712,7 +145912,7 @@ function gitBranch() {
145712
145912
 
145713
145913
  // src/ui/App.tsx
145714
145914
  init_proc();
145715
- var jsx_dev_runtime12 = __toESM(require_jsx_dev_runtime(), 1);
145915
+ var jsx_dev_runtime13 = __toESM(require_jsx_dev_runtime(), 1);
145716
145916
  import { basename as basename3, extname, resolve as resolve12 } from "node:path";
145717
145917
  import { existsSync as existsSync11, readFileSync as readFileSync16, statSync as statSync5 } from "node:fs";
145718
145918
  import { writeFile as fsWriteFile } from "node:fs/promises";
@@ -145851,16 +146051,16 @@ function ActivityRail({ items, width }) {
145851
146051
  const toolText = tools2.map((t2) => `${t2.status === "running" ? spin2 : t2.status === "err" ? "!" : "✓"} ${t2.name.replace(/_file$/, "").replace("run_shell", "shell")}`).join(" · ");
145852
146052
  const checkText = checks4.map((c) => `${c.ok ? "✓" : "!"} ${c.command}`).join(" · ");
145853
146053
  const line = [model ? model.model : null, phase ? phase.label : null, toolText || null, checkText || null].filter(Boolean).join(" · ");
145854
- return /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
146054
+ return /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
145855
146055
  paddingX: 1,
145856
146056
  marginTop: 1,
145857
146057
  width,
145858
146058
  children: [
145859
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
146059
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
145860
146060
  color: color.accentDim,
145861
146061
  children: "activity "
145862
146062
  }, undefined, false, undefined, this),
145863
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
146063
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
145864
146064
  color: color.faint,
145865
146065
  children: line.slice(0, Math.max(width - 10, 20))
145866
146066
  }, undefined, false, undefined, this)
@@ -145870,31 +146070,31 @@ function ActivityRail({ items, width }) {
145870
146070
  function SetupSplash({ state, width, skin, splashSize }) {
145871
146071
  const detected = state.importable.length + state.cloudImportable.length;
145872
146072
  const panelWidth = Math.min(Math.max(width - 4, 30), 58);
145873
- return /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
146073
+ return /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
145874
146074
  flexDirection: "column",
145875
146075
  alignItems: "center",
145876
146076
  children: [
145877
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(MascotSplash, {
146077
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(MascotSplash, {
145878
146078
  skin,
145879
146079
  size: splashSize
145880
146080
  }, undefined, false, undefined, this),
145881
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
146081
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
145882
146082
  marginTop: 1,
145883
146083
  flexDirection: "column",
145884
146084
  alignItems: "center",
145885
146085
  children: [
145886
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
146086
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
145887
146087
  color: color.accent,
145888
146088
  bold: true,
145889
146089
  children: "gearbox"
145890
146090
  }, undefined, false, undefined, this),
145891
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
146091
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
145892
146092
  color: color.dim,
145893
146093
  children: "one terminal · every model you already pay for"
145894
146094
  }, undefined, false, undefined, this)
145895
146095
  ]
145896
146096
  }, undefined, true, undefined, this),
145897
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
146097
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
145898
146098
  marginTop: 2,
145899
146099
  width: panelWidth,
145900
146100
  borderStyle: "round",
@@ -145903,18 +146103,18 @@ function SetupSplash({ state, width, skin, splashSize }) {
145903
146103
  paddingY: 1,
145904
146104
  flexDirection: "column",
145905
146105
  children: [
145906
- detected > 0 ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(jsx_dev_runtime12.Fragment, {
146106
+ detected > 0 ? /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(jsx_dev_runtime13.Fragment, {
145907
146107
  children: [
145908
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
146108
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
145909
146109
  children: [
145910
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
146110
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
145911
146111
  color: color.ok,
145912
146112
  children: [
145913
146113
  glyph.on,
145914
146114
  " "
145915
146115
  ]
145916
146116
  }, undefined, true, undefined, this),
145917
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
146117
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
145918
146118
  color: color.text,
145919
146119
  children: [
145920
146120
  detected,
@@ -145925,75 +146125,75 @@ function SetupSplash({ state, width, skin, splashSize }) {
145925
146125
  }, undefined, true, undefined, this)
145926
146126
  ]
145927
146127
  }, undefined, true, undefined, this),
145928
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
146128
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
145929
146129
  marginTop: 1,
145930
146130
  children: [
145931
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
146131
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
145932
146132
  color: color.accent,
145933
146133
  children: "/account import"
145934
146134
  }, undefined, false, undefined, this),
145935
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
146135
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
145936
146136
  color: color.dim,
145937
146137
  children: " connect automatically"
145938
146138
  }, undefined, false, undefined, this)
145939
146139
  ]
145940
146140
  }, undefined, true, undefined, this),
145941
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
146141
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
145942
146142
  marginTop: 1,
145943
146143
  children: [
145944
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
146144
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
145945
146145
  color: color.faint,
145946
146146
  children: "or add a different key: "
145947
146147
  }, undefined, false, undefined, this),
145948
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
146148
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
145949
146149
  color: color.accent,
145950
146150
  children: "/account add <api-key>"
145951
146151
  }, undefined, false, undefined, this)
145952
146152
  ]
145953
146153
  }, undefined, true, undefined, this)
145954
146154
  ]
145955
- }, undefined, true, undefined, this) : /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(jsx_dev_runtime12.Fragment, {
146155
+ }, undefined, true, undefined, this) : /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(jsx_dev_runtime13.Fragment, {
145956
146156
  children: [
145957
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
146157
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
145958
146158
  color: color.dim,
145959
146159
  children: "paste or type a key to get started"
145960
146160
  }, undefined, false, undefined, this),
145961
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
146161
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
145962
146162
  marginTop: 1,
145963
- children: /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
146163
+ children: /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
145964
146164
  color: color.accent,
145965
146165
  children: "/account add <api-key>"
145966
146166
  }, undefined, false, undefined, this)
145967
146167
  }, undefined, false, undefined, this)
145968
146168
  ]
145969
146169
  }, undefined, true, undefined, this),
145970
- (state.hasClaudeCli || state.hasCodexCli) && /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
146170
+ (state.hasClaudeCli || state.hasCodexCli) && /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
145971
146171
  marginTop: 2,
145972
146172
  flexDirection: "column",
145973
146173
  children: [
145974
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
146174
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
145975
146175
  color: color.faint,
145976
146176
  children: "subscriptions detected"
145977
146177
  }, undefined, false, undefined, this),
145978
- state.hasClaudeCli && /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
146178
+ state.hasClaudeCli && /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
145979
146179
  children: [
145980
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
146180
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
145981
146181
  color: color.accent,
145982
146182
  children: "/account add claude"
145983
146183
  }, undefined, false, undefined, this),
145984
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
146184
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
145985
146185
  color: color.faint,
145986
146186
  children: " Claude Pro / Max"
145987
146187
  }, undefined, false, undefined, this)
145988
146188
  ]
145989
146189
  }, undefined, true, undefined, this),
145990
- state.hasCodexCli && /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
146190
+ state.hasCodexCli && /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
145991
146191
  children: [
145992
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
146192
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
145993
146193
  color: color.accent,
145994
146194
  children: "/account add codex"
145995
146195
  }, undefined, false, undefined, this),
145996
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
146196
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
145997
146197
  color: color.faint,
145998
146198
  children: " ChatGPT Plus"
145999
146199
  }, undefined, false, undefined, this)
@@ -146003,9 +146203,9 @@ function SetupSplash({ state, width, skin, splashSize }) {
146003
146203
  }, undefined, true, undefined, this)
146004
146204
  ]
146005
146205
  }, undefined, true, undefined, this),
146006
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
146206
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
146007
146207
  marginTop: 1,
146008
- children: /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
146208
+ children: /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
146009
146209
  color: color.faint,
146010
146210
  children: "/onboard · /account · /help"
146011
146211
  }, undefined, false, undefined, this)
@@ -146080,6 +146280,22 @@ function App2({ selector: initialSelector, runner, fullscreen = false, resumeId
146080
146280
  quickPickerIndexRef.current = n;
146081
146281
  setQuickPickerIndexState(n);
146082
146282
  };
146283
+ const [panel, setPanelState] = import_react26.useState(null);
146284
+ const panelRef = import_react26.useRef(null);
146285
+ const setPanel = (p) => {
146286
+ panelRef.current = p;
146287
+ setPanelState(p);
146288
+ };
146289
+ const panelMaxScrollRef = import_react26.useRef(0);
146290
+ const panelAccountNumbersRef = import_react26.useRef([]);
146291
+ const buildPanelModelRows = (cur) => modelRegistry().filter((m2) => providerAvailable(m2.provider)).map((m2) => ({ id: m2.id, label: m2.label, provider: m2.provider, current: m2.id === cur }));
146292
+ const openInfoPanel = (title, item) => {
146293
+ if (!fullscreen)
146294
+ return false;
146295
+ atBottomRef.current = true;
146296
+ setPanel({ kind: "static", title, items: [item], scroll: 0 });
146297
+ return true;
146298
+ };
146083
146299
  const setSearch = (s2) => {
146084
146300
  searchRef.current = s2;
146085
146301
  setSearchState(s2);
@@ -147402,10 +147618,14 @@ ${fetched.join(`
147402
147618
  loadInto(pick3);
147403
147619
  return;
147404
147620
  }
147405
- case "help":
147621
+ case "help": {
147622
+ const it = { kind: "notice", id: idRef.current++, text: helpText() };
147623
+ if (openInfoPanel("help", it))
147624
+ return;
147406
147625
  echo(text2);
147407
- notice(helpText());
147626
+ push(it);
147408
147627
  return;
147628
+ }
147409
147629
  case "plan":
147410
147630
  echo(text2);
147411
147631
  togglePlan();
@@ -147440,10 +147660,14 @@ ${fetched.join(`
147440
147660
  fsWriteFile(file5, transcriptMarkdown(itemsRef.current)).then(() => notice(`exported transcript → ${file5}`)).catch((e2) => notice(`couldn't write ${file5}: ${e2?.message ?? String(e2)}`));
147441
147661
  return;
147442
147662
  }
147443
- case "keys":
147663
+ case "keys": {
147664
+ const it = { kind: "notice", id: idRef.current++, text: KEYS_HELP };
147665
+ if (openInfoPanel("keyboard shortcuts", it))
147666
+ return;
147444
147667
  echo(text2);
147445
- notice(KEYS_HELP);
147668
+ push(it);
147446
147669
  return;
147670
+ }
147447
147671
  case "vim": {
147448
147672
  echo(text2);
147449
147673
  const on = vimRef.current === "off";
@@ -147531,6 +147755,10 @@ ${fetched.join(`
147531
147755
  return;
147532
147756
  }
147533
147757
  case "model":
147758
+ if ((!arg || arg.toLowerCase() === "all") && !activeCliRef.current && fullscreen) {
147759
+ setPanel({ kind: "models", title: "models · ⏎ to pin", index: 0, filter: "" });
147760
+ return;
147761
+ }
147534
147762
  echo(text2);
147535
147763
  if (!arg || arg.toLowerCase() === "all") {
147536
147764
  const routing2 = selectorRef.current instanceof RoutingSelector;
@@ -147637,18 +147865,21 @@ ${fetched.join(`
147637
147865
  return;
147638
147866
  }
147639
147867
  case "memory": {
147640
- echo(text2);
147641
147868
  if (arg) {
147869
+ echo(text2);
147642
147870
  notice(appendFact(arg) ? "remembered" : "couldn't save that note");
147643
147871
  return;
147644
147872
  }
147645
147873
  const facts = loadFacts().trim();
147646
- notice(facts ? `remembered facts:
147647
- ` + facts : "no remembered facts yet — add one with #<note> or /memory <note>");
147874
+ const it = { kind: "notice", id: idRef.current++, text: facts ? `remembered facts:
147875
+ ` + facts : "no remembered facts yet — add one with #<note> or /memory <note>" };
147876
+ if (openInfoPanel("memory", it))
147877
+ return;
147878
+ echo(text2);
147879
+ push(it);
147648
147880
  return;
147649
147881
  }
147650
147882
  case "context": {
147651
- echo(text2);
147652
147883
  const m2 = (() => {
147653
147884
  try {
147654
147885
  return selectorRef.current.select({ prompt: "" }).model;
@@ -147657,13 +147888,18 @@ ${fetched.join(`
147657
147888
  }
147658
147889
  })();
147659
147890
  if (!m2) {
147891
+ echo(text2);
147660
147892
  notice(`no model available — add a provider first
147661
147893
 
147662
147894
  ` + onboardingSummary(onboardingState));
147663
147895
  return;
147664
147896
  }
147665
147897
  const { sections } = buildContext({ history: msgRef.current, userText: lastPromptRef.current || "(your next message)", model: m2, plan: modeRef.current === "plan" });
147666
- push({ kind: "context", id: idRef.current++, view: buildContextView(sections, m2.contextWindow, process.cwd()) });
147898
+ const it = { kind: "context", id: idRef.current++, view: buildContextView(sections, m2.contextWindow, process.cwd()) };
147899
+ if (openInfoPanel("context", it))
147900
+ return;
147901
+ echo(text2);
147902
+ push(it);
147667
147903
  return;
147668
147904
  }
147669
147905
  case "onboard": {
@@ -147727,6 +147963,10 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
147727
147963
  }
147728
147964
  case "accounts":
147729
147965
  case "account": {
147966
+ if (!arg.trim() && fullscreen) {
147967
+ setPanel({ kind: "accounts", title: "accounts · ⏎ to switch", index: 0 });
147968
+ return;
147969
+ }
147730
147970
  echo(text2);
147731
147971
  const parts = arg.split(/\s+/).filter(Boolean);
147732
147972
  const subL = (parts[0] || "").toLowerCase();
@@ -147975,7 +148215,6 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
147975
148215
  }
147976
148216
  case "cost":
147977
148217
  case "usage": {
147978
- echo(text2);
147979
148218
  const accounts = listAccounts();
147980
148219
  const resolve13 = (id) => {
147981
148220
  const a = getAccount(id);
@@ -147995,9 +148234,14 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
147995
148234
  const session = estimateCost(sessionRef.current.turns);
147996
148235
  const withBalance = accounts.filter((a) => a.exec !== "cli" && balanceExposed(a.provider));
147997
148236
  if (!withBalance.length) {
147998
- pushUsage(buildUsageView(session, resolve13, Date.now(), accounts.map((a) => a.id)));
148237
+ const it = { kind: "usage", id: idRef.current++, view: buildUsageView(session, resolve13, Date.now(), accounts.map((a) => a.id)) };
148238
+ if (openInfoPanel("cost", it))
148239
+ return;
148240
+ echo(text2);
148241
+ push(it);
147999
148242
  return;
148000
148243
  }
148244
+ echo(text2);
148001
148245
  notice("checking balances…");
148002
148246
  (async () => {
148003
148247
  for (const a of withBalance) {
@@ -148236,6 +148480,66 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
148236
148480
  notice("press ⌃C again to quit");
148237
148481
  return;
148238
148482
  }
148483
+ if (panelRef.current) {
148484
+ const p = panelRef.current;
148485
+ if (key.escape) {
148486
+ setPanel(null);
148487
+ return;
148488
+ }
148489
+ if (p.kind === "static") {
148490
+ if (input === "q") {
148491
+ setPanel(null);
148492
+ return;
148493
+ }
148494
+ const max2 = panelMaxScrollRef.current;
148495
+ const page = Math.max(1, panelBodyHeight(viewportHeightRef.current) - 1);
148496
+ if (key.upArrow)
148497
+ setPanel({ ...p, scroll: clampScroll(p.scroll - 1, max2) });
148498
+ else if (key.downArrow)
148499
+ setPanel({ ...p, scroll: clampScroll(p.scroll + 1, max2) });
148500
+ else if (key.pageUp)
148501
+ setPanel({ ...p, scroll: clampScroll(p.scroll - page, max2) });
148502
+ else if (key.pageDown)
148503
+ setPanel({ ...p, scroll: clampScroll(p.scroll + page, max2) });
148504
+ return;
148505
+ }
148506
+ if (p.kind === "accounts") {
148507
+ const nums = panelAccountNumbersRef.current;
148508
+ const n = nums.length;
148509
+ if (key.upArrow)
148510
+ setPanel({ ...p, index: clampIndex(p.index - 1, n) });
148511
+ else if (key.downArrow)
148512
+ setPanel({ ...p, index: clampIndex(p.index + 1, n) });
148513
+ else if (key.return) {
148514
+ const num = nums[clampIndex(p.index, n)];
148515
+ setPanel(null);
148516
+ if (num)
148517
+ handleCommand(`/account ${num}`);
148518
+ } else if (/^[1-9]$/.test(input)) {
148519
+ const k = Number(input);
148520
+ if (k <= n) {
148521
+ setPanel(null);
148522
+ handleCommand(`/account ${k}`);
148523
+ }
148524
+ }
148525
+ return;
148526
+ }
148527
+ const rows2 = filterModelRows(buildPanelModelRows(), p.filter);
148528
+ if (key.upArrow)
148529
+ setPanel({ ...p, index: clampIndex(p.index - 1, rows2.length) });
148530
+ else if (key.downArrow)
148531
+ setPanel({ ...p, index: clampIndex(p.index + 1, rows2.length) });
148532
+ else if (key.return) {
148533
+ const r2 = rows2[clampIndex(p.index, rows2.length)];
148534
+ setPanel(null);
148535
+ if (r2)
148536
+ handleCommand(`/model ${r2.id}`);
148537
+ } else if (key.backspace || key.delete)
148538
+ setPanel(backspaceFilter(p));
148539
+ else if (input && !key.ctrl && !key.meta && !key.tab && input.length === 1 && input >= " ")
148540
+ setPanel(appendFilter(p, input));
148541
+ return;
148542
+ }
148239
148543
  if (key.ctrl && input === "r") {
148240
148544
  const cur = searchRef.current;
148241
148545
  setSearch(cur ? { q: cur.q, idx: cur.idx + 1 } : { q: "", idx: 0 });
@@ -148491,91 +148795,106 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
148491
148795
  paletteRowsLiveRef.current = PALETTE_ROWS;
148492
148796
  maxScrollRef.current = maxScroll;
148493
148797
  scrollTopRef.current = effScroll;
148798
+ const panelW = width - 2;
148799
+ const panelInnerW = Math.max(4, panelW - 2);
148800
+ const panelCurrentModel = loadPrefs().pinnedModel ?? null;
148801
+ let panelStaticLines;
148802
+ let panelAccountView;
148803
+ let panelModels;
148804
+ if (panel?.kind === "static") {
148805
+ panelStaticLines = itemsToLines(panel.items, panelInnerW);
148806
+ panelMaxScrollRef.current = Math.max(0, panelStaticLines.length - panelBodyHeight(transcriptHeight));
148807
+ } else if (panel?.kind === "accounts") {
148808
+ panelAccountView = buildAccountView(listAccounts(), activeCliRef.current?.id ?? null, importableEnvCreds(), accountStatusCacheRef.current);
148809
+ panelAccountNumbersRef.current = panelAccountView.rows.map((r2) => r2.number);
148810
+ } else if (panel?.kind === "models") {
148811
+ panelModels = buildPanelModelRows(panelCurrentModel);
148812
+ }
148494
148813
  import_react26.useEffect(() => {
148495
148814
  if (atBottomRef.current)
148496
148815
  setScrollTop(maxScroll);
148497
148816
  }, [lines.length, maxScroll]);
148498
- const hero = /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
148817
+ const hero = /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
148499
148818
  flexDirection: "column",
148500
148819
  alignItems: "center",
148501
- children: setupRequired ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(SetupSplash, {
148820
+ children: setupRequired ? /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(SetupSplash, {
148502
148821
  state: onboardingState,
148503
148822
  width,
148504
148823
  skin: ghostSkin,
148505
148824
  splashSize
148506
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(jsx_dev_runtime12.Fragment, {
148825
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(jsx_dev_runtime13.Fragment, {
148507
148826
  children: [
148508
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(MascotSplash, {
148827
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(MascotSplash, {
148509
148828
  skin: ghostSkin,
148510
148829
  size: splashSize
148511
148830
  }, undefined, false, undefined, this),
148512
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
148831
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
148513
148832
  marginTop: 1,
148514
148833
  children: [
148515
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
148834
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148516
148835
  color: color.dim,
148517
148836
  children: "talk or type "
148518
148837
  }, undefined, false, undefined, this),
148519
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
148838
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148520
148839
  color: color.faint,
148521
148840
  children: [
148522
148841
  glyph.bullet,
148523
148842
  " "
148524
148843
  ]
148525
148844
  }, undefined, true, undefined, this),
148526
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
148845
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148527
148846
  color: color.accentDim,
148528
148847
  children: "/"
148529
148848
  }, undefined, false, undefined, this),
148530
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
148849
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148531
148850
  color: color.dim,
148532
148851
  children: "commands "
148533
148852
  }, undefined, false, undefined, this),
148534
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
148853
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148535
148854
  color: color.accentDim,
148536
148855
  children: "@"
148537
148856
  }, undefined, false, undefined, this),
148538
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
148857
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148539
148858
  color: color.dim,
148540
148859
  children: "files "
148541
148860
  }, undefined, false, undefined, this),
148542
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
148861
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148543
148862
  color: color.accentDim,
148544
148863
  children: "!"
148545
148864
  }, undefined, false, undefined, this),
148546
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
148865
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148547
148866
  color: color.dim,
148548
148867
  children: "shell"
148549
148868
  }, undefined, false, undefined, this)
148550
148869
  ]
148551
148870
  }, undefined, true, undefined, this),
148552
- firstRunRef.current ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
148871
+ firstRunRef.current ? /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
148553
148872
  marginTop: 1,
148554
148873
  flexDirection: "column",
148555
148874
  alignItems: "center",
148556
148875
  children: [
148557
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
148876
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148558
148877
  color: color.faint,
148559
148878
  children: [
148560
148879
  "new here? press ",
148561
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
148880
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148562
148881
  color: color.accent,
148563
148882
  children: "?"
148564
148883
  }, undefined, false, undefined, this),
148565
148884
  " for shortcuts · ",
148566
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
148885
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148567
148886
  color: color.accent,
148568
148887
  children: "shift+tab"
148569
148888
  }, undefined, false, undefined, this),
148570
148889
  " cycles modes · ",
148571
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
148890
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148572
148891
  color: color.accent,
148573
148892
  children: "⌃Y"
148574
148893
  }, undefined, false, undefined, this),
148575
148894
  " copies the last reply"
148576
148895
  ]
148577
148896
  }, undefined, true, undefined, this),
148578
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
148897
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148579
148898
  color: color.faint,
148580
148899
  children: "/config inline on for terminal scrollback · /keys for shortcuts"
148581
148900
  }, undefined, false, undefined, this)
@@ -148584,17 +148903,17 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
148584
148903
  ]
148585
148904
  }, undefined, true, undefined, this)
148586
148905
  }, undefined, false, undefined, this);
148587
- const paletteJsx = pickerRows.length || cmdMatches.length || fileMatches.length ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
148906
+ const paletteJsx = pickerRows.length || cmdMatches.length || fileMatches.length ? /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
148588
148907
  flexDirection: "column",
148589
148908
  children: [
148590
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(CommandPalette, {
148909
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(CommandPalette, {
148591
148910
  draft: edit2.value,
148592
148911
  selected: selectedPalette,
148593
148912
  limit: 7,
148594
148913
  rows: pickerRows,
148595
148914
  width
148596
148915
  }, undefined, false, undefined, this),
148597
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(FilePalette, {
148916
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(FilePalette, {
148598
148917
  matches: fileMatches,
148599
148918
  selected: selectedPalette,
148600
148919
  limit: 5,
@@ -148602,24 +148921,24 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
148602
148921
  }, undefined, false, undefined, this)
148603
148922
  ]
148604
148923
  }, undefined, true, undefined, this) : null;
148605
- const quickPickerJsx = quickPicker && quickRows.length ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
148924
+ const quickPickerJsx = quickPicker && quickRows.length ? /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
148606
148925
  flexDirection: "column",
148607
148926
  marginTop: 1,
148608
148927
  children: [
148609
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
148928
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
148610
148929
  paddingX: 1,
148611
148930
  children: [
148612
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
148931
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148613
148932
  color: color.accent,
148614
148933
  children: quickPicker === "model" ? "model" : "effort"
148615
148934
  }, undefined, false, undefined, this),
148616
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
148935
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148617
148936
  color: color.faint,
148618
148937
  children: " · ↑↓ select · ⏎ apply · esc close"
148619
148938
  }, undefined, false, undefined, this)
148620
148939
  ]
148621
148940
  }, undefined, true, undefined, this),
148622
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(CommandPalette, {
148941
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(CommandPalette, {
148623
148942
  draft: "",
148624
148943
  selected: Math.min(quickPickerIndex, quickRows.length - 1),
148625
148944
  limit: quickPickerLimit,
@@ -148628,10 +148947,10 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
148628
148947
  }, undefined, false, undefined, this)
148629
148948
  ]
148630
148949
  }, undefined, true, undefined, this) : null;
148631
- const composerJsx = perm ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(PermissionPrompt, {
148950
+ const composerJsx = perm ? /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(PermissionPrompt, {
148632
148951
  req: perm,
148633
148952
  width
148634
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Composer, {
148953
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Composer, {
148635
148954
  value: edit2.value,
148636
148955
  cursor: edit2.cursor,
148637
148956
  selectionAnchor: edit2.selectionAnchor,
@@ -148641,9 +148960,9 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
148641
148960
  width,
148642
148961
  vim
148643
148962
  }, undefined, false, undefined, this);
148644
- const footerJsx = /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(jsx_dev_runtime12.Fragment, {
148963
+ const footerJsx = /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(jsx_dev_runtime13.Fragment, {
148645
148964
  children: [
148646
- busy || linger ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Working, {
148965
+ busy || linger ? /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Working, {
148647
148966
  state: mascotState,
148648
148967
  skin: ghostSkin,
148649
148968
  verb,
@@ -148652,15 +148971,15 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
148652
148971
  linger: linger && !busy,
148653
148972
  width
148654
148973
  }, undefined, false, undefined, this) : null,
148655
- busy ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(ActivityRail, {
148974
+ busy ? /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(ActivityRail, {
148656
148975
  items,
148657
148976
  width
148658
148977
  }, undefined, false, undefined, this) : null,
148659
- queued.length ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
148978
+ queued.length ? /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
148660
148979
  paddingX: 1,
148661
148980
  marginTop: 1,
148662
148981
  flexDirection: "column",
148663
- children: queued.map((q, i2) => /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
148982
+ children: queued.map((q, i2) => /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148664
148983
  color: color.faint,
148665
148984
  children: [
148666
148985
  "↳ queued: ",
@@ -148668,11 +148987,11 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
148668
148987
  ]
148669
148988
  }, i2, true, undefined, this))
148670
148989
  }, undefined, false, undefined, this) : null,
148671
- mode2 !== "normal" ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
148990
+ mode2 !== "normal" ? /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
148672
148991
  paddingX: 1,
148673
148992
  marginTop: 1,
148674
148993
  children: [
148675
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
148994
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148676
148995
  color: color.accent,
148677
148996
  children: [
148678
148997
  glyph.notice,
@@ -148680,7 +148999,7 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
148680
148999
  mode2 === "plan" ? "plan mode" : "auto-accept edits"
148681
149000
  ]
148682
149001
  }, undefined, true, undefined, this),
148683
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
149002
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148684
149003
  color: color.faint,
148685
149004
  children: [
148686
149005
  " · ",
@@ -148690,14 +149009,14 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
148690
149009
  }, undefined, true, undefined, this)
148691
149010
  ]
148692
149011
  }, undefined, true, undefined, this) : null,
148693
- search ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
149012
+ search ? /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
148694
149013
  paddingX: 1,
148695
149014
  children: [
148696
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
149015
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148697
149016
  color: color.accent,
148698
149017
  children: "(reverse-i-search)"
148699
149018
  }, undefined, false, undefined, this),
148700
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
149019
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148701
149020
  color: color.text,
148702
149021
  children: [
148703
149022
  "`",
@@ -148705,15 +149024,15 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
148705
149024
  "`: "
148706
149025
  ]
148707
149026
  }, undefined, true, undefined, this),
148708
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
149027
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148709
149028
  color: color.dim,
148710
149029
  children: searchHistory(historyRef.current, search.q, search.idx) ?? (search.q ? "(no match)" : "")
148711
149030
  }, undefined, false, undefined, this)
148712
149031
  ]
148713
149032
  }, undefined, true, undefined, this) : null,
148714
- copiedNotice ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
149033
+ copiedNotice ? /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
148715
149034
  paddingX: 1,
148716
- children: /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
149035
+ children: /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Text, {
148717
149036
  color: color.ok,
148718
149037
  children: [
148719
149038
  glyph.notice,
@@ -148723,7 +149042,7 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
148723
149042
  }, undefined, true, undefined, this)
148724
149043
  }, undefined, false, undefined, this) : null,
148725
149044
  quickPickerJsx,
148726
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(StatusBar, {
149045
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(StatusBar, {
148727
149046
  model: modelLabel,
148728
149047
  branch,
148729
149048
  routing,
@@ -148737,7 +149056,7 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
148737
149056
  effort: displayEffort,
148738
149057
  online
148739
149058
  }, undefined, false, undefined, this),
148740
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
149059
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
148741
149060
  height: PALETTE_ROWS,
148742
149061
  flexDirection: "column",
148743
149062
  children: paletteJsx
@@ -148745,31 +149064,42 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
148745
149064
  composerJsx
148746
149065
  ]
148747
149066
  }, undefined, true, undefined, this);
148748
- const inlineFooterJsx = /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(jsx_dev_runtime12.Fragment, {
149067
+ const inlineFooterJsx = /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(jsx_dev_runtime13.Fragment, {
148749
149068
  children: [
148750
149069
  paletteJsx,
148751
149070
  composerJsx
148752
149071
  ]
148753
149072
  }, undefined, true, undefined, this);
148754
149073
  if (fullscreen) {
148755
- return /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
149074
+ return /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
148756
149075
  flexDirection: "column",
148757
149076
  width,
148758
149077
  height: rows,
148759
149078
  children: [
148760
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Banner, {
149079
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Banner, {
148761
149080
  model: modelLabel,
148762
149081
  cwd: basename3(process.cwd()),
148763
149082
  width
148764
149083
  }, undefined, false, undefined, this),
148765
- welcome ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
149084
+ panel ? /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
149085
+ paddingX: 1,
149086
+ children: /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Panel, {
149087
+ panel,
149088
+ width: panelW,
149089
+ height: transcriptHeight,
149090
+ accounts: panelAccountView,
149091
+ models: panelModels,
149092
+ currentModelId: panelCurrentModel,
149093
+ staticLines: panelStaticLines
149094
+ }, undefined, false, undefined, this)
149095
+ }, undefined, false, undefined, this) : welcome ? /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
148766
149096
  height: transcriptHeight,
148767
149097
  flexDirection: "column",
148768
149098
  justifyContent: "center",
148769
149099
  children: hero
148770
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
149100
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
148771
149101
  paddingX: 1,
148772
- children: /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Viewport, {
149102
+ children: /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Viewport, {
148773
149103
  lines,
148774
149104
  scrollTop: effScroll,
148775
149105
  height: transcriptHeight,
@@ -148781,24 +149111,24 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
148781
149111
  ]
148782
149112
  }, undefined, true, undefined, this);
148783
149113
  }
148784
- const banner = /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Banner, {
149114
+ const banner = /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Banner, {
148785
149115
  model: modelLabel,
148786
149116
  cwd: basename3(process.cwd()),
148787
149117
  width
148788
149118
  }, undefined, false, undefined, this);
148789
- return /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
149119
+ return /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
148790
149120
  flexDirection: "column",
148791
149121
  width,
148792
149122
  children: [
148793
- welcome ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(jsx_dev_runtime12.Fragment, {
149123
+ welcome ? /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(jsx_dev_runtime13.Fragment, {
148794
149124
  children: [
148795
149125
  banner,
148796
- /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Box_default, {
149126
+ /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Box_default, {
148797
149127
  marginTop: 1,
148798
149128
  children: hero
148799
149129
  }, undefined, false, undefined, this)
148800
149130
  ]
148801
- }, undefined, true, undefined, this) : /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Transcript, {
149131
+ }, undefined, true, undefined, this) : /* @__PURE__ */ jsx_dev_runtime13.jsxDEV(Transcript, {
148802
149132
  items,
148803
149133
  width,
148804
149134
  header: banner,
@@ -148812,7 +149142,7 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
148812
149142
  // src/cli.tsx
148813
149143
  init_providers();
148814
149144
  init_permission();
148815
- var jsx_dev_runtime13 = __toESM(require_jsx_dev_runtime(), 1);
149145
+ var jsx_dev_runtime14 = __toESM(require_jsx_dev_runtime(), 1);
148816
149146
  process.env.LANG = process.env.LANG || "en_US.UTF-8";
148817
149147
  process.env.LC_ALL = process.env.LC_ALL || "en_US.UTF-8";
148818
149148
  var VERSION16 = "0.1.32";
@@ -149310,7 +149640,7 @@ if (fullscreen)
149310
149640
  process.stdout.write("\x1B[?1049h\x1B[2J\x1B[H");
149311
149641
  if (mouse)
149312
149642
  process.stdout.write("\x1B[?1000h\x1B[?1002h\x1B[?1006h");
149313
- var app = render_default(/* @__PURE__ */ jsx_dev_runtime13.jsxDEV(App2, {
149643
+ var app = render_default(/* @__PURE__ */ jsx_dev_runtime14.jsxDEV(App2, {
149314
149644
  selector,
149315
149645
  fullscreen,
149316
149646
  resumeId
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gearbox-code",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "description": "A beautiful multi-provider coding harness for the terminal. (Intelligent model routing lands on top of this soon.)",
5
5
  "type": "module",
6
6
  "license": "MIT",