Skip to content
4 changes: 2 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,8 @@ Ink 7 + React 19, full-screen via the alternate-screen buffer.
- Hooks: `use-gates` (permission/plan/operator gates), `use-keymap`, `use-scroll`, `use-mouse-scroll`, `use-spinner`, `use-terminal-size`, `use-layout-geometry`, `use-mcp-status`, `use-provider-manager`.
- Components: `header`, `event-log`, `chat-input`, `status-bar`, `task-view`, `operator-modal`, `permission-modal`, `permissions-manager`, `plugins-manager`, `settings-overlay`, `agent-modal`, `exit-confirm`, `help-overlay`, `hook-panel`, `codex-login-modal`, `mcp-auth-prompt`, `onboarding-animation`, `in-flight-indicator`.
- Support: `stdin-filter.ts` (strips SGR mouse sequences before Ink parses input — see below), `tool-formatter.ts` (human-readable tool args/results), `markdown-parser.ts`, `keymap-table.ts`, `theme.ts` (semantic color roles including `dim` and `live`).
- Slash commands: `commands/registry.ts` (extensible registry) + `commands/built-in.ts` (`/help`, `/model`, `/settings`, `/permissions`, `/plugins`, `/clear`, `/new`, `/mcp`). There is no `/login` — connect providers from `/model` (c / Ctrl+A).
- `/model` configuration surface (`components/agent-modal.tsx`): a full-screen, section-based modal. The Provider/Model section reuses the provider catalog (from `config.providers`) and applies a switch live via `agent.setSource()` — the runtime's in-place source mutation, read at the next inference call, so no agent recreation. **Connect provider** lists first-class providers (Codex, xAI, Zen, Anthropic, OpenAI, Google, OpenCode Go); OAuth opens `codex-login-modal`, API-key flows pre-seed models and persist on save. OpenCode Go sources are built with per-model protocol routing (`buildGoSource` / `resolveGoEndpoint`). "Set as default" persists the selection (selection-only, no credentials) to the per-repo `.corbits/settings.json` via `saveLocalSettings`.
- Slash commands: `commands/registry.ts` (extensible registry) + `commands/built-in.ts` (`/help`, `/model`, `/settings`, `/permissions`, `/plugins`, `/clear`, `/new`, `/mcp`). There is no `/login` — connect providers from `/model` (Alt+A / c).
- `/model` configuration surface (`components/agent-modal.tsx`): a full-screen, section-based modal. **Default step is models-first** (Recent / Favorites / Providers via `buildModelsFirstList`); advanced provider drill-down remains on **a**. **Connect** lists first-class providers (OpenAI dual-path, xAI, Zen, Anthropic, Google, OpenCode Go, Z.AI, Custom); OAuth opens `codex-login-modal` / xAI login, API-key flows use an auth-only form and pre-seed models on save. OpenCode Go sources are built with per-model protocol routing (`buildGoSource` / `resolveGoEndpoint`) and a forced Go base URL when `opencodeGo` is set. "Set as default" persists the selection (selection-only, no credentials) to the per-repo `.corbits/settings.json` via `saveLocalSettings`. Recent/favorite model pairs persist in global settings.

#### Event log rendering

Expand Down
2 changes: 1 addition & 1 deletion docs/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ Profiles supply per-project or named-profile overrides for `model`, `maxTurns`,

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. From `/model`, press **c** or **Ctrl+A** to open Connect provider. First-class providers (Codex, xAI, OpenCode Zen, Anthropic, OpenAI, Google, OpenCode Go) 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; API-key providers pre-seed models and persist on save so selection works without restart.
**Models-first connect.** There is no standalone `/login` command. `/model` opens on a flat model list (Recent, Favorites, then provider groups) built by `buildModelsFirstList` (`src/tui/model-picker.ts`). **Alt+A** / **c** opens Connect; API-key first-class rows use an auth-only form (key only; catalog base URL is display-only). **Alt+F** toggles favorites; recent/favorite pairs live in global settings (`recentModels` / `favoriteModels`). 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; API-key providers pre-seed models and persist on save so selection works without restart. 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 @@ -74,9 +74,9 @@ Continues from the last saved state in the working directory.

The TUI has an extensible slash-command framework. Built-ins include `/help` (shortcut + command overlay), `/model` (open the agent configuration surface — connect providers with **c** / **Ctrl+A**, pick models, tiers, and profiles), `/settings`, `/permissions`, `/plugins`, `/clear`, `/new`, `/mcp`, and `/goal` (session goal: expand a brief into an acceptance checklist and auto-continue until every criterion is done — see `/goal [turns] <brief>`, `/goal pause|resume|clear|status`, optional `--tokens N` / `--replace`), plus a `/<name>` command per available workflow. Plugins can register additional commands.

Providers are **models-first**: there is no standalone `/login` command. From `/model`, Connect provider lists first-class providers (Codex, xAI, OpenCode Zen, Anthropic, OpenAI, Google, OpenCode Go). OAuth providers (Codex, xAI) open their existing browser login; API-key providers prompt for a key, validate, and persist pre-seeded models for immediate selection. 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 on a **model list** (Recent, Favorites, then providers) so you pick a model without drilling provider first. **Alt+A** (or **c**) opens Connect; **Alt+F** toggles favorite on the highlighted model; **a** opens the advanced provider drill-down (edit/delete/tiers). Connect lists first-class providers (OpenAI dual-path ChatGPT OAuth or API key, xAI, OpenCode Zen, Anthropic, Google, OpenCode Go, Z.AI Coding Plan, Custom). OAuth providers open their existing browser login; API-key providers show an **auth-only** form (key + fixed catalog base URL), validate, and persist pre-seeded models for immediate selection. 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.

`/model` opens a dedicated full-screen modal — the single place agent configuration lives. It lists configured providers, marks the active one, and lets you connect new providers (**c** / **Ctrl+A**), switch provider and model, and manage tiers and profiles. A switch applies to the running session immediately (no restart), and can be saved as this project's default (written to the per-repo selection file). The surface is section-based so future configuration (system-prompt overrides, profiles) lands as additional sections rather than new slash commands.
`/model` opens a dedicated full-screen modal — the single place agent configuration lives. The default view is models-first (Recent / Favorites / Providers); connect, tiers, and profiles remain reachable from the same surface. A switch applies to the running session immediately (no restart), and 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).

`/goal <brief>` arms a session-scoped goal governor. The operator brief is **not** the completion condition: the agent must clarify success (via `ask_operator` when vague) and expand it into a multi-item **acceptance** checklist with `manage_goal` *before* substantial work. Work steps go in `manage_tasks` (shown as **Work** while a goal is active) — separate from acceptance. Lifecycle phases surface in the UI: **planning** (define Acceptance) → **implementing** (Work primary; Acceptance compact; `doing` on a criterion stays here) → **reviewing** (starts when any criterion is `done` or `blocked`) → **completed** (all non-cancelled criteria done; auto-achieves). After each clean yield the agent is re-inferred until every acceptance criterion is done, a finite turn/token budget soft-stops, or the operator pauses/clears. **Default turn budget is unlimited** (`0`); an optional leading integer caps continues (`/goal 40 ship the feature`). Resume restores a prior goal as **paused** (never silently re-armed); unlimited goals stay unlimited on resume, finite ones get headroom. While a goal is **active**, permission prompts that still need a human answer auto-skip after ~15s with a note back to the agent (human may be away — continue another way); the operator can still approve/deny earlier. Pair with auto mode and/or `--dangerously-skip-permissions` for longer unattended runs. Goal mode does not shrink tools, skills, slash commands, sub-agents, or MCP.

Expand Down
4 changes: 4 additions & 0 deletions packages/first-class-providers/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
export {
FIRST_CLASS_PROVIDERS,
connectListProviders,
firstClassPathAsProvider,
firstClassProviderById,
} from "./providers.js";
export type {
FirstClassAuthKind,
FirstClassBillingProduct,
FirstClassOAuthProvider,
FirstClassProviderDef,
FirstClassProviderPath,
} from "./types.js";

// Re-export Go package surface so hosts can depend on one entry for connect.
Expand Down
88 changes: 80 additions & 8 deletions packages/first-class-providers/src/providers.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,62 @@
import { describe, expect, test } from "bun:test";
import { FIRST_CLASS_PROVIDERS, firstClassProviderById } from "./providers.js";
import {
FIRST_CLASS_PROVIDERS,
connectListProviders,
firstClassPathAsProvider,
firstClassProviderById,
} from "./providers.js";

describe("FIRST_CLASS_PROVIDERS", () => {
test("lists all first-class providers in product order", () => {
test("lists Tier A providers in product order", () => {
expect(FIRST_CLASS_PROVIDERS.map((p) => p.id)).toEqual([
"codex",
"openai",
"xai",
"opencode-go",
"zen",
"zai",
"anthropic",
"openai",
"google",
"opencode-go",
"custom",
]);
});

test("Codex and xAI are OAuth; others are API key", () => {
expect(firstClassProviderById("codex")?.auth).toBe("oauth");
test("connectListProviders matches FIRST_CLASS_PROVIDERS", () => {
expect(connectListProviders()).toBe(FIRST_CLASS_PROVIDERS);
});

test("has no separate Codex connect row", () => {
expect(FIRST_CLASS_PROVIDERS.some((p) => p.id === "codex")).toBe(false);
expect(FIRST_CLASS_PROVIDERS.map((p) => p.label)).not.toContain("OpenAI Codex");
});

test("Custom is last and uses custom auth", () => {
const last = FIRST_CLASS_PROVIDERS[FIRST_CLASS_PROVIDERS.length - 1];
expect(last?.id).toBe("custom");
expect(last?.auth).toBe("custom");
});

test("OpenAI is a chooser with ChatGPT oauth and API key paths", () => {
const openai = firstClassProviderById("openai");
expect(openai?.auth).toBe("chooser");
expect(openai?.paths?.map((p) => p.id)).toEqual(["chatgpt", "api"]);

const chatgpt = openai?.paths?.find((p) => p.id === "chatgpt");
expect(chatgpt?.auth).toBe("oauth");
expect(chatgpt?.oauth).toBe("codex");
expect(chatgpt?.providerId).toBe("codex");

const api = openai?.paths?.find((p) => p.id === "api");
expect(api?.auth).toBe("api-key");
expect(api?.providerId).toBe("openai");
expect(api?.baseURL).toBe("https://api.openai.com/v1");
expect((api?.models ?? []).length).toBeGreaterThan(0);
expect(api?.models).toContain(api?.defaultModel);
});

test("xAI is OAuth; Go/Zen/Z.AI/Anthropic/Google are API key", () => {
expect(firstClassProviderById("xai")?.auth).toBe("oauth");
for (const id of ["zen", "anthropic", "openai", "google", "opencode-go"] as const) {
expect(firstClassProviderById("xai")?.oauth).toBe("xai");
for (const id of ["opencode-go", "zen", "zai", "anthropic", "google"] as const) {
expect(firstClassProviderById(id)?.auth).toBe("api-key");
}
});
Expand All @@ -32,7 +71,40 @@ describe("FIRST_CLASS_PROVIDERS", () => {
}
});

test("Z.AI Coding Plan uses coding paas base URL", () => {
const zai = firstClassProviderById("zai");
expect(zai?.label).toBe("Z.AI Coding Plan");
expect(zai?.baseURL).toBe("https://api.z.ai/api/coding/paas/v4");
expect(zai?.defaultModel).toBe("glm-5.2");
expect(zai?.models).toContain("glm-5.2");
});

test("OpenCode Go is flagged with subscription billing", () => {
const go = firstClassProviderById("opencode-go");
expect(go?.opencodeGo).toBe(true);
expect(go?.billingProduct).toBe("subscription");
expect(go?.authHint?.toLowerCase()).toContain("subscription");
expect(go?.authHint).toContain("https://opencode.ai/auth");
});

test("OpenCode Zen uses credits billing and auth page hint", () => {
const zen = firstClassProviderById("zen");
expect(zen?.billingProduct).toBe("credits");
expect(zen?.authHint?.toLowerCase()).toMatch(/credit|pay-as-you-go/);
expect(zen?.authHint).toContain("https://opencode.ai/auth");
});

test("Anthropic is flagged for messages adapter", () => {
expect(firstClassProviderById("anthropic")?.anthropic).toBe(true);
});

test("firstClassPathAsProvider seeds OpenAI API path", () => {
const openai = firstClassProviderById("openai");
if (openai === undefined) throw new Error("openai missing");
const seeded = firstClassPathAsProvider(openai, "api");
expect(seeded?.id).toBe("openai");
expect(seeded?.auth).toBe("api-key");
expect(seeded?.baseURL).toBe("https://api.openai.com/v1");
expect(firstClassPathAsProvider(openai, "chatgpt")).toBeUndefined();
});
});
Loading
Loading