Claude Code — UI Layout Reference
Agent-specific layout reference for Claude Code (Anthropic). See agent-ui-analysis.md for shared concepts.
Observed version: v2.1.81 (2026-03-21)
Rendering engine: Ink (React for terminals)
Rendering approach: ANSI relative cursor positioning (\033[NA, \033[1B)
Layout Anatomy (bottom → top)
[agent output / response text]
[empty line(s)]
✶ Undulating… (1m 32s · ↓ 2.2k tokens) (spinner — above separator, while working)
[empty line]
──────────────────────────────────── (upper separator, may contain label)
❯ [user input] (prompt line)
──────────────────────────────────── (lower separator)
[status line(s)] (0-N lines, indented 2 spaces)
[mode line] (last row, indented 2 spaces)
Real-world Examples (live sessions, 2026-03-21)
Standard idle — no subprocess
❯
──────────────────────────────────────────────────────────────────────────
[Opus 4.6 (1M context) | Max] │ tuicommander git:(main*)
Context █░░░░░░░░░ 8% $0 (~$2.97) │ Usage ⚠ (429)
⏵⏵ bypass permissions on (shift+tab to cycle)
With subprocess — new format (count left)
───────────────────────────────────────────────────────── extractor ──
❯
──────────────────────────────────────────────────────────────────────────
[Opus 4.6 (1M context) | Max] │ mdkb git:(feat/document-organizer*)
Context ░░░░░░░░░░ 4% $0 (~$79.88) │ Usage ⚠ (429)
1 shell · ⏵⏵ bypass permissions on
Subprocess only — no mode indicator
───────────────────────────────────────────────────────── extractor ──
❯
──────────────────────────────────────────────────────────────────────────
[Opus 4.6 (1M context) | Max] │ mdkb git:(feat/document-organizer*)
Context ░░░░░░░░░░ 4% $0 (~$79.81) │ Usage ⚠ (429)
1 shell
Default mode (no bypass) — single status line, no mode line
❯
───────────────────────────────────────────────────────────────────────────────
[Opus 4.6 (1M context) | Max] ░░░░░░░░░░ 3% | tuicommander git:(main*) |… ○ low · /ef…
Agent working — spinner above, no prompt box
⎿ $ ls -la /Users/stefano.straus/Documents/.mdkb/index.sqlite
✶ Undulating…
Separator Line
A row containing a run of 4+ box-drawing characters (─ ━ ═ — ╌ ╍).
May contain embedded labels:
────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────── extractor ──
──────── ■■■ Medium /model ────────
Spinner / Timer Lines (above upper separator)
✶ Undulating…
✻ Sautéed for 1m 19s
✳ Ideating… (1m 32s · ↓ 2.2k tokens)
✻ Sautéed for 2m 9s · 1 local agent still running
· Proofing… (1m 14s · ↓ 1.6k tokens)
Markers: ✶ (U+2736), ✻ (U+273B), ✳ (U+2733), ✢ (U+2722), · (U+00B7).
Detected by is_chrome_row (✻ check) and parse_status_line (dingbat range U+2720–U+273F).
Status Lines (between lower separator and mode line)
Zero or more lines. Content is arbitrary and agent-customizable.
Indented with 2 spaces (via \033[2C).
[Opus 4.6 (1M context) | Max] │ tuicommander git:(main*)
Context █░░░░░░░░░ 5% $0 (~$0.64) │ Usage ⚠ (429)
Or Wiz HUD:
[Opus 4.6 | Team] 54% | wiz-agents git:(main)
5h: 42% (3h) | 7d: 27% (2d)
✓ Edit ×7 | ✓ Bash ×5
Configured via ~/.claude/settings.json → statusLine, but rendered
through the PTY using ANSI cursor positioning. They appear as changed_rows
and pass through is_chrome_row.
Custom status lines are never a detection signal. Their content is
user-defined — the two blocks above are just two of countless possible layouts —
so nothing in the state machine reads them: no model name, no context
percentage, no cost, no usage counter is parsed from these rows. They matter in
exactly one direction, getting classified as chrome so they do not reset the
silence timer or stamp last_output_ms.
Coverage today (chrome.rs, asserted by status_line_not_chrome,
cc_status_context_bar, cc_wiz_hud_line):
| Row | is_chrome_row | Why |
|---|---|---|
Context █░░░░░░░░░ 8% $0 (~$2.97) │ Usage ⚠ (429) | yes | block glyphs █ / ░ |
[Opus 4.6 (1M context) | Max] │ repo git:(main*) | no | carries no chrome glyph — known gap |
5h: 42% (3h) | 7d: 27% (2d) | no | same gap |
A status line is also barred from proving the agent is alive: is_spinner_row
requires the spinner glyph to lead the row, so a HUD progress bar that ticks
every second cannot be read as a working spinner. That rule is the fix for the
regression that pinned Claude BUSY forever under a wiz status bar (#446-596f).
Mode Line (last row)
The final visible row. Always chrome. Indented with 2 spaces.
Observed variants
| Format | Example | Source |
|---|---|---|
| Mode only | ⏵⏵ bypass permissions on | test |
| Mode + hint | ⏵⏵ bypass permissions on (shift+tab to cycle) | live |
| Mode + subprocess (old) | ⏵⏵ bypass permissions on · 1 shell | test |
| Mode + subprocess (old, plural) | ⏵⏵ bypass permissions on · 2 local agents | test |
| Subprocess + mode (new) | 1 shell · ⏵⏵ bypass permissions on | live |
| Subprocess only | 1 shell | screenshot |
| Plan mode | ⏸ plan mode on (shift+tab to cycle) | test |
| Accept edits | ⏵⏵ accept edits on (shift+tab to cycle) | test |
| Auto mode | ⏵⏵ auto mode | test |
| Empty | `` | test |
| Absent (default mode) | N/A — no mode line at all | live |
Key markers
⏵⏵(U+23F5 x2) — current mode-line prefix››(U+203A x2) — older mode-line prefix⏸(U+23F8) — plan mode prefix·(U+00B7) — separator between mode and subprocess count
Subprocess types observed
shell / shells, local agent / local agents, bash, background tasks
Interactive Menus
Permission Prompt
Replaces the entire bottom zone when CC requests tool approval.
⏺ Write(/tmp/test-permission-prompt.txt)
────────────────────────────────────────── (BLUE separator, rgb 177,185,249)
Create file
../../../../../tmp/test-permission-prompt.txt
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ (dotted ╌ U+254C, rgb 80,80,80)
1 hello
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
Do you want to create test-permission-prompt.txt?
❯ 1. Yes (❯ colored BLUE, rgb 177,185,249)
2. Yes, allow all edits in tmp/ during this session (shift+tab)
3. No
Esc to cancel · Tab to amend
| Feature | Normal | Permission prompt |
|---|---|---|
| Top separator | Gray ─ (rgb 136,136,136) | Blue ─ (rgb 177,185,249) |
| Content separator | None | Dotted ╌ (U+254C) |
❯ char | Gray prompt | Blue selection (rgb 177,185,249) |
| Mode line | ⏵⏵/⏸ on last row | None |
| Last line | Mode indicator | Esc to cancel · Tab to amend |
Custom Ink Menu (e.g., /wiz:setup)
────────────────────────────────────── (dim gray separator, \033[2m)
← ☐ Essential ☐ Workflow ☐ Tools ✔ Submit →
Select essential components to install:
❯ 1. [ ] notifications (checkbox, ❯ blue)
...
────────────────────────────────────── (dim gray separator)
6. Chat about this
Enter to select · Tab/Arrow keys to navigate · Esc to cancel
May clear the entire screen (\033[2J\033[3J\033[H).
Built-in Menu (/mcp)
───────────────────────────────────── (blue separator, rgb 177,185,249)
Manage MCP servers
❯ tuicommander · ✔ connected
context7 · ✔ connected
mac · ✘ failed
※ Run claude --debug to see error logs
https://code.claude.com/docs/en/mcp for help (OSC 8 hyperlink)
↑↓ to navigate · Enter to confirm · Esc to cancel (italic, \033[3m)
Built-in (/stats)
───────────────────────────────────────────────────────────────────────
Overview Models
Mar Apr May Jun ... Mar
··········░·▓██▓█·
...
You've used ~29x more tokens than The Count of Monte Cristo
Esc to cancel · r to cycle dates · ctrl+s to copy
Built-in (/status)
───────────────────────────────────────────────────────────────────────
Status Config Usage
╭───────────────────────────────────────────────────────────────────╮
│ ⌕ Search settings... │
╰───────────────────────────────────────────────────────────────────╯
Auto-compact true
...
↓ 3 more below
←/→ tab to switch · ↓ to return · Esc to close
Uses box-drawing ╭╮╰╯│ for search box (not matched by is_separator_line).
OSC 777 Notifications
CC emits terminal notifications for user attention events:
\033]777;notify;Claude Code;Claude needs your permission to use Write\007
\033]777;notify;Claude Code;Claude Code needs your attention\007
All share the prefix \033]777;notify;Claude Code;.
Ink Rendering Mechanics
Spinner animation (above separator)
\033[8A ← cursor UP 8 rows (to spinner position)
✶ ← overwrite spinner character
\r\n × 7 ← 7 empty newlines back down to bottom
The cursor-up count equals the bottom zone height:
- 8 = bypass mode (spinner + empty + sep + prompt + sep + 2 status + mode)
- 6 = default mode (spinner + empty + sep + prompt + sep + 1 status, no mode)
- 10 = with subprocess in bypass mode
Full bottom zone redraw
\r\033[1B separator ────
\r\033[1B ❯ [input]
\r\033[1B separator ────
\r\n \033[2C [status line 1] ← 2C = cursor forward 2 = indent
\r\n \033[2C [status line 2]
\r\n \033[2C [mode line]
Key implications
- All bottom-zone rows transit the PTY as ANSI sequences
- The vt100 parser processes them into screen buffer rows
- Spinner updates touch the spinner row AND all rows below it, causing the entire bottom zone to appear as changed rows
- The 2-space indent on status/mode lines comes from
\033[2C