Screen System: Designing Fullscreen Interaction Modes
A deep dive into Claude Code's fullscreen system — Doctor diagnostics, Resume session recovery, the REPL screen, and the switching mechanism between conversation and fullscreen modes
Setting the Stage
When you type claude to start a new session, the terminal is taken over by a fullscreen interactive interface — messages scroll in the upper area, the input box is fixed at the bottom, and the mouse wheel lets you browse history. When you type /doctor, the entire interface is replaced by a diagnostic panel. When you run claude --resume, a session picker appears, letting you browse and restore past conversations. Three completely different interaction modes, all running in the same terminal window.
How are these different "screens" organized? How do they switch between each other? What fundamentally distinguishes fullscreen mode from normal terminal output? How is the terminal's alternate screen buffer utilized? This article dives into Claude Code's src/screens/ directory to analyze the architecture of this screen system.
Core questions include:
- Screen directory architecture: How do the three screen files (REPL, Doctor, ResumeConversation) divide responsibilities? What patterns do they share?
- Fullscreen mechanism: How does the
AlternateScreencomponent use the terminal's DEC Private Mode 1049 to implement screen switching? - Lifecycle management: How is screen entry, rendering, interaction, exit, and state restoration orchestrated?
- Cross-screen switching: How does Doctor return to the REPL after diagnostics complete? How does Resume seamlessly transition to the REPL after a session is selected?
screens/ Directory Architecture
Three Core Screen Files
Claude Code's screen system lives in the src/screens/ directory, containing three core files:
The size difference between these three files reflects their scope of responsibility. REPL is the application's main battleground — message flow, tool execution, permission requests, scroll management, and shortcut handling all live here. Doctor is a one-shot diagnostic panel. ResumeConversation is a transitional screen that hands control to the REPL after selection is complete.
Shared Patterns Across Screens
Despite the vast difference in complexity between the three screens, they share several key architectural patterns:
Props-driven lifecycle callbacks. Each screen receives a callback function to signal "done":
The Doctor screen calls onDone when the user presses Enter; ResumeConversation renders an embedded REPL component after loading completes. This pattern makes screen composition declarative — the parent doesn't need to know about the screen's internal state machine.
React state management + async initialization. Each screen initiates async operations on mount (Doctor fetches diagnostic info, Resume loads history logs), driving state transitions from loading to ready via useState + useEffect.
KeybindingSetup wrapping. All screens run within a KeybindingSetup context, ensuring the shortcut system (Ctrl+C to exit, Enter to confirm, etc.) is available in every screen.
REPL Screen: The Core Interaction Hub
Component Scale and Responsibilities
REPL is Claude Code's most central screen and the largest single component file. Its import list spans over 280 lines, covering message management, tool execution, permission control, MCP connections, keybindings, scroll management, voice integration, and nearly every other subsystem.
The props REPL receives define the entire session's initial state — initial messages (for resume), tool list, MCP clients, system prompt, etc. These props are injected by main.tsx at the routing layer.
Fullscreen Layout: FullscreenLayout
REPL's core layout is managed by the FullscreenLayout component. This component divides the terminal viewport into two areas: a scrollable message area and an input area fixed at the bottom.
FullscreenLayout has two rendering paths. In fullscreen mode, it uses ScrollBox (with stickyScroll) as the message container, with the bottom area fixed via flexbox flexShrink={0}:
In non-fullscreen mode, all content renders sequentially, relying on the terminal's native scroll buffer. This is an important degradation path — used in tmux -CC mode or when the user explicitly disables fullscreen.
AlternateScreen: Terminal Double Buffering
The underlying foundation of fullscreen mode is the terminal's alternate screen buffer (DEC Private Mode 1049). The AlternateScreen component encapsulates this mechanism:
This code is worth analyzing line by line:
-
useInsertionEffectrather thanuseLayoutEffect: This is a critical detail. The React reconciler callsresetAfterCommitbetween mutation and layout commit, and Ink'sresetAfterCommittriggersonRender. IfuseLayoutEffectwere used, the firstonRenderwould fire before the effect — writing a full frame to the main screen. Insertion effects execute during the mutation phase, ensuring theENTER_ALT_SCREENsequence reaches the terminal before the first frame. -
height={size?.rows ?? 24}: Fixes the component height to the terminal row count. This is the core constraint for fullscreen mode — without this limit,ScrollBox'sflexGrowhas no upper bound, the viewport equals content height, andscrollTopis always 0. -
Cleanup function ordering: First
setAltScreenActive(false)notifies the Ink renderer, thenclearTextSelectionclears text selection state, and finally writesEXIT_ALT_SCREENto restore the main screen.
REPL wraps AlternateScreen at the root level:
Doctor Screen: /doctor Environment Diagnostics
Launch Path
The Doctor screen has two entry points:
- CLI subcommand:
claude doctorlaunches directly, routed todoctorHandlerthroughmain.tsx's Commander routing - REPL slash command: Typing
/doctorin conversation, where REPL renders the Doctor component in the current context
The CLI entry point's startup code demonstrates the standalone screen launch pattern:
Note the pattern here: create a Promise, pass resolve to the component's onDone callback. When the user presses Enter to close the diagnostic panel, the Promise resolves, then the component is unmounted and the process exits. Doctor as a CLI subcommand doesn't need AlternateScreen — it's a simple information display panel that relies on the terminal's native scrolling.
Diagnostic Data Collection
The Doctor component collects multi-dimensional diagnostic data in parallel on mount:
The collected diagnostic information is rendered across multiple panels:
| Panel | Content |
|---|---|
| Diagnostics | Version number, installation type, path, package manager, ripgrep status |
| Updates | Auto-update status, update permissions, stable/latest version numbers |
| Sandbox | Sandbox isolation status |
| MCP | MCP server configuration parse warnings |
| Keybindings | Keybinding configuration warnings |
| Environment Variables | Environment variable validation errors |
| Version Locks | PID lock file status, stale lock cleanup |
| Agent Parse Errors | Agent definition file parse failures |
| Plugin Errors | Plugin errors |
| Context Usage Warnings | CLAUDE.md size, agent context consumption |
All panels are wrapped with a Pane component, displaying Checking installation status... placeholder text before diagnostic data has loaded.
Exit Mechanism
Doctor uses the simplest exit pattern — a PressEnterToContinue component with keybinding listeners:
Both confirm:yes and confirm:no map to dismiss — whether the user presses Enter or Escape, the panel closes. In CLI mode this triggers process.exit(0); in REPL's /doctor mode this returns control to the conversation flow.
Resume Screen: Session Recovery UI
Progressive Log Loading
ResumeConversation is a transitional screen — its sole purpose is to let the user select a past session, then hand control to the REPL. But the seemingly simple feature of "selecting a past session" involves an elegant progressive loading design.
loadSameRepoMessageLogsProgressive only loads session logs from the same repository (including worktrees). Users can switch to "all projects" mode to load cross-repository logs:
Furthermore, when the user scrolls to the bottom of the list, loadMoreLogs loads additional history on demand:
Session Selection and Recovery Flow
When the user selects a session from LogSelector, it triggers the onSelect callback — the most complex part of the entire recovery flow:
Key steps in the recovery flow (L178-292):
-
Cross-project detection:
checkCrossProjectResumechecks whether the selected session originates from a different directory. For cross-project sessions, it displays a prompt command (already copied to clipboard) for the user to resume in the correct directory. -
Message loading:
loadConversationForResumedeserializes message history from JSONL files. -
Session switching:
switchSessionupdates the global session ID;restoreCostStateForSessionrestores the API call cost statistics for that session. -
Agent restoration:
restoreAgentFromSessionrestores agent definitions and color schemes based on session metadata. -
State transition:
setResumeDatatriggers a React re-render, and the conditional branch switches fromLogSelectorto the REPL component.
The conditional rendering logic for state transitions:
This is a state-machine-driven rendering pattern: loading -> LogSelector -> resuming -> REPL (or CrossProjectMessage). Each state corresponds to a completely different UI.
Screen Lifecycle: Entry / Render / Interact / Exit / State Restore
Lifecycle Model
The three screens each have different lifecycle models, but they can be abstracted into unified phases:
Doctor has the simplest lifecycle:
| Phase | Behavior |
|---|---|
| Init | mount, register keybindings |
| Loading | getDoctorDiagnostic() + parallel data fetching |
| Ready | Render diagnostic panel |
| Interaction | Only listens for Enter/Escape |
| Exiting | Calls onDone, triggers unmount |
ResumeConversation has a transitional lifecycle:
| Phase | Behavior |
|---|---|
| Init | mount |
| Loading | loadSameRepoMessageLogsProgressive() |
| Ready | Render LogSelector |
| Interaction | List navigation, search, cross-project switching |
| Exiting | Not a true exit — switches to rendering REPL |
REPL has a long-lived lifecycle:
| Phase | Behavior |
|---|---|
| Init | mount, enter alt screen, initialize 200+ state variables |
| Loading | Parallel loading of hooks, MCP connections, tool pool |
| Ready | FullscreenLayout rendering |
| Interaction | Infinite loop: user input -> API query -> tool execution -> message rendering |
| Exiting | Execute session end hooks -> exit alt screen -> unmount |
AlternateScreen Entry and Exit
Fullscreen mode entry and exit are accomplished through terminal escape sequences. On entry:
On exit:
The terminal maintains two independent screen buffers. The main screen's content is saved when entering the alt screen and automatically restored when exiting. This means that after Claude Code exits, the previous terminal content (command history, other programs' output) remains intact.
Switching Mechanism with the Main Conversation Flow
main.tsx Routing Layer
Screen switching occurs at two levels: routing at startup and mode switching at runtime.
Startup routing is controlled by main.tsx. It uses Commander.js to parse command-line arguments and determines which screen to render based on subcommands and flags:
launchRepl and launchResumeChooser encapsulate React tree construction and rendering:
Note the use of import() — both REPL and App are dynamically imported. This allows the Doctor subcommand to run without loading the REPL code, saving startup time.
Runtime Screen Switching
During REPL execution, the /doctor slash command doesn't replace the entire React tree — it renders the Doctor component within the REPL's existing context. Doctor's onDone callback injects the result as a system message into the conversation flow:
In this pattern, Doctor doesn't need its own AppStateProvider or KeybindingSetup — it reuses the REPL's existing context. This is also why Doctor's CLI entry point needs to manually wrap these Providers, while REPL's built-in /doctor does not.
Transcript mode (Ctrl+O) is another form of runtime switching. REPL switches rendered content within the same AlternateScreen:
This ensures the alt buffer remains unchanged during mode switching — the React reconciler sees the same type of AlternateScreen component and only updates the children.
Fullscreen Mode Environment Compatibility
tmux -CC Detection
Fullscreen mode is not available in all terminal environments. The trickiest compatibility issue comes from tmux's -CC mode (iTerm2 integration mode). In this mode, tmux's alt screen + mouse tracking can corrupt terminal state.
src/utils/fullscreen.ts implements multi-layer detection:
Detection has two layers:
- Environment variable heuristic (zero process overhead): If
TMUXis set andTERM_PROGRAMisiTerm.app, andTERMdoesn't start withscreen/tmux, then it's identified as-CCmode. - Synchronous tmux probe (~5ms): Only triggered in SSH scenarios (where
TERM_PROGRAMdoesn't propagate), querying tmux'sclient_control_modevariable viaspawnSync.
Using spawnSync (synchronous) rather than the async version is deliberate — this result determines whether to enter fullscreen. Async probing once caused a race condition: React had already rendered the alt screen before the probe completed, killing the user's mouse wheel.
Environment Variable Control
Fullscreen mode is controlled via the CLAUDE_CODE_NO_FLICKER environment variable:
Additional fine-grained controls include:
CLAUDE_CODE_DISABLE_MOUSE: Keeps alt screen but disables mouse tracking (solves copy conflicts in tmux)CLAUDE_CODE_DISABLE_MOUSE_CLICKS: Disables mouse clicks but keeps scroll wheel (prevents accidental clicks)
Portable Patterns
Claude Code's screen system provides several reusable design patterns for terminal applications:
Pattern 1: Props Callback-Driven Screen Lifecycle
Each screen receives an onDone callback, and the parent waits for completion via Promise. This pattern makes screens reusable in different contexts (standalone CLI command, REPL embedded, test environment).
Pattern 2: AlternateScreen Double Buffering
Leveraging the terminal alt screen buffer for fullscreen applications, with automatic content restoration on exit. The key is using useInsertionEffect to ensure escape sequences reach the terminal before the first frame renders.
Pattern 3: Progressive State Machine Rendering
ResumeConversation's loading -> selector -> resuming -> REPL state machine demonstrates how to implement multi-phase UI transitions within a single component, with each phase rendering a completely different component tree.
Pattern 4: Environment Compatibility Degradation
Fullscreen mode's multi-layer detection (environment variable heuristic -> synchronous process probe -> environment variable override) is a reusable terminal capability detection strategy. In environments that can't use fullscreen, the system degrades to sequential rendering mode with no loss of functionality.
Pattern 5: Screen Reuse and Context Inheritance
Doctor reuses REPL's Provider context (AppState, KeybindingSetup, MCPConnectionManager) when running inside the REPL, but brings its own Providers in CLI mode. This design allows the same component to work in both scenarios without modifying the component itself.
Summary
Claude Code's screen system is a carefully designed multi-layer architecture:
- Bottom layer: The
AlternateScreencomponent uses terminal DEC 1049 private mode for double buffering, withuseInsertionEffectguaranteeing correct timing - Layout layer:
FullscreenLayoutdivides the terminal viewport into a scrollable area and a fixed bottom area, using flexbox for adaptive layout - Screen layer: Three screen files each serve their purpose — REPL handles the main interaction loop, Doctor provides diagnostic information, and ResumeConversation manages session recovery transitions
- Routing layer:
main.tsxuses Commander routing to determine which screen to launch, withreplLauncher.tsxanddialogLaunchers.tsxencapsulating React tree construction - Compatibility layer: tmux
-CCdetection, environment variable control, and non-fullscreen degradation ensure the system works across various terminal environments
The biggest design insight of this system is: the terminal's alternate screen buffer is equivalent to a browser's "page navigation". Entering alt screen is like opening a new page (the old page is saved), and exiting is like pressing the back button (the old page is restored). Claude Code builds a React-driven screen management system on this foundation, giving terminal applications a multi-view experience similar to SPAs.