docs(web): clarify session row slots and keyboard limits
This commit is contained in:
1 parent
9dbf907f93
commit
fdf14b2bc9
11 files changed
+42
-26
No files matched your search
@@ -85,7 +85,7 @@ it('places dynamic Session menu rows by order among the shipped ones and removes
|
||||
webSnapshotMode(),
|
||||
)
|
||||
|
||||
await menu.getByRole('menuitem', { name: 'Export session' }).click()
|
||||
await menu.getByRole('menuitem', { name: 'Export session' }).press('Enter')
|
||||
await expect.poll(() => menu.count()).toBe(0)
|
||||
expect(await page.evaluate(() => ({
|
||||
action: document.documentElement.dataset.sessionAction,
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/slots.md
|
||||
/web-client-slots:
|
||||
en: 7675993fd0a8eb2a
|
||||
zh: b2fb70704f2e841c
|
||||
en: 5f160332e4980284
|
||||
zh: b1ae1da451a6209e
|
||||
/web-client-slots/declaration-and-lifecycle:
|
||||
en: f2d2c0d3b8f14ec7
|
||||
zh: 441ed13ff3f81d2a
|
||||
|
||||
@@ -10,7 +10,7 @@ This page documents slot ownership, component inputs, extension APIs, and the sh
|
||||
|
||||
The plugin catalog declares `plugins.add.actions`, a root-scoped list for `MenuItemButton` rows after installation in the Add plugin menu. Its owner supplies `onDismiss()` to close the menu before starting an action. The slot follows the owning Plugins page declaration lifetime.
|
||||
|
||||
Session rows declare the root-scoped `sidebar.session.row.trailing` list for non-interactive icons beside the time and pin marker. Each entry receives `sessionId`; [ui-workspace](../../packages/client/ui-workspace/README.md) owns visibility and keyboard behavior.
|
||||
[ui-workspace](../../packages/client/ui-workspace/README.md#understand-the-implementation) documents Session-row extension positions, visibility, and keyboard behavior.
|
||||
|
||||
## Declaration and lifecycle
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Slots 是 Web Client 的类型化 React 组合系统。[`dsh-client-ui-slots`](.
|
||||
|
||||
插件目录声明 `plugins.add.actions`,它是 root 作用域的 list,在添加插件菜单的安装项之后提供 `MenuItemButton` 行。owner 提供 `onDismiss()`,用于开始动作前关闭菜单。该 slot 遵循所属插件页面声明的生命周期。
|
||||
|
||||
Session 行声明根作用域的 `sidebar.session.row.trailing` 列表,用于时间与置顶标记旁的非交互图标。每个条目接收 `sessionId`;[ui-workspace](../../packages/client/ui-workspace/README.zh.md) 定义显示条件和键盘行为。
|
||||
[ui-workspace](../../packages/client/ui-workspace/README.zh.md#understand-the-implementation) 记录 Session 行的扩展位置、显示条件和键盘行为。
|
||||
|
||||
## 声明与生命周期
|
||||
|
||||
|
||||
@@ -36,14 +36,14 @@
|
||||
en: 8d82539e4e4e890d
|
||||
zh: 7b97a3927de90ea8
|
||||
/deepseek-ai-dsh-client-ui-workspace/understand-the-implementation:
|
||||
en: 721d5a49fe231191
|
||||
zh: 3ef1dd720219e00e
|
||||
en: cb95824ba53e94bc
|
||||
zh: c57a0d8839086001
|
||||
/deepseek-ai-dsh-client-ui-workspace/understand-the-implementation/the-directory-flow-hole:
|
||||
en: 868904dbfb5713f8
|
||||
zh: 225906bfef7398bf
|
||||
/deepseek-ai-dsh-client-ui-workspace/understand-the-implementation/session-row-actions:
|
||||
en: 5c79ecd5268c0fce
|
||||
zh: 739a186c72dcfde5
|
||||
en: 5bbba6e462faf3dc
|
||||
zh: 4734263208ee07c9
|
||||
/deepseek-ai-dsh-client-ui-workspace/understand-the-implementation/session-row-actions/packaged-client-plugin:
|
||||
en: 36934671b80b11ae
|
||||
zh: 8e0aea6162e35e77
|
||||
@@ -66,8 +66,8 @@
|
||||
en: ca75c51c89c2c9b0
|
||||
zh: de54f3467ef3b9be
|
||||
/deepseek-ai-dsh-client-ui-workspace/known-limitations-and-deferred-work:
|
||||
en: 00d7b1ddda7390f5
|
||||
zh: 1e5a1055d1be4d1d
|
||||
en: 8211860a1b10ac61
|
||||
zh: 4ef11b1fce30cacd
|
||||
/deepseek-ai-dsh-client-ui-workspace/known-limitations-and-deferred-work/dev-note:
|
||||
en: e67f633dc6fe7773
|
||||
zh: 3bb0c20c93f16f98
|
||||
@@ -91,7 +91,11 @@ The package injects the Client `remoteAccess` service and provides the root stan
|
||||
|
||||
The package is one composition: both target slots are declared by other plugins, so `apply` uses `slots.inject()` to register for each declaration lifetime and re-register after a declaring slot is restored.
|
||||
|
||||
The browser entry also declares two root-scoped `list` child seats on each Session row: `sidebar.session.row.leading`, rendered only while that row's primary status is idle and left blank on an archived row, and `sidebar.session.row.hover`, mounted only while that row's hover card is open. Both take the row's Session identity and nothing else, so an occupant reads its own data by that id; a Session-scoped seat would force a Session binding, which would activate and retain every listed Session.
|
||||
The browser entry declares three root-scoped `list` child seats on each Session row: `sidebar.session.row.leading`, `sidebar.session.row.trailing`, and `sidebar.session.row.hover`. All receive only `sessionId`, which occupants use to address their own data without creating a Session binding. A Session-scoped seat would activate and retain every listed Session.
|
||||
|
||||
The leading seat occupies the status cell only while the row's primary status is idle and leaves archived rows blank. The trailing seat accepts non-interactive icons in ascending `order`, after the time label and before the pin marker. Blank rows omit the trailing seat; an empty seat adds no spacing. The hover seat mounts only while the row's hover card is open.
|
||||
|
||||
On hover-capable devices, hovering a Session row, focusing it or a descendant, or opening its menu replaces the time, trailing icons, and pin marker with action buttons. Hoverless devices always show the action buttons and hide those static details. Session rows accept Tab focus and open with Enter or Space; Tab also reaches their action buttons.
|
||||
|
||||
Client callers can pass `prompt` and `clearPreviousDraft` in the second argument to `uiWorkspace.startSession`. A prompt is plain text; structured drafts are handled internally during restoration and Workspace transfer. Without clearing, existing text or attachments are preserved; explicit clearing replaces only the target Session's text and inline references, leaving attachments intact. Option values are captured before asynchronous creation and applied to the exact retained binding before navigation commits. Superseded navigation does not write content; a submission-blocked input refuses preparation and keeps the previous selection. Calls without options create a fresh Session using the current Agent preset definition. Workspace reconnection and explicit draft preparation may reuse an existing blank Session. The [draft initialization decision](../../../.agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.md) describes the input owner's data flow.
|
||||
|
||||
@@ -107,8 +111,6 @@ An entry receives only the row identity (`sessionId`, `displayTitle`) and owns e
|
||||
|
||||
The existing notice overlay dispatches `workspace.toast` by notice kind. Keyed registrations inject localized display mappings and their own actions; only archive notices subscribe to the viewing filter. They render the package-local `workspace.toast.content` Factory, which reuses the Toast primitive once for all presentations. The existing notice state, display sequence, latest-notice replacement, and dismissal remain shared; locale or filter changes do not restart the same notice's timer.
|
||||
|
||||
The root-scoped `sidebar.session.row.trailing` list accepts non-interactive icons in ascending `order`, after the time label and before the pin marker. Each entry receives `sessionId` and reads its own data without retaining the Session. Blank rows omit the slot; an empty slot adds no spacing. Hover, focus within the row, or an open row menu replaces these details with the existing action buttons. Touch devices keep the action buttons visible. Mouse-hover styles apply only on hover-capable devices; keyboard users can focus a row, open it with Enter or Space, and Tab into its actions.
|
||||
|
||||
#### Packaged client plugin
|
||||
|
||||
Declare `ui-workspace`, `ui-slots`, `ui-renderer`, `client-locale`, and `ui-primitives` as browser/type development dependencies according to the Client dependency policy. The type-only `ui-workspace/client` import loads this package's `SlotMap` declaration; without it, an independently compiled plugin does not know the slot keys. Keep the Component at module scope and own visible copy in the contributing package's locale namespace.
|
||||
@@ -218,11 +220,12 @@ None; this package neither assembles nor sends a provider request.
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
|
||||
These limits define the search depth, the archive surface, and the picking carrier; they are current package constraints.
|
||||
These limits cover search, navigation, archival, and folder selection.
|
||||
|
||||
- **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** — sessions can be archived but never deleted; archived rows stay recoverable in place through the archived view filter and the search results' unarchive action, and Workspace registration deletion does not delete Sessions.
|
||||
- **Pending user interaction 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.
|
||||
- **Tree keyboard navigation is incomplete** — Workspace and search-result rows do not accept Tab focus, and the browser tree does not implement arrow-key navigation.
|
||||
- **Native folder selection depends on the local Host carrier** — under the `-native` composition, in-process or remote browser deployments cannot open a local operating-system dialog; remote-capable picking is the `-browse` composition's in-app flow.
|
||||
|
||||
<a id="dev-note"></a>
|
||||
|
||||
@@ -91,7 +91,11 @@ Workspace 和 Session 的启动基线均就绪后,空安装环境调用 `works
|
||||
|
||||
本包是一条组合:两个目标 slot 都由其他插件声明,因此 `apply` 使用 `slots.inject()` 在各自的声明生命周期内完成注册,并在目标 slot 的声明恢复后重新注册。
|
||||
|
||||
浏览器入口还为每个 Session 行声明两个 root 作用域的 `list` 子 slot:`sidebar.session.row.leading` 仅在该行主状态为 idle 时渲染、归档行留空,`sidebar.session.row.hover` 仅在该行的悬浮卡片打开时挂载。两者只接收行的 Session 身份,占用方据此读取自己的数据;Session 作用域的 slot 会强制建立 Session 绑定,从而激活并保留列表中每个 Session。
|
||||
浏览器入口为每个 Session 行声明三个 root 作用域的 `list` 子 slot:`sidebar.session.row.leading`、`sidebar.session.row.trailing` 和 `sidebar.session.row.hover`。三者只接收 `sessionId`,占用方据此读取自己的数据,不建立 Session 绑定。Session 作用域的 slot 会激活并保留列表中每个 Session。
|
||||
|
||||
前置 slot 仅在该行主状态为 idle 时占用状态单元,归档行留空。尾部 slot 按 `order` 升序放置非交互图标,位置在时间标签之后、置顶标记之前。空白行不渲染尾部 slot;空 slot 不增加间距。悬浮 slot 仅在该行的悬浮卡片打开时挂载。
|
||||
|
||||
在支持悬停的设备上,鼠标移入 Session 行、聚焦行或其后代元素、打开行菜单,都会用操作按钮替代时间、尾部图标和置顶标记。不支持悬停的设备始终显示操作按钮,并隐藏这些静态信息。Session 行可通过 Tab 聚焦,使用 Enter 或空格打开;Tab 也可移到行内操作按钮。
|
||||
|
||||
客户端调用方可以在 `uiWorkspace.startSession` 的第二参数中传入 `prompt` 与 `clearPreviousDraft`。提示词仅接受普通文字;结构化草稿由内部恢复和工作区携带流程处理。不清空时保留已有文字或附件;显式清空只替换目标 Session 的文字和内联引用,不删除附件。在异步创建前捕获选项值,在导航提交前应用到准确保留的 binding。已被后续操作取代的导航不写内容;输入处于提交阻塞状态时拒绝准备并保留原选择。不传选项时按当前 Agent 预设定义创建新的 Session。工作区重连和显式草稿准备仍可复用已有空 Session。[草稿初始化决策](../../../.agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.zh.md)说明输入 owner 的数据流。
|
||||
|
||||
@@ -107,8 +111,6 @@ Session 行的 "..." 菜单和行尾悬停按钮是 WorkspaceBrowser 注册项
|
||||
|
||||
现有通知浮层按通知 kind 分派 `workspace.toast`。各 keyed 注册注入本地化展示映射与自己的操作,只有归档通知订阅查看筛选状态。它们渲染包内的 `workspace.toast.content` Factory,所有展示共用一处 Toast primitive。现有通知状态、显示序号、最新提示替换及关闭逻辑仍共享;语言或筛选变化不会重新启动同一条提示的计时。
|
||||
|
||||
根作用域的 `sidebar.session.row.trailing` 列表按 `order` 升序放置非交互图标,位置在时间标签之后、置顶标记之前。每个条目接收 `sessionId`,自行读取数据,不保留 Session 引用。空白行不渲染此 slot;空 slot 不增加间距。悬停、行内聚焦或行菜单打开时,这些信息由现有操作按钮替代。触屏设备始终显示操作按钮。鼠标悬停样式仅适用于支持悬停的设备;键盘用户可聚焦行,用 Enter 或空格打开,并用 Tab 移到操作按钮。
|
||||
|
||||
#### 打包客户端插件
|
||||
|
||||
按照 Client 依赖规则,将 `ui-workspace`、`ui-slots`、`ui-renderer`、`client-locale` 与 `ui-primitives` 声明为浏览器/类型开发依赖。纯类型的 `ui-workspace/client` import 会加载本包的 `SlotMap` 声明;缺少该 import 时,独立编译的插件不会知道这些 slot key。Component 保持模块级稳定身份;用户可见文案由贡献包自己的 locale namespace 持有。
|
||||
@@ -218,11 +220,12 @@ Workspace 与 Session 悬浮卡片会复制对应行被截断的值:激活 Wor
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
|
||||
这些限制定义搜索深度、归档界面与选取载体;它们是当前包约束。
|
||||
这些限制涵盖搜索、导航、归档和文件夹选择。
|
||||
|
||||
- **没有模糊内容搜索或事件深链接**:内容后端采用字面 token/短语匹配,选择结果会打开 Session,而不是匹配的事件。
|
||||
- **没有 Session 删除**:会话可以归档但绝不会被删除;已归档的行通过「已归档会话」视图筛选与搜索结果中的取消归档操作原位恢复,删除 Workspace 注册记录不会删除 Session。
|
||||
- **待处理的用户交互不会聚合到折叠的分组上**:折叠分组内正在等待的行不会点亮分组头指示,只有展开该分组后才可见。
|
||||
- **树形列表键盘导航尚不完整**:Workspace 行和搜索结果行不接受 Tab 聚焦,浏览器树形列表也未实现方向键导航。
|
||||
- **原生文件夹选择依赖本地 Host 载体**:在 `-native` 组合下,进程内部署或远程浏览器部署无法打开本地操作系统对话框;可远程的选取是 `-browse` 组合的应用内流程。
|
||||
|
||||
<a id="dev-note"></a>
|
||||
|
||||
@@ -139,7 +139,8 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
* Non-interactive icons after the row's time label, before its pin marker,
|
||||
* in ascending `order`. Hidden on hoverless devices and while the row is
|
||||
* hovered, contains focus, or has an open action menu. Blank Sessions omit
|
||||
* the slot. Occupants receive only the Session id and must not retain it.
|
||||
* the slot. Occupants address their own data by Session id; they must not
|
||||
* bind or retain the Session.
|
||||
*/
|
||||
'sidebar.session.row.trailing': { kind: 'list'; scope: 'root'; inline: true; owner: SessionRowTrailingOwnerProps }
|
||||
/**
|
||||
|
||||
@@ -533,6 +533,8 @@ export function SearchResultItem({ result, currentId, onOpen, onUnarchive, t }:
|
||||
* keeps that cell and renders no seat, so an ambient automation mark never
|
||||
* appears beside the row's own state dot. An archived row keeps the cell blank:
|
||||
* neither marker renders there, and its live status stays on the hover card.
|
||||
* The row accepts Tab focus and opens with Enter or Space; descendant controls
|
||||
* handle their own activation without opening the row.
|
||||
* @param props.node - derived session node.
|
||||
* @param props.currentId - selected session id (row highlight).
|
||||
* @param props.now - epoch ms for relative-time formatting.
|
||||
@@ -657,6 +659,8 @@ export function SessionNodeItem({
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
{/* Blank rows have no first prompt, so neither trailing details nor
|
||||
actions for existing content are available. */}
|
||||
{!row.blank && (
|
||||
<span className={css.rowResting}>
|
||||
<span
|
||||
|
||||
@@ -784,7 +784,12 @@ describe('workspace browser rows', () => {
|
||||
expect(rendered).toHaveBeenCalledWith(
|
||||
'sidebar.workspaces.session.menu.item', { sessionId: node.id, displayTitle: 'One' }, [true, expect.any(Function)],
|
||||
)
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: 'Export' }))
|
||||
const entry = screen.getByRole('menuitem', { name: 'Export' })
|
||||
expect(screen.getByRole('treeitem').contains(entry)).toBe(false)
|
||||
entry.focus()
|
||||
expect(fireEvent.keyDown(entry, { key: 'Enter', code: 'Enter' })).toBe(true)
|
||||
expect(onOpen).not.toHaveBeenCalled()
|
||||
fireEvent.click(entry)
|
||||
expect(onEntry).toHaveBeenCalledWith(node.id, 'One', true)
|
||||
// The entry dismissed the menu through the state pair; the list returns focus.
|
||||
expect(screen.queryByRole('menu')).toBeNull()
|
||||
|
||||
@@ -4346,7 +4346,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'none',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.session.row.hover\', () => ctx.slots.register(\n { name: \'sidebar.session.row.hover\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-workspace/src/client/contract/slots.ts:149',
|
||||
source: 'packages/client/ui-workspace/src/client/contract/slots.ts:150',
|
||||
},
|
||||
{
|
||||
key: 'sidebar.session.row.leading',
|
||||
@@ -4407,7 +4407,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
kind: 'list',
|
||||
scope: 'root',
|
||||
summary: 'Non-interactive icons after the row\'s time label, before its pin marker, in ascending `order`.',
|
||||
doc: 'Non-interactive icons after the row\'s time label, before its pin marker,\nin ascending `order`. Hidden on hoverless devices and while the row is\nhovered, contains focus, or has an open action menu. Blank Sessions omit\nthe slot. Occupants receive only the Session id and must not retain it.',
|
||||
doc: 'Non-interactive icons after the row\'s time label, before its pin marker,\nin ascending `order`. Hidden on hoverless devices and while the row is\nhovered, contains focus, or has an open action menu. Blank Sessions omit\nthe slot. Occupants address their own data by Session id; they must not\nbind or retain the Session.',
|
||||
registerOptions: [
|
||||
{
|
||||
name: 'id',
|
||||
@@ -4452,7 +4452,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
occupants: [],
|
||||
replaceRisk: 'none',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.session.row.trailing\', () => ctx.slots.register(\n { name: \'sidebar.session.row.trailing\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-workspace/src/client/contract/slots.ts:144',
|
||||
source: 'packages/client/ui-workspace/src/client/contract/slots.ts:145',
|
||||
},
|
||||
{
|
||||
key: 'sidebar.settings',
|
||||
@@ -4641,7 +4641,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'none',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n const copyLabel = \'Copy session title\' // Localize in the contributing package.\n ctx.slots.inject(\'sidebar.workspaces.session.menu.item\', () => ctx.slots.register(\n { name: \'sidebar.workspaces.session.menu.item\', id: \'copy-session-title\', order: 500 },\n ({ displayTitle, useMenuOpenState }) => {\n const [, setMenuOpen] = useMenuOpenState()\n return React.createElement(\n \'button\',\n {\n type: \'button\', role: \'menuitem\',\n onClick: () => { setMenuOpen(false); void navigator.clipboard.writeText(displayTitle) },\n },\n copyLabel,\n )\n },\n ))\n },\n}',
|
||||
source: 'packages/client/ui-workspace/src/client/contract/slots.ts:184',
|
||||
source: 'packages/client/ui-workspace/src/client/contract/slots.ts:185',
|
||||
},
|
||||
{
|
||||
key: 'sidebar.workspaces.session.row.action',
|
||||
@@ -4696,7 +4696,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'none',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.workspaces.session.row.action\', () => ctx.slots.register(\n { name: \'sidebar.workspaces.session.row.action\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-workspace/src/client/contract/slots.ts:202',
|
||||
source: 'packages/client/ui-workspace/src/client/contract/slots.ts:203',
|
||||
},
|
||||
{
|
||||
key: 'tool.call.images',
|
||||
|
||||
Reference in new issue
Block a user