Appearance
功能规格说明:桌面端远程主机
本规格描述「本机之外的那台机器」:怎么连上去、远端缺什么就装什么、会话与 daemon 在远端怎么活。会话本身的新建/切换/重命名/看板与 worktree 隔离见 desktop-sessions.md;远程会话的差异/终端/预览面板见各自面板规格。
用户场景与测试
用户故事:SSH 远程主机(优先级:P2)
作为用户,我希望在工作目录选择器之前先选择「本地」或一个 SSH 主机(可添加新主机),让会话运行在远程主机上,以便我直接编辑远端服务器上的代码仓库(体验与 VS Code Remote-SSH 一致)。选中远程主机后,工作目录通过远程目录浏览面板以点击方式选择(如 VS Code Remote-SSH 的「打开文件夹」:路径输入框 + 面包屑 + 子目录列表),无需手输完整路径。
为什么是这个优先级:远程开发是独立于本地桌面体验的新能力,不影响本地会话基础能力;「运行在远端」的架构(整个 wave --stdio 子进程通过 ssh <host> -- wave --stdio 在远端执行,JSON-RPC 走 ssh 管道)使 agent 的 bash/文件/git 工具天然在远端文件系统上工作,与本地会话共享同一套 stdio 协议、通知路由与会话索引。
独立测试:配置 ~/.ssh/config 一个可用主机后,选择该主机,在远程目录浏览面板中逐级点击进入子目录并选择远端仓库根目录开始会话,验证 agent 的读写/命令/工具作用在远端;用 ssh user@host -p port 格式添加新主机验证写入配置文件并选中;断开网络或使用无密钥的主机验证快速失败与错误提示;重启应用验证主机选择重置为「本地」且最近打开列表保留。远程会话中让 agent 启动 dev server,点击消息中的 localhost 链接验证端口转发与预览面板加载、重复点击复用同一转发、服务停止后错误提示与重试、元素评论记录远端原始地址。
验收场景:
- 假设 用户打开应用并处于新会话状态,当 查看工作目录选择器,则 必须在其左侧显示主机选择器:首项为「本地」,其后为
~/.ssh/config中解析出的主机列表(顶层Host块名称,通配符条目除外),末项为「添加主机…」;默认选中「本地」,本地行为与现状完全一致。 - 假设 用户选中某台主机,当 选择生效,则 工作目录选择器必须切换到该主机的远程目录上下文:菜单列出该主机的最近打开远程目录与「浏览…」入口,不再提供本地目录浏览入口;分支选择器与 worktree 勾选项在远程 git 仓库目录下照常显示(git 命令经该主机在远端执行)。
- 假设 用户选中远程主机后通过远程目录浏览面板选定目录并确认,当 校验通过,则 必须将该(主机,路径)加入该主机最近打开列表并开始新会话,会话实际工作在远端路径;当 路径在远端不存在或不是目录,则 必须显示明确错误、不进入会话。
- 假设 用户选中远程主机后开始会话,当 会话运行,则 agent 的全部工具(bash/读文件/写文件/搜索/git、worktree 创建、分支查询、权限确认等)必须在远端主机上执行,行为与本地会话一致;消息流式、确认交互、任务列表、媒体附件等在 UI 中的呈现不变。
- 假设 用户点击「添加主机…」,当 输入
ssh user@hostname -p port形式的连接串并确认,则 必须将新主机写入~/.ssh/config(追加Host块并解析出 User/HostName/Port 选项),刷新主机列表并自动选中该主机;当 该主机名已存在,则 必须提示且不得覆盖或重复写入。 - 假设 远端连接需要密码或键盘交互认证,当 应用发起连接,则 不得挂起等待输入:必须以密钥/ssh-agent 方式快速失败,并显示可操作的错误提示(如「Permission denied (publickey)——请检查 SSH 密钥与 ssh-agent」);认证不要求任何用户交互。
- 假设 远端未安装
wave-codeCLI,当 应用连接该主机,则 必须把本机内置 CLI bundle(bin/wave-code.js+dist/bundle/wave.mjs+package.json)经 ssh 推送到远端固定目录~/.wave/cli/desktop/后继续启动流程(详见 desktop-distribution.md「内置 CLI 一致保障」),并同时推送匹配远端平台的 Node 运行时到~/.wave/runtime/node/、用它启动 daemon——与远端有没有 Node.js 无关(详见 desktop-distribution.md「远端主机无需预装 Node」),不得要求用户在远端安装 Node;当 运行时取不到(桌面端无网 / 镜像无该平台包 / 校验不通过),则 必须显示含失败原因、实际请求 URL 与WAVE_NODE_MIRROR指引的可操作错误,不得回退到远端系统 Node;当 远端缺少 grep 依赖 rg 且自取失败,则 只降级(Grep 报「ripgrep is not available」,其余功能正常),不阻断 daemon 启动、不得无限重试。 - 假设 本机 ssh 命令不可用(如 Windows 未启用 OpenSSH 客户端),当 应用连接,则 必须显示明确的安装引导错误,不得进入不可用状态。
- 假设 会话从本地切换到远程主机(或反之),当 切换发生,则 会话与通知必须按(主机,sessionId)寻址,各主机会话互不串话;侧边栏会话树中远程会话与本地会话不得混入同一目录分组(分组键含主机)。
- 假设 某分屏展示的是远程会话,当 用户查看该分屏,则 终端、差异与预览面板必须全部可用:终端经本机 ssh 在远端分配 PTY 运行 shell,差异在会话所在主机上经 CLI RPC 执行 git 与文件读取(远程会话即远端 daemon 进程,见场景 14),预览经本机 SSH 端口转发访问远端 localhost 服务;远程会话消息与终端输出中的 localhost 链接必须经端口转发后在预览面板中打开,不得用系统默认浏览器打开(本地浏览器访问不到远端 localhost)。
- 假设 用户关闭并重新打开应用,当 应用启动,则 主机选择必须重置为「本地」(与每次启动全新开始的规则一致);
~/.ssh/config主机列表与各主机最近打开目录在重启后仍可用。 - 假设 本地不存在
~/.ssh/config,当 用户打开主机选择器,则 必须仅显示「本地」与「添加主机…」,不得报错;首次添加主机时按需创建该文件。 - 假设 某分屏展示的是远程会话且分屏有打开的终端面板,当 用户向终端输入或调整终端尺寸,则 输入与尺寸调整必须经本机 ssh 会话转发到远端 PTY 中运行的用户默认 shell,工作目录为该分屏 agent 的当前工作目录(远端路径);当 远端连接中断或 shell 退出,则 终端面板必须显示退出状态/错误,与本地终端一致。
- 假设 某分屏展示的是远程 git 仓库会话,当 用户打开差异面板,则 必须显示该远程仓库相对 HEAD 的工作区差异(含未跟踪文件、二进制/超限文件按本地规则处理);取数必须与本地会话走同一条 CLI RPC(
getWorkspaceDiff,见 desktop-distribution.md「内置 CLI 一致保障」):git 命令与文件读取全部在会话所在主机的 CLI 进程内一次执行完,结果经既有 ssh 隧道单次回传本机渲染,不得由本机主进程对远端逐文件发起 ssh 命令。 - 假设 某分屏展示的是远程会话,当 用户点击该会话消息或终端输出中的 localhost 链接(如
http://localhost:5173),则 主进程必须按需建立 SSH 端口转发(ssh -N -L,本地端口默认与远端端口相同、被占用时递增至首个空闲端口,仅绑定本机回环 127.0.0.1),将链接重写为转发后的本地地址(如http://localhost:5173)并在预览面板中加载;同一(主机,远端端口)的转发必须复用,重复点击不得重复建立。 - 假设 远程 localhost 链接的端口转发已建立,当 远端服务不可达(未启动/已停止/连接中断)或转发建立失败(主机不可达、本地端口分配失败),则 预览面板必须显示可操作的错误提示与重试入口(与本地预览「无法连接」一致),不得白屏或静默失败;当 用户点击重试或再次点击链接,则 必须重新建立转发并加载。
- 假设 远程预览面板已打开且元素拾取可用,当 用户拾取元素并提交评论,则 评论中的页面 URL 必须记录远端原始地址(如
http://localhost:5173)而非转发后的本地地址,确保 agent 在远端可访问该 URL;拾取交互与评论格式与本地预览一致。 - 假设 远程会话已建立端口转发,当 该会话被删除(会话从树中移除),则 主进程必须终止对应
ssh -N -L转发进程并释放本地端口,不得残留孤儿进程;同一(主机,远端端口)转发被多个会话引用时,仅当最后一个引用会话删除后才终止。隧道所有权归会话而非面板:跨主机切换、同一对话中点击不同链接(无论路径还是 origin 不同)、关闭或重开预览面板、pane 卸载/移动、焦点切换等任何其他情况均不得销毁转发。ssh 转发进程自身意外退出(被动失败)与应用退出(进程终结的必然清理)同样终止转发。 - 假设 远端通过版本管理器(如 nvm)安装 Node.js、或远端 PATH 上根本没有
node,当 应用连接该主机,则 两种情况结果必须完全一致:daemon 一律用推送的<远端运行时>/bin/node启动,不做 Node 探测、不读登录 shell 的 PATH 配置、不因非交互 SSH 会话缺少 PATH 而改变行为——远端 shell 环境与初始化文件不得影响运行时选择;rg 自取与 daemon 运行都在远端 CLI 自己的进程内完成(远端出网即可),与远端 shell 初始化文件无关。 - 假设 用户选中远程主机后点击工作目录选择器的「浏览…」,当 打开远程目录浏览面板,则 面板必须呈现 VS Code「打开文件夹」式结构:弹层从触发按钮向上展开,底部固定于触发按钮上方,面板高度随加载/列表/筛选变化时输入框位置必须保持稳定(输入框固定在弹层底部,不得随高度伸缩上下移动);顶部为面包屑(路径各层级可点击跳转)、中部为当前目录的子目录列表(仅目录项、不含文件,按名称排序,点击进入子目录;非根目录时列表首项为「…」上级目录)、底部为筛选输入框与「选择此目录」确认按钮;筛选输入框支持输入关键词实时过滤当前单级目录列表并高亮匹配项(大小写不敏感的子串匹配,不匹配的目录项隐藏,过滤为空时显示无匹配提示),也支持输入
/或~开头的完整路径后按 Enter 直达(~开头自动展开为远端家目录);当 列表非空,则 用户按↑/↓必须在过滤后的子目录项中移动选中高亮(「…」上级项不参与),按 Enter 进入当前选中的子目录(无选中项时 Enter 仍按完整路径直达),点击某项同样将其置为选中;当 用户在筛选输入框中输入或修改关键词,则 存在匹配项时匹配列表必须默认选中第一项(无匹配项时无选中),无需方向键即可直接按 Enter 进入该匹配项;导航进入其他目录后选中状态必须重置;面板初开时定位到该主机的家目录。 - 假设 用户在远程目录浏览面板中浏览、输入关键词筛选或输入完整路径,当 目录列表经 ssh 在远端读取失败(连接中断、无读权限、路径非目录),则 面板必须显示明确的错误提示并停留在可重试状态,不得静默空白或直接进入会话;当 用户点击「选择此目录」或输入完整路径后按 Enter 确认,则 必须按场景 3 校验并开始会话。
- 假设 用户处于远程目录浏览面板,当 面板被关闭(点击外部、Esc)后再次打开,则 面板定位必须回到该主机最近一次浏览的位置或家目录,不得每次从根目录开始。
- 假设 用户处于新对话状态(分屏无任何消息)并将主机从「本地」切换到某台远程主机(或从一台远程主机切换到另一台),当 切换生效,则 工作目录选择器必须默认选中该主机最近打开目录列表的第一项(该主机最近打开列表为空时无默认目录,等待用户通过「浏览…」或最近目录菜单选择),不得沿用切换前主机的工作目录;用户不另作选择直接开始会话时,会话必须工作在该默认目录上。
- 假设 用户处于远程会话并通过「+ 上传文件」选择本地文件,当 上传完成,则 文件内容必须传输到远端主机并落盘到远端临时目录,回传并插入输入框的必须是远端主机上可访问的文件路径(如远端
/tmp/wave-artifacts/<文件名>),agent 的读文件等工具必须能在远端读取到该文件内容;当 远端上传失败,则 必须显示与本地一致的明确错误提示,不得静默成功。本地会话中该功能行为与现状完全一致(文件落盘到本地临时目录、路径为本地路径)。 - 假设 用户处于新会话状态并将主机从「本地」切换到某台远程主机(或从一台远程主机切换到另一台),当 新主机的工作目录已确定但该目录的分支查询尚未返回(需先建立 SSH 隧道与远端 daemon,可能耗时数秒),则 分支选择器与 worktree 勾选项位置必须在查询超过约 300ms 仍未返回时显示加载文案,不得整块消失且无任何反馈;查询返回后按该目录是否 git 仓库显示控件或隐藏(判定细则见「基于分支的 worktree 隔离会话」场景 7/8)。
- 假设
~/.ssh/config中主机极多(如上百条,远超一屏),当 用户打开主机选择器,则 菜单必须有高度上限(不超过约 340px),不得向上撑出窗口顶部导致条目看不见;菜单内只有具体主机条目所在的列表滚动,「本地」、分组标题(如「SSH 主机」)与底部「添加主机…」入口(含展开后的连接串输入框)全部固定不动,任何滚动位置都可见可点,打开菜单时焦点落在「本地」上;图标只出现在「本地」行与分组标题行(两者同构:一颗图标 + 文字),具体主机名不再各带图标,主机名缩进到与分组标题文字对齐。工作目录选择器的「最近打开」菜单与基准分支下拉菜单适用同一高度上限(条目多时滚动,不得撑出窗口)。 - 假设 同时存在「WSL 发行版」与「SSH 主机」两个分组(host 下发顺序为 本地 → WSL 发行版 → SSH 主机),且 SSH 主机极多,当 用户打开主机选择器,则 高度上限按组分配:条目少的组保持整组可见(不因另一组太长被压缩、被顶没),条目多的组让出高度并在自己的列表里滚动;两个分组各自独立滚动,各自的标题行常驻;空间确实不够(小窗口)时两组都不被压缩到看不见,各保留可滚动的可见区域;「WSL 发行版」标题行的图标与「SSH 主机」遵循同一规范(一颗图标 + 文字,具体条目行不带图标)。
用户故事:SSH 远程后台会话(优先级:P2)
作为用户,我希望远程主机上的会话在本地断开连接(ssh 中断、关闭桌面应用)后继续在远端后台运行,重新打开应用后能直接接回正在运行的会话并处理等待中的审批,以便我可以在本地不打开应用的情况下让远端任务持续执行。
为什么是这个优先级:远程开发(SSH 故事)已交付,但会话生命周期被 ssh 管道绑定——本地一断,远端任务即中断,与「后台」的预期相悖。真后台(远端 daemon 托管 + attach 接回)是远程开发形成闭环的关键能力,依赖 CLI daemon 化与 desktop 重连机制,改动横跨两层,优先级与 SSH 故事一致。
独立测试:在远程主机开始一个耗时任务后关闭应用并断开网络,验证任务在远端继续完成;重新打开应用连接该主机,验证会话从侧边栏直接接回(attach,非重放)且断线期间产生的消息可见、可正常发送与中断;在无客户端期间触发权限审批,验证会话挂起并标记「待确认」,重连进入后弹出确认框,确认后任务继续;验证侧边栏在未进入会话时即显示后台会话的「运行中」/「待确认」状态;删除后台会话验证远端 agent 终止;重启远端主机后验证历史会话仍可从转录恢复。
验收场景:
- 假设 某远程主机上存在正在运行(生成中/任务执行中)的会话,当 ssh 连接中断(断网、本地 app 退出或关闭窗口),则 远端会话必须脱离本地连接继续运行:正在执行的任务持续推进、生成中的回复继续生成,会话数据与运行状态在远端保留,不受本地断开影响。
- 假设 本地 app 重新打开并连接某台存在后台会话的远程主机,当 连接建立,则 应用必须自动检测该主机远端仍在运行的会话并将它们显示在侧边栏会话树(该主机分组下,附「运行中」/「待确认」标识),无需重新创建或手动恢复;当 用户点击其中某条会话,则 必须直接接入(attach)该运行中的会话——补回断线期间产生的消息与进行中的流式状态,用户可正常交互(发送消息、中断生成、响应确认),行为与从未断线一致。
- 假设 某后台会话在无客户端连接期间完成了生成或产生了新消息,当 用户重新连接并进入该会话,则 必须看到断线期间产生的全部消息,不得丢失或回退到断线前的状态。
- 假设 某后台会话(无客户端连接)执行到需要权限审批的步骤(bash 命令/文件编辑/计划确认等),当 审批请求产生,则 该会话必须挂起等待(任务不继续推进),审批请求在远端保留为「待确认」状态,等待用户响应;挂起期间该会话不得因无客户端而被丢弃或终止。
- 假设 远端存在处于「待确认」状态的会话,当 用户重新连接并进入该会话,则 必须弹出与在线会话一致的确认对话框(展示待审批的命令/改动内容),用户确认或拒绝后该会话继续执行或中止,行为与从未断线一致。
- 假设 远端 daemon 中存在运行中或待确认的后台会话,当 用户打开应用连接该主机(尚未进入任何会话),则 侧边栏对应会话条目前必须显示「运行中」/「待确认」标识(与现有标识规则一致:待确认优先于运行中),无需先进入会话即可感知远端会话状态;该状态在用户进入会话并处理(或审批完成)后消失。
- 假设 远端后台会话产生新的审批请求,当 审批产生,则 应用不得发送系统级通知(macOS 通知中心等),仅通过应用内呈现(侧边栏「待确认」标识 + 应用内 toast 提示 + 进入会话后的确认对话框)提示用户。
- 假设 同一远程主机的多个后台会话在远端同时运行,当 用户连接该主机,则 所有会话保持运行互不影响,任一会话均可接入查看与交互,会话与通知按(主机,sessionId)寻址,互不串话。
- 假设 用户删除一条后台会话,当 删除确认,则 远端对应 agent 必须终止(不再后台运行),worktree 目录与临时分支清理等既有删除逻辑照常执行。
- 假设 远端机器重启或远端 daemon 进程退出,当 用户重新连接该主机,则 断线前正在进行的任务终止(运行状态丢失),但历史会话仍可从侧边栏按现有恢复流程从远端转录恢复,不得出现幽灵「运行中」状态或脏数据。
- 假设 远端 daemon 正在运行,当 应用与之通信,则 通信必须全部经 ssh 隧道完成(如 unix socket 经 ssh 转发),daemon 不得监听公网地址或提供无认证访问;认证继续沿用现有 ssh 密钥机制,不新增凭据体系。
- 假设 本地会话与远程会话并存,当 用户连接远程主机或远程后台会话运行期间,则 本地会话行为与现有完全一致,不受远程后台模式影响;本地会话不启用后台保活(本地 app 退出即终止,与现状一致)。
用户故事:SSH 远程会话断线自动重连(优先级:P2)
作为用户,我希望远程主机的连接意外断开后(系统睡眠唤醒、网络闪断、远端 sshd 重启),桌面应用自己在后台重建连接并把分屏接回原来的会话,以便长任务跨睡眠与断网持续可见、可交互,不必回到应用后手动从侧边栏把会话找回来。
为什么是这个优先级:真后台已交付——会话活在远端 daemon 里,连接断开只丢传输层(见「SSH 远程后台会话」);当前断开只做解绑,恢复全靠用户下次访问。自动重连补齐「断开 → 重新可见」的最后一段,改动集中在主进程(重建隧道 + 复用既有恢复路径),不触碰 daemon 协议。是否坚持重连由「有没有正在运行的会话要接回」决定:有就近乎持续重试,没有就只试几下落回懒重连——这条分流是参考 claude.ai 桌面端后定下的口径,避免「没有目标还无限重拨」和「长任务一断就放弃」两种极端。
独立测试:在远程主机启动长任务后断网数十秒再恢复,验证不点击任何会话即自动接回、断线期间产生的消息可见;锁屏让系统睡眠后唤醒,验证不把断线当失败、等网络就绪且人回到应用后接回;关闭所有分屏后断网,验证只做有限次尝试即停止、下次点击会话仍能连上;短时间内反复断连,验证退避被拉长且界面提示连接不稳定、稳定后恢复正常节奏;自动重连进行中切到别的会话,验证以用户操作为准。
验收场景:
- 假设 某分屏显示远程会话且会话正在生成或执行任务,当 该主机的 ssh 连接意外断开(网络闪断、远端 sshd 重启、系统睡眠唤醒后连接失效),则 主进程必须自动重建该主机连接并把该分屏接回原会话,无需用户点击侧边栏;接回后发送消息、中断生成、响应确认全部恢复,行为与从未断线一致。
- 假设 自动重连正在进行,当 用户查看该分屏,则 分屏必须显示恢复中指示(复用既有历史会话恢复的加载动画),不得空白或无反馈;接回成功或放弃后指示消失。
- 假设 该主机仍有要接回的目标(分屏绑定的会话,或仍在运行的会话),当 一次重连失败,则 必须按指数退避继续尝试(约 1s、2s、4s……间隔封顶约 1 分钟),不设固定次数上限(场景 13 的不可恢复原因除外);连续失败到数十次后则 退成约 5 分钟一次的慢速重试(避免长期空转);单轮重连持续超过约 10 分钟仍未成功,则 必须继续尝试并向用户说明「仍在尝试连接」,不得静默。
- 假设 该主机没有任何待接回的目标(分屏都已关闭,且该主机上没有运行中的会话),当 连接断开,则 只做有限次快速尝试(约 1s、2s、5s 三档)后停止,回落到既有懒重连——下次访问该主机或会话时再建立连接。
- 假设 连接因系统睡眠而失效,当 系统唤醒,则 这一次失败不得计入重试次数:应用必须在网络就绪且用户回到应用之后才开始尝试;一轮重连中最多容许约 5 次睡眠中断,超出后按普通失败处理。
- 假设 系统刚唤醒、网络尚未就绪(Wi-Fi 关联、DHCP、VPN 认证未完成),当 应用准备重连,则 不得在无网络期空跑重试(等网络可用信号或探测到主机可达再拨),不得把重试窗口耗在唤醒瞬间。
- 假设 系统睡眠或断网使隧道呈半开状态(本地未收到 RST/FIN,连接看似存活),当 用户回到应用,则 应用必须在可预期时间内识别该连接已失效并触发重连(经应用层心跳或链路探测),不得长时间停留在「看似连接但无任何更新」的状态。
- 假设 连接在短时间内反复断开(约 5 分钟内十次量级),当 断开次数超过阈值,则 必须拉长重连间隔,并在界面把该主机标记为连接不稳定(给出可操作提示),不得以正常节奏无限重拨;连接稳定一段时间(约 5 分钟)后自动恢复正常重连节奏并撤掉提示。
- 假设 用户当前不在电脑前(锁屏、应用不在前台且无近期交互),当 连接断开,则 不得立即启动自动重连(避免空转与认证提示打扰);当 用户解锁、切回应用、或在该会话发送消息/点击重试,则 必须立即开始一次尝试,不等下次退避到点。
- 假设 自动重连进行中,当 用户切换到别的会话、关闭该分屏或切换主机,则 该分屏的自动重连必须让位:不得覆盖用户最新的选择,也不得在用户离开后把会话强塞回分屏。
- 假设 自动重连成功,当 会话接回,则 必须补回断线期间产生的全部消息与运行状态(生成、任务、后台任务),与手动点击侧边栏接回的行为一致;断线期间产生的权限审批必须重新弹出,用户响应后会话继续。
- 假设 断线期间远端 daemon 退出或远端机器重启,当 重连成功,则 会话必须如实显示为未运行并按既有「从转录恢复」流程处理,不得出现幽灵「运行中」标识。
- 假设 失败原因属于不可恢复类(认证失败——密钥被拒或主机密钥变更、主机已从
~/.ssh/config移除、用户主动断开该主机的连接),当 判定成立,则 必须立即停止自动重连,不再重拨。 - 假设 自动重连已放弃(不可恢复原因,或已无待接回目标仍停留失败),当 序列结束,则 该分屏必须显示与既有「恢复会话失败」一致的可操作错误提示与手动重试入口,会话树状态如实呈现(不得显示幽灵「运行中」);用户点击侧边栏该会话仍可手动连上。
- 假设 同一远程主机的多个分屏绑定了多个会话,当 连接断开并重连成功,则 所有分屏必须接回各自的会话,按(主机,sessionId)寻址互不串话。
- 假设 自动重连准备接回某会话时,该会话已在断线期间被删除(本机或其它客户端删除),当 重连前核验,则 必须跳过它、不得把它从磁盘转录恢复回来(不得出现「删掉的会话又自己冒出来」);对应分屏按「会话已删除」处理。
- 假设 本地会话,当 发生异常断开(仅存在于进程生命周期内,正常使用不触发),则 不启用自动重连,本地会话行为与现状完全一致。
用户故事:远程 daemon 常驻(空闲不退出)(优先级:P2)
作为用户,我希望远端主机上的 daemon 一经拉起即常驻运行——即使所有会话都已停止、会话内没有后台任务(后台 bash、子代理、工作流)且没有任何客户端连接也不自动退出,以便任务完成后再次连接仍接回同一进程,内存态(挂起审批、未落盘消息)持续保留;daemon 只在被外部回收时消失(CLI 升级重启 / 远端机器重启 / 手动 kill)。
为什么是这个优先级:原「空闲自动退出」在任务完成后回收 daemon,但会清空进程内存态并要求会话从磁盘转录重新载入。改为常驻后与桌面端既有回收机制配合——CLI 升级时重启 daemon 终止旧代码进程(见 desktop-distribution.md「内置 CLI 一致保障」场景 3/4),远端机器重启时进程随系统消失;实现集中在 code 侧 daemon 服务器(移除空闲计时器),桌面端零改动。
独立测试:在远程主机开始一个耗时任务后关闭应用,验证任务完成(含所有后台任务/子代理结束)后 daemon 仍保持运行(socket 探测成功、同一进程仍在),期间重新打开应用连接直接接回同一 daemon;对 daemon 执行 CLI 升级重启或重启远端主机后重新连接,验证历史会话从远端转录恢复、无幽灵「运行中」状态。
验收场景:
- 假设 远端 daemon 中所有会话均已停止(无生成、无排队消息)、会话内无任何后台任务,且无客户端连接,当 该空闲状态持续(远超原 60 秒宽限期),则 daemon 必须保持运行不自动退出——socket 继续监听、同一进程继续服务;期间重新连接直接接回同一 daemon,进程内存中的会话注册表与挂起状态(含待确认审批)得以保留。
- 假设 远端 daemon 因 CLI 升级被重启(终止旧 daemon 并启动新 daemon,见 desktop-distribution.md「内置 CLI 一致保障」)或远端机器重启,当 重新连接该主机,则 daemon 以新进程启动,历史会话从远端转录恢复,断线前正在进行的任务终止(与 daemon 退出的既有语义一致),不得出现幽灵「运行中」状态或脏数据。
- 假设 远端 daemon 启动后从未创建会话(无任何会话)且无客户端连接,当 该状态持续,则 daemon 同样保持运行不自动退出(不因「无会话」退出);无用的 daemon 由用户按既有方式回收(删除会话后 kill 进程或重启主机)。
边界情况
远端不探测 Node.js:连接远程主机时既不跑
node -v、也不读登录 shell 的 PATH——远端未装 Node、版本低于 22、由 nvm 管理、或装了却不可执行,一律不影响连接:应用总是推送匹配远端平台的托管运行时并用它启动 daemon(见 desktop-distribution.md「远端主机无需预装 Node」)。仅当桌面端取不到该运行时(无网 / 镜像无该平台包 / 校验不通过)时,才向用户显示可操作的错误(含失败原因、实际请求的 URL 与WAVE_NODE_MIRROR指引),而不得回退到远端已有的系统 Node——那会把「我们验证过的运行时」换成「版本未知、能否执行未知」的一份。首次连接(含下载并推送运行时、同步 CLI)期间的进度文案属跨客户端口径,见 stdio-transport.md「首次启动的进度文案」;它在桌面端显示于恢复动画下方(desktop-sessions.md 场景 9)。远端 rg 自取失败只降级、不阻断:rg/sharp 由远端 CLI 在启动期自己从 npmmirror 取(既有机制),远端无出网或 registry 不可达时 daemon 照常启动——Grep 报「ripgrep is not available」、超限图片走降级路径,网络恢复后下次启动自动补齐;不得为此弹初始化错误、不得把连接判死、不得无限重试。这与运行时走 ssh 推送是两条独立的出网侧(见 desktop-distribution.md「运行时的出网侧与 rg 不同」)。
远端主机不可达时不得删除会话/最近目录(SSH 远程适用):目录存在性校验(如
ssh test -d)失败必须区分「远端不可达」(ssh 连接失败、主机离线、探测超时)与「目录确实不存在」。当 远端不可达,则 会话索引记录与最近工作目录必须保留(不得按「目录已删除」逻辑清理),仅提示无法连接主机、用户可稍后重试;当 确认目录确实不存在,则 按既有逻辑从会话列表/最近列表移除。ssh 命令不可用:本机未安装 ssh(如 Windows 未启用 OpenSSH 客户端)时,应用必须给出安装指引,不得直接抛错。
~/.ssh/config 不存在或不可读:主机列表为空(仅「本地」与「添加主机…」),不报错;添加主机时按需创建文件。
~/.ssh/config 写入失败(权限不足、文件被占用等):添加主机失败时必须显示错误,不得影响本地会话。
添加主机名冲突:与现有
Host同名时提示,不覆盖、不重复写入。首次连接主机密钥未知:自动接受并写入 known_hosts(
StrictHostKeyChecking=accept-new);known_hosts 中密钥与远端不符时连接失败并提示用户手动执行ssh <host>排查。连接超时或主机不可达:在 ConnectTimeout 内快速失败并提示,不得长时间挂起。
远程会话删除/清理:删除远程 worktree 会话时通过该主机客户端在远端执行清理,与本地清理行为一致;ssh 子进程随应用退出一并终止。
会话记录在磁盘上不存在或中段损坏:会话恢复失败且原因为文件缺失/损坏(CLI 报
Session … not found on disk/Session not found:,本地与远端一致),当 恢复结束,则 必须自动从会话列表移除该条目(会话索引与输入草稿一并清理,最近工作目录保留)并 toast 提示原因(如「会话记录在磁盘上不存在或已损坏,无法恢复,已从列表移除」),分屏回落新会话态——不得向对话流推送原始英文错误、不得让条目留在列表中可被反复点击制造重复报错。仅「文件缺失/损坏」类错误走自动移除;网络不可达、连接中断等瞬时错误维持现状(保留条目与错误提示,见上文「远端主机不可达时不得删除会话」)。会话还在进程池里、但已被别的客户端销毁:同一口径覆盖「池里那个 agent 仍在、会话已被远控页 / 另一台客户端
destroy掉」的情形——此时点侧栏条目走的是「活会话秒切」那条路(不经过上面的恢复流程),随后为刷新分屏而读消息会答Session not found:;当 该读取失败,则 必须按上一条同样的方式清理(移除条目 + toast + 分屏回落新会话),不得只把界面维持原样(那表现为「点了没反应、停在新对话页」)。唯一的例外是配置重建窗口:改模型、登录、登出后宿主会逐条updateConfig,bridge 在重建期间会临时摘掉该会话的注册表项、读消息同样答Session not found:——这一窗口内不得据此判定「会话已被删除」,否则会误删还活着的条目。判据因此是「读失败 + 不在重建窗口内 + 错误属于缺失/损坏类」,三者同时成立才清理。/resume在当前主机不可达时:给出可见失败提示(无法连接主机/无法读取会话),不得长时间挂起、不得静默显示空列表(会误导用户以为没有历史会话)、也不得据此清理会话索引(见「远端主机不可达时不得删除会话」);当前会话与内容不受影响。/resume的目录缺失判定沿用既有口径:只有确认目录确实不存在(ssh test -d返回不存在)才提示「目录已不存在」;探测失败(主机不可达)不得按目录缺失处理,也不得据此清理会话索引(见上文「远端主机不可达时不得删除会话」)。断线与用户意图同时发生:应用正在退出、用户主动关闭该主机连接、或该会话正被删除时不得重连——自动重连只服务于「意外断开」,不得把用户刚关掉/删掉的东西拉回来(会话删除后不得从转录恢复,见「SSH 远程会话断线自动重连」场景 16)。
重连期间主机配置被编辑(
~/.ssh/config改名或删除该主机):以最新配置为准,旧连接废弃,不再按旧地址重拨;主机已被移除时按不可恢复原因停止重连。多台主机同时断开:各主机独立重连、互不影响——一台连不上不得阻塞或拖慢另一台的接回。
重连刚成功又立刻断开:计入抖动计数,按「连接不稳定」拉长间隔,不得退化成无限快速重拨。
非目标(明确排除)
SSH 交互式认证:仅支持 SSH 密钥与 ssh-agent,不支持密码/键盘交互式输入。
本地 stdio 模式的生命周期:本地
--stdio维持现状(随 stdin 关闭/宿主退出而终止),不常驻;仅 daemon(--daemon)常驻——它既服务 SSH 远端主机,也服务本机的后台委派会话(wave daemon子命令与 wave-daemon 技能,见 multi-agent/daemon-command.md)。Windows 作为 SSH 远端主机:远端主机必须是 POSIX(Linux / macOS)——远端 daemon 的启动/探活/重启依赖
nohup、test -S、pkill -f与登录 shell(bash/zsh -lic),且 OpenSSH 无法把-L转发指向 Windows 命名管道。Windows 本机可以跑 daemon(那是本机用法、不经 SSH 隧道,见 multi-agent/daemon-command.md「平台适配:默认 socket 的形态」);Windows 作为客户端连 POSIX 远端主机是支持的(本地转发端用 127.0.0.1 TCP 端口)。远程会话的差异/终端/预览面板:远程会话支持全部三类面板(差异/终端经 ssh 执行、预览经 SSH 端口转发),面板打开/复用可用;本地会话不受影响。
SSH 端口转发:按 localhost 链接点击按需建立
ssh -N -L转发(仅绑定回环、引用按会话持有);隧道销毁时机仅一种——删除该会话(从树中移除);跨主机切换、同一对话点击不同链接(无论路径还是 origin 不同)、关闭/重开预览面板、pane 卸载/移动、焦点切换均不销毁;ssh 转发进程自身意外退出(被动失败)与应用退出(dispose)时同样终止并释放本地端口;不提供常驻转发列表或手动端口管理界面,转发进程退出后本地端口立即释放。远端目录浏览的深度:浏览面板仅做单级目录列表(点击进入子目录/上级、面包屑跳转、输入完整路径确认);筛选关键词仅作用于当前单级列表,不做跨层级模糊搜索;目录项经 ssh 在远端读取;不做递归目录树、文件预览、目录收藏与重命名/创建等文件管理操作。
主机管理界面(编辑/删除):首版仅提供「添加主机…」写入
~/.ssh/config,编辑/删除由用户直接修改配置文件。~/.ssh/config 的 Include 与通配符解析:主机列表仅展示本文件顶层
Host块名称(跳过通配符);连接参数由 ssh 自身按配置文件解析。主机选择与远程连接状态持久化:重启回到「本地」与全新开始,不恢复上次主机选择;远程连接不跨重启复用。
应用启动时的后台重连:启动时不主动为任何远程主机拨号、也不后台接回上次运行的会话;连接仍由用户打开会话或既有分屏恢复触发(见「SSH 远程会话断线自动重连」只处理运行期间的意外断开)。