InkdownInkdown
Start writing

Claude-Code

62 files·4 subfolders

Shared Workspace

Claude-Code
codex

06-ui-components

Shared from "Claude-Code" on Inkdown

UI Components Architecture

Overview

Claude Code's UI is built with Ink - a React renderer for terminals. It provides a component-based architecture similar to web React, but outputting to the terminal instead of the DOM.

Plain text
┌─────────────────────────────────────────────────────────────────────────────┐
│                        UI ARCHITECTURE                                       │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                      REACT + INK STACK                              │   │
│  │                                                                       │   │
│  │   ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐       │   │
│  │   │  React   │───►│  Ink     │───►│ Yoga     │───►│ ANSI     │       │   │
│  │   │Components│    │Renderer  │    │Layout    │    │Output   │       │   │
│  │   └──────────┘    └──────────┘    └──────────┘    └──────────┘       │   │
│  │        │              │              │              │                  │   │
│  │        ▼              ▼              ▼              ▼                  │   │
│  │   JSX/TSX       Reconciler      Flexbox       Terminal                 │   │
│  │   Components    (diffing)       Layout        Escape Codes             │   │
│  │                                                                       │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                      COMPONENT HIERARCHY                              │   │
│  │                                                                       │   │
│  │                         ┌──────────┐                                 │   │
│  │                         │   App    │                                 │   │
│  │                         └────┬─────┘                                 │   │
│  │                              │                                        │   │
│  │              ┌───────────────┼───────────────┐                      │   │
│  │              ▼               ▼               ▼                      │   │
│  │        ┌────────┐    ┌────────┐    ┌────────────┐                 │   │
│  │        │  REPL  │    │ Dialogs│    │ Fullscreen │                 │   │
│  │        │        │    │        │    │   Layouts   │                 │   │
│  │        └───┬────┘    └────────┘    └────────────┘                 │   │
│  │            │                                                         │   │
│  │    ┌───────┴───────┐                                                │   │
│  │    ▼               ▼                                                │   │
│  │ ┌────────┐   ┌──────────┐                                          │   │
│  │ │Prompt  │   │ Messages │                                          │   │
│  │ │Input   │   │ Panel    │                                          │   │
│  │ └────┬───┘   └────┬─────┘                                          │   │
│  │      │            │                                                │   │
│  │      ▼            ▼                                                 │   │
│  │  TextInput    MessageList                                           │   │
│  │                                                                         │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
0000_start_here_index_and_recommended_reading_order.md
0100_project_overview_tech_stack_runtime_modes_and_folder_map.md
0200_startup_flow_entry_points_and_cold_start_sequence.md
0300_codebase_modules_layers_state_models_and_schemas.md
0400_system_architecture_and_design_rationale.md
0500_interactive_repl_request_flow_end_to_end.md
0600_headless_sdk_and_print_mode_request_flow_end_to_end.md
0700_mcp_integration_connection_and_tool_call_flow.md
0800_external_services_sdks_storage_and_local_dependencies.md
0900_environment_variables_settings_feature_flags_and_failure_modes.md
1000_non_obvious_patterns_gotchas_and_debugging_traps.md
1100_full_codebase_file_inventory_grouped_by_directory.md
kimi
00-overview.md
01-entrypoints.md
02-state-management.md
03-query-system.md
04-tools-system.md
05-tasks-system.md
06-ui-components.md
07-bridge-remote.md
08-services.md
09-skills-plugins.md
10-commands.md
11-testing-architecture.md
12-permission-system.md
13-build-system.md
14-ink-internals.md
15-git-internals.md
16-context-compaction.md
17-vim-mode.md
18-mailbox-notifications.md
19-session-persistence.md
20-hooks-system.md
21-error-recovery.md
README.md
qwen
00-overview.md
01-entry-points.md
02-query-engine.md
03-tools-and-tasks.md
04-commands-and-skills.md
05-state-management.md
06-ink-rendering.md
07-bridge-remote.md
08-mcp-services.md
09-services-overview.md
10-multi-agent.md
11-system-prompt-constants.md
12-tool-interface.md
13-memory-system.md
14-buddy-companion.md
15-keybindings.md
16-stop-hooks.md
17-vim-mode.md
18-upstreamproxy.md
19-cost-tracking-history.md
20-contexts-styles-onboarding.md
21-hooks.md
22-screens.md
tweets-explain
claude-code-memory-analysis.md
compact
memory-system
agentic-architecture

Core UI Files

Directory/FilePurpose
ink/Ink framework internals
components/App.tsxRoot application component
components/REPL.tsxMain REPL interface
components/PromptInput.tsxUser input area
components/Messages/Message display components
context/React contexts (notifications, modals)
hooks/React hooks for UI logic

Ink Framework (Custom)

Claude Code uses a customized Ink in src/ink/:

Plain text
ink/
├── components/           # Ink built-in components
│   ├── App.tsx          # Ink app container
│   ├── Box.tsx          # Flexbox container
│   ├── Text.tsx         # Text rendering
│   ├── ScrollBox.tsx   # Scrollable area
│   └── ...
├── hooks/               # Ink hooks
│   ├── use-input.ts    # Keyboard input
│   ├── use-stdin.ts    # Stdin access
│   └── ...
├── layout/             # Layout engine
│   ├── engine.ts       # Layout calculation
│   ├── yoga.ts         # Yoga (flexbox) integration
│   └── node.ts         # Layout nodes
├── reconciler.ts       # React reconciler
└── render-to-screen.ts # Output rendering
Key Ink Components
Box (Flexbox Layout)
TypeScript
// Like CSS flexbox but for terminal
<Box
  flexDirection="column"    // row | column
  justifyContent="center"   // flex-start | center | flex-end
  alignItems="center"       // flex-start | center | flex-end
  padding={1}               // Padding in cells
  borderStyle="round"       // single | double | round
>
  <Text>Content</Text>
</Box>
Text (Styled Text)
TypeScript
<Text
  color="green"           # Named color
  backgroundColor="blue"  # Background
  bold                    # Styles
  italic
  underline
  dimColor               # Low intensity
>
  Hello World
</Text>
ScrollBox (Scrollable)
TypeScript
<ScrollBox
  height={20}             # Fixed height
  scrollTop={scrollPos}   # Controlled scroll
  onScroll={setScrollPos}
>
  <Text>{longContent}</Text>
</ScrollBox>

Main Application Components

App.tsx

Root component that sets up providers:

TypeScript
export default function App({ config }: { config: AppConfig }) {
  return (
    <AppStateProvider initialState={config.initialState}>
      <MailboxProvider>
        <VoiceProvider>
          <ModalProvider>
            <NotificationProvider>
              <ErrorBoundary>
                <AppContent config={config} />
              </ErrorBoundary>
            </NotificationProvider>
          </ModalProvider>
        </VoiceProvider>
      </MailboxProvider>
    </AppStateProvider>
  )
}
REPL.tsx

The main REPL (Read-Eval-Print Loop) interface:

TypeScript
function REPL() {
  const messages = useAppState(s => s.messages)
  const tasks = useAppState(s => s.tasks)
  const [input, setInput] = useState('')

  return (
    <FullscreenLayout>
      {/* Messages Area */}
      <Box flexDirection="column" flexGrow={1}>
        <MessageList messages={messages} />
      </Box>

      {/* Task Footer */}
      {hasActiveTasks(tasks) && <TaskPanel tasks={tasks} />}

      {/* Input Area */}
      <PromptInput
        value={input}
        onChange={setInput}
        onSubmit={handleSubmit}
      />

      {/* Status Line */}
      <StatusLine />
    </FullscreenLayout>
  )
}

Message Rendering

Message Types
TypeScript
// Message display by type
function MessageItem({ message }: { message: Message }) {
  switch (message.type) {
    case 'user':
      return <UserMessageView message={message} />
    case 'assistant':
      return <AssistantMessageView message={message} />
    case 'tool_use':
      return <ToolUseView toolUse={message} />
    case 'tool_result':
      return <ToolResultView result={message} />
    case 'system':
      return <SystemMessageView message={message} />
    default:
      return <UnknownMessageView message={message} />
  }
}
Assistant Message (Streaming)
TypeScript
function AssistantMessageView({ message }: { message: AssistantMessage }) {
  const { content, thinking } = message

  return (
    <Box flexDirection="column">
      {/* Logo/Avatar */}
      <Logo />

      {/* Thinking block (collapsible) */}
      {thinking && <ThinkingBlock content={thinking} />}

      {/* Main content */}
      <MarkdownText content={content} />
    </Box>
  )
}
Tool Use Display
TypeScript
function ToolUseView({ toolUse }: { toolUse: ToolUseBlock }) {
  const tool = findToolByName(toolUse.name)
  const status = getToolStatus(toolUse.id)

  return (
    <Box flexDirection="column" borderStyle="single">
      {/* Header */}
      <Box>
        <Text color="blue">●</Text>
        <Text bold>{toolUse.name}</Text>
        {status === 'running' && <Spinner />}
      </Box>

      {/* Tool-specific UI */}
      {tool?.renderUI ? (
        tool.renderUI({
          input: toolUse.input,
          status,
        })
      ) : (
        <DefaultToolView input={toolUse.input} />
      )}
    </Box>
  )
}

Prompt Input

BaseTextInput.tsx

The core text input component:

TypeScript
export function BaseTextInput({
  value,
  onChange,
  onSubmit,
  onExit,
  placeholder,
  focus,
}: TextInputProps) {
  const { stdin, setRawMode } = useStdin()

  useInput((input, key) => {
    if (key.return) {
      onSubmit(value)
    } else if (key.escape) {
      onExit()
    } else if (key.backspace || key.delete) {
      onChange(value.slice(0, -1))
    } else {
      onChange(value + input)
    }
  })

  return (
    <Box>
      <Text color="green">❯</Text>
      <Text>{value || placeholder}</Text>
      {focus && <Cursor />}
    </Box>
  )
}
PromptInput.tsx (Full)

Complete input with suggestions, history, etc.:

TypeScript
export function PromptInput({
  onSubmit,
}: {
  onSubmit: (value: string) => void
}) {
  const [input, setInput] = useState('')
  const suggestions = useInputSuggestions(input)
  const history = useArrowKeyHistory()

  return (
    <Box flexDirection="column">
      {/* Input line */}
      <Box>
        <Text color="green">❯</Text>
        <BaseTextInput
          value={input}
          onChange={setInput}
          onSubmit={onSubmit}
          focus
        />
      </Box>

      {/* Suggestions */}
      {suggestions.length > 0 && (
        <SuggestionsList items={suggestions} />
      )}

      {/* Footer with hints */}
      <PromptFooter />
    </Box>
  )
}

Fullscreen Layouts

FullscreenLayout.tsx

Manages fullscreen dialogs and overlays:

TypeScript
export function FullscreenLayout({ children }: { children: ReactNode }) {
  const modal = useModal()
  const { width, height } = useTerminalSize()

  return (
    <Box flexDirection="column" width={width} height={height}>
      {/* Main content */}
      <Box flexGrow={1}>{children}</Box>

      {/* Modal overlay */}
      {modal.isOpen && (
        <Box
          position="absolute"
          top={0}
          left={0}
          width={width}
          height={height}
          backgroundColor="black"
        >
          <ModalContent {...modal} />
        </Box>
      )}

      {/* Notifications */}
      <NotificationArea />
    </Box>
  )
}

Keybindings System

useGlobalKeybindings.tsx
TypeScript
export function useGlobalKeybindings() {
  const setAppState = useSetAppState()
  const currentView = useAppState(s => s.expandedView)

  useInput((input, key) => {
    // Ctrl+C - Cancel/Exit
    if (key.ctrl && input === 'c') {
      handleCancel()
    }

    // Ctrl+T - Toggle task view
    if (key.ctrl && input === 't') {
      setAppState(prev => ({
        ...prev,
        expandedView: prev.expandedView === 'tasks' ? 'none' : 'tasks',
      }))
    }

    // Ctrl+V - Toggle verbose
    if (key.ctrl && input === 'v') {
      setAppState(prev => ({ ...prev, verbose: !prev.verbose }))
    }

    // / - Start command
    if (input === '/' && !isTyping()) {
      showCommandMenu()
    }
  })
}
Vim Mode
TypeScript
// vim/motions.ts, vim/operators.ts
export function useVimMode() {
  const [mode, setMode] = useState<'normal' | 'insert'>('normal')
  const [buffer, setBuffer] = useState('')

  useInput((input, key) => {
    if (mode === 'normal') {
      // h/j/k/l navigation
      // dd - delete line
      // yy - yank line
      // p - paste
      // etc.
    }
  })
}

Notification System

notifications.tsx
TypeScript
export type Notification = {
  id: string
  type: 'info' | 'success' | 'warning' | 'error'
  title: string
  message?: string
  timeout?: number
}

export function NotificationProvider({ children }) {
  const [notifications, setNotifications] = useState<Notification[]>([])

  const showNotification = (notification: Omit<Notification, 'id'>) => {
    const id = generateId()
    setNotifications(prev => [...prev, { ...notification, id }])

    // Auto-dismiss
    if (notification.timeout) {
      setTimeout(() => {
        dismissNotification(id)
      }, notification.timeout)
    }
  }

  return (
    <NotificationContext.Provider value={{ showNotification, dismissNotification }}>
      {children}
      <NotificationArea notifications={notifications} />
    </NotificationContext.Provider>
  )
}

Dialog System

Dialog Components

Common dialog pattern:

TypeScript
// components/SomeDialog.tsx
export function SomeDialog({
  isOpen,
  onClose,
  onConfirm,
}: DialogProps) {
  if (!isOpen) return null

  return (
    <Box
      flexDirection="column"
      borderStyle="round"
      padding={1}
    >
      <Text bold>Dialog Title</Text>
      <Text>Dialog content...</Text>

      <Box>
        <Button onPress={onClose}>Cancel</Button>
        <Button onPress={onConfirm}>Confirm</Button>
      </Box>
    </Box>
  )
}
Permission Dialog
TypeScript
export function ToolPermissionDialog({
  tool,
  input,
  onAllow,
  onDeny,
  onAllowAlways,
}: PermissionDialogProps) {
  return (
    <Box borderStyle="double" borderColor="yellow">
      <Text bold color="yellow">
        ⚠️ Permission Required
      </Text>

      <Text>Claude wants to run:</Text>
      <Box paddingLeft={2}>
        <Text bold>{tool.name}</Text>
        <Text>{JSON.stringify(input, null, 2)}</Text>
      </Box>

      <Text>Allow this action?</Text>

      <Box>
        <Button onPress={() => onDeny()}>Deny</Button>
        <Button onPress={() => onAllow()}>Allow Once</Button>
        <Button onPress={() => onAllowAlways()}>Always Allow</Button>
      </Box>
    </Box>
  )
}

Styling System

Theme Integration
TypeScript
// utils/theme.ts
export type Theme = {
  colors: {
    primary: string
    secondary: string
    success: string
    error: string
    warning: string
    info: string
  }
}

// components use theme
const theme = useTheme()

return <Text color={theme.colors.primary}>Primary text</Text>
Color Utilities
TypeScript
// ink/colorize.ts
export function colorize(text: string, styles: Style[]): string {
  const codes = styles.map(s => STYLE_CODES[s])
  return `\x1b[${codes.join(';')}m${text}\x1b[0m`
}

// Usage
colorize('Hello', ['bold', 'green'])  // "\x1b[1;32mHello\x1b[0m"

Performance Patterns

1. Selective Subscriptions
TypeScript
// ❌ BAD - Re-renders on any state change
const state = useAppState(s => s)

// ✅ GOOD - Only re-renders when messages change
const messages = useAppState(s => s.messages)
2. Memoization
TypeScript
const processedMessages = useMemo(
  () => processMessages(messages),
  [messages]
)
3. Virtual Scrolling
TypeScript
// For long message lists
function MessageList({ messages }: { messages: Message[] }) {
  const { height } = useTerminalSize()
  const visibleCount = height - 10

  // Only render visible messages
  const visibleMessages = messages.slice(-visibleCount)

  return (
    <Box flexDirection="column">
      {visibleMessages.map(m => (
        <MessageItem key={m.uuid} message={m} />
      ))}
    </Box>
  )
}

Common UI Patterns

Spinner/Loading
TypeScript
function Spinner() {
  const [frame, setFrame] = useState(0)
  const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']

  useInterval(() => {
    setFrame(f => (f + 1) % frames.length)
  }, 80)

  return <Text color="blue">{frames[frame]}</Text>
}
Progress Bar
TypeScript
function ProgressBar({
  percent,
  width = 20,
}: {
  percent: number
  width?: number
}) {
  const filled = Math.round((percent / 100) * width)
  const empty = width - filled

  return (
    <Text>
      {'█'.repeat(filled)}
      {'░'.repeat(empty)}
      {' '}{percent.toFixed(0)}%
    </Text>
  )
}
Collapsible Section
TypeScript
function Collapsible({
  title,
  children,
}: {
  title: string
  children: ReactNode
}) {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <Box flexDirection="column">
      <Box onPress={() => setIsOpen(!isOpen)}>
        <Text>{isOpen ? '▼' : '▶'}</Text>
        <Text bold>{title}</Text>
      </Box>
      {isOpen && <Box paddingLeft={2}>{children}</Box>}
    </Box>
  )
}

Key Concepts

1. Flexbox for Terminal

Ink uses Yoga (Facebook's flexbox engine) for layout. Think CSS but for character cells.

2. Component = Function

Just like React web - components are functions returning JSX.

3. Hooks Work the Same

useState, useEffect, useContext - all work like web React.

4. Input Handling

useInput hook provides keyboard events. Not DOM events - raw stdin.

5. No Browser APIs

No document, window, localStorage. Use Node.js APIs instead.

6. ANSI Escape Codes

Colors, cursor movement, clearing - all manual ANSI sequences.


Related Documentation

  • State Management - How UI connects to state
  • Tools System - Tool UI rendering
  • Tasks System - Task panel UI