DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

daoyu1993-lab /

daoyu1993-lab/dsh-plugin-type-to-compose

Verified

DSH plugin: type or paste anywhere in the window and it lands in the current conversation's composer — no click needed.

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHubProject homepage
READMESource: main@450b477f

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 的构建期依赖:

  1. DOM focus({preventScroll:true}) 拿焦点;
  2. 如果元素上挂着 Lexical 的编辑器实例(Lexical 在根元素上暴露 __lexicalEditor), 调它的 focus() 还原光标;异常就忽略;
  3. 没有编辑器实例(元素不是 Lexical、或将来被换掉)时,退化为把光标放到草稿末尾, 保证「接着写」仍是追加而不是插队。

三种情形都在测试里断言过(无句柄追加到末尾 / 有句柄则委托给它并落在它恢复的位置)。

粘贴:同样只搬焦点

粘贴不能照搬打字的做法。官方的粘贴逻辑是注册在输入框自己身上的一条 Lexical 命令 (PASTE_COMMAND):它从 event.clipboardData 取文本、走 intakeFiles 收图片/文件、 再给这次粘贴打一个历史标记。所以只要 paste 事件能落在输入框上,官方的全套行为 (文字、截图、文件、撤销粒度)都会原样生效,我们一行都不用重写。

于是分两条路:

  1. ⌘V / Ctrl+V / Shift+Insert 的 keydown → 只把焦点交给输入框,不 preventDefault。 浏览器随后发起的粘贴自然落在刚获得焦点的输入框上。 (实测:tools/paste-to-compose-probe.mjs 里 focus 变体的 paste 目标是 composer, 对照组 none 落在 BODY —— 这正是「粘贴没激活输入框」的原因。)
  2. 没有 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 真实剪贴板实验
—/ 5

No ratings yet

Verified DSH bundle

Commit 450b477f8bd8

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout