Kode-cli/docs/develop/modules/repl-interface.md
CrazyBoyM 7a3c4a7baa Refactor project structure and update documentation
- Update project branding from claude-cli to Kode
- Reorganize documentation with new development guides
- Add CONTRIBUTING.md and Chinese README
- Remove worktree_merge command and relocate system-design.md
- Update dependencies and package configuration
- Improve custom commands service with better error handling
- Clean up storage utilities and debug logging
2025-08-11 21:31:18 +08:00

13 KiB

REPL Interface Module

Overview

The REPL module (src/screens/REPL.tsx) provides the main interactive interface for Kode. It's a sophisticated React-based terminal UI that handles user input, displays responses, manages conversation state, and orchestrates the entire interactive experience.

Architecture

Component Structure

interface REPLProps {
  commands: Command[]
  initialPrompt?: string
  messageLogName: string
  shouldShowPromptInput: boolean
  verbose?: boolean
  tools: Tool[]
  safeMode?: boolean
  mcpClients?: MCPClient[]
  isDefaultModel: boolean
  initialMessages?: Message[]
  initialForkNumber?: number
}

export function REPL(props: REPLProps): JSX.Element {
  // State management
  const [messages, setMessages] = useState<Message[]>([])
  const [isLoading, setIsLoading] = useState(false)
  const [currentModel, setCurrentModel] = useState<Model>()
  
  // Conversation handling
  // UI rendering
  // Event handlers
}

State Management

Message State

interface ConversationState {
  messages: Message[]
  pendingMessages: Message[]
  streamingContent: string
  currentToolUse?: ToolUse
  error?: Error
}

const useConversationState = () => {
  const [state, dispatch] = useReducer(
    conversationReducer,
    initialState
  )
  
  const addMessage = (message: Message) => {
    dispatch({ type: 'ADD_MESSAGE', payload: message })
  }
  
  const updateStreamingContent = (content: string) => {
    dispatch({ type: 'UPDATE_STREAMING', payload: content })
  }
  
  return { state, addMessage, updateStreamingContent }
}

Model State

const useModelState = () => {
  const [modelPointer, setModelPointer] = useState('main')
  const [modelProfile, setModelProfile] = useState<ModelProfile>()
  const [isDefaultModel, setIsDefaultModel] = useState(true)
  
  const switchModel = async (pointer: string) => {
    const profile = await resolveModelProfile(pointer)
    setModelProfile(profile)
    setModelPointer(pointer)
  }
  
  return { modelProfile, switchModel, isDefaultModel }
}

User Input Handling

Prompt Input Component

const PromptInput: React.FC<{
  onSubmit: (input: string) => void
  isLoading: boolean
  multiline: boolean
}> = ({ onSubmit, isLoading, multiline }) => {
  const [value, setValue] = useState('')
  const [cursorPosition, setCursorPosition] = useState(0)
  
  const handleKeyPress = (key: string, event: KeyEvent) => {
    if (key === 'enter' && !event.shift) {
      if (!isLoading && value.trim()) {
        onSubmit(value)
        setValue('')
      }
    }
    // Handle other keys (arrows, backspace, etc.)
  }
  
  return (
    <Box flexDirection="column">
      <TextInput
        value={value}
        onChange={setValue}
        onKeyPress={handleKeyPress}
        placeholder={isLoading ? 'Processing...' : 'Enter prompt...'}
        isDisabled={isLoading}
      />
      {multiline && <MultilineEditor value={value} />}
    </Box>
  )
}

Command Processing

async function processUserInput(
  input: string,
  context: REPLContext
): Promise<void> {
  // Check for slash commands
  if (input.startsWith('/')) {
    await handleSlashCommand(input, context)
    return
  }
  
  // Check for special shortcuts
  if (input === '!!') {
    await retryLastCommand(context)
    return
  }
  
  // Process as AI conversation
  await handleAIConversation(input, context)
}

Message Rendering

Message Display Pipeline

const MessageRenderer: React.FC<{
  message: Message
  verbose: boolean
}> = ({ message, verbose }) => {
  switch (message.type) {
    case 'user':
      return <UserMessage message={message} />
      
    case 'assistant':
      return <AssistantMessage message={message} verbose={verbose} />
      
    case 'tool_use':
      return <ToolUseMessage message={message} />
      
    case 'tool_result':
      return <ToolResultMessage message={message} />
      
    case 'error':
      return <ErrorMessage message={message} />
      
    default:
      return null
  }
}

Streaming Response Display

const StreamingMessage: React.FC<{
  content: string
  isThinking?: boolean
}> = ({ content, isThinking }) => {
  const [displayContent, setDisplayContent] = useState('')
  const [cursor, setCursor] = useState(true)
  
  // Animate content appearance
  useEffect(() => {
    const chars = content.split('')
    let index = 0
    
    const interval = setInterval(() => {
      if (index < chars.length) {
        setDisplayContent(prev => prev + chars[index])
        index++
      } else {
        clearInterval(interval)
      }
    }, 10) // Typing animation speed
    
    return () => clearInterval(interval)
  }, [content])
  
  // Cursor blink
  useEffect(() => {
    const interval = setInterval(() => {
      setCursor(prev => !prev)
    }, 500)
    
    return () => clearInterval(interval)
  }, [])
  
  return (
    <Box>
      <Text color={isThinking ? 'gray' : 'white'}>
        {displayContent}
        {cursor && '█'}
      </Text>
    </Box>
  )
}

Conversation Management

Query Orchestration

async function executeQuery(
  prompt: string,
  context: REPLContext
): Promise<void> {
  const abortController = new AbortController()
  
  try {
    setIsLoading(true)
    
    // Add user message
    const userMessage = createUserMessage(prompt)
    addMessage(userMessage)
    
    // Execute query
    const stream = query({
      prompt,
      messages: context.messages,
      model: context.currentModel,
      tools: context.tools,
      abortSignal: abortController.signal,
      safeMode: context.safeMode
    })
    
    // Process stream
    for await (const event of stream) {
      await processStreamEvent(event, context)
    }
    
  } catch (error) {
    handleQueryError(error, context)
  } finally {
    setIsLoading(false)
  }
}

Stream Event Processing

async function processStreamEvent(
  event: QueryStreamEvent,
  context: REPLContext
): Promise<void> {
  switch (event.type) {
    case 'text_delta':
      updateStreamingContent(event.text)
      break
      
    case 'tool_request':
      await handleToolRequest(event.tool, context)
      break
      
    case 'tool_result':
      displayToolResult(event.result)
      break
      
    case 'thinking':
      if (context.showThinking) {
        displayThinking(event.content)
      }
      break
      
    case 'complete':
      finalizeResponse(context)
      break
      
    case 'error':
      handleStreamError(event.error, context)
      break
  }
}

Tool Integration

Tool Execution Display

const ToolExecutionDisplay: React.FC<{
  toolUse: ToolUse
  status: 'pending' | 'running' | 'complete' | 'error'
}> = ({ toolUse, status }) => {
  const getStatusIcon = () => {
    switch (status) {
      case 'pending': return '⏳'
      case 'running': return <Spinner />
      case 'complete': return '✅'
      case 'error': return '❌'
    }
  }
  
  return (
    <Box flexDirection="column" borderStyle="round" padding={1}>
      <Box>
        <Text bold>{getStatusIcon()} {toolUse.name}</Text>
      </Box>
      <Box marginTop={1}>
        <Text dim>{JSON.stringify(toolUse.input, null, 2)}</Text>
      </Box>
      {status === 'complete' && (
        <Box marginTop={1}>
          <Text color="green">Tool completed successfully</Text>
        </Box>
      )}
    </Box>
  )
}

Permission Requests

const PermissionRequestHandler: React.FC<{
  request: PermissionRequest
  onApprove: () => void
  onDeny: () => void
}> = ({ request, onApprove, onDeny }) => {
  const [showDetails, setShowDetails] = useState(false)
  
  return (
    <Box flexDirection="column" borderStyle="double" borderColor="yellow">
      <Text bold color="yellow">⚠️ Permission Required</Text>
      <Text>{request.description}</Text>
      
      {showDetails && (
        <Box marginTop={1}>
          <Text dim>{request.details}</Text>
        </Box>
      )}
      
      <Box marginTop={1}>
        <SelectInput
          items={[
            { label: 'Approve', value: 'approve' },
            { label: 'Deny', value: 'deny' },
            { label: 'View Details', value: 'details' }
          ]}
          onSelect={(item) => {
            switch (item.value) {
              case 'approve': onApprove(); break
              case 'deny': onDeny(); break
              case 'details': setShowDetails(true); break
            }
          }}
        />
      </Box>
    </Box>
  )
}

UI Features

Keyboard Shortcuts

const useKeyboardShortcuts = (context: REPLContext) => {
  useInput((input, key) => {
    // Global shortcuts
    if (key.ctrl && input === 'c') {
      handleCancel(context)
    }
    
    if (key.ctrl && input === 'l') {
      clearScreen()
    }
    
    if (key.ctrl && input === 'r') {
      searchHistory(context)
    }
    
    // Vim mode shortcuts
    if (context.vimMode) {
      handleVimKeys(input, key, context)
    }
  })
}

Status Bar

const StatusBar: React.FC<{
  model: Model
  cost: number
  mode: 'normal' | 'safe'
  isLoading: boolean
}> = ({ model, cost, mode, isLoading }) => {
  return (
    <Box justifyContent="space-between" width="100%">
      <Box>
        <Text dim>Model: </Text>
        <Text color="cyan">{model.name}</Text>
      </Box>
      
      <Box>
        <Text dim>Cost: </Text>
        <Text color={cost > 1 ? 'red' : 'green'}>${cost.toFixed(4)}</Text>
      </Box>
      
      <Box>
        {mode === 'safe' && <Text color="yellow">🛡️ Safe Mode</Text>}
        {isLoading && <Spinner />}
      </Box>
    </Box>
  )
}

History Management

Conversation History

class ConversationHistory {
  private history: Message[][] = []
  private currentIndex: number = -1
  
  save(messages: Message[]): void {
    this.history.push([...messages])
    this.currentIndex = this.history.length - 1
  }
  
  navigate(direction: 'prev' | 'next'): Message[] | null {
    if (direction === 'prev' && this.currentIndex > 0) {
      this.currentIndex--
      return this.history[this.currentIndex]
    }
    
    if (direction === 'next' && this.currentIndex < this.history.length - 1) {
      this.currentIndex++
      return this.history[this.currentIndex]
    }
    
    return null
  }
  
  search(query: string): Message[][] {
    return this.history.filter(messages =>
      messages.some(m => m.content.includes(query))
    )
  }
}

Log Persistence

async function saveConversationLog(
  messages: Message[],
  logName: string
): Promise<void> {
  const logPath = path.join(CACHE_DIR, 'messages', `${logName}.json`)
  
  const logData = {
    timestamp: new Date().toISOString(),
    messages: messages.map(sanitizeMessage),
    metadata: {
      model: getCurrentModel(),
      cost: calculateCost(messages),
      duration: getSessionDuration()
    }
  }
  
  await fs.writeFile(logPath, JSON.stringify(logData, null, 2))
}

Error Handling

Error Display

const ErrorDisplay: React.FC<{ error: Error }> = ({ error }) => {
  const [showDetails, setShowDetails] = useState(false)
  
  return (
    <Box flexDirection="column" borderStyle="round" borderColor="red">
      <Text color="red" bold> Error</Text>
      <Text>{error.message}</Text>
      
      {showDetails && (
        <Box marginTop={1} flexDirection="column">
          <Text dim>Stack trace:</Text>
          <Text dim wrap="wrap">{error.stack}</Text>
        </Box>
      )}
      
      <Box marginTop={1}>
        <Text dim>
          Press 'd' for details, 'r' to retry, 'c' to continue
        </Text>
      </Box>
    </Box>
  )
}

Recovery Options

function handleError(
  error: Error,
  context: REPLContext
): RecoveryAction {
  if (error.name === 'AbortError') {
    return { type: 'cancelled' }
  }
  
  if (error.name === 'RateLimitError') {
    return {
      type: 'switch_model',
      suggestion: 'Switch to a different model?'
    }
  }
  
  if (error.name === 'ContextLengthError') {
    return {
      type: 'compact_context',
      suggestion: 'Compact conversation history?'
    }
  }
  
  return {
    type: 'retry',
    suggestion: 'Retry the operation?'
  }
}

Performance Optimizations

Virtual Scrolling

const MessageList: React.FC<{
  messages: Message[]
  height: number
}> = ({ messages, height }) => {
  const [visibleRange, setVisibleRange] = useState({ start: 0, end: 50 })
  
  const handleScroll = (offset: number) => {
    const start = Math.floor(offset / MESSAGE_HEIGHT)
    const end = start + Math.ceil(height / MESSAGE_HEIGHT)
    setVisibleRange({ start, end })
  }
  
  const visibleMessages = messages.slice(
    visibleRange.start,
    visibleRange.end
  )
  
  return (
    <VirtualScroll
      height={height}
      itemCount={messages.length}
      itemHeight={MESSAGE_HEIGHT}
      onScroll={handleScroll}
    >
      {visibleMessages.map(msg => (
        <MessageRenderer key={msg.id} message={msg} />
      ))}
    </VirtualScroll>
  )
}

Memoization

const MemoizedMessage = React.memo(
  MessageRenderer,
  (prevProps, nextProps) => {
    // Only re-render if message content changes
    return prevProps.message.content === nextProps.message.content &&
           prevProps.verbose === nextProps.verbose
  }
)

The REPL module provides a sophisticated, responsive, and user-friendly interface for AI conversations with comprehensive state management, error handling, and performance optimizations.