dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DSH 插件:Web 输入框支持 shell 风格历史——↑ 翻出本会话已发送的文字,↓ 逐条回翻并恢复你在打的草稿。 | DSH plugin: shell-style composer input history - ArrowUp recalls sent messages, ArrowDown walks forward and restores the draft you were typing. 支持 DSH 0.2.0-rc.1+。
给 DeepSeek Harness Web 输入框加上 shell 风格的输入历史:按 ↑ 翻出本会话里已经发出去的文字,按 ↓ 往回翻,翻过最新一条就把你原本在打的草稿还回来(原本是空的就清空)。
装上即用。没有设置页,不替换任何官方 UI。
English | 中文
| 操作 | 结果 |
|---|---|
输入框为空时按 ↑ | 显示本会话发出的最后一条消息 |
光标在第一行时按 ↑ | 同上(多行草稿里光标上下移动照常,不会被抢) |
继续按 ↑ | 逐条往回翻,翻到最老一条后停住 |
按 ↓ | 逐条往回翻 |
翻过最新一条再按 ↓ | 恢复到「开始翻历史之前」的草稿;当时为空则清空 |
| 翻历史过程中打字 | 立即退出历史浏览,保留你打的字 |
↑/↓ 会持续翻历史;你一旦改动草稿、或把光标点到文字中间,浏览态立刻结束,方向键恢复成普通光标移动。↑ 从最新消息开始,而不是接着上次翻到的地方。Shift / Ctrl / Cmd / Alt + 方向键(选择文本、应用快捷键)/)打开时(那是菜单的导航键)isComposing / keyCode 229)# 从 GitHub 仓库安装(pnpm 会把仓库拉进 profile)
dsh plugin --profile web add github:masknull/dsh-composer-history
安装后 profile 的 dsh.profile.bundles 会多出 dsh-composer-history,Web 端重新加载页面即生效。目前尚未发布到 npm;dsh plugin --profile web add dsh-composer-history 这种写法要等发布之后才可用。
卸载:
dsh plugin --profile web remove dsh-composer-history
>= 0.2.0-rc.1(在 0.2.0-rc.2 上实测)dsh.client.platform: "web",在终端或 Desktop 原生键盘通路上不做事。下面每一处缝合点都是读官方 0.2.0-rc.2 源码确认过的,没有靠猜。
拿得到官方的写入接口。 conversation.input.overlay(list 槽、session 作用域)由 @deepseek-ai/dsh-client-ui-conversation 渲染在常驻输入卡片内部,槽位目录里明确声明每个占用者都能拿到标准 props useInput(InputState 快照)和 inputActions(SessionInputShell.actions)。其中 inputActions.setDraft(text) 就是「整段草稿的程序化写入」:做占位符清理、按 \n 拆段落、光标落到末尾、并与撤销历史合并。
找得到稳定 DOM。 输入卡片是 [data-composer-card],里面的 Lexical contenteditable 是 [data-composer-input] —— 都是 ComposerContentEditable 挂的稳定属性,不是带构建 hash 的 class。
抢得到按键、又抢得准。 官方 composer 的 keymap 只仲裁触发器菜单的按键,其余按键交给编辑器 —— 所以 ↑/↓ 本来没有归属。监听器必须挂在卡片的捕获阶段:
document 上,一个页面上两个 composer 实例(主区 + 内嵌)会同时响应一次按键。
捕获挂在各自的卡片上,一次按键只会命中一张卡片;每个占用者只渲染一个 hidden 的 <span>,它唯一的用途就是把监听器锚定到自己这张卡片。不抢菜单的键。 触发器菜单的 viewport 带 role="listbox",它可见时插件直接放行。
对启动顺序免疫。 客户端插件是并发启动的,apply 执行时 slots 服务可能还不存在,而且只监听 internal/service 也不一定兜得住(开发时真踩到了:占用者迟迟不出现)。因此插件用双通道取服务(ctx.slots + ctx.get('slots'))+ 有界重试(每 400 ms 一次、最多 25 次),服务一出现立刻注册。
官方发送一条普通消息时,状态机的 phase 保持 plain(SubmitMachine.beginDetached),而 Session 的提交回声要等 sink 里附件编码的 await 之后才登记 —— 也就是说「草稿被清空」那一刻,唯一可观测的事实就是清空 + 一个发送手势。于是插件记录两个手势:
Enter(排除 Shift、Alt、Ctrl+Cmd 这些不发送的组合)<button> 的点击 —— 即主提交按钮:工具行里所有按钮(包括 conversation.input.right 的占用者)都在它前面,它的 class 带构建 hash,位置才是稳定属性任一手势配上 700 ms 内的一次「非空 → 空」草稿变化,就把那份文字记进历史;同一个文本 1200 ms 内只记一次,用来吃掉两个信号的重叠。
Enter 发送和用主按钮发送都能记到,但手动清空输入框(全选删除)如果恰好发生在点击卡片按钮后约 700 ms 内,可能被误记一条。代价仅是多一条历史。InputState.draft(剪贴板投影),所以 @文件、技能引用这类 chip 翻回来后是纯文本,不再是引用芯片。npm run check # 对两个半边做 node --check
npm test # 历史状态机的 Node 单元测试
lib/client.js 里的历史状态机(createHistory / recordSent / recallUp / recallDown)刻意做成不依赖 React 与 DOM 的纯函数,测试直接驱动它们;__internals 只为此暴露,Host、Loader 和浏览器内核都不读这个键。
浏览器 bundle 是自包含的单文件(window.__ModuleLoader__.load(...) + 工厂函数):客户端模块系统只解析平台种子表、已 materialize 的模块、boot graph 行和已注册工厂,跨文件的相对 require 解析不到。
MIT —— 见 LICENSE。