Merge remote-tracking branch 'origin/master' into worktree/web-textarea-refactor-991614

# Conflicts:
#	packages/client/ui-chat/src/client/chat/MessageItem.module.css
#	packages/client/ui-conversation/package.json
#	packages/client/ui-input-trigger/README.i18n.yaml
#	packages/client/ui-input-trigger/README.md
#	packages/client/ui-input-trigger/README.zh.md
#	packages/client/ui-reference/README.i18n.yaml
#	packages/client/ui-reference/README.md
#	packages/client/ui-reference/README.zh.md
#	pnpm-lock.yaml
This commit is contained in:
Yichen Jiang
2026-08-26 10:10:09 +08:00
1524 changed files with 73055 additions and 15807 deletions
@@ -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 packages/client/ui-input-trigger/README.md
README.md: e4bcf975251d19c91938ced335024fcfcf83034f
README.zh.md: b81c29a12a0b2accb6d9d9731ea5ed790aa3a833
README.md: 1760881ba84492d967d405a43da179f5032763e1
README.zh.md: 7a6e127c12a0f2f1f0d9c95021bb0d41ddd17e7b
+70 -7
View File
@@ -1,18 +1,66 @@
---
description: "Input trigger pipeline for the Web GUI: / and @ detection under the caret, the grouped candidate menu, and pick routing to registered sources; for users and maintainers of slash commands and references."
kind: "package-reference"
---
# @deepseek-ai/dsh-client-ui-input-trigger
English | [中文](README.zh.md)
Input trigger pipeline plugin: `/` and `@` detection under the caret (word-boundary + guard-tier rules), the grouped candidate menu, and pick routing to registered sources. `ctx.inputTriggers` owns the source roster and resolves one `InputTriggerController` per session scope (`sessionOf`); the conversation wiring layer drives `track`/`arbitrate`/`onSpace`/`adjudicate` on the controller. The same controller exposes `toggleSource` for a chrome launcher to open exactly one registered source over a synthetic selection span; the resulting candidates still use the ordinary menu, keyboard arbitration, pick callback, and scoped input mutations. Sources receive a `ClientSessionContext` projection per call — sessions are always agent-backed, so the projection is the session identity alone. A source is warmed in every session controller it can reach: the roster present at scope birth warms during controller construction, and a source registered later is warmed into every live controller by the registration itself. Sources whose `lexicon` roll changes after warm implement `subscribeLexicon(session, listener)`; the controller re-polls on each notification and publishes the aggregation through its `lexicon` snapshot store. The pipeline is command-agnostic: space/enter adjudication polls the optional `matchSpace`/`matchEnter` hooks in registration order and the first non-undefined answer wins. Enter adjudication also carries a `SubmitEnvelope` (the composer's image-attachment count) so a source can refuse a submission it cannot consume whole; a `CommandClaim` declares `images: true` when its command accepts composer images, and its `submit` then receives the serialized payloads as a third argument.
## Summary
Layering: `src/core/` is the pure core — `detectTrigger`, `menuReduce`/`seedGroups`/`MENU_CLOSED`, `exactMatch`, zero React/DOM/cordis; `src/client/service.ts` is the shell wiring the core to the menu snapshot store, the per-hit candidate fetch (generation-gated, `AbortSignal`-superseded, failed sources drop silently with a console record), and the three pick paths. `ReferenceInsert.appearance` optionally identifies a `session`, `file`, or `folder` display without changing its serialized `ref`; the consuming composer owns the glyph and color. `src/types.ts` and the two `contract.ts` files are the frozen cross-package contract; changes require main-thread arbitration.
This package powers the input trigger pipeline of the Web GUI: it detects `/` and `@` typed under the caret, shows a grouped candidate menu, and routes a pick to the registered source. Sources register through `ctx.inputTriggers` — the `/` command source (ui-commands), the `@` file and session reference sources (ui-reference), and any business package — and the conversation wiring drives the pipeline per session. Typing a trigger seeds every source registered for it; a chrome launcher can also open exactly one source over the current selection. The pipeline is presentation-only: picks produce command claims or reference inserts whose consequences belong to the consuming host and input packages.
MenuView renders the menu store into the `conversation.input.overlay` slot (list kind, session scope) and renders null while closed. Typed triggers seed every source registered for that trigger; a programmatic launcher seeds only its requested source and publishes the source name through the controller's `launcher` snapshot store until the menu closes or typed tracking resumes. Groups sort by the optional `InputTriggerSource.order` (lower first, default 0, ties keep registration order) under title rows localized through the `inputTriggers.menu` locale namespace (an unknown source shows its raw name). `showGroupTitle: false` suppresses that row through pending and ready states, while a ready group whose candidates declare sections uses those section rows in place of the source title. The list height clamps to the space above the composer, and a pointer down outside both the menu and the surrounding composer card dismisses it. The slot is owned by ui-conversation's composer entry (anchor, children declaration, lifecycle); its SlotMap type merge lives in this package's `src/client/slots.ts` because the dependency direction (ui-conversation → ui-input-trigger) admits no reverse type import. Combobox pattern: focus stays in the composer surface, rows pick on mousedown, the highlight rides `aria-activedescendant`. A candidate declaring `drill: true` carries a second verb beside the settling pick: its trailing chevron and the Tab key route the same row through `onPick` with `action: 'drill'` (every other path reports `'pick'`), and Tab passes untouched on rows without the flag so native focus traversal survives.
## Table of Contents
The `/client` exports are the plugin body (`apply`/`inject`), `InputTriggerService`, `MenuViewInjected`, and the contract types. MenuView itself is internal — the slot registration closes over it.
- [Use this package](#use-this-package)
- [Understand the implementation](#understand-the-implementation)
- [Further Exploration](#further-exploration)
- [Model Experience](#model-experience)
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
- [Dev Note](#dev-note)
-----
<a id="use-this-package"></a>
## Use this package
Mount this plugin alongside `ui-conversation`; the menu then appears in the input overlay when the user types a trigger under the caret. Grouped candidates render under title rows; a pick routes to the source, and the consuming surface applies the result — a slash command opens its popup or executes, a reference inserts its inline token.
### Keyboard and mouse
The composer surface keeps focus while the menu is open: rows pick on mousedown, the highlight rides `aria-activedescendant`, and a pointer press outside both the menu and the composer card dismisses it. Space and Enter adjudication polls the optional `matchSpace`/`matchEnter` hooks in registration order; the first non-undefined answer wins, and a source can refuse a submission it cannot consume whole. A candidate declaring `drill: true` carries a second verb beside the settling pick: its trailing chevron and the Tab key route the same row through `onPick` with `action: 'drill'` (every other path reports `'pick'`), and Tab passes untouched on rows without the flag so native focus traversal survives.
-----
<a id="understand-the-implementation"></a>
## Understand the implementation
<details>
<summary>Implementation internals — click to expand</summary>
`src/core/` is the pure core — trigger detection, menu reduction, and exact match, with zero React/DOM/cordis — while `src/client/service.ts` wires the core to the menu snapshot store, the per-hit candidate fetch (generation-gated, `AbortSignal`-superseded, failed sources dropping silently with a console record), and the pick paths. One `InputTriggerController` resolves per session scope (`sessionOf`); the conversation wiring layer drives `track`/`arbitrate`/`onSpace`/`adjudicate` on the controller. A source is warmed into every session controller it can reach; sources whose `lexicon` rolls change after warm implement `subscribeLexicon` and the controller re-polls on each notification. `MenuView` self-registers into `conversation.input.overlay` (list kind, session scope) and renders null while closed. The overlay SlotMap merge lives here because the dependency direction (ui-conversation → ui-input-trigger) admits no reverse type import.
</details>
-----
<a id="further-exploration"></a>
## Further Exploration
Read these pages when the trigger pipeline is not enough. They move from the pipeline to the sources that register into it and the shell that owns the input.
- [ui-commands](../ui-commands/README.md) — registers the `/` command source into this pipeline and owns the command popup shell.
- [ui-reference](../ui-reference/README.md) — registers the `@` file and session reference sources.
- [ui-conversation](../ui-conversation/README.md) — declares the input overlay slot and owns the composer and input machine.
- [Web client architecture](../../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md) — how browser plugin rows load and register slots.
-----
<a id="model-experience"></a>
## Model Experience
None, as the trigger pipeline is browser presentation only — picks produce `CommandClaim`/`ReferenceInsert` data whose model-visible consequences (host command execution; inserted reference text riding an ordinary prompt) are owned by the consuming host and input-machine packages.
None, as the trigger pipeline is browser presentation only — picks produce command claims and reference inserts whose model-visible consequences are owned by the consuming host and input-machine packages.
#### KV Cache effect
@@ -20,6 +68,21 @@ None; this package neither assembles nor sends a provider request.
## Known Limitations and Deferred Work
- **Global source layer only** — session-scope source registration (per-session shadowing, ScopedLayers-alike) is designed but not enabled; the ledger tracks the trigger condition (a real per-session source need).
- **`InputTriggerCandidate.icon` renders as text** — MenuView drops the string into the icon slot verbatim; wiring to the design-system icon enum (iconFile five-variant family) lands when that enum ships.
<a id="known-limitations-and-deferred-work"></a>
These limits define the current trigger pipeline. They are current package constraints, not a general menu comparison or a task backlog.
- **Global source layer only** — session-scope source registration (per-session shadowing) is designed but not enabled; the ledger tracks the trigger condition, a real per-session source need.
- **`InputTriggerCandidate.icon` renders as text** — `MenuView` drops the string into the icon slot verbatim; wiring to the design-system icon enum lands when that enum ships.
- **Overlay SlotMap merge home is split from slot ownership** — the sole `conversation.input.overlay` merge lives here, while ui-conversation owns its anchor, children declaration, and lifecycle because the dependency direction is ui-conversation → ui-input-trigger.
<a id="dev-note"></a>
### Dev Note
<details>
<summary>Working context for maintainers — click to expand</summary>
None.
</details>
+72 -9
View File
@@ -1,25 +1,88 @@
---
description: "Web GUI 的输入触发流水线:光标处的 / 与 @ 检测、分组候选菜单,以及把 pick 路由到已注册 source;供斜杠命令与引用的用户与维护者阅读。"
kind: "package-reference"
---
# @deepseek-ai/dsh-client-ui-input-trigger
[English](README.md) | 中文
输入触发流水线插件:光标处的 `/``@` 检测(词边界 + guard tier 规则)、分组候选菜单,以及把 pick 路由到已注册 source。`ctx.inputTriggers` 拥有 source roster,并按会话 scope`sessionOf`)各解析一个 `InputTriggerController`;对话接线层在 controller 上驱动 `track``arbitrate``onSpace``adjudicate`。同一个 controller 还暴露 `toggleSource`,供 chrome launcher 在一段合成 selection span 上只打开一个已注册 source;所得候选仍走通常的菜单、键盘仲裁、pick callback 与 scoped 输入改写。source 每次调用收到一个 `ClientSessionContext` 投影——会话始终由 agent(智能体)支撑,因此投影只含会话身份。source 在它能触达的每个会话 controller 中都会被预热:scope 创建时 roster 中已有的 source 会在 controller 构造期间预热,晚于此注册的 source 由注册动作本身预热进每个仍存续的 controller。`lexicon` 名录在预热后仍会变化的 source 实现 `subscribeLexicon(session, listener)`controller 每收到通知就重拉,并把聚合结果经其 `lexicon` 快照 store 发布。流水线与命令无关:空格/回车裁决按注册序轮询可选的 `matchSpace``matchEnter` 钩子,第一个非 undefined 的应答胜出。回车裁决还携带 `SubmitEnvelope`(composer 的图片附件数量),使 source 能拒绝它无法整体消费的提交;命令接受 composer 图片时,`CommandClaim` 声明 `images: true`,其 `submit` 随之以第三个参数收到序列化后的图片载荷。
## 概述
分层:`src/core/` 是纯内核——`detectTrigger``menuReduce``seedGroups``MENU_CLOSED``exactMatch`,零 ReactDOMcordis`src/client/service.ts` 是壳层,把内核接到菜单快照 store、逐 hit 候选拉取(以 generation 把关、后继请求经 `AbortSignal` 取代旧请求、失败的 source 静默丢弃并留一条 console 记录)和三条 pick 路径上。`ReferenceInsert.appearance` 可以把显示类型标为 `session``file``folder`,且不会改变其序列化 `ref`;图标与颜色由消费它的输入框负责。`src/types.ts` 与两个 `contract.ts` 文件是冻结的跨包约定;变更需经主线程仲裁
本包为 Web GUI 提供输入触发流水线:检测光标处键入的 `/``@`,显示分组候选菜单,并把 pick 路由到已注册 source。source 经 `ctx.inputTriggers` 注册——`/` 命令 sourceui-commands)、`@` 文件与会话引用 sourceui-reference),以及任何业务包——对话接线层按会话驱动这条流水线。键入触发器会 seed 为该触发器注册的所有 sourcechrome launcher 也可以在当前选区上只打开一个 source。流水线仅做呈现:pick 产出命令声明或引用插入,其后果属于消费它们的宿主与输入包
MenuView 把菜单 store 渲染进 `conversation.input.overlay` slot(列表类,会话 scope),菜单关闭期间渲染 null。键入式 trigger 会 seed 为该 trigger 注册的所有 source;程序化 launcher 只 seed 所请求的 source,并在菜单关闭或重新开始键入式 tracking 前,通过 controller 的 `launcher` 快照 store 发布该 source 名称。分组按可选的 `InputTriggerSource.order` 排序(越小越靠前,默认 0,同值保持注册序),组标题行经 `inputTriggers.menu` locale 命名空间本地化(未知 source 显示其原名)。`showGroupTitle: false` 会在 pending 与 ready 状态全程隐藏该行,ready 且候选项声明了 section 的组则以这些 section 标题行取代 source 标题。列表高度受限于 composer 上方的可用空间,指针落在菜单与所在 composer 卡片之外即关闭菜单。该 slot 由 ui-conversation 的组合器条目拥有(锚点、children 声明、生命周期);其 SlotMap 类型合并放在本包的 `src/client/slots.ts`,因为依赖方向(ui-conversation → ui-input-trigger)不允许反向的类型导入。combobox 模式:焦点始终留在编辑器表面,行在 mousedown 时完成 pick,高亮由 `aria-activedescendant` 承载。声明 `drill: true` 的候选行在选定 pick 之外携带第二个动词:行尾的 chevron 与 Tab 键把同一行以 `action: 'drill'` 送入 `onPick`(其余路径一律报告 `'pick'`);未声明该标记的行上 Tab 原样放行,原生焦点遍历不受影响。
## 目录
`/client` 导出接口是插件主体(`apply``inject`)、`InputTriggerService``MenuViewInjected` 与约定类型。MenuView 本身是内部实现——slot 注册以闭包持有它。
- [使用本包](#use-this-package)
- [理解实现](#understand-the-implementation)
- [进一步探索](#further-exploration)
- [模型体验](#model-experience)
- [已知限制与延期工作](#known-limitations-and-deferred-work)
- [开发备注](#dev-note)
-----
<a id="use-this-package"></a>
## 使用本包
`ui-conversation` 一起挂载本插件;用户在光标处键入触发器时,菜单随即出现在输入浮层中。分组候选项渲染在标题行之下;pick 路由到 source,消费方表面应用其结果——斜杠命令打开其弹窗或执行,引用插入其行内 token。
### 键盘与鼠标
菜单打开期间 composer 表面保持焦点:行在 mousedown 时完成 pick,高亮由 `aria-activedescendant` 承载,指针落在菜单与所在 composer 卡片之外即关闭菜单。空格与回车裁决按注册序轮询可选的 `matchSpace``matchEnter` 钩子;第一个非 undefined 的应答胜出,source 也可以拒绝它无法整体消费的提交。声明 `drill: true` 的候选行在选定 pick 之外携带第二个动词:行尾的 chevron 与 Tab 键把同一行以 `action: 'drill'` 送入 `onPick`(其余路径一律报告 `'pick'`);未声明该标记的行上 Tab 原样放行,原生焦点遍历不受影响。
-----
<a id="understand-the-implementation"></a>
## 理解实现
<details>
<summary>实现细节——点击展开</summary>
`src/core/` 是纯内核——触发器检测、菜单归约与精确匹配,零 React/DOM/cordis——而 `src/client/service.ts` 把内核接到菜单快照 store、逐 hit 候选拉取(以 generation 把关、后继请求经 `AbortSignal` 取代、失败的 source 静默丢弃并留一条 console 记录)与 pick 路径上。每个会话 scope 各解析一个 `InputTriggerController``sessionOf`);对话接线层在 controller 上驱动 `track``arbitrate``onSpace``adjudicate`。source 会被预热进它能触达的每个会话 controller;`lexicon` 名录在预热后变化的 source 实现 `subscribeLexicon`controller 每收到通知就重拉。`MenuView` 自注册进 `conversation.input.overlay`(列表类,会话 scope),菜单关闭期间渲染 null。overlay 的 SlotMap 合并放在本包,因为依赖方向(ui-conversation → ui-input-trigger)不允许反向的类型导入。
</details>
-----
<a id="further-exploration"></a>
## 进一步探索
当触发流水线不够用时阅读以下页面。它们从流水线进入注册进它的 source,以及拥有输入的会话外壳。
- [ui-commands](../ui-commands/README.zh.md)——把 `/` 命令 source 注册进本流水线并拥有命令弹窗外壳。
- [ui-reference](../ui-reference/README.zh.md)——注册 `@` 文件与会话引用 source。
- [ui-conversation](../ui-conversation/README.zh.md)——声明输入浮层槽位并拥有 composer 与输入状态机。
- [Web 客户端架构](../../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md)——浏览器插件行如何加载并注册槽位。
-----
<a id="model-experience"></a>
## 模型体验
无。触发流水线只是浏览器呈现——pick 产出 `CommandClaim``ReferenceInsert` 数据,其模型可见后果(宿主命令执行;插入的引用文本随普通提示词发送)由负责消费这些数据的宿主与输入状态机包负责。
无。触发流水线只是浏览器呈现——pick 产出命令声明与引用插入,其模型可见后果由消费它们的宿主与输入状态机包负责。
#### KV Cache 影响
无;该包既不组装也不发送提供方请求。
## 已知限制与暂缓事项
## 已知限制与延期工作
- **只有全局 source 层**:会话 scope 的 source 注册(逐会话遮蔽、类 ScopedLayers 机制)已有设计但未启用;台账记录着触发条件(出现真实的逐会话 source 需求)。
- **`InputTriggerCandidate.icon` 以文本渲染**:MenuView 把该字符串原样放进图标位;与设计系统图标枚举(iconFile 五变体家族)的接入将在该枚举交付后完成。
- **overlay 的 SlotMap 合并归属与 slot 所有权分离**:唯一的 `conversation.input.overlay` 合并放在本包,而 ui-conversation 负责其锚点、children 声明和生命周期,因为依赖方向是 ui-conversation → ui-input-trigger。
<a id="known-limitations-and-deferred-work"></a>
这些限制界定了当前触发流水线。它们是当前包约束,不是通用菜单对比或任务积压。
- **只有全局 source 层**——会话 scope 的 source 注册(逐会话遮蔽)已有设计但未启用;台账记录着触发条件,即真实的逐会话 source 需求。
- **`InputTriggerCandidate.icon` 以文本渲染**——`MenuView` 把该字符串原样放进图标位;与设计系统图标枚举的接入将在该枚举交付后完成。
- **overlay 的 SlotMap 合并归属与槽位所有权分离**——唯一的 `conversation.input.overlay` 合并放在本包,而 ui-conversation 拥有其锚点、children 声明与生命周期,因为依赖方向是 ui-conversation → ui-input-trigger。
<a id="dev-note"></a>
### 开发备注
<details>
<summary>维护者的工作上下文——点击展开</summary>
无。
</details>