Why this matters
Claude Code becomes interactive in three steps. main.tsx seeds the session
state, launchRepl() loads the shell wrapper, and REPL.tsx becomes the
long-lived screen that keeps reading and updating that state.
Interaction shell handoff
Startup seeds state, launchRepl loads the wrapper, and REPL stays live for the session.
From Startup Handoff To Live Shell
The important boundary is launchRepl(). Before that call, main.tsx is still
doing startup work and assembling the first session state. At that call, startup
hands ownership to the interaction shell. After that, the shell stays alive and
REPL.tsx keeps reading the state that was already seeded.
That is why this part is not “one giant screen file.” It is a wrapper around
state plus REPL, with launchRepl() sitting between boot-time setup and the
live screen.
The session stays interactive because the state is created first, then passed into the shell before the screen starts reading it.
The field-by-field tour belongs in app-state-shape-and-defaults.
const initialState: AppState = {
settings: getInitialSettings(),
tasks: {},
agentNameRegistry: new Map(),
verbose: verbose ?? getGlobalConfig().verbose ?? false,
mainLoopModel: initialMainLoopModel,
mainLoopModelForSession: null,
isBriefOnly: initialIsBriefOnly,
expandedView: getGlobalConfig().showSpinnerTree ? 'teammates' : getGlobalConfig().showExpandedTodos ? 'tasks' : 'none',
showTeammateMessagePreview: isAgentSwarmsEnabled() ? false : undefined,
selectedIPAgentIndex: -1,
coordinatorTaskIndex: -1,
viewSelectionMode: 'none',
footerSelection: null,
toolPermissionContext: effectiveToolPermissionContext,
agent: mainThreadAgentDefinition?.agentType,
agentDefinitions,
mcp: {
clients: [],
tools: [],
commands: [],
resources: {},
pluginReconnectKey: 0
},
plugins: {
enabled: [],
disabled: [],
commands: [],
errors: [],
installationStatus: {
marketplaces: [],
plugins: []
},
needsRefresh: false
},
statusLineText: undefined,
await launchRepl(root, {
getFpsMetrics,
stats,
initialState
}, {
...sessionConfig,
initialMessages,
pendingHookMessages
}, renderAndRun);
launchRepl() does the handoff without hiding the app structure. It lazy-loads
the wrapper pieces, then renders the shell by nesting REPL inside App.
export async function launchRepl(root: Root, appProps: AppWrapperProps, replProps: REPLProps, renderAndRun: (root: Root, element: React.ReactNode) => Promise<void>): Promise<void> {
const {
App
} = await import('./components/App.js');
const {
REPL
} = await import('./screens/REPL.js');
await renderAndRun(root, <App {...appProps}>
<REPL {...replProps} />
</App>);
}
How this part breaks down
launch-repl-and-app-wrapperStart with the handoff frommain.tsxintolaunchRepl(), because that is where startup stops and the interactive shell begins.app-state-shape-and-defaultsMeet theAppStateobject and the default values that seed the REPL before any user input arrives.app-state-provider-and-store-bridgeLearn how the provider, tiny store, and change hook expose state to React without turning the whole app into one giant re-render.repl-screen-state-slices-and-input-routingSee howREPL.tsxreads many small state slices and uses them to drive tasks, overlays, and prompt input.