dsh-next-prompt 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -0
- package/README.en.md +5 -4
- package/README.md +5 -4
- package/docs/development.md +3 -3
- package/docs/similar-plugins.md +1 -1
- package/lib/client.js +89 -21
- package/lib/core.js +23 -8
- package/lib/index.js +12 -6
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| 版本 | 主要变化 |
|
|
4
4
|
| --- | --- |
|
|
5
|
+
| 0.6.0 | `↑` 不再只能填上一条:接着按 `↑` 往更早翻、按 `↓` 往回翻,在最近一条上按 `↓` 清空;改过翻出的内容后 `↑` / `↓` 恢复成移动光标。相邻重复发送的消息只算一条 |
|
|
5
6
|
| 0.5.2 | 适配 DSH 0.2.1 的插件热更新:卸载时等提示词缓存写完再让新实例加载;缓存的临时文件名不再只带进程号,避免新旧实例同时写盘失败;界面半更新后替换页面里旧版本的样式 |
|
|
6
7
|
| 0.5.1 | 功能不变;改由 GitHub Actions 自动发布,npm 页面会显示来源证明(provenance),可核对安装包确实由本仓库构建 |
|
|
7
8
|
| 0.5.0 | 改名为 `dsh-next-prompt`(原 `dsh-prompt-prefill`),避免和模型的「回答预填(prefill)」混淆;插件 id、RPC 路径、日志前缀随之改名。新增:关掉 DSH 再打开、切回会话后还原上次的灰字:后台把每个会话最近一条提示词存到 DSH 主目录,之后没有新对话时还原,有新对话、新回合开始或按 Escape 关掉时丢弃;新增配置 `persistSuggestions` |
|
package/README.en.md
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
A DeepSeek Harness (DSH) desktop plugin that adds two small features to the input box:
|
|
9
9
|
|
|
10
10
|
- **→ / Tab: accept a suggested next prompt.** When the agent finishes replying, a light gray suggestion appears in the empty input box, guessing what you are likely to say next based on the recent conversation. Press `Tab` or `→` to put it into your draft.
|
|
11
|
-
-
|
|
11
|
+
- **↑ / ↓: browse what you sent.** With the input box empty, press `↑` to fill in the last message you sent in this session; keep pressing `↑` to go further back and `↓` to come forward again.
|
|
12
12
|
|
|
13
13
|
Both only fill the input box. **Nothing is ever sent automatically.**
|
|
14
14
|
|
|
15
15
|
> This plugin was called `dsh-prompt-prefill` before 0.5.0. If you installed the old one, run `dsh plugin --profile desktop remove dsh-prompt-prefill` first, then install as below.
|
|
16
16
|
|
|
17
|
-
Tested on DSH desktop **0.2.1-rc.2** and **0.2.0-rc.2** (other versions are untested).
|
|
17
|
+
Tested on DSH desktop **0.2.1-rc.3**, **0.2.1-rc.2** and **0.2.0-rc.2** (other versions are untested).
|
|
18
18
|
|
|
19
19
|
## Install
|
|
20
20
|
|
|
@@ -63,8 +63,9 @@ Only the text of the last few messages is sent to the model. Common key and toke
|
|
|
63
63
|
## Notes
|
|
64
64
|
|
|
65
65
|
- The gray text is an overlay aligned with the input box; very long suggestions may be cut off visually, but accepting still inserts the full text.
|
|
66
|
-
- `↑`
|
|
67
|
-
-
|
|
66
|
+
- Once you edit a recalled message, `↑` / `↓` move the cursor again; in a multi-line message the caret has to reach the start (`↑`) or end (`↓`) before it moves on. Pressing `↓` on the newest message clears the input box.
|
|
67
|
+
- Text only; image attachments are not restored.
|
|
68
|
+
- Do not install alongside other plugins that also intercept `Tab` / `→` / `↑` / `↓` (see [similar plugins](docs/similar-plugins.md)).
|
|
68
69
|
|
|
69
70
|
## More docs (Chinese)
|
|
70
71
|
|
package/README.md
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
DeepSeek Harness(DSH)桌面端插件,给输入框加两个小功能:
|
|
9
9
|
|
|
10
10
|
- **→ / Tab:采纳建议的下一句。** Agent 回答完后,输入框里出现一条浅灰色的提示词,是根据最近的对话猜你接下来想说的话。按 `Tab` 或 `→` 填进草稿。
|
|
11
|
-
-
|
|
11
|
+
- **↑ / ↓:翻看发送过的内容。** 输入框为空时按 `↑`,填入这个会话里你上一次发出的消息;接着按 `↑` 往更早翻,按 `↓` 往回翻。
|
|
12
12
|
|
|
13
13
|
两者都**只填进输入框,不会自动发送**。
|
|
14
14
|
|
|
15
15
|
> 本插件原名 `dsh-prompt-prefill`,0.5.0 起改名为 `dsh-next-prompt`。装过旧版的话,先 `dsh plugin --profile desktop remove dsh-prompt-prefill`,再按下面的方法安装新版。
|
|
16
16
|
|
|
17
|
-
适用版本:DSH 桌面端 **0.2.1-rc.2**、**0.2.0-rc.2**(其他版本未验证)。
|
|
17
|
+
适用版本:DSH 桌面端 **0.2.1-rc.3**、**0.2.1-rc.2**、**0.2.0-rc.2**(其他版本未验证)。
|
|
18
18
|
|
|
19
19
|
## 安装
|
|
20
20
|
|
|
@@ -63,8 +63,9 @@ DSH 0.2.1 起,装完不用重启,DSH 开着时直接运行命令就行,插
|
|
|
63
63
|
## 注意事项
|
|
64
64
|
|
|
65
65
|
- 灰字是对齐在输入框上的浮层,候选很长时可能显示不全,采纳后仍是完整内容。
|
|
66
|
-
-
|
|
67
|
-
-
|
|
66
|
+
- 翻出的内容一旦改动,`↑` / `↓` 就恢复成移动光标;多行内容要等光标移到开头(`↑`)或末尾(`↓`)才接着翻。在最近一条上按 `↓` 会清空输入框。
|
|
67
|
+
- 只恢复文字,不恢复图片附件。
|
|
68
|
+
- 不要和其他同样拦截 `Tab` / `→` / `↑` / `↓` 的插件同时安装,见[同类插件](docs/similar-plugins.md)。
|
|
68
69
|
|
|
69
70
|
## 更多文档
|
|
70
71
|
|
package/docs/development.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
**重开后还原灰字**:模型生成成功后,后台把提示词连同当时最后一条对话消息的指纹(`historyFingerprint`,忽略切换模型、压缩上下文等 DSH 插入的消息)写进 `<DSH 主目录>/plugin-data/dsh-next-prompt/suggestions.json`(主目录取 `profileContext.home`,退回 `$DSH_HOME` 或 `~/.dsh`;最多 200 个会话,整文件原子替换)。界面第一次挂载某个会话、内存里又没有提示词时问一次(`method: 'cached'`),指纹对得上才还原,对不上就删掉。重开后会话要等用户在里面发消息才会加载(`sessions.get()` 取不到),这时没法核对,直接信任缓存,因为插件运行期间每次 `turn/start` 都会删掉缓存;会话已加载却读不到历史时回 `NOT_READY`,界面每 1.5 秒再问,最多 4 次。`turn/start` 和 Escape(`method: 'dismiss'`)也会删掉。关掉 `persistSuggestions` 则完全不读写磁盘。
|
|
20
20
|
|
|
21
|
-
**↑ 的流程**:界面拦截空输入框上的
|
|
21
|
+
**↑ / ↓ 的流程**:界面拦截空输入框上的 `↑`,向后台要这个会话最近一条真人消息(`method: 'lastSent'`,`index: 0`),拿到后 `setDraft()` 并记下翻到第几条。输入框里还是这段原文(光标在开头 / 末尾,单行不看光标)时,`↑` 要 `index + 1`、`↓` 要 `index - 1`,在第 0 条上按 `↓` 清空输入框;翻过最早一条时后台回 `END_OF_HISTORY`,界面不动。后台列历史时相邻重复的消息只算一条。等待期间输入框变了或切换了会话,结果就丢弃。读不到选区(`window.getSelection`)时多行原文不拦截,留给光标换行。
|
|
22
22
|
|
|
23
23
|
**接口的安全边界**:只接受 POST;TCP 对端和 `Host` 都必须是本机回环地址;带 `Origin` 时必须与连接精确同源;`Sec-Fetch-Site: cross-site` 一律拒绝。桌面端转发请求时会去掉 `Origin`,所以没有 `Origin` 时放行。插件没有做用户级鉴权。
|
|
24
24
|
|
|
@@ -50,7 +50,7 @@ dsh-next-prompt/
|
|
|
50
50
|
│ ├── core.js 纯逻辑:配置归一化、脱敏、取最近对话、判断上一轮是否完成、清洗模型输出
|
|
51
51
|
│ ├── index.js 后台:回合跟踪、RPC(suggestion / lastSent / cached / dismiss / diagnostics)、调用模型、诊断记录
|
|
52
52
|
│ ├── cache.js 后台:提示词的磁盘缓存(重开 DSH 后还原灰字)
|
|
53
|
-
│ └── client.js 界面:灰字浮层、Tab / → / ↑ / Escape、会话状态、样式注入
|
|
53
|
+
│ └── client.js 界面:灰字浮层、Tab / → / ↑ / ↓ / Escape、会话状态、样式注入
|
|
54
54
|
└── test/
|
|
55
55
|
├── core.test.mjs 纯逻辑
|
|
56
56
|
├── host.test.mjs 后台:路由与安全校验、回合事件、模型失败与重试、诊断
|
|
@@ -84,7 +84,7 @@ git push --follow-tags
|
|
|
84
84
|
7. 切到别的会话再切回:之前的灰字还在。
|
|
85
85
|
8. 回答进行中:不出现灰字。
|
|
86
86
|
9. 回答出错或中途停止:不出现灰字。
|
|
87
|
-
10. 输入框为空时按
|
|
87
|
+
10. 输入框为空时按 `↑`:填入上一次发送的内容;再按 `↑` 填入更早一条,按 `↓` 往回,在最近一条上按 `↓` 清空。自己打的字或改过的内容上按 `↑` / `↓`,光标正常移动。翻出一条多行消息时,光标在中间行按 `↑` 只移动光标,到第一行开头后再按才翻到更早一条。
|
|
88
88
|
11. 有灰字时完全退出 DSH 再打开,切回这个会话:灰字还在,且诊断里没有新的生成记录;按 `Escape` 关掉后再重开,不再出现。
|
|
89
89
|
|
|
90
90
|
DSH 0.2.1 起桌面端默认开启插件热更新:`dsh plugin` 安装、删除或更新后,后台半不用重启就会换成新代码(旧实例先卸载,新实例再加载,两者在同一个进程里)。界面半在浏览器页面里另有一套加载方式;0.2.1-rc.2 Windows 真机上不刷新窗口也生效了。改动后再过一遍:
|
package/docs/similar-plugins.md
CHANGED
|
@@ -14,4 +14,4 @@
|
|
|
14
14
|
| [converk/dsh-tweaks · prompt-history](https://github.com/converk/dsh-tweaks/tree/main/plugins/prompt-history) | 空输入框 ↑ / ↓ 连续翻历史,显示位置角标 |
|
|
15
15
|
| [PerryLink/dsh-composer-history](https://github.com/PerryLink/dsh-composer-history) | 终端风格的输入历史,支持 Ctrl+R 搜索 |
|
|
16
16
|
|
|
17
|
-
它们同样会拦截 `Tab` / `→` /
|
|
17
|
+
它们同样会拦截 `Tab` / `→` / `↑` / `↓`,**不要与本插件同时安装**。本插件的特点是把「下一句建议」和「历史回填」放在一个小插件里,并带诊断记录,方便排查。
|
package/lib/client.js
CHANGED
|
@@ -152,6 +152,11 @@ window.__ModuleLoader__.load({
|
|
|
152
152
|
* (表现为:刚出现的候选被立刻抹掉)。因此用计数决定真正清理的时机。
|
|
153
153
|
*/
|
|
154
154
|
instances: 0,
|
|
155
|
+
/**
|
|
156
|
+
* 按 ↑ / ↓ 翻到的历史:`{ index, text }`,index 为 0 是最近发送的一条。
|
|
157
|
+
* 只有输入框里还是这段原文时才接着翻;改过、清空或发送后视为重新开始。
|
|
158
|
+
*/
|
|
159
|
+
recall: undefined,
|
|
155
160
|
/** 交给 Harness 原生内联建议的 id;未使用原生接口时为 undefined。 */
|
|
156
161
|
suggestionId: undefined,
|
|
157
162
|
/** 上一次观察到的原生建议 id,用来区分「被 Escape 关闭」与「被采纳」。 */
|
|
@@ -392,31 +397,35 @@ window.__ModuleLoader__.load({
|
|
|
392
397
|
}
|
|
393
398
|
|
|
394
399
|
/**
|
|
395
|
-
* 按 ↑
|
|
400
|
+
* 按 ↑ / ↓ 时,向宿主半要这个会话发送过的第 `index` 条内容(0 是最近一条)并写入草稿。
|
|
396
401
|
*
|
|
397
402
|
* 历史只存在宿主侧的会话记录里(InputActions / InputState 都不提供),
|
|
398
|
-
* 所以这里走一次本机 RPC
|
|
399
|
-
*
|
|
403
|
+
* 所以这里走一次本机 RPC;等待期间用户改了输入框、切换了会话、
|
|
404
|
+
* 输入被锁或组件已卸载时,一律不覆盖。已经翻到最早一条时什么也不做。
|
|
400
405
|
*
|
|
401
406
|
* @param sessionId - 按键时的会话 id。
|
|
402
407
|
* @param store - 该会话的状态对象(用 `recalling` 防止连按重复请求)。
|
|
403
408
|
* @param interactionRef - 组件最新的草稿、锁定状态与 inputActions。
|
|
409
|
+
* @param index - 要第几条。
|
|
410
|
+
* @param draft - 按键时的草稿;回来时草稿变了就不覆盖。
|
|
404
411
|
*/
|
|
405
|
-
function
|
|
412
|
+
function recallSent(sessionId, store, interactionRef, index, draft) {
|
|
406
413
|
if (store.recalling) return
|
|
407
414
|
store.recalling = true
|
|
408
|
-
const warn = (message) => log('warn',
|
|
409
|
-
rpc({ method: 'lastSent', sessionId })
|
|
415
|
+
const warn = (message) => log('warn', `没有取到发送过的内容:${message}`)
|
|
416
|
+
rpc({ method: 'lastSent', sessionId, index })
|
|
410
417
|
.then((payload) => {
|
|
418
|
+
if (payload?.code === 'END_OF_HISTORY') return
|
|
411
419
|
if (!payload || payload.ok !== true || typeof payload.text !== 'string' || payload.text === '') {
|
|
412
420
|
warn(typeof payload?.message === 'string' ? payload.message : '会话里没有可用的消息')
|
|
413
421
|
return
|
|
414
422
|
}
|
|
415
423
|
const current = interactionRef.current
|
|
416
424
|
if (store.instances === 0 || !current || current.sessionId !== sessionId) return
|
|
417
|
-
if (current.draft !==
|
|
425
|
+
if (current.draft !== draft || current.locked || current.removed) return
|
|
418
426
|
if (typeof current.actions?.setDraft !== 'function') return
|
|
419
427
|
current.actions.setDraft(payload.text)
|
|
428
|
+
store.recall = { index, text: payload.text }
|
|
420
429
|
})
|
|
421
430
|
.catch((error) => warn(String(error?.message ?? error)))
|
|
422
431
|
.finally(() => {
|
|
@@ -424,6 +433,51 @@ window.__ModuleLoader__.load({
|
|
|
424
433
|
})
|
|
425
434
|
}
|
|
426
435
|
|
|
436
|
+
/** 比较输入框文字与翻出的原文:忽略换行符差异和首尾空白(编辑器可能补一个换行)。 */
|
|
437
|
+
function sameText(a, b) {
|
|
438
|
+
const normalize = (text) => String(text).replace(/\r\n?/g, '\n').trim()
|
|
439
|
+
return normalize(a) === normalize(b)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* 光标是否在输入框文字的开头(`edge` 为 'start')或结尾('end')。
|
|
444
|
+
*
|
|
445
|
+
* 多行内容里 ↑ / ↓ 要留给光标换行,只有光标已经到头时才翻历史;
|
|
446
|
+
* 单行内容不用看光标。读不到选区时保守地返回 false,把按键交还给输入框。
|
|
447
|
+
*/
|
|
448
|
+
function caretAtEdge(input, text, edge) {
|
|
449
|
+
if (!text.includes('\n')) return true
|
|
450
|
+
try {
|
|
451
|
+
const selection = typeof window.getSelection === 'function' ? window.getSelection() : null
|
|
452
|
+
if (!selection || selection.rangeCount === 0 || !selection.isCollapsed) return false
|
|
453
|
+
const caret = selection.getRangeAt(0)
|
|
454
|
+
const range = document.createRange()
|
|
455
|
+
range.selectNodeContents(input)
|
|
456
|
+
if (edge === 'start') range.setEnd(caret.startContainer, caret.startOffset)
|
|
457
|
+
else range.setStart(caret.startContainer, caret.startOffset)
|
|
458
|
+
return range.toString().trim() === ''
|
|
459
|
+
} catch {
|
|
460
|
+
return false
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* 算出这次 ↑ / ↓ 要翻到第几条;不该拦截时返回 undefined。
|
|
466
|
+
*
|
|
467
|
+
* - 输入框为空:↑ 取最近一条,↓ 不拦截。
|
|
468
|
+
* - 输入框里还是上次翻出的原文、光标在头(↑)或尾(↓):↑ 往更早翻,↓ 往回翻;
|
|
469
|
+
* 在最近一条上按 ↓ 返回 -1,表示清空输入框。
|
|
470
|
+
* - 其他情况(自己打的字、改过的历史)一律交给光标移动。
|
|
471
|
+
*/
|
|
472
|
+
function historyStep(key, current, input, store) {
|
|
473
|
+
const text = domTextOf(input)
|
|
474
|
+
if (current.draft === '' && text.trim() === '') return key === 'ArrowUp' ? 0 : undefined
|
|
475
|
+
const recall = store.recall
|
|
476
|
+
if (!recall || !sameText(current.draft, recall.text) || !sameText(text, recall.text)) return undefined
|
|
477
|
+
if (!caretAtEdge(input, text, key === 'ArrowUp' ? 'start' : 'end')) return undefined
|
|
478
|
+
return key === 'ArrowUp' ? recall.index + 1 : recall.index - 1
|
|
479
|
+
}
|
|
480
|
+
|
|
427
481
|
/**
|
|
428
482
|
* 把幽灵浮层对齐到 textarea 的内容区。
|
|
429
483
|
*
|
|
@@ -819,17 +873,18 @@ window.__ModuleLoader__.load({
|
|
|
819
873
|
// (光标移动、Tab 切换焦点)执行。按键时的状态一律从 interactionRef 读最新值。
|
|
820
874
|
//
|
|
821
875
|
// - Tab / →:幽灵文本可见时采纳(与 Claude Code、Harness 原生建议一致)。
|
|
822
|
-
// -
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
//
|
|
876
|
+
// - ↑ / ↓:翻看这个会话发送过的内容。输入框为空时 ↑ 填入最近一条;输入框里还是
|
|
877
|
+
// 翻出的原文时,↑ 往更早翻、↓ 往回翻,在最近一条上按 ↓ 清空。见 historyStep。
|
|
878
|
+
// 与幽灵文本是否显示无关,也不受原生建议接口影响;Agent 正在回答时也可用
|
|
879
|
+
// (Harness 支持回答中排队发送下一条)。DSH 自己只在 `/`、`@` 菜单弹出时使用
|
|
880
|
+
// ↑ / ↓,而菜单弹出时输入框里是 `/`、`@` 开头的字,不会等于翻出的原文。
|
|
826
881
|
// - Escape:关闭幽灵文本。
|
|
827
882
|
react.useEffect(() => {
|
|
828
883
|
if (sessionId === undefined) return
|
|
829
884
|
|
|
830
885
|
const onKeyDown = (event) => {
|
|
831
886
|
const { key } = event
|
|
832
|
-
if (key !== 'ArrowRight' && key !== 'Tab' && key !== 'ArrowUp' && key !== 'Escape') return
|
|
887
|
+
if (key !== 'ArrowRight' && key !== 'Tab' && key !== 'ArrowUp' && key !== 'ArrowDown' && key !== 'Escape') return
|
|
833
888
|
const current = interactionRef.current
|
|
834
889
|
if (!current) return
|
|
835
890
|
|
|
@@ -844,15 +899,33 @@ window.__ModuleLoader__.load({
|
|
|
844
899
|
if (event.defaultPrevented || event.isComposing || event.repeat) return
|
|
845
900
|
// 带修饰键的 → / Tab / ↑ 是别的操作(选区移动、Shift+Tab 反向切换焦点等),不拦截。
|
|
846
901
|
if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return
|
|
847
|
-
if (current.locked
|
|
902
|
+
if (current.locked) return
|
|
848
903
|
if (typeof current.actions?.setDraft !== 'function') return
|
|
849
904
|
|
|
850
|
-
if (key === 'ArrowUp') {
|
|
905
|
+
if (key === 'ArrowUp' || key === 'ArrowDown') {
|
|
851
906
|
if (current.removed) return
|
|
852
|
-
|
|
853
|
-
if (
|
|
907
|
+
const input = focusedComposerInput(anchorRef.current)
|
|
908
|
+
if (input === null) return
|
|
909
|
+
const index = historyStep(key, current, input, store)
|
|
910
|
+
if (index === undefined) return
|
|
911
|
+
event.preventDefault()
|
|
912
|
+
event.stopPropagation()
|
|
913
|
+
if (index >= 0) {
|
|
914
|
+
recallSent(sessionId, store, interactionRef, index, current.draft)
|
|
915
|
+
return
|
|
916
|
+
}
|
|
917
|
+
store.recall = undefined
|
|
918
|
+
try {
|
|
919
|
+
current.actions.setDraft('')
|
|
920
|
+
} catch {
|
|
921
|
+
/* 清不掉就保留原文 */
|
|
922
|
+
}
|
|
923
|
+
return
|
|
854
924
|
}
|
|
855
925
|
|
|
926
|
+
if (current.draft !== '') return
|
|
927
|
+
if (!current.visible || current.busy || store.candidate !== current.candidate) return
|
|
928
|
+
|
|
856
929
|
const input = focusedComposerInput(anchorRef.current)
|
|
857
930
|
// 只有「输入框真的为空」时才拦截,避免抢走正常的行内光标移动。
|
|
858
931
|
// 输入框是 contentEditable div(没有 .value),所以必须用 domIsEmpty。
|
|
@@ -860,11 +933,6 @@ window.__ModuleLoader__.load({
|
|
|
860
933
|
|
|
861
934
|
event.preventDefault()
|
|
862
935
|
event.stopPropagation()
|
|
863
|
-
|
|
864
|
-
if (key === 'ArrowUp') {
|
|
865
|
-
recallLastSent(sessionId, store, interactionRef)
|
|
866
|
-
return
|
|
867
|
-
}
|
|
868
936
|
try {
|
|
869
937
|
current.actions.setDraft(current.candidate)
|
|
870
938
|
} catch {
|
package/lib/core.js
CHANGED
|
@@ -227,18 +227,22 @@ export function extractRecentTurns(items, config) {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
/**
|
|
230
|
-
*
|
|
230
|
+
* 列出用户在这个会话里发出过的消息文字,最新的在前。
|
|
231
231
|
*
|
|
232
|
-
* 只认真人输入(`source.kind === 'user'
|
|
232
|
+
* 只认真人输入(`source.kind === 'user'`),工具结果、系统注入不算;纯空白的跳过。
|
|
233
|
+
* 相邻两条完全相同的只留一条(与终端历史的 ignoredups 一样),连发几次同一句时
|
|
234
|
+
* 按 ↑ 不会原地踏步。
|
|
233
235
|
* 返回原文:这段文字只会回填到用户自己的输入框,不会发给模型,因此不脱敏、不截断。
|
|
234
236
|
* 两种形状都接受,见 extractRecentTurns。
|
|
235
237
|
*
|
|
236
238
|
* @param items - Message 数组或 SessionEvent 数组。
|
|
237
|
-
* @
|
|
239
|
+
* @param limit - 最多返回几条。
|
|
240
|
+
* @returns 用户消息文字的数组,最新的在前;没有时为空数组。
|
|
238
241
|
*/
|
|
239
|
-
export function
|
|
240
|
-
|
|
241
|
-
|
|
242
|
+
export function userTexts(items, limit = Infinity) {
|
|
243
|
+
const texts = []
|
|
244
|
+
if (!Array.isArray(items)) return texts
|
|
245
|
+
for (let index = items.length - 1; index >= 0 && texts.length < limit; index -= 1) {
|
|
242
246
|
const item = items[index]
|
|
243
247
|
if (!item || typeof item !== 'object') continue
|
|
244
248
|
let payload
|
|
@@ -247,9 +251,20 @@ export function lastUserText(items) {
|
|
|
247
251
|
else continue
|
|
248
252
|
if (!payload || !payload.source || payload.source.kind !== 'user') continue
|
|
249
253
|
const text = messageText(payload)
|
|
250
|
-
if (text.trim()
|
|
254
|
+
if (text.trim() === '' || text === texts[texts.length - 1]) continue
|
|
255
|
+
texts.push(text)
|
|
251
256
|
}
|
|
252
|
-
return
|
|
257
|
+
return texts
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 找出用户在这个会话里最后发出的那条消息的文字,规则同 userTexts。
|
|
262
|
+
*
|
|
263
|
+
* @param items - Message 数组或 SessionEvent 数组。
|
|
264
|
+
* @returns 最后一条用户消息的文字;没有时返回 undefined。
|
|
265
|
+
*/
|
|
266
|
+
export function lastUserText(items) {
|
|
267
|
+
return userTexts(items, 1)[0]
|
|
253
268
|
}
|
|
254
269
|
|
|
255
270
|
/**
|
package/lib/index.js
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
extractRecentTurns,
|
|
23
23
|
historyFingerprint,
|
|
24
24
|
lastTurnState,
|
|
25
|
-
|
|
25
|
+
userTexts,
|
|
26
26
|
parseCandidate,
|
|
27
27
|
pickFallback,
|
|
28
28
|
resolveConfig,
|
|
@@ -878,12 +878,18 @@ function registerRoute(ctx, config) {
|
|
|
878
878
|
return
|
|
879
879
|
}
|
|
880
880
|
|
|
881
|
-
// 按 ↑
|
|
881
|
+
// 按 ↑ / ↓ 翻看发送过的内容:只读会话历史,不调用模型。
|
|
882
|
+
// index 为 0 是最近一条,越大越早;不传时取最近一条。
|
|
882
883
|
if (body.method === 'lastSent') {
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
884
|
+
const index = Number.isSafeInteger(body.index) && body.index >= 0 ? body.index : 0
|
|
885
|
+
const texts = userTexts(sessionHistory(ctx, sessionId), index + 1)
|
|
886
|
+
if (texts.length === 0) {
|
|
887
|
+
json(response, 200, { ok: false, code: 'NO_HISTORY', message: '这个会话还没有发送过消息' })
|
|
888
|
+
} else if (index >= texts.length) {
|
|
889
|
+
json(response, 200, { ok: false, code: 'END_OF_HISTORY', message: '已经是最早的一条' })
|
|
890
|
+
} else {
|
|
891
|
+
json(response, 200, { ok: true, text: texts[index], index })
|
|
892
|
+
}
|
|
887
893
|
return
|
|
888
894
|
}
|
|
889
895
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-next-prompt",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DeepSeek Harness 插件:Agent 回答结束后在输入框预填一条灰色的下一句建议,按 Tab 或 → 采纳;输入框为空时按 ↑
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "DeepSeek Harness 插件:Agent 回答结束后在输入框预填一条灰色的下一句建议,按 Tab 或 → 采纳;输入框为空时按 ↑ / ↓ 翻看发送过的内容。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|