Files
deepseek-harness/packages/client/ui-workspace
Yif 92dd231ce4 polish(web): 打磨 Web 会话界面的布局、自适应与组件一致性
1. 统一会话列宽度轴:新增 --dsh-chat-content-width(748px),输入框、todo、goal、queue、approval、plan review、ask question 等容器的宽度与边距全部由该变量推导,消除各面板之间的像素漂移,窄视口下的边缘留白也保持一致。
2. 输入框自适应与细节:控制行改为容器查询(460px 阈值以下权限选择器只显示图标+下拉,隐藏文字);卡片圆角 20→22、行内边距调整并整体下移 2px(发送按钮除外);permission/model 触发器统一 24px 圆角;Plan 与 Read Only 间距 +8。
3. 修复浮层菜单溢出:slash 菜单与命令弹层钳制到输入卡片宽度,超长行以省略号截断;Tooltip 增加 12px 视口边缘安全距离。
4. 增加与替换图标:Add provider 改用输入框同款加号图标(IconPlusOutline16),统一图标尺寸与字号。
5. 统一 Settings → Models 组件:补齐按钮 hover 态、select 下拉箭头不再贴边、标题区与 provider 卡片间距 +12。
6. 侧边栏交互:add workspace / group by / create session / 收起侧边栏四个图标按钮增加 500ms 延迟 tooltip(前两个向下弹出);展开态的 New Session 不再重复显示 tooltip;侧边栏窄屏自适应收起逻辑微调。
7. 其他:hero 区 workspace 徽章右移对齐;附带 Agent Note(中英双语)记录共享宽度轴与容器查询的设计取舍。
2026-08-04 14:14:12 +08:00
..
wip
2026-07-31 02:13:53 +08:00

@deepseek-ai/dsh-client-ui-workspace

English | 中文

Shared Workspace browser and picker plugin. WorkspaceBrowser fills the sidebar's sidebar.workspaces slot, while WorkspacePicker fills the page-local Session Intent hero's conversation.hero.workspace slot; both surfaces use the same Workspace menu and add flow.

The browser renders grouped or flat Session rows from the global runtime hooks and owns the Workspace add/rename and in-Workspace reorder flows. A non-blank search query replaces either browsing mode with one flat result list: case-insensitive title and Workspace substring matches appear immediately, while a 250 ms debounced Host request adds ranked current-conversation content matches and snippets. The English search input and its defensive request path remove NUL, cap the query at the wire schema's 500 UTF-16 code units without splitting a surrogate pair, and preserve the existing debounce and cancellation behavior. Each new query aborts the preceding request; a failed content search leaves metadata matches visible with a warning. The list is capped at 20, asks the user to narrow broader queries, and opens the selected Session without clearing the query or jumping to a specific event.

The picker lists real Host Workspace entities through the global useWorkspaces hook. Selecting a Workspace invokes the slot owner's onPick callback to retarget the frontend Session object. Distinct canonical paths remain separate id-keyed Workspaces when their basenames and display titles match; the sidebar hover detail exposes the full path. Each registration declares a directory-flow child hole (single kind: conversation.hero.workspace.directoryFlow / sidebar.workspaces.directoryFlow) that the composed picker package's client half fills with its picking interaction — the -native backend's renderless OS-chooser driver today, an in-app browsing dialog under a -browse composition. The flat Add workspace... action renders only while the surface's hole is occupied (occupancy read per menu render; an empty hole means the composition has no picking affordance — the seam's documented no-flow default, under which the sidebar header drops its add button rather than offering a dead one). This package owns the trigger and the adoption: the occupant reports one picked path per open through the hole's owner conversation (open/busy/onPicked/onCancel/onError), and the owner adopts it through the object layer, selecting the committed Workspace only after its list projection has refreshed; cancellation is silent, and errors land in the retryable folder dialog whose Choose again reopens the flow. Adding has exactly one route: the occupant's own create-folder affordance already covers a brand-new directory, so no separate create-by-name dialog exists. A menu only appears where there is something to choose between — with no Workspace listed, the anchor gesture raises the flow directly instead of a one-row popover, and it waits for the list baseline before treating an empty list as final. The runtime Session and Workspace services own materialization. The Workspace row's Delete action opens a confirmation that states the retention boundary, blocks duplicate submission, and keeps failures open; success removes the group while its Sessions remain under Ungrouped. The Session row's Rename action opens the same browser-owned dialog pattern prefilled with the row's display title: no client-side conflict rule exists (the host normalizes and may reject with title-invalid, rendered in the dialog alert), and confirming an unchanged title is deliberately allowed — it pins the current automatic title against regeneration. The Session row's Archive action commits without a confirmation dialog (non-destructive: the log and the workspace accounting slot remain) through ctx.workspaces.archiveSession; the row disappears from every grouping surface — workspace groups, Ungrouped, content search, and the flat list — when the archive-set echo lands, and failures are console diagnostics that leave the tree unchanged. A blank New Session row is a pure placeholder: it renders no row menu and no time label (nothing has happened in it yet), so rename, fork, and archive first apply once the first prompt lands.

Workspace and Session hover cards copy the value their row clips: activating a Workspace card writes its full directory path, while activating a non-blank Session card writes its full display title. A provisional blank New Session card remains read-only because its localized label is a placeholder rather than session content. The card reports the dictionary-driven copied state only after the browser accepts the clipboard write.

The Session row's Fork action forks at the source's last completed turn, increments the inherited persisted title on the client, and then opens the child; a trailing ASCII or fullwidth parenthesized number is incremented in the same style, while an unnumbered title gets (1) appended. The source and child always appear as peer rows within a workspace group, with lineage retained only as session data. A fork or rename failure leaves the current selection unchanged; after a rename failure, the created child remains in the list.

Session rows distinguish the runtime's live waitingApproval approval-request fact from an otherwise blue in-flight Session: an amber warning dot takes precedence over the running indicator, and the hover card reports Waiting for approval until the request is resolved. Every lit state carries a visually hidden label (Waiting for approval or Running) for assistive technology; an idle row leaves the reserved status slot empty. Question waits do not set a list-level status bit such as waitingApproval.

Both target slots are declared by other plugins, so apply registers through declaration-aware deferral and re-registers after a declaring slot is restored.

The shared sidebar projection hides rows whose durable Session summary has origin: 'subagent'; users enter those conversations through the selected parent's subagent header catalog. Ordinary forks remain visible because lineage alone does not set that origin. The runtime keeps hidden rows available for conversation, title, and addressed transport state.

Model Experience

None, as the picker is browser chrome; nothing here reaches a model request.

KV Cache effect

None; this package neither assembles nor sends a provider request.

Known Limitations and Deferred Work

  • No fuzzy content search or event deep links — the content backend uses literal token/phrase matching, and selecting a result opens the Session rather than the matching event.
  • No Session deletion or unarchive control — archiving replaces the former Delete placeholder; archived sessions have no viewing or unarchive surface yet, and Workspace registration deletion does not delete Sessions.
  • Approval waiting is not aggregated into collapsed groups — a waiting row inside a collapsed group lights no group-header indicator and becomes visible only after that group is expanded.
  • Native folder selection depends on the local Host carrier — under the -native composition, fixture-only or remote browser deployments cannot open a local operating-system dialog; platform failures are shown in a retryable modal. Remote-capable picking is the -browse composition's in-app flow.