Skip to content

dsh-hotkey-enhance

Verified

@brantshi1/dsh-hotkey-enhance · v0.3.0 · MIT · Web UI

Desktop keyboard enhancement for DSH: a Codex-style hold-to-preview session switcher plus the desktop keymap DSH never applies. DSH Desktop only.

Install

dsh plugin add @brantshi1/dsh-hotkey-enhance

Confirm the layer applied with dsh --profile default --dump-config — see the install guide.

Source

Tags

Creators

Readme

dsh-hotkey-enhance

npm: @brantshi1/dsh-hotkey-enhance English | 中文

DSH Desktop 的键盘增强插件:Codex 式的会话切换手感 + 一套 DSH 2.0.18 从未生效过的桌面键位。 2.0.21-next 起宿主键位表自己接管了其中 3 条(⌘B / ⌘, / ⌘T),插件只保留宿主没有的 8 条(见「让位规则」)。

仅适配 DSH Desktop。 本插件绑定的每一个组合(Ctrl+Tab、⌘N、⌘W、⌘D、⌘J)在真实浏览器里 都归浏览器所有——Ctrl+Tab 切标签页、⌘N 开新窗口、⌘W 关标签页、⌘D 加书签、⌘J 开下载。因此插件在 非 Electron 环境里主动不安装任何监听器,加载即惰性返回;在浏览器里使用不会有任何效果,也不会抢键。

为什么做这个

从 Codex 迁到 DSH 之后,最不习惯的是切换会话的手感:Codex 里按住 Ctrl 连点 Tab,会浮出最近会话列表, 松手才真正切过去;DSH 里没有这个能力,而且 Desktop 2.0.18 的 desktop:* 键位表整套失效(详见下节), 连 ⌘N 都按不出来。这个插件就是补这两件事。宿主从 2.0.21-next 起已经恢复 desktop:* 默认键位,其中 ⌘B / ⌘, / ⌘T 三条已交还宿主,插件只管剩下的八条。

历史背景:2.0.18 为什么不走注册表

DSH Desktop 2.0.18 的客户端跑在 web runtime(以下都是该版本的事实):

  • dsh-client-shortcuts 的 detectEnvironment() 读 document.documentElement.dataset.platform, 但2.0.18 的安装包里没有任何代码给这个属性赋值(preload 只暴露了 protocolVersion,没有 dshDesktop.keyboard 桥)。
  • 于是在 2.0.18 上所有命令的 desktop:* 默认键位全部失效,实际生效的是 web:* 那套 (新会话 Cmd+Alt+N、搜会话 Cmd+Alt+K、加工作区 Cmd+Alt+O、侧栏 Cmd+Alt+B)。2.0.21-next 起 preload 会打 data-platform,desktop 默认键位恢复生效。
  • 更关键的是 isWebBindingAllowed():web runtime 下注册表主动拒绝浏览器保留组合,只放行 三个及以上修饰键、Cmd+Alt+X / Cmd+Shift+X、以及 Cmd+, Cmd+\ Ctrl+\`` Cmd+/等少数键。Cmd+N、Cmd+W、Cmd+D、Ctrl+Tab**全部在白名单外**,注册时会抛Unsupported Web shortcut: `。

因此本插件不向注册表注册命令,而是直接在 window 捕获阶段监听 keydown——这也是社区插件 (dsh-custom-hotkeys、dsh-file-finder)面对同一限制时的做法。

副作用:这八条键不会出现在 设置 → 通用 → 快捷键 里(那是注册表的目录)。改键改由本插件自己的 卡片负责:设置 → 通用 → 会话快捷键。

提供什么

按键 动作
Cmd/Ctrl+N 新建会话
Cmd/Ctrl+W 关闭当前会话,回会话列表
Cmd/Ctrl+Shift+W 同上(macOS 原生菜单的 File → Close Window 也绑了 Cmd+W,这个键是备用)
Cmd/Ctrl+D 置顶 / 取消置顶当前会话
Ctrl+Tab 打开最近会话浮窗;按住连按移动高亮,松开才切换
Ctrl+Shift+Tab 同上,反向(从最旧的一项开始)
Cmd+J 新建终端
Cmd+Opt+S 打开侧边对话(需 better-sidebar)

上述八条全部可在 设置 → 通用 → 桌面快捷键 里录制改键。

⌘B / ⌘, / ⌘T 不在本插件里:DSH 原生键位表已经同键同命令接管(sidebar.left.toggle / settings.open / browser.new),要改它们请去 设置 → 快捷键。

依赖关系只说一次:⌘J 转发 DSH 自带的 terminal.new,不依赖任何第三方插件; ⌘⌥S 需要 dsh-better-sidebar。没装它时,设置卡片里这一行会 置灰并注明「需要安装 dsh-better-sidebar」,按键也不会被吞——装上并重启后自动恢复可用。

浮窗切换器(Ctrl+Tab)

按下 Ctrl+Tab 会弹出最近会话列表,而不是立刻切换:

┌────────────────────────────────┐
│ 最近会话                        │
│   当前会话            当前  1m  │   ← 真实当前会话,带「当前」徽标
│ ▸ 两分钟前                    2m │   ← 高亮行,从「下一个」开始
│   一小时前的会话              1h │
│                                │
│ 松开 ⌃ 切换 · Tab 继续选 · Esc   │
└────────────────────────────────┘
动作 结果
Ctrl+Tab 打开浮窗,高亮落在「下一个」会话
松开 Tab(Ctrl 仍按住) 浮窗保留,不切换——Tab 是可以反复点的「下一项」按钮
再点 Tab 高亮下移一格,到底回卷
按住 Tab 自动重复,逐行走
松开 Ctrl 提交:切到高亮行并关闭浮窗
Esc 取消,不切换
窗口失焦 / 页面隐藏 / 30s 无任何事件 取消(仅作 keyup 丢失的兜底,正常使用不会触发)

实现要点:

  • 浮窗挂在 shell.overlay 插槽,pointer-events: none,不抢焦点——否则 keyup 收不到。
  • 候选列表在打开瞬间快照,避免后台会话刷新顺序导致高亮行游走。
  • 浮窗打开期间,其余会话快捷键暂时被抑制(否则 Cmd+N 会在预览中途插会话)。
  • 列表上限 9 条;过滤规则与之前一致(跳过 blank、归档、origin: "subagent")。
  • 每行显示:标题 · 「当前」徽标 · 相对时间(刚刚 / N 分钟前 / N 小时前 / N 天前)。

让位规则:按注册表实际占用判定

按下任一动作时,插件先取这条动作当前生效的 chord(改过就用改过的),再到 ctx.shortcuts.catalog 里找 有没有命令的 binding 与它相同:

  • 有 → 让位:不执行、不 preventDefault,交给宿主键盘层,避免同一个命令触发两次;
  • 没有 → 由插件自己执行。

这条判定与 runtime 无关,web 与 desktop 走的是同一条规则。旧版是按 shortcuts.runtime 猜的: 2.0.21-next 的桌面端 preload 给 <html> 打 data-platform 之后 runtime 变成 desktop,插件于是把 5 条动作 全部让出去,而宿主键位表里根本没有侧边对话对应的命令——⌘⌥S 于是两头没人接,按下去什么都不发生。

⌘J 是唯一的转发型动作:它按 terminal.new 当前的 binding 合成一次 keydown,所以你在设置里改过它的 键这里自动跟随;若 terminal.new 本来就绑在 ⌘J 上,上层判定已经让位,不会再重放一次(宿主 2.0.21-next 把它 绑在「⌃⌘ + 反引号」上,因此 ⌘J 会正常转发)。

⌘⌥S 调 better-sidebar 的公开服务 ctx.betterSidebar.openTab({ type: "sidechat" }, { sessionId })——侧边对话是 它的功能,它自己没有注册任何快捷键。服务是可选的,所以用 ctx.get() 读取而不是写进 inject:写进去的话 没装它的用户整个插件都不会激活。

设置卡片里,当前 chord 已被原生键位表接管的行会置灰且不可录制,并标出占用它的 command id; 录制新组合时与内置命令冲突的组合依旧会被拒绝并提示占用者 id。

改键与持久化

设置 → 通用 底部有一张「会话快捷键」卡片:

  • 每一行显示 动作 / 当前键 / 修改 / 恢复
  • 点 修改 后按下新组合即保存,Esc 取消
  • 组合必须至少包含一个 Ctrl/Alt/⌘;与其它行重复会被拒绝并提示
  • 全部恢复默认 一键回到出厂键位

键位存在 localStorage 的 dsh.sessionkeys.bindings.v1(同一 Electron 分区,重启保留), 结构为 { version: 1, bindings: { <动作>: "<修饰键>+<code>" } },例如 {"close":"Meta+KeyW","cycle":"Ctrl+Tab"}。损坏或不可读时自动回落到默认值。

只在命中组合时 preventDefault() + stopPropagation();未命中一律放行,不干扰输入框、终端、内嵌页面。

语义细节

  • 「最近会话」按 sessions.list 的 updatedAt 倒序,updatedAt 相同时用快照 ids 的原始顺序兜底, 保证连续按键时轮转顺序稳定。
  • 候选过滤:跳过 blank 会话、已归档会话、origin === "subagent" 的子会话 ——Ctrl+Tab 永远不会跳进 Agent Teams 的成员会话。
  • 当前会话判定:优先读 uiWorkspace.mainReference.sessionId(权威引用),读不到再回退到 retainedBy.mainView > 0 的投影。
  • Cmd+W 两级:主视图有会话 → uiWorkspace.clearMain() 回列表;已在列表 → 不消费事件, 让原生菜单/窗口处理。
  • Cmd+D 是 toggle:用 workspaces.list.pinnedSessionIds 判断当前状态,走 uiWorkspace.pinSession() / unpinSession()。
  • session.new(Cmd+N)在 web runtime 下原本是 Cmd+Alt+N,本插件用自己的处理器补上 Cmd+N; 若将来 DSH 让 desktop:* 生效并给 session.new 绑上 ⌘N,上面那条「按注册表占用让位」的判定会自动把这条键 交还给宿主,不需要再改代码。

安装

dsh plugin --profile desktop add @brantshi1/dsh-hotkey-enhance

然后重启 DSH Desktop。设置 → 通用 底部应出现「桌面快捷键」一节。

从源码装(本地开发用,改动即时生效):

dsh plugin --profile desktop add /path/to/dsh-hotkey-enhance

卸载

dsh plugin --profile desktop remove @brantshi1/dsh-hotkey-enhance

手动回滚:从 ~/.dsh/profiles/desktop/package.json 的 dependencies 与 dsh.profile.bundles 里删掉该包名,在 profile 目录执行一次 pnpm install,然后重启。

已录制的键位保存在 localStorage(dsh.sessionkeys.bindings.v1),卸载不会清除;重装后仍在。

验证

离线行为测试(假 window + 假 localStorage + 假 React + 假 cordis ctx,74 条断言,覆盖按键映射、 候选过滤、mainReference 优先级、Cmd+W 列表态不消费、输入法/修饰键忽略、监听器释放、录制改键、 Esc 取消、重复/弱组合拒绝、单条与全部恢复、持久化跨重载、按 catalog 占用让位(含 ⌘⌥S 在 desktop runtime 下的回归用例与 ⌘J 的转发/让位两态),以及浮窗的轻点/按住/回卷/反向/ Esc/先松 Ctrl/失焦/看门狗/预览期间抑制/快照冻结/单候选用例):

pnpm test      # 等价于 node tests/hotkey-enhance.test.mjs

调试真实按键:把 lib/client.js 里的 DEBUG 改成 true,每条命中的按键会以 [dsh-hotkey-enhance] ... 写进 ~/Library/Application Support/DSH Desktop/logs/dsh-<date>.error.log(renderer console 会被桌面壳捕获)。

已知边界

  • 因为绕过注册表,这八条键不会出现在 DSH 原生快捷键目录里;键位冲突由插件自己比对 catalog 的 binding 来判定。
  • 持久化用的是 localStorage,不是 profile 的 host 配置。要改成 host 配置(ctx.configForms) 需要给宿主侧补 Config(schemastery)并让插件目录能解析 @deepseek-ai/schemastery,且要重启。
  • macOS 上 Cmd+W 可能与原生 File → Close Window 菜单加速键竞争。当前实测渲染进程能收到该按键 (日志里出现过 keydown primary+W),行为正确;若某天菜单先手,用 Cmd+Shift+W 或改安装包去掉 菜单加速键。
  • 浮窗用 z-index: 2147483000 固定在顶层;若某个模态层更高,浮窗会被遮住(此时仍能松手切换,只是看不见)。
  • Cmd+1..9 未实现:侧栏可见顺序算在 ui-workspace 的 React 层,外部插件读不到 (groupExpansion / orderBy / 手动排序都不在服务面)。要做对,需要上游把可见顺序发布成状态。