dsh-plugin-type-to-compose
在 DSH 窗口里任意位置直接打字或粘贴,内容会落进当前对话的输入框 —— 不用先点一下输入框。
English · MIT · 零依赖 · 零构建
典型场景:点开一条历史消息、在侧边栏点了个会话、或者用鼠标选完文字之后, 直接敲键盘 / 按 ⌘V 就能继续写;不用把鼠标移回底部输入框再点一下。
| 你正在哪里 | 按键 | 结果 |
|---|---|---|
| 消息区 / 侧边栏按钮 / 页面空白(焦点在 body) | a 1 / 中… |
✅ 焦点移交输入框,字符进入输入框 |
| 消息区 / 侧边栏按钮 / 页面空白 | ⌘V / Ctrl+V / Shift+Insert | ✅ 焦点移交输入框,粘贴内容进入输入框(图片/文件也走官方粘贴逻辑) |
| 输入框本身(已经聚焦) | 任意 | ✅ 照常输入,不重复、不抢焦点 |
| 搜索框 / 自定义回答框等其它输入控件 | 任意 | ⛔ 不动,字符/粘贴留在原处 |
终端(.xterm) |
任意 | ⛔ 不动 |
已打开的弹窗 / 菜单(role=dialog[aria-modal]、role=menu) |
任意 | ⛔ 不动,键盘归弹窗 |
| 输入框被禁用 / 未就绪(Hero 态、被阻塞的会话) | 任意 | ⛔ 不动(没有可写的地方) |
| — | 其它 ⌘/Ctrl/Alt 组合键、Tab、Enter、Esc、方向键、退格、空格 | ⛔ 不动(见下) |
安装
# 本地目录(本仓库 clone 下来之后)
dsh plugin --profile desktop add "link:$PWD"
# 或直接从 GitHub
dsh plugin --profile desktop add "github:daoyu1993-lab/dsh-plugin-type-to-compose"
它会把这个包加进 profile 的 dependencies 与 dsh.profile.bundles,
并在 node_modules/ 下建一个指向本目录的软链接 —— 所以在仓库里改 client.js
会被 dsh-hmr 热更新直接读到。应用运行中也能安装。
装完刷新页面(⌘R)即可生效。
dsh-hmr会监听 profile 的package.json,新增 bundle 会被热同步。
作者机器上的 profile(~/.dsh/profiles/desktop/package.json)就是这样装的:
"dependencies": {
"dsh-plugin-type-to-compose": "link:/Users/wangdaoyu/Documents/deepseek-harness/DSH UI Lab/dsh-plugin-type-to-compose"
},
"dsh": { "profile": { "bundles": [ ..., "dsh-plugin-type-to-compose" ] } }
卸载 / 回滚
dsh plugin --profile desktop remove dsh-plugin-type-to-compose # 需完全退出 App
或直接编辑 ~/.dsh/profiles/desktop/package.json:从 dependencies 与
dsh.profile.bundles 里各删掉一行,重启应用。插件本身不改任何官方文件,
删掉之后界面立即回到原样。
实现方式:一个 document 级监听(keydown + paste)
DSH 的输入框是 Lexical 托管的 contenteditable,官方在它身上挂了稳定的锚点
[data-composer-input](@deepseek-ai/dsh-client-ui-conversation 里的
ComposerContentEditable),旁边还有 [data-input-scroll] / [data-composer-placeholder]。
本插件只做一件事:
keydown(用户开始打字)
→ 判断这一下是不是「文本输入意图」、目标是不是别人家的控件
→ 把焦点移到 [data-composer-input]
→ 浏览器自己的默认动作把字符插进这个刚获得焦点的元素
没有回放、没有合成输入事件、没有任何槽位注册,也没有一行 CSS —— 最坏情况是 「条件判断失效 → 不接管」,永远不会把输入框弄坏。
为什么监听写在冒泡阶段
捕获阶段会在所有其它处理器之前跑,可能抢走组件已经认领的键。冒泡阶段里
React(挂在 #root)与页面上的处理器都已跑完,于是 event.defaultPrevented
是个真实信号:任何被 DSH 自己消费掉的键我们都不碰。
为什么是「只聚焦」而不是「preventDefault + 手动插入」
两条路线都实现过并实测(tools/type-to-compose-probe.mjs,真实 Chrome + CDP 真实按键):
| 变体 | 打字后输入框内容 | 焦点 | 备注 |
|---|---|---|---|
| 不接管(对照组) | "" |
BODY | 点开消息后打字确实什么都不会发生 |
| 只聚焦(本插件采用) | "abc" |
composer | 依赖浏览器默认动作插进「刚获得焦点」的元素,零副作用 |
preventDefault + execCommand('insertText') |
"abc" |
composer | 结果相同,但多用了已废弃 API、且放弃原生插入路径 |
两者结果一致,所以选副作用更小的那条:不阻断默认行为,只搬焦点。
中文/日文输入同理:keyCode === 229(key === "Process")这种「交给输入法」的键
也会把焦点交过去,但绝不 preventDefault,输入法组字不受干扰。
光标落在哪里(已有草稿时)
只做 DOM focus() 有个坑:内容可编辑元素会把光标放在草稿开头,于是「接着写」变成
「插到最前面」。官方输入栏自己就注释过这一点,它的解法是「DOM focus + editor.focus()」——
Lexical 会把光标还原到用户上次的位置。
本插件照做,而且不引入对 Lexical 的构建期依赖:
- DOM
focus({preventScroll:true})拿焦点; - 如果元素上挂着 Lexical 的编辑器实例(Lexical 在根元素上暴露
__lexicalEditor), 调它的focus()还原光标;异常就忽略; - 没有编辑器实例(元素不是 Lexical、或将来被换掉)时,退化为把光标放到草稿末尾, 保证「接着写」仍是追加而不是插队。
三种情形都在测试里断言过(无句柄追加到末尾 / 有句柄则委托给它并落在它恢复的位置)。
粘贴:同样只搬焦点
粘贴不能照搬打字的做法。官方的粘贴逻辑是注册在输入框自己身上的一条 Lexical 命令
(PASTE_COMMAND):它从 event.clipboardData 取文本、走 intakeFiles 收图片/文件、
再给这次粘贴打一个历史标记。所以只要 paste 事件能落在输入框上,官方的全套行为
(文字、截图、文件、撤销粒度)都会原样生效,我们一行都不用重写。
于是分两条路:
- ⌘V / Ctrl+V / Shift+Insert 的 keydown → 只把焦点交给输入框,不 preventDefault。
浏览器随后发起的粘贴自然落在刚获得焦点的输入框上。
(实测:
tools/paste-to-compose-probe.mjs里focus变体的 paste 目标是composer, 对照组none落在BODY—— 这正是「粘贴没激活输入框」的原因。) - 没有 keydown 的粘贴(原生 Edit 菜单粘贴、鼠标中键粘贴)→ document 级
paste监听兜底:preventDefault后把焦点交给输入框,并把原始clipboardData重新投递给 它(new ClipboardEvent('paste', { clipboardData })),因此图片/文件同样不丢; 只有当输入框没有消费这次事件时,才退化成写入纯文本。 如果第 1 条已经生效,paste 事件本身就落在输入框上,这条兜底会因为「目标就是输入框」直接返回,不会粘两次。
哪些键不接管
- 除粘贴(⌘V / Ctrl+V / Shift+Insert)以外的 ⌘/Ctrl/Alt 组合键;
- 一切非单字符键:Tab、Enter、Esc、方向键、退格、功能键、死键(
Dead); - 空格:空格是文本键,但也是「往下翻页」的常用键。只聚焦会把「翻页」变成
「以空格开头的新消息」,所以默认让路。想连空格一起接管,把
client.js里的DEFER_SPACE改成false即可(输入框已聚焦时不受影响,空格照常输入)。
多个对话面板
同一个窗口里可能有多个会话面板(例如嵌入式子会话),各自带一个 [data-composer-input]。
插件只会选当前可写且已排版(isContentEditable + getClientRects() 非空)的那个,
并在有多个候选时优先选用户最后点过或聚焦过的面板(pointerdown / focusin 追踪,
[data-conversation-session] 作为面板边界);隐藏的面板不会被选中。
验证
1. 真实引擎下的行为测试:tools/test-type-to-compose.mjs
把 client.js 原样加载进一个 DSH 外壳的 DOM 副本(侧边栏按钮、可见对话栏、
可显示/隐藏的第二栏、搜索框、xterm 式终端、可开关的 dialog),再用 Chrome DevTools
Protocol 派发真实按键,逐条断言:
# Node ≥ 21(脚本用到全局 fetch / WebSocket);本机没装 node 时也可以用 DSH 自带的:
node tools/test-type-to-compose.mjs
# NODE="/Applications/DeepSeek Harness.app/Contents/Resources/runtime/primary-runtime/dependencies/node/bin/node"
# "$NODE" tools/test-type-to-compose.mjs
当前结果:43/43 通过,退出码 0。覆盖:页面空白/侧边栏按钮上打字进输入框;
其它输入控件与终端不被抢;空格让路;⌘/Ctrl 组合键与 Tab/Enter/Esc/方向键不接管;
隐藏 dialog 不阻断、可见 dialog 接管键盘;输入框不可编辑时不动焦点;
双栏按最近点击选择且隐藏栏被跳过;已聚焦输入框不重复插入;Shift 大写;
已有草稿时的光标位置(无句柄追加末尾 / 有句柄委托 editor.focus());
⌘V 后 paste 落在输入框、Shift+Insert 同样生效、无 keydown 的粘贴被转发且复用原始
clipboardData、粘贴在别的输入框/弹窗打开时不抢;
IME 起始键(229)只搬焦点不写字;卸载后不再接管;全过程无控制台错误。
测试页里有一个测试专用的
preventDefault兜底监听,用来规避一个 Chromium DevTools 注入按键的怪癖:headless 下未被消费的 keydown 会无限自动重复。 它注册在插件监听之后,所以插件看到的仍是defaultPrevented === false, 不影响被测行为。
2. 机制对照实验
tools/type-to-compose-probe.mjs:上面「只聚焦 vs 手动插入」那张表的来源。tools/paste-to-compose-probe.mjs:粘贴落点对照(--headful用真实系统剪贴板):变体(真实剪贴板) paste 落在哪 输入框内容 不接管 BODY(有数据但没人接)""← 就是「粘贴没激活输入框」的现象按下 ⌘V 时先交出焦点 composer"PASTED-TEXT-中文"没有 keydown,靠 document 监听转发 BODY→ 转发到composer"PASTED-TEXT-中文"目标本来就是别的输入框 留在原输入框 不抢 这条实验解释了为什么两条路都要有:浏览器/Web 表面与 Windows 桌面把 ⌘V 作为按键送到页面, 「先聚焦」那条就够;macOS 桌面的原生 Edit 菜单会先吃掉 ⌘V 加速键,页面只收到一个落在
BODY上的paste事件(Electron 的 Paste 角色最终调用的正是同一个 Blink 命令), 这时靠 document 监听兜底转发。 headless Chrome 没有剪贴板(内容为空),所以真实内容断言用--headful跑; headless 下可测的信号是「paste 落在哪个元素」。正文内容另由tools/test-type-to-compose.mjs用构造的ClipboardEvent断言。
已知边界
- 依赖
[data-composer-input]这个锚点(官方为「输入框宿主」专门打的数据属性, 不含构建哈希)。若上游改名,插件会静默停止生效 —— 不会报错、也不会破坏界面。 - 只在浏览器/Web 表面生效;宿主侧模块是空实现(每个 profile 行都需要一个 Node 侧模块)。
- 空格默认让路(见上,一行开关)。
- 目前 DSH 没有任何「无修饰键的全局单键快捷键」(已核对:只有方向键/Enter/Esc 以及带修饰键的组合),所以接管单字符键不会遮蔽官方快捷键; 将来若新增,请把它加进跳过名单。
仓库结构
client.js 浏览器侧:全部逻辑(唯一有行为的文件)
index.js 宿主侧:空实现(每个 profile bundle 都需要一个 Node 侧模块)
cordis.patch.yml 以 profile bundle 层挂载本包
package.json 含 dsh.bundle.patch / dsh.client 声明
README.md / README.en.md 中英文说明
LICENSE MIT
tools/test-type-to-compose.mjs 真实 Chrome + 真实按键的行为测试(43 项)
tools/type-to-compose-probe.mjs 打字:只聚焦 vs 手动插入 的对照实验
tools/paste-to-compose-probe.mjs 粘贴:paste 落点与 --headful 真实剪贴板实验
No comments yet. Be the first to write one.