dsh-session-flow 1.2.0 → 1.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/lib/archive.js +9 -2
- package/lib/client.js +644 -92
- package/lib/host.js +6 -0
- package/lib/index-store.js +10 -8
- package/package.json +1 -1
package/lib/archive.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// lib/archive.js — 会话存档核心:扫描 / 解码 / 解析 / 统计。
|
|
2
2
|
//
|
|
3
|
-
// 数据来源:~/.dsh/sessions/<workspace>/session-<uuid>/session.jsonl.zstd
|
|
3
|
+
// 数据来源:~/.dsh/sessions/<workspace>/session-<uuid>/session[.v2].jsonl.zstd
|
|
4
4
|
// (zstd 压缩的多帧 JSONL;compression: none 时为明文 session.jsonl)。
|
|
5
|
+
// dsh v0.1.3 起存档升级 v2(session.v2.jsonl.zstd,行信封 {type,seq,time,data} 不变,
|
|
6
|
+
// header version:2 + 新增 session/end-seed 等事件):v1 文件冻结在迁移时刻、v2 持续
|
|
7
|
+
// 写入——读取必须优先 v2,否则读到冻结旧数据、纯 v2 新会话不可见(0.1.3 适配)。
|
|
5
8
|
//
|
|
6
9
|
// 解码使用 Node 内置 node:zlib 的 zstd 支持(Node >= 22.19),零第三方依赖;
|
|
7
10
|
// 帧扫描算法借鉴自 @deepseek-ai/dsh-session-persistence-jsonl (MIT)。
|
|
@@ -246,8 +249,12 @@ export function summarizeParsed({ header, title, events }) {
|
|
|
246
249
|
}
|
|
247
250
|
}
|
|
248
251
|
|
|
249
|
-
/** 定位一个会话目录里的存档文件(优先 zstd
|
|
252
|
+
/** 定位一个会话目录里的存档文件(优先 v2,回退 v1 zstd,再回退明文)。
|
|
253
|
+
* dsh v0.1.3 起 v2 存档与 v1 并存:v1 冻结在迁移时刻、v2 持续写入——
|
|
254
|
+
* 必须优先 v2,否则老会话读到冻结数据、纯 v2 新会话直接漏掉(0.1.3 适配)。 */
|
|
250
255
|
export function sessionFileOf(sessionDir) {
|
|
256
|
+
const v2 = join(sessionDir, 'session.v2.jsonl.zstd')
|
|
257
|
+
if (existsSync(v2)) return v2
|
|
251
258
|
const zstd = join(sessionDir, 'session.jsonl.zstd')
|
|
252
259
|
if (existsSync(zstd)) return zstd
|
|
253
260
|
const plain = join(sessionDir, 'session.jsonl')
|
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
|
|
|
@@ -44,7 +47,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
44
47
|
dockPinNotice: '📌 直播已钉住原会话(当前已切换会话)', dockPinRebind: '转播当前会话',
|
|
45
48
|
rename: '重命名', renamePlaceholder: '输入新标题…', renameFail: '重命名失败', origTitle: '原名',
|
|
46
49
|
search: '搜索标题/工具/文件… 支持 tool: pwsh · file: src · err:', allWorkspaces: '全部工作区',
|
|
47
|
-
sortRecent: '最近运行', sortNewest: '最近创建', sortOldest: '最早创建', sortTools: '工具最多', sortLongest: '耗时最长',
|
|
50
|
+
sortRecent: '最近运行', sortNewest: '最近创建', sortOldest: '最早创建', sortTools: '工具最多', sortLongest: '耗时最长', sortSize: '占用最大',
|
|
48
51
|
noMatch: '没有匹配的会话', noData: '暂无会话数据',
|
|
49
52
|
running: '进行中', ended: '已结束', subagent: '子代理',
|
|
50
53
|
turns: '回合', steps: '步骤', tools: '工具', errors: '错误', msgs: '消息',
|
|
@@ -65,7 +68,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
65
68
|
emptyHint: '此会话已创建但尚未开始对话', userNav: '用户发言', noUserNav: '(无用户发言)',
|
|
66
69
|
conclusion: '结论', injected: '注入', lineage: '血缘', noLineage: '(无子代理血缘信息)', task: '任务', liveTree: '运行时血缘', offlineTree: '档案血缘', modeOneShot: '一次性', modeContinuable: '可续',
|
|
67
70
|
subagentDetail: '子代理详情', loadDetail: '加载子代理详情…', noDetail: '该子代理无可见事件',
|
|
68
|
-
viewSubagent: '查看子代理', toolTop: '工具 Top', issues: '问题会话', issuesHint: '只显示有错误记录的会话',
|
|
71
|
+
viewSubagent: '查看子代理', toolTop: '工具 Top', issues: '问题会话', issuesHint: '只显示有错误记录的会话', storage: '存储', storageHint: '当前范围会话存档总占用(只读)',
|
|
69
72
|
searchTab: '检索', searchInPlaceholder: '会话内检索…', noMatches: '无匹配位置', matches: '命中', navUsers: '用户',
|
|
70
73
|
cache: '缓存管理', cacheTotal: '缓存总量', cacheIndex: '会话索引', cacheTimeline: '时间线缓存',
|
|
71
74
|
cleanTimeline: '清理时间线缓存', cleanAll: '清理全部缓存', cacheDone: '已清理', cacheEmpty: '(无缓存文件)',
|
|
@@ -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 后重试)',
|
|
@@ -100,7 +108,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
100
108
|
dockPinNotice: '📌 Live pinned to previous session (session switched)', dockPinRebind: 'Follow current session',
|
|
101
109
|
rename: 'Rename', renamePlaceholder: 'New title…', renameFail: 'Rename failed', origTitle: 'Original',
|
|
102
110
|
search: 'Search title / tools / files… use tool: · file: · err:', allWorkspaces: 'All workspaces',
|
|
103
|
-
sortRecent: 'Recently run', sortNewest: 'Recently created', sortOldest: 'Oldest first', sortTools: 'Most tools', sortLongest: 'Longest',
|
|
111
|
+
sortRecent: 'Recently run', sortNewest: 'Recently created', sortOldest: 'Oldest first', sortTools: 'Most tools', sortLongest: 'Longest', sortSize: 'Largest',
|
|
104
112
|
noMatch: 'No matching sessions', noData: 'No session data yet',
|
|
105
113
|
running: 'Running', ended: 'Ended', subagent: 'subagent',
|
|
106
114
|
turns: 'turns', steps: 'steps', tools: 'tools', errors: 'errors', msgs: 'msgs',
|
|
@@ -122,7 +130,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
122
130
|
conclusion: 'Conclusion', injected: 'Injected', lineage: 'Lineage', noLineage: '(no subagent lineage)', task: 'Task',
|
|
123
131
|
liveTree: 'Live lineage', offlineTree: 'Archive lineage', modeOneShot: 'one-shot', modeContinuable: 'continuable',
|
|
124
132
|
subagentDetail: 'Subagent detail', loadDetail: 'Loading subagent detail…', noDetail: 'No visible events for this subagent',
|
|
125
|
-
viewSubagent: 'View subagent', toolTop: 'Tool Top', issues: 'Problem sessions', issuesHint: 'Only sessions with errors',
|
|
133
|
+
viewSubagent: 'View subagent', toolTop: 'Tool Top', issues: 'Problem sessions', issuesHint: 'Only sessions with errors', storage: 'Storage', storageHint: 'Total archive size of sessions in scope (read-only)',
|
|
126
134
|
searchTab: 'Search', searchInPlaceholder: 'Search in session…', noMatches: 'No matches', matches: 'matches', navUsers: 'User',
|
|
127
135
|
cache: 'Cache Mgmt', cacheTotal: 'Cache total', cacheIndex: 'Index', cacheTimeline: 'Timeline cache',
|
|
128
136
|
cleanTimeline: 'Clean timeline cache', cleanAll: 'Clean all cache', cacheDone: 'Cleaned', cacheEmpty: '(no cache files)',
|
|
@@ -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
|
|
@@ -1223,6 +1398,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1223
1398
|
if (sort === 'oldest') return r.createdAt || 0
|
|
1224
1399
|
if (sort === 'tools') return r.toolCalls || 0
|
|
1225
1400
|
if (sort === 'longest') return (r.lastEventTime || 0) - (r.createdAt || 0)
|
|
1401
|
+
if (sort === 'size') return r.sizeBytes || 0
|
|
1226
1402
|
return 0
|
|
1227
1403
|
}
|
|
1228
1404
|
const ascending = sort === 'oldest'
|
|
@@ -1235,17 +1411,19 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1235
1411
|
const toolAgg = new Map()
|
|
1236
1412
|
let issues = 0
|
|
1237
1413
|
let roots = 0
|
|
1414
|
+
let totalSize = 0
|
|
1238
1415
|
for (const g of (state.data && state.data.workspaces) || []) {
|
|
1239
1416
|
if (wsFilter !== '' && wsFilter !== g.name) continue // 统计范围 = 当前选中的工作区
|
|
1240
1417
|
for (const s of g.sessions) {
|
|
1241
1418
|
if (s.parentSession) continue
|
|
1242
1419
|
roots++
|
|
1243
1420
|
if (s.toolErrors > 0) issues++
|
|
1421
|
+
totalSize += s.sizeBytes || 0
|
|
1244
1422
|
for (const n of s.toolNames || []) toolAgg.set(n, (toolAgg.get(n) || 0) + 1)
|
|
1245
1423
|
}
|
|
1246
1424
|
}
|
|
1247
1425
|
const topTools = [...toolAgg.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6)
|
|
1248
|
-
return { topTools, issues, roots }
|
|
1426
|
+
return { topTools, issues, roots, totalSize }
|
|
1249
1427
|
}, [state.data, wsFilter])
|
|
1250
1428
|
|
|
1251
1429
|
const wsNames = ((state.data && state.data.workspaces) || []).map((g) => g.name)
|
|
@@ -1270,6 +1448,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1270
1448
|
h('option', { value: 'oldest' }, STR.sortOldest),
|
|
1271
1449
|
h('option', { value: 'tools' }, STR.sortTools),
|
|
1272
1450
|
h('option', { value: 'longest' }, STR.sortLongest),
|
|
1451
|
+
h('option', { value: 'size' }, STR.sortSize),
|
|
1273
1452
|
),
|
|
1274
1453
|
h('label', { className: 'sf-muted', style: { display: 'inline-flex', alignItems: 'center', gap: 4, cursor: 'pointer', whiteSpace: 'nowrap' } },
|
|
1275
1454
|
h('input', { type: 'checkbox', checked: showEmpty, onChange: (e) => setShowEmpty(e.target.checked) }),
|
|
@@ -1356,6 +1535,10 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1356
1535
|
h('span', {}, '⚠ ' + STR.issues),
|
|
1357
1536
|
h('span', { className: 'sf-statCount err' }, stats.issues),
|
|
1358
1537
|
),
|
|
1538
|
+
h('span', { className: 'sf-statChip', title: STR.storageHint },
|
|
1539
|
+
h('span', {}, STR.storage),
|
|
1540
|
+
h('span', { className: 'sf-statCount' }, fmtSize(stats.totalSize)),
|
|
1541
|
+
),
|
|
1359
1542
|
),
|
|
1360
1543
|
state.phase === 'loading' && h('div', { className: 'sf-hint' }, STR.scanning),
|
|
1361
1544
|
state.phase === 'error' && h('div', { className: 'sf-hint', style: { color: '#d43b3b' } }, STR.loadFailed + ': ' + String(state.error)),
|
|
@@ -1721,16 +1904,12 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1721
1904
|
}
|
|
1722
1905
|
} catch (e) {}
|
|
1723
1906
|
// 2) 运行时 history 桥接(在线子代理)→ host derive 得 light 结构
|
|
1724
|
-
if (
|
|
1907
|
+
if (node.parentId !== undefined && sfRemoteRef.current) {
|
|
1725
1908
|
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)
|
|
1909
|
+
const subVal = await sfRemoteRef.current.pageBySub(node.parentId, node.id, node.mode, 120)
|
|
1910
|
+
const subEvents = historyEventsOf(subVal)
|
|
1911
|
+
if (subEvents.length > 0) {
|
|
1912
|
+
const events = subEvents
|
|
1734
1913
|
const derived = await api('derive', { events })
|
|
1735
1914
|
if (derived && derived.ok) {
|
|
1736
1915
|
// derive 返回完整 timeline——转成 light 结构
|
|
@@ -1848,8 +2027,9 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1848
2027
|
})
|
|
1849
2028
|
const trimmed = String(title || '').trim()
|
|
1850
2029
|
if (trimmed === '' && session.titleSource === 'user') return Promise.reject(new Error(STR.renameClearBlocked))
|
|
1851
|
-
if (trimmed !== '' &&
|
|
1852
|
-
return
|
|
2030
|
+
if (trimmed !== '' && sfRemoteRef.current && sfRemoteRef.current.canRename) {
|
|
2031
|
+
return sfRemoteRef.current.rename(session.id, trimmed)
|
|
2032
|
+
.then((accepted) => { if (typeof accepted !== 'string') throw new Error(STR.renameFail); return accepted })
|
|
1853
2033
|
.then((accepted) => { setTitleOverride(accepted); setRenaming(false); return true })
|
|
1854
2034
|
.catch(() => legacy())
|
|
1855
2035
|
}
|
|
@@ -1990,18 +2170,13 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1990
2170
|
}
|
|
1991
2171
|
setLiveState((s) => ({ ...s, phase: 'loading' }))
|
|
1992
2172
|
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)) {
|
|
2173
|
+
// v0.1.2:session RPC 经双面适配器(remote.session.page / 旧 history)。
|
|
2174
|
+
const val = sfRemoteRef.current ? await sfRemoteRef.current.pageBySession(session.id, 400) : undefined
|
|
2175
|
+
if (val === undefined) {
|
|
2001
2176
|
setLiveState((s) => ({ ...s, phase: 'error', error: STR.liveFail }))
|
|
2002
2177
|
return
|
|
2003
2178
|
}
|
|
2004
|
-
const events = val
|
|
2179
|
+
const events = historyEventsOf(val)
|
|
2005
2180
|
const derived = await api('derive', { events, now: Date.now() })
|
|
2006
2181
|
if (!derived || !derived.ok) {
|
|
2007
2182
|
setLiveState((s) => ({ ...s, phase: 'error', error: (derived && derived.error) || STR.liveFail }))
|
|
@@ -2638,19 +2813,18 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2638
2813
|
|
|
2639
2814
|
const fetchLive = useRef(async () => {})
|
|
2640
2815
|
fetchLive.current = async () => {
|
|
2641
|
-
if (
|
|
2816
|
+
if (!sfRemoteRef.current) {
|
|
2642
2817
|
setState((s) => ({ ...s, phase: 'error', error: STR.liveUnavailable }))
|
|
2643
2818
|
return
|
|
2644
2819
|
}
|
|
2645
2820
|
setState((s) => ({ ...s, phase: s.timeline === null ? 'loading' : 'ready' }))
|
|
2646
2821
|
try {
|
|
2647
|
-
const
|
|
2648
|
-
|
|
2649
|
-
if (val === undefined || !Array.isArray(val.events)) {
|
|
2822
|
+
const val = await sfRemoteRef.current.pageBySession(sessionId, 400)
|
|
2823
|
+
if (val === undefined) {
|
|
2650
2824
|
setState((s) => ({ ...s, phase: 'error', error: STR.liveFail }))
|
|
2651
2825
|
return
|
|
2652
2826
|
}
|
|
2653
|
-
const events = val
|
|
2827
|
+
const events = historyEventsOf(val)
|
|
2654
2828
|
const derived = await api('derive', { events, now: Date.now() })
|
|
2655
2829
|
if (!derived || !derived.ok) {
|
|
2656
2830
|
setState((s) => ({ ...s, phase: 'error', error: (derived && derived.error) || STR.liveFail }))
|
|
@@ -2741,6 +2915,33 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2741
2915
|
})()
|
|
2742
2916
|
return () => ac.abort()
|
|
2743
2917
|
}, [props.connection])
|
|
2918
|
+
// v0.1.2 起 events.mux 消费面移除 → 改用 sessions.list 订阅:官方 rename 后
|
|
2919
|
+
// 列表行 byId[sessionId].title 由 title projection 即时更新(新控制器
|
|
2920
|
+
// dsh-api-session-controller rename 后 projections.apply('title') 即落列表),
|
|
2921
|
+
// 订阅回调秒级收集全量标题快照,浅比较防抖。与 mux 路径并存(旧版 dsh 仍走 mux)。
|
|
2922
|
+
useEffect(() => {
|
|
2923
|
+
const svc = props.sessions
|
|
2924
|
+
const list = svc && svc.list
|
|
2925
|
+
if (!list || typeof list.getSnapshot !== 'function' || typeof list.subscribe !== 'function') return undefined
|
|
2926
|
+
const collect = () => {
|
|
2927
|
+
const snap = list.getSnapshot()
|
|
2928
|
+
const byId = snap && snap.byId
|
|
2929
|
+
if (!byId) return
|
|
2930
|
+
const map = {}
|
|
2931
|
+
for (const id of Object.keys(byId)) {
|
|
2932
|
+
const t = byId[id] && byId[id].title
|
|
2933
|
+
if (typeof t === 'string' && t !== '') map[id] = t
|
|
2934
|
+
}
|
|
2935
|
+
setLiveTitles((prev) => {
|
|
2936
|
+
const pk = Object.keys(prev)
|
|
2937
|
+
const nk = Object.keys(map)
|
|
2938
|
+
if (pk.length === nk.length && nk.every((k) => prev[k] === map[k])) return prev
|
|
2939
|
+
return map
|
|
2940
|
+
})
|
|
2941
|
+
}
|
|
2942
|
+
collect()
|
|
2943
|
+
return list.subscribe(collect)
|
|
2944
|
+
}, [props.sessions])
|
|
2744
2945
|
|
|
2745
2946
|
return detail === null
|
|
2746
2947
|
? h(SessionFlowOverview, {
|
|
@@ -2826,17 +3027,74 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2826
3027
|
return undefined
|
|
2827
3028
|
}
|
|
2828
3029
|
|
|
3030
|
+
// ── 共享:纯色矢量图标 + 轮次 meta chips(rail 增强层与钢琴键详情框共用)──
|
|
3031
|
+
// stroke=currentColor 随主题取色;不用 emoji(用户裁定 2026-09-05)。
|
|
3032
|
+
const SF_ICON_PATHS = {
|
|
3033
|
+
clock: '<circle cx="8" cy="8" r="6.3"/><path d="M8 4.6V8l2.4 1.5"/>',
|
|
3034
|
+
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"/>',
|
|
3035
|
+
alert: '<circle cx="8" cy="8" r="6.3"/><path d="M8 5.2v3.2M8 11.2h.01"/>',
|
|
3036
|
+
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"/>',
|
|
3037
|
+
}
|
|
3038
|
+
const sfIcon = (name) => {
|
|
3039
|
+
const span = document.createElement('span')
|
|
3040
|
+
span.className = 'sf-ico'
|
|
3041
|
+
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>'
|
|
3042
|
+
return span
|
|
3043
|
+
}
|
|
3044
|
+
// 时长格式化(ms → 42s / 2m 14s;与 host 导出 fmtDur 同风格)。
|
|
3045
|
+
const sfFmtDur = (ms) => {
|
|
3046
|
+
if (!ms || ms <= 0) return ''
|
|
3047
|
+
const s = Math.round(ms / 1000)
|
|
3048
|
+
if (s < 60) return s + 's'
|
|
3049
|
+
const m = Math.floor(s / 60)
|
|
3050
|
+
return s % 60 === 0 ? m + 'm' : m + 'm ' + (s % 60) + 's'
|
|
3051
|
+
}
|
|
3052
|
+
// meta chips:耗时 / 工具调用 / 错误(红)/ 思考标记。无任何信息时返回 null。
|
|
3053
|
+
const buildTurnMeta = (entry) => {
|
|
3054
|
+
const meta = document.createElement('div')
|
|
3055
|
+
meta.className = 'sf-turnMeta'
|
|
3056
|
+
const chip = (icon, text, cls) => {
|
|
3057
|
+
const c = document.createElement('span')
|
|
3058
|
+
c.className = 'sf-turnMetaChip' + (cls ? ' ' + cls : '')
|
|
3059
|
+
c.appendChild(sfIcon(icon))
|
|
3060
|
+
const t = document.createElement('span')
|
|
3061
|
+
t.textContent = text
|
|
3062
|
+
c.appendChild(t)
|
|
3063
|
+
meta.appendChild(c)
|
|
3064
|
+
}
|
|
3065
|
+
if (entry.durMs > 0) chip('clock', sfFmtDur(entry.durMs))
|
|
3066
|
+
if (entry.tools > 0) chip('tool', STR.railMetaCalls.replace('{tools}', String(entry.tools)))
|
|
3067
|
+
if (entry.errors > 0) chip('alert', STR.railMetaErrors.replace('{errors}', String(entry.errors)), 'err')
|
|
3068
|
+
if (entry.thinking) chip('think', STR.railThinking)
|
|
3069
|
+
return meta.childNodes.length > 0 ? meta : null
|
|
3070
|
+
}
|
|
3071
|
+
// lightTurn → 共享字段(tools/errors/durMs/thinking/conclusion)。
|
|
3072
|
+
const ltMeta = (lt) => ({
|
|
3073
|
+
tools: lt.toolCount || 0,
|
|
3074
|
+
errors: lt.errorCount || 0,
|
|
3075
|
+
durMs: (typeof lt.startTime === 'number' && typeof lt.endTime === 'number' && lt.endTime > lt.startTime) ? lt.endTime - lt.startTime : 0,
|
|
3076
|
+
thinking: lt.hasThinking === true,
|
|
3077
|
+
})
|
|
3078
|
+
// getTurn → 工具分布文本(top4:pwsh ×6 · edit ×3);共享给 rail 与钢琴键。
|
|
3079
|
+
const toolDistOf = (turn) => {
|
|
3080
|
+
const counts = new Map()
|
|
3081
|
+
for (const st of (turn && turn.steps) || []) for (const c of st.toolCalls || []) counts.set(c.name, (counts.get(c.name) || 0) + 1)
|
|
3082
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 4).map(([n, c]) => n + ' ×' + c).join(' · ')
|
|
3083
|
+
}
|
|
3084
|
+
|
|
2829
3085
|
// ── M11:本会话轮次导航(会话页左侧常驻悬浮条目条,v3)──────────
|
|
2830
3086
|
// 范围:只显示【当前会话】的对话轮次(用户消息),不跨会话/不跨工作区。
|
|
2831
|
-
// 数据:官方消息行锚点 DOM 驱动(data-
|
|
2832
|
-
//
|
|
3087
|
+
// 数据:官方消息行锚点 DOM 驱动(v0.1.2 起 [data-chat-flow] [data-chat-flow-kind=user],
|
|
3088
|
+
// 旧版回退 data-time-hover-root;零 RPC 实时)。
|
|
2833
3089
|
// 轮次 → 结论预览:按顺序索引映射 host get 的 lightTurns(用户消息摊平)。
|
|
2834
3090
|
// 交互:点击滚动定位该轮、active 跟随视口第一条、窗口裁剪 + 滚轮/▲▼、
|
|
2835
3091
|
// 键盘 ↑↓/Enter、hover 预览卡(该轮用户消息 + 结论)、右键唤起会话流工作台。
|
|
2836
3092
|
// 样式:DeepSeek 网页版右侧悬浮消息导航风格(圆角卡片 + 单行截断文字),移到左侧。
|
|
2837
3093
|
function TurnKeys(props) {
|
|
2838
3094
|
const { sessions, onOpenWorkbench } = props
|
|
2839
|
-
const [entries, setEntries] = useState([]) // [{turn, seq, preview}] host 全量用户轮次
|
|
3095
|
+
const [entries, setEntries] = useState([]) // [{turn, seq, preview, tools, errors, durMs, thinking}] host 全量用户轮次
|
|
3096
|
+
const distCache = useRef(new Map()) // turn → 工具分布文本(getTurn 懒加载缓存)
|
|
3097
|
+
const [, setDistTick] = useState(0) // distCache 到位后触发详情框重渲染
|
|
2840
3098
|
const [activeIdx, setActiveIdx] = useState(-1)
|
|
2841
3099
|
// T5 平滑滚动:连续滚动位置(浮点,单位=行)。替代原整数 winOffset 的「每格硬跳
|
|
2842
3100
|
// 1 条」——滚轮增量累积(deltaMode 归一)+ 列表 transform 百分比位移(transition
|
|
@@ -2918,7 +3176,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2918
3176
|
const flat = []
|
|
2919
3177
|
for (const lt of json.lightTurns || []) {
|
|
2920
3178
|
for (const u of lt.userMessages || []) {
|
|
2921
|
-
flat.push({ turn: lt.turn, seq: u.seq, preview: u.preview })
|
|
3179
|
+
flat.push({ turn: lt.turn, seq: u.seq, preview: u.preview, ...ltMeta(lt) })
|
|
2922
3180
|
}
|
|
2923
3181
|
}
|
|
2924
3182
|
// 防御:同一 seq 只保留一条(避免同一条消息重复渲染 = 「最后一条显示两次」)。
|
|
@@ -2962,10 +3220,18 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2962
3220
|
return () => { alive = false; if (iv) clearInterval(iv); if (flowObs) flowObs.disconnect() }
|
|
2963
3221
|
}, [currentId])
|
|
2964
3222
|
|
|
2965
|
-
// DOM
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
3223
|
+
// DOM 行工具:用户消息行锚点。v0.1.2 起官方会话流重构进 dsh-client-ui-chat:
|
|
3224
|
+
// data-time-hover-root 移除,行结构 = [data-chat-flow] > [data-chat-flow-key]
|
|
3225
|
+
// (data-chat-flow-kind 区分 user/steering/turn-process/…,官方内部查行同款
|
|
3226
|
+
// 选择器);旧版 dsh 回退 data-time-hover-root 锚点(dsh-navbar 同款机制)。
|
|
3227
|
+
// 零 RPC 实时。steering(随转消息)不算新轮次,不计入。
|
|
3228
|
+
const domRows = () => {
|
|
3229
|
+
const next = document.querySelectorAll('[data-chat-flow] [data-chat-flow-kind="user"]')
|
|
3230
|
+
if (next.length > 0) return [...next]
|
|
3231
|
+
return [...document.querySelectorAll('[data-time-hover-root]')]
|
|
3232
|
+
.filter((r) => !r.hasAttribute('data-pending-steering'))
|
|
3233
|
+
.filter((r) => !r.hasAttribute('data-turn-tail') && r.querySelector('[class*=bubble]') !== null)
|
|
3234
|
+
}
|
|
2969
3235
|
const textOf = (row) => {
|
|
2970
3236
|
const b = row.querySelector('[class*=bubble]')
|
|
2971
3237
|
return b ? String(b.textContent || '').replace(/\s+/g, ' ').trim() : ''
|
|
@@ -3010,16 +3276,74 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
3010
3276
|
return null
|
|
3011
3277
|
}
|
|
3012
3278
|
|
|
3279
|
+
// 未加载轮次 → 委托官方轮次导航引擎(v0.1.2+ 吸收官方独有能力):合成 click
|
|
3280
|
+
// 命中目标刻度,官方 navigate 自动加载未载入页并跳转(其引擎含 busy 态/加载锚定,
|
|
3281
|
+
// 优于我们的「只提示」)。合成事件 clientY 按官方 itemAtPointer 几何反解
|
|
3282
|
+
// (offset = clientY - rect.top + scrollTop - 6 = idx*10);官方处理挂在 nav 的
|
|
3283
|
+
// onClick 且只读 event.clientY,与事件落在哪个子元素无关(刻度按钮
|
|
3284
|
+
// pointer-events:none,不能直接 btn.click()——clientY=0 会误算成第一条)。
|
|
3285
|
+
const delegateToRail = (entry) => {
|
|
3286
|
+
try {
|
|
3287
|
+
const nav = document.querySelector(RAIL_SELECTOR)
|
|
3288
|
+
if (!nav) return false
|
|
3289
|
+
const turns = [...nav.querySelectorAll('button[aria-label]')]
|
|
3290
|
+
.map((b) => { const m = /(\d+)/.exec(b.getAttribute('aria-label') || ''); return m ? Number(m[1]) : null })
|
|
3291
|
+
const idx = turns.indexOf(entry.turn)
|
|
3292
|
+
if (idx < 0) return false
|
|
3293
|
+
const rect = nav.getBoundingClientRect()
|
|
3294
|
+
const scroller = nav.firstElementChild
|
|
3295
|
+
const st = scroller ? scroller.scrollTop : 0
|
|
3296
|
+
const y = rect.top + (idx * 10 + 6) - st
|
|
3297
|
+
nav.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: rect.left + 10, clientY: y }))
|
|
3298
|
+
return true
|
|
3299
|
+
} catch (e) { return false }
|
|
3300
|
+
}
|
|
3013
3301
|
// 点击定位:先查该轮次是否已加载(DOM 中能否匹配到对应行)——
|
|
3014
|
-
// 已加载 → 平滑滚动定位;未加载 →
|
|
3015
|
-
//
|
|
3302
|
+
// 已加载 → 平滑滚动定位;未加载 → 委托官方 rail 引擎加载跳转(v0.1.2+);
|
|
3303
|
+
// rail 缺席(旧版 dsh)→ 只提示、不跳转(自动加载循环会让消息流乱滚/跳错
|
|
3304
|
+
// 位置,实测踩坑;用户需先在会话区向上滚动加载历史再点击)。
|
|
3016
3305
|
const open = (entry) => {
|
|
3017
3306
|
const row = findRow(entry)
|
|
3018
3307
|
if (row) { row.scrollIntoView({ behavior: 'smooth', block: 'start' }); return }
|
|
3308
|
+
if (delegateToRail(entry)) return
|
|
3019
3309
|
setJumpFail(true)
|
|
3020
3310
|
setTimeout(() => setJumpFail(false), 2600)
|
|
3021
3311
|
}
|
|
3022
3312
|
|
|
3313
|
+
// 详情框 meta 注入(与 rail 增强层同款信息:矢量 chips + 工具分布行;
|
|
3314
|
+
// imperative ref 填充,data-sig 幂等)。工具分布懒加载:详情驻留 200ms 拉
|
|
3315
|
+
// getTurn 聚合(distCache 跨 hover 缓存,到位后 tick 触发 ref 重填)。
|
|
3316
|
+
const fillDetailMeta = (el, entry) => {
|
|
3317
|
+
if (!el) return
|
|
3318
|
+
const dist = distCache.current.get(entry.turn) || ''
|
|
3319
|
+
const sig = entry.turn + '|' + dist
|
|
3320
|
+
if (el.dataset.sig === sig) return
|
|
3321
|
+
el.dataset.sig = sig
|
|
3322
|
+
el.replaceChildren()
|
|
3323
|
+
const meta = buildTurnMeta(entry)
|
|
3324
|
+
if (meta) el.appendChild(meta)
|
|
3325
|
+
if (dist !== '') {
|
|
3326
|
+
const tools = document.createElement('div')
|
|
3327
|
+
tools.className = 'sf-rail-conclusionTools'
|
|
3328
|
+
tools.textContent = dist
|
|
3329
|
+
el.appendChild(tools)
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
useEffect(() => {
|
|
3333
|
+
if (detailIdx === null || !currentId) return undefined
|
|
3334
|
+
const entry = entries[detailIdx]
|
|
3335
|
+
if (!entry || entry.tools === 0 || distCache.current.has(entry.turn)) return undefined
|
|
3336
|
+
const turn = entry.turn
|
|
3337
|
+
const timer = setTimeout(() => {
|
|
3338
|
+
api('getTurn', { sessionId: currentId, turn }).then((json) => {
|
|
3339
|
+
if (!json || !json.ok || !json.turn) return
|
|
3340
|
+
distCache.current.set(turn, toolDistOf(json.turn))
|
|
3341
|
+
setDistTick((n) => n + 1)
|
|
3342
|
+
}).catch(() => {})
|
|
3343
|
+
}, 200)
|
|
3344
|
+
return () => clearTimeout(timer)
|
|
3345
|
+
}, [detailIdx, currentId, entries])
|
|
3346
|
+
|
|
3023
3347
|
// active:视口内最靠上的可见行 → 映射条目索引高亮(阅读位置跟随)。
|
|
3024
3348
|
// 可见判定用 bottom 进入视口(含被顶部裁切一半的行)——此前 top>=0 会
|
|
3025
3349
|
// 跳过被裁切行导致高亮偏晚一轮(T1 修复点二)。
|
|
@@ -3328,6 +3652,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
3328
3652
|
// 常驻渲染 + hidden 类做滑入/滑出;内容 key 变化触发 pkSlideIn。
|
|
3329
3653
|
h('div', { className: 'pk-detail' + (detailEntry ? '' : ' hidden'), style: detailPos, onMouseEnter: () => showDetail(detailIdx), onMouseLeave: hideDetail },
|
|
3330
3654
|
detailEntry && h('div', { key: 'h' + detailEntry.turn, className: 'pk-detailHead' }, 'T' + detailEntry.turn + ' · ' + STR.user),
|
|
3655
|
+
detailEntry && h('div', { key: 'm' + detailEntry.turn, className: 'pk-detailMeta', ref: (el) => fillDetailMeta(el, detailEntry) }),
|
|
3331
3656
|
detailEntry && h('div', { key: 'b' + detailEntry.turn, className: 'pk-detailBody' }, detailEntry.preview || '(空)'),
|
|
3332
3657
|
),
|
|
3333
3658
|
// 跳转状态提示:常驻渲染 + hidden 类(从下方淡入/淡出);未加载轮次提示。
|
|
@@ -3380,14 +3705,13 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
3380
3705
|
}
|
|
3381
3706
|
if (liveStore.emit !== null) liveStore.emit({ ...liveStateKeep(), phase: 'loading' })
|
|
3382
3707
|
try {
|
|
3383
|
-
const
|
|
3384
|
-
|
|
3385
|
-
if (val === undefined || !Array.isArray(val.events)) {
|
|
3708
|
+
const val = sfRemoteRef.current ? await sfRemoteRef.current.pageBySession(sid, 400) : undefined
|
|
3709
|
+
if (val === undefined) {
|
|
3386
3710
|
liveStore.lastState = { ...liveStateKeep(), phase: 'error', error: STR.liveFail }
|
|
3387
3711
|
if (liveStore.emit !== null) liveStore.emit(liveStore.lastState)
|
|
3388
3712
|
return
|
|
3389
3713
|
}
|
|
3390
|
-
const events = val
|
|
3714
|
+
const events = historyEventsOf(val)
|
|
3391
3715
|
const derived = await api('derive', { events, now: Date.now() })
|
|
3392
3716
|
if (!derived || !derived.ok) {
|
|
3393
3717
|
liveStore.lastState = { ...liveStateKeep(), phase: 'error', error: (derived && derived.error) || STR.liveFail }
|
|
@@ -3626,11 +3950,233 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
3626
3950
|
)
|
|
3627
3951
|
}
|
|
3628
3952
|
|
|
3953
|
+
// ── M11×v0.1.2:官方轮次导航(TurnNavigatorRail)增强层 ─────────────
|
|
3954
|
+
// 官方 rail 无扩展槽位(chat 槽位仅 node/turnTail/assistant-actions/commandview),
|
|
3955
|
+
// 本层走 DOM 观察(家族传统:观察器 + 自愈重挂),挂载我们的独有增量:
|
|
3956
|
+
// 1) hover 结论段注入官方预览卡:该轮「结论摘要」(官方预览只有 prompt/response
|
|
3957
|
+
// 片段)+ 工具统计(错误角标方案已撤——开发会话多数轮次都有工具错误,
|
|
3958
|
+
// 刻度全员红点等于无信息,用户实机裁定 2026-09-05);
|
|
3959
|
+
// 2) 右键 rail → 唤起会话流工作台。
|
|
3960
|
+
// 定位:nav[aria-label=轮次导航|Turn navigation];几何换算复刻官方 itemAtPointer
|
|
3961
|
+
// (TURN_SPACING 10px / RAIL_INSET 6px + scroller.scrollTop);turn 号从刻度按钮
|
|
3962
|
+
// aria-label(跳转到第 N 轮 / Jump to turn N)提取,索引↔turn 精确映射,不依赖
|
|
3963
|
+
// 「index+1 == turn」假设。官方 navigate 引擎(含未载入轮次自动加载跳转)原样保留,
|
|
3964
|
+
// 我们不拦截点击。
|
|
3965
|
+
const RAIL_SELECTOR = 'nav[aria-label="轮次导航"], nav[aria-label="Turn navigation"]'
|
|
3966
|
+
function RailEnhancer(props) {
|
|
3967
|
+
const { sessions, railEl, onOpenWorkbench } = props
|
|
3968
|
+
const [turnData, setTurnData] = useState([]) // [{turn, preview, conclusion, tools, errors}]
|
|
3969
|
+
const [hoverInfo, setHoverInfo] = useState(null) // {turn, clientY, railLeft}
|
|
3970
|
+
const [, force] = useState(0)
|
|
3971
|
+
|
|
3972
|
+
// 订阅会话列表变化:切换会话 → 重拉条目。
|
|
3973
|
+
useEffect(() => {
|
|
3974
|
+
if (sessions && sessions.list && typeof sessions.list.subscribe === 'function') {
|
|
3975
|
+
return sessions.list.subscribe(() => force((n) => n + 1))
|
|
3976
|
+
}
|
|
3977
|
+
return undefined
|
|
3978
|
+
}, [sessions])
|
|
3979
|
+
|
|
3980
|
+
const currentId = (() => {
|
|
3981
|
+
try {
|
|
3982
|
+
const s = sessions && sessions.list && typeof sessions.list.getSnapshot === 'function' ? sessions.list.getSnapshot() : null
|
|
3983
|
+
return s && s.current ? s.current : null
|
|
3984
|
+
} catch (e) { return null }
|
|
3985
|
+
})()
|
|
3986
|
+
|
|
3987
|
+
// 条目数据:host get lightTurns(每轮一条,对齐官方 rail item 粒度)。
|
|
3988
|
+
// 轮询节奏同钢琴键:DOM 变化切 3s 高频,空闲 15s 兜底。
|
|
3989
|
+
useEffect(() => {
|
|
3990
|
+
if (!currentId) { setTurnData([]); return undefined }
|
|
3991
|
+
let alive = true
|
|
3992
|
+
const refresh = () => {
|
|
3993
|
+
api('get', { sessionId: currentId }).then((json) => {
|
|
3994
|
+
if (!alive || !json || !json.ok) return
|
|
3995
|
+
const rows = (json.lightTurns || []).map((lt) => ({
|
|
3996
|
+
turn: lt.turn,
|
|
3997
|
+
preview: lt.userMessages && lt.userMessages[0] ? String(lt.userMessages[0].preview || '') : '',
|
|
3998
|
+
conclusion: String(lt.conclusionPreview || ''),
|
|
3999
|
+
tools: lt.toolCount || 0,
|
|
4000
|
+
errors: lt.errorCount || 0,
|
|
4001
|
+
durMs: (typeof lt.startTime === 'number' && typeof lt.endTime === 'number' && lt.endTime > lt.startTime) ? lt.endTime - lt.startTime : 0,
|
|
4002
|
+
thinking: lt.hasThinking === true,
|
|
4003
|
+
}))
|
|
4004
|
+
setTurnData((prev) => {
|
|
4005
|
+
if (prev.length === rows.length && prev.length > 0 && prev[prev.length - 1].turn === rows[rows.length - 1].turn) return prev
|
|
4006
|
+
return rows
|
|
4007
|
+
})
|
|
4008
|
+
}).catch(() => {})
|
|
4009
|
+
}
|
|
4010
|
+
const ACTIVE_MS = 3000
|
|
4011
|
+
const IDLE_MS = 15000
|
|
4012
|
+
let iv = null
|
|
4013
|
+
const schedule = (delay) => { if (iv) clearInterval(iv); iv = setInterval(refresh, delay) }
|
|
4014
|
+
let flowObs = null
|
|
4015
|
+
let lastDomRefresh = 0
|
|
4016
|
+
const onDomChange = () => {
|
|
4017
|
+
if (!alive) return
|
|
4018
|
+
const now = Date.now()
|
|
4019
|
+
if (now - lastDomRefresh < 1000) return
|
|
4020
|
+
lastDomRefresh = now
|
|
4021
|
+
refresh()
|
|
4022
|
+
schedule(ACTIVE_MS)
|
|
4023
|
+
}
|
|
4024
|
+
const flowEl = document.querySelector('[data-chat-flow=""]') || document.querySelector('[data-chat-flow]')
|
|
4025
|
+
if (flowEl) {
|
|
4026
|
+
flowObs = new MutationObserver(onDomChange)
|
|
4027
|
+
flowObs.observe(flowEl, { childList: true, subtree: true })
|
|
4028
|
+
}
|
|
4029
|
+
refresh()
|
|
4030
|
+
schedule(IDLE_MS)
|
|
4031
|
+
return () => { alive = false; if (iv) clearInterval(iv); if (flowObs) flowObs.disconnect() }
|
|
4032
|
+
}, [currentId])
|
|
4033
|
+
|
|
4034
|
+
// rail 交互:hover 几何换算 + 右键唤起工作台。
|
|
4035
|
+
useEffect(() => {
|
|
4036
|
+
if (!railEl) { setHoverInfo(null); return undefined }
|
|
4037
|
+
const scroller = railEl.firstElementChild // 官方结构:nav.frame > div.scroller
|
|
4038
|
+
const railTurns = () => [...railEl.querySelectorAll('button[aria-label]')]
|
|
4039
|
+
.map((b) => { const m = /(\d+)/.exec(b.getAttribute('aria-label') || ''); return m ? Number(m[1]) : null })
|
|
4040
|
+
.filter((n) => n !== null)
|
|
4041
|
+
const onMove = (e) => {
|
|
4042
|
+
const rect = railEl.getBoundingClientRect()
|
|
4043
|
+
const st = scroller ? scroller.scrollTop : 0
|
|
4044
|
+
const idx = Math.max(0, Math.round((e.clientY - rect.top + st - 6) / 10))
|
|
4045
|
+
const turns = railTurns()
|
|
4046
|
+
const turn = idx < turns.length ? turns[idx] : null
|
|
4047
|
+
setHoverInfo((prev) => {
|
|
4048
|
+
const prevTurn = prev === null ? null : prev.turn
|
|
4049
|
+
return prevTurn === turn ? prev : (turn === null ? null : { turn })
|
|
4050
|
+
})
|
|
4051
|
+
}
|
|
4052
|
+
const onLeave = () => setHoverInfo(null)
|
|
4053
|
+
const onContext = (e) => { e.preventDefault(); onOpenWorkbench() }
|
|
4054
|
+
railEl.addEventListener('pointermove', onMove)
|
|
4055
|
+
railEl.addEventListener('pointerleave', onLeave)
|
|
4056
|
+
railEl.addEventListener('contextmenu', onContext)
|
|
4057
|
+
return () => {
|
|
4058
|
+
railEl.removeEventListener('pointermove', onMove)
|
|
4059
|
+
railEl.removeEventListener('pointerleave', onLeave)
|
|
4060
|
+
railEl.removeEventListener('contextmenu', onContext)
|
|
4061
|
+
}
|
|
4062
|
+
}, [railEl, onOpenWorkbench])
|
|
4063
|
+
|
|
4064
|
+
// 工具分布懒加载(独特信息维度,官方 preview 只有 prompt/response 片段):
|
|
4065
|
+
// hover 驻留 250ms 后拉 getTurn,聚合工具名 ×次数 top4(防扫过时每刻度一请求)。
|
|
4066
|
+
const [toolDist, setToolDist] = useState(null) // {turn, text}
|
|
4067
|
+
useEffect(() => {
|
|
4068
|
+
if (!railEl || hoverInfo === null || !currentId) { setToolDist(null); return undefined }
|
|
4069
|
+
const turn = hoverInfo.turn
|
|
4070
|
+
const timer = setTimeout(() => {
|
|
4071
|
+
api('getTurn', { sessionId: currentId, turn }).then((json) => {
|
|
4072
|
+
if (!json || !json.ok || !json.turn) return
|
|
4073
|
+
const text = toolDistOf(json.turn)
|
|
4074
|
+
setToolDist((prev) => (prev && prev.turn === turn && prev.text === text) ? prev : { turn, text })
|
|
4075
|
+
}).catch(() => {})
|
|
4076
|
+
}, 250)
|
|
4077
|
+
return () => clearTimeout(timer)
|
|
4078
|
+
}, [railEl, hoverInfo, currentId])
|
|
4079
|
+
|
|
4080
|
+
// hover 增强段注入官方预览卡内部(单卡融合——另起一卡会与官方预览上下堆叠成
|
|
4081
|
+
// 两大块,实测很丑)。官方 preview 容器 = rail 内 class 以 _preview 结尾的元素
|
|
4082
|
+
//(CSS module hash 前缀 + preview/previewPrompt/previewResponse 三段,用
|
|
4083
|
+
// /(?:^|_)preview(?:$|\s)/ 精确命中容器);React 复用 preview 元素跨轮次渲染 →
|
|
4084
|
+
// data-sig 核对(轮次+工具分布到位状态),变化即重建;observer 监视 rail 子树,
|
|
4085
|
+
// preview 出现/消失/被重渲染时自愈。**独特性原则(用户裁定 2026-09-05):只给
|
|
4086
|
+
// 官方 preview 没有的信息维度——耗时 / 工具统计与分布 / 错误 / 思考标记;
|
|
4087
|
+
// 结论不再展示(与官方 response 片段重复,用户二次裁定撤下);图标一律纯色
|
|
4088
|
+
// 矢量(stroke=currentColor),不用 emoji**。
|
|
4089
|
+
// 高度解禁:官方 preview 固定 --turn-preview-height:100px 且溢出裁剪——注入段
|
|
4090
|
+
// 会被裁成「残缺卡」(实测踩坑);注入期间内联 height:auto + maxHeight 放开,
|
|
4091
|
+
// 撤注入时还原内联样式。
|
|
4092
|
+
useEffect(() => {
|
|
4093
|
+
if (!railEl) return undefined
|
|
4094
|
+
const findPreview = () => [...railEl.querySelectorAll('[class*=preview]')]
|
|
4095
|
+
.find((el) => /(?:^|_)preview(?:$|\s)/.test(String(el.className)))
|
|
4096
|
+
const cleanup = () => {
|
|
4097
|
+
for (const n of railEl.querySelectorAll('.sf-rail-conclusion')) n.remove()
|
|
4098
|
+
const p = findPreview()
|
|
4099
|
+
if (p) { p.style.height = ''; p.style.maxHeight = ''; p.style.overflow = '' }
|
|
4100
|
+
}
|
|
4101
|
+
const enhance = () => {
|
|
4102
|
+
const preview = findPreview()
|
|
4103
|
+
const turn = hoverInfo === null ? null : hoverInfo.turn
|
|
4104
|
+
const entry = turn === null ? undefined : turnData.find((t) => t.turn === turn)
|
|
4105
|
+
if (!preview || !entry || (entry.durMs === 0 && entry.tools === 0)) { cleanup(); return }
|
|
4106
|
+
preview.style.height = 'auto'
|
|
4107
|
+
preview.style.maxHeight = '280px'
|
|
4108
|
+
preview.style.overflow = 'hidden'
|
|
4109
|
+
let node = preview.querySelector(':scope > .sf-rail-conclusion')
|
|
4110
|
+
if (!node) {
|
|
4111
|
+
node = document.createElement('div')
|
|
4112
|
+
node.className = 'sf-rail-conclusion'
|
|
4113
|
+
preview.appendChild(node)
|
|
4114
|
+
}
|
|
4115
|
+
const dist = toolDist !== null && toolDist.turn === entry.turn ? toolDist.text : ''
|
|
4116
|
+
const sig = entry.turn + '|' + dist
|
|
4117
|
+
if (node.dataset.sig === sig) return
|
|
4118
|
+
node.dataset.sig = sig
|
|
4119
|
+
node.replaceChildren()
|
|
4120
|
+
// meta chips 行(纯色矢量图标):耗时 · n 次工具 · m 错误(红)· 含思考
|
|
4121
|
+
const meta = buildTurnMeta(entry)
|
|
4122
|
+
if (meta) node.appendChild(meta)
|
|
4123
|
+
// 工具分布行(懒加载到位后经 sig 变化重建本节点)
|
|
4124
|
+
if (dist !== '') {
|
|
4125
|
+
const tools = document.createElement('div')
|
|
4126
|
+
tools.className = 'sf-rail-conclusionTools'
|
|
4127
|
+
tools.textContent = dist
|
|
4128
|
+
node.appendChild(tools)
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
enhance()
|
|
4132
|
+
const obs = new MutationObserver(enhance)
|
|
4133
|
+
obs.observe(railEl, { childList: true, subtree: true })
|
|
4134
|
+
return () => { obs.disconnect(); cleanup() }
|
|
4135
|
+
}, [railEl, hoverInfo, turnData, toolDist])
|
|
4136
|
+
|
|
4137
|
+
return null
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
// ── 导航总挂载:官方 rail 增强层 ⇄ 经典钢琴键悬浮条 ────────────────
|
|
4141
|
+
// 默认(v0.1.2+,用户裁定 2026-09-05):经典悬浮条默认开启(pianoClassicStrip
|
|
4142
|
+
// 默认 true),与官方 rail 增强层并存——左右双导航各司其职(我们:预览卡/右键
|
|
4143
|
+
// 工作台/滚轮窗口/键盘;官方:完整历史刻度);设置里可各自关闭。旧版 dsh
|
|
4144
|
+
// (无官方 rail)railEl=null 时 classic 恒 true,功能不丢。
|
|
4145
|
+
function NavRoot(props) {
|
|
4146
|
+
const { settingsCtrl } = props
|
|
4147
|
+
const [, force] = useState(0)
|
|
4148
|
+
const [railEl, setRailEl] = useState(null)
|
|
4149
|
+
useEffect(() => (settingsCtrl ? settingsCtrl.subscribe(() => force((n) => n + 1)) : undefined), [settingsCtrl])
|
|
4150
|
+
useEffect(() => {
|
|
4151
|
+
const find = () => {
|
|
4152
|
+
const nav = document.querySelector(RAIL_SELECTOR)
|
|
4153
|
+
setRailEl((prev) => (prev === nav || (prev === null && nav === null) ? prev : nav))
|
|
4154
|
+
}
|
|
4155
|
+
find()
|
|
4156
|
+
const obs = new MutationObserver(find)
|
|
4157
|
+
obs.observe(document.body, { childList: true, subtree: true })
|
|
4158
|
+
return () => obs.disconnect()
|
|
4159
|
+
}, [])
|
|
4160
|
+
const classicWanted = sfSettings.current.pianoClassicStrip === true
|
|
4161
|
+
const enhanceWanted = sfSettings.current.railEnhance !== false
|
|
4162
|
+
// railHideOfficial:完全屏蔽官方刻度条(只留我们的悬浮条);隐藏期间增强层不挂。
|
|
4163
|
+
const hideOfficial = sfSettings.current.railHideOfficial === true
|
|
4164
|
+
useEffect(() => {
|
|
4165
|
+
if (railEl) railEl.style.display = hideOfficial ? 'none' : ''
|
|
4166
|
+
return () => { if (railEl) railEl.style.display = '' }
|
|
4167
|
+
}, [railEl, hideOfficial])
|
|
4168
|
+
const showClassic = classicWanted || railEl === null
|
|
4169
|
+
const showEnhance = enhanceWanted && railEl !== null && !hideOfficial
|
|
4170
|
+
return h(React.Fragment, null,
|
|
4171
|
+
showClassic ? h(TurnKeys, { sessions: props.sessions, connection: props.connection, onOpenWorkbench: props.onOpenWorkbench }) : null,
|
|
4172
|
+
showEnhance ? h(RailEnhancer, { sessions: props.sessions, railEl, onOpenWorkbench: props.onOpenWorkbench }) : null)
|
|
4173
|
+
}
|
|
4174
|
+
|
|
3629
4175
|
// ── M11:钢琴键竖条挂载(DOM 注入 + 自愈)────────────────────────
|
|
3630
4176
|
// 容器挂 document.body(dsh-navbar 同款模式):完全脱离会话区布局树,
|
|
3631
4177
|
// 不干扰 conversation 的 flex/grid 布局;fixed 定位由组件自管理。
|
|
3632
4178
|
// MutationObserver 自愈:容器丢失(布局重建)时自动恢复。
|
|
3633
|
-
function mountPianoKeys(controller, sessions, connection) {
|
|
4179
|
+
function mountPianoKeys(controller, sessions, connection, settingsCtrl) {
|
|
3634
4180
|
let root = undefined
|
|
3635
4181
|
let container = undefined
|
|
3636
4182
|
let waitObserver = null
|
|
@@ -3654,9 +4200,10 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
3654
4200
|
document.body.appendChild(container)
|
|
3655
4201
|
try {
|
|
3656
4202
|
root = createRoot(container)
|
|
3657
|
-
root.render(h(
|
|
4203
|
+
root.render(h(NavRoot, {
|
|
3658
4204
|
sessions,
|
|
3659
4205
|
connection,
|
|
4206
|
+
settingsCtrl: settingsCtrl || null,
|
|
3660
4207
|
onOpenWorkbench: () => controller.open(),
|
|
3661
4208
|
}))
|
|
3662
4209
|
syncChatVisibility()
|
|
@@ -3824,7 +4371,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
3824
4371
|
}
|
|
3825
4372
|
|
|
3826
4373
|
// ── 插件体 ─────────────────────────────────────────────────────────
|
|
3827
|
-
const inject = ['sessions', 'connection', 'slots', 'layout']
|
|
4374
|
+
const inject = ['sessions', 'connection', 'slots', 'layout', 'remote', 'remote.session']
|
|
3828
4375
|
|
|
3829
4376
|
function apply(ctx) {
|
|
3830
4377
|
try {
|
|
@@ -3838,6 +4385,11 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
3838
4385
|
|
|
3839
4386
|
const sessions = ctx.get('sessions')
|
|
3840
4387
|
const connection = ctx.get('connection')
|
|
4388
|
+
// v0.1.2 RPC 双面适配(remote.session 新面优先,connection 旧面回退)。
|
|
4389
|
+
sfRemoteRef.current = sfBuildRemote(ctx, connection)
|
|
4390
|
+
// 设置控制器引用:settings.section 注册块内创建,挂载钢琴键/导航增强层时
|
|
4391
|
+
// 传给 NavRoot 做设置变更即时切换(不可用则为 null,走默认值)。
|
|
4392
|
+
let sfSettingsCtrl = null
|
|
3841
4393
|
|
|
3842
4394
|
const state = { open: false, listeners: new Set() }
|
|
3843
4395
|
const controller = {
|
|
@@ -3919,7 +4471,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
3919
4471
|
try {
|
|
3920
4472
|
const settingsBinder = ctx.get('webUiSettings') ?? ctx.get('settingsScope')
|
|
3921
4473
|
if (settingsBinder !== undefined && settingsBinder !== null && typeof settingsBinder.bind === 'function') {
|
|
3922
|
-
|
|
4474
|
+
sfSettingsCtrl = new SfSettingsController(settingsBinder.bind({ namespace: 'session-flow' }))
|
|
3923
4475
|
slots.inject('settings.section', () => {
|
|
3924
4476
|
const unregister = slots.register({
|
|
3925
4477
|
name: 'settings.section',
|
|
@@ -4047,7 +4599,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
4047
4599
|
disposers.push(mountSidebarEntry(controller))
|
|
4048
4600
|
disposers.push(mountBoard(controller, sessions, connection))
|
|
4049
4601
|
// M11:钢琴键会话快切(会话页左侧常驻竖条);挂载失败不影响其他功能。
|
|
4050
|
-
disposers.push(mountPianoKeys(controller, sessions, connection))
|
|
4602
|
+
disposers.push(mountPianoKeys(controller, sessions, connection, sfSettingsCtrl))
|
|
4051
4603
|
} catch (error) {
|
|
4052
4604
|
console.error('[dsh-session-flow] mount failed:', error)
|
|
4053
4605
|
}
|
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/lib/index-store.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from 'node:fs'
|
|
6
6
|
import { join } from 'node:path'
|
|
7
7
|
import { homedir } from 'node:os'
|
|
8
|
-
import { listSessionDirs, summarizeSessionFile } from './archive.js'
|
|
8
|
+
import { listSessionDirs, sessionFileOf, summarizeSessionFile } from './archive.js'
|
|
9
9
|
|
|
10
10
|
export function dshHome() {
|
|
11
11
|
return process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
@@ -22,8 +22,10 @@ export function workspaceIndexFile(home, wsName) {
|
|
|
22
22
|
return join(indexRoot(home), `index-${safe}.json`)
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/** 索引格式版本:升级时旧缓存会被自动全量重扫一次(避免字段缺失)。
|
|
26
|
-
|
|
25
|
+
/** 索引格式版本:升级时旧缓存会被自动全量重扫一次(避免字段缺失)。
|
|
26
|
+
* 5(2026-09-08):dsh v0.1.3 存档 v2 布局适配——存档文件改为 session.v2.jsonl.zstd
|
|
27
|
+
* (v1 冻结),强制全量重扫让摘要/统计全部来自 v2。 */
|
|
28
|
+
const INDEX_VERSION = 5
|
|
27
29
|
|
|
28
30
|
export function readIndex(home, wsName) {
|
|
29
31
|
try {
|
|
@@ -107,7 +109,9 @@ export function scanWorkspaceIndex(home, wsName, options = {}) {
|
|
|
107
109
|
return { index, scanned, skipped, removed }
|
|
108
110
|
}
|
|
109
111
|
|
|
110
|
-
/** 按会话 id 在所有工作区中定位存档(返回 { workspace, dir, file } 或 null)。
|
|
112
|
+
/** 按会话 id 在所有工作区中定位存档(返回 { workspace, dir, file } 或 null)。
|
|
113
|
+
* 文件定位统一走 sessionFileOf(v2 优先,0.1.3 适配——此前硬编码 v1 文件名,
|
|
114
|
+
* 纯 v2 新会话全部 404「not found in archives」,钢琴条/详情/检索静默失效)。 */
|
|
111
115
|
export function findWorkspaceOfSession(home, sessionId) {
|
|
112
116
|
const root = join(home, 'sessions')
|
|
113
117
|
if (!existsSync(root)) return null
|
|
@@ -115,10 +119,8 @@ export function findWorkspaceOfSession(home, sessionId) {
|
|
|
115
119
|
const wsDir = join(root, wsName)
|
|
116
120
|
try { if (!statSync(wsDir).isDirectory()) continue } catch { continue }
|
|
117
121
|
const dir = join(wsDir, sessionId)
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
120
|
-
const plain = join(dir, 'session.jsonl')
|
|
121
|
-
if (existsSync(plain)) return { workspace: wsName, dir, file: plain }
|
|
122
|
+
const file = sessionFileOf(dir)
|
|
123
|
+
if (file !== null) return { workspace: wsName, dir, file }
|
|
122
124
|
}
|
|
123
125
|
return null
|
|
124
126
|
}
|
package/package.json
CHANGED