@mzzsfy/dsh-context-manager 0.2.1 → 0.4.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/README.md +8 -6
- package/package.json +1 -1
- package/src/client.js +238 -26
- package/src/core.mjs +15 -3
- package/src/index.js +97 -39
- package/test/client-fork-enhance.test.mjs +70 -5
- package/test/client-help.test.mjs +3 -1
- package/test/client-history-button.test.mjs +158 -0
- package/test/client-history-scope.test.mjs +34 -0
- package/test/client-runtime.test.mjs +148 -0
- package/test/core.test.mjs +50 -0
- package/test/index.test.mjs +215 -8
package/README.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# dsh-context-manager
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
对话增强插件:历史输入回溯(Alt+↑ 或输入框工具排按钮唤起)、插话撤回、对话分叉(进行中轮分叉/自动重发)、标题栏复制 sessionId。dsh-session-manager 历史输入功能的独立延续包(原包已移除该功能)。
|
|
4
4
|
|
|
5
5
|
## 功能
|
|
6
6
|
|
|
7
|
-
### 历史输入浮层(Alt+↑)
|
|
7
|
+
### 历史输入浮层(Alt+↑ 或输入框按钮)
|
|
8
8
|
|
|
9
|
-
输入框按 `Alt
|
|
9
|
+
输入框按 `Alt+↑`,或点击输入框下方工具排的历史按钮(指令/附件右侧,与 Alt+↑ 等效,再点关闭)唤起历史输入浮层:
|
|
10
10
|
|
|
11
|
-
-
|
|
11
|
+
- 范围导航:顶栏范围标签点击展开 pill 组选择;`←/→` 按 常用(prompts)→ 当前会话(session)→ 本工作区(workspace)→ 全部工作区(global) 依次切换(边界停住),列表时间倒序
|
|
12
|
+
- 收录范围:普通输入与斜杠命令(如 `/goal …`,按会话日志的 command/run 重组为完整命令行),工具结果回填与插件注入不收录
|
|
12
13
|
- 搜索:输入即时过滤(IME 组合输入守卫,命中计数),`↑/↓` 选中、`Enter` 回填
|
|
13
14
|
- 回填经宿主公共契约 `inputActions.setDraft`;`Esc` 两级退出(搜索词非空先清词,再按关闭浮层)
|
|
14
15
|
- 列表行悬停星标收藏/取消常用提示词;常用范围内点「编辑」进入编辑态(重命名/删除/新增),数据落全局 `prompts.json`,与自动历史互不覆写
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
|
|
41
42
|
## 启停
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
设置页「插件」→「对话增强」卡片六开关(历史输入/输入框历史按钮/插话撤回/对话分叉/分叉后自动重发/标题栏复制 sessionId),经宿主 settings 持久,变更刷新页面生效。「输入框历史按钮」仅控制工具排按钮显隐,停用后 Alt+↑ 与浮层不受影响。
|
|
44
45
|
|
|
45
46
|
## 依赖与门控
|
|
46
47
|
|
|
@@ -55,6 +56,7 @@
|
|
|
55
56
|
| `/api/context/inputs` | GET | 历史输入(`?sessionId=&scope=`) |
|
|
56
57
|
| `/api/context/prompts/toggle` | POST | 收藏/取消收藏(按截断后文本查重) |
|
|
57
58
|
| `/api/context/history-enabled` | GET/POST | 历史浮层启停 |
|
|
59
|
+
| `/api/context/history-button-enabled` | GET/POST | 输入框历史按钮启停 |
|
|
58
60
|
| `/api/context/steer-recall-enabled` | GET/POST | 插话撤回启停 |
|
|
59
61
|
| `/api/context/fork-enabled` | GET/POST | 对话分叉启停 |
|
|
60
62
|
| `/api/context/fork-auto-resend-enabled` | GET/POST | 分叉后自动重发启停 |
|
|
@@ -64,4 +66,4 @@
|
|
|
64
66
|
|
|
65
67
|
## dsh 版本兼容
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
三版本(0.1.2-rc.1 / 0.1.5-rc.3 / 0.1.7-rc.1)兼容通过:样式注入 data-plugin 契约、历史输入浮层(Alt+↑ 或输入框按钮)、启停路由与方法白名单、激活 live。0.1.6 设置分区卡片位于「插件」页,非独立导航项;0.1.7-rc.1 内置插件页改版为分组列表(无卡片开关锚),样式与 API 面实测正常。
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
// dsh-context-manager Client 半区:「插件」设置页可配置卡片(
|
|
2
|
-
// + 历史输入浮层(Alt+↑
|
|
1
|
+
// dsh-context-manager Client 半区:「插件」设置页可配置卡片(六启停开关)
|
|
2
|
+
// + 历史输入浮层(Alt+↑ 或输入框工具排按钮唤起,范围导航/搜索/收藏回填)
|
|
3
|
+
// + 插话撤回 + 对话 fork
|
|
3
4
|
// + 标题栏复制 sessionId(悬停显隐)。
|
|
4
5
|
// 历史输入挂官方 conversation.input.dock 插槽(渲染为零高度锚点),回填走宿主公共
|
|
5
6
|
// 契约 inputActions.setDraft;插话撤回注入官方 pending steering 气泡操作图标排;
|
|
6
7
|
// fork 注入消息气泡操作排,分叉到该轮之前并把该轮用户输入回填子会话输入框(重试
|
|
7
8
|
// 语义),RPC 走宿主 sessions 服务面(fork+open),锚点经 remote.session.follow
|
|
8
|
-
// 开场帧建立轮号→{ 结束 seq, 首问文本 }
|
|
9
|
+
// 开场帧建立轮号→{ 结束 seq, 首问文本 } 映射;回填前经官方 input 快照(useInput)
|
|
10
|
+
// 探测输入框占用,已被用户输入则不覆盖(权限高于回填);复制 sessionId 挂官方
|
|
9
11
|
// conversation.session.header.actions 插槽。浏览器半区经 webServer 路由
|
|
10
12
|
// ('/api/context/*')访问 Host。打包为单文件自包含格式,无法跨文件 require;
|
|
11
13
|
// 与 src/core.mjs 镜像的纯函数(filterHistoryInputs/forkFailureText/forkRetryText)
|
|
@@ -16,6 +18,7 @@ window.__ModuleLoader__.load({
|
|
|
16
18
|
factory(require) {
|
|
17
19
|
const React = require('react')
|
|
18
20
|
const { useState, useEffect, useRef } = React
|
|
21
|
+
const PKG_ID = '@mzzsfy/dsh-context-manager'
|
|
19
22
|
|
|
20
23
|
// 导航图标声明:交给 dsh-settings-nav-icons 统一渲染;键 = 市场短名(发现页
|
|
21
24
|
// 收录显示形态);该插件未就绪时入队,由其启动时排空
|
|
@@ -78,11 +81,19 @@ const CSS = [
|
|
|
78
81
|
' color-scheme:light dark; color:light-dark(#0f1115, #e8eaed);',
|
|
79
82
|
' background:light-dark(#fff, #1e1f22); border-radius:14px;',
|
|
80
83
|
' box-shadow:0 0 0 0.5px light-dark(rgba(15,17,21,.18), rgba(255,255,255,.14)), 0 4px 16px rgba(0,0,0,.08), 0 16px 48px rgba(0,0,0,.16); }',
|
|
81
|
-
'.cx-hist__hint { display:flex; align-items:center; gap:10px; padding:9px 14px; flex:none;',
|
|
84
|
+
'.cx-hist__hint { display:flex; flex-wrap:wrap; align-items:center; gap:10px; padding:9px 14px; flex:none;',
|
|
82
85
|
' border-bottom:1px solid light-dark(rgba(15,17,21,.08), rgba(255,255,255,.1));',
|
|
83
86
|
' font:var(--dsw-font-xxs-12, 12px/18px sans-serif); color:light-dark(rgba(15,17,21,.55), rgba(232,234,237,.55)); }',
|
|
84
|
-
|
|
85
|
-
'
|
|
87
|
+
// 范围 pill 组:点击直达切换,选中实底蓝,未选淡底
|
|
88
|
+
'.cx-hist__scopebar { display:flex; align-items:center; gap:4px; flex:none; }',
|
|
89
|
+
'.cx-hist__pill { appearance:none; border:0; cursor:pointer; flex:none; padding:2px 9px;',
|
|
90
|
+
' border-radius:999px; font:var(--dsw-font-xxs-12, 12px/18px sans-serif);',
|
|
91
|
+
' color:light-dark(rgba(15,17,21,.6), rgba(232,234,237,.6));',
|
|
92
|
+
' background:light-dark(rgba(15,17,21,.06), rgba(255,255,255,.08)); }',
|
|
93
|
+
'.cx-hist__pill:hover { color:light-dark(rgba(15,17,21,.85), rgba(232,234,237,.85));',
|
|
94
|
+
' background:light-dark(rgba(15,17,21,.1), rgba(255,255,255,.14)); }',
|
|
95
|
+
'.cx-hist__pill--on, .cx-hist__pill--on:hover { background:#1677ff; color:#fff; font-weight:600; }',
|
|
96
|
+
'.cx-hist__pill:focus-visible { outline:2px solid #1677ff; outline-offset:1px; }',
|
|
86
97
|
'.cx-hist__search { flex:1; min-width:0; padding:3px 10px; border-radius:8px; color-scheme:light dark;',
|
|
87
98
|
' border:1px solid light-dark(rgba(15,17,21,.14), rgba(255,255,255,.18));',
|
|
88
99
|
' background:transparent; color:inherit; font:var(--dsw-font-xxs-12, 12px/18px sans-serif); }',
|
|
@@ -154,6 +165,7 @@ const CSS = [
|
|
|
154
165
|
const INPUTS_URL = '/api/context/inputs'
|
|
155
166
|
const PROMPTS_TOGGLE_URL = '/api/context/prompts/toggle'
|
|
156
167
|
const HISTORY_ENABLED_URL = '/api/context/history-enabled'
|
|
168
|
+
const HISTORY_BUTTON_ENABLED_URL = '/api/context/history-button-enabled'
|
|
157
169
|
const STEER_ENABLED_URL = '/api/context/steer-recall-enabled'
|
|
158
170
|
const FORK_ENABLED_URL = '/api/context/fork-enabled'
|
|
159
171
|
const FORK_AUTO_RESEND_URL = '/api/context/fork-auto-resend-enabled'
|
|
@@ -236,11 +248,70 @@ function forkRetryText(data) {
|
|
|
236
248
|
return text.trim() === '' ? null : text
|
|
237
249
|
}
|
|
238
250
|
|
|
251
|
+
// ── 输入框历史按钮:注入官方输入工具排,点击开关浮层(与 Alt+↑ 等效)──
|
|
252
|
+
|
|
253
|
+
// 注入标记:识别自家节点,防重复注入;停用/卸载按标记全量移除
|
|
254
|
+
const HISTORY_BTN_FLAG = 'data-cx-hist-trigger'
|
|
255
|
+
// 官方输入工具排定位锚点:行类名后缀 + 官方 _add 图标按钮,双锚点一致才认;
|
|
256
|
+
// 官方类名漂移时寻行返回 null,注入静默跳过(干净禁用)
|
|
257
|
+
const TOOLS_ROW_SUFFIX = '_tools'
|
|
258
|
+
// 按钮悬停说明:点击与 Alt+↑ 等效
|
|
259
|
+
const HISTORY_BUTTON_TITLE = '历史输入(Alt+↑)'
|
|
260
|
+
|
|
261
|
+
// 官方输入工具排寻行:遍历官方 _add 图标按钮取其所属行
|
|
262
|
+
function findToolsRow(doc) {
|
|
263
|
+
for (const button of doc.querySelectorAll('button[class$="_add"]')) {
|
|
264
|
+
const row = button.parentElement
|
|
265
|
+
if (row && typeof row.className === 'string' && row.className.endsWith(TOOLS_ROW_SUFFIX)) return row
|
|
266
|
+
}
|
|
267
|
+
return null
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 历史图标 svg(时钟回拨):fill 继承 currentColor,视觉重量与官方 14px 图标一致
|
|
271
|
+
function histButtonSvg(doc) {
|
|
272
|
+
const namespace = 'http://www.w3.org/2000/svg'
|
|
273
|
+
const svg = doc.createElementNS(namespace, 'svg')
|
|
274
|
+
svg.setAttribute('width', '14')
|
|
275
|
+
svg.setAttribute('height', '14')
|
|
276
|
+
svg.setAttribute('viewBox', '0 0 24 24')
|
|
277
|
+
svg.setAttribute('fill', 'none')
|
|
278
|
+
svg.setAttribute('aria-hidden', 'true')
|
|
279
|
+
const path = doc.createElementNS(namespace, 'path')
|
|
280
|
+
path.setAttribute('fill', 'currentColor')
|
|
281
|
+
path.setAttribute('d', 'M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L8.85 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 16.99 10.51 18 13 18c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z')
|
|
282
|
+
svg.appendChild(path)
|
|
283
|
+
return svg
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 幂等注入:未注入时克隆官方 _add 类名创建按钮(尺寸/hover 全部原生),位置
|
|
287
|
+
// 固定在最后一个官方 _add 之后(与指令/附件同簇);已注入原样返回。
|
|
288
|
+
// doc 注入便于测试与宿主文档解耦
|
|
289
|
+
function ensureHistoryButton(doc, row, onToggle) {
|
|
290
|
+
const existing = row.querySelector('[' + HISTORY_BTN_FLAG + ']')
|
|
291
|
+
if (existing) return existing
|
|
292
|
+
const official = row.querySelector('button[class$="_add"]')
|
|
293
|
+
if (!official) return null
|
|
294
|
+
const button = doc.createElement('button')
|
|
295
|
+
button.type = 'button'
|
|
296
|
+
button.setAttribute(HISTORY_BTN_FLAG, '')
|
|
297
|
+
button.className = official.className
|
|
298
|
+
button.title = HISTORY_BUTTON_TITLE
|
|
299
|
+
button.setAttribute('aria-label', '历史输入')
|
|
300
|
+
button.addEventListener('click', onToggle)
|
|
301
|
+
button.appendChild(histButtonSvg(doc))
|
|
302
|
+
const officialButtons = row.querySelectorAll('button[class$="_add"]')
|
|
303
|
+
const last = officialButtons[officialButtons.length - 1]
|
|
304
|
+
row.insertBefore(button, (last && last.nextSibling) || null)
|
|
305
|
+
return button
|
|
306
|
+
}
|
|
307
|
+
|
|
239
308
|
function HistoryDock({ session, inputActions }) {
|
|
240
309
|
const [open, setOpen] = useState(false)
|
|
241
310
|
const [items, setItems] = useState(null)
|
|
242
311
|
const [cursor, setCursor] = useState(-1)
|
|
243
312
|
const [scopeIndex, setScopeIndex] = useState(HISTORY_SCOPE_DEFAULT)
|
|
313
|
+
// 范围标签展开态:默认单胶囊只显当前范围,点击展开 pill 组,选择后收起
|
|
314
|
+
const [scopeOpen, setScopeOpen] = useState(false)
|
|
244
315
|
const [aligning, setAligning] = useState(false)
|
|
245
316
|
const [loadError, setLoadError] = useState(false)
|
|
246
317
|
// 收藏集合:浮层打开时并行拉一次,行悬停星标据此显示实/空心;
|
|
@@ -262,8 +333,13 @@ function HistoryDock({ session, inputActions }) {
|
|
|
262
333
|
// 历史浮层启停(设置开关):挂载拉取一次,停用即整体不渲染、键盘放行
|
|
263
334
|
const enabledRef = React.useRef(true)
|
|
264
335
|
const [historyEnabled, setHistoryEnabled] = useState(true)
|
|
336
|
+
// 输入框历史按钮启停(设置开关):false 仅隐藏按钮,快捷键与浮层不受影响
|
|
337
|
+
const buttonEnabledRef = React.useRef(true)
|
|
338
|
+
const [buttonEnabled, setButtonEnabled] = useState(true)
|
|
265
339
|
sessionRef.current = session
|
|
266
340
|
inputActionsRef.current = inputActions
|
|
341
|
+
// 条件 return 之前同步:停用路径的扫描守卫也要看到最新值(与 SteerRecallDock 同构)
|
|
342
|
+
buttonEnabledRef.current = buttonEnabled
|
|
267
343
|
useEffect(() => {
|
|
268
344
|
api(HISTORY_ENABLED_URL)
|
|
269
345
|
.then((payload) => {
|
|
@@ -272,6 +348,9 @@ function HistoryDock({ session, inputActions }) {
|
|
|
272
348
|
setHistoryEnabled(on)
|
|
273
349
|
})
|
|
274
350
|
.catch(() => {})
|
|
351
|
+
api(HISTORY_BUTTON_ENABLED_URL)
|
|
352
|
+
.then((payload) => setButtonEnabled(payload ? payload.enabled !== false : true))
|
|
353
|
+
.catch(() => {})
|
|
275
354
|
}, [])
|
|
276
355
|
|
|
277
356
|
function syncView(patch) {
|
|
@@ -388,6 +467,7 @@ function HistoryDock({ session, inputActions }) {
|
|
|
388
467
|
rawItemsRef.current = null
|
|
389
468
|
queryRef.current = ''
|
|
390
469
|
setQuery('')
|
|
470
|
+
setScopeOpen(false)
|
|
391
471
|
syncView({ open: true, items: null, cursor: -1, scopeIndex: HISTORY_SCOPE_DEFAULT, aligning: false })
|
|
392
472
|
setOpen(true)
|
|
393
473
|
setScopeIndex(HISTORY_SCOPE_DEFAULT)
|
|
@@ -400,10 +480,10 @@ function HistoryDock({ session, inputActions }) {
|
|
|
400
480
|
requestScope(HISTORY_SCOPE_DEFAULT)
|
|
401
481
|
}
|
|
402
482
|
|
|
403
|
-
//
|
|
404
|
-
function
|
|
405
|
-
|
|
406
|
-
|
|
483
|
+
// 范围切换共用落点:清过滤与选择态后拉取目标范围;同范围不动,切换即收起展开态
|
|
484
|
+
function selectScope(next) {
|
|
485
|
+
if (next === viewRef.current.scopeIndex) return
|
|
486
|
+
setScopeOpen(false)
|
|
407
487
|
rawItemsRef.current = null
|
|
408
488
|
queryRef.current = ''
|
|
409
489
|
setQuery('')
|
|
@@ -417,6 +497,13 @@ function HistoryDock({ session, inputActions }) {
|
|
|
417
497
|
requestScope(next)
|
|
418
498
|
}
|
|
419
499
|
|
|
500
|
+
// ←/→ 相对切换:→ 向大(工作区/全局),← 返回收藏;边界停住
|
|
501
|
+
function switchScope(delta) {
|
|
502
|
+
const next = viewRef.current.scopeIndex + delta
|
|
503
|
+
if (next < 0 || next >= HISTORY_SCOPES.length) return
|
|
504
|
+
selectScope(next)
|
|
505
|
+
}
|
|
506
|
+
|
|
420
507
|
// Alt+↑ 唤起浮层;浮层开 = 菜单模态,捕获阶段拦截导航键,先于 Lexical 光标移动。
|
|
421
508
|
// 设置开关停用时监听器直接放行(读 ref,开关值挂载拉取后即时反映)
|
|
422
509
|
useEffect(() => {
|
|
@@ -493,11 +580,15 @@ function HistoryDock({ session, inputActions }) {
|
|
|
493
580
|
if (row) row.scrollIntoView({ block: 'nearest' })
|
|
494
581
|
}, [open, cursor, items])
|
|
495
582
|
|
|
496
|
-
// 浮层外点击关闭(双写:漏写 viewRef 会让键盘层误判浮层仍开,吞掉输入框方向键)
|
|
583
|
+
// 浮层外点击关闭(双写:漏写 viewRef 会让键盘层误判浮层仍开,吞掉输入框方向键)。
|
|
584
|
+
// 注入按钮在本容器外,closest 守卫跳过:否则 mousedown 先关、click 再开,
|
|
585
|
+
// 浮层开时点按钮等于常开,toggle 语义失效
|
|
497
586
|
useEffect(() => {
|
|
498
587
|
if (!open) return undefined
|
|
499
588
|
function onPointerDown(event) {
|
|
500
|
-
|
|
589
|
+
const target = event.target
|
|
590
|
+
if (target && typeof target.closest === 'function' && target.closest('[' + HISTORY_BTN_FLAG + ']')) return
|
|
591
|
+
if (rootRef.current && !rootRef.current.contains(target)) {
|
|
501
592
|
syncView({ open: false })
|
|
502
593
|
setOpen(false)
|
|
503
594
|
}
|
|
@@ -506,6 +597,49 @@ function HistoryDock({ session, inputActions }) {
|
|
|
506
597
|
return () => document.removeEventListener('mousedown', onPointerDown, true)
|
|
507
598
|
}, [open])
|
|
508
599
|
|
|
600
|
+
// 注入按钮点击:开→关,关→唤起;经 ref 接线,挂载一次的监听器恒读最新闭包
|
|
601
|
+
function toggleFromButton() {
|
|
602
|
+
if (viewRef.current.open) {
|
|
603
|
+
syncView({ open: false })
|
|
604
|
+
setOpen(false)
|
|
605
|
+
return
|
|
606
|
+
}
|
|
607
|
+
openPopup()
|
|
608
|
+
}
|
|
609
|
+
const toggleRef = React.useRef(() => {})
|
|
610
|
+
toggleRef.current = toggleFromButton
|
|
611
|
+
|
|
612
|
+
// 输入框工具排按钮(设置开关):挂载后扫描官方工具排注入;官方工具排随布局
|
|
613
|
+
// 状态被 React 重建时经观察器补注,开关值异步到达(无 DOM 变更)经重扫对账。
|
|
614
|
+
// 启停读 ref(开关值挂载拉取后即时反映),停用/卸载按标记全量移除
|
|
615
|
+
const buttonScanRef = React.useRef(null)
|
|
616
|
+
useEffect(() => {
|
|
617
|
+
function removeAll() {
|
|
618
|
+
document.querySelectorAll('[' + HISTORY_BTN_FLAG + ']').forEach((button) => button.remove())
|
|
619
|
+
}
|
|
620
|
+
function sync() {
|
|
621
|
+
if (enabledRef.current === false || buttonEnabledRef.current === false) {
|
|
622
|
+
removeAll()
|
|
623
|
+
return
|
|
624
|
+
}
|
|
625
|
+
const row = findToolsRow(document)
|
|
626
|
+
if (row) ensureHistoryButton(document, row, () => toggleRef.current())
|
|
627
|
+
}
|
|
628
|
+
buttonScanRef.current = sync
|
|
629
|
+
const observer = new MutationObserver(sync)
|
|
630
|
+
observer.observe(document.documentElement, { childList: true, subtree: true })
|
|
631
|
+
sync()
|
|
632
|
+
return () => {
|
|
633
|
+
buttonScanRef.current = null
|
|
634
|
+
observer.disconnect()
|
|
635
|
+
removeAll()
|
|
636
|
+
}
|
|
637
|
+
}, [])
|
|
638
|
+
// 开关状态/浮层启停变化即时重扫(观察器只覆盖 DOM 变更,状态变化需主动对账)
|
|
639
|
+
useEffect(() => {
|
|
640
|
+
if (buttonScanRef.current) buttonScanRef.current()
|
|
641
|
+
}, [historyEnabled, buttonEnabled])
|
|
642
|
+
|
|
509
643
|
// 零高度锚点:平时不占任何界面空间,仅浮层打开时渲染;设置停用整体不渲染
|
|
510
644
|
if (session === undefined || inputActions === undefined || historyEnabled === false) return null
|
|
511
645
|
const promptsIdx = HISTORY_SCOPES.indexOf('prompts')
|
|
@@ -560,7 +694,20 @@ function HistoryDock({ session, inputActions }) {
|
|
|
560
694
|
return h('div', { className: 'cx-hist', ref: rootRef },
|
|
561
695
|
open && h('div', { className: 'cx-hist__pop' },
|
|
562
696
|
h('div', { className: 'cx-hist__hint' },
|
|
563
|
-
h('span', { className: 'cx-
|
|
697
|
+
h('span', { className: 'cx-hist__scopebar' },
|
|
698
|
+
scopeOpen
|
|
699
|
+
? HISTORY_SCOPE_LABELS.map((label, idx) => h('button', {
|
|
700
|
+
key: label,
|
|
701
|
+
className: 'cx-hist__pill' + (idx === scopeIndex ? ' cx-hist__pill--on' : ''),
|
|
702
|
+
title: idx === scopeIndex ? '收起' : '切换到' + label,
|
|
703
|
+
onClick: () => (idx === scopeIndex ? setScopeOpen(false) : selectScope(idx)),
|
|
704
|
+
}, label))
|
|
705
|
+
: h('button', {
|
|
706
|
+
className: 'cx-hist__pill cx-hist__pill--on',
|
|
707
|
+
title: '点击选择范围',
|
|
708
|
+
onClick: () => setScopeOpen(true),
|
|
709
|
+
}, HISTORY_SCOPE_LABELS[scopeIndex] + ' ▾'),
|
|
710
|
+
),
|
|
564
711
|
inPrompts
|
|
565
712
|
? h('button', {
|
|
566
713
|
className: 'cx-hist__editbtn',
|
|
@@ -717,6 +864,9 @@ const FORK_PAGE_MAX_MESSAGES = 2000
|
|
|
717
864
|
// 重试草稿通道:分叉成功后子会话尚未挂载,原输入先按子会话 id 暂存,
|
|
718
865
|
// 子会话的 dock 挂载(inputActions 就绪)时消费回填并清除
|
|
719
866
|
const pendingForkDrafts = new Map()
|
|
867
|
+
// 已消费会话:effect 依赖(inputActions 身份)变化会重跑,重跑时草稿已被 delete,
|
|
868
|
+
// 若不做标记会落入「无草稿」路径并对输入框产生二次写入(空写即清空)
|
|
869
|
+
const consumedForkDrafts = new Set()
|
|
720
870
|
// 草稿消费重查:覆盖 open 切换期间「挂载先于登记」的窗口(单次即可,上限留裕量)
|
|
721
871
|
const FORK_DRAFT_POLL_MS = 400
|
|
722
872
|
const FORK_DRAFT_POLLS = 3
|
|
@@ -832,7 +982,7 @@ function SteerRecallDock({ session, useSession, inputActions, updateQueue }) {
|
|
|
832
982
|
// 轮号无映射 = 该轮未完成,首问无文本(纯图等)= 无法重试,均不注入;
|
|
833
983
|
// 首轮无前锚(宿主 fork 边界必须落在 turn/end 上,复制零事件不可表达),也不注入。
|
|
834
984
|
// 分叉动作经宿主 sessions 服务面 fork+open(与官方 chat 同构),成功 toast 并切换
|
|
835
|
-
function ForkDockWithBootstrap({ session, inputActions, forkSession, cancelSession, submitPrompt, loadTurnEnds }) {
|
|
985
|
+
function ForkDockWithBootstrap({ session, inputActions, useInput, forkSession, cancelSession, submitPrompt, loadTurnEnds }) {
|
|
836
986
|
// 依赖键 = 会话 id:session 快照身份随每次投影更新漂移,不能作 effect 依赖;
|
|
837
987
|
// loadTurnEnds 闭包身份同样不稳定,经 ref 取用
|
|
838
988
|
const sessionId = session && session.sessionId
|
|
@@ -842,15 +992,39 @@ function ForkDockWithBootstrap({ session, inputActions, forkSession, cancelSessi
|
|
|
842
992
|
const submitRef = useRef(null)
|
|
843
993
|
submitRef.current = submitPrompt
|
|
844
994
|
const [turnEnds, setTurnEnds] = useState(null)
|
|
995
|
+
// 映射基线:重拉结果与基线比对,相同则不 setState——setState 会触发重渲染并再次
|
|
996
|
+
// 触发扫描,新轮在映射中缺失时形成 拉取→setState→扫描→拉取 的无限循环
|
|
997
|
+
const turnEndsRef = useRef(null)
|
|
998
|
+
// 在途门控:MutationObserver 在流式输出期间高频触发,无门控会造成请求风暴
|
|
999
|
+
const refreshPendingRef = useRef(false)
|
|
1000
|
+
// 重拉入口装配到 ref 供扫描调用:新轮 turn/end 落在拉取窗口之后时映射里没有该轮,
|
|
1001
|
+
// 注入资格判空即跳过,按钮永不出现(实测:必须刷新页面才恢复)。门控保证同时在途
|
|
1002
|
+
// 最多一次;结果与基线相同则不 setState,避免 setState→扫描→拉取的循环
|
|
1003
|
+
const refreshRef = useRef(null)
|
|
1004
|
+
refreshRef.current = () => {
|
|
1005
|
+
if (refreshPendingRef.current) return
|
|
1006
|
+
refreshPendingRef.current = true
|
|
1007
|
+
Promise.resolve(loadRef.current())
|
|
1008
|
+
.then((map) => {
|
|
1009
|
+
refreshPendingRef.current = false
|
|
1010
|
+
if (turnEndsRef.current === map) return
|
|
1011
|
+
turnEndsRef.current = map
|
|
1012
|
+
setTurnEnds(map)
|
|
1013
|
+
})
|
|
1014
|
+
.catch(() => { refreshPendingRef.current = false })
|
|
1015
|
+
}
|
|
845
1016
|
useEffect(() => {
|
|
846
1017
|
if (sessionId === undefined) return undefined
|
|
847
1018
|
let disposed = false
|
|
848
1019
|
// 拉取前置空:会话切换后旧会话的映射不得继续服务新会话——轮号跨会话重叠,
|
|
849
1020
|
// 旧 seq 会让分叉锚点错位;置空期间 ForkDock 以 null 短路,不注入按钮
|
|
850
1021
|
setTurnEnds(null)
|
|
1022
|
+
turnEndsRef.current = null
|
|
851
1023
|
loadRef.current()
|
|
852
1024
|
.then((map) => {
|
|
853
|
-
if (
|
|
1025
|
+
if (disposed) return
|
|
1026
|
+
turnEndsRef.current = map
|
|
1027
|
+
setTurnEnds(map)
|
|
854
1028
|
})
|
|
855
1029
|
.catch((error) => {
|
|
856
1030
|
// 映射拉取失败(网络/宿主繁忙):fork 无锚点可用,保持禁用不注入;
|
|
@@ -866,8 +1040,14 @@ function ForkDockWithBootstrap({ session, inputActions, forkSession, cancelSessi
|
|
|
866
1040
|
// 自动重发(开关开):草稿带 autoSubmit 标记,setDraft 后经 remote.session.prompt
|
|
867
1041
|
// 立即提交原文(重生成语义);prompt 失败仅 toast——文本已在输入框,可手动发送
|
|
868
1042
|
const [draftPollTick, setDraftPollTick] = useState(0)
|
|
1043
|
+
// 回填前探测输入框当前草稿:宿主输入快照(useInput)只在快照面可用,
|
|
1044
|
+
// 缺失时按空草稿处理(旧宿主降级为原行为)
|
|
1045
|
+
const liveDraft = typeof useInput === 'function' ? useInput((state) => state.draft) : ''
|
|
1046
|
+
const liveDraftRef = useRef('')
|
|
1047
|
+
liveDraftRef.current = typeof liveDraft === 'string' ? liveDraft : ''
|
|
869
1048
|
useEffect(() => {
|
|
870
1049
|
if (sessionId === undefined || !inputActions || typeof inputActions.setDraft !== 'function') return undefined
|
|
1050
|
+
if (consumedForkDrafts.has(sessionId)) return undefined
|
|
871
1051
|
const draft = pendingForkDrafts.get(sessionId)
|
|
872
1052
|
if (draft === undefined) {
|
|
873
1053
|
if (draftPollTick >= FORK_DRAFT_POLLS) return undefined
|
|
@@ -875,20 +1055,34 @@ function ForkDockWithBootstrap({ session, inputActions, forkSession, cancelSessi
|
|
|
875
1055
|
return () => clearTimeout(timer)
|
|
876
1056
|
}
|
|
877
1057
|
pendingForkDrafts.delete(sessionId)
|
|
1058
|
+
consumedForkDrafts.add(sessionId)
|
|
878
1059
|
const text = draft && typeof draft === 'object' ? draft.text : draft
|
|
879
1060
|
const autoSubmit = Boolean(draft && typeof draft === 'object' && draft.autoSubmit === true)
|
|
880
|
-
|
|
1061
|
+
// 用户已开始输入:回填是破坏性整体替换(setDraft 清根重建),优先级低于用户输入;
|
|
1062
|
+
// 放弃回填并提示,避免吞掉用户在 dock 挂载窗口内的击键
|
|
1063
|
+
const draftOccupied = liveDraftRef.current.trim() !== ''
|
|
881
1064
|
if (autoSubmit && submitRef.current) {
|
|
1065
|
+
// 提交即无草稿(官方发送语义):原文已发出,输入框不得残留,否则用户再发即重复消息;
|
|
1066
|
+
// 提交失败才回填文本,供手动重发
|
|
1067
|
+
if (draftOccupied) {
|
|
1068
|
+
toast('子会话已有输入,原输入未自动发送')
|
|
1069
|
+
return undefined
|
|
1070
|
+
}
|
|
882
1071
|
Promise.resolve(submitRef.current({ sessionId, text })).catch((error) => {
|
|
883
|
-
|
|
1072
|
+
if (typeof inputActions.setDraft === 'function') inputActions.setDraft(text)
|
|
1073
|
+
toast('自动重发失败,文本已回填输入框可手动发送: ' + String(error && error.message || error), { kind: 'error' })
|
|
884
1074
|
})
|
|
1075
|
+
} else if (draftOccupied) {
|
|
1076
|
+
toast('子会话已有输入,原输入未回填')
|
|
1077
|
+
} else {
|
|
1078
|
+
inputActions.setDraft(text)
|
|
885
1079
|
}
|
|
886
1080
|
return undefined
|
|
887
1081
|
}, [sessionId, inputActions, draftPollTick])
|
|
888
|
-
return h(ForkDock, { session, forkSession, cancelSession, turnEnds })
|
|
1082
|
+
return h(ForkDock, { session, forkSession, cancelSession, turnEnds, refreshRef })
|
|
889
1083
|
}
|
|
890
1084
|
|
|
891
|
-
function ForkDock({ session, forkSession, cancelSession, turnEnds }) {
|
|
1085
|
+
function ForkDock({ session, forkSession, cancelSession, turnEnds, refreshRef }) {
|
|
892
1086
|
const [enabled, setEnabled] = useState(true)
|
|
893
1087
|
const [autoResend, setAutoResend] = useState(false)
|
|
894
1088
|
const turnEndsRef = useRef(null)
|
|
@@ -937,13 +1131,18 @@ function ForkDock({ session, forkSession, cancelSession, turnEnds }) {
|
|
|
937
1131
|
if (rawTurn === null || rawTurn.trim() === '') return
|
|
938
1132
|
const turn = Number(rawTurn)
|
|
939
1133
|
if (!Number.isInteger(turn)) return
|
|
940
|
-
// 重试资格三查:映射缺失的轮(
|
|
1134
|
+
// 重试资格三查:映射缺失的轮(超窗口/新轮)不可分叉;首轮无前锚(宿主
|
|
941
1135
|
// fork 边界必须落在 turn/end,复制零事件不可表达);无首问文本(纯图等)
|
|
942
1136
|
// 回填无从谈起。open=true 的进行中轮 turn/end 未落账,同样可分叉——
|
|
943
1137
|
// 锚点取其前一个闭合轮,该轮未完成的回复分叉后停掉(见点击处理器)。
|
|
944
1138
|
// 按钮在场即暗示可用,禁用态文案无法区分成因,不注入即误导
|
|
945
1139
|
const entry = turnEndsRef.current.get(turn)
|
|
946
|
-
if (entry === undefined
|
|
1140
|
+
if (entry === undefined) {
|
|
1141
|
+
// 新轮:映射是挂载时的一次快照,补拉后由 setState 触发重扫
|
|
1142
|
+
if (refreshRef.current) refreshRef.current()
|
|
1143
|
+
return
|
|
1144
|
+
}
|
|
1145
|
+
if (entry.text === null) return
|
|
947
1146
|
let previous = null
|
|
948
1147
|
for (const key of turnEndsRef.current.keys()) {
|
|
949
1148
|
const candidate = turnEndsRef.current.get(key)
|
|
@@ -976,9 +1175,10 @@ function ForkDock({ session, forkSession, cancelSession, turnEnds }) {
|
|
|
976
1175
|
.then((childId) => {
|
|
977
1176
|
// 暂存先于 toast:open 的子会话挂载可能紧随 resolve,回填承诺必须先就位;
|
|
978
1177
|
// open 失败时暂存保留,用户手动打开子会话仍兑现;
|
|
979
|
-
//
|
|
1178
|
+
// 自动重发开关开:登记带标记草稿,子会话消费时直接提交(不留草稿)
|
|
980
1179
|
if (typeof childId === 'string' && childId !== '') {
|
|
981
1180
|
pendingForkDrafts.set(childId, { text: currentEntry.text, autoSubmit: autoResendOn })
|
|
1181
|
+
consumedForkDrafts.delete(childId)
|
|
982
1182
|
}
|
|
983
1183
|
// 进行中轮分叉后原会话该轮无人再读,停掉止损;失败不影响分叉成功事实
|
|
984
1184
|
if (currentEntry.open === true && typeof cancelSessionFn === 'function') {
|
|
@@ -986,7 +1186,7 @@ function ForkDock({ session, forkSession, cancelSession, turnEnds }) {
|
|
|
986
1186
|
console.warn('[context-manager] 分叉后停止原会话进行中回复失败', error)
|
|
987
1187
|
})
|
|
988
1188
|
}
|
|
989
|
-
toast('已分叉,原输入已填入子会话输入框')
|
|
1189
|
+
toast(autoResendOn ? '已分叉,原输入已自动发送到子会话' : '已分叉,原输入已填入子会话输入框')
|
|
990
1190
|
return childId
|
|
991
1191
|
})
|
|
992
1192
|
.catch((error) => {
|
|
@@ -1016,7 +1216,8 @@ function ForkDock({ session, forkSession, cancelSession, turnEnds }) {
|
|
|
1016
1216
|
|
|
1017
1217
|
// 面板设置项悬停说明:原生 title(设置侧栏为滚动容器,CSS 气泡会被 overflow
|
|
1018
1218
|
// 裁剪,JS 定位复杂度不成比例);文案与功能行为同源维护,由源码契约测试锁定
|
|
1019
|
-
const HISTORY_SWITCH_TITLE = '在输入框按 Alt+↑ 唤起历史输入浮层,浏览并回填历史输入;浮层内 ←/→
|
|
1219
|
+
const HISTORY_SWITCH_TITLE = '在输入框按 Alt+↑ 唤起历史输入浮层,浏览并回填历史输入;浮层内 ←/→ 切换范围,点击顶栏范围标签展开选择(常用 / 当前会话 / 本工作区 / 全部工作区),顶部搜索框过滤条目,行悬停星标可收藏常用提示词。停用后快捷键与浮层整体关闭,刷新页面生效。'
|
|
1220
|
+
const HISTORY_BUTTON_SWITCH_TITLE = '输入框下方工具排显示历史输入按钮,点击打开浮层,再点关闭,与 Alt+↑ 等效;停用仅隐藏按钮,快捷键与浮层不受影响;刷新页面生效。'
|
|
1020
1221
|
const STEER_SWITCH_TITLE = '插话发送后、尚未被智能体应用期间,在该插话气泡的操作图标排显示撤回按钮,点击撤回并把原文填回输入框(覆盖输入框现有草稿);含附件的插话不可撤回;消息被应用后按钮随气泡消失,恰在应用瞬间点击会提示已应用且不动草稿。停用即不再注入,刷新页面生效。'
|
|
1021
1222
|
const FORK_SWITCH_TITLE = '消息气泡操作排显示分叉按钮,点击分叉出新会话到该轮之前(该轮不带入子会话),该轮的用户输入自动回填子会话输入框供编辑重发,子会话自动打开且标题尾号递增;进行中的轮(回复尚未完成)同样可分叉,分叉后自动停止本会话该轮未完成的回复;首轮(无更早上下文可继承,新建会话即为同义操作)与无文本输入的轮(纯图等,无从重发)不注入;停用即不再注入,刷新页面生效。'
|
|
1022
1223
|
const FORK_AUTO_RESEND_SWITCH_TITLE = '分叉成功后自动把该轮原输入发送到子会话立即开跑(重生成语义,相当于原输入重跑一遍);关闭时分叉仅把原输入回填子会话输入框,由你编辑后再手动发送。'
|
|
@@ -1111,6 +1312,7 @@ function switchRow(url, label, title, okText) {
|
|
|
1111
1312
|
}
|
|
1112
1313
|
|
|
1113
1314
|
const HistorySwitchRow = switchRow(HISTORY_ENABLED_URL, '历史输入浮层(Alt+↑)', HISTORY_SWITCH_TITLE, '历史输入浮层')
|
|
1315
|
+
const HistoryButtonSwitchRow = switchRow(HISTORY_BUTTON_ENABLED_URL, '输入框历史按钮', HISTORY_BUTTON_SWITCH_TITLE, '输入框历史按钮')
|
|
1114
1316
|
const SteerSwitchRow = switchRow(STEER_ENABLED_URL, '插话撤回', STEER_SWITCH_TITLE, '插话撤回')
|
|
1115
1317
|
const ForkSwitchRow = switchRow(FORK_ENABLED_URL, '对话 fork', FORK_SWITCH_TITLE, '对话 fork')
|
|
1116
1318
|
const ForkAutoResendSwitchRow = switchRow(FORK_AUTO_RESEND_URL, '分叉后自动重发', FORK_AUTO_RESEND_SWITCH_TITLE, '分叉后自动重发')
|
|
@@ -1135,6 +1337,7 @@ function ContextPanel() {
|
|
|
1135
1337
|
open && h('div', { className: 'cx-card__body' },
|
|
1136
1338
|
h('span', { className: 'cx-panel__hint' }, '变更刷新页面生效。'),
|
|
1137
1339
|
h(HistorySwitchRow),
|
|
1340
|
+
h(HistoryButtonSwitchRow),
|
|
1138
1341
|
h(SteerSwitchRow),
|
|
1139
1342
|
h(ForkSwitchRow),
|
|
1140
1343
|
h(ForkAutoResendSwitchRow),
|
|
@@ -1193,15 +1396,24 @@ function ContextPanel() {
|
|
|
1193
1396
|
}, 'context-manager styles')
|
|
1194
1397
|
|
|
1195
1398
|
// 设置卡片:注册进官方「插件」设置页的可配置插件区(keyed slot,
|
|
1196
|
-
// key = 本插件 settings namespace)
|
|
1399
|
+
// key = 本插件 settings namespace),不占独立设置导航项。
|
|
1400
|
+
// 0.1.7-rc.1+ 该槽被宿主移除,卡片换形注册进插件管理页包详情配置槽
|
|
1401
|
+
// (plugins.bundle.config,key = 包名,管理页 ledger.bundles 判定在场);
|
|
1402
|
+
// 两代槽位并存注册:inject 是声明生命周期效应,错误世代宿主永不声明
|
|
1403
|
+
// 该槽,回调挂起不执行,零成本
|
|
1197
1404
|
ctx.slots.inject('settings.plugin.item', () =>
|
|
1198
1405
|
ctx.slots.register(
|
|
1199
1406
|
{ name: 'settings.plugin.item', id: 'context', key: 'context' },
|
|
1200
1407
|
() => React.createElement(ContextPanel),
|
|
1201
1408
|
))
|
|
1409
|
+
ctx.slots.inject('plugins.bundle.config', () =>
|
|
1410
|
+
ctx.slots.register(
|
|
1411
|
+
{ name: 'plugins.bundle.config', key: PKG_ID },
|
|
1412
|
+
() => React.createElement(ContextPanel),
|
|
1413
|
+
))
|
|
1202
1414
|
|
|
1203
1415
|
// 历史输入回溯入口:官方 conversation.input.dock 插槽(dsh-client-ui-goal 同构)。
|
|
1204
|
-
//
|
|
1416
|
+
// 宿主缺该插槽时 inject 挂起不注册(声明生命周期效应),本功能静默缺席
|
|
1205
1417
|
try {
|
|
1206
1418
|
ctx.slots.inject('conversation.input.dock', () =>
|
|
1207
1419
|
ctx.slots.register(
|
package/src/core.mjs
CHANGED
|
@@ -17,6 +17,9 @@ export const HISTORY_INPUT_LIMIT = 200
|
|
|
17
17
|
export const HISTORY_INPUT_MAX_CHARS = 20 * 1000
|
|
18
18
|
export const HISTORY_SCOPES = ['prompts', 'session', 'workspace', 'global']
|
|
19
19
|
export const HISTORY_PROMPTS_MAX = 100
|
|
20
|
+
// 提取器输出格式版本:升级即令全部 extracts 指纹缓存失效重解压(自愈补齐新格式条目);
|
|
21
|
+
// 已知边界——超 MAX_ARTIFACT 的巨产物会话对齐时直接跳过,其旧版条目不随升级补齐
|
|
22
|
+
export const HISTORY_EXTRACT_VERSION = 2
|
|
20
23
|
export const HISTORY_ALIGN_THROTTLE_MS = 30 * 1000
|
|
21
24
|
export const HISTORY_STARTUP_DELAY_MS = 30 * 1000
|
|
22
25
|
export const HISTORY_STARTUP_SCAN_LIMIT = 100
|
|
@@ -53,15 +56,24 @@ export function isSessionRunning({ agents, sessionId }) {
|
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
59
|
+
* 从会话事件流提取人类输入:user/message(来源为用户本人,文本块按行拼接)与
|
|
60
|
+
* command/run(斜杠命令,重组为 /name+args;args 是命令名后 verbatim rawInput,
|
|
61
|
+
* 含分隔空白,recordInput: false 的命令无 args 产出裸命令名)。
|
|
62
|
+
* 工具结果回填、插件注入与空白输入(纯图/空/纯空白)均不产出。
|
|
58
63
|
* @param events - readSession 返回的原始事件数组
|
|
59
64
|
* @returns 条目 { text, at },时间取事件时间
|
|
60
65
|
*/
|
|
61
66
|
export function extractUserInputs(events) {
|
|
62
67
|
const entries = []
|
|
63
68
|
for (const event of events || []) {
|
|
64
|
-
if (!event
|
|
69
|
+
if (!event) continue
|
|
70
|
+
if (event.type === 'command/run') {
|
|
71
|
+
const data = event.data
|
|
72
|
+
if (!data || !data.source || data.source.kind !== 'user' || typeof data.name !== 'string' || data.name === '') continue
|
|
73
|
+
entries.push({ text: '/' + data.name + (typeof data.args === 'string' ? data.args : ''), at: event.time })
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
if (event.type !== 'user/message') continue
|
|
65
77
|
const message = event.data
|
|
66
78
|
if (!message || !message.source || message.source.kind !== 'user') continue
|
|
67
79
|
const text = (message.content || [])
|