Caret architecture
Caret is a Cursor-class AI code editor with a Rust core (Tauri 2) and a React + Monaco frontend rendered by the OS WebView. No bundled Chromium, no Node runtime at run time.
┌─────────────────────────── WebView (React 19 + Monaco + xterm.js) ───────────────────────────┐
│ Layout: TitleBar · ActivityBar · Sidebar(Explorer/Search/SCM/Agents) · EditorArea · Panel │
│ Chat/Agent UI · Command palette · Settings UI · Status bar · Lean LSP client → Monaco │
└──────────────▲────────────── invoke() / Channel<T> / listen() ───────────────▲───────────────┘
│ │
┌──────────────┴────────────────────── Rust core (src-tauri) ───────────────────┴───────────────┐
│ core/ fs · search(grep crates) · git(git2 + git cli) · pty(portable-pty) · lsp relay · │
│ watcher(notify) · settings · sessions · workspace · sysmon │
│ ai/ providers(anthropic raw HTTP SSE, openai-compatible) · llm abstraction · │
│ agent/{runner,tools,context,rules,memories,hooks,skills,commands,subagents,checkpoints}│
│ tab(FIM + next-edit) · inline_edit(Cmd+K) · commit · review(Bugbot) · apply │
│ index/ walker · chunker · tantivy BM25 · embeddings · hybrid search · docs crawler │
│ mcp/ MCP client manager (stdio / streamable-http / sse) │
│ orchestrator/ Caret as an MCP server (loopback Streamable HTTP, bearer token): threads as tools │
│ browser/ agent-driven browser windows (navigate, DOM snapshot, click, type, eval) │
│ remote/ `caret serve`: hyper + WebSocket relay serving the UI to LAN/Tailscale browsers │
└───────────────────────────────────────────────────────────────────────────────────────────────┘Conventions (binding for every module)
- Commands are
snake_casedomain_actionTauri commands. JS calls them throughsrc/lib/api.tswith camelCase argument names (Tauri mapsfilePath→file_path). Every command returnsResult<T, CaretError>;CaretErrorserializes as{ code: string, message: string }. - Streams (LLM deltas, PTY output, search hits, index progress) use
tauri::ipc::Channel<T>passed from JS (new Channel<T>()), never high-frequency events. - Events (
listen) are for low-frequency broadcasts:caret://fs/changed,caret://git/changed,caret://lsp/message/{serverId},caret://settings/changed,caret://agent/event,caret://index/status,caret://notify. - Shared types live in
src/types.ts(TS) and mirror serde structs 1:1 (#[serde(rename_all = "camelCase")]). Rust is the source of truth; TS must match it. - State: each Rust domain owns a state struct managed with
app.manage(...); commands takeState<'_, T>. Registration lines live inlib.rsbetween// REGISTER:<domain>markers. - Memory rules: stream large files, never buffer whole directory trees eagerly (lazy tree),
bounded PTY scrollback (ring buffer),
lrucaches with hard caps, tantivy mmap index, dispose Monaco models on close, virtualize long lists, no polling loops (use notify/watchers). - Compatibility: Caret reads its own
.caret/project dir and Cursor's.cursor/(rules/*.mdc,mcp.json,hooks.json,commands/,skills/,agents/), plus.cursorrules,.cursorignore,.cursorindexingignore,AGENTS.md,CLAUDE.md. - Global config dir:
~/.caret/(settings.json,secrets.json(0600),keybindings.json,mcp.json,rules/,memories/,sessions/,checkpoints/,index/,docs/,logs/).
Feature map (Cursor feature → Caret module)
| Cursor feature | Caret |
|---|---|
| Tab (autocomplete, multi-line, partial accept, next-edit jump) | ai/tab.rs + lib/monaco/inlineCompletions.ts, nextEdit.ts |
| Cmd+K inline edit / quick question / terminal Cmd+K | ai/inline_edit.rs + lib/monaco/inlineEdit.ts, terminal prompt bar |
| Agent / Ask / Plan / Debug / custom modes | ai/agent/runner.rs modes + components/chat |
| Agent tools (read/edit/search/terminal/web/todo/memory/MCP/browser/subagent) | ai/agent/tools/* |
| @-mentions (Files, Folders, Code, Codebase, Git, Web, Docs, Terminal, Lint, Recent changes, Past chats, Rules, Link) | ai/agent/context.rs + components/chat/MentionPicker |
| Checkpoints / Review changes / accept-reject per file | ai/agent/checkpoints.rs + components/chat/ReviewBar |
| Rules (Always / Auto Attached / Agent Requested / Manual), AGENTS.md | ai/agent/rules.rs |
| Memories | ai/agent/memories.rs |
| Hooks (beforeSubmitPrompt, beforeShellExecution, beforeMCPExecution, beforeReadFile, afterFileEdit, afterAgentResponse, stop) | ai/agent/hooks.rs |
Commands (/slash), Skills (SKILL.md), Subagents |
ai/agent/{commands,skills,subagents}.rs |
| Codebase indexing (ignore files, incremental, embeddings) + @Docs | index/* |
| MCP (stdio, streamable-http, sse; tools/resources/prompts) | mcp/* |
| Orchestrator: Caret as an MCP server so Claude Code, Codex, Cursor or another Caret can spawn, steer, read and approve threads on any provider or ACP agent (T3 Code parity) | orchestrator/* (server, tools, lifecycle) over AgentManager::subscribe_events(), settings/pages/OrchestratorSection.tsx (docs/ORCHESTRATOR.md) |
| External agents via the Agent Client Protocol (Claude Code, Codex, Gemini CLI, Pi, GLM, custom) driving the same chat/review/approval UI (Caret-only; Cursor has no ACP client) | acp/* (protocol, process, client, bridge, registry, commands) → chat_commands.rs routing, components/chat/composer/AgentPicker.tsx, settings/pages/Agents.tsx — docs/ACP.md |
| Command safety: shell AST + deterministic risk classifier, Seatbelt/bwrap sandbox, MCP tool pinning (Caret-only; Cursor has allow/deny lists) | sandbox/* → ai/agent/policy.rs, tools/shell_tools.rs, components/chat/messages/ToolCallCard.tsx, settings/pages/SandboxSection.tsx — docs/SANDBOX.md |
| Background / parallel agents, worktrees, Agents layout | AgentManager in runner + components/agents |
| Browser automation | browser/* |
Remote access from a browser (caret serve, Tailscale), relayed through the desktop webview (Caret-only) |
remote/* + remote/shim.js → lib/remoteExecutor.ts, settings/pages/Remote.tsx; docs/REMOTE.md |
| Bugbot-style review, commit message generation, blame | ai/review.rs, ai/commit.rs, core/git.rs |
| Auto-run with allow/deny lists, file-delete protection, sandboxed shell | ai/agent/policy.rs |
| Privacy mode, usage/cost tracking, model picker, thinking/effort | settings + runner usage accounting |
| Usage page: local token/cost ledger, breakdowns, external agent histories (Claude Code, Codex, OpenCode) and subscription windows (Caret-only) | ai/usage.rs (ledger, pricing, aggregation, parsers) → settings/pages/Usage.tsx, status bar spend item; see docs/USAGE.md |
| Integrated terminal, git, search, LSP intelligence, themes, keybindings, settings UI | core/* + shell UI |
UI motion rules
Motion is a shared system, not per-component improvisation (src/styles.css, src/lib/motion.ts):
- Three durations (
--dur-fast120 ms,--dur-base180 ms,--dur-slow240 ms) and one ease-out curve (--ease). Settings → Appearance → Motion sets<html data-motion="off|subtle|expressive">, which scales or zeroes them;Reduce motionand the OS preference forceoff. Anything driven by the tokens therefore degrades to instant. - Animate
transform,opacityand colours only. Pane toggles may transition width/height, never while dragging. Nothing runs longer than--dur-slow; nothing blocks input. - Entry:
animate-fade-up/animate-pop/animate-slide-in-*once on mount (key by id — never re-animate on store updates). Exit: keep the node mounted withusePresence(open)and toggle.is-leaving. - Depth comes from tone, not lines: sidebar/panels low, editor high, floating surfaces (
surface-float) lifted with a soft shadow. Hairline borders only where equal tones meet. Radii: 8 px cards/inputs/popovers, 10 px dialogs, pills round.
Editor completion rules
- Document sync before requests.
LspManagerdebouncesdidChange(150 ms) for diagnostics traffic, but every provider resolves its client throughflushFor(model)first, so completion/hover/signature requests always run against the current text. Quick suggestions fire ~10 ms after a keystroke; never add a request path that skips the flush. - Ordered writes.
LspClient.sendchains Tauri invokes; two invokes are not ordered otherwise, and adidChangeovertaken by the request that follows it yields completions computed at the wrong position. - Tab beside the suggest widget. Monaco only shows an inline completion while the widget is open if it augments
the highlighted item (
insertTextstarts with the item's text over the same range).inlineCompletions.tsbuilds the request as if the item were accepted and returnsitem.text + completionon the item's range. Monaco's ownsuggest.previewstays off so the ghost-text slot belongs to Tab;showOnSuggestConflict: 'always'keeps an already visible ghost text when the widget pops. - In-flight sharing. Requests are keyed by context; a second identical call adopts the running one, token cancellation releases it with a 60 ms grace, and only non-empty answers are cached. The Rust side mirrors this: a waiter on a cancelled or failed request runs its own instead of inheriting an empty result.
- Automation.
window.__caretexposesmonaco,lsp(the manager) andtab(last round-trips) forscripts/e2e.mjs eval; type witheditor.trigger('keyboard', 'type', …)or quick suggestions will not fire.
File index tiers
core/fs.rs keeps one in-memory file index per workspace. Tier 0 is the ignore-aware walk (gitignore, .caretignore,
.cursorignore, settings excludes). Tier 1 is a second walk with ignore files off that still skips IGNORED_TIER_SKIP
(dependency trees, build output, tool caches) and the settings excludes, capped at IGNORED_CAP; its entries carry
ignored: true, lose IGNORED_PENALTY in fuzzy ranking and are omitted from fs_list_files. The watcher classifies
late additions with the same IgnoreSet so behaviour does not depend on whether a file was touched after start-up.
Semantic indexing (index/) and text search keep their own ignore handling; text search exposes a "Use .gitignore"
toggle instead.
Terminal extension points
components/terminal/pathLinks.tsis the pure path detector;TerminalInstanceregisters it as an xterm link provider and validates candidates withfs_exists(cwd first, then workspace root) before underlining them.components/terminal/suggestions.tsowns ghost text: it needs the OSC 133 B/C/D hooks from the instance (prompt input start / command start), reads the typed prefix from the buffer, and draws a decoration at the cursor. Sources are injected (history,ai) so the engine stays testable; keep the model call behind the 700 ms pause and the three-character minimum.- Colours:
lib/terminalColors.ts(palettes, overrides, importers) →components/terminal/xtermTheme.ts(xtermTheme(theme, { ansi, background, tint })). Per-theme overrides live inui.terminalAnsi[themeId]; a session'scoloris a tint, never a palette change. - Fix-it and ⌘K share the
CmdKBar; proposals arrive throughuseTerminals.cmdkPrefillso any feature can hand the user an editable command (Run / Insert / Cancel) without a second UI. - Persisted history is
core/terminal_history.rs(JSONL under~/.caret/terminal/), appended from the store onblock-end; Quick-Open-style search lives incomponents/palette/TerminalHistory.tsx.