- 首页
- Documentation
- 会话
- 会话操作:export、dump、share、fork、resume/continue
会话操作:export、dump、share、fork、resume/continue
本文档描述了当前实现中,操作者可见的会话导出/分享/分叉/恢复操作行为。
../src/modes/controllers/command-controller.ts../src/session/agent-session.ts../src/session/session-manager.ts../src/export/html/index.ts../src/export/custom-share.ts../src/main.ts
| 操作 | 入口路径 | 会话变更 | 会话文件创建/切换 | 输出产物 |
|---|---|---|---|---|
/dump | 交互式斜杠命令 | 否 | 否 | 剪贴板文本 |
/export [path] | 交互式斜杠命令 | 否 | 否 | HTML 文件 |
--export <session.jsonl> [outputPath] | CLI 启动快速路径 | 无运行时会话变更 | 无活跃会话;读取目标文件 | HTML 文件 |
/share | 交互式斜杠命令 | 否 | 否 | 临时 HTML + 分享 URL/gist |
/fork | 交互式斜杠命令 | 是(活跃会话身份改变) | 创建新会话文件并将当前会话切换至该文件(仅持久化模式) | 当存在产物目录时,将其复制到新会话命名空间 |
/resume | 交互式斜杠命令 | 是(活跃的内存状态被替换) | 切换到所选的现有会话文件 | 无 |
--resume | CLI 启动(选择器) | 会话创建后是 | 打开所选的现有会话文件 | 无 |
--resume <id|path> | CLI 启动 | 会话创建后是 | 打开现有会话;跨项目情况可分叉到当前项目 | 无 |
--continue | CLI 启动 | 会话创建后是 | 打开终端面包屑或最近的会话;如果不存在则创建新会话 | 无 |
/export [outputPath](交互式)
Section titled “/export [outputPath](交互式)”流程:
InputController将/export...路由到CommandController.handleExportCommand。- 命令按空白字符分割,仅使用
/export之后的第一个参数作为outputPath。 AgentSession.exportToHtml()调用exportSessionToHtml(sessionManager, state, { outputPath, themeName })。- 成功后,UI 显示路径并在浏览器中打开文件。
行为细节:
--copy、clipboard和copy参数会被明确拒绝,并提示使用/dump。- 导出会嵌入会话头部/条目/叶节点以及来自代理状态的当前
systemPrompt和工具描述。 - 导出过程中不会追加任何会话条目。
注意事项:
- 参数解析基于空白字符(
text.split(/\s+/)),因此包含空格的带引号路径不会被此命令路径保留为单一路径。
--export <inputSessionFile> [outputPath](CLI)
Section titled “--export <inputSessionFile> [outputPath](CLI)”main.ts 中的流程:
- 在交互式/会话启动之前提前处理。
- 调用
exportFromFile(inputPath, outputPath?)。 SessionManager.open(inputPath)加载条目,然后生成并写入 HTML。- 进程输出
Exported to: ...后退出。
行为细节:
- 缺少输入文件时显示
File not found: <path>。 - 此路径不创建
AgentSession,也不变更任何运行中的会话。
/dump(交互式剪贴板导出)
Section titled “/dump(交互式剪贴板导出)”流程:
CommandController.handleDumpCommand()调用session.formatSessionAsText()。- 如果返回空字符串,报告
No messages to dump yet. - 否则通过原生
copyToClipboard复制到剪贴板。
转储内容包括:
- 系统提示词
- 活跃的模型/思考级别
- 工具定义 + 参数
- 用户/助手消息
- 思考块和工具调用
- 工具结果和执行块(不包括
excludeFromContext的 bash/python 条目) - 自定义/钩子/文件提及/分支摘要/压缩摘要条目
转储不会进行任何会话持久化更改。
/share 仅限交互式使用,始终从将当前会话导出到临时 HTML 文件开始。
阶段 1:临时导出
Section titled “阶段 1:临时导出”- 临时文件路径:
${os.tmpdir()}/${Snowflake.next()}.html - 使用
session.exportToHtml(tmpFile) - 如果导出失败(特别是内存会话),分享操作以错误结束。
阶段 2:自定义分享处理器(如果存在)
Section titled “阶段 2:自定义分享处理器(如果存在)”loadCustomShare() 在 ~/.xcsh/agent 中检查第一个存在的候选文件:
share.tsshare.jsshare.mjs
要求:
- 模块必须默认导出一个函数
(htmlPath) => Promise<CustomShareResult | string | undefined>。
如果存在且有效:
- UI 进入
Sharing...加载状态。 - 处理器结果解释:
- 字符串 => 视为 URL,显示并打开
- 对象 => 显示
url和/或message;打开url undefined/假值 => 通用的Session shared
- 完成后删除临时文件。
关键的回退行为:
- 如果自定义处理器存在但加载失败,命令报错并返回。
- 如果自定义处理器执行后抛出异常,命令报错并返回。
- 在这两种失败情况下,不会回退到 GitHub gist。
- 仅当不存在自定义分享脚本时,才会触发 gist 回退。
阶段 3:默认 gist 回退
Section titled “阶段 3:默认 gist 回退”仅当未找到自定义分享处理器时:
- 验证
gh auth status。 - 显示
Creating gist...加载状态。 - 运行
gh gist create --public=false <tmpFile>。 - 解析 gist URL,提取 gist id,构建预览 URL
https://gistpreview.github.io/?<id>。 - 同时显示预览和 gist URL;打开预览。
分享中的取消/中止语义:
- 加载器有
onAbort钩子,用于恢复编辑器 UI 并报告Share cancelled。 - 在此代码路径中,底层的
gh gist create命令未传递中止信号;取消是 UI 级别的,在命令返回后进行检查。
/fork 从当前会话创建新会话,并切换活跃的会话身份。
前置条件和即时守卫
Section titled “前置条件和即时守卫”- 如果代理正在流式传输,
/fork会被拒绝并发出警告。 - 操作前清除 UI 状态/加载指示器。
AgentSession.fork():
- 以
reason: "fork"发出session_before_switch事件(可取消)。 - 刷新待写入内容。
- 调用
SessionManager.fork()。 - 将产物目录从旧会话命名空间复制到新命名空间(尽力而为;非 ENOENT 的复制失败会被记录日志,但不是致命错误)。
- 更新
agent.sessionId。 - 以
reason: "fork"发出session_switch事件。
SessionManager.fork() 行为:
- 需要持久化模式和现有会话文件。
- 创建新的会话 id 和新的 JSONL 文件路径。
- 重写头部,包含:
- 新
id - 新时间戳
cwd不变parentSession设置为前一个会话 id
- 新
- 新文件中保留所有非头部条目不变。
非持久化行为
Section titled “非持久化行为”- 内存会话管理器从
fork()返回undefined。 AgentSession.fork()返回false。- UI 报告
Fork failed (session not persisted or cancelled)。
交互式 /resume
Section titled “交互式 /resume”流程:
- 打开通过
SessionManager.list(currentCwd, currentSessionDir)填充的会话选择器。 - 选择后,
SelectorController.handleResumeSession(sessionPath)调用session.switchSession(sessionPath)。 - UI 清除/重建聊天和待办事项,然后报告
Resumed session。
注意:
- 此选择器仅列出当前会话目录范围内的会话。
- 不使用全局跨项目搜索。
CLI --resume
Section titled “CLI --resume”--resume(无值)
Section titled “--resume(无值)”main.ts列出当前 cwd/sessionDir 的会话并打开选择器。- 在会话创建之前,使用
SessionManager.open(selectedPath)打开所选路径。
--resume <value>
Section titled “--resume <value>”createSessionManager() 解析顺序:
- 如果值看起来像路径(
/、\或.jsonl),直接打开。 - 否则视为 id 前缀:
- 在当前范围搜索(
SessionManager.list(cwd, sessionDir)) - 如果未找到且没有显式
sessionDir,进行全局搜索(SessionManager.listAll())
- 在当前范围搜索(
跨项目 id 匹配行为:
- 如果匹配的会话 cwd 与当前 cwd 不同,CLI 会询问:
Session found in different project ... Fork into current directory? [y/N]
- 选择是:
SessionManager.forkFrom(match.path, cwd, sessionDir)创建一个新的本地分叉文件。 - 选择否/非 TTY 默认值:命令报错。
CLI --continue
Section titled “CLI --continue”SessionManager.continueRecent(cwd, sessionDir):
- 解析当前 cwd 的会话目录。
- 首先读取终端范围的面包屑。
- 回退到最近修改的会话文件。
- 打开找到的会话;如果不存在,创建新会话。
这是仅在启动时的行为;不存在交互式 /continue 斜杠命令。
会话切换如何实际变更运行时状态
Section titled “会话切换如何实际变更运行时状态”AgentSession.switchSession(sessionPath) 执行恢复类操作使用的运行时转换:
- 以
reason: "resume"和targetSessionFile发出session_before_switch事件(可取消)。 - 断开代理事件订阅并中止进行中的工作。
- 清除排队的引导/后续/下一轮消息。
- 刷新当前会话管理器写入。
sessionManager.setSessionFile(sessionPath)并更新agent.sessionId。- 从加载的条目构建会话上下文。
- 以
reason: "resume"发出session_switch事件。 - 从上下文替换代理消息。
- 恢复模型(如果在当前注册表中可用)。
- 恢复或初始化思考级别。
- 重新连接代理事件订阅。
switchSession() 本身不创建新的会话文件。
事件发出和取消点
Section titled “事件发出和取消点”切换/分叉生命周期钩子
Section titled “切换/分叉生命周期钩子”对于 newSession、fork 和 switchSession:
- 前置事件:
session_before_switch- 原因:
new、fork、resume - 可通过返回
{ cancel: true }取消
- 原因:
- 后置事件:
session_switch- 相同的原因集
- 包含
previousSessionFile
ExtensionRunner.emit() 在遇到第一个取消性的前置事件结果时提前返回。
自定义工具 onSession 行为
Section titled “自定义工具 onSession 行为”SDK 将扩展会话事件桥接到自定义工具的 onSession 回调:
session_switch->onSession({ reason: "switch", previousSessionFile })session_branch->reason: "branch"session_start->reason: "start"session_tree->reason: "tree"session_shutdown->reason: "shutdown"
这些回调是观察性的;它们不能取消切换/分叉。
与本文档相关的其他取消面
Section titled “与本文档相关的其他取消面”/fork在流式传输期间被阻止(用户必须先等待/中止当前响应)。/resume选择器可以通过用户关闭选择器来取消。- 跨项目
--resume <id>可以通过拒绝分叉提示来取消。 /share在 gist 流程中有 UI 中止路径(Share cancelled);在此代码路径中不对gh gist create连接进程终止语义。
非持久化(内存)会话行为
Section titled “非持久化(内存)会话行为”当使用 SessionManager.inMemory()(--no-session)创建会话管理器时:
- 会话文件路径不存在。
/export和/share失败,提示Cannot export in-memory session to HTML(传播到命令错误 UI)。/fork失败,因为SessionManager.fork()需要持久化。/dump仍然有效,因为它序列化的是内存中的代理状态。- 如果设置了
--no-session,CLI 的 resume/continue 语义会被绕过,因为管理器创建会立即返回内存模式。
已知的实现注意事项(基于当前代码)
Section titled “已知的实现注意事项(基于当前代码)”SelectorController.handleResumeSession()不检查session.switchSession(...)的布尔返回值;钩子取消的切换仍可能继续执行 UI 的 “Resumed session” 重绘/状态路径。/share自定义分享失败不会降级到默认 gist 回退;它们会以错误终止命令。/export参数分词方式简单,不能保留包含空格的带引号路径。