System Design

Design Cursor (AI Code Editor with Agent Edits)

hardarchitectureAI

Design Cursor (AI Code Editor with Agent Edits)

Asked In

CursorOpenAIAnthropicGitHubVercel

Key Challenges

  • ·Context chips (@file, selection, open tabs) that budget the prompt window without pasting whole repos
  • ·Agent timelines that propose multi-file edits as reviewable events — not silent buffer writes
  • ·Pending diffs with Accept / Reject so the model never owns the editor buffer
  • ·Streaming patches that can be stopped mid-flight without corrupting committed code
  • ·Keeping API keys and tool orchestration behind a BFF while the IDE stays snappy in the browser

2-Minute Answer — High-Level TL;DR

Read this before your interview

HLD Interview Focus

I would design Cursor as three systems sharing one editor buffer. First, context chips — @file, the current selection, open tabs — are first-class UI state that budgets what goes into the prompt, not a giant paste. Second, the composer talks to a BFF that owns keys, tools, and model routing; the browser never calls the LLM API directly. Third, agent output arrives as SSE events: tokens for chat, and patch_proposed for edits. Patches land in a pending-diff queue. Accept merges into the buffer with an undo entry; Reject discards. The sticky rule: proposed edits are a review queue — the model never silently owns the file.

My Approach: Building the Solution

How to think about this problem

Start from the user moment: they highlight a buggy auth helper, @-mention auth.ts, and ask the agent to refactor. Juniors draw a chat panel and a Monaco editor. Seniors draw context chips, an agent timeline, and a pending-diff queue that gates every write into the buffer.

Why This Approach?

Most weak answers treat this as 'ChatGPT + CodeMirror.' That collapses when the interviewer asks what happens if the model rewrites the wrong file mid-stream, or how Stop interacts with a half-applied patch. Once you separate committed buffer from pending patches, Accept/Reject, multi-file tabs, and Abort become natural — the same way regenerate forced a message tree in ChatGPT.

Think of agent edits like a pull request, not autocomplete. Autocomplete inserts as you type. A PR opens a reviewable diff; merge is an explicit human action. Cursor's Accept button is merge. Streaming tokens into the file without review is force-pushing to main.

R

Requirements: What Makes a Great Solution?

Clarify scope before designing anything

Requirements Exploration Questions

Ask your interviewer these questions to refine requirements

What product modes are in scope?

  • ·Ask mode (answer questions, no file writes) vs Agent mode (propose multi-file patches)
  • ·Inline edit (Cmd-K style) vs chat sidebar agent
  • ·Single-file vs workspace-wide edits
  • ·Local / on-device models vs cloud API behind a BFF

What context can the user attach?

  • ·@file, @folder, current selection, open tabs, git diff, docs links
  • ·Hard token budget — which chips get truncated first?
  • ·Privacy: can context leave the machine for cloud models?

What are the latency and safety expectations?

  • ·Time-to-first-token for chat; time-to-first-patch for agent edits
  • ·Stop must cancel server work and freeze pending patches
  • ·Accept/Reject required, or auto-apply with undo?
  • ·Conflict if the user typed into the buffer while a patch was pending

Functional Requirements

Must Have — MVP

  • Composer with context chips that attach/detach files and selections
  • Streaming assistant replies (SSE) with Stop via AbortController
  • Agent mode that proposes unified diffs as pending patches
  • Accept / Reject per patch (or per hunk) before mutating the buffer
  • Multi-file tabs reflecting which files have pending edits
  • Agent step timeline (planning, tool call, patch proposed)
  • Model / mode picker that gates capabilities (e.g. Agent disabled on tiny models)

Advanced (If Time Permits)

  • +Inline ghost-text speculative completions reconciled against the real stream
  • +Multi-tab sync of draft composer + pending patches
  • +Checkpoint / restore of buffer before a batch of agent edits
  • +MCP / tool cards embedded in the agent timeline

Non-Functional Requirements

Performance

  • ·TTFT <800ms p50 for Ask; first patch_proposed <2s p50 for small refactors
  • ·Editor remains responsive while streaming — never block the main thread on huge diffs
  • ·Virtualize long agent timelines and large unified diffs

Reliability

  • ·Stop aborts fetch + freezes pending patches mid-stream
  • ·If the user edits a file with a pending patch, mark conflict and require re-base or Reject
  • ·Network drop keeps partial chat; pending patches stay reviewable

Security & privacy

  • ·No provider API keys in the browser — BFF / AI gateway only
  • ·Sanitize rendered Markdown / code from the model (XSS)
  • ·Respect workspace ignore rules for context gathering

Accessibility

  • ·Keyboard Accept / Reject / Stop
  • ·aria-live for agent status without shouting every token
  • ·Diff view usable with screen readers (line-level announcements on focus)
A

Architecture (Conceptual)

Component structure, data flow, rendering strategy

Architecture wireframe

browser → model

browser / client

Cloud

API key exposed

Direct → LLM API

Anthropic, OpenAI — don't

BFF / AI gateway

key stays server-side

LLM API

Anthropic, OpenAI

Self-hosted

vLLM, TGI

Edge

edge inference

Cloudflare, Vercel

Closer to users; great for small models and filters

On-device

WebLLM / ONNX

runs in the browser

Privacy wins; model download + device limits apply

ApproachLatencyData privacyCostGood for
Cloud API, directHighData leaves the clientPer tokenNothing — it exposes your API key
Cloud API behind a BFFHigh, plus one hopYour server controls the flowPer token + serverMost products
Self-hosted modelMediumFull controlInfrastructureRegulated / sensitive data
Edge inferenceMediumDepends on providerPer request / planContent moderation, light models
On-device (WebLLM)Low after loadStays on deviceUser CPU/GPUOffline, privacy-first demos

Hover or click a row / column to highlight the matching approach.

Agent edit pipeline

propose → review → apply

Interview note

Pending diff queue

patch_proposed lands here. Accept merges into the buffer; Reject discards. The model never owns the file.

Cursor pending-diff playground

pending

Context chips

Editor buffer (committed)
export function login(user: string, pass: string) {
  return fetch('/api/login', {
    method: 'POST',
    body: JSON.stringify({ user, pass }),
  });
}
Pending patch
@@ auth.ts
-export function login(user: string, pass: string) {
-  return fetch('/api/login', {
-    method: 'POST',
-    body: JSON.stringify({ user, pass }),
-  });
-}
+export async function login(user: string, pass: string) {
+  const res = await fetch('/api/login', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ user, pass }),
+  });
+  if (!res.ok) throw new Error('login failed');
+  return res.json();
+}

Remember: proposed edits are a review queue — never setValue from the raw SSE stream.

Component Hierarchy

Top-Level: CursorIdeShell

  • ·Owns FileTab[] + EditorBuffer map (committed text)
  • ·Owns ContextChip[] and ComposerDraft
  • ·Coordinates AgentStreamController (AbortController + SSE decode)
  • ·Owns PendingPatch[] review queue and Accept/Reject mutations
component-hierarchy-tree/
ONE APP · CLEAR BOUNDARIES
CursorIdeShell (smart)
 FileTabBar
 EditorPane (Monaco / CodeMirror) — committed buffer only
 ContextPicker
    ContextChip[] (@file, selection, tab)
 Composer
    ModePicker (Ask | Agent)
    ModelPicker
    PromptTextarea + Send/Stop
 AgentTimeline
    AgentStep[] (plan | tool | patch | error)
 DiffReviewQueue
    PendingPatchCard[] (Accept | Reject)
 AgentStreamController (logical)
     AbortController per run
     SSE decode → store actions
imports flow one way: features use ui, data-access, and utils; utils imports nothing

React-Specific Architecture Patterns

Committed vs pending buffers

  • ·EditorPane reads only committed text
  • ·PendingPatch holds unified diff + target path + baseHash
  • ·Accept applies patch → updates buffer + clears pending; Reject drops pending
  • ·Trade-off: auto-apply is faster but needs a strong undo stack and conflict UX

Context as budgeted product state

  • ·Chips are ordered; overflow truncates lowest-priority chips first
  • ·Selection chip always wins over whole-file when both present
  • ·Composer sends chip ids / hashes, not necessarily full file bodies every time (BFF can hydrate)

BFF owns tools and secrets

  • ·Browser never holds provider keys
  • ·BFF enforces rate limits, workspace ACLs, and tool allowlists
  • ·Client only decodes typed SSE events into store actions
architecture-diagram/
ONE APP · CLEAR BOUNDARIES

                     CURSOR IDEFRONTEND ARCHITECTURE                         

            
   FileTabBar      EditorPane       ContextChips    Composer+Modes   
      (committed)         
                                                          
                   
                                                                            
                                                      
                    CursorIdeShell                                         
                    + StreamController                                     
                                                      
                              SSE                                          
                                                                           
                       X direct LLM API               
                    BFF / AI gateway   (API key exposed)           
                                                      
                                                
                                                                          
          LLM API     self-hosted      tools/MCP                             
                                                                             
  Events → AgentTimeline + DiffReviewQueue (pending) → Accept → EditorBuffer 
imports flow one way: features use ui, data-access, and utils; utils imports nothing

Why React Query + Zustand?

URL may hold workspace id + active file path. React state / Zustand holds chips, pending patches, and stream status. Committed file text may live in an editor model + IndexedDB draft — but pending patches must stay outside the editor model until Accept.

D

Data Model (API + Entities + Cache)

Entities, interfaces, cache shape, consistency rules

Separate committed files from reviewable proposals. ContextChip is prompt budget. PendingPatch is a PR-sized unit of work. AgentStep is the timeline row.

ContextChip

Source: Client
Belongs to: Composer
Fields: id, kind(file|selection|tab|diff), path?, range?, priority, tokenEstimate

PendingPatch

Source: Server→Client
Belongs to: DiffReviewQueue
Fields: id, path, baseHash, unifiedDiff, status(pending|accepted|rejected), createdAt

AgentStep

Source: Server→Client
Belongs to: AgentTimeline
Fields: id, type(plan|tool|patch|error), title, detail?, status

FileTab

Source: Client
Belongs to: Shell
Fields: path, isDirty, hasPendingPatch

ComposerDraft

Source: Client
Belongs to: Composer
Fields: text, mode(ask|agent), modelId, chipIds[]
cursor-types.ts
export type ContextChipKind = 'file' | 'selection' | 'tab' | 'diff';

export interface ContextChip {
  id: string;
  kind: ContextChipKind;
  path?: string;
  range?: { startLine: number; endLine: number };
  priority: number;
  tokenEstimate: number;
}

export interface PendingPatch {
  id: string;
  path: string;
  baseHash: string;
  unifiedDiff: string;
  status: 'pending' | 'accepted' | 'rejected';
}

export type AgentSseEvent =
  | { type: 'token'; text: string }
  | { type: 'tool_start'; name: string; argsSummary: string }
  | { type: 'tool_result'; name: string; ok: boolean }
  | { type: 'patch_proposed'; patch: PendingPatch }
  | { type: 'error'; message: string }
  | { type: 'done' };
I

Interface Design (React Integration)

API contracts, hooks, integration patterns

The headline call is POST /api/agent/run — one SSE stream of typed events. The client never talks to the model provider. Context is sent as chip references; the BFF hydrates file bodies server-side when allowed.

EndpointMethodPurpose
/api/agent/runPOSTOpen SSE stream for Ask or Agent run
/api/agent/run/:id/abortPOSTServer-side abort if TCP close is not enough
/api/context/hydratePOSTResolve chip refs to prompt segments (optional separate call)
/api/modelsGETModel list with latency/cost/capabilities for the picker
Agent run (SSE)
POST /api/agent/run
body: { mode: 'agent', modelId, prompt, chips: ContextChip[] }
=> text/event-stream
event: token | tool_start | tool_result | patch_proposed | error | done
agent-api.ts
export interface AgentRunRequest {
  mode: 'ask' | 'agent';
  modelId: string;
  prompt: string;
  chips: ContextChip[];
  activePath?: string;
}
O

Optimization (Performance + Scale)

Rendering, media, network, and memory optimizations

User asks the agent to refactor auth. Tokens start streaming in chat. Then a patch_proposed event arrives for auth.ts.

The naive move is editor.setValue(newText) as the model streams. It feels alive — until Stop leaves a half-written function, or the model targets the wrong file and production login breaks with no review step.

Where it breaks: there is no Accept gate, no baseHash check, and Abort only hides the spinner while the buffer is already corrupted.

The fix: keep EditorPane on committed text. Park patches in DiffReviewQueue. Accept applies with an undo checkpoint; Reject drops. Batch chat token paints on rAF; virtualize huge diffs. Prefer time-to-first-patch as the agent metric users feel, alongside TTFT for Ask mode.

Pending-diff hot path

  • ·Do not mutate Monaco models on patch_proposed — only on Accept
  • ·Store unified diffs; apply with a tested patch library + baseHash guard
  • ·Virtualize diffs over ~500 lines
  • ·Trade-off: auto-apply needs undo stack + conflict UX or you will ship silent overwrites

Streaming UI

  • ·Batch token setState on requestAnimationFrame
  • ·Agent timeline append is append-only; don't rebuild the whole list each event
  • ·Cancel in-flight hydrate when chips change rapidly

Context budget

  • ·Estimate tokens per chip; show budget meter
  • ·Drop lowest priority chips first when over budget
  • ·Prefer selection snippets over whole files when both present

Optimistic Updates

Scene: user runs Agent on a selection in auth.ts.

Naive move: stream the replacement code straight into the editor.

Where it breaks: Stop mid-function; wrong-file overwrite; no review for teammates watching the buffer.

The fix: patch_proposed → PendingPatchCard. Accept applies; Reject discards. Chat can still stream tokens optimistically because chat is not the source of truth for disk.

When not to auto-apply: auth, payments, migrations, anything without a clear undo story.

optimisticUpdate.ts
type PatchStatus = 'pending' | 'accepted' | 'rejected';

function onSseEvent(e: AgentSseEvent, store: IdeStore) {
  if (e.type === 'token') store.appendChat(e.text);
  if (e.type === 'patch_proposed') store.enqueuePatch({ ...e.patch, status: 'pending' });
  if (e.type === 'done') store.setAgentStatus('idle');
}

function acceptPatch(patchId: string, store: IdeStore) {
  const patch = store.pendingById[patchId];
  if (!patch || patch.status !== 'pending') return;
  const current = store.buffersByPath[patch.path] ?? '';
  if (hash(current) !== patch.baseHash) {
    store.markConflict(patchId);
    return;
  }
  store.pushUndo(patch.path, current);
  store.buffersByPath[patch.path] = applyUnifiedDiff(current, patch.unifiedDiff);
  store.pendingById[patchId] = { ...patch, status: 'accepted' };
}

Remember

Proposed edits are a review queue; the committed buffer is user-owned.

Say this in the interview

I would stream agent patches into a pending-diff queue and only mutate the editor buffer on Accept, with a baseHash check and undo checkpoint — never setValue from the raw SSE stream.

+

Bonus: Implementation Cookbook (Optional)

LLD snippets — only when asked by interviewer

Stretch topics if time remains: speculative ghost-text, MCP tool cards in the timeline, privacy modes (on-device WebLLM), and eval metrics (TTFT vs time-to-first-patch).

Included (Minimal)

  • ·Ask vs Agent mode picker UX
  • ·Inline vs sidebar diff review
  • ·Local model path (WebLLM) for privacy-sensitive workspaces
  • ·Checkpoint entire workspace before a multi-file agent batch
mode-gate.ts
export function canUseAgent(model: { capabilities: string[] }) {
  return model.capabilities.includes('tools') && model.capabilities.includes('patches');
}

Key Takeaways

  • Cursor is not ChatGPT beside an editor — context chips, agent steps, and pending diffs share one buffer.
  • Never stream patches straight into the file — park them in a PendingPatch queue until Accept.
  • Context (@file / selection / tabs) is product state that budgets the prompt, not a paste affordance.
  • Talk to the model through a BFF / AI gateway — keys, tools, and rate limits stay server-side.
  • Stop must AbortController the agent stream and freeze in-flight pending patches.
  • Ask mode answers; Agent mode proposes edits — different UX contracts for the same composer.