Claude Code 源码分析Claude Code 源码分析
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
  • 概览

    • Claude Code 源码分析
    • 源码统计
    • CodeGraph 图谱
  • 架构

    • 系统架构
    • UML 图表索引
    • 查询引擎
    • 核心流程
    • 消息系统
    • 状态管理
  • 功能模块

    • 工具系统
    • 斜杠命令
    • 服务层
    • MCP 协议
    • Skills 技能
    • 子代理系统
  • 分层深度

    • 入口层
    • UI / Ink 层
    • utils 基础设施
    • 桥接 / 远程
    • 上下文压缩
  • 原理与安全

    • 底层原理
    • 技术难点
    • 权限与安全
    • 内部机制
    • 遥测与分析
  • 深度专题

    • Hooks 系统
    • 插件系统
    • 记忆系统
    • API 通信层
    • Ink 终端 UI
    • 认证系统
    • 构建与发布
    • 术语表
  • 调用分析

    • 调用链分析
    • 核心文件索引
  • 模块详解

    • utils

      • 模块: utils
      • messages · 消息工厂与规范化
      • session-storage · JSONL 会话持久化
      • permissions · 工具权限决策
      • shell-hooks · 用户 Shell Hook 系统
    • components

      • 模块: components
      • REPL · 主屏编排
      • messages · 消息行渲染
      • PermissionRequest · 权限弹窗
      • PromptInput · 底部输入
    • services

      • 模块: services
      • api-claude · Anthropic API 流式与重试
      • mcp-client · MCP 连接与工具调用
      • compact · 上下文压缩与自动触发
      • analytics · GrowthBook、Datadog 与 1P 事件
    • tools

      • 模块: tools
      • tool-interface · Tool 契约与注册表
      • bash-tool · Shell 执行与权限
      • streaming-executor · 流式工具并发调度
      • agent-tool · 子 Agent 委派
    • commands

      • 模块: commands
      • command-registry · commands.ts 注册与分派
      • model-command · /model 模型选择
      • mcp-commands · /mcp 服务器管理
      • compact-memory-commands · /compact 与 /memory
    • ink

      • 模块: ink
      • Ink 渲染管线 · Screen 与终端输出
      • 终端事件 · resize、paste、stdin
      • Ink Hooks · 输入、搜索、终端状态
      • Ink 组件 · Box、Text、ScrollBox 原语
    • hooks

      • 模块: hooks
      • useCanUseTool · 权限 UI 接缝
      • 输入与快捷键 Hook
      • 合并态 Hook(MCP + 本地)
      • notifs 通知 Hook
    • bridge

      • 模块: bridge
      • repl-bridge · REPL 桥初始化与传输
      • bridge-messaging · 桥消息路由与入站处理
      • remote-bridge-core · env-less 核心与守护主循环
      • bridge-permissions-ui · 权限、API 与 TUI
    • cli

      • 模块: cli
      • Structured IO · NDJSON SDK 协议
      • CLI Transports · Session Ingress 传输层
      • CLI Handlers · 子命令懒加载实现
      • Update & Upload · 自更新与串行上传原语
    • screens

      • 模块: screens
      • REPL 屏 · Screen 类型与顶层路由
      • ResumeConversation · 会话恢复选择器
      • Doctor · 安装诊断全屏
    • entrypoints

      • 模块: entrypoints
      • cli-entrypoint · Bootstrap 与快路径
      • sdk-types · core / control / runtime 类型体系
      • mcp-entrypoint · MCP stdio 服务器
      • sandbox-types · 沙箱配置单一真相源
    • skills

      • 模块: skills
      • skills-loading · 磁盘加载与 bundled 注册表
      • bundled-skills · 内置 skill 与 initBundledSkills
      • mcp-skills · MCP prompt 转 skill
      • skill-tool-integration · SkillTool 与命令注册
    • types

      • 模块: types
      • message-types · Message 联合与 content blocks
      • tool-permission-types · Tool、Permission、Command 类型
      • api-sdk-types · API 与 Hooks 协议类型
      • misc-types · ids、plugin、generated 与其余类型
    • tasks

      • 模块: tasks
      • local-agent-task · 本地 Agent 与主会话后台化
      • remote-agent-task · 远程 CCR 与 In-Process Teammate
      • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
      • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybindings

      • 模块: keybindings
      • keybinding-registry · 注册、Provider 与 useKeybinding
      • default-bindings · 默认键位表与平台差异
      • command-bindings · command:* 动态斜杠命令绑定
      • vim-bindings · Vim 模式与 keybindings 边界
    • memdir

      • 模块: memdir
      • memdir-core · 路径、加载与 MEMORY.md
      • memory-extraction · extractMemories 与 SessionMemory
      • memdir-commands · /memory、/remember 与命令集成
    • state

      • 模块: state
      • app-state-core · store、AppState 类型与 Provider
      • app-state-selectors · selectors 与 onChangeAppState
      • teammate-state · 队友视图与 swarm 状态
      • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query

      • 模块: query
      • query config 与 deps · 配置快照与依赖注入
      • query tokenBudget · +500k 自动续跑
      • query transitions · Continue / Terminal 状态机
      • query stopHooks · Stop 事件与 turn 结束编排
  • 模块详解(扩展)

    • messages · 消息工厂与规范化
    • session-storage · JSONL 会话持久化
    • permissions · 工具权限决策
    • shell-hooks · 用户 Shell Hook 系统
    • REPL · 主屏编排
    • messages · 消息行渲染
    • PermissionRequest · 权限弹窗
    • PromptInput · 底部输入
    • api-claude · Anthropic API 流式与重试
    • mcp-client · MCP 连接与工具调用
    • compact · 上下文压缩与自动触发
    • analytics · GrowthBook、Datadog 与 1P 事件
    • tool-interface · Tool 契约与注册表
    • bash-tool · Shell 执行与权限
    • streaming-executor · 流式工具并发调度
    • agent-tool · 子 Agent 委派
    • command-registry · commands.ts 注册与分派
    • model-command · /model 模型选择
    • mcp-commands · /mcp 服务器管理
    • compact-memory-commands · /compact 与 /memory
    • Ink 渲染管线 · Screen 与终端输出
    • 终端事件 · resize、paste、stdin
    • Ink Hooks · 输入、搜索、终端状态
    • Ink 组件 · Box、Text、ScrollBox 原语
    • useCanUseTool · 权限 UI 接缝
    • 输入与快捷键 Hook
    • 合并态 Hook(MCP + 本地)
    • notifs 通知 Hook
    • repl-bridge · REPL 桥初始化与传输
    • bridge-messaging · 桥消息路由与入站处理
    • remote-bridge-core · env-less 核心与守护主循环
    • bridge-permissions-ui · 权限、API 与 TUI
    • Structured IO · NDJSON SDK 协议
    • CLI Transports · Session Ingress 传输层
    • CLI Handlers · 子命令懒加载实现
    • Update & Upload · 自更新与串行上传原语
    • REPL 屏 · Screen 类型与顶层路由
    • ResumeConversation · 会话恢复选择器
    • Doctor · 安装诊断全屏
    • cli-entrypoint · Bootstrap 与快路径
    • sdk-types · core / control / runtime 类型体系
    • mcp-entrypoint · MCP stdio 服务器
    • sandbox-types · 沙箱配置单一真相源
    • skills-loading · 磁盘加载与 bundled 注册表
    • bundled-skills · 内置 skill 与 initBundledSkills
    • mcp-skills · MCP prompt 转 skill
    • skill-tool-integration · SkillTool 与命令注册
    • message-types · Message 联合与 content blocks
    • tool-permission-types · Tool、Permission、Command 类型
    • api-sdk-types · API 与 Hooks 协议类型
    • misc-types · ids、plugin、generated 与其余类型
    • local-agent-task · 本地 Agent 与主会话后台化
    • remote-agent-task · 远程 CCR 与 In-Process Teammate
    • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
    • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybinding-registry · 注册、Provider 与 useKeybinding
    • default-bindings · 默认键位表与平台差异
    • command-bindings · command:* 动态斜杠命令绑定
    • vim-bindings · Vim 模式与 keybindings 边界
    • memdir-core · 路径、加载与 MEMORY.md
    • memory-extraction · extractMemories 与 SessionMemory
    • memdir-commands · /memory、/remember 与命令集成
    • app-state-core · store、AppState 类型与 Provider
    • app-state-selectors · selectors 与 onChangeAppState
    • teammate-state · 队友视图与 swarm 状态
    • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query config 与 deps · 配置快照与依赖注入
    • query tokenBudget · +500k 自动续跑
    • query transitions · Continue / Terminal 状态机
    • query stopHooks · Stop 事件与 turn 结束编排
  • 工具详解

    • tool-interface · Tool 契约与注册表
    • tool-permission-types · Tool、Permission、Command 类型
    • 工具: Bash
    • 工具: PowerShell
    • 工具: Agent
    • 工具: LSP
    • 工具: FileEdit
    • 工具: FileRead
    • 工具: Skill
    • 工具: WebFetch
    • 工具: MCP
    • 工具: SendMessage
    • 工具: FileWrite
    • 工具: Config
    • 工具: Grep
    • 工具: Brief
    • 工具: ExitPlanMode
    • 工具: ToolSearch
    • 工具: NotebookEdit
    • 工具: TaskOutput
    • 工具: WebSearch
    • 工具: ScheduleCron

本章总览

ResumeConversation.tsx(约 400 行)是 Claude Code 交互式 会话恢复 picker:扫描 ~/.claude 下 JSONL 日志,用 LogSelector 让用户选择后继续聊天。选中后它不定义新 Screen 类型,而是 unmount picker、mount REPL 并传入 initialMessages。本章覆盖渐进式日志加载、跨项目 resume、fork 模式与 launchResumeChooser 挂载路径。

学完本章你应该能

  • 说明 launchResumeChooser 与 launchRepl 的并行 import 策略
  • 解释 loadSameRepoMessageLogsProgressive / enrichLogs 分页加载
  • 描述 onSelect 内 switchSession、restoreAgentFromSession、context collapse 恢复顺序
  • 理解 crossProjectCommand 与 filterByPr 分支
  • 掌握 resumeData → REPL 的状态交接字段清单

核心概念(先读懂这些)

ResumeConversation 是 REPL 的前置向导

与 Doctor(一次性诊断)不同,ResumeConversation 是过渡态组件:loading → LogSelector → resuming spinner → REPL。不存在持久的「resume screen」路由枚举;CLI claude --resume 与交互 picker 最终都 converge 到同一 REPL Props 形状。

渐进式日志加载控制内存

sessionLogResultRef 持有 SessionLogResult,enrichLogs 批量填充 LogOption 元数据。logCountRef 镜像 logs.length,保证 setLogs updater 纯函数。showAllProjects 切换时重新 loadAllProjectsMessageLogsProgressive,支持跨仓库浏览历史。

建议学习步骤

  1. 阅读 Props 与 export function ResumeConversation(源码块 A)
  2. 阅读 filteredLogs 与初始 loadEffect(源码块 B)
  3. 走读 onSelect 恢复链(源码块 C)
  4. 阅读条件 render:resumeData → REPL(源码块 D)
  5. 对照 dialogLaunchers launchResumeChooser(源码块 E)

常见误区

注意

forkSession 时不 switchSession,但可能 recordContentReplacement

注意

COORDINATOR_MODE 下 matchSessionMode 可能注入 warning system message

注意

crossProject 同 repo worktree 可继续;纯跨目录则只复制命令不 mount REPL

挂载路径:launchResumeChooser

main.tsx 在用户未指定 session id 且需交互选择时调用 launchResumeChooser(dialogLaunchers.tsx ~117 行)。

与 launchRepl 对比:

项launchRepllaunchResumeChooser
根组件REPLResumeConversation
KeybindingSetup无(App 内处理)包裹 ResumeConversation
并行 Promise.allApp + REPLworktreePaths + ResumeConversation + App
render 方式renderAndRunrenderAndRun(非 showSetupDialog)

worktreePathsPromise 与组件 import 并行,避免串行等待 git worktree 探测拖慢首屏。

源码引用: src/dialogLaunchers.tsx · 第 112–132 行(共 202 行)

 112|   })
 113|   const resultPromise = showSetupDialog<string | null>(root, done => (
 114|     <NewInstallWizard
 115|       defaultDir={defaultDir}
 116|       onInstalled={dir => done(dir)}
 117|       onCancel={() => done(null)}
 118|       onError={message =>
 119|         rejectWithError(new Error(`Installation failed: ${message}`))
 120|       }
 121|     />
 122|   ))
 123|   return Promise.race([resultPromise, errorPromise])
 124| }
 125| 
 126| /**
 127|  * Site ~4549: TeleportResumeWrapper (interactive teleport session picker).
 128|  * Original callback wiring: onComplete={done}, onCancel={() => done(null)}, source="cliArg".
 129|  */
 130| export async function launchTeleportResumeWrapper(
 131|   root: Root,
 132| ): Promise<TeleportRemoteResponse | null> {

源码引用: src/dialogLaunchers.tsx · 第 21–23 行(共 202 行)

  21| // Type-only access to ResumeConversation's Props via the module type.
  22| // No runtime cost - erased at compile time.
  23| type ResumeConversationProps = React.ComponentProps<

Props 与组件入口

ResumeConversation Props 与 REPL 大量重叠(commands、initialTools、mcpClients、thinkingConfig),额外字段:

  • worktreePaths — launcher 注入,用于同 repo 日志过滤
  • forkSession — fork 时不 adopt session file
  • filterByPr — true / number / string(GitHub PR URL)过滤
  • initialSearchQuery — LogSelector 预填搜索
  • onTurnComplete — 传递给下游 REPL

组件内 state:logs、loading、resuming、showAllProjects、resumeData、crossProjectCommand。

源码引用: src/screens/ResumeConversation.tsx · 第 47–86 行(共 469 行)

  47|   recordContentReplacement,
  48|   resetSessionFilePointer,
  49|   restoreSessionMetadata,
  50|   type SessionLogResult,
  51| } from '../utils/sessionStorage.js'
  52| import type { ThinkingConfig } from '../utils/thinking.js'
  53| import type { ContentReplacementRecord } from '../utils/toolResultStorage.js'
  54| import { REPL } from './REPL.js'
  55| 
  56| function parsePrIdentifier(value: string): number | null {
  57|   const directNumber = parseInt(value, 10)
  58|   if (!isNaN(directNumber) && directNumber > 0) {
  59|     return directNumber
  60|   }
  61|   const urlMatch = value.match(/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)/)
  62|   if (urlMatch?.[1]) {
  63|     return parseInt(urlMatch[1], 10)
  64|   }
  65|   return null
  66| }
  67| 
  68| type Props = {
  69|   commands: Command[]
  70|   worktreePaths: string[]
  71|   initialTools: Tool[]
  72|   mcpClients?: MCPServerConnection[]
  73|   dynamicMcpConfig?: Record<string, ScopedMcpServerConfig>
  74|   debug: boolean
  75|   mainThreadAgentDefinition?: AgentDefinition
  76|   autoConnectIdeFlag?: boolean
  77|   strictMcpConfig?: boolean
  78|   systemPrompt?: string
  79|   appendSystemPrompt?: string
  80|   initialSearchQuery?: string
  81|   disableSlashCommands?: boolean
  82|   forkSession?: boolean
  83|   taskListId?: string
  84|   filterByPr?: boolean | number | string
  85|   thinkingConfig: ThinkingConfig
  86|   onTurnComplete?: (messages: Message[]) => void | Promise<void>

源码引用: src/screens/ResumeConversation.tsx · 第 92–104 行(共 469 行)

  92|   initialTools,
  93|   mcpClients,
  94|   dynamicMcpConfig,
  95|   debug,
  96|   mainThreadAgentDefinition,
  97|   autoConnectIdeFlag,
  98|   strictMcpConfig = false,
  99|   systemPrompt,
 100|   appendSystemPrompt,
 101|   initialSearchQuery,
 102|   disableSlashCommands = false,
 103|   forkSession,
 104|   taskListId,

日志加载与 filterByPr

filteredLogs useMemo:

  1. 排除 sidechain 日志(!l.isSidechain)
  2. 按 filterByPr 过滤:true = 任意 PR;number = 精确 PR;string = parsePrIdentifier(支持 github.com/.../pull/N URL)

loadSameRepoMessageLogsProgressive(worktreePaths) 在 mount useEffect 触发,完成后 setLoading(false)。

loadMoreLogs 调用 enrichLogs 追加批次;若 enrich 返回空但仍有 stat logs,递归 loadMoreLogs 跳过损坏条目。

handleToggleAllProjects 切换 loadAllProjectsMessageLogsProgressive,允许浏览其他仓库会话(仍受 crossProject 规则约束)。

源码引用: src/screens/ResumeConversation.tsx · 第 109–136 行(共 469 行)

 109|   const { rows } = useTerminalSize()
 110|   const agentDefinitions = useAppState(s => s.agentDefinitions)
 111|   const setAppState = useSetAppState()
 112|   const [logs, setLogs] = React.useState<LogOption[]>([])
 113|   const [loading, setLoading] = React.useState(true)
 114|   const [resuming, setResuming] = React.useState(false)
 115|   const [showAllProjects, setShowAllProjects] = React.useState(false)
 116|   const [resumeData, setResumeData] = React.useState<{
 117|     messages: Message[]
 118|     fileHistorySnapshots?: FileHistorySnapshot[]
 119|     contentReplacements?: ContentReplacementRecord[]
 120|     agentName?: string
 121|     agentColor?: AgentColorName
 122|     mainThreadAgentDefinition?: AgentDefinition
 123|   } | null>(null)
 124|   const [crossProjectCommand, setCrossProjectCommand] = React.useState<
 125|     string | null
 126|   >(null)
 127|   const sessionLogResultRef = React.useRef<SessionLogResult | null>(null)
 128|   // Mirror of logs.length so loadMoreLogs can compute value indices outside
 129|   // the setLogs updater (keeping it pure per React's contract).
 130|   const logCountRef = React.useRef(0)
 131| 
 132|   const filteredLogs = React.useMemo(() => {
 133|     let result = logs.filter(l => !l.isSidechain)
 134|     if (filterByPr !== undefined) {
 135|       if (filterByPr === true) {
 136|         result = result.filter(l => l.prNumber !== undefined)

源码引用: src/screens/ResumeConversation.tsx · 第 137–173 行(共 469 行)

 137|       } else if (typeof filterByPr === 'number') {
 138|         result = result.filter(l => l.prNumber === filterByPr)
 139|       } else if (typeof filterByPr === 'string') {
 140|         const prNumber = parsePrIdentifier(filterByPr)
 141|         if (prNumber !== null) {
 142|           result = result.filter(l => l.prNumber === prNumber)
 143|         }
 144|       }
 145|     }
 146|     return result
 147|   }, [logs, filterByPr])
 148|   const isResumeWithRenameEnabled = isCustomTitleEnabled()
 149| 
 150|   React.useEffect(() => {
 151|     loadSameRepoMessageLogsProgressive(worktreePaths)
 152|       .then(result => {
 153|         sessionLogResultRef.current = result
 154|         logCountRef.current = result.logs.length
 155|         setLogs(result.logs)
 156|         setLoading(false)
 157|       })
 158|       .catch(error => {
 159|         logError(error)
 160|         setLoading(false)
 161|       })
 162|   }, [worktreePaths])
 163| 
 164|   const loadMoreLogs = React.useCallback((count: number) => {
 165|     const ref = sessionLogResultRef.current
 166|     if (!ref || ref.nextIndex >= ref.allStatLogs.length) return
 167| 
 168|     void enrichLogs(ref.allStatLogs, ref.nextIndex, count).then(result => {
 169|       ref.nextIndex = result.nextIndex
 170|       if (result.logs.length > 0) {
 171|         // enrichLogs returns fresh unshared objects — safe to mutate in place.
 172|         // Offset comes from logCountRef so the setLogs updater stays pure.
 173|         const offset = logCountRef.current

源码引用: src/screens/ResumeConversation.tsx · 第 36–46 行(共 469 行)

  36| import {
  37|   computeStandaloneAgentContext,
  38|   restoreAgentFromSession,
  39|   restoreWorktreeForResume,
  40| } from '../utils/sessionRestore.js'
  41| import {
  42|   adoptResumedSessionFile,
  43|   enrichLogs,
  44|   isCustomTitleEnabled,
  45|   loadAllProjectsMessageLogsProgressive,
  46|   loadSameRepoMessageLogsProgressive,

onSelect:会话恢复 orchestration

用户选中 LogOption 后 onSelect 执行长链恢复(async):

checkCrossProjectResume → 可能 CrossProjectMessage
loadConversationForResume(log)
  → coordinator mode warning(可选)
  → switchSession + renameRecording + resetSessionFilePointer(非 fork)
  → restoreCostStateForSession
  → restoreAgentFromSession → setAppState agent
  → restoreSessionMetadata + restoreWorktreeForResume
  → adoptResumedSessionFile
  → context collapse restore(feature gate)
  → logEvent tengu_session_resumed
  → setResumeData({ messages, fileHistorySnapshots, ... })

失败时 logEvent success:false 并 rethrow。性能指标 resume_duration_ms 写入 analytics。

agenticSessionSearch 作为 LogSelector 的 onAgenticSearch prop,支持自然语言搜会话。

源码引用: src/screens/ResumeConversation.tsx · 第 178–220 行(共 469 行)

 178|         logCountRef.current += result.logs.length
 179|       } else if (ref.nextIndex < ref.allStatLogs.length) {
 180|         loadMoreLogs(count)
 181|       }
 182|     })
 183|   }, [])
 184| 
 185|   const loadLogs = React.useCallback(
 186|     (allProjects: boolean) => {
 187|       setLoading(true)
 188|       const promise = allProjects
 189|         ? loadAllProjectsMessageLogsProgressive()
 190|         : loadSameRepoMessageLogsProgressive(worktreePaths)
 191|       promise
 192|         .then(result => {
 193|           sessionLogResultRef.current = result
 194|           logCountRef.current = result.logs.length
 195|           setLogs(result.logs)
 196|         })
 197|         .catch(error => {
 198|           logError(error)
 199|         })
 200|         .finally(() => {
 201|           setLoading(false)
 202|         })
 203|     },
 204|     [worktreePaths],
 205|   )
 206| 
 207|   const handleToggleAllProjects = React.useCallback(() => {
 208|     const newValue = !showAllProjects
 209|     setShowAllProjects(newValue)
 210|     loadLogs(newValue)
 211|   }, [showAllProjects, loadLogs])
 212| 
 213|   function onCancel() {
 214|     // eslint-disable-next-line custom-rules/no-process-exit
 215|     process.exit(1)
 216|   }
 217| 
 218|   async function onSelect(log: LogOption) {
 219|     setResuming(true)
 220|     const resumeStart = performance.now()

源码引用: src/screens/ResumeConversation.tsx · 第 220–292 行(共 469 行)

 220|     const resumeStart = performance.now()
 221| 
 222|     const crossProjectCheck = checkCrossProjectResume(
 223|       log,
 224|       showAllProjects,
 225|       worktreePaths,
 226|     )
 227|     if (crossProjectCheck.isCrossProject) {
 228|       if (!crossProjectCheck.isSameRepoWorktree) {
 229|         const raw = await setClipboard(crossProjectCheck.command)
 230|         if (raw) process.stdout.write(raw)
 231|         setCrossProjectCommand(crossProjectCheck.command)
 232|         return
 233|       }
 234|     }
 235| 
 236|     try {
 237|       const result = await loadConversationForResume(log, undefined)
 238|       if (!result) {
 239|         throw new Error('Failed to load conversation')
 240|       }
 241| 
 242|       if (feature('COORDINATOR_MODE')) {
 243|         /* eslint-disable @typescript-eslint/no-require-imports */
 244|         const coordinatorModule =
 245|           require('../coordinator/coordinatorMode.js') as typeof import('../coordinator/coordinatorMode.js')
 246|         /* eslint-enable @typescript-eslint/no-require-imports */
 247|         const warning = coordinatorModule.matchSessionMode(result.mode)
 248|         if (warning) {
 249|           /* eslint-disable @typescript-eslint/no-require-imports */
 250|           const { getAgentDefinitionsWithOverrides, getActiveAgentsFromList } =
 251|             require('../tools/AgentTool/loadAgentsDir.js') as typeof import('../tools/AgentTool/loadAgentsDir.js')
 252|           /* eslint-enable @typescript-eslint/no-require-imports */
 253|           getAgentDefinitionsWithOverrides.cache.clear?.()
 254|           const freshAgentDefs = await getAgentDefinitionsWithOverrides(
 255|             getOriginalCwd(),
 256|           )
 257|           setAppState(prev => ({
 258|             ...prev,
 259|             agentDefinitions: {
 260|               ...freshAgentDefs,
 261|               allAgents: freshAgentDefs.allAgents,
 262|               activeAgents: getActiveAgentsFromList(freshAgentDefs.allAgents),
 263|             },
 264|           }))
 265|           result.messages.push(createSystemMessage(warning, 'warning'))
 266|         }
 267|       }
 268| 
 269|       if (result.sessionId && !forkSession) {
 270|         switchSession(
 271|           asSessionId(result.sessionId),
 272|           log.fullPath ? dirname(log.fullPath) : null,
 273|         )
 274|         await renameRecordingForSession()
 275|         await resetSessionFilePointer()
 276|         restoreCostStateForSession(result.sessionId)
 277|       } else if (forkSession && result.contentReplacements?.length) {
 278|         await recordContentReplacement(result.contentReplacements)
 279|       }
 280| 
 281|       const { agentDefinition: resolvedAgentDef } = restoreAgentFromSession(
 282|         result.agentSetting,
 283|         mainThreadAgentDefinition,
 284|         agentDefinitions,
 285|       )
 286|       setAppState(prev => ({ ...prev, agent: resolvedAgentDef?.agentType }))
 287| 
 288|       if (feature('COORDINATOR_MODE')) {
 289|         /* eslint-disable @typescript-eslint/no-require-imports */
 290|         const { saveMode } = require('../utils/sessionStorage.js')
 291|         const { isCoordinatorMode } =
 292|           require('../coordinator/coordinatorMode.js') as typeof import('../coordinator/coordinatorMode.js')

条件渲染:handoff 到 REPL

render 优先级(短路顺序):

  1. crossProjectCommand → CrossProjectMessage(复制 cd 命令)
  2. resumeData → &lt;REPL ... initialMessages={resumeData.messages} /&gt;
  3. loading → Spinner "Loading conversations…"
  4. resuming → Spinner "Resuming conversation…"
  5. filteredLogs.length === 0 → NoConversationsMessage(Ctrl+C exit)
  6. 默认 → LogSelector

LogSelector 接收 maxHeight={rows}(useTerminalSize)、onLoadMore、showAllProjects 切换、isResumeWithRenameEnabled 时 onLogsChanged 刷新列表。

NoConversationsMessage 绑定 app:interrupt → process.exit(1)。

源码引用: src/screens/ResumeConversation.tsx · 第 293–315 行(共 469 行)

 293|         /* eslint-enable @typescript-eslint/no-require-imports */
 294|         saveMode(isCoordinatorMode() ? 'coordinator' : 'normal')
 295|       }
 296| 
 297|       const standaloneAgentContext = computeStandaloneAgentContext(
 298|         result.agentName,
 299|         result.agentColor,
 300|       )
 301|       if (standaloneAgentContext) {
 302|         setAppState(prev => ({ ...prev, standaloneAgentContext }))
 303|       }
 304|       void updateSessionName(result.agentName)
 305| 
 306|       restoreSessionMetadata(
 307|         forkSession ? { ...result, worktreeSession: undefined } : result,
 308|       )
 309| 
 310|       if (!forkSession) {
 311|         restoreWorktreeForResume(result.worktreeSession)
 312|         if (result.sessionId) {
 313|           adoptResumedSessionFile()
 314|         }
 315|       }

源码引用: src/screens/ResumeConversation.tsx · 第 316–336 行(共 469 行)

 316| 
 317|       if (feature('CONTEXT_COLLAPSE')) {
 318|         /* eslint-disable @typescript-eslint/no-require-imports */
 319|         ;(
 320|           require('../services/contextCollapse/persist.js') as typeof import('../services/contextCollapse/persist.js')
 321|         ).restoreFromEntries(
 322|           result.contextCollapseCommits ?? [],
 323|           result.contextCollapseSnapshot,
 324|         )
 325|         /* eslint-enable @typescript-eslint/no-require-imports */
 326|       }
 327| 
 328|       logEvent('tengu_session_resumed', {
 329|         entrypoint:
 330|           'picker' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
 331|         success: true,
 332|         resume_duration_ms: Math.round(performance.now() - resumeStart),
 333|       })
 334| 
 335|       setLogs([])
 336|       setResumeData({

CrossProjectMessage 与剪贴板

跨目录 resume 时 setClipboard(crossProjectCheck.command) 写入 OSC 52 序列,stdout 可能写入 raw escape。UI 展示:

  • "This conversation is from a different directory."
  • 建议用户在目标目录运行显示的 claude 命令

同 repo 的 worktree 路径例外:checkCrossProjectResume 允许继续 onSelect,无需 CrossProjectMessage。

这与 screens 层「路由」概念相关:某些 resume 场景 拒绝 在当前 cwd mount REPL,强制用户切换到正确目录——安全与路径一致性考虑。

源码引用: src/screens/ResumeConversation.tsx · 第 340–370 行(共 469 行)

 340|         agentName: result.agentName,
 341|         agentColor: (result.agentColor === 'default'
 342|           ? undefined
 343|           : result.agentColor) as AgentColorName | undefined,
 344|         mainThreadAgentDefinition: resolvedAgentDef,
 345|       })
 346|     } catch (e) {
 347|       logEvent('tengu_session_resumed', {
 348|         entrypoint:
 349|           'picker' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
 350|         success: false,
 351|       })
 352|       logError(e as Error)
 353|       throw e
 354|     }
 355|   }
 356| 
 357|   if (crossProjectCommand) {
 358|     return <CrossProjectMessage command={crossProjectCommand} />
 359|   }
 360| 
 361|   if (resumeData) {
 362|     return (
 363|       <REPL
 364|         debug={debug}
 365|         commands={commands}
 366|         initialTools={initialTools}
 367|         initialMessages={resumeData.messages}
 368|         initialFileHistorySnapshots={resumeData.fileHistorySnapshots}
 369|         initialContentReplacements={resumeData.contentReplacements}
 370|         initialAgentName={resumeData.agentName}

源码引用: src/screens/ResumeConversation.tsx · 第 181–189 行(共 469 行)

 181|       }
 182|     })
 183|   }, [])
 184| 
 185|   const loadLogs = React.useCallback(
 186|     (allProjects: boolean) => {
 187|       setLoading(true)
 188|       const promise = allProjects
 189|         ? loadAllProjectsMessageLogsProgressive()

LogSelector 交互与 agentic 搜索

LogSelector 组件(components/LogSelector.js)接收 ResumeConversation 传入的 props:

  • maxHeight={rows} — 终端高度自适应列表
  • onLoadMore={loadMoreLogs} — 滚动到底加载 enrichLogs 批次
  • onAgenticSearch={agenticSessionSearch} — 自然语言搜索会话,与手动 filter 互补
  • initialSearchQuery — CLI --resume-search 等入口预填
  • showAllProjects / onToggleAllProjects — 跨项目浏览开关

isResumeWithRenameEnabled() 为 true 时注册 onLogsChanged 回调,用户重命名 session 后 refresh 列表而不重启 picker。

onCancel 绑定 process.exit(1),与 NoConversationsMessage 的 app:interrupt 一致——Resume picker 无「返回空 REPL」选项,取消即退出进程。

forkSession 与 contentReplacements

forkSession Prop 改变恢复语义:

行为forkSession=falseforkSession=true
switchSession执行跳过
adoptResumedSessionFile执行跳过
recordContentReplacement跳过可能执行
restoreSessionMetadata含 worktreeSession清除 worktreeSession

fork 用于「从旧会话分支新会话」场景:保留 messages 快照但不接管原 session id 文件指针,避免覆盖原 JSONL。

contentReplacements 来自 tool result 外置存储恢复,REPL 通过 initialContentReplacements 传入以还原大 payload 引用。

源码引用: src/screens/ResumeConversation.tsx · 第 220–227 行(共 469 行)

 220|     const resumeStart = performance.now()
 221| 
 222|     const crossProjectCheck = checkCrossProjectResume(
 223|       log,
 224|       showAllProjects,
 225|       worktreePaths,
 226|     )
 227|     if (crossProjectCheck.isCrossProject) {

本章小结与延伸

ResumeConversation = 日志 picker + 会话恢复 orchestration + REPL handoff。REPL Screen 类型见 repl-screen 章。 继续学习:

  • repl-screen
  • utils session-storage
Prev
REPL 屏 · Screen 类型与顶层路由
Next
Doctor · 安装诊断全屏