main:会话控制、可操作链接与展示语义

buffin · origin/main...main · 2026-07-22 · 自包含,读完即弃

7 commits
99 文件
+5,382 / −628
31.6% 测试

写法说明:每段代码均取自当前分支并由主线程复核;青色斜体是解读,灰色是源码注释。每条旅程最后给出排查路标。

1TL;DR

这 7 个本地领先提交将桌面会话从“发送文本、显示流”扩展为三条路径:slash 命令被解析为结构化运行时控制;助手输出中的路径可在当前会话工作目录中预览、打开或定位;流式文本和系统主题把权威事实与表面展示策略分开保存。provider 差异分别收敛在 daemon 回执、renderer 命令目录和 Electron main 文件授权三个边界。

2变更地图

desktop renderer
5,052 行 · 84.1%
daemon
118 行 · 2.0%
patch + docs
218 行 · 3.6%
设计重心外围变更
命令目录/状态机、控制回执、流式 reveal cursor、文件链接、main/preload 桥接、系统主题。locale、锁文件解析产物、Tailwind token/hover 变体。测试占 1,899 变更行。

3架构一图流

以前 · 独立的表面行为

Composer
文本 send
startTurn
Incremark
默认内联节点
文件操作

现在 · 结构化控制与受限文件通道

command catalog
command / receipt
daemon store
SessionLink
preload / cwd
Electron main

4数据与状态先行

store 增加 session 命令目录、消息开始时间和持续时长;文件请求包含 sessionId、路径和可选源位置。展示层不保存真相,只从 store 派生策略。

apps/desktop/src/renderer/features/session/models/session-store.ts真实代码(节选)
export interface Message {
  id: string; role: 'user' | 'assistant'; parts: MessagePart[]
  status: 'complete' | 'running'; startedAt?: number; durationMs?: number
}
export interface SessionState {
  messages: Message[]; isRunning: boolean; usage: UsageInfo | null
  commands: AgentControlCommandDefDto[] | null
}
apps/desktop/src/shared/session-files.ts真实代码(节选)
export type SessionFileRequest = {
  sessionId: string; path: string; line?: number; column?: number; endLine?: number
}

5旅程一:输入命令与运行时配置

全景
草稿
use-composer-draft
命令目录
session-command
controllertRPCdaemon receipt

创建前使用 provider 的命令目录;已有会话优先使用 daemon 推送的 commands.updated。同一解析器同时给补全菜单和 Enter 提交使用,未知 slash 文本仍是普通消息,缺少必填参数则只补全。

apps/desktop/src/renderer/features/session/models/session-command.ts真实代码(节选)
const commands = (sessionCommands ?? providerCommands ?? []).filter(isReachableCommand)
function commandSubmission(commands, input) {
  const parsed = parseSlashCommand(input.trim())
  if (!parsed) return { kind: 'message', text: input.trim() }
  const command = commands.find((candidate) => candidate.name === parsed.name)
  if (!command) return { kind: 'message', text: input.trim() }
  return commandInvocation(command, parsed.raw)
}

controller 监听权威 isRunning,而不是由组件猜测 run 生命周期;模型和 reasoning/effort 合并在一个 Popover,已有会话只显示 provider 标记为 runtime 的字段。

apps/daemon/src/agent/service.ts真实代码(节选)
const result = await handle.runner.runControl(command)
await handle.sink.flushTerminalDeltas({ runId: run.id })
await handle.sink.ensureControlCompletion({ runId: run.id, command: command.name, result })
await this.finishControlAndBroadcast(run.id, result.status === 'error' ? 'failed' : 'completed')
排查路标 · 命令
症状入口
候选缺失、slash 被当普通文本session-command.tsuse-composer-draft.ts
命令后状态或错误不正确use-session-composer-controller.tsservice.ts

6旅程二:助手链接到会话文件

全景
SessionLinkclassify / suffixpreload IPCmain cwd 校验

纯函数先区分外链、文件和未知 scheme,并处理 :12:3:12-14#L12-L14。main 才按 session ID 查询 cwd,再对 cwd 和目标执行 realpath;因此符号链接和上级目录越界也受约束。

apps/desktop/src/main/session-file-path.ts真实代码(节选)
const canonicalCwd = await realpath(cwd)
const candidate = await realpath(resolve(canonicalCwd, expandedPath))
const relativePath = relative(canonicalCwd, candidate)
if (relativePath === '..' || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
  throw new Error('File link points outside the session working directory.')
}
if (!(await stat(candidate)).isFile()) throw new Error('File link target is not a regular file.')

预览最多返回目标附近 200 行;到目标行前扫描超过 4 MiB 时返回 unavailable。打开另有限制,已知可执行扩展名不会交给系统 shell。

排查路标 · 文件链接
症状入口
路径未成为控件detect-paths.tsclassify.tsSessionLink.tsx
控件可见但预览不可用use-file-link.tssession-file-bridge.ts
工作区文件无法打开resolveSessionFilePath 与文件类型限制

7旅程三:流式展示与系统主题

“文本是否还会增长”“run 是否结束”“这个挂载是否已显示全文”是不同状态。ThreadRevealPolicy 为 surface 派生 skip / pace / holdusePlaybackBuffer 持有本挂载的游标,所以 pace 后完成时仍会逐步追平,而非突然跳到末尾。

apps/desktop/src/renderer/features/session/models/thread-presentation.ts真实代码(节选)
function deriveRevealPolicy(input, execution): ThreadRevealPolicy {
  if (!input.visible || input.isHydrating || input.reduceMotion) return 'skip'
  if (execution === 'waiting-approval' || execution === 'waiting-input') return 'hold'
  return execution === 'running' ? 'pace' : 'skip'
}

主题也是同一拆分:ThemePreference 保存用户意图,ThemeName 是 system 当前解析出的 dark/light 色板;renderer 更新 CSS token,main 收到原始 system 值并同步 nativeTheme/title-bar overlay。

apps/desktop/src/renderer/lib/theme-preference.ts真实代码(节选)
export function applyThemePreference(preference, prefersDark?) {
  activeThemePreference = preference
  const theme = resolveThemePreference(preference, prefersDark)
  applyTheme(theme)
  window.buffin.setTheme(preference)
  return theme
}
排查路标 · 展示与主题
症状入口
完成消息跳字或停在半截thread-presentation.tsuse-playback-buffer.ts
运行时长不对session-runtime-store.tsSessionThread.tsx
system 主题原生标题栏不一致theme-preference.tstheme-bridge.ts

8测试与风险地图

有兜底薄冰(事实)
命令目录、参数、control RPC;reveal 策略;URI/裸路径/行列范围;cwd、符号链接越界、预览上限;system 主题和 tab action。🟡 未见真实 Electron 进程注册 bridge 的端到端测试。
🟡 未见 runner 抛异常后 error receipt 内容的专门断言。
⚪ session cwd cache 是否可跨 cwd 变更复用,当前材料无法证明,uncertain。

9覆盖声明

本报告覆盖 origin/main...main 的 99 个文件和 7 个提交。session renderer、desktop 跨进程/主题、daemon 与依赖/文档分别全量精读;主线程二次读取并裁剪了报告中的命令、链接、文件桥接、展示、主题和 daemon 代码。locale、lockfile 和 token 配置已按外围变更归类,未略读。