Skip to content

Chat API

The toolbar’s chat drives the installed Codex CLI through its app-server protocol and Claude Code through its stream-json protocol. devhost adds no model service and stores no API keys: each CLI keeps its own sign-in, permissions, settings, and environment. This page documents the routes the chat uses; for using chat, see Chat with a coding agent.

All routes live on the page’s own hostname and follow the Control API security rules.

Every chat request names a target, which devhost turns into the agent’s working directory. The browser never supplies a directory, executable, flags, or environment.

TargetWorking directory
projectThe checkout of the routed service that pageURL belongs to
devhostdevhost’s own source checkout. Only development builds have one; other builds answer 400, and /__devhost__/config.json reports an empty devhostSourceRootPath.

A missing or unknown target gets 400 Bad Request. GET requests, the WebSocket, and session close pass pageURL and chatTarget as query parameters; creating a session passes them as pageURL and target in its JSON body.

GET /__devhost__/chat/harnesses?pageURL=<page-url>&chatTarget=<target>

devhost asks each installed CLI for its model list without sending a prompt. Each harness has id, label, available, models, and an optional error; each model has an id, a label, and a description. A harness can be available and still fail to answer prompts when its CLI is not signed in.

POST /__devhost__/chat/sessions
{
"harness": "codex",
"pageURL": "https://app.localhost/",
"target": "project",
"model": "optional-model-id",
"resumeId": "optional-native-session-id"
}

Leaving out model keeps the CLI’s configured default. resumeId resumes the CLI’s own saved conversation; the session then carries that ID as nativeSessionId from the start, and replaces closed sessions of the same conversation in the list. Resuming a conversation that belongs to another project is rejected.

The response, like every session record, has id, harness, projectRootPath, status, an optional nativeSessionId, and an optional error. nativeSessionId can appear later; Claude Code usually reports it after its first turn.

statusMeaning
startingThe CLI is starting.
idleWaiting for a message.
runningA turn is in progress.
requires_actionThe agent waits for an approval or an answer.
errorThe last turn failed; error has the reason. The next message clears it.
closedThe process ended. The session stays listed and can be resumed.
GET /__devhost__/chat/sessions?pageURL=<page-url>&chatTarget=<target>
POST /__devhost__/chat/sessions/<id>/close?pageURL=<page-url>&chatTarget=<target>

Listing returns only the sessions of the directory that pageURL and the target resolve to. Closing ends the session’s process and returns its record with status closed. A session of another directory answers 404.

Sessions belong to the running stack: closing a tab or reloading never ends a turn, and stopping the stack ends every session. A new session uses the checkout selected at that time; existing sessions keep their directory. Interrupting a turn (below) leaves the session open.

GET /__devhost__/agents takes the same pageURL and chatTarget query as the session list, answers 404 while chat is off, and lists the agents running in that directory, including subdirectories. A scan covers every agent on the machine, so pages within five seconds of each other share one.

FieldContent
keyIdentifies the row while the agent runs; not a session ID
harnessclaude-code, codex, pi, or opencode
stateworking, waiting, idle, or unknown
nameThe session’s name, when it has one
subdirectoryWhere it runs, relative to the directory; . for the directory
lastActivityUnix milliseconds
isOwnedWhether it is a chat session of this stack

The list is ordered by who needs attention: waiting, working, unknown, then idle. A top-level error reports a harness that could not be scanned, beside what the others found. Page scripts can read this response, so it carries no transcript paths or native session IDs; transcript content is only on the activity stream.

/__devhost__/agents/activity is a page, and /__devhost__/ws/agents/activity its stream, taking the same query. Both answer only on the control server’s own address, http://127.0.0.1:<port>, which config.json names as controlOrigin: routed hostnames get 403. The control API has no token, so its origin is the only boundary, and a transcript must never reach an origin that application scripts share.

Each stream message is one event: harness, pid, eventType, timestamp, and optionally role, toolName, text, and isError. Event types are those of agent-parser, and text stops at 4 KiB. A stream follows the top-level agents in the directory from the moment it opens, without history, and stops watching when it closes. An agent that resumes an old session after the stream opens replays that session.

GET /__devhost__/chat/panel returns {position}, where position is {left, top} in CSS pixels or null for the default place. PUT with the same body saves it, and needs an Origin like the other chat requests. A position is saved per manifest under devhost’s state directory, because browser storage is separate for each hostname. A session without a manifest answers 404 and keeps no position.

wss://<host>/__devhost__/ws/chat?sessionId=<id>&pageURL=<page-url>&chatTarget=<target>

A missing sessionId gets 400 and an unknown one 404, before the upgrade. devhost checks the session’s directory when the socket attaches and again before every command, so an old socket cannot send into a checkout the page has switched away from.

typeContent
snapshotFirst frame: the session, every retained event, and pendingRequestIds.
eventOne CLI event: an increasing sequence and the unchanged native payload.
stateSession metadata and the current pendingRequestIds.
command_result{sessionId, commandId, delivery?, error?} for one send, on the socket that sent it.
errorA command that failed and could not be matched to a commandId.
typeFieldsEffect
sendcommandId (non-empty string), text, optional model, optional images: [{dataURL}]Starts a turn, or adds the message to the active turn
interruptnoneInterrupts the current turn and cancels queued input
respondrequestId (string or number), native resultAnswers one pending approval or question
  • A send is acknowledged once devhost knows what became of it, not when the turn ends. error means it definitely failed. Otherwise delivery is queued (the CLI has it), started (the agent picked it up), or unconfirmed (no evidence arrived in time, so the agent may or may not have it). Acknowledgements are not shared with other browsers or kept in history.
  • A commandId that is a lowercase UUID becomes the message’s native ID, so its progress can be read from event frames: command_lifecycle frames with that command_uuid and the echoed user record for Claude Code, and the userMessage item with that clientId for Codex. A page reloaded mid-turn reads the same frames from snapshot.
  • devhost never resends a message. Neither CLI drops a repeat of a message it has already received, so a resend can run twice. Retrying is up to the person using the chat, after checking the conversation.
  • During an active turn, Codex receives the message through turn/steer and Claude Code through its streaming input. Model changes then wait for the next turn. If Codex rejects a steer because the turn just ended, devhost starts a new turn with the message.
  • For Claude Code, queued comes from the command_lifecycle frame the CLI writes on receipt, which is an internal, undocumented frame. Its echo of the user message arrives only at pickup, which a long tool or a pending approval can delay by a minute or more, so without that frame a message reads as unconfirmed until the echo arrives. For Codex, the response to turn/start or turn/steer is queued, and the userMessage item marks pickup.
  • interrupt differs by CLI in what happens to queued input. Codex discards it. Claude Code would run it after the turn stops, so devhost asks it to cancel queued input; versions without that ability still run it.
  • model set to an empty string returns to the CLI’s configured default; leaving it out keeps the session’s current model.
  • Sends from one socket are delivered in order. Approvals and interrupts are handled while a send waits for its acknowledgement.

Images must be PNG or JPEG data: URLs; remote URLs and file paths are rejected. A whole send frame, as UTF-8 JSON, must fit in 8 MiB. The browser checks this before sending, and devhost enforces it.

devhost appends [devtools.chat].prompt to the text of each non-empty send, as a labeled section of the user message. The CLI’s own system prompt and permissions are untouched. Without prompt, devhost appends its default worktree workflow; an empty string appends nothing. A changed prompt applies to the next send of every session.

Approvals and questions stay in the CLI’s own message format. devhost never answers them itself, and rejects a respond for a request that is not pending, including one already answered or cancelled. A reconnecting page uses pendingRequestIds to tell which requests in its history still need an answer.

  • A Codex turn that ends with status failed sets the session to error with the CLI’s message, or Native Codex turn failed when there is none. Errors Codex retries on its own leave the turn running until it ends.
  • A Claude Code result with is_error: true is an error even when its subtype says success, and keeps the CLI’s error text.
  • A CLI that is not signed in fails at the first turn, even when discovery found it. Sign in through the CLI, never in the browser.

Closing a session closes its socket with code 1000; stopping the stack uses 1001. devhost stops every process a session started, including children that ignore the first signal.

devhost keeps each session’s full event history in a private temporary file and streams it to every page that attaches. On Unix systems the file is unlinked as soon as it opens, so its space is freed when devhost closes it, even after a crash. A closed session keeps its history until it is resumed or the stack stops.

The history grows with the conversation. If devhost cannot write it, it stops that session’s process and reports an error rather than dropping events. The history does not survive a stack restart; resume through the CLI’s own saved conversations.

The page stores its chat state in localStorage, under keys starting with devhost:settings: that include the project directory and the page’s origin. Pages that differ only in path, query, or fragment share it. It holds:

  • whether chat is open, its side, and the chat target,
  • the selected harness, model, and session,
  • the unsent text, until the send is acknowledged,
  • the conversation’s scroll position,
  • whether reloads are held, whether the hot reload notice was dismissed, and the session and command of a Disable Reloads request the agent has not finished.

Each target keeps its own conversation state, so switching to devhost and back restores the project’s conversation. Invalid or outdated saved data resets to defaults. Transcripts, screenshots, selections, drawings, screen-sharing permission, and approval answers are never stored in the browser.

After a reload, the page reattaches the saved session only if it still exists and is not closed, and a saved model that the CLI no longer offers returns to the configured default.

Before a screenshot is sent, the page shows a preview. Choosing an area sends only that crop as its own PNG, with its position and the full screenshot’s size in the page context; cancelling keeps the draft and context.

Chat activity appears in the stack’s terminal and under chat in the log minimap. Every line carries the devhost session ID and harness; CLI turn and tool IDs appear where the CLI supplies them, and Claude Code turns get IDs local to the session.

RecordIncludes
SessionStart, resume, and close; the project directory and requested model; the CLI’s session ID once known
TurnStart with the selected or configured model (and the model Claude Code reports using), outcome, and duration
MessageCommand ID, number of attachments, time queued, whether it started a turn or joined one, and the CLI’s acceptance or rejection time
ToolTool name, path or working directory, outcome, duration, and exit code where available
Approval or questionRequest ID and kind, time waited, whether it was answered or cancelled, and whether the answer was delivered
HeartbeatFor a turn with no CLI event in the last 30 seconds: elapsed time, quiet time, active tools, and pending requests. Checked every 30 seconds.
UsageCodex token counts per turn; Claude Code usage, duration, and total_cost_usd (an estimate). Only what the CLI reports.
ErrorStartup, protocol, process, and timeout failures, with a short diagnostic

Log lines never include prompts, replies, screenshots, tool input or output, or answers to approvals. They can include file paths, project paths, IDs, and a process’s error output. Error details are cut at 512 bytes and a whole line at 2 KiB. The browser keeps the latest 512 log entries; the terminal keeps whatever your terminal keeps.