docs,test: settle review follow-ups for the create-by-name deletion

Rewrite the three sibling Agent Note pairs that still described create-by-name
as current (workspace-ui-product-flow, session-list-browsing-and-manual-order,
same-basename-workspace-adoption) and the one-route note's own alternative and
section title; delete scripts/hero-composer-dom-continuity.mjs, which drove the
name dialog removed by the one-route change; mark WorkspaceRegistry.create's
now test-only title parameter with a deletion TODO; pin the retired { name }
spelling as a schema rejection; align the workspace spec on stageDir and the
fixture spec title on path creates.
This commit is contained in:
creatixchu
2026-08-07 14:48:26 +08:00
parent fbd3a6272b
commit 40ee7f5e27
17 changed files with 34 additions and 109 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-31-same-basename-workspace-adoption.md
2026-07-31-same-basename-workspace-adoption.md: ed53804ea64df0d61db16e579c3d65af803dbb97
2026-07-31-same-basename-workspace-adoption.zh.md: 82cfb7d90afca28f8e666742a758fda0202909f3
2026-07-31-same-basename-workspace-adoption.md: 1192558632fdc8bd732ea59f0eca5051f49f5d2c
2026-07-31-same-basename-workspace-adoption.zh.md: 9c1f1ffd221936e24300b223ad395e1f44552810

View File

@@ -14,7 +14,7 @@ A Workspace is identified by its stable id and canonical directory path, while i
The Host's `workspace.create({ path })` adoption route inherits that rule. The Workspace manager, picker, grouping tree, selection, rename, deletion, and Session creation continue to use `WorkspaceId`, so equal labels neither merge records nor redirect an operation. The sidebar hover card exposes each canonical path when the labels need disambiguation.
Explicit naming remains stricter. `workspace.create({ name })` and `workspace.rename` continue to reject a title already registered, as described by [manual Workspace naming](../feature/2026-07-25-session-list-browsing-and-manual-order.md). This prevents a user from deliberately introducing another ambiguous label while accepting collisions imposed by existing directory names. The path-adoption rule supersedes only the title-conflict clauses in the [Workspace product flow](../feature/2026-07-25-workspace-ui-product-flow.md) and [native directory picker](../feature/2026-07-27-native-workspace-directory-picker.md).
Explicit naming remains stricter. `workspace.rename` continues to reject a title already registered, as described by [manual Workspace naming](../feature/2026-07-25-session-list-browsing-and-manual-order.md). This prevents a user from deliberately introducing another ambiguous label while accepting collisions imposed by existing directory names. The path-adoption rule supersedes only the title-conflict clauses in the [Workspace product flow](../feature/2026-07-25-workspace-ui-product-flow.md) and [native directory picker](../feature/2026-07-27-native-workspace-directory-picker.md).
The durable schema does not change: Workspace records already store id, path, and title independently, bootstrap can derive equal basenames, and startup validates duplicate paths rather than titles.
@@ -30,7 +30,7 @@ Workspace registry and Host API tests create two real directories under differen
**Use the full path as every Workspace title.** This removes the collision but makes the primary navigation label unnecessarily long. The full path remains available in the hover detail while the concise basename stays useful.
**Permit collisions from explicit rename and create-by-name operations too.** The registry supports that state, but those operations intentionally ask the user to choose a display name. Retaining their conflict response preserves the existing naming guard without blocking filesystem-selected paths.
**Permit collisions from the explicit rename operation too.** The registry supports that state, but rename intentionally asks the user to choose a display name. Retaining its conflict response preserves the existing naming guard without blocking filesystem-selected paths.
## Consequences

View File

@@ -14,7 +14,7 @@ Workspace 的身份由其稳定 id 和规范目录路径确定,标题则是可
Host 的 `workspace.create({ path })` 接纳入口沿用该规则。Workspace 管理器、选择器、分组树、选择、重命名、删除和 Session 创建仍使用 `WorkspaceId`,因此相同标签既不会合并记录,也不会把操作指向其他记录。需要区分相同标签时,侧边栏悬停详情卡会显示各自的规范路径。
显式命名仍采用更严格的规则。`workspace.create({ name })``workspace.rename` 仍会拒绝已注册的标题,具体见[手动 Workspace 命名](../feature/2026-07-25-session-list-browsing-and-manual-order.md)。这既防止用户主动引入另一个难以区分的标签,又允许既有目录名称造成的重名。路径接纳规则仅取代 [Workspace 产品流](../feature/2026-07-25-workspace-ui-product-flow.md)和[原生目录选择器](../feature/2026-07-27-native-workspace-directory-picker.md)中的标题冲突条款。
显式命名仍采用更严格的规则。`workspace.rename` 仍会拒绝已注册的标题,具体见[手动 Workspace 命名](../feature/2026-07-25-session-list-browsing-and-manual-order.md)。这既防止用户主动引入另一个难以区分的标签,又允许既有目录名称造成的重名。路径接纳规则仅取代 [Workspace 产品流](../feature/2026-07-25-workspace-ui-product-flow.md)和[原生目录选择器](../feature/2026-07-27-native-workspace-directory-picker.md)中的标题冲突条款。
持久化 schema 未变Workspace 记录本就分别存储 id、path 和 title引导初始化可以派生出相同的 basename启动校验检查的是重复路径而非重复标题。
@@ -30,7 +30,7 @@ Workspace 注册表与 Host API 测试会在不同父目录下创建两个末级
**将完整路径用作每个 Workspace 的标题。** 这会消除冲突,却使主导航标签不必要地过长。完整路径仍可在悬停详情中查看,而简洁的 basename 仍有价值。
**也允许显式重命名和按名称创建操作产生重名。** 注册表支持这种状态,但这些操作本就是明确要求用户选择显示名称。保留冲突响应可维持现有命名防护,同时不阻止从文件系统选取的路径。
**也允许显式重命名操作产生重名。** 注册表支持这种状态,但操作本就是明确要求用户选择显示名称。保留冲突响应可维持现有命名防护,同时不阻止从文件系统选取的路径。
## 后果

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-25-session-list-browsing-and-manual-order.md
2026-07-25-session-list-browsing-and-manual-order.md: bd04e7f74c8a4540d68e60ad68965e05de76bce9
2026-07-25-session-list-browsing-and-manual-order.zh.md: 8ec5f71943a68a70f46fbd4c7702e4556b1892ec
2026-07-25-session-list-browsing-and-manual-order.md: 2894542e7b3b720702c764dbae4112b001e2602c
2026-07-25-session-list-browsing-and-manual-order.zh.md: 81c613a6d2ac738a993f82207ef7c3820cd751f0

View File

@@ -24,7 +24,7 @@ The group-by menu offers two modes, WorkSpace / In one list. WorkSpace mode rend
### workspace.rename
`workspace.rename({ workspaceId, title })`: the title is trimmed and must be non-blank; both the same-title no-op and the duplicate check evaluate inside the Host's serialized workspace-operation chain (shared with create-by-name, so concurrent explicit naming operations cannot interleave a duplicate or an out-of-order fake success), and a conflict returns `workspace-name-conflict`. Path adoption may derive a title already present because canonical path, not title, owns identity ([decision](../bug-fix/2026-07-31-same-basename-workspace-adoption.md)). Durability goes through `setTitle`'s mutate path, and the `domain/changed` listener broadcasts the `host/workspace-changed` frame automatically. The UI is a standard modal with a client-side duplicate pre-check.
`workspace.rename({ workspaceId, title })`: the title is trimmed and must be non-blank; both the same-title no-op and the duplicate check evaluate inside the Host's serialized workspace-operation chain (shared with path adoption and deletion, so concurrent workspace operations cannot interleave a duplicate or an out-of-order fake success), and a conflict returns `workspace-name-conflict`. Path adoption may derive a title already present because canonical path, not title, owns identity ([decision](../bug-fix/2026-07-31-same-basename-workspace-adoption.md)). Durability goes through `setTitle`'s mutate path, and the `domain/changed` listener broadcasts the `host/workspace-changed` frame automatically. The UI is a standard modal with a client-side duplicate pre-check.
### Manual order: insertSessionBefore replaces activity pinning

View File

@@ -24,7 +24,7 @@ group-by 菜单提供 WorkSpace / In one list 两种模式。WorkSpace 模式按
### workspace.rename
`workspace.rename({ workspaceId, title })`title trim 后非空;同名 no-op 与重名查重都在 Host 的 Workspace 操作串行链内求值(与按名称创建共链,并发的显式命名操作不能穿插出重名或乱序假成功),冲突返回 `workspace-name-conflict`。按路径收编可以派生出已有 title因为拥有身份的是 canonical path而不是 title见[身份决策](../bug-fix/2026-07-31-same-basename-workspace-adoption.md))。落盘经 `setTitle` 的 mutate 通道,`domain/changed` 监听自动广播 `host/workspace-changed` 帧。UI 为标准 Modalclient 侧另做重名预检。
`workspace.rename({ workspaceId, title })`title trim 后非空;同名 no-op 与重名查重都在 Host 的 Workspace 操作串行链内求值(与按路径收编和删除共链,并发的 Workspace 操作不能穿插出重名或乱序假成功),冲突返回 `workspace-name-conflict`。按路径收编可以派生出已有 title因为拥有身份的是 canonical path而不是 title见[身份决策](../bug-fix/2026-07-31-same-basename-workspace-adoption.md))。落盘经 `setTitle` 的 mutate 通道,`domain/changed` 监听自动广播 `host/workspace-changed` 帧。UI 为标准 Modalclient 侧另做重名预检。
### 手动排序:insertSessionBefore 取代活动置顶

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.md
2026-07-25-workspace-ui-product-flow.md: 7a6a41e19d2930fbcbf7ba5fc9e6809d96e23166
2026-07-25-workspace-ui-product-flow.zh.md: a40f374fd794b11bff0de72cbc822fd638cd267c
2026-07-25-workspace-ui-product-flow.md: 9f241562c2d07801b22619c8e1406984bab22aba
2026-07-25-workspace-ui-product-flow.zh.md: 7fca17d32837deede4fb751ca4317582d796e005

View File

@@ -19,13 +19,12 @@ The Host provides the following GUI wiring on the Workspace entity:
| RPC | Behavior |
| --- | --- |
| `workspace.list` | Returns persistent Workspaces in order and filters out Session ids that fail header validation |
| `workspace.create({ name })` | Creates a directory and Workspace at `workspaceRoot/name`; fails on a display-name conflict |
| `workspace.create({ path })` | Adopts an existing directory by canonical path; basename-derived display titles may repeat |
| `workspace.delete({ workspaceId })` | Removes the Workspace registration while retaining its directory and session logs; its Sessions become Ungrouped |
| `session.create({ workspaceId, sessionId? })` | Resolves cwd from the Workspace, idempotently creates a Session with an optional preallocated id, and attaches it |
| `session.create({ cwd })` | Remains available to non-Workspace callers and creates an Ungrouped Session |
`workspaceRoot` is an independent Host setting that falls back to the Host cwd when unset; it is unrelated to `storageRoot`, which stores Workspace domain data. The Host stream pushes Workspace and Session deltas, including `host/workspace-removed`, and the Client refreshes the `workspace.list` and `session.list` baselines separately after reconnecting. Registration-deletion ownership and safety are defined in the [Workspace registration deletion Agent Note](2026-07-27-workspace-registration-deletion.md).
The Host stream pushes Workspace and Session deltas, including `host/workspace-removed`, and the Client refreshes the `workspace.list` and `session.list` baselines separately after reconnecting. Registration-deletion ownership and safety are defined in the [Workspace registration deletion Agent Note](2026-07-27-workspace-registration-deletion.md).
A Workspace's `sessionIds` is an ordered candidate index. A membership projection requires both that an id appear in the index and that the corresponding canonicalized `SessionHeader.cwd` equal the Workspace path; SessionHeader does not gain a `workspaceId`. A Session whose cwd matches but whose id is absent from the index remains Ungrouped, while an indexed id is filtered out if its header is missing, its cwd is invalid, or its cwd does not match. Two Workspace indexes claiming the same Session is corrupt state and fails loudly.
@@ -52,7 +51,7 @@ When no Workspace exists, the page creates a frontend Workspace object named `wo
Top-level New Session, the plus button on a Workspace row, and the Workspace picker all invoke the same New Session action. An explicit Workspace id becomes the target directly; when none is specified, the action uses the most recent Workspace, or the Workspace Intent if no real Workspace exists. The Workspace picker's one Add workspace action ([one-route Note](../simplification/2026-07-31-one-route-to-add-a-workspace.md); it was a pair of Use-an-existing-folder and create-by-name actions when this was decided) immediately creates a real Workspace when the user confirms a directory, then retargets the frontend Session to it; an explicitly created empty Workspace remains even if the user sends no message.
A new Workspace takes its display name from the directory it was created in. Distinct canonical paths may share the same basename-derived title ([identity decision](../bug-fix/2026-07-31-same-basename-workspace-adoption.md)); explicit create-by-name and rename operations retain their duplicate-title checks. Moving Sessions across Workspaces, manual adoption from Ungrouped, and separate display-name and directory-name inputs remain outside this flow.
A new Workspace takes its display name from the directory it was created in. Distinct canonical paths may share the same basename-derived title ([identity decision](../bug-fix/2026-07-31-same-basename-workspace-adoption.md)); the explicit rename operation retains its duplicate-title check. Moving Sessions across Workspaces, manual adoption from Ungrouped, and separate display-name and directory-name inputs remain outside this flow.
### First send and recovery
@@ -108,7 +107,7 @@ The Sidebar and conversation empty hero receive standardized actions through slo
- Workspace list performs one reentrant bootstrap using only headers; an initialized empty registry does not initialize again after restart, and membership reads validate both the index and canonical cwd.
- The initial default target is determined exactly once after both baselines are ready; Workspace groups are not reordered as a whole by hydration or Session activity, and an active Session moves only itself to the front.
- A frontend Session under a real Workspace temporarily counts toward the sidebar total, while a Workspace Intent remains hidden; neither publication nor refresh leaves duplicate rows or counts.
- The UI and Host admit distinct same-basename directories as separate Workspaces, while explicit create-by-name and rename operations reject duplicate titles; cwd-only Sessions, Sessions with invalid historical cwd values, and unattached Sessions remain Ungrouped.
- The UI and Host admit distinct same-basename directories as separate Workspaces, while the explicit rename operation rejects duplicate titles; cwd-only Sessions, Sessions with invalid historical cwd values, and unattached Sessions remain Ungrouped.
- Confirmed Workspace deletion removes only the registration, retains the current Session, directory, files, and session log, and survives reload; package tests pin unary/frame/baseline races and failure rollback.
- Keyless runnable snapshots cover the zero state, explicit creation, and the first send; package-level tests cover bootstrap, membership validation, ordering, idempotency, failure recovery, and arbitrary frame order.

View File

@@ -19,13 +19,12 @@ Host 在 Workspace entity 上提供以下 GUI 接线:
| RPC | 行为 |
| --- | --- |
| `workspace.list` | 返回持久有序的 Workspace并过滤未通过 header 校验的 Session id |
| `workspace.create({ name })` | 在 `workspaceRoot/name` 创建目录和 Workspace显示名冲突时失败 |
| `workspace.create({ path })` | 按 canonical path 收编已有目录;由 basename 派生的显示名可以重复 |
| `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](2026-07-27-workspace-registration-deletion.md)定义。
Host stream 推送 Workspace 与 Session 增量,包括 `host/workspace-removed`Client 重连后分别刷新 `workspace.list``session.list` 基线。删除注册记录的所有权与安全边界由 [Workspace 注册记录删除 Agent Note](2026-07-27-workspace-registration-deletion.md)定义。
Workspace 的 `sessionIds` 是有序候选索引。成员投影同时要求 id 位于索引且对应 `SessionHeader.cwd` canonical 后等于 Workspace pathSessionHeader 不增加 `workspaceId`。cwd 匹配但未入索引的 Session 保持 Ungrouped索引命中但 header 缺失、cwd 无效或 cwd 不匹配的 id 被过滤。同一 Session 被两个 Workspace 索引占用属于损坏状态并 fail loud。
@@ -52,7 +51,7 @@ Session 自己持有首条输入并驱动一条内部流水线:必要时以预
顶部 New Session、Workspace 行内加号和 Workspace picker 最终都调用同一 New Session 动作:显式 Workspace id 直接成为目标,未指定时使用最近 Workspace没有真实 Workspace 时使用 Workspace Intent。Workspace picker 的单一 Add workspace 动作(见[单一路径 Note](../simplification/2026-07-31-one-route-to-add-a-workspace.md);本决策做出时是 Use an existing folder 与按名称创建两个动作)会在用户确认目录时立即创建真实 Workspace再把前端 Session 定位到该 Workspace即使用户不发送消息显式创建的空 Workspace 也保留。
新建 Workspace 的显示名取自其所在目录。不同 canonical path 可以拥有相同的 basename 派生显示名(见[身份决策](../bug-fix/2026-07-31-same-basename-workspace-adoption.md));显式的按名称创建和重命名操作仍保留显示名重名检查。跨 Workspace 移动 Session、从 Ungrouped 手动收编以及分别输入显示名和目录名仍不在此动线范围内。
新建 Workspace 的显示名取自其所在目录。不同 canonical path 可以拥有相同的 basename 派生显示名(见[身份决策](../bug-fix/2026-07-31-same-basename-workspace-adoption.md));显式的重命名操作仍保留显示名重名检查。跨 Workspace 移动 Session、从 Ungrouped 手动收编以及分别输入显示名和目录名仍不在此动线范围内。
### 首次发送与恢复
@@ -108,7 +107,7 @@ Sidebar 与 conversation empty hero 通过 slot 获得标准化动作:`startSe
- Workspace list 只读取 header 完成一次可重入 bootstrapinitialized 的空 registry 重启不重复初始化,成员读取同时校验索引与 canonical cwd。
- 初始默认目标只在两份基线 ready 后确定一次Workspace 组不因 hydration 或 Session 活跃整体重排,单个活跃 Session 只前移自身。
- 真实 Workspace 下的前端 Session 临时计入 sidebar 数量Workspace Intent 保持隐藏,发布与刷新都不会留下重复行或重复计数。
- UI 与 Host 会将 canonical path 不同但 basename 相同的目录接纳为独立 Workspace而显式的按名称创建和重命名操作会拒绝重复显示名cwd-only Session、无效历史 cwd 和未 attach Session 保持 Ungrouped。
- UI 与 Host 会将 canonical path 不同但 basename 相同的目录接纳为独立 Workspace而显式的重命名操作会拒绝重复显示名cwd-only Session、无效历史 cwd 和未 attach Session 保持 Ungrouped。
- 经确认的 Workspace 删除只移除注册记录,保留当前 Session、目录、文件和会话日志并在刷新后保持该状态包级测试固定一元响应基线竞态和失败回滚行为。
- keyless runnable snapshot 覆盖零态、显式创建和首次发送;包级测试覆盖 bootstrap、成员校验、排序、幂等、失败恢复及任意 frame 顺序。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.md
2026-07-31-one-route-to-add-a-workspace.md: 853d641e0a2c0044ee7bfd6ed42bcc3763520192
2026-07-31-one-route-to-add-a-workspace.zh.md: 6a2884d75138ddc241276131f7ff85080fc5d794
2026-07-31-one-route-to-add-a-workspace.md: d0a1a820a8eb0a47245d99635b5e1e0448d7eca5
2026-07-31-one-route-to-add-a-workspace.zh.md: 3486fcdae24a77d3ba82234355ba52d5f43e22d0

View File

@@ -25,7 +25,7 @@ The direct-open path carries the busy rule the menu entry states: while a pick i
`WorkspaceCreateFlow` is now `WorkspacePickFlow` and its `createOnly` prop is `addOnly`; the injected `createWorkspace` narrows from `{ name } | { path }` to `{ path }`.
## Wire and CLI residue
## Wire and CLI follow-up (shipped)
Deleted in the follow-up change this section used to scope: `workspace.create` accepts only `{ path }` (the `name` member left the wire schema and `WorkspaceApi`), the gateway lost its `workspaceRoot` config and default, the client seam narrowed to the path spelling (`WorkspaceCreateInput`, `WorkspacesService.create`, `intentName`), and the `dsh web --workspace-root` flag is gone together with its `apps/cli` reference lines. `workspace-name-conflict` remains on the wire as `workspace.rename`'s duplicate-title error.
@@ -45,7 +45,7 @@ Deleted in the follow-up change this section used to scope: `workspace.create` a
**Keep the menu shell for entries we might add later (clone a repo, remote directory).** Rejected under "require a current owner and need": no such entry exists, and restoring a menu when one arrives is a smaller change than shipping an empty frame now.
**Delete the wire's create-by-name branch in the same change.** Rejected for this PR: it is backend/CLI surface with a different reviewer and a wider test fallout, and the urgent decision is the UI. See the residue section — it is marked, not forgotten.
**Delete the wire's create-by-name branch in the same change.** Rejected for the UI PR: it was backend/CLI surface with a different reviewer and a wider test fallout, and the urgent decision was the UI. The deletion shipped as its own follow-up change; the follow-up section above records what it removed.
**Register the workspace through the host in the e2e scaffold instead of driving the dialog.** Rejected: it would have decoupled all 15 scenarios from the picker, so nothing in the lane would prove the surviving route reaches a live composer. Every scenario now walks the real dialog to adopt its directory; only the create-a-folder half is concentrated in one scenario, because repeating it everywhere makes the shared helper non-idempotent for no extra signal.

View File

@@ -25,7 +25,7 @@ Status: implemented
`WorkspaceCreateFlow` 现更名为 `WorkspacePickFlow`,其 `createOnly` prop 更名为 `addOnly`;注入的 `createWorkspace``{ name } | { path }` 收窄为 `{ path }`
## Wire and CLI residue
## Wire and CLI follow-up (shipped)
本节曾划定的后续删除已经落地:`workspace.create` 只接受 `{ path }``name` 成员已从 wire schema 与 `WorkspaceApi` 移除),网关失去了 `workspaceRoot` 配置及其默认值,客户端 seam 收窄为 path 写法(`WorkspaceCreateInput``WorkspacesService.create``intentName``dsh web --workspace-root` flag 连同其 `apps/cli` reference 文档行一并删除。`workspace-name-conflict` 仍留在 wire 上,作为 `workspace.rename` 的重名错误。
@@ -45,7 +45,7 @@ Status: implemented
**为将来可能新增的入口(克隆仓库、远程目录)保留菜单壳。** 否决,依据"require a current owner and need":这样的入口目前并不存在,而等它到来时再恢复菜单,比现在就发一个空壳的改动更小。
**在同一改动中删除 wire 的按名称创建分支。** PR 否决:那是 backend/CLI 面reviewer 不同、测试波及面更广,而紧急的决定是 UI。见 residue 一节——它是被标记了,不是被遗忘了
**在同一改动中删除 wire 的按名称创建分支。** UI PR 否决:那是 backend/CLI 面reviewer 不同、测试波及面更广,而当时紧急的决定是 UI。删除随后作为独立的后续改动落地,上文 follow-up 一节记录了它移除的内容
**在 e2e scaffold 中经 host 注册 workspace而不驱动对话框。** 否决:那会让全部 15 个场景与选择器解耦,整条 lane 将无法证明幸存的这条路径能走到可用的 composer。现在每个场景都会走真实对话框来接纳自己的目录只有"新建文件夹"那一半集中在一个场景里,因为处处重复只会让共享辅助函数失去幂等性,却换不来额外信号。

View File

@@ -535,7 +535,7 @@ describe('createFixtureApi', () => {
expect(reused.result.value).toMatchObject({ created: false, workspace: { workspaceId: 'fx-ws-fixture' } })
})
it('workspace.create by name mints a new entity and pushes host/workspace-changed', async () => {
it('workspace.create on a fresh path mints a new entity and pushes host/workspace-changed', async () => {
const api = createFixtureApi()
const abort = new AbortController()
const seen: HostFrame[] = []

View File

@@ -252,8 +252,7 @@ describe('host.openPath', () => {
describe('workspace.create', () => {
it('serializes concurrent creates of one path into a single registration', async () => {
const { api, root } = await harness()
const target = join(root, 'alpha')
mkdirSync(target)
const target = stageDir(root, 'alpha')
const responses = await Promise.all([
api.workspace.create(request({ path: target })),
api.workspace.create(request({ path: target })),
@@ -269,8 +268,7 @@ describe('workspace.create', () => {
it('adopts only existing directories', async () => {
const { api, root } = await harness()
const existing = join(root, 'existing')
mkdirSync(existing)
const existing = stageDir(root, 'existing')
const first = expectOk(await api.workspace.create(request({ path: existing })))
const repeated = expectOk(await api.workspace.create(request({ path: existing })))
expect(first).toMatchObject({ created: true, workspace: { path: existing, title: 'existing' } })

View File

@@ -327,6 +327,8 @@ describe('workspace domain schemas', () => {
it('create requires a path', () => {
expect(workspaceCreateRequestSchema.parse({ path: '/p' }).path).toBe('/p')
expect(() => workspaceCreateRequestSchema.parse({})).toThrow()
// The retired create-by-name spelling stays a clean schema rejection.
expect(() => workspaceCreateRequestSchema.parse({ name: 'n' })).toThrow()
expect(workspaceCreateValueSchema.parse({ workspace: view, created: false }).created).toBe(false)
})

View File

@@ -139,6 +139,11 @@ export class WorkspaceRegistry extends Service {
* @param title - Display title used only when a new record is created.
* @returns the existing or newly durable workspace.
*/
// TODO: `title` lost its last production caller when the gateway's
// create-by-name branch was deleted
// (.agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.md);
// drop the parameter with its @param clause and the `create(path, title?)`
// lines in this package's README pair.
async create(path: string, title?: string): Promise<Workspace> {
const canonical = await realpathNormalize(path)
if (!(await stat(canonical)).isDirectory()) {

View File

@@ -1,78 +0,0 @@
// Regression drive for the unified hero composer (0729-0357-hero-unify):
// cold start with zero workspaces -> create a workspace -> type. Asserts the
// composer textarea is the SAME DOM node across the disabled->live flip (a
// remount drops the __heroMark marker property) — the session-maybe
// composer.bar contract.
//
// Prereqs: `pnpm run build`, then a fresh server against empty state:
// rm -rf .storages && DSH_HOME=$(mktemp -d) node --experimental-transform-types \
// --import ./scripts/tspath-loader.ts apps/cli/src/bin.ts web --port 44285 \
// --workspace-root $(mktemp -d)
// Run: node scripts/hero-composer-dom-continuity.mjs
// (BASE_URL overrides the target; screenshots land in .artifacts/.)
import { createRequire } from 'node:module'
// playwright is a devDependency of apps/web only — resolve through its tree.
const require = createRequire(new URL('../apps/web/package.json', import.meta.url))
const { chromium } = require('playwright')
const BASE = process.env.BASE_URL ?? 'http://127.0.0.1:44285'
const SHOTS = new URL('../.artifacts/screenshots/0729-0357-hero-unify/', import.meta.url).pathname
const browser = await chromium.launch()
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } })
page.on('console', msg => { if (msg.type() === 'error') console.log('[console.error]', msg.text()) })
page.on('pageerror', err => { console.log('[pageerror]', err.message) })
await page.goto(BASE)
await page.waitForSelector('textarea', { timeout: 20000 })
await page.screenshot({ path: SHOTS + '01-cold-start.png' })
const initial = await page.evaluate(() => {
const boxes = [...document.querySelectorAll('textarea')]
boxes.forEach((b, i) => { b.__heroMark = 'alive-' + i })
return boxes.map(b => ({ disabled: b.disabled, placeholder: b.placeholder }))
})
console.log('cold-start textareas:', JSON.stringify(initial))
// Open the picker and create a workspace by name (typed-input flow). The name
// must be unique per registry; keystrokes go through pressSequentially so the
// dialog's React onChange enables the submit button.
await page.getByRole('button', { name: 'Choose workspace' }).click()
await page.getByText('Create a new workspace').click()
await page.screenshot({ path: SHOTS + '03-create-form.png' })
const nameBox = page.getByPlaceholder('Workspace name')
await nameBox.click()
const wsName = 'proj-' + Date.now().toString(36)
await nameBox.pressSequentially(wsName, { delay: 30 })
await page.locator('button:text-is("Create workspace")').click()
// Wait for the composer to go live (placeholder flips, textarea enabled).
await page.waitForFunction(() => {
const box = document.querySelector('textarea')
return box !== null && !box.disabled
}, { timeout: 20000 })
await page.screenshot({ path: SHOTS + '04-live.png' })
const after = await page.evaluate(() => {
const boxes = [...document.querySelectorAll('textarea')]
return boxes.map(b => ({
mark: b.__heroMark ?? 'REMOUNTED',
disabled: b.disabled,
placeholder: b.placeholder,
}))
})
console.log('post-pick textareas:', JSON.stringify(after))
// Type into the live composer.
await page.locator('textarea').first().fill('hello from acceptance run')
const typed = await page.evaluate(() => document.querySelector('textarea')?.value)
console.log('typed value:', JSON.stringify(typed))
await page.screenshot({ path: SHOTS + '05-typed.png' })
const survived = after.length === 1 && after[0].mark === 'alive-0'
console.log(survived
? 'DOM-CONTINUITY: PASS (same textarea node across cold-start -> live)'
: 'DOM-CONTINUITY: FAIL ' + JSON.stringify(after))
await browser.close()
process.exit(survived && typed === 'hello from acceptance run' ? 0 : 1)