dsh-session-flow 1.2.0 → 1.3.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/lib/client.js +631 -87
- package/lib/host.js +6 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -24,7 +24,10 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
24
24
|
const OTHER_ACTIVE_ATTRS = ['data-dsh-taskboard-active', 'data-dsh-ssh-active']
|
|
25
25
|
const ACTIVATE_EVENT = 'dsh-panel-activate'
|
|
26
26
|
const PANEL_NAME = 'sessionflow'
|
|
27
|
-
|
|
27
|
+
// 会话面板容器选择器:dsh v0.1.2 起官方移除 data-pane="conversation"(会话流
|
|
28
|
+
// 重构进 dsh-client-ui-chat),兜底命中布局中栏(CenterColumn,类名含 centerCol
|
|
29
|
+
// 哈希后缀稳定片段)。工作台/标签切换/钢琴键定位共用此锚。
|
|
30
|
+
const CONVERSATION_SELECTOR = '[data-pane="conversation"], [class*=centerCol]'
|
|
28
31
|
const SIDEBAR_ROW_SELECTOR = '[class*="sessionRow"], [class*="projectRow"], [class*="searchResultRow"], [class*="searchResultWorkspace"], [class*="newSession"]'
|
|
29
32
|
const ACTIVE_WINDOW_MS = 15 * 60 * 1000 // 最近 15 分钟有事件 → 视为进行中(归档/总览徽标用)
|
|
30
33
|
|
|
@@ -74,12 +77,13 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
74
77
|
summaryStale: '⚠ 摘要生成后有新对话,内容可能已不准确,建议重新生成',
|
|
75
78
|
llmSummary: '生成 LLM 摘要', llmGenerating: '生成中…', llmFail: '生成失败', summaryRuleTag: '规则', summaryLlmTag: 'LLM',
|
|
76
79
|
cacheHint: '仅清理本插件的缓存(可自动重建),不影响 DSH 会话存档与任何其他数据', cacheLimit: '时间线缓存上限',
|
|
77
|
-
healthActive: '活跃', healthToolWait: '工具执行中', healthQuiet: '静默中', healthStalled: '疑似卡死',
|
|
80
|
+
healthActive: '活跃', healthToolWait: '工具执行中', healthQuiet: '静默中', healthStalled: '疑似卡死', healthIdle: '空闲',
|
|
81
|
+
agoJustNow: '刚刚', agoMin: '{N} 分钟前', agoHour: '{N} 小时前', agoDay: '{N} 天前',
|
|
78
82
|
healthSilentMin: '静默 {N} 分钟', healthSilentSec: '静默 {N} 秒',
|
|
79
83
|
healthChipTitle: '会话健康状态(会话流)· 点击打开详情',
|
|
80
84
|
renameClearBlocked: '该会话标题由官方会话管理:请输入新标题(不支持清空)',
|
|
81
|
-
settingsTitle: '会话流', settingsDesc: '
|
|
82
|
-
setGroupPiano: '轮次悬浮条', setGroupLive: '实时跟踪', setGroupHealth: '健康监控',
|
|
85
|
+
settingsTitle: '会话流', settingsDesc: '调整轮次导航、实时跟踪与健康监控的行为参数。保存后立即生效(轮次悬浮条行数在重进会话页后完全生效)。',
|
|
86
|
+
setGroupPiano: '轮次悬浮条', setGroupLive: '实时跟踪', setGroupHealth: '健康监控', setGroupNav: '轮次导航(v0.1.2+)',
|
|
83
87
|
setPianoWindow: '可见行数', setPianoWindowHint: '悬浮条同时显示的轮次条数(6–18)',
|
|
84
88
|
setPianoWheelSpeed: '滚轮灵敏度', setPianoWheelSpeedHint: '每像素滚动的行数(0.005–0.03),越大滚得越快',
|
|
85
89
|
setPianoSnapMs: '静止吸附延迟(ms)', setPianoSnapMsHint: '滚轮停止多少毫秒后吸附到整行(100–400)',
|
|
@@ -87,6 +91,10 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
87
91
|
setLiveFollowPx: '吸底阈值(px)', setLiveFollowPxHint: '距底部多少像素内视为「在底部」并跟随滚动(20–120)',
|
|
88
92
|
setLiveHistoryTurns: '保留历史回合', setLiveHistoryTurnsHint: '实时模式下保留的历史回合数(0–10),下次开播生效',
|
|
89
93
|
setStallThresholdMin: '卡死阈值(分钟)', setStallThresholdMinHint: '运行中但静默超过该时长判定为疑似卡死(1–10)',
|
|
94
|
+
setRailEnhance: '官方轮次导航增强', setRailEnhanceHint: '在官方右侧轮次导航上叠加:预览卡内追加结论摘要、右键唤起工作台',
|
|
95
|
+
setPianoClassic: '经典左侧悬浮条', setPianoClassicHint: '插件自带的左侧轮次悬浮条(默认开,与官方右侧导航并存;点击未载入轮次可委托官方引擎自动加载跳转)',
|
|
96
|
+
setRailHide: '隐藏官方轮次导航', setRailHideHint: '完全屏蔽官方右侧刻度条,只保留我们的悬浮条(开启后「官方轮次导航增强」自动不生效)',
|
|
97
|
+
railMetaCalls: '{tools} 次工具', railMetaErrors: '{errors} 错误', railThinking: '含思考',
|
|
90
98
|
setSave: '保存', setDiscard: '放弃更改', setSaveFail: '部分设置未保存成功,请检查后重试',
|
|
91
99
|
setReset: '恢复默认', setOverridden: '已修改', setInvalid: '超出范围或格式无效',
|
|
92
100
|
setUnavailable: '当前环境未暴露设置服务,无法编辑(请升级 dsh 后重试)',
|
|
@@ -131,12 +139,13 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
131
139
|
summaryStale: '⚠ New conversation since summary was generated; content may be outdated — regenerate',
|
|
132
140
|
llmSummary: 'Generate LLM summary', llmGenerating: 'Generating…', llmFail: 'Failed', summaryRuleTag: 'rule', summaryLlmTag: 'LLM',
|
|
133
141
|
cacheHint: "Only this plugin's cache (auto-rebuilds); never touches DSH session archives or other data", cacheLimit: 'Timeline cache limit',
|
|
134
|
-
healthActive: 'Active', healthToolWait: 'Tool running', healthQuiet: 'Quiet', healthStalled: 'Likely stalled',
|
|
142
|
+
healthActive: 'Active', healthToolWait: 'Tool running', healthQuiet: 'Quiet', healthStalled: 'Likely stalled', healthIdle: 'Idle',
|
|
143
|
+
agoJustNow: 'just now', agoMin: '{N}m ago', agoHour: '{N}h ago', agoDay: '{N}d ago',
|
|
135
144
|
healthSilentMin: 'silent {N} min', healthSilentSec: 'silent {N} s',
|
|
136
145
|
healthChipTitle: 'Session health (Session Flow) · click to open detail',
|
|
137
146
|
renameClearBlocked: 'Title is managed by the official session: enter a new title (clearing unsupported)',
|
|
138
|
-
settingsTitle: 'Session Flow', settingsDesc: 'Tune
|
|
139
|
-
setGroupPiano: 'Turn strip', setGroupLive: 'Live tracking', setGroupHealth: 'Health monitor',
|
|
147
|
+
settingsTitle: 'Session Flow', settingsDesc: 'Tune turn navigation, live-tracking, and health-monitor behavior. Applies on save (strip row count fully applies after re-entering a session page).',
|
|
148
|
+
setGroupPiano: 'Turn strip', setGroupLive: 'Live tracking', setGroupHealth: 'Health monitor', setGroupNav: 'Turn navigation (v0.1.2+)',
|
|
140
149
|
setPianoWindow: 'Visible rows', setPianoWindowHint: 'How many turn keys the strip shows at once (6–18)',
|
|
141
150
|
setPianoWheelSpeed: 'Wheel speed', setPianoWheelSpeedHint: 'Rows scrolled per pixel (0.005–0.03); larger scrolls faster',
|
|
142
151
|
setPianoSnapMs: 'Snap delay (ms)', setPianoSnapMsHint: 'Delay after the wheel stops before snapping to a whole row (100–400)',
|
|
@@ -144,6 +153,10 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
144
153
|
setLiveFollowPx: 'Follow threshold (px)', setLiveFollowPxHint: 'Distance from the bottom that still counts as "at bottom" and follows (20–120)',
|
|
145
154
|
setLiveHistoryTurns: 'History turns kept', setLiveHistoryTurnsHint: 'History turns kept in live mode (0–10); applies to the next live session',
|
|
146
155
|
setStallThresholdMin: 'Stall threshold (min)', setStallThresholdMinHint: 'Running but silent beyond this counts as likely stalled (1–10)',
|
|
156
|
+
setRailEnhance: 'Enhance official turn navigator', setRailEnhanceHint: 'Overlay on the official right-side rail: conclusion section inside its preview card, right-click to open the workbench',
|
|
157
|
+
setPianoClassic: 'Classic left strip', setPianoClassicHint: 'The plugin\'s own left turn strip (on by default, coexists with the official right rail; clicking an unloaded turn delegates to the official auto-load navigation)',
|
|
158
|
+
setRailHide: 'Hide official turn navigator', setRailHideHint: 'Fully hide the official right-side rail and keep only our strip (the rail enhancement is inert while this is on)',
|
|
159
|
+
railMetaCalls: '{tools} tools', railMetaErrors: '{errors} errors', railThinking: 'thinking',
|
|
147
160
|
setSave: 'Save', setDiscard: 'Discard', setSaveFail: 'Some settings were not saved — check and retry',
|
|
148
161
|
setReset: 'Reset', setOverridden: 'Modified', setInvalid: 'Out of range or invalid format',
|
|
149
162
|
setUnavailable: 'The settings service is not exposed in this environment (upgrade dsh and retry)',
|
|
@@ -153,12 +166,13 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
153
166
|
// Host 注册 settings 命名空间 'session-flow'(lib/host.js SETTINGS_DEFAULTS 同源);
|
|
154
167
|
// client 经 settingsScope 读写(pet 同款:优先 webUiSettings 桥,回退 ctx.settingsScope)。
|
|
155
168
|
// 全部消费点读 sfSettings.current.xxx,缺省 = 原硬编码值——无设置时行为零变化。
|
|
156
|
-
const SETTINGS_DEFAULTS = { pianoWindow: 12, pianoWheelSpeed: 0.012, pianoSnapMs: 170, livePollMs: 3000, liveFollowPx: 40, liveHistoryTurns: 3, stallThresholdMin: 3 }
|
|
169
|
+
const SETTINGS_DEFAULTS = { pianoWindow: 12, pianoWheelSpeed: 0.012, pianoSnapMs: 170, livePollMs: 3000, liveFollowPx: 40, liveHistoryTurns: 3, stallThresholdMin: 3, railEnhance: true, railHideOfficial: false, pianoClassicStrip: true }
|
|
157
170
|
const sfSettings = { current: { ...SETTINGS_DEFAULTS } }
|
|
158
171
|
const SETTING_GROUPS = [
|
|
159
172
|
{ titleKey: 'setGroupPiano', fields: ['pianoWindow', 'pianoWheelSpeed', 'pianoSnapMs'] },
|
|
160
173
|
{ titleKey: 'setGroupLive', fields: ['livePollMs', 'liveFollowPx', 'liveHistoryTurns'] },
|
|
161
174
|
{ titleKey: 'setGroupHealth', fields: ['stallThresholdMin'] },
|
|
175
|
+
{ titleKey: 'setGroupNav', fields: ['railEnhance', 'railHideOfficial', 'pianoClassicStrip'] },
|
|
162
176
|
]
|
|
163
177
|
const SETTING_FIELD_SPECS = {
|
|
164
178
|
pianoWindow: { min: 6, max: 18, integer: true, labelKey: 'setPianoWindow', hintKey: 'setPianoWindowHint' },
|
|
@@ -168,9 +182,16 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
168
182
|
liveFollowPx: { min: 20, max: 120, integer: true, labelKey: 'setLiveFollowPx', hintKey: 'setLiveFollowPxHint' },
|
|
169
183
|
liveHistoryTurns: { min: 0, max: 10, integer: true, labelKey: 'setLiveHistoryTurns', hintKey: 'setLiveHistoryTurnsHint' },
|
|
170
184
|
stallThresholdMin: { min: 1, max: 10, integer: true, labelKey: 'setStallThresholdMin', hintKey: 'setStallThresholdMinHint' },
|
|
185
|
+
railEnhance: { kind: 'bool', labelKey: 'setRailEnhance', hintKey: 'setRailEnhanceHint' },
|
|
186
|
+
pianoClassicStrip: { kind: 'bool', labelKey: 'setPianoClassic', hintKey: 'setPianoClassicHint' },
|
|
187
|
+
railHideOfficial: { kind: 'bool', labelKey: 'setRailHide', hintKey: 'setRailHideHint' },
|
|
171
188
|
}
|
|
172
189
|
function formatSettingValue(v) { return v === undefined || v === null ? '' : String(v) }
|
|
173
190
|
function parseSettingValue(spec, text) {
|
|
191
|
+
if (spec.kind === 'bool') {
|
|
192
|
+
const t = String(text).trim()
|
|
193
|
+
return t === 'true' ? true : t === 'false' ? false : undefined
|
|
194
|
+
}
|
|
174
195
|
const n = Number(String(text).trim())
|
|
175
196
|
if (!Number.isFinite(n) || n < spec.min || n > spec.max) return undefined
|
|
176
197
|
if (spec.integer && !Number.isInteger(n)) return undefined
|
|
@@ -298,12 +319,18 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
298
319
|
field.overridden ? h('span', { className: 'sfset-overridden' }, STR.setOverridden) : null),
|
|
299
320
|
h('div', { className: 'sfset-hint' }, STR[spec.hintKey])),
|
|
300
321
|
h('div', { className: 'sfset-controls' },
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
322
|
+
spec.kind === 'bool'
|
|
323
|
+
? h('input', {
|
|
324
|
+
className: 'sfset-check', type: 'checkbox',
|
|
325
|
+
checked: field.text === 'true', disabled,
|
|
326
|
+
onChange: (e) => controller.edit(key, String(e.target.checked)),
|
|
327
|
+
})
|
|
328
|
+
: h('input', {
|
|
329
|
+
className: 'sfset-input', type: 'number',
|
|
330
|
+
min: spec.min, max: spec.max, step: spec.integer ? 1 : 0.001,
|
|
331
|
+
value: field.text, disabled,
|
|
332
|
+
onChange: (e) => controller.edit(key, e.target.value),
|
|
333
|
+
}),
|
|
307
334
|
h('button', { type: 'button', className: 'sfset-reset', disabled, onClick: () => controller.resetField(key) }, STR.setReset))))
|
|
308
335
|
}
|
|
309
336
|
}
|
|
@@ -340,6 +367,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
340
367
|
`.sfset-discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}`,
|
|
341
368
|
`.sfset-discard:disabled{opacity:.4;cursor:default}`,
|
|
342
369
|
`.sfset-error{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)}`,
|
|
370
|
+
`.sfset-check{width:16px;height:16px;accent-color:var(--dsw-alias-label-primary);cursor:pointer}`,
|
|
343
371
|
`.sfset-unavailable{font-size:13px;line-height:1.5;color:var(--dsw-alias-state-warn-primary)}`,
|
|
344
372
|
].join('\n')
|
|
345
373
|
|
|
@@ -358,6 +386,13 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
358
386
|
// 这样 top 栏(flex:none + sticky)天然固定,不会随内容滚出视口。
|
|
359
387
|
const STYLE = [
|
|
360
388
|
`[data-pane=conversation]{position:relative}`,
|
|
389
|
+
`[class*=centerCol]{position:relative}`,
|
|
390
|
+
`.sf-rail-conclusion{margin-top:8px;padding-top:8px;border-top:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.25));font-size:12px;line-height:1.5;color:var(--dsw-alias-label-primary,#222)}`,
|
|
391
|
+
`.sf-turnMeta{display:flex;flex-wrap:wrap;gap:3px 10px;font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary,#555)}`,
|
|
392
|
+
`.sf-turnMetaChip{display:inline-flex;align-items:center;gap:3px}`,
|
|
393
|
+
`.sf-turnMetaChip.err{color:var(--dsw-alias-label-error,#d43b3b)}`,
|
|
394
|
+
`.sf-ico{display:inline-flex}`,
|
|
395
|
+
`.sf-rail-conclusionTools{margin-top:3px;font-size:11px;color:var(--dsw-alias-label-tertiary,#999);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}`,
|
|
361
396
|
`${VIEW_SELECTOR}{z-index:60;background:var(--dsw-alias-bg-base,#fff);display:none!important;position:absolute;inset:0;overflow:hidden}`,
|
|
362
397
|
`html[${ACTIVE_ATTR}]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) ${VIEW_SELECTOR}{display:block!important}`,
|
|
363
398
|
`html[${ACTIVE_ATTR}]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [data-pane=conversation]>:not(${VIEW_SELECTOR}),html[${ACTIVE_ATTR}]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [class*=centerCol]>:not(${VIEW_SELECTOR}){display:none!important}`,
|
|
@@ -438,6 +473,9 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
438
473
|
`.sf-healthChip.hc-wait .sf-hcLabel{color:#a87b00}`,
|
|
439
474
|
`.sf-healthChip.hc-stall .sf-hcBar{background:#d43b3b;animation:sfBadgePulse 2.8s ease-in-out infinite}`,
|
|
440
475
|
`.sf-healthChip.hc-stall .sf-hcLabel{color:#d43b3b}`,
|
|
476
|
+
`.sf-healthChip.hc-idle .sf-hcBar{background:var(--dsw-alias-label-tertiary,#b8bcc4);opacity:.55}`,
|
|
477
|
+
`.sf-healthChip.hc-idle .sf-hcLabel{color:var(--dsw-alias-label-tertiary,#999)}`,
|
|
478
|
+
`.sf-healthChip.hc-idle:hover,.sf-healthChip.hc-idle:focus-visible{background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 72%,rgba(128,128,128,.12));border-color:color-mix(in srgb,var(--dsw-alias-border-l2,#e4e6eb) 60%,#9aa0a8)}`,
|
|
441
479
|
`.sf-statsRow{display:flex;gap:12px;flex-wrap:wrap;margin-top:3px}`,
|
|
442
480
|
`.sf-hint{color:var(--dsw-alias-label-secondary,#888);font-size:12px;padding:2px 2px 0}`,
|
|
443
481
|
// ── 工作区筛选 tabs(独立一行,醒目)──
|
|
@@ -703,6 +741,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
703
741
|
`.pk-detail{position:absolute;left:0;top:0;width:264px;max-height:calc(60vh - 40px);display:flex;flex-direction:column;gap:6px;background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 76%,transparent);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border:1px solid var(--dsw-alias-border-l2,#e4e6eb);border-radius:12px;box-shadow:0 8px 28px rgba(0,0,0,.18);padding:10px 12px;z-index:6;overflow:hidden;opacity:1;transform:translateX(0);transition:opacity .18s ease .28s,transform .24s cubic-bezier(.25,1.1,.4,1) .28s,visibility .24s .28s}`,
|
|
704
742
|
`.pk-detail.hidden{opacity:0;transform:translateX(14px);visibility:hidden;pointer-events:none;transition:opacity .18s ease,transform .24s cubic-bezier(.25,1.1,.4,1),visibility .24s}`,
|
|
705
743
|
`.pk-detailHead{flex:none;font-size:10.5px;font-weight:700;color:var(--dsw-alias-label-secondary,#666);font-family:ui-monospace,Consolas,monospace;letter-spacing:.2px}`,
|
|
744
|
+
`.pk-detailMeta{flex:none;margin:3px 0 2px}`,
|
|
706
745
|
// 面板内容随条目切换:新内容从右轻滑入(key 变化重新挂载触发 animation)。
|
|
707
746
|
`.pk-detailBody{min-height:0;font-size:12.5px;line-height:1.6;color:var(--dsw-alias-label-primary,#222);white-space:pre-wrap;word-break:break-word;overflow-y:auto;max-height:320px;animation:pkSlideIn .22s ease}`,
|
|
708
747
|
`@keyframes pkSlideIn{from{opacity:0;transform:translateX(10px)}to{opacity:1;transform:translateX(0)}}`,
|
|
@@ -875,6 +914,79 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
875
914
|
}
|
|
876
915
|
const canOfficialRename = (conn) =>
|
|
877
916
|
conn !== undefined && conn.api !== undefined && conn.api.sessions !== undefined && typeof conn.api.sessions.rename === 'function'
|
|
917
|
+
// ── v0.1.2 兼容(三):session RPC 双面适配器 ─────────────────────
|
|
918
|
+
// 新面:ctx.remote.session.*(inject 需声明 remote/remote.session,否则代理抛
|
|
919
|
+
// 「cannot get property remote.session without inject」);信封顶层 {ok,value};
|
|
920
|
+
// history 改名 page({address, maxMessages,...}) → {records, hasMore};rename/list
|
|
921
|
+
// 仍在。旧面:connection.api.sessions.*({result:{ok,value}};subagents.history)。
|
|
922
|
+
// apply 时探测(ctx.get('remote') 旧宿主抛错 → 回退旧面)。
|
|
923
|
+
const sfRemoteRef = { current: null }
|
|
924
|
+
const sfBuildRemote = (ctx, connection) => {
|
|
925
|
+
try {
|
|
926
|
+
const remote = ctx && typeof ctx.get === 'function' ? ctx.get('remote') : null
|
|
927
|
+
const sess = remote && remote.session
|
|
928
|
+
if (sess && typeof sess.page === 'function') {
|
|
929
|
+
// 宿主硬校验:throughSeq > 日志 cursor 直接 gateway/bad-request——不存在
|
|
930
|
+
// 「取最新」的大数哨兵。cursor 来源 = list items[].projections.asOfSeq
|
|
931
|
+
//(投影截至 seq,恒 ≤ 真实日志 cursor,安全)。2s TTL 缓存防探测并发打爆 list。
|
|
932
|
+
let listCache = null
|
|
933
|
+
const cursorOf = async (sessionId) => {
|
|
934
|
+
const now = Date.now()
|
|
935
|
+
if (listCache === null || now - listCache.at > 2000) {
|
|
936
|
+
const r = await sess.list({})
|
|
937
|
+
listCache = { at: now, items: r && r.ok && r.value && Array.isArray(r.value.items) ? r.value.items : [] }
|
|
938
|
+
}
|
|
939
|
+
const it = listCache.items.find((x) => x.sessionId === sessionId)
|
|
940
|
+
return it && it.projections && typeof it.projections.asOfSeq === 'number' ? it.projections.asOfSeq : -1
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
kind: 'remote',
|
|
944
|
+
async pageBySession(sessionId, maxMessages) {
|
|
945
|
+
const throughSeq = await cursorOf(sessionId)
|
|
946
|
+
if (throughSeq < 0) return undefined
|
|
947
|
+
const r = await sess.page({ address: { kind: 'session', sessionId }, throughSeq, maxMessages })
|
|
948
|
+
return r && r.ok ? r.value : undefined
|
|
949
|
+
},
|
|
950
|
+
async pageBySub(parentSessionId, childSessionId, mode, maxMessages) {
|
|
951
|
+
const throughSeq = await cursorOf(childSessionId)
|
|
952
|
+
if (throughSeq < 0) return undefined
|
|
953
|
+
const r = await sess.page({ address: { kind: 'subagent', parentSessionId, childSessionId, mode }, throughSeq, maxMessages })
|
|
954
|
+
return r && r.ok ? r.value : undefined
|
|
955
|
+
},
|
|
956
|
+
async listSessions() { if (typeof sess.list !== 'function') return undefined; const r = await sess.list({}); return r && r.ok ? r.value : undefined },
|
|
957
|
+
canRename: typeof sess.rename === 'function',
|
|
958
|
+
async rename(sessionId, title) { const r = await sess.rename({ sessionId, title }); return r && r.ok && r.value && typeof r.value.title === 'string' ? r.value.title : undefined },
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
} catch (e) {}
|
|
962
|
+
if (connection && connection.api && connection.api.sessions && typeof connection.api.sessions.history === 'function') {
|
|
963
|
+
const envVal = (r) => (r && r.result && r.result.ok ? r.result.value : (r && r.ok ? r.value : undefined))
|
|
964
|
+
return {
|
|
965
|
+
kind: 'legacy',
|
|
966
|
+
async pageBySession(sessionId, maxMessages) { return envVal(await connection.api.sessions.history({ sessionId, maxMessages })) },
|
|
967
|
+
async pageBySub(parentSessionId, childSessionId, mode, maxMessages) { return envVal(await connection.api.subagents.history({ parentSessionId, childSessionId, mode, maxMessages })) },
|
|
968
|
+
async listSessions() { return envVal(await connection.api.sessions.list({})) },
|
|
969
|
+
canRename: canOfficialRename(connection),
|
|
970
|
+
async rename(sessionId, title) { const t = await renameViaOfficial(connection, sessionId, title).catch(() => undefined); return t },
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return null
|
|
974
|
+
}
|
|
975
|
+
// v0.1.2 兼容(一):sessions.history 返回值从 {events} 改为 {records, hasMore}
|
|
976
|
+
//(记录 = {type:'event', event} 或 chunk 行 {type:'chunks', event:{type:'chunkrow/…'}})。
|
|
977
|
+
// 旧版 {events} 每项亦可能包 {event}。统一解析为事件数组;丢弃 chunkrow 合成
|
|
978
|
+
// 事件(derive 只认真实事件类型,混入会污染分类)。全部 6 个消费点共用。
|
|
979
|
+
const historyEventsOf = (val) => {
|
|
980
|
+
if (!val) return []
|
|
981
|
+
const list = Array.isArray(val.events) ? val.events : (Array.isArray(val.records) ? val.records : [])
|
|
982
|
+
const out = []
|
|
983
|
+
for (const r of list) {
|
|
984
|
+
const e = (r && r.event) || r
|
|
985
|
+
if (!e || typeof e.type !== 'string' || e.type.startsWith('chunkrow/')) continue
|
|
986
|
+
out.push(e)
|
|
987
|
+
}
|
|
988
|
+
return out
|
|
989
|
+
}
|
|
878
990
|
/** 静默时长文本({N} 占位)。 */
|
|
879
991
|
const fmtSilent = (idleMs) => {
|
|
880
992
|
if (idleMs === null || idleMs === undefined) return ''
|
|
@@ -883,6 +995,16 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
883
995
|
? STR.healthSilentMin.replace('{N}', String(min))
|
|
884
996
|
: STR.healthSilentSec.replace('{N}', String(Math.max(1, Math.round(idleMs / 1000))))
|
|
885
997
|
}
|
|
998
|
+
/** 相对时间文本(空闲胶囊「N 分钟前」;分钟粒度足够,不追秒级新鲜度)。 */
|
|
999
|
+
const fmtAgo = (ms) => {
|
|
1000
|
+
if (!ms || ms < 0) return ''
|
|
1001
|
+
const min = Math.floor(ms / 60000)
|
|
1002
|
+
if (min < 1) return STR.agoJustNow
|
|
1003
|
+
if (min < 60) return STR.agoMin.replace('{N}', String(min))
|
|
1004
|
+
const h = Math.floor(min / 60)
|
|
1005
|
+
if (h < 24) return STR.agoHour.replace('{N}', String(h))
|
|
1006
|
+
return STR.agoDay.replace('{N}', String(Math.floor(h / 24)))
|
|
1007
|
+
}
|
|
886
1008
|
/** 健康徽标(详情实时条 / 总览卡片共用);active/ended/unknown 不渲染(已有进行中/已结束徽标)。 */
|
|
887
1009
|
const healthBadge = (health) => {
|
|
888
1010
|
if (!health) return null
|
|
@@ -893,24 +1015,41 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
893
1015
|
return null
|
|
894
1016
|
}
|
|
895
1017
|
|
|
896
|
-
// ──
|
|
1018
|
+
// ── 会话页头部健康胶囊(官方 conversation.session.header.actions 槽位)──
|
|
897
1019
|
// 位置:官方模式标识(agent-preset label, order -10)右侧(order -9)。
|
|
898
|
-
//
|
|
899
|
-
//
|
|
1020
|
+
// 常驻四态(用户需求 2026-09-05:非运行也展示):
|
|
1021
|
+
// 空闲(灰条 +「N 分钟前」相对时间,零 RPC,来自 byId.updatedAt)
|
|
1022
|
+
// 运行·活跃(绿)/ 运行·工具执行中·静默中(黄)/ 运行·疑似卡死(红,脉冲)
|
|
1023
|
+
// —— 运行时 10s 轮询 tail history → host derive 健康分类;probe 未返回前
|
|
1024
|
+
// 乐观显示「活跃」(避免胶囊闪烁)。空白新会话不显示。
|
|
900
1025
|
// 点击:唤起会话流工作台并直达该会话详情(workbenchBridge 意图桥)。
|
|
1026
|
+
// 槽位 props 兜底:v0.1.2 若 sessionId 标准 prop 管道变化,退回 sessions.list
|
|
1027
|
+
// 快照的 current(头部胶囊面对的就是当前会话)。
|
|
1028
|
+
const currentSessionIdOf = (sessions) => {
|
|
1029
|
+
try {
|
|
1030
|
+
const snap = sessions && sessions.list && typeof sessions.list.getSnapshot === 'function' ? sessions.list.getSnapshot() : null
|
|
1031
|
+
return snap && snap.current ? snap.current : undefined
|
|
1032
|
+
} catch (e) { return undefined }
|
|
1033
|
+
}
|
|
1034
|
+
// 【临时诊断】chipDbg 已于 v1.3.0 验收后撤除;probe error 静默(与总览探测同口径)。
|
|
901
1035
|
function SessionHealthChip(props) {
|
|
902
|
-
const {
|
|
1036
|
+
const { connection, sessions } = props
|
|
1037
|
+
const sid = props.sessionId || currentSessionIdOf(sessions)
|
|
1038
|
+
const sessionId = sid
|
|
903
1039
|
const [running, setRunning] = useState(false)
|
|
1040
|
+
const [meta, setMeta] = useState({ updatedAt: 0, blank: false })
|
|
904
1041
|
const [health, setHealth] = useState(null)
|
|
905
1042
|
useEffect(() => {
|
|
906
1043
|
if (!sessionId || !sessions || !sessions.list || typeof sessions.list.getSnapshot !== 'function') return undefined
|
|
907
1044
|
const compute = () => {
|
|
908
1045
|
try {
|
|
909
|
-
// sessions.list 快照 = { ids, current, byId }(byId[id] 含 running
|
|
910
|
-
// 不是 { items }(那是 workspace 浏览器用的另一个视图快照,曾误用导致芯片恒 null)。
|
|
1046
|
+
// sessions.list 快照 = { ids, current, byId }(byId[id] 含 running/updatedAt/blank)。
|
|
911
1047
|
const snap = sessions.list.getSnapshot()
|
|
912
1048
|
const it = snap && snap.byId ? snap.byId[sessionId] : undefined
|
|
913
1049
|
setRunning(it ? it.running === true : false)
|
|
1050
|
+
const u = it && typeof it.updatedAt === 'number' ? it.updatedAt : 0
|
|
1051
|
+
const b = it ? it.blank === true : false
|
|
1052
|
+
setMeta((prev) => (prev.updatedAt === u && prev.blank === b ? prev : { updatedAt: u, blank: b }))
|
|
914
1053
|
} catch (e) { setRunning(false) }
|
|
915
1054
|
}
|
|
916
1055
|
compute()
|
|
@@ -918,13 +1057,12 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
918
1057
|
return undefined
|
|
919
1058
|
}, [sessions, sessionId])
|
|
920
1059
|
useEffect(() => {
|
|
921
|
-
if (!running || !sessionId || !
|
|
1060
|
+
if (!running || !sessionId || !sfRemoteRef.current) { setHealth(null); return undefined }
|
|
922
1061
|
let alive = true
|
|
923
1062
|
const probe = async () => {
|
|
924
1063
|
try {
|
|
925
|
-
const
|
|
926
|
-
const
|
|
927
|
-
const events = val && Array.isArray(val.events) ? val.events.map((e) => (e && e.event) || e) : []
|
|
1064
|
+
const val = await sfRemoteRef.current.pageBySession(sessionId, 3)
|
|
1065
|
+
const events = historyEventsOf(val)
|
|
928
1066
|
if (events.length === 0) { if (alive) setHealth(null); return }
|
|
929
1067
|
const d = await api('derive', { events, now: Date.now(), assumeRunning: true })
|
|
930
1068
|
if (alive && d && d.ok) setHealth(d.health || null)
|
|
@@ -934,22 +1072,61 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
934
1072
|
const iv = setInterval(probe, 10000)
|
|
935
1073
|
return () => { alive = false; clearInterval(iv) }
|
|
936
1074
|
}, [running, sessionId, connection])
|
|
937
|
-
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1075
|
+
// 空闲态每 30s 轻 tick 一次,驱动「N 分钟前」相对时间保鲜。
|
|
1076
|
+
const [, setTick] = useState(0)
|
|
1077
|
+
useEffect(() => {
|
|
1078
|
+
if (running) return undefined
|
|
1079
|
+
const iv = setInterval(() => setTick((n) => n + 1), 30000)
|
|
1080
|
+
return () => clearInterval(iv)
|
|
1081
|
+
}, [running])
|
|
1082
|
+
// 空闲计时基准 = 最后一条事件的 time(= 会话结束时刻)。byId.updatedAt 是
|
|
1083
|
+
// 「列表投影上次变化」时间戳——运行中投影值不变就停在上次用户消息,语义不对
|
|
1084
|
+
//(用户实测裁定)。空闲期间该时刻不再变化 → 会话切入空闲时拉一次即可。
|
|
1085
|
+
const [idleSince, setIdleSince] = useState(0)
|
|
1086
|
+
useEffect(() => {
|
|
1087
|
+
if (running || !sessionId || !sfRemoteRef.current) { setIdleSince(0); return undefined }
|
|
1088
|
+
let alive = true
|
|
1089
|
+
;(async () => {
|
|
1090
|
+
try {
|
|
1091
|
+
const val = await sfRemoteRef.current.pageBySession(sessionId, 3)
|
|
1092
|
+
const events = historyEventsOf(val)
|
|
1093
|
+
const last = events.length > 0 ? events[events.length - 1] : null
|
|
1094
|
+
const t = last && typeof last.time === 'number' ? last.time : 0
|
|
1095
|
+
if (alive) setIdleSince(t)
|
|
1096
|
+
} catch (e) { if (alive) setIdleSince(0) }
|
|
1097
|
+
})()
|
|
1098
|
+
return () => { alive = false }
|
|
1099
|
+
}, [running, sessionId])
|
|
1100
|
+
if (!sessionId) return null
|
|
1101
|
+
// 空闲常驻态:灰条 + 「距最后事件」相对时间。空白新会话不显示(避免噪音)。
|
|
1102
|
+
if (!running) {
|
|
1103
|
+
if (meta.blank) return null
|
|
1104
|
+
const ago = idleSince > 0 ? fmtAgo(Date.now() - idleSince) : ''
|
|
1105
|
+
return h('button', {
|
|
1106
|
+
className: 'sf-healthChip hc-idle',
|
|
1107
|
+
title: STR.healthChipTitle,
|
|
1108
|
+
onClick: (e) => { e.stopPropagation(); if (props.onOpen) props.onOpen() },
|
|
1109
|
+
},
|
|
1110
|
+
h('span', { className: 'sf-hcBar' }),
|
|
1111
|
+
h('span', { className: 'sf-hcLabel' }, STR.healthIdle + (ago ? ' · ' + ago : '')),
|
|
1112
|
+
)
|
|
1113
|
+
}
|
|
1114
|
+
// 运行态:probe 未返回前乐观显示「活跃」(防闪烁);derive ended/unknown 亦按活跃渲染。
|
|
1115
|
+
let text = STR.healthActive, cls = 'hc-active', silent = ''
|
|
1116
|
+
if (health) {
|
|
1117
|
+
const kind = health.kind
|
|
1118
|
+
silent = fmtSilent(health.idleMs)
|
|
1119
|
+
if (kind === 'tool-wait') { text = STR.healthToolWait; cls = 'hc-wait' }
|
|
1120
|
+
else if (kind === 'quiet') { text = STR.healthQuiet; cls = 'hc-wait' }
|
|
1121
|
+
else if (kind === 'stalled') { text = STR.healthStalled; cls = 'hc-stall' }
|
|
1122
|
+
}
|
|
946
1123
|
return h('button', {
|
|
947
1124
|
className: 'sf-healthChip ' + cls,
|
|
948
1125
|
title: STR.healthChipTitle,
|
|
949
1126
|
onClick: (e) => { e.stopPropagation(); if (props.onOpen) props.onOpen() },
|
|
950
1127
|
},
|
|
951
1128
|
h('span', { className: 'sf-hcBar' }),
|
|
952
|
-
h('span', { className: 'sf-hcLabel' }, text + (silent &&
|
|
1129
|
+
h('span', { className: 'sf-hcLabel' }, text + (silent && cls !== 'hc-active' ? ' · ' + silent : '')),
|
|
953
1130
|
)
|
|
954
1131
|
}
|
|
955
1132
|
|
|
@@ -1015,8 +1192,9 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1015
1192
|
const target = all.find((x) => x.id === sid)
|
|
1016
1193
|
if (target && target.titleSource === 'user') return Promise.reject(new Error(STR.renameClearBlocked))
|
|
1017
1194
|
}
|
|
1018
|
-
if (trimmed !== '' &&
|
|
1019
|
-
return
|
|
1195
|
+
if (trimmed !== '' && sfRemoteRef.current && sfRemoteRef.current.canRename) {
|
|
1196
|
+
return sfRemoteRef.current.rename(sid, trimmed)
|
|
1197
|
+
.then((accepted) => { if (typeof accepted !== 'string') throw new Error(STR.renameFail); return accepted })
|
|
1020
1198
|
.then((accepted) => applyLocal(accepted))
|
|
1021
1199
|
.catch(() => legacy())
|
|
1022
1200
|
}
|
|
@@ -1028,16 +1206,15 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1028
1206
|
const [healthMap, setHealthMap] = useState({})
|
|
1029
1207
|
const [liveRunning, setLiveRunning] = useState(null) // null=未探测;Set<sessionId>
|
|
1030
1208
|
useEffect(() => {
|
|
1031
|
-
|
|
1032
|
-
if (!canOfficialRename(conn) || state.phase !== 'ready') return undefined
|
|
1209
|
+
if (!sfRemoteRef.current || state.phase !== 'ready') return undefined
|
|
1033
1210
|
let alive = true
|
|
1034
1211
|
let probing = false
|
|
1035
1212
|
const probe = async () => {
|
|
1036
1213
|
if (probing) return
|
|
1037
1214
|
probing = true
|
|
1038
1215
|
try {
|
|
1039
|
-
const
|
|
1040
|
-
const items = (
|
|
1216
|
+
const val = await sfRemoteRef.current.listSessions()
|
|
1217
|
+
const items = (val && val.items) || []
|
|
1041
1218
|
const runningIds = items.filter((i) => i.running === true).map((i) => i.sessionId)
|
|
1042
1219
|
if (!alive) return
|
|
1043
1220
|
setLiveRunning(new Set(runningIds))
|
|
@@ -1045,9 +1222,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1045
1222
|
for (let i = 0; i < Math.min(runningIds.length, 8); i += 3) {
|
|
1046
1223
|
await Promise.all(runningIds.slice(i, i + 3).map(async (sid) => {
|
|
1047
1224
|
try {
|
|
1048
|
-
const
|
|
1049
|
-
const val = hr && hr.result && hr.result.value
|
|
1050
|
-
const events = val && Array.isArray(val.events) ? val.events.map((e) => (e && e.event) || e) : []
|
|
1225
|
+
const events = historyEventsOf(await sfRemoteRef.current.pageBySession(sid, 3))
|
|
1051
1226
|
if (events.length === 0) return
|
|
1052
1227
|
const d = await api('derive', { events, now: Date.now(), assumeRunning: true })
|
|
1053
1228
|
if (d && d.ok && d.health) next[sid] = d.health
|
|
@@ -1721,16 +1896,12 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1721
1896
|
}
|
|
1722
1897
|
} catch (e) {}
|
|
1723
1898
|
// 2) 运行时 history 桥接(在线子代理)→ host derive 得 light 结构
|
|
1724
|
-
if (
|
|
1899
|
+
if (node.parentId !== undefined && sfRemoteRef.current) {
|
|
1725
1900
|
try {
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
maxMessages: 120,
|
|
1731
|
-
})
|
|
1732
|
-
if (resp && resp.result && resp.result.ok && resp.result.value && Array.isArray(resp.result.value.events)) {
|
|
1733
|
-
const events = resp.result.value.events.map((entry) => entry.event)
|
|
1901
|
+
const subVal = await sfRemoteRef.current.pageBySub(node.parentId, node.id, node.mode, 120)
|
|
1902
|
+
const subEvents = historyEventsOf(subVal)
|
|
1903
|
+
if (subEvents.length > 0) {
|
|
1904
|
+
const events = subEvents
|
|
1734
1905
|
const derived = await api('derive', { events })
|
|
1735
1906
|
if (derived && derived.ok) {
|
|
1736
1907
|
// derive 返回完整 timeline——转成 light 结构
|
|
@@ -1848,8 +2019,9 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1848
2019
|
})
|
|
1849
2020
|
const trimmed = String(title || '').trim()
|
|
1850
2021
|
if (trimmed === '' && session.titleSource === 'user') return Promise.reject(new Error(STR.renameClearBlocked))
|
|
1851
|
-
if (trimmed !== '' &&
|
|
1852
|
-
return
|
|
2022
|
+
if (trimmed !== '' && sfRemoteRef.current && sfRemoteRef.current.canRename) {
|
|
2023
|
+
return sfRemoteRef.current.rename(session.id, trimmed)
|
|
2024
|
+
.then((accepted) => { if (typeof accepted !== 'string') throw new Error(STR.renameFail); return accepted })
|
|
1853
2025
|
.then((accepted) => { setTitleOverride(accepted); setRenaming(false); return true })
|
|
1854
2026
|
.catch(() => legacy())
|
|
1855
2027
|
}
|
|
@@ -1990,18 +2162,13 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1990
2162
|
}
|
|
1991
2163
|
setLiveState((s) => ({ ...s, phase: 'loading' }))
|
|
1992
2164
|
try {
|
|
1993
|
-
//
|
|
1994
|
-
const
|
|
1995
|
-
|
|
1996
|
-
mode: 'full',
|
|
1997
|
-
maxMessages: 400,
|
|
1998
|
-
})
|
|
1999
|
-
const val = resp && resp.result && resp.result.value
|
|
2000
|
-
if (val === undefined || !Array.isArray(val.events)) {
|
|
2165
|
+
// v0.1.2:session RPC 经双面适配器(remote.session.page / 旧 history)。
|
|
2166
|
+
const val = sfRemoteRef.current ? await sfRemoteRef.current.pageBySession(session.id, 400) : undefined
|
|
2167
|
+
if (val === undefined) {
|
|
2001
2168
|
setLiveState((s) => ({ ...s, phase: 'error', error: STR.liveFail }))
|
|
2002
2169
|
return
|
|
2003
2170
|
}
|
|
2004
|
-
const events = val
|
|
2171
|
+
const events = historyEventsOf(val)
|
|
2005
2172
|
const derived = await api('derive', { events, now: Date.now() })
|
|
2006
2173
|
if (!derived || !derived.ok) {
|
|
2007
2174
|
setLiveState((s) => ({ ...s, phase: 'error', error: (derived && derived.error) || STR.liveFail }))
|
|
@@ -2638,19 +2805,18 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2638
2805
|
|
|
2639
2806
|
const fetchLive = useRef(async () => {})
|
|
2640
2807
|
fetchLive.current = async () => {
|
|
2641
|
-
if (
|
|
2808
|
+
if (!sfRemoteRef.current) {
|
|
2642
2809
|
setState((s) => ({ ...s, phase: 'error', error: STR.liveUnavailable }))
|
|
2643
2810
|
return
|
|
2644
2811
|
}
|
|
2645
2812
|
setState((s) => ({ ...s, phase: s.timeline === null ? 'loading' : 'ready' }))
|
|
2646
2813
|
try {
|
|
2647
|
-
const
|
|
2648
|
-
|
|
2649
|
-
if (val === undefined || !Array.isArray(val.events)) {
|
|
2814
|
+
const val = await sfRemoteRef.current.pageBySession(sessionId, 400)
|
|
2815
|
+
if (val === undefined) {
|
|
2650
2816
|
setState((s) => ({ ...s, phase: 'error', error: STR.liveFail }))
|
|
2651
2817
|
return
|
|
2652
2818
|
}
|
|
2653
|
-
const events = val
|
|
2819
|
+
const events = historyEventsOf(val)
|
|
2654
2820
|
const derived = await api('derive', { events, now: Date.now() })
|
|
2655
2821
|
if (!derived || !derived.ok) {
|
|
2656
2822
|
setState((s) => ({ ...s, phase: 'error', error: (derived && derived.error) || STR.liveFail }))
|
|
@@ -2741,6 +2907,33 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2741
2907
|
})()
|
|
2742
2908
|
return () => ac.abort()
|
|
2743
2909
|
}, [props.connection])
|
|
2910
|
+
// v0.1.2 起 events.mux 消费面移除 → 改用 sessions.list 订阅:官方 rename 后
|
|
2911
|
+
// 列表行 byId[sessionId].title 由 title projection 即时更新(新控制器
|
|
2912
|
+
// dsh-api-session-controller rename 后 projections.apply('title') 即落列表),
|
|
2913
|
+
// 订阅回调秒级收集全量标题快照,浅比较防抖。与 mux 路径并存(旧版 dsh 仍走 mux)。
|
|
2914
|
+
useEffect(() => {
|
|
2915
|
+
const svc = props.sessions
|
|
2916
|
+
const list = svc && svc.list
|
|
2917
|
+
if (!list || typeof list.getSnapshot !== 'function' || typeof list.subscribe !== 'function') return undefined
|
|
2918
|
+
const collect = () => {
|
|
2919
|
+
const snap = list.getSnapshot()
|
|
2920
|
+
const byId = snap && snap.byId
|
|
2921
|
+
if (!byId) return
|
|
2922
|
+
const map = {}
|
|
2923
|
+
for (const id of Object.keys(byId)) {
|
|
2924
|
+
const t = byId[id] && byId[id].title
|
|
2925
|
+
if (typeof t === 'string' && t !== '') map[id] = t
|
|
2926
|
+
}
|
|
2927
|
+
setLiveTitles((prev) => {
|
|
2928
|
+
const pk = Object.keys(prev)
|
|
2929
|
+
const nk = Object.keys(map)
|
|
2930
|
+
if (pk.length === nk.length && nk.every((k) => prev[k] === map[k])) return prev
|
|
2931
|
+
return map
|
|
2932
|
+
})
|
|
2933
|
+
}
|
|
2934
|
+
collect()
|
|
2935
|
+
return list.subscribe(collect)
|
|
2936
|
+
}, [props.sessions])
|
|
2744
2937
|
|
|
2745
2938
|
return detail === null
|
|
2746
2939
|
? h(SessionFlowOverview, {
|
|
@@ -2826,17 +3019,74 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2826
3019
|
return undefined
|
|
2827
3020
|
}
|
|
2828
3021
|
|
|
3022
|
+
// ── 共享:纯色矢量图标 + 轮次 meta chips(rail 增强层与钢琴键详情框共用)──
|
|
3023
|
+
// stroke=currentColor 随主题取色;不用 emoji(用户裁定 2026-09-05)。
|
|
3024
|
+
const SF_ICON_PATHS = {
|
|
3025
|
+
clock: '<circle cx="8" cy="8" r="6.3"/><path d="M8 4.6V8l2.4 1.5"/>',
|
|
3026
|
+
tool: '<rect x="2.4" y="3" width="11.2" height="10" rx="1.6"/><path d="M5 7l2.2 2L5 11M8.6 11h2.6"/>',
|
|
3027
|
+
alert: '<circle cx="8" cy="8" r="6.3"/><path d="M8 5.2v3.2M8 11.2h.01"/>',
|
|
3028
|
+
think: '<path d="M8 2.2a4 4 0 0 0-4 4c0 1.7.9 2.7 1.6 3.5.3.4.4 1.3.4 1.3h4s.1-.9.4-1.3c.7-.8 1.6-1.8 1.6-3.5a4 4 0 0 0-4-4z"/><path d="M6.4 13.6h3.2"/>',
|
|
3029
|
+
}
|
|
3030
|
+
const sfIcon = (name) => {
|
|
3031
|
+
const span = document.createElement('span')
|
|
3032
|
+
span.className = 'sf-ico'
|
|
3033
|
+
span.innerHTML = '<svg viewBox="0 0 16 16" width="11" height="11" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">' + SF_ICON_PATHS[name] + '</svg>'
|
|
3034
|
+
return span
|
|
3035
|
+
}
|
|
3036
|
+
// 时长格式化(ms → 42s / 2m 14s;与 host 导出 fmtDur 同风格)。
|
|
3037
|
+
const sfFmtDur = (ms) => {
|
|
3038
|
+
if (!ms || ms <= 0) return ''
|
|
3039
|
+
const s = Math.round(ms / 1000)
|
|
3040
|
+
if (s < 60) return s + 's'
|
|
3041
|
+
const m = Math.floor(s / 60)
|
|
3042
|
+
return s % 60 === 0 ? m + 'm' : m + 'm ' + (s % 60) + 's'
|
|
3043
|
+
}
|
|
3044
|
+
// meta chips:耗时 / 工具调用 / 错误(红)/ 思考标记。无任何信息时返回 null。
|
|
3045
|
+
const buildTurnMeta = (entry) => {
|
|
3046
|
+
const meta = document.createElement('div')
|
|
3047
|
+
meta.className = 'sf-turnMeta'
|
|
3048
|
+
const chip = (icon, text, cls) => {
|
|
3049
|
+
const c = document.createElement('span')
|
|
3050
|
+
c.className = 'sf-turnMetaChip' + (cls ? ' ' + cls : '')
|
|
3051
|
+
c.appendChild(sfIcon(icon))
|
|
3052
|
+
const t = document.createElement('span')
|
|
3053
|
+
t.textContent = text
|
|
3054
|
+
c.appendChild(t)
|
|
3055
|
+
meta.appendChild(c)
|
|
3056
|
+
}
|
|
3057
|
+
if (entry.durMs > 0) chip('clock', sfFmtDur(entry.durMs))
|
|
3058
|
+
if (entry.tools > 0) chip('tool', STR.railMetaCalls.replace('{tools}', String(entry.tools)))
|
|
3059
|
+
if (entry.errors > 0) chip('alert', STR.railMetaErrors.replace('{errors}', String(entry.errors)), 'err')
|
|
3060
|
+
if (entry.thinking) chip('think', STR.railThinking)
|
|
3061
|
+
return meta.childNodes.length > 0 ? meta : null
|
|
3062
|
+
}
|
|
3063
|
+
// lightTurn → 共享字段(tools/errors/durMs/thinking/conclusion)。
|
|
3064
|
+
const ltMeta = (lt) => ({
|
|
3065
|
+
tools: lt.toolCount || 0,
|
|
3066
|
+
errors: lt.errorCount || 0,
|
|
3067
|
+
durMs: (typeof lt.startTime === 'number' && typeof lt.endTime === 'number' && lt.endTime > lt.startTime) ? lt.endTime - lt.startTime : 0,
|
|
3068
|
+
thinking: lt.hasThinking === true,
|
|
3069
|
+
})
|
|
3070
|
+
// getTurn → 工具分布文本(top4:pwsh ×6 · edit ×3);共享给 rail 与钢琴键。
|
|
3071
|
+
const toolDistOf = (turn) => {
|
|
3072
|
+
const counts = new Map()
|
|
3073
|
+
for (const st of (turn && turn.steps) || []) for (const c of st.toolCalls || []) counts.set(c.name, (counts.get(c.name) || 0) + 1)
|
|
3074
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 4).map(([n, c]) => n + ' ×' + c).join(' · ')
|
|
3075
|
+
}
|
|
3076
|
+
|
|
2829
3077
|
// ── M11:本会话轮次导航(会话页左侧常驻悬浮条目条,v3)──────────
|
|
2830
3078
|
// 范围:只显示【当前会话】的对话轮次(用户消息),不跨会话/不跨工作区。
|
|
2831
|
-
// 数据:官方消息行锚点 DOM 驱动(data-
|
|
2832
|
-
//
|
|
3079
|
+
// 数据:官方消息行锚点 DOM 驱动(v0.1.2 起 [data-chat-flow] [data-chat-flow-kind=user],
|
|
3080
|
+
// 旧版回退 data-time-hover-root;零 RPC 实时)。
|
|
2833
3081
|
// 轮次 → 结论预览:按顺序索引映射 host get 的 lightTurns(用户消息摊平)。
|
|
2834
3082
|
// 交互:点击滚动定位该轮、active 跟随视口第一条、窗口裁剪 + 滚轮/▲▼、
|
|
2835
3083
|
// 键盘 ↑↓/Enter、hover 预览卡(该轮用户消息 + 结论)、右键唤起会话流工作台。
|
|
2836
3084
|
// 样式:DeepSeek 网页版右侧悬浮消息导航风格(圆角卡片 + 单行截断文字),移到左侧。
|
|
2837
3085
|
function TurnKeys(props) {
|
|
2838
3086
|
const { sessions, onOpenWorkbench } = props
|
|
2839
|
-
const [entries, setEntries] = useState([]) // [{turn, seq, preview}] host 全量用户轮次
|
|
3087
|
+
const [entries, setEntries] = useState([]) // [{turn, seq, preview, tools, errors, durMs, thinking}] host 全量用户轮次
|
|
3088
|
+
const distCache = useRef(new Map()) // turn → 工具分布文本(getTurn 懒加载缓存)
|
|
3089
|
+
const [, setDistTick] = useState(0) // distCache 到位后触发详情框重渲染
|
|
2840
3090
|
const [activeIdx, setActiveIdx] = useState(-1)
|
|
2841
3091
|
// T5 平滑滚动:连续滚动位置(浮点,单位=行)。替代原整数 winOffset 的「每格硬跳
|
|
2842
3092
|
// 1 条」——滚轮增量累积(deltaMode 归一)+ 列表 transform 百分比位移(transition
|
|
@@ -2918,7 +3168,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2918
3168
|
const flat = []
|
|
2919
3169
|
for (const lt of json.lightTurns || []) {
|
|
2920
3170
|
for (const u of lt.userMessages || []) {
|
|
2921
|
-
flat.push({ turn: lt.turn, seq: u.seq, preview: u.preview })
|
|
3171
|
+
flat.push({ turn: lt.turn, seq: u.seq, preview: u.preview, ...ltMeta(lt) })
|
|
2922
3172
|
}
|
|
2923
3173
|
}
|
|
2924
3174
|
// 防御:同一 seq 只保留一条(避免同一条消息重复渲染 = 「最后一条显示两次」)。
|
|
@@ -2962,10 +3212,18 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2962
3212
|
return () => { alive = false; if (iv) clearInterval(iv); if (flowObs) flowObs.disconnect() }
|
|
2963
3213
|
}, [currentId])
|
|
2964
3214
|
|
|
2965
|
-
// DOM
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
3215
|
+
// DOM 行工具:用户消息行锚点。v0.1.2 起官方会话流重构进 dsh-client-ui-chat:
|
|
3216
|
+
// data-time-hover-root 移除,行结构 = [data-chat-flow] > [data-chat-flow-key]
|
|
3217
|
+
// (data-chat-flow-kind 区分 user/steering/turn-process/…,官方内部查行同款
|
|
3218
|
+
// 选择器);旧版 dsh 回退 data-time-hover-root 锚点(dsh-navbar 同款机制)。
|
|
3219
|
+
// 零 RPC 实时。steering(随转消息)不算新轮次,不计入。
|
|
3220
|
+
const domRows = () => {
|
|
3221
|
+
const next = document.querySelectorAll('[data-chat-flow] [data-chat-flow-kind="user"]')
|
|
3222
|
+
if (next.length > 0) return [...next]
|
|
3223
|
+
return [...document.querySelectorAll('[data-time-hover-root]')]
|
|
3224
|
+
.filter((r) => !r.hasAttribute('data-pending-steering'))
|
|
3225
|
+
.filter((r) => !r.hasAttribute('data-turn-tail') && r.querySelector('[class*=bubble]') !== null)
|
|
3226
|
+
}
|
|
2969
3227
|
const textOf = (row) => {
|
|
2970
3228
|
const b = row.querySelector('[class*=bubble]')
|
|
2971
3229
|
return b ? String(b.textContent || '').replace(/\s+/g, ' ').trim() : ''
|
|
@@ -3010,16 +3268,74 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
3010
3268
|
return null
|
|
3011
3269
|
}
|
|
3012
3270
|
|
|
3271
|
+
// 未加载轮次 → 委托官方轮次导航引擎(v0.1.2+ 吸收官方独有能力):合成 click
|
|
3272
|
+
// 命中目标刻度,官方 navigate 自动加载未载入页并跳转(其引擎含 busy 态/加载锚定,
|
|
3273
|
+
// 优于我们的「只提示」)。合成事件 clientY 按官方 itemAtPointer 几何反解
|
|
3274
|
+
// (offset = clientY - rect.top + scrollTop - 6 = idx*10);官方处理挂在 nav 的
|
|
3275
|
+
// onClick 且只读 event.clientY,与事件落在哪个子元素无关(刻度按钮
|
|
3276
|
+
// pointer-events:none,不能直接 btn.click()——clientY=0 会误算成第一条)。
|
|
3277
|
+
const delegateToRail = (entry) => {
|
|
3278
|
+
try {
|
|
3279
|
+
const nav = document.querySelector(RAIL_SELECTOR)
|
|
3280
|
+
if (!nav) return false
|
|
3281
|
+
const turns = [...nav.querySelectorAll('button[aria-label]')]
|
|
3282
|
+
.map((b) => { const m = /(\d+)/.exec(b.getAttribute('aria-label') || ''); return m ? Number(m[1]) : null })
|
|
3283
|
+
const idx = turns.indexOf(entry.turn)
|
|
3284
|
+
if (idx < 0) return false
|
|
3285
|
+
const rect = nav.getBoundingClientRect()
|
|
3286
|
+
const scroller = nav.firstElementChild
|
|
3287
|
+
const st = scroller ? scroller.scrollTop : 0
|
|
3288
|
+
const y = rect.top + (idx * 10 + 6) - st
|
|
3289
|
+
nav.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: rect.left + 10, clientY: y }))
|
|
3290
|
+
return true
|
|
3291
|
+
} catch (e) { return false }
|
|
3292
|
+
}
|
|
3013
3293
|
// 点击定位:先查该轮次是否已加载(DOM 中能否匹配到对应行)——
|
|
3014
|
-
// 已加载 → 平滑滚动定位;未加载 →
|
|
3015
|
-
//
|
|
3294
|
+
// 已加载 → 平滑滚动定位;未加载 → 委托官方 rail 引擎加载跳转(v0.1.2+);
|
|
3295
|
+
// rail 缺席(旧版 dsh)→ 只提示、不跳转(自动加载循环会让消息流乱滚/跳错
|
|
3296
|
+
// 位置,实测踩坑;用户需先在会话区向上滚动加载历史再点击)。
|
|
3016
3297
|
const open = (entry) => {
|
|
3017
3298
|
const row = findRow(entry)
|
|
3018
3299
|
if (row) { row.scrollIntoView({ behavior: 'smooth', block: 'start' }); return }
|
|
3300
|
+
if (delegateToRail(entry)) return
|
|
3019
3301
|
setJumpFail(true)
|
|
3020
3302
|
setTimeout(() => setJumpFail(false), 2600)
|
|
3021
3303
|
}
|
|
3022
3304
|
|
|
3305
|
+
// 详情框 meta 注入(与 rail 增强层同款信息:矢量 chips + 工具分布行;
|
|
3306
|
+
// imperative ref 填充,data-sig 幂等)。工具分布懒加载:详情驻留 200ms 拉
|
|
3307
|
+
// getTurn 聚合(distCache 跨 hover 缓存,到位后 tick 触发 ref 重填)。
|
|
3308
|
+
const fillDetailMeta = (el, entry) => {
|
|
3309
|
+
if (!el) return
|
|
3310
|
+
const dist = distCache.current.get(entry.turn) || ''
|
|
3311
|
+
const sig = entry.turn + '|' + dist
|
|
3312
|
+
if (el.dataset.sig === sig) return
|
|
3313
|
+
el.dataset.sig = sig
|
|
3314
|
+
el.replaceChildren()
|
|
3315
|
+
const meta = buildTurnMeta(entry)
|
|
3316
|
+
if (meta) el.appendChild(meta)
|
|
3317
|
+
if (dist !== '') {
|
|
3318
|
+
const tools = document.createElement('div')
|
|
3319
|
+
tools.className = 'sf-rail-conclusionTools'
|
|
3320
|
+
tools.textContent = dist
|
|
3321
|
+
el.appendChild(tools)
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
useEffect(() => {
|
|
3325
|
+
if (detailIdx === null || !currentId) return undefined
|
|
3326
|
+
const entry = entries[detailIdx]
|
|
3327
|
+
if (!entry || entry.tools === 0 || distCache.current.has(entry.turn)) return undefined
|
|
3328
|
+
const turn = entry.turn
|
|
3329
|
+
const timer = setTimeout(() => {
|
|
3330
|
+
api('getTurn', { sessionId: currentId, turn }).then((json) => {
|
|
3331
|
+
if (!json || !json.ok || !json.turn) return
|
|
3332
|
+
distCache.current.set(turn, toolDistOf(json.turn))
|
|
3333
|
+
setDistTick((n) => n + 1)
|
|
3334
|
+
}).catch(() => {})
|
|
3335
|
+
}, 200)
|
|
3336
|
+
return () => clearTimeout(timer)
|
|
3337
|
+
}, [detailIdx, currentId, entries])
|
|
3338
|
+
|
|
3023
3339
|
// active:视口内最靠上的可见行 → 映射条目索引高亮(阅读位置跟随)。
|
|
3024
3340
|
// 可见判定用 bottom 进入视口(含被顶部裁切一半的行)——此前 top>=0 会
|
|
3025
3341
|
// 跳过被裁切行导致高亮偏晚一轮(T1 修复点二)。
|
|
@@ -3328,6 +3644,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
3328
3644
|
// 常驻渲染 + hidden 类做滑入/滑出;内容 key 变化触发 pkSlideIn。
|
|
3329
3645
|
h('div', { className: 'pk-detail' + (detailEntry ? '' : ' hidden'), style: detailPos, onMouseEnter: () => showDetail(detailIdx), onMouseLeave: hideDetail },
|
|
3330
3646
|
detailEntry && h('div', { key: 'h' + detailEntry.turn, className: 'pk-detailHead' }, 'T' + detailEntry.turn + ' · ' + STR.user),
|
|
3647
|
+
detailEntry && h('div', { key: 'm' + detailEntry.turn, className: 'pk-detailMeta', ref: (el) => fillDetailMeta(el, detailEntry) }),
|
|
3331
3648
|
detailEntry && h('div', { key: 'b' + detailEntry.turn, className: 'pk-detailBody' }, detailEntry.preview || '(空)'),
|
|
3332
3649
|
),
|
|
3333
3650
|
// 跳转状态提示:常驻渲染 + hidden 类(从下方淡入/淡出);未加载轮次提示。
|
|
@@ -3380,14 +3697,13 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
3380
3697
|
}
|
|
3381
3698
|
if (liveStore.emit !== null) liveStore.emit({ ...liveStateKeep(), phase: 'loading' })
|
|
3382
3699
|
try {
|
|
3383
|
-
const
|
|
3384
|
-
|
|
3385
|
-
if (val === undefined || !Array.isArray(val.events)) {
|
|
3700
|
+
const val = sfRemoteRef.current ? await sfRemoteRef.current.pageBySession(sid, 400) : undefined
|
|
3701
|
+
if (val === undefined) {
|
|
3386
3702
|
liveStore.lastState = { ...liveStateKeep(), phase: 'error', error: STR.liveFail }
|
|
3387
3703
|
if (liveStore.emit !== null) liveStore.emit(liveStore.lastState)
|
|
3388
3704
|
return
|
|
3389
3705
|
}
|
|
3390
|
-
const events = val
|
|
3706
|
+
const events = historyEventsOf(val)
|
|
3391
3707
|
const derived = await api('derive', { events, now: Date.now() })
|
|
3392
3708
|
if (!derived || !derived.ok) {
|
|
3393
3709
|
liveStore.lastState = { ...liveStateKeep(), phase: 'error', error: (derived && derived.error) || STR.liveFail }
|
|
@@ -3626,11 +3942,233 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
3626
3942
|
)
|
|
3627
3943
|
}
|
|
3628
3944
|
|
|
3945
|
+
// ── M11×v0.1.2:官方轮次导航(TurnNavigatorRail)增强层 ─────────────
|
|
3946
|
+
// 官方 rail 无扩展槽位(chat 槽位仅 node/turnTail/assistant-actions/commandview),
|
|
3947
|
+
// 本层走 DOM 观察(家族传统:观察器 + 自愈重挂),挂载我们的独有增量:
|
|
3948
|
+
// 1) hover 结论段注入官方预览卡:该轮「结论摘要」(官方预览只有 prompt/response
|
|
3949
|
+
// 片段)+ 工具统计(错误角标方案已撤——开发会话多数轮次都有工具错误,
|
|
3950
|
+
// 刻度全员红点等于无信息,用户实机裁定 2026-09-05);
|
|
3951
|
+
// 2) 右键 rail → 唤起会话流工作台。
|
|
3952
|
+
// 定位:nav[aria-label=轮次导航|Turn navigation];几何换算复刻官方 itemAtPointer
|
|
3953
|
+
// (TURN_SPACING 10px / RAIL_INSET 6px + scroller.scrollTop);turn 号从刻度按钮
|
|
3954
|
+
// aria-label(跳转到第 N 轮 / Jump to turn N)提取,索引↔turn 精确映射,不依赖
|
|
3955
|
+
// 「index+1 == turn」假设。官方 navigate 引擎(含未载入轮次自动加载跳转)原样保留,
|
|
3956
|
+
// 我们不拦截点击。
|
|
3957
|
+
const RAIL_SELECTOR = 'nav[aria-label="轮次导航"], nav[aria-label="Turn navigation"]'
|
|
3958
|
+
function RailEnhancer(props) {
|
|
3959
|
+
const { sessions, railEl, onOpenWorkbench } = props
|
|
3960
|
+
const [turnData, setTurnData] = useState([]) // [{turn, preview, conclusion, tools, errors}]
|
|
3961
|
+
const [hoverInfo, setHoverInfo] = useState(null) // {turn, clientY, railLeft}
|
|
3962
|
+
const [, force] = useState(0)
|
|
3963
|
+
|
|
3964
|
+
// 订阅会话列表变化:切换会话 → 重拉条目。
|
|
3965
|
+
useEffect(() => {
|
|
3966
|
+
if (sessions && sessions.list && typeof sessions.list.subscribe === 'function') {
|
|
3967
|
+
return sessions.list.subscribe(() => force((n) => n + 1))
|
|
3968
|
+
}
|
|
3969
|
+
return undefined
|
|
3970
|
+
}, [sessions])
|
|
3971
|
+
|
|
3972
|
+
const currentId = (() => {
|
|
3973
|
+
try {
|
|
3974
|
+
const s = sessions && sessions.list && typeof sessions.list.getSnapshot === 'function' ? sessions.list.getSnapshot() : null
|
|
3975
|
+
return s && s.current ? s.current : null
|
|
3976
|
+
} catch (e) { return null }
|
|
3977
|
+
})()
|
|
3978
|
+
|
|
3979
|
+
// 条目数据:host get lightTurns(每轮一条,对齐官方 rail item 粒度)。
|
|
3980
|
+
// 轮询节奏同钢琴键:DOM 变化切 3s 高频,空闲 15s 兜底。
|
|
3981
|
+
useEffect(() => {
|
|
3982
|
+
if (!currentId) { setTurnData([]); return undefined }
|
|
3983
|
+
let alive = true
|
|
3984
|
+
const refresh = () => {
|
|
3985
|
+
api('get', { sessionId: currentId }).then((json) => {
|
|
3986
|
+
if (!alive || !json || !json.ok) return
|
|
3987
|
+
const rows = (json.lightTurns || []).map((lt) => ({
|
|
3988
|
+
turn: lt.turn,
|
|
3989
|
+
preview: lt.userMessages && lt.userMessages[0] ? String(lt.userMessages[0].preview || '') : '',
|
|
3990
|
+
conclusion: String(lt.conclusionPreview || ''),
|
|
3991
|
+
tools: lt.toolCount || 0,
|
|
3992
|
+
errors: lt.errorCount || 0,
|
|
3993
|
+
durMs: (typeof lt.startTime === 'number' && typeof lt.endTime === 'number' && lt.endTime > lt.startTime) ? lt.endTime - lt.startTime : 0,
|
|
3994
|
+
thinking: lt.hasThinking === true,
|
|
3995
|
+
}))
|
|
3996
|
+
setTurnData((prev) => {
|
|
3997
|
+
if (prev.length === rows.length && prev.length > 0 && prev[prev.length - 1].turn === rows[rows.length - 1].turn) return prev
|
|
3998
|
+
return rows
|
|
3999
|
+
})
|
|
4000
|
+
}).catch(() => {})
|
|
4001
|
+
}
|
|
4002
|
+
const ACTIVE_MS = 3000
|
|
4003
|
+
const IDLE_MS = 15000
|
|
4004
|
+
let iv = null
|
|
4005
|
+
const schedule = (delay) => { if (iv) clearInterval(iv); iv = setInterval(refresh, delay) }
|
|
4006
|
+
let flowObs = null
|
|
4007
|
+
let lastDomRefresh = 0
|
|
4008
|
+
const onDomChange = () => {
|
|
4009
|
+
if (!alive) return
|
|
4010
|
+
const now = Date.now()
|
|
4011
|
+
if (now - lastDomRefresh < 1000) return
|
|
4012
|
+
lastDomRefresh = now
|
|
4013
|
+
refresh()
|
|
4014
|
+
schedule(ACTIVE_MS)
|
|
4015
|
+
}
|
|
4016
|
+
const flowEl = document.querySelector('[data-chat-flow=""]') || document.querySelector('[data-chat-flow]')
|
|
4017
|
+
if (flowEl) {
|
|
4018
|
+
flowObs = new MutationObserver(onDomChange)
|
|
4019
|
+
flowObs.observe(flowEl, { childList: true, subtree: true })
|
|
4020
|
+
}
|
|
4021
|
+
refresh()
|
|
4022
|
+
schedule(IDLE_MS)
|
|
4023
|
+
return () => { alive = false; if (iv) clearInterval(iv); if (flowObs) flowObs.disconnect() }
|
|
4024
|
+
}, [currentId])
|
|
4025
|
+
|
|
4026
|
+
// rail 交互:hover 几何换算 + 右键唤起工作台。
|
|
4027
|
+
useEffect(() => {
|
|
4028
|
+
if (!railEl) { setHoverInfo(null); return undefined }
|
|
4029
|
+
const scroller = railEl.firstElementChild // 官方结构:nav.frame > div.scroller
|
|
4030
|
+
const railTurns = () => [...railEl.querySelectorAll('button[aria-label]')]
|
|
4031
|
+
.map((b) => { const m = /(\d+)/.exec(b.getAttribute('aria-label') || ''); return m ? Number(m[1]) : null })
|
|
4032
|
+
.filter((n) => n !== null)
|
|
4033
|
+
const onMove = (e) => {
|
|
4034
|
+
const rect = railEl.getBoundingClientRect()
|
|
4035
|
+
const st = scroller ? scroller.scrollTop : 0
|
|
4036
|
+
const idx = Math.max(0, Math.round((e.clientY - rect.top + st - 6) / 10))
|
|
4037
|
+
const turns = railTurns()
|
|
4038
|
+
const turn = idx < turns.length ? turns[idx] : null
|
|
4039
|
+
setHoverInfo((prev) => {
|
|
4040
|
+
const prevTurn = prev === null ? null : prev.turn
|
|
4041
|
+
return prevTurn === turn ? prev : (turn === null ? null : { turn })
|
|
4042
|
+
})
|
|
4043
|
+
}
|
|
4044
|
+
const onLeave = () => setHoverInfo(null)
|
|
4045
|
+
const onContext = (e) => { e.preventDefault(); onOpenWorkbench() }
|
|
4046
|
+
railEl.addEventListener('pointermove', onMove)
|
|
4047
|
+
railEl.addEventListener('pointerleave', onLeave)
|
|
4048
|
+
railEl.addEventListener('contextmenu', onContext)
|
|
4049
|
+
return () => {
|
|
4050
|
+
railEl.removeEventListener('pointermove', onMove)
|
|
4051
|
+
railEl.removeEventListener('pointerleave', onLeave)
|
|
4052
|
+
railEl.removeEventListener('contextmenu', onContext)
|
|
4053
|
+
}
|
|
4054
|
+
}, [railEl, onOpenWorkbench])
|
|
4055
|
+
|
|
4056
|
+
// 工具分布懒加载(独特信息维度,官方 preview 只有 prompt/response 片段):
|
|
4057
|
+
// hover 驻留 250ms 后拉 getTurn,聚合工具名 ×次数 top4(防扫过时每刻度一请求)。
|
|
4058
|
+
const [toolDist, setToolDist] = useState(null) // {turn, text}
|
|
4059
|
+
useEffect(() => {
|
|
4060
|
+
if (!railEl || hoverInfo === null || !currentId) { setToolDist(null); return undefined }
|
|
4061
|
+
const turn = hoverInfo.turn
|
|
4062
|
+
const timer = setTimeout(() => {
|
|
4063
|
+
api('getTurn', { sessionId: currentId, turn }).then((json) => {
|
|
4064
|
+
if (!json || !json.ok || !json.turn) return
|
|
4065
|
+
const text = toolDistOf(json.turn)
|
|
4066
|
+
setToolDist((prev) => (prev && prev.turn === turn && prev.text === text) ? prev : { turn, text })
|
|
4067
|
+
}).catch(() => {})
|
|
4068
|
+
}, 250)
|
|
4069
|
+
return () => clearTimeout(timer)
|
|
4070
|
+
}, [railEl, hoverInfo, currentId])
|
|
4071
|
+
|
|
4072
|
+
// hover 增强段注入官方预览卡内部(单卡融合——另起一卡会与官方预览上下堆叠成
|
|
4073
|
+
// 两大块,实测很丑)。官方 preview 容器 = rail 内 class 以 _preview 结尾的元素
|
|
4074
|
+
//(CSS module hash 前缀 + preview/previewPrompt/previewResponse 三段,用
|
|
4075
|
+
// /(?:^|_)preview(?:$|\s)/ 精确命中容器);React 复用 preview 元素跨轮次渲染 →
|
|
4076
|
+
// data-sig 核对(轮次+工具分布到位状态),变化即重建;observer 监视 rail 子树,
|
|
4077
|
+
// preview 出现/消失/被重渲染时自愈。**独特性原则(用户裁定 2026-09-05):只给
|
|
4078
|
+
// 官方 preview 没有的信息维度——耗时 / 工具统计与分布 / 错误 / 思考标记;
|
|
4079
|
+
// 结论不再展示(与官方 response 片段重复,用户二次裁定撤下);图标一律纯色
|
|
4080
|
+
// 矢量(stroke=currentColor),不用 emoji**。
|
|
4081
|
+
// 高度解禁:官方 preview 固定 --turn-preview-height:100px 且溢出裁剪——注入段
|
|
4082
|
+
// 会被裁成「残缺卡」(实测踩坑);注入期间内联 height:auto + maxHeight 放开,
|
|
4083
|
+
// 撤注入时还原内联样式。
|
|
4084
|
+
useEffect(() => {
|
|
4085
|
+
if (!railEl) return undefined
|
|
4086
|
+
const findPreview = () => [...railEl.querySelectorAll('[class*=preview]')]
|
|
4087
|
+
.find((el) => /(?:^|_)preview(?:$|\s)/.test(String(el.className)))
|
|
4088
|
+
const cleanup = () => {
|
|
4089
|
+
for (const n of railEl.querySelectorAll('.sf-rail-conclusion')) n.remove()
|
|
4090
|
+
const p = findPreview()
|
|
4091
|
+
if (p) { p.style.height = ''; p.style.maxHeight = ''; p.style.overflow = '' }
|
|
4092
|
+
}
|
|
4093
|
+
const enhance = () => {
|
|
4094
|
+
const preview = findPreview()
|
|
4095
|
+
const turn = hoverInfo === null ? null : hoverInfo.turn
|
|
4096
|
+
const entry = turn === null ? undefined : turnData.find((t) => t.turn === turn)
|
|
4097
|
+
if (!preview || !entry || (entry.durMs === 0 && entry.tools === 0)) { cleanup(); return }
|
|
4098
|
+
preview.style.height = 'auto'
|
|
4099
|
+
preview.style.maxHeight = '280px'
|
|
4100
|
+
preview.style.overflow = 'hidden'
|
|
4101
|
+
let node = preview.querySelector(':scope > .sf-rail-conclusion')
|
|
4102
|
+
if (!node) {
|
|
4103
|
+
node = document.createElement('div')
|
|
4104
|
+
node.className = 'sf-rail-conclusion'
|
|
4105
|
+
preview.appendChild(node)
|
|
4106
|
+
}
|
|
4107
|
+
const dist = toolDist !== null && toolDist.turn === entry.turn ? toolDist.text : ''
|
|
4108
|
+
const sig = entry.turn + '|' + dist
|
|
4109
|
+
if (node.dataset.sig === sig) return
|
|
4110
|
+
node.dataset.sig = sig
|
|
4111
|
+
node.replaceChildren()
|
|
4112
|
+
// meta chips 行(纯色矢量图标):耗时 · n 次工具 · m 错误(红)· 含思考
|
|
4113
|
+
const meta = buildTurnMeta(entry)
|
|
4114
|
+
if (meta) node.appendChild(meta)
|
|
4115
|
+
// 工具分布行(懒加载到位后经 sig 变化重建本节点)
|
|
4116
|
+
if (dist !== '') {
|
|
4117
|
+
const tools = document.createElement('div')
|
|
4118
|
+
tools.className = 'sf-rail-conclusionTools'
|
|
4119
|
+
tools.textContent = dist
|
|
4120
|
+
node.appendChild(tools)
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
enhance()
|
|
4124
|
+
const obs = new MutationObserver(enhance)
|
|
4125
|
+
obs.observe(railEl, { childList: true, subtree: true })
|
|
4126
|
+
return () => { obs.disconnect(); cleanup() }
|
|
4127
|
+
}, [railEl, hoverInfo, turnData, toolDist])
|
|
4128
|
+
|
|
4129
|
+
return null
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
// ── 导航总挂载:官方 rail 增强层 ⇄ 经典钢琴键悬浮条 ────────────────
|
|
4133
|
+
// 默认(v0.1.2+,用户裁定 2026-09-05):经典悬浮条默认开启(pianoClassicStrip
|
|
4134
|
+
// 默认 true),与官方 rail 增强层并存——左右双导航各司其职(我们:预览卡/右键
|
|
4135
|
+
// 工作台/滚轮窗口/键盘;官方:完整历史刻度);设置里可各自关闭。旧版 dsh
|
|
4136
|
+
// (无官方 rail)railEl=null 时 classic 恒 true,功能不丢。
|
|
4137
|
+
function NavRoot(props) {
|
|
4138
|
+
const { settingsCtrl } = props
|
|
4139
|
+
const [, force] = useState(0)
|
|
4140
|
+
const [railEl, setRailEl] = useState(null)
|
|
4141
|
+
useEffect(() => (settingsCtrl ? settingsCtrl.subscribe(() => force((n) => n + 1)) : undefined), [settingsCtrl])
|
|
4142
|
+
useEffect(() => {
|
|
4143
|
+
const find = () => {
|
|
4144
|
+
const nav = document.querySelector(RAIL_SELECTOR)
|
|
4145
|
+
setRailEl((prev) => (prev === nav || (prev === null && nav === null) ? prev : nav))
|
|
4146
|
+
}
|
|
4147
|
+
find()
|
|
4148
|
+
const obs = new MutationObserver(find)
|
|
4149
|
+
obs.observe(document.body, { childList: true, subtree: true })
|
|
4150
|
+
return () => obs.disconnect()
|
|
4151
|
+
}, [])
|
|
4152
|
+
const classicWanted = sfSettings.current.pianoClassicStrip === true
|
|
4153
|
+
const enhanceWanted = sfSettings.current.railEnhance !== false
|
|
4154
|
+
// railHideOfficial:完全屏蔽官方刻度条(只留我们的悬浮条);隐藏期间增强层不挂。
|
|
4155
|
+
const hideOfficial = sfSettings.current.railHideOfficial === true
|
|
4156
|
+
useEffect(() => {
|
|
4157
|
+
if (railEl) railEl.style.display = hideOfficial ? 'none' : ''
|
|
4158
|
+
return () => { if (railEl) railEl.style.display = '' }
|
|
4159
|
+
}, [railEl, hideOfficial])
|
|
4160
|
+
const showClassic = classicWanted || railEl === null
|
|
4161
|
+
const showEnhance = enhanceWanted && railEl !== null && !hideOfficial
|
|
4162
|
+
return h(React.Fragment, null,
|
|
4163
|
+
showClassic ? h(TurnKeys, { sessions: props.sessions, connection: props.connection, onOpenWorkbench: props.onOpenWorkbench }) : null,
|
|
4164
|
+
showEnhance ? h(RailEnhancer, { sessions: props.sessions, railEl, onOpenWorkbench: props.onOpenWorkbench }) : null)
|
|
4165
|
+
}
|
|
4166
|
+
|
|
3629
4167
|
// ── M11:钢琴键竖条挂载(DOM 注入 + 自愈)────────────────────────
|
|
3630
4168
|
// 容器挂 document.body(dsh-navbar 同款模式):完全脱离会话区布局树,
|
|
3631
4169
|
// 不干扰 conversation 的 flex/grid 布局;fixed 定位由组件自管理。
|
|
3632
4170
|
// MutationObserver 自愈:容器丢失(布局重建)时自动恢复。
|
|
3633
|
-
function mountPianoKeys(controller, sessions, connection) {
|
|
4171
|
+
function mountPianoKeys(controller, sessions, connection, settingsCtrl) {
|
|
3634
4172
|
let root = undefined
|
|
3635
4173
|
let container = undefined
|
|
3636
4174
|
let waitObserver = null
|
|
@@ -3654,9 +4192,10 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
3654
4192
|
document.body.appendChild(container)
|
|
3655
4193
|
try {
|
|
3656
4194
|
root = createRoot(container)
|
|
3657
|
-
root.render(h(
|
|
4195
|
+
root.render(h(NavRoot, {
|
|
3658
4196
|
sessions,
|
|
3659
4197
|
connection,
|
|
4198
|
+
settingsCtrl: settingsCtrl || null,
|
|
3660
4199
|
onOpenWorkbench: () => controller.open(),
|
|
3661
4200
|
}))
|
|
3662
4201
|
syncChatVisibility()
|
|
@@ -3824,7 +4363,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
3824
4363
|
}
|
|
3825
4364
|
|
|
3826
4365
|
// ── 插件体 ─────────────────────────────────────────────────────────
|
|
3827
|
-
const inject = ['sessions', 'connection', 'slots', 'layout']
|
|
4366
|
+
const inject = ['sessions', 'connection', 'slots', 'layout', 'remote', 'remote.session']
|
|
3828
4367
|
|
|
3829
4368
|
function apply(ctx) {
|
|
3830
4369
|
try {
|
|
@@ -3838,6 +4377,11 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
3838
4377
|
|
|
3839
4378
|
const sessions = ctx.get('sessions')
|
|
3840
4379
|
const connection = ctx.get('connection')
|
|
4380
|
+
// v0.1.2 RPC 双面适配(remote.session 新面优先,connection 旧面回退)。
|
|
4381
|
+
sfRemoteRef.current = sfBuildRemote(ctx, connection)
|
|
4382
|
+
// 设置控制器引用:settings.section 注册块内创建,挂载钢琴键/导航增强层时
|
|
4383
|
+
// 传给 NavRoot 做设置变更即时切换(不可用则为 null,走默认值)。
|
|
4384
|
+
let sfSettingsCtrl = null
|
|
3841
4385
|
|
|
3842
4386
|
const state = { open: false, listeners: new Set() }
|
|
3843
4387
|
const controller = {
|
|
@@ -3919,7 +4463,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
3919
4463
|
try {
|
|
3920
4464
|
const settingsBinder = ctx.get('webUiSettings') ?? ctx.get('settingsScope')
|
|
3921
4465
|
if (settingsBinder !== undefined && settingsBinder !== null && typeof settingsBinder.bind === 'function') {
|
|
3922
|
-
|
|
4466
|
+
sfSettingsCtrl = new SfSettingsController(settingsBinder.bind({ namespace: 'session-flow' }))
|
|
3923
4467
|
slots.inject('settings.section', () => {
|
|
3924
4468
|
const unregister = slots.register({
|
|
3925
4469
|
name: 'settings.section',
|
|
@@ -4047,7 +4591,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
4047
4591
|
disposers.push(mountSidebarEntry(controller))
|
|
4048
4592
|
disposers.push(mountBoard(controller, sessions, connection))
|
|
4049
4593
|
// M11:钢琴键会话快切(会话页左侧常驻竖条);挂载失败不影响其他功能。
|
|
4050
|
-
disposers.push(mountPianoKeys(controller, sessions, connection))
|
|
4594
|
+
disposers.push(mountPianoKeys(controller, sessions, connection, sfSettingsCtrl))
|
|
4051
4595
|
} catch (error) {
|
|
4052
4596
|
console.error('[dsh-session-flow] mount failed:', error)
|
|
4053
4597
|
}
|
package/lib/host.js
CHANGED
|
@@ -85,6 +85,9 @@ export const SETTINGS_DEFAULTS = {
|
|
|
85
85
|
liveFollowPx: 40, // 实时吸底阈值(px)
|
|
86
86
|
liveHistoryTurns: 3, // 详情页实时模式保留的历史回合数
|
|
87
87
|
stallThresholdMin: 3, // 疑似卡死阈值(分钟)
|
|
88
|
+
railEnhance: true, // 官方轮次导航(TurnNavigatorRail)增强层开关(v0.1.2+)
|
|
89
|
+
railHideOfficial: false, // 完全屏蔽官方轮次导航(只留我们的悬浮条)
|
|
90
|
+
pianoClassicStrip: true, // 经典左侧轮次悬浮条(默认开,与官方 rail 并存;用户裁定 2026-09-05)
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
/** schemastery schema:settings 服务 resolve 时调用 schema(merged),并需要 toJSON()(describe 用)。 */
|
|
@@ -96,6 +99,9 @@ const SETTINGS_SCHEMA = z.object({
|
|
|
96
99
|
liveFollowPx: z.number().step(1).min(20).max(120).default(SETTINGS_DEFAULTS.liveFollowPx),
|
|
97
100
|
liveHistoryTurns: z.number().step(1).min(0).max(10).default(SETTINGS_DEFAULTS.liveHistoryTurns),
|
|
98
101
|
stallThresholdMin: z.number().step(1).min(1).max(10).default(SETTINGS_DEFAULTS.stallThresholdMin),
|
|
102
|
+
railEnhance: z.boolean().default(SETTINGS_DEFAULTS.railEnhance),
|
|
103
|
+
railHideOfficial: z.boolean().default(SETTINGS_DEFAULTS.railHideOfficial),
|
|
104
|
+
pianoClassicStrip: z.boolean().default(SETTINGS_DEFAULTS.pianoClassicStrip),
|
|
99
105
|
})
|
|
100
106
|
|
|
101
107
|
/** 解析后的当前设置值(base + user 层);host 侧消费点(卡死阈值)从这里读。 */
|
package/package.json
CHANGED