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 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
- - **↑: recall your last message.** With the input box empty, press `↑` to fill in the last message you sent in this session.
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
- - `↑` only recalls the most recent message, text only (no image attachments).
67
- - Do not install alongside other plugins that also intercept `Tab` / `→` / `↑` (see [similar plugins](docs/similar-plugins.md)).
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
- - 不要和其他同样拦截 `Tab` / `→` / `↑` 的插件同时安装,见[同类插件](docs/similar-plugins.md)。
66
+ - 翻出的内容一旦改动,`↑` / `↓` 就恢复成移动光标;多行内容要等光标移到开头(`↑`)或末尾(`↓`)才接着翻。在最近一条上按 `↓` 会清空输入框。
67
+ - 只恢复文字,不恢复图片附件。
68
+ - 不要和其他同样拦截 `Tab` / `→` / `↑` / `↓` 的插件同时安装,见[同类插件](docs/similar-plugins.md)。
68
69
 
69
70
  ## 更多文档
70
71
 
@@ -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
- **↑ 的流程**:界面拦截空输入框上的 `↑`,向后台要这个会话最后一条真人消息(`method: 'lastSent'`),拿到后 `setDraft()`。等待期间你开始打字或切换会话,结果就丢弃。
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 真机上不刷新窗口也生效了。改动后再过一遍:
@@ -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 recallLastSent(sessionId, store, interactionRef) {
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', `没有取到上一次发送的内容:${message}`)
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 !== '' || current.locked || current.removed) return
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
- // 建议接口影响;Agent 正在回答时也可用(Harness 支持回答中排队发送下一条)。
824
- // DSH 自己只在 `/`、`@` 菜单弹出时使用 ↑,而菜单弹出时输入框必然有字,
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 || current.draft !== '') return
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
- } else {
853
- if (!current.visible || current.busy || store.candidate !== current.candidate) return
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
- * @returns 最后一条用户消息的文字;没有时返回 undefined。
239
+ * @param limit - 最多返回几条。
240
+ * @returns 用户消息文字的数组,最新的在前;没有时为空数组。
238
241
  */
239
- export function lastUserText(items) {
240
- if (!Array.isArray(items)) return undefined
241
- for (let index = items.length - 1; index >= 0; index -= 1) {
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() !== '') return text
254
+ if (text.trim() === '' || text === texts[texts.length - 1]) continue
255
+ texts.push(text)
251
256
  }
252
- return undefined
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
- lastUserText,
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 text = lastUserText(sessionHistory(ctx, sessionId))
884
- json(response, 200, text === undefined
885
- ? { ok: false, code: 'NO_HISTORY', message: '这个会话还没有发送过消息' }
886
- : { ok: true, text })
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.5.2",
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": {