输入建议 / Prompt Suggestions
包名
dsh-composer-suggestions(GitHub 仓库Heeweelee/dsh-prompt-suggestions)· DSH 插件 · MIT · v1.1.0
English summary. A DSH composer plugin. ↑ / ↓ recall every prompt you have sent in the current session — including ones you accepted from a prediction, not only what you typed — Ctrl+↑ opens a searchable history picker, grey ghost text previews your next message, and → accepts it as plain, editable text. It never auto-sends: every key only edits the draft, and sending still takes your own Enter.
给 DSH 输入框加上「历史回滚 + 下一句预测」,像 Claude Desktop 那样不用重复打字。
DSH 的输入框只有当前这一句;这个插件把两件事补上:往回翻——把你在本会话里发过的提问一键翻回来重新编辑;往前猜——根据刚才的对话,把你的下一句话以灰色幽灵文字写在输入框里,按一下 → 就能用。
功能一览
| 你能得到什么 | 怎么用 |
|---|---|
| 不再重打旧提问:本会话发过的每一条都能翻回来,长会话也能一直翻到第一句 | 空输入框按 ↑,按一次退一条 |
| 翻过头了能回来,草稿不会丢 | ↓ 逐条前进;越过最新一条时恢复你原本没发出去的草稿 |
| 历史太多时直接搜:面板里按关键词过滤,独立输入框,不打断正文 | Ctrl+↑ 打开面板,输入关键词,↑↓ 选择,Enter 填入 |
| 下一句不用从头想:模型读最近几轮对话,给出你大概率要发的那句 | 空闲时自动出现在输入框里,灰色 |
| 一键采纳,仍可编辑:采纳后就是普通文本,改完再发 | → 采纳 |
| 绝不自动发送:任何按键都只改草稿,发不发、什么时候发由你决定 | — |
回滚覆盖整场会话:手打、粘贴、→ 采纳预测后发送、排队插话发送,全都算「你发过的」 |
判据是会话记录里的 source.kind = user,运行时注入(时间上下文、技能目录等)自动排除 |
| 不劫持键盘:多行编辑、光标在中间、输入法组字时方向键照常工作 | 只在草稿为空 / 单行草稿 / 光标停在开头时接管 ↑ |
| 按键 | 行为 |
|---|---|
| ↑ | 把本会话上一条用户消息回填到输入框(再按继续往前) |
| ↓ | 回到下一条;越过最新一条时恢复你未发送的草稿 |
| Ctrl+↑ | 打开本会话历史提问面板:可输入关键词筛选、↑↓ 选择、Enter 填入、Esc 关闭 |
| → | 采纳灰色幽灵文字,变成普通可编辑文本 |
永远不会自动发送:以上按键只改草稿,发送仍需你自己按 Enter。
安装
推荐从 GitHub 直接装(不需要 registry):
dsh plugin --profile desktop add github:Heeweelee/dsh-prompt-suggestions
其他方式详见 INSTALL.md:
# A. 从 npm 装
plugin_manager install_bundle dsh-composer-suggestions
# B. 用 Releases 里的 tarball
# 在 profile 的 package.json 里加 "dsh-composer-suggestions": "file:D:/path/dsh-composer-suggestions-1.1.0.tgz"
# C. 本地开发目录
plugin_manager install_bundle D:\Ai_Tool\dsh_tool\packages\prompt-suggestions
安装后在「插件 → 已安装插件」里可开关。
两半结构
| 文件 | 平台 | 作用 |
|---|---|---|
index.js |
Host | 在 DSH Web 服务器上注册 POST /prompt-suggestions/suggest(预测下一条)与 POST /prompt-suggestions/history(本会话全部用户提问) |
client.js |
Web Client | 注册 conversation.input.overlay 槽位:拦截方向键、回溯历史、绘制幽灵文字 |
历史回填的实现
数据源有两层,Host 优先:
- Host 权威列表(
POST /prompt-suggestions/history→{prompts:[{text,time}]}):直接读该 Session 的完整事件日志,取所有user/message且source.kind === 'user'的消息,按时间正序返回最近 200 条,每条保留完整原文(含换行,上限 4000 字符)。这样不受聊天窗口分页限制,也不受 compaction 把早期轮次从 surface 剪掉的影响 —— 长会话里↑能一直翻到第一句。 - 本地窗口(
useChat的order+nodes)作为兜底:Host 路由不可达时仍然可用;并把 Host 列表之后新产生、尚未被拉到的那条消息补在末尾。
source.kind 是区分「人发的」与「机器塞的」的唯一依据:运行时注入(时间上下文、技能目录、压缩存档、子任务通知)也是 user 角色消息,必须排除。凡是成功提交的提问 —— 手打、粘贴、从插件预测 → 采纳后发送、排队/插话发送 —— 在会话里都是同一条 user/message(kind: "user"),因此全部覆盖。
回填走 inputActions.setDraft(text),不做提交。
↑ 的拦截条件:草稿为空,或单行草稿(其内容会被保存,↓ 可恢复),或多行草稿但光标停在最开头 —— 因此多行编辑时光标移动不受影响。
历史面板(Ctrl+↑)
不需要 DOM 剪贴或额外数据源:直接复用同一份 chat 投影,按时间正序渲染,打开时选中最新一条并滚动到底部。面板是 position: fixed 的浮层,锚在输入卡片正上方(left/width 取自卡片矩形,max-height 取卡片上方空间,避免压住输入框)。
面板打开时键盘交给面板自己的 <input>:筛选框是真实输入框,因此 Enter / ↑↓ / Esc 都在面板内部消费,不与宿主输入框抢事件;卡片上的捕获监听同时被短路(面板渲染在槽位内,事件仍会冒泡到卡片)。任何外部 mousedown 关闭面板并把焦点交还编辑器。填入走 inputActions.setDraft,同样不提交。
幽灵文字的两种绘制方式
都只在 Session 空闲(未在生成)且草稿为空白 / 是预测前缀时显示:
- 草稿为空 — 在
conversation.input.overlay里绝对定位一层灰色文本,位置对齐宿主占位符所在的盒子,字体行高从编辑器getComputedStyle复制,底色用--dsw-specific-input-major盖住占位符。 - 草稿是预测的前缀 — 写入宿主自带的行内提示通道:把 remainder 写进
[data-composer-card]的--dsh-composer-hint变量,宿主的 CSSp:last-child::after { content: var(--dsh-composer-hint) }会把半透明灰色文字接在正文后面,因此换行与字距与真实输入完全一致;卸载时移除该变量。
→ 采纳:草稿为空时 setDraft(prediction),草稿是前缀时 insertText(remainder, caretSpan)(走宿主自己的撤销栈)。
预测请求
POST /prompt-suggestions/suggest
x-prompt-suggestions: 1
{"sessionId": "session-..."} → {"suggestion": "...", "cached": false}
- 上下文取自
session.deriveMessages()(与模型看到的历史一致),取最后 10 条,单条截断 700 字符,总长 4000 字符,以 JSON 框架嵌入提示词。 - 模型路由取
ctx.agentDefaultModel.currentSelection(),reasoningEffort强制降到该路由最省的一档(本机为off),maxTokens: 700。 - 5 分钟按
sessionId + 消息数缓存,避免刷新页面重复计费。 - 传输层用
document.baseURI解析 URL,不假设http(s):桌面版把界面跑在自定义协议dsh-app://app/上(渲染进程带--standard-schemes=dsh-app --fetch-schemes=dsh-app),主进程把该 scheme 下所有非静态资源路径连同 Host Cookie 转发给真正的 Host 服务。用location.origin判断 scheme 会拿到"null",桌面版就永远发不出请求。 - 防空话重试:如果模型只回了「继续 / 好的 / ok / 继续吧」之类的空话,会带上更严格的提示词再要一次具体建议;两次都是空话则不显示幽灵文字(宁可不猜,也不糊你一脸「继续」)。
- 失败重试:请求失败(Host 路由尚未注册、刷新撞上启动)按 0.6s / 1.2s / 2.4s 退避重试,四次都失败才降级到本地规则;降级后窗口重新获得焦点或可见性恢复时会再试一次——一次启动竞态不会锁死整轮。
- 调试:加
x-prompt-suggestions-debug: 1返回debug字段(阶段、attempts、filler、原始输出、finish 原因、实际路由)。调试请求绕过缓存读(但仍写回),便于不换会话就拿到一次真实结果。
预测来源是可见的
角标同时是溯源标记:
| 角标 | 含义 |
|---|---|
→ 采纳 |
模型预测(悬停显示「模型预测 · 1832ms」) |
→ 采纳 · 本地 |
服务端没响应,用的是本地规则 |
预测不可用 |
服务端没响应且本地规则也给不出有意义的建议(此时不显示幽灵文字,悬停会显示失败原因,如 HTTP 404 / 网络错误 …) |
DevTools Console 每轮也会有一行 [prompt-suggestions] model 1832ms <文本>,或 local fallback <ms> 加上失败原因和实际请求的 URL。
已验证(真实浏览器 + 真实模型)
在 http://127.0.0.1:54796 上用独立 Chromium 通过 CDP 实测(打开真实历史会话「可拆分相机3D模型网页」):
- 模型链路:幽灵文字
给每个零件加上编号,展开时按装配顺序逐个演示拆解动画,与POST /prompt-suggestions/suggest的响应体逐字一致,cached:false,角标→ 采纳、title="模型预测 · 2267ms"、data-ca-source="model",Console 一行[prompt-suggestions] model 2267ms … - 启动竞态恢复:加载时把该路由阻断 2.5s(
blocked: 2,前两次请求被拦),解除后第三次请求成功,最终仍是model+ 幽灵文字,没有降级 - 服务端一直不可达(阻断且不放行):角标
预测不可用、data-ca-source="local",不出幽灵文字 - 防空话重试:把首次提示词临时改成「只回答继续」,实测
attempts: 2、filler: false,最终给出的仍是具体建议 - 空会话(新会话首页):路由返回空建议,不出幽灵文字、不显示任何角标
↑一次即回填上一条提问;↓越过最新一条恢复原草稿,幽灵文字随之恢复- 采纳:
→后草稿内容与预测文本逐字相同,全程没有任何消息被提交 - 前缀:先输入预测的前 3 个字,宿主的行内
::after显示剩余部分,→后草稿等于完整预测 Ctrl+↑面板:2/2 条、选中最新一条、筛选「继续」后 1/2、Enter 填入后草稿等于该条、Esc 关闭且草稿不变、筛选输入不影响草稿- 全量回滚实测(1.1.0):在当前这个长会话(547 条消息、已被压缩过、聊天窗口只加载最后一页)里,
Ctrl+↑面板显示9/9,连按 9 次↑依次回填到1/9,第一条是完整的多行原文(187 字符,未截断成第一行);POST /prompt-suggestions/history返回同样的 9 条。同一版本在 DFM 会话上16/16,预测链路同时复测为model+→ 采纳(模型预测 · 20ms),说明两条功能没有互相影响。 - 无页面异常;插件页显示「输入建议」标题、中文描述与自绘图标
- 打包产物实测(1.0.0 tarball):
plugin_manager install_bundle file:…/dsh-prompt-suggestions-1.0.0.tgz→ pnpm+1 package、bundle 热应用;POST /prompt-suggestions/suggest返回 200 且cached:false;浏览器里幽灵文字与角标正常。另在干净目录npm install <tgz>后import('dsh-prompt-suggestions')得到inject = ["sessions","llm","agentDefaultModel","webServer"]与apply函数,说明包结构(exports/dsh.bundle.patch/files)正确。 - 桌面壳(
dsh-app://app/)走的转发链路由主进程实现:protocol.handle把非静态路径连同 Host Cookie 转发给本地服务,只接受Origin为空或dsh-app://app的请求——这也是之前桌面版拿不到预测的原因(旧版用location.origin判断,dsh-app的 origin 是"null")。
已知限制
- 预测只在 Session 空闲时请求;正在生成时没有幽灵文字(避免打断)。
- 模型如果两次都只给空话,就不显示幽灵文字——「有时没预测」是设计上可接受的失败方式。
- 空会话(还没有任何消息)不会预测:没有上下文可依据。
- 界面语言在激活时解析:优先取 Client locale 服务,服务尚未注册时退回
navigator.languages,最后才是<html lang>(后者在启动早期仍是文档的静态值,不能作为首选)。 - 面板不做时间分组、不做跨会话搜索;只列当前会话的提问。
- 幽灵文字的角标位置按编辑器矩形计算,若宿主改动输入框内边距,需要重新对齐。
- 历史来自 Session 的完整事件日志(不受分页与 compaction 影响),但 Host 侧只保留最近 200 条、每条 4000 字符;更早的提问仍在会话里,只是不再进入回滚列表。
- Host 路由不可达时会自动降级为「当前已加载窗口」,此时长会话里能翻到的深度取决于聊天视图加载了多少。
/prompt-suggestions/suggest与/prompt-suggestions/history走 DSH Web 服务器,未经过浏览器 Cookie 认证,仅靠自定义请求头 + 监听回环地址保护;跨站请求无法带上该请求头。- 面板用
position: fixed定位;若宿主将来给输入卡片的祖先加上transform,浮层会改为相对该祖先定位,需要改用 portal。
No comments yet. Be the first to write one.