Skip to content

Commit 469a29f

Browse files
committed
Dismiss add-provider from /connect without reopening models
Esc from /connect was reopening the model picker because the opener always wired onCancel to openModels. Only Alt+A from an already-open picker needs that return path.
1 parent 5c13bff commit 469a29f

7 files changed

Lines changed: 39 additions & 14 deletions

File tree

docs/IMPLEMENTATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ Profiles supply per-project or named-profile overrides for `model` and `systemPr
303303

304304
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.
305305

306-
**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.
306+
**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.
307307

308308
**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.
309309

docs/PRODUCT.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,11 @@ recovery line instead of dumping the file path and parse details.
103103

104104
## Slash Commands (TUI)
105105

106-
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.
106+
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.
107107

108108
**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.
109109

110-
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, 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.
110+
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, 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.
111111

112112
`/model` opens a dedicated full-screen modal — the single place agent configuration lives. The default view is models-only (Recent / Favorites / connected models); add-provider, tiers, and profiles remain reachable from the same surface without in-list “connect →” rows. A switch applies to the running session immediately (no restart): inference, permission identity, grant persistence identity, and advertised tool schemas cut over together, and the choice can be saved as this project's default (written to the per-repo selection file). Recent and favorite model pairs are stored in global settings (no credentials).
113113

src/tui/command-surfaces.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,8 @@ export interface CommandSurfaceDeps {
167167
readonly settings?: SettingsSurfaceDeps;
168168
/** Opens the host's model/provider picker (owned by the product host). */
169169
readonly openModels?: () => void;
170-
/** Opens the host's add-provider selector (owned by the product host). */
171-
readonly openAddProvider?: () => void;
170+
/** Opens the host's add-provider selector (owned by the product host). `/connect` omits returnToModels. */
171+
readonly openAddProvider?: (opts?: { returnToModels?: boolean }) => void;
172172
/** Fallback channel for surfaces with no live data source. */
173173
readonly notify: (text: string) => void;
174174
}

src/tui/overlays.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ export interface OpenAddProviderOpts {
208208
readonly onAccept?: (selection: OverlaySelection) => void;
209209
/** Description-zone source, keyed by the focused row's id. */
210210
readonly describe?: (itemId: string) => ItemDescription | null;
211-
/** Per-open Esc/dismiss — the caller returns to the model list. */
211+
/** Per-open Esc/dismiss. Set when Esc should return to the model list (Alt+A). */
212212
readonly onCancel?: () => void;
213213
}
214214

src/tui/product-host.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,26 @@ describe("flat type-to-filter model picker", () => {
911911
}
912912
});
913913

914+
test("Esc after openAddProvider from a closed prompt does not reopen the model list", async () => {
915+
const { harness, host } = await mountPicker({
916+
onConnectProvider: () => {},
917+
addProviderChoices: () => [{ id: "codex", label: "Codex", hint: "", accountCount: 1 }],
918+
});
919+
try {
920+
expect(host.shell.overlayKind).toBeNull();
921+
host.openAddProvider?.();
922+
await harness.renderOnce();
923+
expect(host.shell.overlayKind).toBe("add_provider");
924+
closeInsetOverlay(host.shell);
925+
await harness.renderOnce();
926+
expect(host.shell.overlayKind).not.toBe("model_picker");
927+
expect(host.shell.overlayKind).toBeNull();
928+
} finally {
929+
host.dispose();
930+
harness.destroy();
931+
}
932+
});
933+
914934
test("Enter on an add-provider row runs the connect flow for that provider", async () => {
915935
const connected: string[] = [];
916936
const { harness, host } = await mountPicker({

src/tui/product-host.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,11 @@ export interface ProductHost {
207207
readonly openModels?: (focusId?: string) => void;
208208
/**
209209
* Opens the add-provider selector; absent when connect choices are not wired.
210+
* Pass `returnToModels: true` when opening from the model picker (Alt+A) so
211+
* Esc returns there. `/connect` and other closed-prompt callers omit it so
212+
* Esc dismisses to a closed overlay.
210213
*/
211-
readonly openAddProvider?: () => void;
214+
readonly openAddProvider?: (opts?: { returnToModels?: boolean }) => void;
212215
/** Swap the picker's rows/descriptions in place (e.g. after a provider connects). */
213216
readonly setModels?: (
214217
models: readonly ProductHostModelOption[],
@@ -518,7 +521,7 @@ export async function mountProductHost(config: ProductHostConfig): Promise<Produ
518521
let currentModels = config.models ?? [];
519522
let currentDescribeModel = config.describeModel;
520523
let openModels: ((focusId?: string) => void) | undefined;
521-
let openAddProvider: (() => void) | undefined;
524+
let openAddProvider: ((opts?: { returnToModels?: boolean }) => void) | undefined;
522525
if (config.onModelSelect) {
523526
const onSelect = config.onModelSelect;
524527
const onConnect = config.onConnectProvider;
@@ -535,7 +538,7 @@ export async function mountProductHost(config: ProductHostConfig): Promise<Produ
535538
// which does not apply here.
536539
openAddProvider =
537540
addProviderChoices !== undefined && onConnect !== undefined
538-
? (): void => {
541+
? (opts?: { returnToModels?: boolean }): void => {
539542
const rows = addProviderChoices();
540543
closeInsetOverlay(shell);
541544
openAddProviderOverlay(shell, {
@@ -558,9 +561,10 @@ export async function mountProductHost(config: ProductHostConfig): Promise<Produ
558561
tone: "plain",
559562
};
560563
},
561-
// Esc returns to the model list through the same entry point
562-
// Alt+A itself, /model, and a completed connect all use.
563-
onCancel: () => openModels?.(),
564+
// Alt+A from the model picker: Esc returns through the same entry
565+
// point Alt+A itself, /model, and a completed connect all use.
566+
// /connect from a closed prompt omits this so Esc dismisses.
567+
...(opts?.returnToModels === true ? { onCancel: () => openModels?.() } : {}),
564568
});
565569
}
566570
: undefined;
@@ -595,7 +599,7 @@ export async function mountProductHost(config: ProductHostConfig): Promise<Produ
595599
// Alt+A / composed Option+A (å/Å) — never bare ASCII `a`;
596600
// type-to-filter claims ordinary printables.
597601
if (openAddProvider !== undefined && isAddProviderShortcutKey(key)) {
598-
openAddProvider();
602+
openAddProvider({ returnToModels: true });
599603
return true;
600604
}
601605
if (!(key.meta || key.option)) return false;

src/tui/runner.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2173,7 +2173,8 @@ export async function runTUI(initialConfig: Config): Promise<number> {
21732173
return;
21742174
case "overlay":
21752175
if (!host.openSurface(result.overlay)) {
2176-
systemNotice(`No surface for /${result.overlay}.`);
2176+
const named = result.overlay === "add-provider" ? "connect" : result.overlay;
2177+
systemNotice(`No surface for /${named}.`);
21772178
}
21782179
return;
21792180
case "modal":

0 commit comments

Comments
 (0)