cleanup(web): give adding a Workspace one route
Both Workspace surfaces offered "Open local folder…" and "Create a new
workspace" for one outcome. The browse occupant already carries its own
New folder affordance, so picking a directory covered creating one; the
name dialog only added a second vocabulary and a create target the
operator could neither see nor choose.
The surviving entry is named after the outcome — "Add workspace…" — and a
menu now appears only where there is something to choose between: with no
Workspace listed (the add-only sidebar header, or an empty hero list) the
anchor gesture raises the directory flow directly instead of a one-row
popover. An empty list counts as final only after the list baseline lands,
and a composition with no directory-flow occupant hides the sidebar button
rather than offering a dead one.
WorkspaceCreateFlow becomes WorkspacePickFlow (createOnly -> addOnly) and
the injected createWorkspace narrows to { path }. The host's
workspace.create({ name }) branch and `dsh web --workspace-root` lost their
last product consumer; both are marked at the call site for a follow-up.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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: a236be35d1f94af3cb583d542bb827472797ffe4
|
||||
2026-07-31-one-route-to-add-a-workspace.zh.md: fdc559649083189bac5e047a68ec3466f691dbf1
|
||||
@@ -0,0 +1,52 @@
|
||||
# Agent Note: One route to add a Workspace
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-31-one-route-to-add-a-workspace.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Both Workspace surfaces — the sidebar region header's `+` and the conversation hero's chip — offered two ways to get a Workspace: **Open local folder…**, which raised the composed directory flow, and **Create a new workspace**, which took a name and created `<workspaceRoot>/<name>`. The two overlapped: the browse occupant carries its own **New folder** affordance, so picking a directory already covered creating one. Two entries meant two vocabularies for one outcome, a name dialog with its own duplicate-name rule, and a create target the operator could neither see nor choose.
|
||||
|
||||
Removing the weaker entry leaves the sidebar header with exactly one action, which raised the presentation question this Note also settles: what a popover with a single row should look like.
|
||||
|
||||
## Decision
|
||||
|
||||
Adding a Workspace has one route: pick a host directory through the composed directory flow, new or existing. `menu.addWorkspace` ("添加工作区…" / "Add workspace…") is the entry; the create-by-name dialog and its `create.*` / `menu.createWorkspace` / `workspace.new` strings are gone. The label names the outcome, not the mechanism, because it is now the only door to that outcome — a user looking for "新建" must find it.
|
||||
|
||||
**A menu exists to disambiguate between targets.** When the only entry left is the add action — the add-only sidebar surface, or the hero with an empty list — the anchor gesture *is* that action: the flow opens directly and no popover renders. A one-row popover costs a click and offers nothing to choose between. The rule is one predicate (`addIsTheOnlyEntry`) covering both surfaces rather than a per-surface special case.
|
||||
|
||||
Two boundaries fall out of that rule and are part of it:
|
||||
|
||||
- **An empty list is only final once the baseline lands.** While `phase` is `pending` the hero keeps its menu and loading status instead of jumping into a flow that the arriving workspaces would have made unnecessary. The add-only surface lists nothing and never waits.
|
||||
- **An unoccupied directory-flow hole leaves nothing to add with.** The sidebar header then renders no button at all rather than a dead one; the hero's menu keeps working as a picker over whatever is listed. This is the seam's documented no-flow default reaching its conclusion: with the occupant gone, so is the only creation affordance.
|
||||
|
||||
`WorkspaceCreateFlow` is now `WorkspacePickFlow` and its `createOnly` prop is `addOnly`; the injected `createWorkspace` narrows from `{ name } | { path }` to `{ path }`.
|
||||
|
||||
## Wire and CLI residue
|
||||
|
||||
The host's `workspace.create` still accepts `{ name }`, and `dsh web --workspace-root` still feeds its target directory, but no product surface reaches either any more. Both are marked for deletion at the call site in `packages/host/apiproxy/src/api-proxy.ts` and left to a follow-up change: they are backend and CLI surface with their own reviewer and their own test fallout (the api-proxy workspace suite, the config catalog), and the release-blocking part of this decision is the UI.
|
||||
|
||||
## Testing
|
||||
|
||||
`connectFreshWorkspace` — the helper every web e2e scenario boots through — now drives the real dialog: path editor → **New folder** → **Create** → **Open**. That keeps the produced path (`<workspaceCwd>/workspace`) identical to what create-by-name produced, so scenario goldens stay valid, and it puts the surviving creation route under every scenario in the lane rather than one. `workspace-management.e2e.ts` owns the focused coverage (adding two workspaces on folders it creates, reusing a deleted title on a different directory, the browser-dialog aria golden).
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep `Open local folder…` as the label.** Rejected: after the merge the entry both opens and creates, and naming it after the mechanism hides the creation half from exactly the users whose entry we removed. The counter-argument — "本地" usefully disambiguates the browser's machine from the harness's — is answered one step later by the dialog's own title and breadcrumbs.
|
||||
|
||||
**Keep the two-entry menu and make `Create a new workspace` open the same flow.** Rejected: two labels for one action is the confusion we were asked to remove, not a smaller version of it.
|
||||
|
||||
**Keep a one-row popover for consistency with the hero's menu.** Rejected: a popover that offers no choice is a wasted click and reads as unfinished. Consistency here is the *rule* (menu ⇔ a choice exists), not the widget.
|
||||
|
||||
**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.
|
||||
|
||||
**Seed workspaces out-of-band in the e2e scaffold instead of driving the dialog.** Rejected: it would have decoupled 15 scenarios from the picker entirely, leaving the product's only creation route covered by a single scenario. Driving the real dialog costs a few interactions per scenario and buys coverage everywhere.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Creating a Workspace outside the operator's chosen directory is no longer possible from the UI; the server-controlled `--workspace-root` target was the one way to constrain where new workspace folders land, and nothing replaces it. A deployment that needs that constraint has to re-introduce it deliberately.
|
||||
- The one remaining route browses the host filesystem, so the picker's reach is now the whole host rather than one configured parent. That is already the browse occupant's contract; this change makes it the only contract.
|
||||
- A composition that mounts `ui-workspace` without any directory-picker package can no longer add a Workspace at all, and now says so by omitting the button instead of offering a create-by-name fallback.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Agent Note: One route to add a Workspace
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-31-one-route-to-add-a-workspace.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
两处 Workspace 表层——侧边栏区头的 `+` 与会话主视觉区的 chip——都提供了两条获得 Workspace 的路径:**打开本地文件夹…** 拉起组合的目录流程,**新建工作区** 接收一个名称并创建 `<workspaceRoot>/<name>`。两者重叠:浏览占用者自带 **新建文件夹** 能力,因此"选一个目录"本就覆盖了"建一个目录"。两个入口意味着同一结果有两套词汇、一个自带重名规则的名称对话框,以及一个操作者既看不到也选不了的创建位置。
|
||||
|
||||
删掉较弱的那个入口后,侧边栏区头只剩一个动作,于是引出了本 Note 一并裁定的展示问题:只有一行的浮层应该长什么样。
|
||||
|
||||
## Decision
|
||||
|
||||
添加 Workspace 只有一条路径:通过组合的目录流程选一个宿主机目录,新建的或已存在的都可以。入口是 `menu.addWorkspace`("添加工作区…" / "Add workspace…");按名称创建的对话框及其 `create.*`/`menu.createWorkspace`/`workspace.new` 文案全部删除。标签命名的是结果而非机制,因为它现在是通往该结果的唯一一扇门——找"新建"的用户必须能找到它。
|
||||
|
||||
**菜单的存在是为了在多个目标之间消歧。** 当只剩添加这一个入口时——仅添加的侧边栏表层,或列表为空的主视觉区——锚点手势*就是*该动作:直接拉起流程,不渲染浮层。只有一行的浮层多花一次点击,却没有任何可选项。这条规则是一个谓词(`addIsTheOnlyEntry`)覆盖两处表层,而不是逐表层特判。
|
||||
|
||||
由该规则派生出两条边界,它们同属这个决定:
|
||||
|
||||
- **列表为空只有在基线落地后才算最终结果。** `phase` 仍为 `pending` 时,主视觉区保留菜单与加载状态,而不是跳进一个即将到达的 workspace 会使其变得多余的流程。仅添加表层不列任何东西,因此从不等待。
|
||||
- **目录流程的洞没有占用者时,就没有任何可添加的手段。** 此时侧边栏区头干脆不渲染按钮,而不是留下一个点了没反应的按钮;主视觉区的菜单则继续作为选择器工作,列出已有内容。这是 seam 文档化的无流程默认行为走到它的结论:占用者不在,唯一的创建能力也就不在。
|
||||
|
||||
`WorkspaceCreateFlow` 现更名为 `WorkspacePickFlow`,其 `createOnly` prop 更名为 `addOnly`;注入的 `createWorkspace` 从 `{ name } | { path }` 收窄为 `{ path }`。
|
||||
|
||||
## Wire and CLI residue
|
||||
|
||||
Host 侧的 `workspace.create` 仍接受 `{ name }`,`dsh web --workspace-root` 也仍在为它提供目标目录,但已没有任何产品表层会走到它们。两者都在 `packages/host/apiproxy/src/api-proxy.ts` 的调用点标记为待删除,并留给后续改动:它们属于 backend 与 CLI 面,有各自的 reviewer 和各自的测试波及面(api-proxy workspace 套件、配置目录),而本决定中阻塞发布的部分是 UI。
|
||||
|
||||
## Testing
|
||||
|
||||
`connectFreshWorkspace`——所有 web e2e 场景启动时都会走的辅助函数——现在驱动真实对话框:路径编辑器 → **新建文件夹** → **创建** → **打开**。这让产出的路径(`<workspaceCwd>/workspace`)与按名称创建时完全一致,因此场景 golden 保持有效;同时它把幸存的创建路径放到了整条 lane 的每个场景之下,而不只是某一个场景。`workspace-management.e2e.ts` 承担针对性覆盖(在自己创建的文件夹上添加两个 workspace、在另一个目录上复用已删除的标题、浏览对话框的 aria golden)。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**保留 `打开本地文件夹…` 作为标签。** 否决:合并后该入口既能打开也能创建,用机制命名会恰好对那些入口被我们删掉的用户隐藏创建这一半。反方理由——"本地"二字有效区分了浏览器所在机器与 harness 所在机器——在下一步就由对话框自身的标题和面包屑回答了。
|
||||
|
||||
**保留双入口菜单,让 `新建工作区` 也打开同一个流程。** 否决:同一动作两个标签正是我们被要求消除的混淆,而不是它的缩小版。
|
||||
|
||||
**为了与主视觉区菜单保持一致而保留只有一行的浮层。** 否决:不提供选择的浮层是一次浪费的点击,读起来像半成品。这里要一致的是*规则*(有菜单 ⇔ 存在选择),不是控件。
|
||||
|
||||
**为将来可能新增的入口(克隆仓库、远程目录)保留菜单壳。** 否决,依据"require a current owner and need":这样的入口目前并不存在,而等它到来时再恢复菜单,比现在就发一个空壳的改动更小。
|
||||
|
||||
**在同一改动中删除 wire 的按名称创建分支。** 本 PR 否决:那是 backend/CLI 面,reviewer 不同、测试波及面更广,而紧急的决定是 UI。见 residue 一节——它是被标记了,不是被遗忘了。
|
||||
|
||||
**在 e2e scaffold 中旁路种入 workspace,而不驱动对话框。** 否决:那会让 15 个场景彻底与选择器解耦,使产品唯一的创建路径只剩一个场景覆盖。驱动真实对话框每个场景多花几次交互,换来的是处处都有覆盖。
|
||||
|
||||
## Consequences
|
||||
|
||||
- 从 UI 已无法在操作者选定目录之外创建 Workspace;服务端控制的 `--workspace-root` 目标曾是约束新 workspace 文件夹落点的唯一手段,现在没有替代品。需要该约束的部署必须有意识地重新引入它。
|
||||
- 仅存的这条路径会浏览宿主机文件系统,因此选择器的可达范围现在是整台宿主机,而非一个配置好的父目录。这本就是浏览占用者的契约,本改动使它成为唯一的契约。
|
||||
- 挂载了 `ui-workspace` 但未挂任何 directory-picker 包的组合,已完全无法添加 Workspace;现在它通过不渲染按钮来说明这一点,而不是提供一个按名称创建的兜底。
|
||||
Reference in New Issue
Block a user