A multi-model AI agent desktop client · Electron 40 + Next.js 16 + SQLite + Claude Agent SDK + Native Runtime + Codex Runtime
commit
v0.62.0· about 1266 product TS/TSX (about 1448 including资料/vendor) · BUSL-1.1Every claim is tagged
file:line, traceable to concrete source.
I. Project overview
1.1 What CodePilot is
CodePilot is not another coding agent. It is a multi-model AI agent desktop client — a Next.js app wrapped in an Electron shell, moving “talk to AI” from the terminal onto the desktop, and expanding “conversation” into a “general agent platform”:
- 17+ AI providers (Anthropic / OpenAI / Google / xAI / GLM / Kimi / DeepSeek / Ollama / Bedrock / Vertex…)
- Three Runtimes (Claude Code SDK / Native first-party / Codex app-server)
- Remote IM Bridge (Telegram / Feishu / Discord / QQ / WeChat)
- MCP + Skills ecology
- Media generation (Gemini images)
- Task scheduling (cron)
- Session replay (rewind to any checkpoint)
Its relation to Claude Code: Claude Code is a coding agent in the terminal, reading your input line by line, calling tools to change files. CodePilot is a general agent client on the desktop — it reuses Claude Code’s SDK as one of three Runtimes, while also shipping a Native Runtime for non-Claude models, and attaching OpenAI Codex as the third Runtime.
Its relation to open-design: open-design is also a “host that does not write the agent loop,” but the host it builds is design (generating HTML/PPT). CodePilot builds a general agent desktop — chat, coding, tasks, media, remote control, all stuffed into one Electron window.
1.2 The project’s shape in numbers
| Dimension | Number | Notes |
|---|---|---|
| Source files | about 1266 product TS/TSX (about 1448 including 资料) | excluding node_modules; 资料/ is WeChat/Feishu vendor |
| Total files | 2522 | including JSON/MD/images etc. |
| Core engine | claude-client.ts 3628 lines | SDK wrap + message stream |
| Native loop | agent-loop.ts 1019 lines | while-loop driven |
| Database | db.ts 5752 lines | SQLite schema + migrations |
| API endpoints | about 170+ route handlers (about 29 first-level domains) | the one that actually runs the Agent is POST /api/chat; /api/chat/messages only persists |
| Bridge adapters | 5 | Telegram/Feishu/Discord/QQ/WeChat |
| Provider presets | 17+ | provider-catalog.ts 2400 lines |
| Skill directory | public/skills/ | compatible with Claude Code format |
| License | BUSL-1.1 | commercial use restricted |
1.3 The essential difference among the three Runtimes
This is CodePilot’s most core architectural decision — the same frontend, three different Agent engines:
| Runtime | Engine | Main-loop location | Scene |
|---|---|---|---|
| claude-code-sdk | @anthropic-ai/claude-agent-sdk |
inside the SDK child process | Claude models, full Claude Code capability |
| native | first-party while loop + Vercel AI SDK | agent-loop.ts:373 |
non-Claude models (GLM/Kimi/DeepSeek…) |
| codex | codex app-server child process |
inside Codex | OpenAI Codex models |
The three Runtimes share the same frontend, the same database, the same permission system, the same Bridge — but the main-loop implementations are completely different. That is CodePilot’s most distinctive architectural decision: Runtime is swappable.
1.4 The fundamental difference vs Claude Code
| Dimension | Claude Code | CodePilot |
|---|---|---|
| UI | terminal TUI | Electron desktop GUI |
| Models | locked to Anthropic | 17+ providers |
| Runtime | single (first-party) | three, swappable |
| Main loop | first-party while loop | SDK outsourced / Native first-party / Codex outsourced |
| Tools | hardcoded builtins | standard AI SDK ToolSet |
| Permissions | five-gate adjudication | three layers (rule engine + popup + Profile) |
| Sessions | terminal session | SQLite persist + rewind |
| Remote | none | IM Bridge |
| Sub-agents | Task tool | subagent_runs durable lifecycle |
II. Panorama
2.1 Process topology
┌─────────────────────────────────────────────────────────┐
│ Electron 主进程 (electron/main.ts, 2566 行) │
│ ├── 窗口管理 (BrowserWindow) │
│ ├── utilityProcess.fork → Next.js standalone server │
│ ├── TerminalManager (pty) │
│ ├── Tray / 通知 / 文件对话框 │
│ └── 日志轮转 (50MB×5) │
└────────────────────┬────────────────────────────────────┘
│ HTTP/SSE (localhost)
┌────────────────────▼────────────────────────────────────┐
│ Next.js 16 Server (App Router) │
│ ├── src/app/api/ — 约 170+ route handlers(约 29 一级域) │
│ │ ├── /api/chat — 聊天(消息/中断/权限) │
│ │ ├── /api/media — 图片生成 │
│ │ ├── /api/settings — 设置 │
│ │ ├── /api/providers — Provider 管理 │
│ │ ├── /api/tasks — 任务调度 │
│ │ └── … │
│ ├── src/lib/ — 核心业务逻辑 │
│ │ ├── claude-client.ts — SDK 封装(3628 行) │
│ │ ├── agent-loop.ts — Native 循环(1019 行) │
│ │ ├── stream-session-manager.ts — SSE 流管理 │
│ │ ├── conversation-registry.ts — 会话注册表 │
│ │ ├── db.ts — SQLite(5752 行) │
│ │ ├── runtime/ — Runtime 注册表 + 三实现 │
│ │ ├── tools/ — 8 个编码工具 │
│ │ ├── builtin-tools/ — 平台工具 │
│ │ ├── permission/ — 三层权限 │
│ │ ├── bridge/ — IM Bridge 子系统 │
│ │ ├── channels/ — 渠道插件 │
│ │ ├── codex/ — Codex 集成 │
│ │ └── harness/ — 能力契约 + 上下文编译 │
│ └── src/components/ — React 组件 │
│ ├── chat/ — 聊天界面 │
│ ├── ai-elements/ — AI 响应渲染 │
│ ├── layout/ — 布局 │
│ └── … │
└────────────────────┬────────────────────────────────────┘
│ SQLite (better-sqlite3, WAL)
┌────────────────────▼────────────────────────────────────┐
│ ~/.codepilot/codepilot.db │
│ ├── chat_sessions / messages │
│ ├── subagent_runs / subagent_run_events │
│ ├── api_providers / settings / tasks │
│ ├── media_generations / media_tags / media_jobs │
│ └── channel_bindings / channel_offsets │
└─────────────────────────────────────────────────────────┘
2.2 Four boundaries you must remember
① Electron is only the shell. electron/main.ts uses utilityProcess.fork to start the packed Next.js standalone server (serverProcess), listens on a free port, and after GET /api/bridge health-check passes, mainWindow.loadURL(http://127.0.0.1:${port}). Electron is unaware of any Agent logic — it only owns windows, terminals (pty), notifications, file dialogs.
② The three Runtimes share the same infrastructure. resolveRuntime() in runtime/registry.ts:72-152 (the file is 198 lines; L21 is only the Map declaration) selects Runtime in five levels: “Codex explicit → explicit override → cli_enabled=false → global settings → auto.” After selection, frontend, DB, permissions, Bridge are all reused.
③ The SDK path’s “main loop” is an event pump, not a true loop. for await (const message of conversation) at claude-client.ts:2239 only translates SDK-child-process events into SSE. The real “model→tools→feed back→model” loop happens inside the SDK child (claude CLI). The CodePilot application layer only does reactive forwarding + guardrails.
④ The Native path is the true loop. while (step < maxSteps) at agent-loop.ts:373 manually calls streamText() each step, inserting permission checks, DB persist, timeout budgets, context trim, SSE forwarding between steps — hooks the AI SDK’s automatic mode cannot attach.
2.3 Data flow: from user input to AI response
用户输入 → MessageInput / startStream
→ POST /api/chat ← 真正触发 Agent(chat/route.ts)
→ stream-session-manager 会话锁(chat 路径常传 600s TTL;锁默认常量另有 300s)
→ streamClaude()(claude-client.ts)解析 Provider + Runtime
→ addMessage 入库 + assembleContext(含 context-compressor / context-compiler)
→ runtime.stream() 分流:
├── SDK: streamClaudeSdk() → SDK query() 子进程
├── Native: runAgentLoop() → while (step < maxSteps) → streamText()
└── Codex: getCodexAppServer() → app-server → thread/start → turn/start
→ SSE 流式返回 → useSSEStream → MessageList
→ db.ts 持久化到 SQLite
说明:POST /api/chat/messages 只持久化消息、不跑模型(图片生成等模式用)。
Bridge data flow (remote IM control):
Telegram/飞书消息
→ Adapter 长轮询/WebSocket 接收
→ channelRouter 路由到 CodePilot session
→ conversationEngine 调用 SDK
→ SDK SSE 响应
→ deliveryLayer 格式化 + 分片
→ Adapter 发送回 IM
III. Startup
3.1 Electron main-process startup
Startup sequence of electron/main.ts (2566 lines):
- Sentry init (can opt-out)
utilityProcess.forkstarts the packed Next.js standalone server- listen on a free port (
serverProcess) GET /api/bridgehealth check- SIGTERM → 3s → SIGKILL shutdown
mainWindow.loadURL(http://127.0.0.1:${port})- TerminalManager (pty) init
- Tray / notifications / file dialogs registered
- Log rotation (50MB×5)
Key design: Electron main process and Next.js server are two independent processes, communicating over HTTP/SSE. In Dev mode it connects directly to the next dev dev server; in Prod it uses utilityProcess.fork to start the standalone server.
3.2 Next.js Server startup
When Next.js 16 App Router starts:
1. src/lib/db.ts initializes SQLite (WAL + foreign keys + busy_timeout=5000)
2. src/lib/runtime-log.ts initializes a console ring buffer (200 entries, auto-redact)
3. src/lib/bridge/bridge-manager.ts initializes the Bridge manager (globalThis singleton, HMR-resistant)
4. src/lib/conversation-registry.ts initializes the active-session registry
3.3 Database init
initDb() at db.ts:186:
-- 核心表
chat_sessions (id, title, model, provider_id, sdk_session_id, …)
messages (id, session_id, role, content, …)
subagent_runs (id, parent_session_id, status, …)
subagent_run_events (id, run_id, event_type, …)
settings (key, value)
tasks (id, session_id, title, status, …)
api_providers (id, name, protocol, base_url, …)
-- 媒体表
media_generations / media_tags / media_jobs / media_job_items / media_context_events
-- Bridge 表
channel_bindings / channel_offsets / permission_link / …
Migration mechanism: withMigrationLock (db.ts:84) uses O_CREAT|O_EXCL file lock + 10s retry, preventing concurrent migration from multiple Next.js workers. migrateDb (db.ts:482) uses PRAGMA table_info probe + safeAddColumn for idempotent ALTER TABLE incremental columns.
IV. Input capture and routing
4.1 Chat input
src/components/chat/MessageInput.tsx captures user input → via startStream goes POST /api/chat (the one that actually triggers the Agent). POST /api/chat/messages only persists, does not run the model.
4.2 API route handling
Full flow of src/app/api/chat/route.ts (990-line POST):
- Validate body (session_id, content, model, …)
- Pre-check:
hasCodePilotProvider(412 NEEDS_PROVIDER_SETUP) - Session lock:
acquireSessionLock(409 SESSION_BUSY, 600s TTL + renew/watchdog) - Resolve Provider + Runtime:
resolveRuntime()five-level select - Message persist:
addMessage - Context assembly:
assembleContext(including context-compressor) - Route to Runtime:
- SDK:
streamClaudeSdk() - Native:
runAgentLoop() - Codex:
codex app-server - SSE stream return
- Background persist:
collectStreamResponse(continues even if the renderer disconnects) - Release lock
4.3 Session lock
The session_runtime_locks table (db.ts:891) implements session-level mutual exclusion:
acquireSessionLock: acquire the lock, 600s TTLisLockOwner: verify lock ownership (lockId)- renew/watchdog: prevent lock expiry causing concurrency issues
Why a lock is needed: the same session cannot have two streams running at once — otherwise message order would scramble.
4.4 Interrupt
/api/chat/interrupt:
- explicit Stop: abort the parent turn and propagate to children
- stopStreamWith at stream-session-manager.ts:1046: first unconditionally arm a 2s force-abort safety net (#578 fix), then best-effort call /api/chat/interrupt
V. Context assembly
5.1 assembleContext
Context assembly is “pack user input + history + system prompt + tool descriptions” into the request sent to the model.
Key components:
- agent-system-prompt.ts: build the system prompt
- harness/context-compiler.ts: pure function compileContext(input) — given enabled capabilities + user/external extension scan results, produce an ordered, deduped system prompt + tool surface + artifact contract
- context-compressor: context compact
5.2 Harness capability contract
harness/ is one of CodePilot’s most distinctive subsystems — capability contract + context compilation layer:
capability-contract.ts: declares canonical prompt fragments and tool descriptors for all capabilities (memory/widget/dashboard/tasks_and_notify/cli_tools/media_import/image_generation…)context-compiler.ts: pure functioncompileContext(input)— no IO, no tool exec, no permission decisions, only the compiled contextmutation-level.ts: tool mutation-level classification (safe_read / safe_write / dangerous), the source of the permission allowlistbuiltin-event-bus.ts: tool-media-result side channelauto-invoke-accounting.ts: context accounting
Why Harness is needed: the three Runtimes need to show the model a consistent capability description. Harness guarantees Native/SDK/Codex system prompts do not drift.
5.3 Context compact
When context exceeds the model window, context-compressor triggers compact. On the SDK path, compact completes inside the SDK child; on the Native path, pruneOldToolResults trims between steps of agent-loop.ts.
VI. The agent loop
6.1 Comparing the three Runtimes’ main loops
This is CodePilot’s most core chapter. The three Runtimes’ main-loop implementations are completely different:
SDK path (claude-client.ts)
for await (const message of conversation) at claude-client.ts:2239:
// 这不是真循环,是事件泵
for await (const message of conversation) {
switch (message.type) {
case 'assistant': // 提取 tool_use → SSE
case 'user': // tool_result / 媒体块 / TodoWrite 同步
case 'stream_event': // 文本 delta
case 'system': // init 元数据
case 'result': // extractTokenUsage + terminal_reason
}
}
Key points: - the real Agent loop is inside the SDK child (claude CLI) - CodePilot only does event forwarding + guardrails - guardrails include: two-level idle budget (10min before first token, 5.5min after), PTL compact retry, #577 result authority
Native path (agent-loop.ts)
while (step < maxSteps) at agent-loop.ts:373 (default 50):
while (step < maxSteps) {
// 1. 清洗模型参数
sanitizeClaudeModelOptions()
buildAnthropicProviderOptions()
// 2. 上下文裁剪
pruneOldToolResults()
// 3. 超时预算上膛
timeoutCtl.onStepRequest()
// 4. 单次 streamText()
const result = streamText({ model, tools, messages, … })
// 5. 逐事件翻译为 SSE
for await (event of timeoutCtl.guardStream(result.fullStream)) {
// text-delta→text, reasoning-delta→thinking,
// tool-call→tool_use, tool-result→tool_result, …
}
// 6. 终止判断
if (!hasToolCalls) break
if (doomLoopDetected) break
// 7. 进入下一轮
messages += response.messages
step++
}
Key points: - the manual loop exists so permission checks, DB persist, timeout budgets, context trim can be inserted between steps - these cannot hook the AI SDK’s automatic mode - doom-loop detection: consecutive calls of the same tool
Codex path (codex/runtime.ts)
codex/runtime.ts:
- each conversation thread/resume|thread/start
- subscribe to notifications
- turn/start
- event-mapper.translateCodexNotification translates into canonical events then SSE
6.2 Essential differences of the main loop
| Dimension | Claude Code | CodePilot SDK | CodePilot Native | CodePilot Codex |
|---|---|---|---|---|
| Loop location | inside CLI | inside SDK child | agent-loop.ts while | inside Codex |
| Loop drive | first-party | SDK query() | manual while + streamText | Codex turn |
| Event forwarding | none (terminal direct) | for-await event pump | for-await translate | event-mapper |
| Guardrails | none | idle budget + PTL retry | timeout budget + doom detect | none |
| Context compact | internal | inside SDK | pruneOldToolResults | inside Codex |
6.3 Stream management
stream-session-manager.ts (1439 lines) manages SSE stream lifecycle:
startStream: create the stream,fetch('/api/chat'),consumeSSEStream- Two-level idle budget (#635): 10min before first token, 5.5min after,
setIntervaldetects timeout then abort - Complete:
buildFinalMessageContent→ phase=completed →scheduleGC(reclaim after 5min) - Error branches: idle timeout / tool timeout auto-retry / user stop / general error
- Stop:
stopStreamWithfirst arms a 2s force-abort safety net, then calls/api/chat/interrupt
6.4 Conversation registry
conversation-registry.ts (70 lines) manages active SDK sessions:
- globalThis
__activeConversationsV2__(V2 key so HMR does not get an old-shape Map) Map<sessionId, {query, lockId, abortController}>unregisterConversationgated by lockId — only delete if lockId matches, preventing a superseded old turn’s late teardown from evicting the Query registered by the new turn (I1 race)
VII. Tools and permissions
7.1 Two layers of tool sources
CodePilot’s tool system is two layers:
Coding-core tools (tools/)
8 tools: Read / Write / Edit / Bash / Glob / Grep / Skill / Agent
Each is a factory createXxxTool(ctx); tools/index.ts:45 createBuiltinTools() aggregates them into an AI SDK ToolSet:
// 标准 Vercel AI SDK ToolSet
{
name: {
description: string,
inputSchema: zodSchema,
execute: async (input) => result
}
}
Fed directly to streamText({ tools }).
Platform tools (builtin-tools/)
7 groups of platform tools: - notification: notify / schedule_task / list / cancel / hatch_buddy - memory-search: 3 memory-search tools - dashboard: 5 Dashboard tools - media: import + generate_image - widget-guidelines: Widget guidelines - session-search: session search - cli-tools: 6 CLI tools - ask-user-question: user Q&A
builtin-tools/index.ts:134 getBuiltinTools() mounts by three gating conditions always / workspace / keywords, generating corresponding system-prompt fragments via harness/runtime-adapter.ts adaptForNative().
7.2 Tool assembly
agent-tools.ts:114 assembleTools():
const tools = {
...createBuiltinTools(ctx), // 编码工具
...getBuiltinTools(ctx), // 平台工具
...buildMcpToolSet(mcpServers), // 外部 MCP 工具
}
// 权限包裹
if (permissionContext) {
tools = wrapWithPermissions(tools, permissionContext)
}
plan mode keeps only PERMISSION_SAFE_TOOLS (agent-tools.ts:53, derived from harness/mutation-level.ts mutationLevel==='safe_read', fail-safe: undeclared tools default ask).
7.3 The essential difference vs Claude Code’s tool system
| Dimension | Claude Code | CodePilot |
|---|---|---|
| Tool format | hardcoded in the bundle | standard AI SDK ToolSet |
| Tool registration | config-style permission rules | zod schema + execute function |
| Tool composition | fixed | freely composable, gating, runtime wrap |
| Platform tools | none | builtin-tools (notify/memory/Dashboard/media…) |
| MCP integration | yes | yes (mcp-tool-adapter.buildMcpToolSet) |
7.4 Three-layer permission system
Layer 1: rule engine
permission-checker.ts:127 checkPermission:
- three modes: explore (read-only) / normal (writes pass, Bash ask) / trust (all pass)
- rule-array
findLastsemantics (specific rules override general) - glob wildcard match on commands/paths
- hard floor:
DANGEROUS_PATTERNS(rm -rf/sudo/kill/dd…) andALWAYS_ASK_TOOLS(AskUserQuestion/ExitPlanMode) must ask in any mode
Layer 2: popup at execute time
agent-tools.ts:236-341:
- non-safe_read tools
checkPermissionbefore execute - on
ask, write DBpermission_requests, sendpermission_requestSSE (with HMACapprovalTokenagainst forgery) registerPendingPermissionhangs waiting for/api/chat/permissionreply (5-minute timeout auto-deny)- supports user-edited input (updatedInput) and “don’t ask this session” (sessionApprovals)
Layer 3: session Profile
permission/profile.ts:23:
- default: ask the user
- auto_review: hand to a restricted reviewer model for approval; human-only categories (credentials/billing/outbound/high-impact/interactive Q&A) never proxy-approve, fail-closed
- full_access: skip completely
review-event.ts + review-audit.ts unify proxy-approval audit events across the three Runtimes.
7.5 Sandbox
CodePilot itself has no OS sandbox: - the Codex path depends on the sandbox that comes with codex app-server - Native relies on the rule engine + human confirm - that is the essential difference vs Claude Code (Seatbelt/Landlock/seccomp)
VIII. Compaction and memory
8.1 Context compact
- SDK path: compact completes inside the SDK child
- Native path:
pruneOldToolResultsbetweenagent-loop.tssteps - PTL compact retry:
claude-client.ts:2859-3152,CONTEXT_TOO_LONGtriggers auto compact retry,ptlRetryAttemptedagainst infinite loops
8.2 SQLite persist
db.ts (5752 lines):
- WAL mode + foreign-key constraints + busy_timeout=5000
- message content is a JSON array
- sdk_session_id used for SDK session resume
8.3 Session replay (rewind)
CodePilot supports rewind to any checkpoint:
- file-checkpoint.createCheckpoint creates snapshots at key nodes
- the user can roll back to a previous checkpoint and continue the conversation
8.4 Memory system
- memory-search: 3 memory-search tools in builtin-tools
- Assistant Workspace: Persona files, persistent memory, onboarding flow, daily check-ins
IX. Sub-agents
9.1 Three spawn topologies
| Runtime | spawn method | Isolation level |
|---|---|---|
| CodePilot Runtime | independent runAgentLoop in the same Next/Electron server process |
not an OS child |
| Claude Runtime | Agent SDK query() starts an independent OS child |
isolated shadow HOME/credentials |
| Codex Runtime | reuse the app-server connection, new isolated thread/start |
thread-level isolation |
9.2 Durable Lifecycle
The subagent_runs table implements a persistent lifecycle:
running → settling → completed/failed/partial/cancelled/timed_out
- write
subagent_runs.runningbefore starting the child (if write fails, fail-closed, do not start) - after end
settling → terminal; terminal is atomically closed only once - user tasks aggregate by
logical_run_id; each execution/retry is an incrementing attempt
9.3 Dependency orchestration
Declarative DAG via workflow_id + task_key + depends_on:
- downstream first
queued, occupies no concurrency slot, does not call a Provider - resolver polls the durable table waiting for upstream terminal (30-minute deadline, 5-second missing grace)
- upstream
result_textis compiled into the downstream prompt tagged<codepilot_dependency_results>as “untrusted data” - reject self-dependency / cycles / placeholder “wait” prompts
9.4 Constraints
- depth fixed at 1: Agent/spawn tools are hard-removed inside the child
- concurrency 2 per parent session
- Claude child 5-minute idle + 30-minute hard cap
- parent abort propagates down
9.5 Multi-model Sub-agent
subagent-models.ts generates an exact providerId + modelId route from all Provider resolvers + the runtime-compat matrix. The child’s assembleTools and runAgentLoop both switch to the target Provider.
X. Ecosystem
10.1 MCP integration
mcp-tool-adapter.buildMcpToolSet: aggregate tools from external MCP servers into the ToolSet- supports stdio / sse / http three transports
- runtime monitoring
10.2 Skill system
- format compatible with Claude Code: YAML frontmatter + Markdown body
- scan paths:
.claude/skills,.claude/commands,~/.claude/skills,~/.claude/commands,~/.agents/skills - marketplace: install/uninstall API
- fork mode: lands in the subagent_runs durable system
10.3 Bridge subsystem
See Chapter 12, “Bridge and remote control.”
10.4 Codex integration
- direct integration:
codex/app-server-manager.ts:603 getCodexAppServer()finds thecodexbinary and spawnscodex app-server(stdio JSON-RPC, cached singleton) - codex/proxy: local Responses API proxy (
/api/codex/proxy/v1/responses), so the Codex CLI engine can run on any user-configured provider
XI. Features
11.1 Provider management
provider-catalog.ts (2400 lines):
- VENDOR_PRESETS array defines 17+ vendor presets
- VendorPreset includes protocol, authStyle, baseUrl, defaultEnvOverrides, defaultModels, roleModels, billing model, sdkProxyOnly, claudeCodeVerified compatibility grading
- Zod-validates the whole table at startup
provider-resolver.ts (1793 lines):
- resolveProvider() priority providerId > sessionProviderId > default_provider_id > active
- virtual providers (env/openai-oauth/xai-oauth/codex_account)
- resolveExactProvider() fail-closed for background tasks
11.2 Task scheduling
taskstable stores tasksensureSchedulerRunningstarts on the first chat request- cron expressions or interval scheduling
11.3 Media generation
image-generator.ts: Gemini/Anthropic image generationjob-executor.ts: batch image-generation job executor- Gallery: image gallery, tags, batch jobs
11.4 Error classification and diagnosis
error-classifier.ts (589 lines):
- 28 classes of structured error
- ERROR_PATTERNS per class defines patterns + errno codes + userMessage + actionHint + retryable
- PROCESS_CRASH containing session keywords is reclassified as SESSION_STATE_ERROR
provider-doctor.ts (1093 lines):
- 5 fast probes in parallel (CLI install/version, credentials/auth, default provider/model, feature compatibility, DNS/connectivity)
- 6th live probe: really spawn the CLI and send a minimal request
- repair actions: REPAIR_ACTIONS + computeRepairs
XII. Bridge and remote control
12.1 Bridge architecture
bridge-manager.ts (1371 lines, globalThis singleton) orchestrates four modules:
| Module | Duty |
|---|---|
channel-adapter.ts |
BaseChannelAdapter abstract base + factory registry |
channel-router.ts |
message routing (IM → session) |
conversation-engine.ts |
consume SSE streams, save messages |
delivery-layer.ts |
outbound reliable delivery |
12.2 Channel adapters
5 adapters: - telegram: long-poll + draft streaming preview - discord: Discord.js - qq: QQ Bot - weixin: WeChat (auth/media/session-guard) - feishu: Feishu (WS long connection + card streaming)
12.3 Permission forwarding
permission-broker.ts:
full_accessprofile and not a human-only tool → directlyresolvePendingPermission(allow)- 30-second dedup window against duplicate cards
- split by channel capability: button-less channels (QQ/WeChat) refuse AskUserQuestion
- emit a card with
inlineButtons, writepermission_linktable - user clicks a button →
handlePermissionCallback()→resolvePendingPermission()unblocks the hanging stream
12.4 Markdown → IR → channel format
A three-stage pipeline:
1. ir.ts (829 lines): markdown-it token stream → MarkdownIR = {text, styles[], links[]}
2. render.ts: generic algorithm — split text at span boundaries, sort nested styles by STYLE_ORDER
3. telegram.ts/discord.ts/feishu.ts: only provide styleMarkers + escapeText + buildLink config
XIII. Design philosophy and trade-offs
13.1 Why three Runtimes
CodePilot’s most core architectural decision is Runtime is swappable. Why?
- Claude Code SDK: best experience for Claude models, but locked to Anthropic
- Native: non-Claude models need their own loop, but cannot copy all of Claude Code’s capability
- Codex: OpenAI models need their own engine
The three Runtimes share frontend, DB, permissions, Bridge — but the main loops are completely different. A typical case of “product positioning deciding technical form.”
13.2 Why Electron + Next.js
- Electron: cross-platform desktop (macOS/Windows/Linux), system capabilities (pty/notifications/file dialogs)
- Next.js: App Router as both frontend and API layer, SSR/SSG, React 19 ecology
- utilityProcess.fork: Next.js server runs in an independent process; the main process is only the shell
13.3 Why SQLite
- better-sqlite3: sync API, WAL mode, zero config
- local-first: all data under
~/.codepilot/, no cloud dependency - session replay: SQLite persist supports rewind
13.4 Trade-off comparison vs Claude Code
| Dimension | Claude Code | CodePilot | Trade-off reason |
|---|---|---|---|
| UI | terminal TUI | Electron GUI | desktop users vs terminal users |
| Models | locked to Anthropic | 17+ providers | openness vs depth of optimization |
| Runtime | single | three, swappable | flexibility vs complexity |
| Tools | hardcoded | standard ToolSet | composability vs consistency |
| Permissions | five-gate adjudication | three layers | GUI popup vs terminal interaction |
| Sessions | terminal session | SQLite + rewind | persist vs lightness |
| Remote | none | IM Bridge | mobile scenes vs desktop scenes |
| Sub-agents | Task tool | durable lifecycle | reliability vs simplicity |
13.5 Three most distinctive designs
-
Runtime is swappable: the same frontend, three different Agent engines. The essential difference between CodePilot and every other agent client.
-
Harness capability contract:
harness/context-compiler.tsis a pure-function compilation layer, guaranteeing the three Runtimes’ capability descriptions to the model do not drift. A delicate balance between “unified infrastructure” and “modules evolving independently.” -
Bridge permission forwarding:
permission-broker.tsturns permission requests into IM inline buttons, breaking the deadlock of “Claude stream blocked waiting for permission.” The key innovation of “how a desktop agent is remotely controlled.”
Side-by-side: CodePilot’s place beyond the thirteen
CodePilot is the sixteenth project analyzed in this repo. Its relation to the previous fifteen is not “another coding agent,” but a form jump of “moving the agent from the terminal onto the desktop.”
Vs Claude Code (the most direct counterpart)
| Dimension | Claude Code | CodePilot | Essence of the difference |
|---|---|---|---|
| Product positioning | terminal coding agent | desktop general agent client | terminal vs desktop |
| UI | Ink TUI | Electron + React | terminal vs GUI |
| Models | locked to Anthropic | 17+ providers | closed vs open |
| Runtime | single first-party | three, swappable | unified vs flexible |
| Main loop | first-party while loop | SDK outsourced / Native first-party / Codex outsourced | autonomous vs mixed |
| Tools | 43 hardcoded | standard ToolSet 8+7 groups | private vs standard |
| Permissions | five-gate adjudication | three layers (rules+popup+Profile) | terminal vs GUI |
| Sessions | terminal session | SQLite + rewind | temporary vs durable |
| Remote | none | IM Bridge 5 channels | none vs yes |
| Sub-agents | Task tool | durable lifecycle | simple vs reliable |
| Sandbox | Seatbelt/Landlock/seccomp | none (Codex brings its own) | yes vs no |
Vs open-design (both “host” forms)
| Dimension | open-design | CodePilot | Essence of the difference |
|---|---|---|---|
| Host object | 25 CLI agents | 3 Runtimes | count vs depth |
| Core innovation | adapter-as-data | Runtime swappable | breadth vs depth |
| Main loop | does not write (outsourced to CLI) | three (outsourced+first-party+outsourced) | pure host vs mixed |
| Content | design system/brand/templates | chat/tasks/media/remote | design vs general |
| Output | HTML/PPT/PDF | dialogue/code/images | documents vs interaction |
| Permissions | none (CLI brings its own) | three layers | none vs yes |
Vs openworker (both “general-task colleagues”)
| Dimension | openworker | CodePilot | Essence of the difference |
|---|---|---|---|
| Positioning | unattended colleague | desktop agent client | background vs foreground |
| Interaction | Slack/email | desktop GUI + IM Bridge | async vs sync |
| Models | provider-agnostic | 17+ providers | similar |
| Tools | 40 connectors | 8+7 groups + MCP | connectors vs standard |
| Permissions | five-gear modes | three layers | similar |
| Persist | SQLite | SQLite | same |
Vs Codex (the source of the third Runtime)
| Dimension | OpenAI Codex CLI | CodePilot Codex Runtime | Essence of the difference |
|---|---|---|---|
| Form | terminal CLI | embedded in desktop GUI | independent vs integrated |
| Models | locked to OpenAI | locked to OpenAI (but via proxy can run any provider) | closed vs half-open |
| Main loop | run_turn two-layer loop | inside Codex | same |
| Sandbox | three-platform OS sandbox | Codex brings its own | same |
| Permissions | AskForApproval × sandbox | reuse CodePilot three layers | independent vs reuse |
CodePilot’s distinctive contributions
CodePilot brings three designs none of the previous fifteen have:
-
Runtime is swappable: the same frontend, three different Agent engines. This is not “multi-provider” (that is openness at the model layer), but “multi-engine” (openness at the loop layer).
-
Harness capability contract:
harness/context-compiler.tsis a pure-function compilation layer, guaranteeing the three Runtimes’ capability descriptions to the model do not drift. A delicate balance between “unified infrastructure” and “modules evolving independently.” -
Bridge permission forwarding:
permission-broker.tsturns permission requests into IM inline buttons, breaking the deadlock of “Claude stream blocked waiting for permission.” The key innovation of “how a desktop agent is remotely controlled.”
Appendix: key-file index
| File | Lines | Notes |
|---|---|---|
src/lib/claude-client.ts |
3628 | SDK wrap + message stream |
src/lib/agent-loop.ts |
1019 | Native loop |
src/lib/db.ts |
5752 | SQLite schema + migrations |
src/lib/stream-session-manager.ts |
1439 | SSE stream management |
src/lib/conversation-registry.ts |
70 | session registry |
src/lib/error-classifier.ts |
589 | error classification |
src/lib/provider-doctor.ts |
1093 | Provider diagnosis |
src/lib/runtime/registry.ts |
198 (resolveRuntime @72-152) |
Runtime registry |
src/lib/tools/index.ts |
67 (createBuiltinTools @45) |
tool aggregation |
src/lib/builtin-tools/index.ts |
351 (getBuiltinTools @134) |
platform tools |
src/lib/permission-checker.ts |
(checkPermission @127; file total lines per the repo) |
permission rule engine |
src/lib/bridge/bridge-manager.ts |
1371 | Bridge manager |
src/lib/harness/context-compiler.ts |
— | context compilation |
src/lib/codex/app-server-manager.ts |
769 (getCodexAppServer @603) |
Codex integration |
src/lib/provider-catalog.ts |
2400 | Provider presets |
src/lib/provider-resolver.ts |
1793 | Provider resolve |
electron/main.ts |
2566 | Electron main process |
src/app/api/chat/route.ts |
990 | chat API |
This lecture was written from a file-by-file analysis of CodePilot v0.62.0 source. Every file:line citation was re-checked with grep -n / sed -n.