Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename

## [Unreleased]

### TUI

- `/connect` opens Add Provider from any layout.
- Option+A still opens Add Provider. Terminals that emit å/Å for Option+A
without the option modifier also open it in `/model` (US/ABC compose).
Dedicated Nordic å in `/model` is treated as that shortcut when Add
Provider is offered.

### Fixed

- Codex ChatGPT subscription sessions no longer show a public-rate dollar
Expand Down
2 changes: 1 addition & 1 deletion docs/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ Profiles supply per-project or named-profile overrides for `model` and `systemPr

Providers and credentials are read exclusively from settings files: the global `~/.corbits/settings.json` (definitions + credentials) and the per-repo `.corbits/settings.json` (selection only). There are no `OPENAI_COMPATIBLE_*` environment-variable overrides, and `index.ts` does not load `.env` files — a deliberately stale or exported key can no longer shadow the configured provider.

**Models-first connect.** There is no standalone `/login` command. `/model` opens on a flat **models-only** list (Recent, Favorites, then connected provider/model rows) built by `buildModelsFirstList` (`src/tui/model-picker.ts`); type-to-filter owns printable keys. Selecting a row runs `applyLiveModelSwitch` (`src/session/live-model-switch.ts`) so inference sources, permission-gate identity, grant persistence identity, and advertised tool schemas cut over together. **Alt+A** opens Connect via `addProviderSelectorChoices` (`src/tui/provider-setup.ts`), which lists every first-class kind including Custom — never bare `c` / Ctrl+A, and never in-list “connect →” rows. First-class API-key rows use a named-instance + auth-only form (instance name, key; catalog base URL is display-only); Custom keeps the full manual form. **Alt+F** toggles favorites; recent/favorite pairs live in global settings (`recentModels` / `favoriteModels`). **Alt+D** sets the default via `setDefaultModel` (global `defaultProvider` + that provider's `defaultModel`) plus `persistConnectedSelection` without switching the live session. First-class providers ship from `packages/first-class-providers` (corbits-agnostic defs) and `packages/opencode-go` (Go catalog, auth validate, multi-protocol endpoints, usage). OAuth providers open the existing browser login modal with a named account step; API-key providers share the same multi-instance naming and pre-seed models on save so selection works without restart. Both OAuth and API-key (including Custom) connects share `persistConnectedSelection` in `provider-setup-submit.ts` so project-local provider/model selection is written alongside global credentials. OpenCode Go forces `OPENCODE_GO_BASE_URL` when `opencodeGo` is set so subscription traffic is not billed as Zen PAYG.
**Models-first connect.** There is no standalone `/login` command. `/model` opens on a flat **models-only** list (Recent, Favorites, then connected provider/model rows) built by `buildModelsFirstList` (`src/tui/model-picker.ts`); type-to-filter owns printable keys. Selecting a row runs `applyLiveModelSwitch` (`src/session/live-model-switch.ts`) so inference sources, permission-gate identity, grant persistence identity, and advertised tool schemas cut over together. **Alt+A** or `/connect` opens Connect via `addProviderSelectorChoices` (`src/tui/provider-setup.ts`), which lists every first-class kind including Custom — never bare `c` / Ctrl+A, and never in-list “connect →” rows. First-class API-key rows use a named-instance + auth-only form (instance name, key; catalog base URL is display-only); Custom keeps the full manual form. **Alt+F** toggles favorites; recent/favorite pairs live in global settings (`recentModels` / `favoriteModels`). **Alt+D** sets the default via `setDefaultModel` (global `defaultProvider` + that provider's `defaultModel`) plus `persistConnectedSelection` without switching the live session. First-class providers ship from `packages/first-class-providers` (corbits-agnostic defs) and `packages/opencode-go` (Go catalog, auth validate, multi-protocol endpoints, usage). OAuth providers open the existing browser login modal with a named account step; API-key providers share the same multi-instance naming and pre-seed models on save so selection works without restart. Both OAuth and API-key (including Custom) connects share `persistConnectedSelection` in `provider-setup-submit.ts` so project-local provider/model selection is written alongside global credentials. OpenCode Go forces `OPENCODE_GO_BASE_URL` when `opencodeGo` is set so subscription traffic is not billed as Zen PAYG.

**OpenCode Go multi-protocol.** Each Go model carries protocol metadata (`chat-completions`, `responses`, or `messages`). `buildGoSource` / `resolveGoEndpoint` pick the adapter and base URL per model (not a single provider-wide OpenAI route). When Go is the active provider, subscription usage is fetched for the status bar and omitted on auth/network failure.

Expand Down
4 changes: 2 additions & 2 deletions docs/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,11 @@ recovery line instead of dumping the file path and parse details.

## Slash Commands (TUI)

The TUI has an extensible slash-command framework. Built-ins include `/help` (shortcut + command overlay), `/model` (models-only picker for connected accounts; **Alt+A** adds a provider), `/settings`, `/permissions`, `/plugins`, `/clear`, `/new`, `/mcp`, and `/yolo` (persists as the user-global skip-permissions default; `--dangerously-skip-permissions` still forces this process; secret-guard and authz still apply; `/yolo [on|off|toggle]`, bare `/yolo` toggles), plus a `/<name>` command per available workflow. When a session starts with the persisted default already on, the TUI shows a startup notice ("Permission prompts are disabled by your saved default…") so the silent machine-wide default is never invisible; `corbits exec` prints the equivalent warning to stderr. Plugins can register additional commands.
The TUI has an extensible slash-command framework. Built-ins include `/help` (shortcut + command overlay), `/model` (models-only picker for connected accounts; **Alt+A** or `/connect` adds a provider), `/settings`, `/permissions`, `/plugins`, `/clear`, `/new`, `/mcp`, and `/yolo` (persists as the user-global skip-permissions default; `--dangerously-skip-permissions` still forces this process; secret-guard and authz still apply; `/yolo [on|off|toggle]`, bare `/yolo` toggles), plus a `/<name>` command per available workflow. When a session starts with the persisted default already on, the TUI shows a startup notice ("Permission prompts are disabled by your saved default…") so the silent machine-wide default is never invisible; `corbits exec` prints the equivalent warning to stderr. Plugins can register additional commands.

**Default skills** exist out of the gate as first-party slash **actions**, not director names: `/implement`, `/plan`, `/refactor`, `/review`, `/pull-request-review`, `/create-issue`, `/scribe`, `/interview`, `/ast-grep`. Each one is a how-to playbook — the slash sends the skill body to the primary, which follows the steps. Skills do not assign identity or route the fleet; that stays on director system prompts. `/review` is how to review a branch; `/scribe` is how to maintain PRODUCT / ARCHITECTURE / IMPLEMENTATION; `/implement` is the per-commit review/build/critique loop; `/plan` authors an eng change plan (files, AC, non-goals, risks, ordered steps) and does not implement. `/create-issue` remains the tracker command: Linear MCP when available; otherwise it `ask_operator`s for the platform (GitHub etc.) and persists `Preferred issue tracker` in `.corbits/MEMORY.md` (GitHub via `gh issue create`). There is no first-party dispatch skill — Skywalker orchestrates natively. `git-rebase`, `linear-issue-workflow`, `style`, `philosophy`, `typescript`, and `opsh` stay `use_skill` only (`user-invocable: false`). Draper and emil are not slashes; they remain closed directors via `task(agent=…)`. There is no catch-all worker. Slash names are also available to the model via `use_skill`. Disable the catalog in `/plugins` (`corbits-skills`) if you want them gone.

Providers are **models-first**: there is no standalone `/login` command. `/model` opens a **models-only list** (Recent, Favorites, then connected provider/model rows) — type-to-filter owns printable keys, so Connect is never a bare letter. **Alt+A** opens a dedicated add-provider selector over every first-class kind (OpenAI dual-path ChatGPT OAuth or API key, xAI, OpenCode Zen, Anthropic, Google, OpenCode Go, Z.AI Coding Plan, Ollama, Custom), each annotated with its live account count and never filtered out for “already connected.” **Alt+F** toggles favorite on the highlighted model. **Alt+D** persists the highlighted pair as the default without switching the live session. Advanced provider drill-down (edit/delete/tiers) stays on the advanced surface, not a bare printable key while the model list is filtering. OAuth providers open their existing browser login with a named account step so multiple accounts per kind coexist (`codex/work`, …). API-key providers use the same named-instance step before the key (auth-only form: instance name + key + fixed catalog base URL), so personal and team keys land as distinct catalog rows (`openai/default`, `anthropic/work`, …); reusing a name re-keys that instance after confirm. Custom remains a free-form single endpoint (full manual form). Successful connect refreshes the catalog and reopens the model list focused on the new account’s default model. OpenCode Go routes each model by its protocol metadata (chat completions, OpenAI responses, or Anthropic messages) and can show subscription usage in the status bar when active (rolling 5h / weekly / monthly windows when the usage API responds; omitted on auth or network failure). When Go returns a quota or rate-limit error — including some HTTP 400 responses that carry limit payloads — Corbits classifies them so quota aborts cleanly and short provider rate limits remain retryable. On a free-tier or subscription quota hit, wait for the window to reset or use OpenCode Zen free models.
Providers are **models-first**: there is no standalone `/login` command. `/model` opens a **models-only list** (Recent, Favorites, then connected provider/model rows) — type-to-filter owns printable keys, so Connect is never a bare letter. **Alt+A** or `/connect` opens a dedicated add-provider selector over every first-class kind (OpenAI dual-path ChatGPT OAuth or API key, xAI, OpenCode Zen, Anthropic, Google, OpenCode Go, Z.AI Coding Plan, Ollama, Custom), each annotated with its live account count and never filtered out for “already connected.” **Alt+F** toggles favorite on the highlighted model. **Alt+D** persists the highlighted pair as the default without switching the live session. Advanced provider drill-down (edit/delete/tiers) stays on the advanced surface, not a bare printable key while the model list is filtering. OAuth providers open their existing browser login with a named account step so multiple accounts per kind coexist (`codex/work`, …). API-key providers use the same named-instance step before the key (auth-only form: instance name + key + fixed catalog base URL), so personal and team keys land as distinct catalog rows (`openai/default`, `anthropic/work`, …); reusing a name re-keys that instance after confirm. Custom remains a free-form single endpoint (full manual form). Successful connect refreshes the catalog and reopens the model list focused on the new account’s default model. OpenCode Go routes each model by its protocol metadata (chat completions, OpenAI responses, or Anthropic messages) and can show subscription usage in the status bar when active (rolling 5h / weekly / monthly windows when the usage API responds; omitted on auth or network failure). When Go returns a quota or rate-limit error — including some HTTP 400 responses that carry limit payloads — Corbits classifies them so quota aborts cleanly and short provider rate limits remain retryable. On a free-tier or subscription quota hit, wait for the window to reset or use OpenCode Zen free models.

Ollama is a first-class, keyless local provider: users can run Corbits against local models without creating a cloud account or storing an API key. Local availability is treated gracefully during setup — an Ollama server that is not running yet is an expected, nonfatal state, with clear retry and server-address editing paths. Once Ollama is reachable, Corbits distinguishes a server with no models from an invalid response so the user gets the relevant recovery guidance. The only prerequisite guidance Corbits gives is to start Ollama and pull at least one model; installation remains Ollama's concern.

Expand Down
17 changes: 11 additions & 6 deletions docs/TUI.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,15 +410,20 @@ not-yet-configured provider kind, filtered out once that kind had any
connected account. That filtering made a second OAuth account (a second
Codex or xAI login) unreachable — OAuth accounts are per-profile, so
kind-level "already connected" filtering hid the connect path the moment the
first profile existed. **Alt+A** now opens `add_provider`
first profile existed. **Alt+A** (US-style Option+A, including composed å/Å)
and **/connect** now open `add_provider`
(`src/tui/overlays.ts:openAddProviderOverlay`), a separate `PrimaryOverlayKind`
listing every first-class provider kind from `providerChoices()` — OAuth,
API-key, keyless local, and Custom alike — each annotated with its live
connected-account count and none of them filtered out. Custom uses the full
manual form (name, base URL, key, model); OAuth and API-key kinds keep their
auth-only or browser login paths, while Ollama has a keyless local setup path.
Esc returns to the model list through the same
`openModels()` entry point the picker itself uses. Picking a row runs the
connected-account count and none of them filtered out. `/connect` is the
layout-proof path: layouts whose Option+A is not å/Å still type a printable
glyph, so Alt+A is a dead chord there. Custom uses the full manual form
(name, base URL, key, model); OAuth and API-key kinds keep their auth-only
or browser login paths, while Ollama has a keyless local setup path. Esc
after Alt+A from `/model` returns to the model list through the same
`openModels()` entry point the picker itself uses. Esc after `/connect`
from a closed prompt dismisses the selector without reopening `/model`.
Picking a row runs the
existing inline connect flow (`provider-connect.ts`); first-class kinds (OAuth
and API-key) both ask for an instance/account name before auth so multiple
instances coexist as `kind/slug` catalog rows, and reusing a name confirms
Expand Down
16 changes: 16 additions & 0 deletions src/tui/command-surfaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,22 @@ describe("model surface", () => {
});
});

describe("add-provider surface", () => {
test("routes to the host opener, and reports the gap when absent", async () => {
await withShell((shell) => {
let opened = 0;
expect(
openCommandSurface(shell, "add-provider", {
notify: () => {},
openAddProvider: () => opened++,
}),
).toBe(true);
expect(opened).toBe(1);
expect(openCommandSurface(shell, "add-provider", { notify: () => {} })).toBe(false);
});
});
});

describe("help surface", () => {
test("opens the keymap overlay", async () => {
await withShell((shell) => {
Expand Down
8 changes: 7 additions & 1 deletion src/tui/command-surfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,15 @@ export interface CommandSurfaceDeps {
readonly settings?: SettingsSurfaceDeps;
/** Opens the host's model/provider picker (owned by the product host). */
readonly openModels?: () => void;
/** Opens the host's add-provider selector (owned by the product host). `/connect` omits returnToModels. */
readonly openAddProvider?: (opts?: { returnToModels?: boolean }) => void;
/** Fallback channel for surfaces with no live data source. */
readonly notify: (text: string) => void;
}

/** Surface a command result can ask for. */
export type CommandSurfaceKind =
"help" | "settings" | "permissions" | "plugins" | "hooks" | "mcp" | "models";
"help" | "settings" | "permissions" | "plugins" | "hooks" | "mcp" | "models" | "add-provider";

const CLOSE_ID = "__close__";
const BACK_ID = "__back__";
Expand Down Expand Up @@ -1034,5 +1036,9 @@ export function openCommandSurface(
if (deps.openModels === undefined) return false;
deps.openModels();
return true;
case "add-provider":
if (deps.openAddProvider === undefined) return false;
deps.openAddProvider();
return true;
}
}
15 changes: 14 additions & 1 deletion src/tui/commands/built-in.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,24 @@ describe("removed commands", () => {
expect(getCommand("scope")).toBeUndefined();
});

it("/login is not registered (connect from /model)", () => {
it("/login is not registered (connect from /model or /connect)", () => {
expect(getCommand("login")).toBeUndefined();
});
});

describe("/connect command", () => {
it("is registered", () => {
expect(getCommand("connect")).toBeDefined();
});

it("requests the add-provider overlay", () => {
expect(getCommand("connect")!.handler("", makeCtx())).toEqual({
type: "overlay",
overlay: "add-provider",
});
});
});

describe("/status command", () => {
it("answers from the live fleet without sending anything to the model", () => {
const ctx: CommandContext = {
Expand Down
10 changes: 7 additions & 3 deletions src/tui/commands/built-in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,13 @@ export function registerBuiltInCommands(): void {
handler: (_args, _ctx) => ({ type: "overlay", overlay: "hooks" }),
});

// Models-first connect: providers are connected from /model via the Alt+A
// add-provider selector, not a standalone /login picker. The OAuth sign-in
// surface is reachable only through that connect flow.
// Layout-proof add-provider path: `/` works on every keyboard. There is no
// standalone /login; OAuth sign-in is still reached only through this flow.
registerCommand({
name: "connect",
description: "Add a provider account",
handler: () => ({ type: "overlay", overlay: "add-provider" }),
});

// signalClear rotates to a fresh session: the on-screen transcript and run
// telemetry are reset and the agent is rebuilt against a new state directory,
Expand Down
5 changes: 4 additions & 1 deletion src/tui/commands/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ export type CommandResult =
| { type: "message"; text: string }
| { type: "send"; text: string }
| { type: "view"; view: "tasks" }
| { type: "overlay"; overlay: "help" | "permissions" | "plugins" | "settings" | "hooks" | "mcp" }
| {
type: "overlay";
overlay: "help" | "permissions" | "plugins" | "settings" | "hooks" | "mcp" | "add-provider";
}
| { type: "modal"; modal: "agent" | "codex-login" | "xai-login" }
| { type: "workflow"; name: string; args?: string }
| { type: "paste-image" }
Expand Down
4 changes: 2 additions & 2 deletions src/tui/overlay-paint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,14 @@ describe("overlay host never shares cells with the prompt border", () => {
kind: "model_picker",
title: "model",
items: ITEMS,
// Mirror production /model, which always wires Alt+A.
// Mirror production /model, which always wires add-provider.
addProviderHint: true,
}),
size,
);

const expected = [
" model · Esc cancel · Enter choose · Alt+A add provider",
" model · Esc cancel · Enter choose · Alt+A /connect add provider",
` > ${ITEMS[0]}`,
...ITEMS.slice(1).map((i) => ` ${i}`),
];
Expand Down
4 changes: 2 additions & 2 deletions src/tui/overlays.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export interface OpenModelPickerOpts {
* as you type. Off by default so other list overlays keep j/k.
*/
readonly typeToFilter?: boolean;
/** Advertise Alt+A in the footer — only when the caller wired the handler. */
/** Advertise Alt+A /connect in the footer — only when the caller wired the handler. */
readonly addProviderHint?: boolean;
/** Advertise Alt+D in the footer — only when the caller wired the handler. */
readonly setDefaultHint?: boolean;
Expand Down Expand Up @@ -208,7 +208,7 @@ export interface OpenAddProviderOpts {
readonly onAccept?: (selection: OverlaySelection) => void;
/** Description-zone source, keyed by the focused row's id. */
readonly describe?: (itemId: string) => ItemDescription | null;
/** Per-open Esc/dismiss — the caller returns to the model list. */
/** Per-open Esc/dismiss. Set when Esc should return to the model list (Alt+A). */
readonly onCancel?: () => void;
}

Expand Down
Loading
Loading