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
6 changes: 3 additions & 3 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,10 +286,10 @@ Ink 7 + React 19, full-screen via the alternate-screen buffer.
- **In-flight indicator** — Spinner uses the `"live"` semantic color (calm blue) rather than the brand orange, reducing visual noise during long runs.

- 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`, `login-provider-picker`, `codex-login-modal`, `mcp-auth-prompt`, `onboarding-animation`, `in-flight-indicator`.
- 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`, `/login`, `/codex`, `/xai`, `/grok`, `/clear`, `/new`, `/mcp`).
- `/agent` 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. "Set as default" persists the selection (selection-only, no credentials) to the per-repo `.corbits/settings.json` via `saveLocalSettings`. The section model leaves room for system-prompt/profile sections without new slash commands, and for the add-provider/onboarding step.
- 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`.

#### Event log rendering

Expand Down
8 changes: 6 additions & 2 deletions docs/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,13 @@ src/
commands/
registry.ts Extensible slash-command registry
built-in.ts /help, /model, /settings, /permissions, /plugins,
/login, /codex, /xai, /grok, /clear, /new, /mcp
/clear, /new, /mcp (connect providers from /model)
components/
header.tsx, event-log.tsx, chat-input.tsx, status-bar.tsx, task-view.tsx,
at-mention/, operator-modal.tsx, permission-modal.tsx,
permissions-manager.tsx, plugins-manager.tsx, settings-overlay.tsx,
agent-modal.tsx, exit-confirm.tsx, help-overlay.tsx, hook-panel.tsx,
login-provider-picker.tsx, codex-login-modal.tsx, mcp-auth-prompt.tsx,
codex-login-modal.tsx, mcp-auth-prompt.tsx,
onboarding-animation.tsx, in-flight-indicator.tsx, modal-stack.tsx
hooks/
use-gates.ts, use-keymap.ts, use-layout-geometry.ts, use-mcp-status.ts,
Expand Down Expand Up @@ -271,6 +271,10 @@ 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.

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

### CLI Verbs and Flags

| Verb / Flag | Default | Description |
Expand Down
11 changes: 5 additions & 6 deletions docs/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Existing coding agents stall. They get stuck in thinking loops, read files endle
6. **Legible loop** — A live event log, working-tree diff panel, plan tracker, and real-time cost meter show what happened, when, and why.
7. **Operator-in-the-loop** — The agent can call `ask_operator` to pause and ask a clarifying question; the operator answers from a modal (TUI) or via stdin when the product agent runs under `corbits exec`.
8. **Mid-run steering** — Two modes while the agent is running: **Enter** interrupts the current run immediately and starts a new turn with your message; **Alt+Enter** queues the message for delivery at the next turn boundary without stopping the current run. A badge on the input shows the count of queued messages. A hint line in the input area makes both options discoverable.
9. **Session mode (TUI)** — **Single-agent** keeps one primary loop on the wire (no `task` / `search_agents` tools). **Orchestrator** is for chatting with the top agent while it delegates via `task` and manages parallel sub-agents. On first launch, Corbits Code asks once; **Enter** saves to global settings (highlight defaults to single-agent; **Ctrl+C** skips save, runs orchestrator this session only, and the prompt returns on later launches until you save). **Settings → Session** can change global or per-repo defaults, but mode takes effect on the **next** session start (unlike `/agent` provider switches). The `exec` path uses the same `sessionMode` resolution as the TUI (global + per-repo settings; defaults to orchestrator when unset).
9. **Session mode (TUI)** — **Single-agent** keeps one primary loop on the wire (no `task` / `search_agents` tools). **Orchestrator** is for chatting with the top agent while it delegates via `task` and manages parallel sub-agents. On first launch, Corbits Code asks once; **Enter** saves to global settings (highlight defaults to single-agent; **Ctrl+C** skips save, runs orchestrator this session only, and the prompt returns on later launches until you save). **Settings → Session** can change global or per-repo defaults, but mode takes effect on the **next** session start (unlike `/model` provider switches). The `exec` path uses the same `sessionMode` resolution as the TUI (global + per-repo settings; defaults to orchestrator when unset).

## User Experience

Expand Down Expand Up @@ -72,14 +72,13 @@ Continues from the last saved state in the working directory.

## Slash Commands (TUI)

The TUI has an extensible slash-command framework. Built-ins include `/help` (shortcut + command overlay), `/model` (open the agent configuration surface), `/settings`, `/permissions`, `/plugins`, `/login`, `/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.

`/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.
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.

`/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.


`/agent` opens a dedicated full-screen modal — the single place agent configuration lives. Today it holds a Provider / Model section: it lists configured providers, marks the active one, and lets you switch provider and model. 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.
`/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.

## Lifecycle Hooks

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
},
"workspaces": [
".",
"./vendor/intx-inference"
"./vendor/intx-inference",
"./packages/*"
],
"overrides": {
"@intx/inference": "workspace:*"
Expand Down
13 changes: 13 additions & 0 deletions packages/first-class-providers/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "@corbits/first-class-providers",
"version": "0.1.0",
"private": true,
"type": "module",
"license": "SEE LICENSE IN LICENSE.md",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
}
}
33 changes: 33 additions & 0 deletions packages/first-class-providers/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export {
FIRST_CLASS_PROVIDERS,
firstClassProviderById,
} from "./providers.js";
export type {
FirstClassAuthKind,
FirstClassOAuthProvider,
FirstClassProviderDef,
} from "./types.js";

// Re-export Go package surface so hosts can depend on one entry for connect.
export {
OPENCODE_GO_ANTHROPIC_BASE_URL,
OPENCODE_GO_AUTH_HINT,
OPENCODE_GO_BASE_URL,
OPENCODE_GO_DEFAULT_MODEL,
OPENCODE_GO_DISPLAY_NAME,
OPENCODE_GO_MODEL_IDS,
OPENCODE_GO_MODELS,
OPENCODE_GO_PROVIDER_ID,
buildGoCatalogEntry,
fetchGoUsage,
formatGoUsage,
isKnownGoModel,
protocolForGoModel,
resolveGoEndpoint,
validateGoApiKey,
type GoCatalogEntry,
type GoEndpoint,
type GoModel,
type GoProtocol,
type GoUsage,
} from "../../opencode-go/src/index.js";
38 changes: 38 additions & 0 deletions packages/first-class-providers/src/providers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, expect, test } from "bun:test";
import { FIRST_CLASS_PROVIDERS, firstClassProviderById } from "./providers.js";

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

test("Codex and xAI are OAuth; others are API key", () => {
expect(firstClassProviderById("codex")?.auth).toBe("oauth");
expect(firstClassProviderById("xai")?.auth).toBe("oauth");
for (const id of ["zen", "anthropic", "openai", "google", "opencode-go"] as const) {
expect(firstClassProviderById(id)?.auth).toBe("api-key");
}
});

test("API-key providers ship baseURL, models, and defaultModel", () => {
for (const def of FIRST_CLASS_PROVIDERS) {
if (def.auth !== "api-key") continue;
expect(def.baseURL?.length ?? 0).toBeGreaterThan(0);
expect((def.models ?? []).length).toBeGreaterThan(0);
expect(def.defaultModel?.length ?? 0).toBeGreaterThan(0);
expect(def.models).toContain(def.defaultModel);
}
});

test("Anthropic is flagged for messages adapter", () => {
expect(firstClassProviderById("anthropic")?.anthropic).toBe(true);
});
});
90 changes: 90 additions & 0 deletions packages/first-class-providers/src/providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
OPENCODE_GO_AUTH_HINT,
OPENCODE_GO_BASE_URL,
OPENCODE_GO_DEFAULT_MODEL,
OPENCODE_GO_DISPLAY_NAME,
OPENCODE_GO_MODEL_IDS,
OPENCODE_GO_PROVIDER_ID,
} from "../../opencode-go/src/index.js";
import type { FirstClassProviderDef } from "./types.js";

/**
* First-class providers shown in the models-surface Connect list.
* Order matches product preference: subscription OAuth first, then API keys.
*/
export const FIRST_CLASS_PROVIDERS: readonly FirstClassProviderDef[] = [
{
id: "codex",
label: "OpenAI Codex",
auth: "oauth",
oauth: "codex",
},
{
id: "xai",
label: "xAI Grok",
auth: "oauth",
oauth: "xai",
},
{
id: "zen",
label: "OpenCode Zen",
auth: "api-key",
baseURL: "https://opencode.ai/zen/v1",
models: [
"gpt-5.4",
"gpt-5.4-mini",
"claude-sonnet-4-5",
"claude-opus-4-5",
"gemini-3-flash",
"gemini-3-pro",
],
defaultModel: "claude-sonnet-4-5",
authHint: "Paste your OpenCode Zen API key from https://opencode.ai/auth",
},
{
id: "anthropic",
label: "Anthropic",
auth: "api-key",
baseURL: "https://api.anthropic.com",
models: [
"claude-opus-4-5",
"claude-sonnet-4-5",
"claude-haiku-4-5",
],
defaultModel: "claude-sonnet-4-5",
authHint: "Paste your Anthropic API key (sk-ant-...)",
anthropic: true,
},
{
id: "openai",
label: "OpenAI",
auth: "api-key",
baseURL: "https://api.openai.com/v1",
models: ["gpt-5.4", "gpt-5.4-mini", "gpt-4.1", "o3", "o4-mini"],
defaultModel: "gpt-5.4",
authHint: "Paste your OpenAI API key (sk-...)",
},
{
id: "google",
label: "Google",
auth: "api-key",
// OpenAI-compatible Gemini endpoint.
baseURL: "https://generativelanguage.googleapis.com/v1beta/openai",
models: ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash"],
defaultModel: "gemini-2.5-pro",
authHint: "Paste your Google AI Studio API key",
},
{
id: OPENCODE_GO_PROVIDER_ID,
label: OPENCODE_GO_DISPLAY_NAME,
auth: "api-key",
baseURL: OPENCODE_GO_BASE_URL,
models: OPENCODE_GO_MODEL_IDS,
defaultModel: OPENCODE_GO_DEFAULT_MODEL,
authHint: OPENCODE_GO_AUTH_HINT,
},
] as const;

export function firstClassProviderById(id: string): FirstClassProviderDef | undefined {
return FIRST_CLASS_PROVIDERS.find((p) => p.id === id);
}
23 changes: 23 additions & 0 deletions packages/first-class-providers/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export type FirstClassAuthKind = "oauth" | "api-key";

export type FirstClassOAuthProvider = "codex" | "xai";

export type FirstClassProviderDef = {
id: string;
label: string;
auth: FirstClassAuthKind;
/** OAuth flow key when auth === "oauth". */
oauth?: FirstClassOAuthProvider;
/** Default inference base URL for api-key providers. */
baseURL?: string;
/** Pre-seeded models for api-key providers (empty until connected for oauth). */
models?: readonly string[];
defaultModel?: string;
/** Short paste hint for api-key providers. */
authHint?: string;
/**
* When true, host should use Anthropic Messages adapter (baseURL without /v1
* if the adapter appends /v1/messages).
*/
anthropic?: boolean;
};
Loading
Loading