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-enhanceEnglish | 中文
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)在webruntime 下原本是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/ 手动排序都不在服务面)。要做对,需要上游把可见顺序发布成状态。