# dsh-client-ui-selection-add

[English](README.md) | 中文

> 源码：[pangzi-club/dsh-client-ui-selection-add](https://github.com/pangzi-club/dsh-client-ui-selection-add)

Web「选中即添加」功能插件：其浏览器侧把一个悬浮的**添加到对话**按钮贡献到会话拥有的 `conversation.input.dock` 槽位。只要某个会话的编辑器处于挂载状态，插件就监听文本选区；一个非空、非可编辑的选区会在其外接矩形上方浮出一个小按钮，点击它会**预填**编辑器草稿（`inputActions.setDraft`）而不发送——由用户自行确认后回车（或点发送）。其宿主侧刻意为空：捕捉选区并预填草稿属于纯浏览器端的展现，宿主无需持有任何状态。

组件挂载在**会话作用域**的 list 槽里，因此框架会交付会话标准 kit（`useSession`、`sessionId`、`useProjection`、`useInput`、`inputActions`），加上 `session`/`input` 的 owner share 与 `selection` 的 locale 席位；组件只取 `inputActions` 与 `t`。由于按钮通过 body 级 portal 渲染，该 dock 条目自身什么都不渲染——编辑器布局不受影响。

有两点值得说明：

- **预填而非直发。** 点击只调用 `setDraft(text)`；刻意不调用 `submit()`，让用户把引用的文本编辑过后再交给模型。（若要点击即发送，在 `SelectionAddButton` 里补一行即可。）
- **可编辑域守卫。** 位于 `input`、`textarea` 或 `contenteditable` 里的选区永不显示按钮——在编辑器里选中是在改草稿，不是引用对话，按钮会与光标打架。

按钮文案在 `dsh-client-locale` 的 `selection` 命名空间下双语。锚点捕捉放在一个与框架无关的 `selection.ts` 模块里（`selectionAnchorOf` / `isEditableField`），单独单元测试。

## 模型体验

无。这里的一切都是浏览器交互：模型看不到工具、会话事件或 schema。该插件是基于输入机公开 `inputActions` 表面的纯呈现层。

#### KV Cache 影响

无失效。插件不读取会话数据，也不发布会话事件，因此会话日志、projections 与 KV 缓存都不受影响。

## 已知限制与后续工作

- **未限定在对话正文。** 选区监听挂在 `document` 上；目前唯一的守卫是可编辑域检查。在侧栏或设置里选文本也会弹出按钮，因为在不改 `ui-conversation`（上面的「零改动核心包」取舍）的前提下，插件无法识别会话的滚动容器。要限定范围，需要正规的 `conversation.selection.*` 槽（见下），或一个会话骨架本就拥有的 `data-*` 钩子。
- **仅预填。** 用户须在发送前确认；悬浮按钮上没有「立即发送」的入口。
- **多行选区** 锚定在外接矩形的顶部（第一行）；若选区起始靠近视口顶部，按钮可能被裁切。
- **按构造即仅限聊天视图。** 按钮只在会话编辑器挂载时出现，因此不会出现在无会话的 hero 阶段；它并不区分当前激活的是哪个视图页签。

## 扩展点（「正规槽」）升级

当前路径借用既有的 `conversation.input.dock`，因此对 `ui-conversation` 零改动。若要把捕捉做成一等扩展点，则在 `ui-conversation`（`packages/client/ui-conversation/src/client/apply.ts` 的 `children` 以及 `contract/slots.ts` 的 `SlotMap` 合并）新增一个会话作用域槽，例如 `conversation.selection.action` `{ kind: 'list', scope: 'session' }`，并把本插件的条目注册到新槽。

## 独立构建并发布（社区插件）

本包是一个**可构建、可独立发布**的 DSH 客户端插件。它**不需要** deepseek-harness monorepo 即可编译：`package.json`、`tsconfig.json` 与 `tsdown.config.ts` 都是独立的，且源码里的 `@deepseek-ai/*` 导入均为 `import type`，会从 `node_modules` 中按其发布版本解析。

### 构建

```bash
pnpm install
pnpm bundle        # tsc（产出 lib/types/**/*.d.ts）+ tsdown（产出 lib/*.js）
pnpm watch         # tsdown --watch
```

产物：
- `lib/index.js` — 宿主半（空的 `apply`；实际工作全部在浏览器半）
- `lib/invariant.js` — 不变式伴生模块
- `lib/client.js` — 浏览器 module-table bundle（`window.__ModuleLoader__.load({ id, factory })`）
- `lib/types/**/*.d.ts` — TypeScript 声明

### 发布

先 `pnpm bundle`（或依赖 `prepack` 自动执行），然后：

```bash
npm publish            # 或 pnpm publish
```

发布面由 `files` 列表决定：`lib/*.js`（+ map）与 `lib/types/**/*.d.ts`，以及 `cordis.patch.yml`。

### 安装到 DSH（社区插件）

本包声明了 `dsh.bundle` manifest，因此与其他社区插件走同一通道（从 npm 安装）：

```bash
npm install dsh-client-ui-selection-add
dsh plugin --profile web add dsh-client-ui-selection-add
```

或对本地构建：

1. 把本目录符号链接/junction 到 `$DSH_HOME/profiles/node_modules/dsh-client-ui-selection-add`。
2. 在 `$DSH_HOME/profiles/web/cordis.patch.yml` 追加宿主行：
   ```yaml
   - insert:
       - id: ui-selection-add
         name: dsh-client-ui-selection-add
   ```
   （或直接 `dsh plugin add`，它会通过 `dsh.bundle.patch` → `cordis.patch.yml` 帮你写入。）

客户端半会注册到框架拥有的 `conversation.input.dock` 槽位，并拉取 `locale` 服务。两者都由标准 `dsh web` 构建提供（`@deepseek-ai/dsh-client-ui-slots`、`@deepseek-ai/dsh-client-locale`、`@deepseek-ai/dsh-client-ui-conversation`），因此标准 `dsh web` 构建能解析这些运行时。由于这些框架包**并非**全部是基线 module-table 行（它们是独立的 `dsh.client` graph 行），裸的社区安装只在已激活它们的 DSH Web 构建下可用——上游 `dsh web` 恰好如此。

### 交叉引用：deepseek-harness monorepo

本仓库是此包的独立家园。同样的源码也接入了 deepseek-harness monorepo，名为 `@deepseek-ai/dsh-client-ui-selection-add`，位于 `packages/client/ui-selection-add`；在那里，tsdown preset 与三处注册面（`tsconfig.client.json` 引用、`packages/bundle/web-app/cordis.patch.yml` 的 `dsh.client` 名册行、`packages/bundle/web-app/package.json` 依赖）都来自 monorepo 本身。本目录则改用上面的自包含配置来构建与发布。
