13 KiB
Agent Note: Workspace UI 完整产品动线
English | 中文
Status: implemented
Problem
Domain KV storage 与 Workspace entity定义了 Workspace 的持久实体、路径规范和有序 Session 账本,但没有定义 Host 接线、历史数据初始化或 GUI 动线。GUI 同时呈现 Workspace 和 Session;用户进入 New Session 后必须立即输入,即使此时还没有 Host Session,甚至没有 Host Workspace。
待创建 Workspace、待创建 Session、输入保留与 Host 实体发布必须具有明确所有者,并在 RPC completion 与 Host frame 以任意顺序到达时保持同一页面身份。若零态提前创建 Host Session,则无输入的页面状态会进入 Host 生命周期。历史 Session 又只有轻量 SessionHeader.cwd 可用于归组,初始化不能读取事件正文。
Decision
Host 与持久数据
Host 在 Workspace entity 上提供以下 GUI 接线:
| RPC | 行为 |
|---|---|
workspace.list |
返回持久有序的 Workspace,并过滤未通过 header 校验的 Session id |
workspace.create({ name }) |
在 workspaceRoot/name 创建目录和 Workspace;显示名冲突时失败 |
workspace.create({ path }) |
收编已经存在的目录,不为任意路径创建目录 |
workspace.delete({ workspaceId }) |
移除 Workspace 注册记录,同时保留目录和会话日志;相关 Session 进入 Ungrouped |
session.create({ workspaceId, sessionId? }) |
从 Workspace 解析 cwd,以可选预分配 id 幂等创建 Session 并 attach |
session.create({ cwd }) |
保留给非 Workspace 调用方,创建 Ungrouped Session |
workspaceRoot 是独立 Host 配置,未配置时回退到 Host cwd;它与保存 Workspace domain 数据的 storageRoot 无关。Host stream 推送 Workspace 与 Session 增量,包括 host/workspace-removed;Client 重连后分别刷新 workspace.list 与 session.list 基线。删除注册记录的所有权与安全边界由 Workspace 注册记录删除 Agent Note定义。
Workspace 的 sessionIds 是有序候选索引。成员投影同时要求 id 位于索引且对应 SessionHeader.cwd canonical 后等于 Workspace path;SessionHeader 不增加 workspaceId。cwd 匹配但未入索引的 Session 保持 Ungrouped,索引命中但 header 缺失、cwd 无效或 cwd 不匹配的 id 被过滤。同一 Session 被两个 Workspace 索引占用属于损坏状态并 fail loud。
Workspace domain 以 durable marker 区分“从未初始化”和“已初始化但为空”。marker 未设置时,Registry 只调用 SessionPersistence.list() 读取 header 元数据,不调用 load、inspect、history 或解析事件正文;有效 cwd 按 canonical path 分组,组内 Session 与 Workspace 组均按 header createdAt 降序初始化。Bootstrap 可重入,最后才写 marker;marker 写入后,绕过 workspaceId 的新 Session 不再被自动收编。
Client 对象模型
Session 与 Workspace 从页面 Intent 阶段开始就是前端对象。
- 前端 Session 创建时预分配 SessionId,并在对象内持有 Intent target 与
pendingPrompt;Hostsession.create成功后仍是同一个 Session 对象。 - 前端 Workspace 在 materialize 前没有 WorkspaceId,并在对象内持有 create input、phase 与 error;Host
workspace.create成功后同一个 Workspace 对象 adopt 返回的 view。 SessionManager与WorkspaceManager负责对象索引、Host 基线和增量合并;对象是 Intent 与 Host view 的唯一状态源。SessionsService提供 Session 对象、真实 selection、scope 与列表投影;WorkspacesService依赖SessionsService,负责默认 Workspace、跨对象 New Session 动线和 Workspace materialize。
页面至多有一个前端 Session Intent 和一个仅在零 Workspace 状态下配套的 Workspace Intent。Intent 只存在于当前页面,刷新后消失;真实 Session selection 可以持久恢复。选择真实 Session 或启动另一个 Session Intent 会放弃旧 Intent 的自动发送资格,但已经由 Host 发布的 Session 和已经接受的消息不会回滚。
Session 自己持有首条输入并驱动一条内部流水线:必要时以预分配 id attach 到 Workspace,然后发送 pendingPrompt。attach 与 send 的失败都落回同一 Session。Workspace 创建 phase/error 只属于 Workspace 对象,Session 不模拟 Workspace 生命周期。
用户动线
应用首次进入时等待 Workspace 与 Session 两份基线 ready。仍有效的真实 Session selection 被恢复;否则进入 New Session,并固定选择一次最近 Workspace。最近 Workspace 取其成员 Session 的最大 updatedAt,空 Workspace 回退到 createdAt;该派生只决定默认目标,不改变 Host Workspace 顺序,也不会在后续 hydration 时二次改选。
完全没有 Workspace 时,页面创建默认名为 workspace 的前端 Workspace 对象和指向它的前端 Session。两者不写 Host,composer 始终可输入;首次发送才依次 materialize Workspace、attach Session、发送消息。
顶部 New Session、Workspace 行内加号和 Workspace picker 最终都调用同一 New Session 动作:显式 Workspace id 直接成为目标,未指定时使用最近 Workspace,没有真实 Workspace 时使用 Workspace Intent。Workspace picker 的 Use an existing folder 与 Create a new workspace 会在用户确认时立即创建真实 Workspace,再把前端 Session 定位到该 Workspace;即使用户不发送消息,显式创建的空 Workspace 也保留。
Create a new workspace 暂时用同一个输入作为目录名和显示名。UI 根据当前 Workspace title 禁止重复确认,Host 继续拒绝绕过 UI 或并发产生的同名请求。跨 Workspace 移动 Session、从 Ungrouped 手动收编以及分别输入显示名和目录名仍不在此动线范围内。
首次发送与恢复
前端 Session 的 pendingPrompt 在 Host 接受消息前始终保留原文。首次发送按 Workspace materialize、Session attach、prompt send 顺序推进:
- Workspace 创建失败时,Workspace Intent 保留输入与错误,Session 仍指向该对象。
- Session 创建在发布前失败时,Session Intent 回到可编辑状态,以同一预分配 SessionId 重试。
workspace-attach-failed证明 Session 已发布;同一 Session 对象进入真实列表并保留 prompt,后续重试 attach。- prompt 失败时,Session 保留 prompt 并只重试 send,不重复创建 Workspace 或 Session。
- Session 创建期间若页面切换到另一个 Intent,旧 Session 即使随后发布也不自动发送;它保留原 prompt 和可见错误。
RPC lost response、Host frame 先于 completion 和 completion 先于 Host frame 都通过预分配 SessionId 与对象身份收敛。Manager 对 Host view 做有序 upsert,本地 materialize 时优先保留原对象身份,不生成同 id 的临时第二行。
Sidebar 与排序
Workspace 组严格使用 Host 返回的持久顺序。Bootstrap 一次性确定历史顺序,显式创建的新 Workspace 放在首位;Session 活跃不会移动 Workspace 组。
组内严格使用 Workspace.sessionIds。新 attach 的 Session 放在首位,后续某个 Session 活跃时 Host 只前移该 id 并持久化。Client 不在 Session list 到达后按时间整体重排,因此不会先显示一套 Workspace 顺序再因 hydration 瞬间跳动。
前端 Session Intent 只有在目标是真实 Workspace 时才作为 “New session” 行显示,并临时计入该组 Session 数量;目标是 Workspace Intent 时,Workspace 与 Session 都不进入 sidebar。Intent 发布后由同一预分配 id 对应的真实行接替,刷新后 Intent 行和临时计数一起消失。搜索模式不保存或筛选 Intent 行。
无法归入任何 Workspace 的真实 Session 进入 Ungrouped。Host session-added 与 workspace-changed 可以任意顺序到达,列表合并不依赖 frame 顺序。
删除 Workspace 注册记录会移除其分组,但不会删除或关闭任何 Session。已记账的 Session(包括当前 Session)会立即进入 Ungrouped;刷新后,独立的 Workspace 与 Session 基线会重建出相同结果。
React 与 slot 边界
React 组件只消费 useSessions、useWorkspaces 与 session-scoped hooks,不拥有实体生命周期。Zustand store 只保留布局、当前 view、普通真实 Session 的 composer 文本和其他纯呈现状态;Session/Workspace Intent、materialize phase、错误和 retained prompt 位于 React-free runtime 对象层。
Sidebar 与 conversation empty hero 通过 slot 获得标准化动作:startSession、updateSessionPrompt、sendSession、open 与 toggleSidebar。Workspace picker 复用同一组件与 createWorkspace seam;owner 只提供 popover 开关、锚点和选中回调。呈现层不直接发送 host/workspace-changed,Host event 只由 Host mutation 与 stream adapter 产生。
Alternatives considered
为待创建 Workspace 与 Session 保存独立页面记录。 该方案在 materialize 后需要替换身份并转交输入、错误、焦点和 sidebar 行;对象自身的 Intent 状态可以保持身份连续。
由呈现层或 root Zustand store 编排对象生命周期。 该方案会重复 Manager/Service 的职责,并把领域状态带回 React。标准化动作由 runtime service 提供,slot 只注入呈现所需的窄接口。
零态立即创建 Host Session 或 Host persistence intent。 未输入页面会进入 Host 生命周期,并改变刷新语义;前端 Session 在首次发送前只保留 page-local Intent。
显式 Create Workspace 延迟到首次发送。 用户确认后 sidebar 仍看不到真实空 Workspace,“创建 Workspace”与“准备 Session”语义混合;只有系统自动产生的零 Workspace Intent 延迟 materialize。
持续按 cwd 动态派生 Workspace。 该方案无法表达空 Workspace、稳定显示名和显式顺序,也会自动收编非 Workspace 调用方;cwd 只用于一次历史 bootstrap 与成员双向校验。
Client 在 Session list 到达后按时间批量重排。 首屏会先展示 Host 顺序再整体跳动,重连也可能改变位置;排序由 Host 持久账本拥有,Client 只合并单项更新。
在 SessionHeader 增加 workspaceId。 它会与 Workspace 索引形成两个持久归属字段并要求双写;header 保留 Session 自身 cwd 事实,Workspace 索引负责显式归属。
Verification
- 完全无 Workspace 的零态不写 Host 且允许输入;显式 Create Workspace 立即创建并显示空 Workspace。
- 前端 Session 与 Workspace 在 materialize 前后保持对象身份,输入、错误、焦点和 sidebar 投影始终来自对象层。
- 首发按 Workspace、Session、prompt 顺序推进,各成功阶段不回滚,输入在 prompt 接受前不丢失,创建重试使用同一 SessionId。
- Workspace list 只读取 header 完成一次可重入 bootstrap;initialized 的空 registry 重启不重复初始化,成员读取同时校验索引与 canonical cwd。
- 初始默认目标只在两份基线 ready 后确定一次;Workspace 组不因 hydration 或 Session 活跃整体重排,单个活跃 Session 只前移自身。
- 真实 Workspace 下的前端 Session 临时计入 sidebar 数量,Workspace Intent 保持隐藏,发布与刷新都不会留下重复行或重复计数。
- UI 与 Host 两层拒绝同名 Workspace;cwd-only Session、无效历史 cwd 和未 attach Session 保持 Ungrouped。
- 经确认的 Workspace 删除只移除注册记录,保留当前 Session、目录、文件和会话日志,并在刷新后保持该状态;包级测试固定一元响应/帧/基线竞态和失败回滚行为。
- keyless runnable snapshot 覆盖零态、显式创建和首次发送;包级测试覆盖 bootstrap、成员校验、排序、幂等、失败恢复及任意 frame 顺序。
Consequences
- SessionHeader 不记录最后活跃时间,历史 bootstrap 只能按
createdAt初始化;此后由真实 Session 活跃事件逐项前移。 - 历史 cwd 缺失、目录无效或 realpath 失败的 Session 留在 Ungrouped;本期没有手动收编入口。
- 页面刷新会丢弃未 materialize 的 Workspace/Session Intent 和尚未被 Host 接受的输入,这是 page-local 契约。
- 显式 Create Workspace 立即落盘,用户不发送就离开也会留下空 Workspace。
- Host Session 在首个事件前仍遵循现有懒持久化语义;前端 Intent 不改变 Host 重启后的空 Session 行为。