Chat mode provides a structured message view for both Claude Code and Codex sessions, as an alternative to the raw terminal TUI. Claude chat sessions use Claude's --output-format stream-json protocol; Codex chat sessions use Codex app-server. Both render markdown, tool visualization, live thinking/status updates, and interactive permission prompts.
When creating a new session:
- Choose the backend first: Claude or Codex
- The mode (Terminal or Chat) defaults to your Default session mode setting (Settings > Session)
- You can switch the mode via the split button on the new session dialog
- Backend-specific defaults (model, permission mode, effort, extra args) come from Settings > Claude or Settings > Codex
When resuming a stopped session:
- The sidebar resume button uses your default mode setting
- The split button next to resume lets you choose Terminal or Chat explicitly
- The mode you last used for a session is remembered
Chat sessions show a speech bubble icon in the sidebar to distinguish them from terminal sessions.
Compact mode (default): A dense, document-style layout with role labels (You/Claude) on the left. Closer to the information density of a terminal session.
Bubble mode: Traditional chat-style bubbles. Toggle between modes via Settings > Chat > Compact mode.
In compact mode, you can choose how user and assistant messages are visually distinguished. Configure via Settings > Chat > Role indicator style:
| Style | Description |
|---|---|
| Border (default) | Colored left border on each message |
| Background | Subtle background tint per role |
| Icon | Small icon before the role label |
| Label | Text label only (You/Claude) |
| Type | Rendering |
|---|---|
| User messages | Markdown-rendered text, inline image attachments |
| Assistant messages | Markdown with syntax highlighting in code blocks |
| Thinking blocks | Collapsible "Thinking..." section with raw text. Codex reasoning streams into this block live. |
| Tool use/result | Specialized tool cards (see below) |
| System notifications | Collapsible details for task-notification and system-reminder |
| System messages | Disconnected/reconnected/session ended notices |
User messages over 500 characters are automatically collapsed showing the first 120 characters as a preview with the total character count (e.g., "这是消息内容前120个字符... (1224 chars)"). Click to expand — the preview text hides and a "Collapse" button appears.
Each tool call is rendered as a structured card showing the tool name, inputs, and results.
| Tool | Card Display |
|---|---|
| Edit | Diff view with +/- prefix columns (flex layout, prefix stays fixed on wrap), suffix context matching |
| Patch | Claude-style diff cards for each changed file. Codex apply_patch results can render as Update/Write/Delete/Move cards with unified diff details. |
| Write | Line count, file size, full content with syntax highlighting + line numbers |
| Read | Line count, full content with syntax highlighting + line numbers |
Syntax highlighting uses highlight.js (30 languages), auto-detected from file extension. A searchable language dropdown next to the Wrap button allows manual override. Files >10KB defer highlighting until the <details> section is expanded.
Non-file tools show a collapsible Input section and a collapsible Output section with the first line as a summary. Failed tools show an error section highlighted in red.
While a tool is running, a spinner indicates progress. For file operations (Edit/Patch/Write/Read), the file path is shown immediately. For other tools, the full input is visible while waiting for the result.
Each tool card has a clipboard button that opens the tool's input and output in a temporary CodeEditor window. This is useful for examining large outputs without scrolling through the chat. Messages also have a clipboard button that opens the message content in a temporary editor.
Every assistant message has a small fork (GitHub repo-forked) icon next to the open-in-editor button on hover. Clicking it branches a new session that contains the conversation up to and including that message, then a popup asks for the first message of the branch. The original session is untouched. See Sessions → Forking. (Claude only.)
When Claude requests permission to use a tool (via --permission-prompt-tool), the permission prompt appears inline on the tool card:
- Allow -- approve this single tool use
- Always Allow -- approve and add a permanent permission rule (shown when suggestions are available)
- Deny -- reject the tool use
After responding, the button area shows the resolution status (Allowed / Always Allowed / Denied). The current permission mode is displayed in the status bar.
If a permission request is cancelled by Claude (e.g., due to interrupt), the prompt updates to show "Cancelled".
To change the permission mode mid-session, click the lock icon in the status bar (see Permission Mode Dropdown under Status Bar).
When Claude uses the Agent tool (subagent), the chat view provides visibility into the subagent's execution.
Subagent viewers use a virtual session model. Each subagent gets a virtual session ID:
- Live agents:
sub-{parentToolUseId}-- the server buffers messages and forwards new ones in real time - Completed agents:
sub-agent-{agentId}-- the server loads messages from the agent's saved JSONL file
The client opens a standard read-only ChatView attached to the virtual session. All buffering and message forwarding is handled server-side -- the client code is the same ChatView class used for regular sessions, just with readOnly: true.
Claude's stream-json mode does not emit subagent assistant text messages (known bug: anthropics/claude-code#8262). To work around this, the server watches subagent JSONL files via fs.watch() for live text messages. Messages from the JSONL watcher are deduplicated by UUID against those already received via stream-json.
While the agent is running, the Agent tool card shows:
- A live message count ("12 messages")
- Current activity (thinking, running ToolName, responding)
- A View Log button to open a live viewer
Clicking View Log on a running agent opens a read-only ChatView window (no input area, no status bar) that displays the subagent's messages in real time. New messages appear as the agent works. You can scroll up to review history while new messages continue arriving -- scrolling back to the bottom re-enables auto-scroll.
For completed agents, the View Log button opens a read-only ChatView populated from the agent's saved JSONL history.
Subagent messages are filtered from the main chat -- they only appear in the dedicated agent log viewer.
Press Ctrl+F to open the search bar. Features:
- Full-text search across the entire conversation history (not just the current view). Falls back to
/api/activewhen JSONL path is unavailable. - Huge sessions too: when a session is so large it can't be held in memory (multi-hundred-MB histories), search automatically switches to a server-side streaming scan of the whole file. Results stream in progressively (
less-style): the counter shows a liveN… searchingthat climbs as matches are found and finalizes to the total when the scan completes (N/500+if it hit the 500-match cap). The first match is jumped to as soon as it's found. A match that's already on screen just scrolls to it; otherwise the jump teleports to that part of the history (seek-loaded around the match's exact file position) and locks onto the highlighted result — reliable regardless of how far back the match is or whether the session is still growing. The scroll-to-bottom button returns you to the live conversation. - CSS Custom Highlight API for non-destructive highlighting (does not modify DOM)
- Match counter showing current position and total matches
- Previous/Next navigation (arrow buttons or Enter/Shift+Enter)
- Search results from outside the current view window automatically load the target messages; the target message's collapsed cards (tool output, thinking) are expanded so the highlight is visible
Jumping to a match scrolls it fully into view — including scrolling inside a card that has its own scrollbar (long code blocks, tool output), which a plain list-scroll can't reach. The active match is a solid high-contrast highlight (the other hits are dimmer) and a pulse briefly flashes on it, so it's obvious where the match is even in a long card. The highlight layer is re-applied when the view changes (scroll, pagination, jumps, reconnect catch-up).
- Enter sends the message (in normal mode). IME composition (CJK input) is detected and Enter is not intercepted during composing.
- Ctrl+Enter sends the message (in expanded mode)
- Shift+Enter inserts a newline
The input area auto-grows up to 200px as you type.
Click the floating expand button (diagonal arrow) inside the textarea to toggle expanded mode. In expanded mode, the textarea gets a fixed 200px height for composing longer messages, and the send shortcut changes to Ctrl+Enter.
Type / to see all available slash commands from the current Claude session. The autocomplete dropdown appears immediately:
- Arrow keys navigate the list
- Tab or Enter accepts the selected command
- Typing narrows the matches (e.g.,
/cofilters to/compact,/config)
Slash commands are loaded from the session's system.init message and persist across reconnects.
Paste an image from your clipboard (Ctrl+V) to add it as an attachment:
- A thumbnail preview appears above the input area
- Multiple images can be attached to a single message
- Click the X button on an attachment to remove it
- Images are sent as base64-encoded content blocks alongside the text message
While the backend is responding, a streaming status bar appears above the input area showing the current activity (thinking, running ToolName, responding) with a spinner. Click the Stop button to interrupt the active turn mid-response.
The interrupt is routed through the backend wrapper. For Claude sessions: the control_request: interrupt protocol message is written to stdin immediately, and a SIGINT fallback is scheduled 2 seconds later. Before firing, the wrapper's meta file is re-read; if streaming:false the protocol interrupt worked and SIGINT is skipped. Sending a new chat message during the 2s window also cancels the pending SIGINT. This avoids the killing-the-whole-session problem where newer Claude Code versions exit on SIGINT instead of just interrupting the turn. Codex chat sessions use Codex's native turn interrupt request with no fallback needed.
When Claude is interrupted or hits a turn limit, the result message shows a human-readable label:
| Subtype | Label |
|---|---|
error_during_execution |
Interrupted |
error_max_turns |
Max turns reached |
| Other | Raw subtype value |
When Claude uses the TodoWrite tool to track task progress, the current TODO state is displayed above the input area:
- Shows the current in-progress item with a hourglass icon and a progress count (e.g., "3/7")
- When all items are completed, the display is hidden
- Click the TODO display to expand a popup showing the full list with status icons:
- Completed items
- In-progress item (highlighted)
- Pending items
The TODO list updates live as Claude calls TodoWrite with updated items.
The status bar at the bottom of the chat view shows session metrics:
| Metric | Display | Description |
|---|---|---|
| Model | Dropdown | Model as last reported by the CLI (full resolved ID; model: ? when not yet reported). Click to switch model mid-session — Claude applies instantly (set_model), Codex from the next turn. |
| Effort | Dropdown | Reasoning effort. Claude: shows the last commanded value (the CLI doesn't report it back); switching applies via the CLI's runtime settings channel and takes effect immediately. Codex: reported per turn; switching applies from the next turn. effort: ? when nothing is known. |
| Permission mode | Lock icon + mode | Current permission mode. Click to open dropdown and change mode mid-session (see below) |
| Background tasks | Spinner + count | Active background tasks (agents + commands). Click for detail popup (see below) |
| Context usage | Colored progress bar + percentage | How much of the context window is used. Colors: green (<70%), yellow (70-85%), orange (85-95%), red (>95%) |
| Cache ratio | Lightning bolt + percentage | Ratio of cache-read tokens to total input tokens |
| Cost | Dollar amount | Cumulative session cost. Colors: green (<$1), orange ($1-5), red (>$5) |
Status data comes from per-turn usage in assistant messages and modelUsage in result messages. The display never guesses: when the context-window size is unknown the bar shows <used>/? instead of a percentage against an assumed window. It is persisted in the session's chatStatus and restored when re-attaching to an existing session.
Click the lock icon in the status bar to open a dropdown listing available permission modes (sourced from claude --help output). Selecting a mode sends a set-permission-mode WebSocket message to the server, which writes a control_request with subtype: 'set_permission_mode' to Claude's stdin. The active mode is always displayed even if unknown (defaults to "default").
Two types of background tasks are tracked:
| Source | Detection | Icon |
|---|---|---|
| Agent tool | Claude system.task_started, or Codex agent/subagent task metadata |
Robot |
| Background commands | Claude tool_use with run_in_background: true |
Lightning bolt |
The task count badge appears in the status bar when tasks are active. Click to open a popup showing each task's description and current activity. Clicking a task in the popup:
- Agent tasks: opens the subagent View Log viewer
- Command tasks: opens a temporary editor showing the command input and output
Tasks are removed when the backend reports completion. For Codex, only background/subagent agent work is treated as a task; ordinary tool calls are not shown here.
The chat message area respects the global terminal font size setting. The message list scales proportionally based on the font size relative to the 14px baseline. Change the font size via the global settings (toolbar gear icon > A-/A+).
The chat view uses a virtual scroll system with a sliding DOM window for efficient rendering of long conversations:
- On attach, the server normalizes all messages via
MessageManagerand sends the last 50 as normalized messages (with stable IDs, merged tool calls). All messages go through the sameMessageManager— no separate "buffer" vs "JSONL" concept exposed to the client. - Scroll up near the top to load earlier messages (50 at a time via
_extendTop). When DOM exceeds ~150 elements, older messages at the bottom are trimmed (_trimBottom). - Huge sessions (transcripts too large to hold in memory) load only the recent tail, then treat the entire earlier history as one continuous scroll: scrolling up seek-loads older messages by byte offset all the way back to the first message, with no "history truncated" notice and nothing to click. Search/minimap jumps teleport to the target, and from there browsing works in both directions — scrolling down seek-loads newer messages continuously until the end of the file. The rendered DOM stays bounded (far-away portions are dropped and transparently re-loaded when you scroll back). The scroll-to-bottom button returns you to the live tail.
- Scroll down past the rendered window loads newer messages (
_extendBottom) and trims the top (_trimTop) with scroll position preservation. - Wheel at top edge: When
scrollTop=0, scroll events stop firing. A wheel listener detects upward scroll intent and triggers pagination. - Auto-fill: If initial content is shorter than viewport (no scrollbar), more messages are loaded automatically.
- Position indicator: A floating pill at top center shows "120–170 / 3000" when not pinned to bottom.
A semantic scrollbar on the right side of the message list provides an overview of the entire conversation:
- User message markers: Thin blue lines at each user input position; the hovered turn's marker lights up
- Compact markers: Wider red lines at context compaction boundaries
- Drag to navigate: Click or drag the minimap to jump to any point in the conversation via
jumpToIndex - Floating label: Follows cursor vertically as a two-line card — time on top, a ~60-char preview of the nearest user message below. Non-today messages include the date.
- Outline (☰) button at the top of the track: opens a filterable list of all your messages (time + two-line preview), scrolled to the most recent. Type to filter, click to jump. This is the fastest way to find a spot you remember sending something at. Works for huge (partially loaded) sessions too.
- Viewport thumb: Semi-transparent bar showing current rendered window position
- Hidden for short conversations (<3 turns). Native scrollbar hidden when minimap is active.
- Huge sessions: when a very large history is loaded tail-only, the minimap switches to whole-conversation time coordinates — markers span the entire file, and clicking/dragging teleports to the target region (seek-loaded on demand by file position).
Turn data comes from MessageManager.turnMap(), included in the attach response or fetched via ?turnmap=1 API.
By default, the view auto-scrolls to show new messages. When you scroll up:
- Auto-scroll is disabled
- A scroll-to-bottom button appears (with a badge showing new message count)
- Live messages arriving while viewing history are deferred — only the total count is updated and the badge increments. Messages load when you return to the bottom.
- Click the button or scroll to the bottom to re-enable auto-scroll
The scroll-to-bottom uses iterative convergence across multiple animation frames (up to 10 rAF) to account for content-visibility: auto CSS optimization that may change scroll heights.
Stopped sessions can be viewed without resuming:
- View History button: In the sidebar expand panel for stopped sessions, click "📋 View History" to open a read-only ChatView that loads JSONL history without running
claude --resume - After terminate: When a running session exits or is terminated while a window is open, the window automatically converts to read-only — the input area is hidden, and closing the window doesn't send a kill signal
- View-only sessions support full scroll-up pagination and minimap navigation
- Virtual session IDs are backend-aware (
view-{backendSessionId}under the hood)
All read-only ChatView windows (view-history, terminated, exited) show a Resume this session button in place of the input area. Click to call app.resumeSession() — the backend starts a live session (via claude --resume / codex resume), the current read-only window closes, and the new live window takes over. Subagent viewers (sub-* virtual sessions) are excluded since they can't be resumed. This unifies the three read-only scenarios so users never have to go back to the sidebar just to continue chatting.
Chat input text is automatically saved every 300ms to the server and synced across all connected clients (Telegram-style):
- Drafts persist across page refresh (stored server-side via StateSync)
- Typing in one browser tab updates the textarea in another tab (unless actively focused)
- Drafts are cleared when the message is sent
URLs and absolute file paths in messages are automatically detected and made interactive. Path detection uses a VS Code-style character exclusion regex. The _linkify function is HTML-aware: it splits content into tags and text segments, skipping <a> and <code> blocks to avoid double-linking.
- Click copies the path/URL to clipboard (with tooltip feedback)
- Ctrl+Click opens the target:
- URLs open in a new browser tab
- File paths open in the file viewer or code editor
- Directory paths open in the file explorer
File paths with line number suffixes are supported:
/path/to/file.js:42-- opens at line 42/path/to/file.js:42:10-- opens at line 42/path/to/file.js:10-20-- opens at line 10
If the WebSocket connection drops (server restart, network interruption):
- A "Disconnected from server" message appears and a red bar shows at the bottom of the message list
- The input stays fully interactive — you can select/copy what you've typed and keep drafting; only sending is blocked (a toast explains, your draft is kept)
- On reconnect, a "Reconnected" message appears
- The chat view automatically re-attaches to the session
- Any messages missed during the disconnection are fetched and rendered
- Sending is re-enabled
This ensures no messages (or drafts) are lost during brief network interruptions.