Studio Notebook

Claude Code Atlas

Interaction Shell And App State

Learn how the REPL, UI components, and shared state produce the interactive experience.

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.

main.tsx seeds state launchRepl() loads wrapper REPL.tsx stays live

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

  1. launch-repl-and-app-wrapper Start with the handoff from main.tsx into launchRepl(), because that is where startup stops and the interactive shell begins.
  2. app-state-shape-and-defaults Meet the AppState object and the default values that seed the REPL before any user input arrives.
  3. app-state-provider-and-store-bridge Learn how the provider, tiny store, and change hook expose state to React without turning the whole app into one giant re-render.
  4. repl-screen-state-slices-and-input-routing See how REPL.tsx reads many small state slices and uses them to drive tasks, overlays, and prompt input.