What happens from the moment you press Enter to the response on screen
Tap any step to expand details ↓
The user types in a contenteditable div. On Enter (or send button), handleKeyDown() fires.
The DOM is parsed by parseFromDOM() into a typed Prompt object — extracting text, @-mentions, image attachments, and agent references.
createPromptSubmit() calls buildRequestParts() to assemble all content parts.
An optimistic message is inserted immediately into local state — this is why the user sees their message before any server round-trip.
Then it calls sendFollowupDraft() which delegates to the SDK.
The typed SDK client (createOpencodeClient()) serialises the request and sends a
POST to the local HTTP server running at a Unix socket / localhost.
The header x-opencode-directory carries the workspace path so the server knows which project is active.
The opencode server validates the request schema, resolves the session by ID, and
calls Session.prompt().
The route also applies permission settings attached to the request payload (e.g. "allow file writes without asking").
createUserMessage() stores the user turn in the SQLite database and emits a
session.message.created event.
Then runLoop() starts the agentic loop — it will keep running until the LLM returns with no pending tool calls.
LLM.stream() calls LLMRequestPrep.prepare() to build the full messages array with
system prompt, conversation history, and tool definitions.
The AI SDK's streamText() then opens an SSE/streaming connection to the provider (Anthropic, OpenAI, etc.).
Events flow back as a typed Stream<LLMEvent>.
The processor consumes each stream event. When a tool call arrives, it:
1. Persists the ToolPart to the database
2. Emits a live event so the UI shows "running tool…"
3. Executes the tool (read file, run shell cmd, search codebase…)
4. Appends the tool result and re-enters the loop — the LLM sees the result on the next turn.
The loop continues until the LLM emits a final text response with no pending tool calls.
The app maintains a persistent SSE event stream (globalSDK.event.start()).
As the server emits events (session.message.updated, message.part.added, …),
the event-reducer.ts merges them into the React context store — no polling needed.
Text deltas update in real-time as the LLM streams tokens.
MessageTimeline reads sync.data.message[sessionID] from the store.
It uses a virtualizer for performance (only renders visible messages).
Each message is decomposed into parts: text (Markdown), tool calls with status badges, file diffs, reasoning blocks, and images.
As the LLM streams, each text-delta appends tokens live — the user sees the response grow word-by-word.