dsh-session-flow 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +5 -2
- package/README.md +5 -2
- package/lib/client.js +1140 -213
- package/lib/host.js +122 -10
- package/package.json +5 -2
package/lib/client.js
CHANGED
|
@@ -13,7 +13,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
13
13
|
var module = { exports: {} }; var exports = module.exports;
|
|
14
14
|
|
|
15
15
|
const React = require('react')
|
|
16
|
-
const { useState, useEffect, useMemo, useRef } = React
|
|
16
|
+
const { useState, useEffect, useMemo, useRef, useSyncExternalStore } = React
|
|
17
17
|
const h = React.createElement
|
|
18
18
|
const { createRoot } = require('react-dom/client')
|
|
19
19
|
|
|
@@ -40,6 +40,8 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
40
40
|
pianoJumpFail: '该轮次尚未加载:请在会话区向上滚动加载历史后再点击',
|
|
41
41
|
fullText: '全文检索', fullTextSearching: '搜索中…', fullTextPartial: '已扫描 {N}/{M} 个会话(结果未完整)', fullTextNone: '全文无命中', fullTextFail: '全文检索失败',
|
|
42
42
|
dock: '并入右栏', dockExit: '退出并入', dockHint: '会话流已并入右侧栏 · 随当前会话切换', dockEmpty: '打开会话后可在此查看会话流',
|
|
43
|
+
dockLiveHint: '实时:只显示开播后的对话,不加载历史回合', dockLiveEmpty: '实时中 · 等待新对话…', dockLiveEnterFail: '当前会话已结束或不可实时',
|
|
44
|
+
dockPinNotice: '📌 直播已钉住原会话(当前已切换会话)', dockPinRebind: '转播当前会话',
|
|
43
45
|
rename: '重命名', renamePlaceholder: '输入新标题…', renameFail: '重命名失败', origTitle: '原名',
|
|
44
46
|
search: '搜索标题/工具/文件… 支持 tool: pwsh · file: src · err:', allWorkspaces: '全部工作区',
|
|
45
47
|
sortRecent: '最近运行', sortNewest: '最近创建', sortOldest: '最早创建', sortTools: '工具最多', sortLongest: '耗时最长',
|
|
@@ -72,12 +74,30 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
72
74
|
summaryStale: '⚠ 摘要生成后有新对话,内容可能已不准确,建议重新生成',
|
|
73
75
|
llmSummary: '生成 LLM 摘要', llmGenerating: '生成中…', llmFail: '生成失败', summaryRuleTag: '规则', summaryLlmTag: 'LLM',
|
|
74
76
|
cacheHint: '仅清理本插件的缓存(可自动重建),不影响 DSH 会话存档与任何其他数据', cacheLimit: '时间线缓存上限',
|
|
77
|
+
healthActive: '活跃', healthToolWait: '工具执行中', healthQuiet: '静默中', healthStalled: '疑似卡死',
|
|
78
|
+
healthSilentMin: '静默 {N} 分钟', healthSilentSec: '静默 {N} 秒',
|
|
79
|
+
healthChipTitle: '会话健康状态(会话流)· 点击打开详情',
|
|
80
|
+
renameClearBlocked: '该会话标题由官方会话管理:请输入新标题(不支持清空)',
|
|
81
|
+
settingsTitle: '会话流', settingsDesc: '调整轮次悬浮条与实时跟踪的行为参数。保存后立即生效(轮次悬浮条行数在重进会话页后完全生效)。',
|
|
82
|
+
setGroupPiano: '轮次悬浮条', setGroupLive: '实时跟踪', setGroupHealth: '健康监控',
|
|
83
|
+
setPianoWindow: '可见行数', setPianoWindowHint: '悬浮条同时显示的轮次条数(6–18)',
|
|
84
|
+
setPianoWheelSpeed: '滚轮灵敏度', setPianoWheelSpeedHint: '每像素滚动的行数(0.005–0.03),越大滚得越快',
|
|
85
|
+
setPianoSnapMs: '静止吸附延迟(ms)', setPianoSnapMsHint: '滚轮停止多少毫秒后吸附到整行(100–400)',
|
|
86
|
+
setLivePollMs: '轮询间隔(ms)', setLivePollMsHint: '实时模式的刷新间隔(1500–10000),下次开播生效',
|
|
87
|
+
setLiveFollowPx: '吸底阈值(px)', setLiveFollowPxHint: '距底部多少像素内视为「在底部」并跟随滚动(20–120)',
|
|
88
|
+
setLiveHistoryTurns: '保留历史回合', setLiveHistoryTurnsHint: '实时模式下保留的历史回合数(0–10),下次开播生效',
|
|
89
|
+
setStallThresholdMin: '卡死阈值(分钟)', setStallThresholdMinHint: '运行中但静默超过该时长判定为疑似卡死(1–10)',
|
|
90
|
+
setSave: '保存', setDiscard: '放弃更改', setSaveFail: '部分设置未保存成功,请检查后重试',
|
|
91
|
+
setReset: '恢复默认', setOverridden: '已修改', setInvalid: '超出范围或格式无效',
|
|
92
|
+
setUnavailable: '当前环境未暴露设置服务,无法编辑(请升级 dsh 后重试)',
|
|
75
93
|
} : {
|
|
76
94
|
entry: 'Session Flow', title: 'Session Flow', rescan: 'Rescan', scanning: 'Scanning…',
|
|
77
95
|
pianoTitle: 'Turns', pianoNew: 'New session', pianoFav: 'Favorite', pianoUnfav: 'Unfavorite', pianoFavs: 'Favorites', pianoOpenInFlow: 'Open in Session Flow',
|
|
78
96
|
pianoJumpFail: 'Turn not loaded — scroll up in the conversation to load history, then retry',
|
|
79
97
|
fullText: 'Full-text', fullTextSearching: 'Searching…', fullTextPartial: 'Scanned {N}/{M} sessions (partial)', fullTextNone: 'No full-text matches', fullTextFail: 'Full-text search failed',
|
|
80
98
|
dock: 'Dock right', dockExit: 'Undock', dockHint: 'Session Flow docked · follows current session', dockEmpty: 'Open a session to view its flow here',
|
|
99
|
+
dockLiveHint: 'Live: only the conversation since enabled, no history', dockLiveEmpty: 'Live · waiting for new turns…', dockLiveEnterFail: 'Session ended or not live-capable',
|
|
100
|
+
dockPinNotice: '📌 Live pinned to previous session (session switched)', dockPinRebind: 'Follow current session',
|
|
81
101
|
rename: 'Rename', renamePlaceholder: 'New title…', renameFail: 'Rename failed', origTitle: 'Original',
|
|
82
102
|
search: 'Search title / tools / files… use tool: · file: · err:', allWorkspaces: 'All workspaces',
|
|
83
103
|
sortRecent: 'Recently run', sortNewest: 'Recently created', sortOldest: 'Oldest first', sortTools: 'Most tools', sortLongest: 'Longest',
|
|
@@ -111,8 +131,218 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
111
131
|
summaryStale: '⚠ New conversation since summary was generated; content may be outdated — regenerate',
|
|
112
132
|
llmSummary: 'Generate LLM summary', llmGenerating: 'Generating…', llmFail: 'Failed', summaryRuleTag: 'rule', summaryLlmTag: 'LLM',
|
|
113
133
|
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',
|
|
135
|
+
healthSilentMin: 'silent {N} min', healthSilentSec: 'silent {N} s',
|
|
136
|
+
healthChipTitle: 'Session health (Session Flow) · click to open detail',
|
|
137
|
+
renameClearBlocked: 'Title is managed by the official session: enter a new title (clearing unsupported)',
|
|
138
|
+
settingsTitle: 'Session Flow', settingsDesc: 'Tune the piano-keys strip and live-tracking behavior. Applies on save (strip row count fully applies after re-entering a session page).',
|
|
139
|
+
setGroupPiano: 'Turn strip', setGroupLive: 'Live tracking', setGroupHealth: 'Health monitor',
|
|
140
|
+
setPianoWindow: 'Visible rows', setPianoWindowHint: 'How many turn keys the strip shows at once (6–18)',
|
|
141
|
+
setPianoWheelSpeed: 'Wheel speed', setPianoWheelSpeedHint: 'Rows scrolled per pixel (0.005–0.03); larger scrolls faster',
|
|
142
|
+
setPianoSnapMs: 'Snap delay (ms)', setPianoSnapMsHint: 'Delay after the wheel stops before snapping to a whole row (100–400)',
|
|
143
|
+
setLivePollMs: 'Poll interval (ms)', setLivePollMsHint: 'Live refresh interval (1500–10000); applies to the next live session',
|
|
144
|
+
setLiveFollowPx: 'Follow threshold (px)', setLiveFollowPxHint: 'Distance from the bottom that still counts as "at bottom" and follows (20–120)',
|
|
145
|
+
setLiveHistoryTurns: 'History turns kept', setLiveHistoryTurnsHint: 'History turns kept in live mode (0–10); applies to the next live session',
|
|
146
|
+
setStallThresholdMin: 'Stall threshold (min)', setStallThresholdMinHint: 'Running but silent beyond this counts as likely stalled (1–10)',
|
|
147
|
+
setSave: 'Save', setDiscard: 'Discard', setSaveFail: 'Some settings were not saved — check and retry',
|
|
148
|
+
setReset: 'Reset', setOverridden: 'Modified', setInvalid: 'Out of range or invalid format',
|
|
149
|
+
setUnavailable: 'The settings service is not exposed in this environment (upgrade dsh and retry)',
|
|
114
150
|
}
|
|
115
151
|
|
|
152
|
+
// ── 插件设置(settings.section 独立 tab)──────────────────────────
|
|
153
|
+
// Host 注册 settings 命名空间 'session-flow'(lib/host.js SETTINGS_DEFAULTS 同源);
|
|
154
|
+
// client 经 settingsScope 读写(pet 同款:优先 webUiSettings 桥,回退 ctx.settingsScope)。
|
|
155
|
+
// 全部消费点读 sfSettings.current.xxx,缺省 = 原硬编码值——无设置时行为零变化。
|
|
156
|
+
const SETTINGS_DEFAULTS = { pianoWindow: 12, pianoWheelSpeed: 0.012, pianoSnapMs: 170, livePollMs: 3000, liveFollowPx: 40, liveHistoryTurns: 3, stallThresholdMin: 3 }
|
|
157
|
+
const sfSettings = { current: { ...SETTINGS_DEFAULTS } }
|
|
158
|
+
const SETTING_GROUPS = [
|
|
159
|
+
{ titleKey: 'setGroupPiano', fields: ['pianoWindow', 'pianoWheelSpeed', 'pianoSnapMs'] },
|
|
160
|
+
{ titleKey: 'setGroupLive', fields: ['livePollMs', 'liveFollowPx', 'liveHistoryTurns'] },
|
|
161
|
+
{ titleKey: 'setGroupHealth', fields: ['stallThresholdMin'] },
|
|
162
|
+
]
|
|
163
|
+
const SETTING_FIELD_SPECS = {
|
|
164
|
+
pianoWindow: { min: 6, max: 18, integer: true, labelKey: 'setPianoWindow', hintKey: 'setPianoWindowHint' },
|
|
165
|
+
pianoWheelSpeed: { min: 0.005, max: 0.03, integer: false, labelKey: 'setPianoWheelSpeed', hintKey: 'setPianoWheelSpeedHint' },
|
|
166
|
+
pianoSnapMs: { min: 100, max: 400, integer: true, labelKey: 'setPianoSnapMs', hintKey: 'setPianoSnapMsHint' },
|
|
167
|
+
livePollMs: { min: 1500, max: 10000, integer: true, labelKey: 'setLivePollMs', hintKey: 'setLivePollMsHint' },
|
|
168
|
+
liveFollowPx: { min: 20, max: 120, integer: true, labelKey: 'setLiveFollowPx', hintKey: 'setLiveFollowPxHint' },
|
|
169
|
+
liveHistoryTurns: { min: 0, max: 10, integer: true, labelKey: 'setLiveHistoryTurns', hintKey: 'setLiveHistoryTurnsHint' },
|
|
170
|
+
stallThresholdMin: { min: 1, max: 10, integer: true, labelKey: 'setStallThresholdMin', hintKey: 'setStallThresholdMinHint' },
|
|
171
|
+
}
|
|
172
|
+
function formatSettingValue(v) { return v === undefined || v === null ? '' : String(v) }
|
|
173
|
+
function parseSettingValue(spec, text) {
|
|
174
|
+
const n = Number(String(text).trim())
|
|
175
|
+
if (!Number.isFinite(n) || n < spec.min || n > spec.max) return undefined
|
|
176
|
+
if (spec.integer && !Number.isInteger(n)) return undefined
|
|
177
|
+
return n
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 设置表单控制器:草稿(staged edits)+ scope 读写(pet CardForm 简化版——
|
|
181
|
+
// 同样是「保存才落盘」语义;getSnapshot 缓存快照供 useSyncExternalStore)。
|
|
182
|
+
class SfSettingsController {
|
|
183
|
+
constructor(scope) {
|
|
184
|
+
this.scope = scope
|
|
185
|
+
this.listeners = new Set()
|
|
186
|
+
this.draft = new Map() // key -> { text, clear }
|
|
187
|
+
this.saving = false
|
|
188
|
+
this.failed = false
|
|
189
|
+
this.cached = null
|
|
190
|
+
this.unsubscribe = scope.subscribe(() => this.notify())
|
|
191
|
+
this.notify()
|
|
192
|
+
}
|
|
193
|
+
dispose() {
|
|
194
|
+
try { if (this.unsubscribe) this.unsubscribe() } catch (e) {}
|
|
195
|
+
this.unsubscribe = null
|
|
196
|
+
this.listeners.clear()
|
|
197
|
+
}
|
|
198
|
+
subscribe(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn) }
|
|
199
|
+
// 快照值合并进模块级桥(所有功能代码的消费点;settings 变化即时下发)。
|
|
200
|
+
applySnapshotToBridge() {
|
|
201
|
+
try {
|
|
202
|
+
const snap = this.scope.getSnapshot()
|
|
203
|
+
if (snap && snap.status === 'ready' && snap.value && typeof snap.value === 'object') {
|
|
204
|
+
sfSettings.current = { ...SETTINGS_DEFAULTS, ...snap.value }
|
|
205
|
+
}
|
|
206
|
+
} catch (e) {}
|
|
207
|
+
}
|
|
208
|
+
notify() {
|
|
209
|
+
this.applySnapshotToBridge()
|
|
210
|
+
this.cached = this.buildSnapshot()
|
|
211
|
+
for (const fn of this.listeners) fn()
|
|
212
|
+
}
|
|
213
|
+
getSnapshot() {
|
|
214
|
+
if (this.cached === null) this.cached = this.buildSnapshot()
|
|
215
|
+
return this.cached
|
|
216
|
+
}
|
|
217
|
+
buildSnapshot() {
|
|
218
|
+
let snap = null
|
|
219
|
+
try { snap = this.scope.getSnapshot() } catch (e) {}
|
|
220
|
+
const status = snap ? String(snap.status || 'unavailable') : 'unavailable'
|
|
221
|
+
const value = { ...SETTINGS_DEFAULTS, ...(snap && snap.value ? snap.value : {}) }
|
|
222
|
+
const user = (snap && snap.user && typeof snap.user === 'object') ? snap.user : {}
|
|
223
|
+
const fields = {}
|
|
224
|
+
for (const key of Object.keys(SETTING_FIELD_SPECS)) {
|
|
225
|
+
const spec = SETTING_FIELD_SPECS[key]
|
|
226
|
+
const staged = this.draft.get(key)
|
|
227
|
+
const stored = Object.prototype.hasOwnProperty.call(user, key)
|
|
228
|
+
if (staged === undefined) {
|
|
229
|
+
fields[key] = { text: formatSettingValue(value[key]), overridden: stored, invalid: false, clear: false }
|
|
230
|
+
} else {
|
|
231
|
+
fields[key] = { text: staged.text, overridden: !staged.clear, invalid: !staged.clear && parseSettingValue(spec, staged.text) === undefined, clear: staged.clear }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
status,
|
|
236
|
+
writable: snap ? snap.writable !== false : false,
|
|
237
|
+
fields,
|
|
238
|
+
dirty: this.draft.size > 0,
|
|
239
|
+
invalid: Object.keys(fields).some((k) => fields[k].invalid),
|
|
240
|
+
saving: this.saving,
|
|
241
|
+
failed: this.failed,
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
edit(key, text) { this.draft.set(key, { text, clear: false }); this.failed = false; this.notify() }
|
|
245
|
+
resetField(key) {
|
|
246
|
+
// 恢复默认 = 清除 user 层覆盖(草稿显示 base 值,保存时 unset)。
|
|
247
|
+
let baseText = formatSettingValue(SETTINGS_DEFAULTS[key])
|
|
248
|
+
try {
|
|
249
|
+
const snap = this.scope.getSnapshot()
|
|
250
|
+
if (snap && snap.base && snap.base[key] !== undefined) baseText = formatSettingValue(snap.base[key])
|
|
251
|
+
} catch (e) {}
|
|
252
|
+
this.draft.set(key, { text: baseText, clear: true })
|
|
253
|
+
this.failed = false
|
|
254
|
+
this.notify()
|
|
255
|
+
}
|
|
256
|
+
discard() { this.draft.clear(); this.failed = false; this.notify() }
|
|
257
|
+
async save() {
|
|
258
|
+
const snap = this.getSnapshot()
|
|
259
|
+
if (this.draft.size === 0 || this.saving || snap.invalid) return
|
|
260
|
+
const pending = new Map(this.draft)
|
|
261
|
+
this.saving = true
|
|
262
|
+
this.failed = false
|
|
263
|
+
this.notify()
|
|
264
|
+
const landed = new Set()
|
|
265
|
+
for (const [key, staged] of pending) {
|
|
266
|
+
try {
|
|
267
|
+
if (staged.clear) { await this.scope.unset(key); landed.add(key); continue }
|
|
268
|
+
const v = parseSettingValue(SETTING_FIELD_SPECS[key], staged.text)
|
|
269
|
+
if (v === undefined) continue
|
|
270
|
+
await this.scope.set(key, v)
|
|
271
|
+
landed.add(key)
|
|
272
|
+
} catch (e) {}
|
|
273
|
+
}
|
|
274
|
+
for (const [key, before] of pending) if (landed.has(key) && this.draft.get(key) === before) this.draft.delete(key)
|
|
275
|
+
this.saving = false
|
|
276
|
+
this.failed = landed.size !== pending.size
|
|
277
|
+
this.notify()
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 设置弹窗「会话流」分节:分组表单(左标签+说明,右数字输入),底部保存/放弃。
|
|
282
|
+
function SessionFlowSettingsSection(props) {
|
|
283
|
+
const controller = props.controller
|
|
284
|
+
const state = useSyncExternalStore((fn) => controller.subscribe(fn), () => controller.getSnapshot())
|
|
285
|
+
if (state.status !== 'ready') {
|
|
286
|
+
return h('div', { className: 'sfset-root' }, h('p', { className: 'sfset-unavailable' }, STR.setUnavailable))
|
|
287
|
+
}
|
|
288
|
+
const disabled = !state.writable || state.saving
|
|
289
|
+
const rows = []
|
|
290
|
+
for (const group of SETTING_GROUPS) {
|
|
291
|
+
rows.push(h('div', { key: group.titleKey, className: 'sfset-groupTitle' }, STR[group.titleKey]))
|
|
292
|
+
for (const key of group.fields) {
|
|
293
|
+
const spec = SETTING_FIELD_SPECS[key]
|
|
294
|
+
const field = state.fields[key]
|
|
295
|
+
rows.push(h('div', { key, className: 'sfset-row' + (field.invalid ? ' sfset-invalid' : '') },
|
|
296
|
+
h('div', { className: 'sfset-meta' },
|
|
297
|
+
h('div', { className: 'sfset-label' }, STR[spec.labelKey],
|
|
298
|
+
field.overridden ? h('span', { className: 'sfset-overridden' }, STR.setOverridden) : null),
|
|
299
|
+
h('div', { className: 'sfset-hint' }, STR[spec.hintKey])),
|
|
300
|
+
h('div', { className: 'sfset-controls' },
|
|
301
|
+
h('input', {
|
|
302
|
+
className: 'sfset-input', type: 'number',
|
|
303
|
+
min: spec.min, max: spec.max, step: spec.integer ? 1 : 0.001,
|
|
304
|
+
value: field.text, disabled,
|
|
305
|
+
onChange: (e) => controller.edit(key, e.target.value),
|
|
306
|
+
}),
|
|
307
|
+
h('button', { type: 'button', className: 'sfset-reset', disabled, onClick: () => controller.resetField(key) }, STR.setReset))))
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return h('div', { className: 'sfset-root' },
|
|
311
|
+
h('p', { className: 'sfset-desc' }, STR.settingsDesc),
|
|
312
|
+
rows,
|
|
313
|
+
h('div', { className: 'sfset-footer' },
|
|
314
|
+
h('button', { type: 'button', className: 'sfset-save', disabled: disabled || !state.dirty || state.invalid, onClick: () => { controller.save() } }, STR.setSave),
|
|
315
|
+
h('button', { type: 'button', className: 'sfset-discard', disabled: disabled || !state.dirty, onClick: () => controller.discard() }, STR.setDiscard),
|
|
316
|
+
state.invalid ? h('span', { className: 'sfset-error' }, STR.setInvalid) : null,
|
|
317
|
+
state.failed ? h('span', { className: 'sfset-error' }, STR.setSaveFail) : null))
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 取色对齐宠物设置卡(同一弹窗内经真机明暗/皮肤验证):纯 dsw-alias 变量、
|
|
321
|
+
// 不写 hex 兜底——变量缺失时继承弹窗上下文色,硬编码兜底在皮肤主题下反而会撞色。
|
|
322
|
+
const SETTINGS_STYLE = [
|
|
323
|
+
`.sfset-root{padding:4px 2px;display:flex;flex-direction:column;max-width:560px}`,
|
|
324
|
+
`.sfset-desc{margin:0 0 10px;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}`,
|
|
325
|
+
`.sfset-groupTitle{margin:14px 0 4px;font-size:13px;font-weight:600;line-height:1.5;color:var(--dsw-alias-label-primary);border-bottom:1px solid var(--dsw-alias-border-l2);padding-bottom:4px}`,
|
|
326
|
+
`.sfset-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:10px 0}`,
|
|
327
|
+
`.sfset-meta{min-width:0;flex:1}`,
|
|
328
|
+
`.sfset-label{font-size:13px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:8px}`,
|
|
329
|
+
`.sfset-overridden{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}`,
|
|
330
|
+
`.sfset-hint{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary);margin-top:2px}`,
|
|
331
|
+
`.sfset-controls{flex:none;display:flex;align-items:center;gap:8px}`,
|
|
332
|
+
`.sfset-input{width:110px;height:34px;box-sizing:border-box;padding:0 12px;font:inherit;font-size:13px;line-height:1.5;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary)}`,
|
|
333
|
+
`.sfset-row.sfset-invalid .sfset-input{border-color:var(--dsw-alias-label-error)}`,
|
|
334
|
+
`.sfset-reset{font:inherit;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0}`,
|
|
335
|
+
`.sfset-reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}`,
|
|
336
|
+
`.sfset-footer{display:flex;align-items:center;gap:8px;margin-top:16px;border-top:1px solid var(--dsw-alias-border-l2);padding-top:12px}`,
|
|
337
|
+
`.sfset-save{appearance:none;font:inherit;font-size:13px;line-height:1.5;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}`,
|
|
338
|
+
`.sfset-save:disabled{opacity:.4;cursor:default}`,
|
|
339
|
+
`.sfset-discard{appearance:none;font:inherit;font-size:13px;line-height:1.5;cursor:pointer;border-radius:8px;padding:5px 14px;border:1px solid var(--dsw-alias-border-l2);background:0 0;color:var(--dsw-alias-label-secondary)}`,
|
|
340
|
+
`.sfset-discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}`,
|
|
341
|
+
`.sfset-discard:disabled{opacity:.4;cursor:default}`,
|
|
342
|
+
`.sfset-error{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)}`,
|
|
343
|
+
`.sfset-unavailable{font-size:13px;line-height:1.5;color:var(--dsw-alias-state-warn-primary)}`,
|
|
344
|
+
].join('\n')
|
|
345
|
+
|
|
116
346
|
// ── 右侧导航图标(组件级共享,定义在顶层避免作用域问题)───────────
|
|
117
347
|
// feather 风格线条,currentColor 自适应明暗主题、与文字同色。
|
|
118
348
|
const NAV_ICON_USER = `<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`
|
|
@@ -140,7 +370,13 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
140
370
|
`[data-dsh-frame][data-sidebar-collapsed] .sf-entryLabel{display:none}`,
|
|
141
371
|
// box-sizing:border-box 必须:height:100% + padding 在 content-box 下会溢出容器,
|
|
142
372
|
// 产生多余的整页滚动条,导致 top 栏随滚动消失(用户实测反馈)。
|
|
143
|
-
|
|
373
|
+
// T2 修复:flex:1 1 0(flex-basis 用绝对 0 长度,非 flex:1 的 0% 百分比)。
|
|
374
|
+
// 根因(诊断数据 d47b82bf 实测):官方 viewArea 是 min-height:auto(聊天视图靠它
|
|
375
|
+
// 撑开内容 + 粘性输入框,不可全局强改);flex:1 的 0% 基准在父级高度不定时被
|
|
376
|
+
// Chrome 按 content 处理 → .sf-view 假设尺寸=内容高 → viewArea 内容下限被顶高 →
|
|
377
|
+
// 溢出 scrollBody → 整块滚动。基准用绝对 0 → 假设尺寸=0 → viewArea 下限=0 →
|
|
378
|
+
// 回到 flex 分配空间,两列各自内部滚动。工作台模式父容器非 flex,flex 无效无害。
|
|
379
|
+
`.sf-view{box-sizing:border-box;background:var(--dsw-alias-bg-base,#fff);min-width:0;height:100%;flex:1 1 0;min-height:0;color:var(--dsw-alias-label-primary,#222);font-family:var(--dsw-font-family,inherit);flex-direction:column;gap:10px;padding:14px 16px 16px;display:flex;overflow:hidden}`,
|
|
144
380
|
// top 栏悬浮固定:flex:none 在布局上已固定;再加 sticky + 背景双保险。
|
|
145
381
|
`.sf-viewHeader{flex:none;position:sticky;top:0;z-index:1;background:var(--dsw-alias-bg-base,#fff);align-items:center;gap:10px;display:flex;flex-wrap:wrap}`,
|
|
146
382
|
`.sf-viewTitle{color:var(--dsw-alias-label-primary,#222);white-space:nowrap;margin:0;font-size:16px;font-weight:700}`,
|
|
@@ -179,6 +415,29 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
179
415
|
`.sf-badgeEnd{background:rgba(120,120,120,.12);color:#888}`,
|
|
180
416
|
`.sf-badgeSub{background:rgba(240,150,60,.15);color:#d97706}`,
|
|
181
417
|
`.sf-badgeErr{background:rgba(230,80,80,.13);color:#d43b3b}`,
|
|
418
|
+
// 卡死监控徽标:疑似卡死(红,呼吸动画引起注意)/ 工具执行中·静默中(黄)。
|
|
419
|
+
`.sf-badgeStall{background:rgba(230,80,80,.16);color:#d43b3b;animation:sfBadgePulse 2.8s ease-in-out infinite}`,
|
|
420
|
+
`.sf-badgeWait{background:rgba(220,170,40,.15);color:#a87b00}`,
|
|
421
|
+
// 会话页头部健康芯片(官方 header.actions 槽位,模式标识右侧)。
|
|
422
|
+
// 钢琴键同款语言:收拢态 = 彩色短胶囊条(语义色保留:绿/黄/红,卡死带呼吸脉冲);
|
|
423
|
+
// 悬浮/聚焦展开为毛玻璃文字标签(backdrop-filter + color-mix,与 pk-key 展开态同构)。
|
|
424
|
+
`.sf-healthChip{display:inline-flex;align-items:center;gap:6px;height:20px;padding:0 3px;margin-left:2px;border:none;border-radius:99px;background:transparent;cursor:pointer;font-family:inherit;overflow:hidden;outline:none;transition:padding .22s ease,background .18s ease,border-color .18s ease,box-shadow .18s ease}`,
|
|
425
|
+
`.sf-healthChip .sf-hcBar{flex:none;width:4px;height:16px;border-radius:99px;transition:width .22s ease,height .22s ease}`,
|
|
426
|
+
`.sf-healthChip .sf-hcLabel{font-size:11px;line-height:16px;white-space:nowrap;max-width:0;opacity:0;overflow:hidden;user-select:none;transition:max-width .24s ease,opacity .16s ease .05s}`,
|
|
427
|
+
// 展开态:毛玻璃底(blur 10px)+ 同色系淡 tint 背景(状态色 14% 透明混入 72% 基底色),
|
|
428
|
+
// 边框带 40% 状态色呼应;tint 淡雅、毛玻璃质感保留。
|
|
429
|
+
`.sf-healthChip:hover,.sf-healthChip:focus-visible{padding:3px 9px;border:1px solid var(--dsw-alias-border-l2,#e4e6eb);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);box-shadow:0 1px 3px rgba(0,0,0,.07)}`,
|
|
430
|
+
`.sf-healthChip.hc-active:hover,.sf-healthChip.hc-active:focus-visible{background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 72%,rgba(28,158,90,.14));border-color:color-mix(in srgb,var(--dsw-alias-border-l2,#e4e6eb) 60%,#1c9e5a)}`,
|
|
431
|
+
`.sf-healthChip.hc-wait:hover,.sf-healthChip.hc-wait:focus-visible{background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 72%,rgba(217,165,20,.14));border-color:color-mix(in srgb,var(--dsw-alias-border-l2,#e4e6eb) 60%,#d9a514)}`,
|
|
432
|
+
`.sf-healthChip.hc-stall:hover,.sf-healthChip.hc-stall:focus-visible{background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 72%,rgba(212,59,59,.14));border-color:color-mix(in srgb,var(--dsw-alias-border-l2,#e4e6eb) 60%,#d43b3b)}`,
|
|
433
|
+
`.sf-healthChip:hover .sf-hcBar,.sf-healthChip:focus-visible .sf-hcBar{width:6px;height:6px;border-radius:99px}`,
|
|
434
|
+
`.sf-healthChip:hover .sf-hcLabel,.sf-healthChip:focus-visible .sf-hcLabel{max-width:200px;opacity:1}`,
|
|
435
|
+
`.sf-healthChip.hc-active .sf-hcBar{background:#1c9e5a}`,
|
|
436
|
+
`.sf-healthChip.hc-active .sf-hcLabel{color:#1c9e5a}`,
|
|
437
|
+
`.sf-healthChip.hc-wait .sf-hcBar{background:#d9a514}`,
|
|
438
|
+
`.sf-healthChip.hc-wait .sf-hcLabel{color:#a87b00}`,
|
|
439
|
+
`.sf-healthChip.hc-stall .sf-hcBar{background:#d43b3b;animation:sfBadgePulse 2.8s ease-in-out infinite}`,
|
|
440
|
+
`.sf-healthChip.hc-stall .sf-hcLabel{color:#d43b3b}`,
|
|
182
441
|
`.sf-statsRow{display:flex;gap:12px;flex-wrap:wrap;margin-top:3px}`,
|
|
183
442
|
`.sf-hint{color:var(--dsw-alias-label-secondary,#888);font-size:12px;padding:2px 2px 0}`,
|
|
184
443
|
// ── 工作区筛选 tabs(独立一行,醒目)──
|
|
@@ -307,17 +566,26 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
307
566
|
`.sf-fulltextCount{flex:none;border-radius:999px;background:rgba(90,140,255,.15);color:#4a7dff;font-size:10px;line-height:15px;min-width:18px;text-align:center;padding:0 5px;font-weight:700}`,
|
|
308
567
|
`.sf-fulltext .sf-muted{font-size:11px;color:var(--dsw-alias-label-secondary,#666)}`,
|
|
309
568
|
// M12:详情并入右侧栏(details 槽位轻量视图)。
|
|
569
|
+
// 头部布局:标题+按钮**聚左**,spacer 吃掉右侧余量——彻底规避右上角
|
|
570
|
+
// dsh-better-sidebar toggleCluster(视口级常驻浮钮,压过官方 details 列右缘,
|
|
571
|
+
// 实测「退出并入」被压)。按钮不再进右上角浮动图标区,任何第三方浮钮都压不到。
|
|
310
572
|
`.dk-root{box-sizing:border-box;height:100%;min-height:0;display:flex;flex-direction:column;gap:8px;padding:10px 12px;overflow:hidden;color:var(--dsw-alias-label-primary,#222);font-family:var(--dsw-font-family,inherit)}`,
|
|
311
|
-
`.dk-head{flex:none;display:flex;align-items:center;gap:8px;min-width:0}`,
|
|
312
|
-
`.dk-title{flex:
|
|
313
|
-
`.dk-
|
|
573
|
+
`.dk-head{flex:none;display:flex;align-items:center;flex-wrap:wrap;gap:8px;min-width:0}`,
|
|
574
|
+
`.dk-title{flex:none;min-width:0;font-size:13px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:40%}`,
|
|
575
|
+
`.dk-headSpacer{flex:1 1 auto;min-width:0;align-self:stretch;pointer-events:none}`,
|
|
576
|
+
`.dk-btn{flex:none;border:1px solid var(--dsw-alias-border-l2,#ddd);background:var(--dsw-alias-bg-layer-1,#f5f5f5);color:var(--dsw-alias-label-primary,#222);border-radius:7px;padding:3px 9px;font-size:11px;cursor:pointer;white-space:nowrap}`,
|
|
314
577
|
`.dk-btn:hover{border-color:var(--dsw-alias-brand-primary,#4a7dff);color:var(--dsw-alias-brand-primary,#4a7dff)}`,
|
|
578
|
+
// T6:dock 实时开关激活态(绿色语义,与详情页 sf-liveBar 同色系)。
|
|
579
|
+
`.dk-btn.dk-btnLive{border-color:rgba(28,158,90,.5);background:rgba(28,158,90,.08);color:#1c9e5a;font-weight:600}`,
|
|
315
580
|
`.dk-body{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:6px}`,
|
|
316
581
|
`.dk-summary{border:1px solid var(--dsw-alias-border-l2,#e4e6eb);border-radius:10px;padding:8px 10px;background:var(--dsw-alias-bg-layer-1,#f8f9fa);flex:none;display:flex;flex-direction:column;gap:4px}`,
|
|
317
582
|
`.dk-summaryRow{display:flex;gap:6px;font-size:11.5px;line-height:1.5;color:var(--dsw-alias-label-secondary,#666);min-width:0}`,
|
|
318
583
|
`.dk-summaryTag{flex:none;border-radius:5px;padding:0 5px;font-size:10px;line-height:16px;font-weight:700;background:rgba(90,140,255,.12);color:#4a7dff}`,
|
|
319
584
|
`.dk-summaryText{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}`,
|
|
320
585
|
`.dk-summaryFull{font-size:12px;line-height:1.6;color:var(--dsw-alias-label-primary,#222);white-space:pre-wrap;word-break:break-word;max-height:140px;overflow:auto}`,
|
|
586
|
+
// T8-B 钉住提示条:琥珀色警示(直播会话 ≠ 当前会话)。
|
|
587
|
+
`.dk-pinBar{flex:none;display:flex;align-items:center;gap:8px;padding:5px 8px;border:1px solid rgba(224,150,63,.45);background:rgba(224,150,63,.08);border-radius:8px}`,
|
|
588
|
+
`.dk-pinText{flex:1;min-width:0;font-size:12px;color:#c07a2a}`,
|
|
321
589
|
// M12:并入期间禁用 layout grid 列宽过渡——官方会话切换会 closeDetails(宽 360→0),
|
|
322
590
|
// 守护重开(0→360)若带过渡动画会产生「关→开」闪烁;禁用后切换直接刷新内容(实测踩坑)。
|
|
323
591
|
`html[data-dsh-dock-active] [class*=frame],html[data-dsh-dock-active] [data-side=details]{transition:none!important}`,
|
|
@@ -362,35 +630,73 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
362
630
|
// 垂直居中悬浮于会话内容区中间(top 由组件算区中心 + translateY(-50%))。
|
|
363
631
|
// 动画体系:一切组件的产生/消失都有「来源与目标」——display 硬切全部改为
|
|
364
632
|
// transform/opacity/max-height 过渡(实测踩坑:display 切换无动画、视觉突兀)。
|
|
365
|
-
`[data-dsh-piano-keys]{position:fixed;left:0;top:50%;width:32px;max-height:60vh;z-index:15;display:flex;flex-direction:column;font-family:var(--dsw-font-family,inherit);transition:width .26s cubic-bezier(.3,1.15,.4,1)
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
|
|
633
|
+
`[data-dsh-piano-keys]{position:fixed;left:0;top:50%;width:32px;max-height:60vh;z-index:15;display:flex;flex-direction:column;padding:0 0 8px;font-family:var(--dsw-font-family,inherit);transition:width .26s cubic-bezier(.3,1.15,.4,1)}`,
|
|
634
|
+
// 收拢时序(T4):宽度与条目高度(.pk-key .22s)**同时起跑**——用户要求纵向横向
|
|
635
|
+
// 同时收拢(此前宽度延迟 .22s 造成「先纵后横」两段式);详情信息框仍先行立即
|
|
636
|
+
// 滑出(.pk-detail.hidden 无延迟),不参与纵向节奏。进入(hover/focus)同态:
|
|
637
|
+
// 无延迟即时展开,收拢=展开的倒放。
|
|
638
|
+
// 收拢锁(collapse-lock):pointerleave 后 300ms 内抑制 :hover 展开(focus-within 不受影响)。
|
|
639
|
+
// 日志实锤的几何自反馈回路:展开态越界 → 整条收拢(几何瞬变)→ 收拢后的胶囊条
|
|
640
|
+
// 「弹到」静止鼠标下方 → 合成 mouseenter → :hover 重新展开 → 再越界……每 300-500ms
|
|
641
|
+
// 一个来回(「横条反复出现消失」)。锁窗内 :hover 不展开,回路断开;真实驻留 300ms
|
|
642
|
+
// 后正常展开(顺带构成驻留去抖:快速掠过不再闪开)。
|
|
643
|
+
`[data-dsh-piano-keys]:not([data-collapse-lock]):hover,[data-dsh-piano-keys]:focus-within{width:200px;transition-delay:0s}`,
|
|
369
644
|
`html[${ACTIVE_ATTR}] [data-dsh-piano-keys]{display:none!important}`,
|
|
370
|
-
|
|
371
|
-
|
|
645
|
+
// T5:strip 高度 JS 显式管理(setStripHeight:可见行数 × 实测行距 + 上下内边距
|
|
646
|
+
// 16 − 间隙 5;收拢/展开键高动画期间 RO 逐帧跟随)——clipper 是 absolute 脱流,
|
|
647
|
+
// strip 不能再靠内容撑高(实测塌缩整条消失)。**必须 flex:1 1 auto**(basis:auto
|
|
648
|
+
// 时 height 属性才是弹性基准;flex:1 = basis 0% 会架空 JS height——实测踩坑)。
|
|
649
|
+
// shrink 保留配合 60vh 硬上限压缩。裁剪面=pk-clip(absolute inset:0 = strip
|
|
650
|
+
// 【内容盒】):CSS overflow 裁剪在内边距盒——上下缓冲键各露 3px 进内边距带
|
|
651
|
+
// (「上面会话的下边框露出一点点」实测踩坑);clipper 把可见区收到恰好 12 行。
|
|
652
|
+
// 垂直呼吸全部移出 strip:标题行下边距 8 + 根容器下内边距 8(均静止件);
|
|
653
|
+
// strip 零 padding——绝对定位 clipper 的 inset:0 解析的是【padding box】而非
|
|
654
|
+
// 内容盒,padding 留在 strip 上不参与裁剪、键却从 y=0 起排画进 padding 带
|
|
655
|
+
// (顶键贴死标题行、底部框线被裁——实测踩坑)。strip 高度=纯内容 rows×pitch−5。
|
|
656
|
+
`.pk-strip{flex:1 1 auto;min-height:0;max-height:calc(60vh - 60px);position:relative;outline:none}`,
|
|
657
|
+
`.pk-clip{position:absolute;inset:0;overflow:hidden}`,
|
|
658
|
+
// 键挂 pk-list 上;transform 由 JS 逐帧驱动(rAF 追逐 scrollPos + 实测行距,
|
|
659
|
+
// renderStart 离散跳变时同步重设——CSS transition 会把内容重排的跳变也做成
|
|
660
|
+
// 动画,每滚一行顿一次,实测踩坑)。
|
|
661
|
+
// 垂直内边距在 strip(静止件)不在 list(移动件):padding 挂移动件会随
|
|
662
|
+
// transform 平移,键从 y=8 起排 → 缓冲键底边 +3px 露出裁剪线(实测踩坑)。
|
|
663
|
+
`.pk-list{display:flex;flex-direction:column;gap:5px;padding:0 4px;will-change:transform}`,
|
|
372
664
|
// 收拢态:短横条(胶囊线);overflow:hidden 使 label 文字被高度裁剪(来源=胶囊);
|
|
373
665
|
// ::before 延伸命中区(±4px,点击/悬浮无死区)。
|
|
374
|
-
|
|
666
|
+
// T4 二阶段:box-sizing:border-box + 确定高度(4px↔30px)——原展开态 height:auto
|
|
667
|
+
// 不可插值(日志实锤:全程无 height 过渡事件),高度在两态间瞬移 120↔408px,
|
|
668
|
+
// 收拢条边缘 hover 进出即整条几何反复跳变(用户实测「闪烁抽搐」)。
|
|
669
|
+
`.pk-key{position:relative;flex:none;box-sizing:border-box;height:4px;min-height:4px;border-radius:99px;border:none;cursor:pointer;padding:0;margin:0 4px;background:var(--dsw-alias-border-l2,#d7dae0);color:var(--dsw-alias-label-primary,#222);overflow:hidden;transition:height .22s ease,padding .22s ease,margin .22s ease,border-radius .22s ease,background .14s ease,border-color .14s ease,box-shadow .18s ease;outline:none;min-width:0}`,
|
|
375
670
|
`.pk-key::before{content:'';position:absolute;left:0;right:0;top:-4px;bottom:-4px}`,
|
|
671
|
+
// 热区填满死区(三日志轮实锤症状一):strip 上下内边距 8px 只有 ::before ±4px 覆盖,
|
|
672
|
+
// 标题行/首键之间残留 4px 死区——鼠标停进去 key[0] 高亮+详情面板熄灭,跨出又点亮,
|
|
673
|
+
// 窄带内反复横跳=「横条出现消失」。首/末键 ::before 精确补到 strip 边缘(-8px),
|
|
674
|
+
// 带内任意位置都算 hover 该键,高亮/面板稳定常驻;键间隙 5px 由相邻 ±4px 互叠已全覆盖。
|
|
675
|
+
`.pk-key:first-child::before{top:-8px}`,
|
|
676
|
+
`.pk-key:last-child::before{bottom:-8px}`,
|
|
376
677
|
`.pk-key:hover{background:var(--dsw-alias-brand-primary,#4a7dff)}`,
|
|
377
678
|
`.pk-key:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4a7dff);outline-offset:-1px}`,
|
|
378
679
|
`.pk-keyActive{background:var(--dsw-alias-brand-primary,#4a7dff)}`,
|
|
379
680
|
`.pk-keyFocus{background:var(--dsw-alias-brand-primary,#4a7dff)}`,
|
|
380
681
|
// 展开态(悬浮/聚焦):条目变文字卡片;label 常驻(缩略态被条目高度裁剪),
|
|
381
682
|
// 展开时随高度过渡露出文字(来源=胶囊内部,目标=卡片全文)。
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
`[data-dsh-piano-keys]:hover .pk-
|
|
683
|
+
// T4 二阶段:height:auto → height:30px(label 18 + padding 10 + border 2 = 原自然
|
|
684
|
+
// 高度,最终几何不变)——确定长度可插值,与收拢态 4px 平滑互转(倒放对称)。
|
|
685
|
+
`[data-dsh-piano-keys]:not([data-collapse-lock]):hover .pk-key,[data-dsh-piano-keys]:focus-within .pk-key{display:flex;align-items:center;gap:6px;height:30px;min-height:30px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2,#e4e6eb);padding:5px 8px;margin:0;background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 80%,transparent);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);box-shadow:0 1px 3px rgba(0,0,0,.07)}`,
|
|
686
|
+
// 热区链(四日志轮定位症状一):head↔gap↔首键 必须连续同态,否则边界 1px 晃动
|
|
687
|
+
// 即反复点亮/熄灭首键高亮(横条出现消失)。gap 由首键 ::before 覆盖(上文),
|
|
688
|
+
// head 由下行选择器链到首键:hover 标题行 ≡ hover 第一个键。
|
|
689
|
+
`[data-dsh-piano-keys]:not([data-collapse-lock]):hover .pk-key:hover,[data-dsh-piano-keys]:focus-within .pk-key:hover,[data-dsh-piano-keys]:not([data-collapse-lock]):hover .pk-head:hover ~ .pk-strip .pk-key:first-child{background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 96%,transparent);border-color:var(--dsw-alias-brand-primary,#4a7dff)}`,
|
|
690
|
+
`[data-dsh-piano-keys]:not([data-collapse-lock]):hover .pk-keyActive,[data-dsh-piano-keys]:focus-within .pk-keyActive{border-color:var(--dsw-alias-brand-primary,#4a7dff);background:color-mix(in srgb,var(--dsw-alias-brand-primary,#4a7dff) 10%,var(--dsw-alias-bg-base,#fff))}`,
|
|
385
691
|
`.pk-label{flex:1;min-width:0;font-size:12px;line-height:18px;max-height:18px;color:var(--dsw-alias-label-primary,#222);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;user-select:none;opacity:0;transition:opacity .18s ease .06s}`,
|
|
386
|
-
`[data-dsh-piano-keys]:hover .pk-label,[data-dsh-piano-keys]:focus-within .pk-label{opacity:1}`,
|
|
692
|
+
`[data-dsh-piano-keys]:not([data-collapse-lock]):hover .pk-label,[data-dsh-piano-keys]:focus-within .pk-label{opacity:1}`,
|
|
387
693
|
// 轮次序号:缩略态折叠(宽度 0 + 透明),展开态从左侧展开露出(来源=左缘)。
|
|
388
694
|
`.pk-turnNo{flex:none;width:0;min-width:0;overflow:hidden;opacity:0;text-align:left;font-size:9.5px;line-height:16px;font-weight:600;color:var(--dsw-alias-label-tertiary,#999);font-family:ui-monospace,Consolas,monospace;user-select:none;white-space:nowrap;transition:width .22s ease,opacity .16s ease .04s}`,
|
|
389
|
-
`[data-dsh-piano-keys]:hover .pk-turnNo,[data-dsh-piano-keys]:focus-within .pk-turnNo{width:28px;min-width:28px;opacity:1}`,
|
|
695
|
+
`[data-dsh-piano-keys]:not([data-collapse-lock]):hover .pk-turnNo,[data-dsh-piano-keys]:focus-within .pk-turnNo{width:28px;min-width:28px;opacity:1}`,
|
|
390
696
|
// hover 条目:在【该条目右侧】展开信息面板(.pk-detail)——absolute 相对悬浮条容器,
|
|
391
697
|
// 坐标按条目位置换算为容器内偏移(注意:容器有 transform:translateY(-50%),fixed 子元素
|
|
392
698
|
// 会被 transform 祖先劫持成相对容器定位——用视口坐标会双重偏移,实测踩坑)。
|
|
393
|
-
`[data-dsh-piano-keys]:hover .pk-key:hover,[data-dsh-piano-keys]:focus-within .pk-key.pk-keyFocus{position:relative;z-index:2;box-shadow:0 4px 16px rgba(0,0,0,.18)}`,
|
|
699
|
+
`[data-dsh-piano-keys]:not([data-collapse-lock]):hover .pk-key:hover,[data-dsh-piano-keys]:focus-within .pk-key.pk-keyFocus,[data-dsh-piano-keys]:not([data-collapse-lock]):hover .pk-head:hover ~ .pk-strip .pk-key:first-child{position:relative;z-index:2;box-shadow:0 4px 16px rgba(0,0,0,.18)}`,
|
|
394
700
|
// 展开时序:详情框延迟 .28s 再滑入——等悬浮条展开动画完成、条目就位后,
|
|
395
701
|
// 位置才准确(第一次展开时条目仍在变形,立即计算会错位,实测踩坑);
|
|
396
702
|
// 隐藏则立即(无延迟)。
|
|
@@ -405,14 +711,10 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
405
711
|
`.pk-status{position:absolute;left:0;right:0;top:calc(100% + 4px);padding:7px 10px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 82%,transparent);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:1px solid var(--dsw-alias-border-l2,#e4e6eb);font-size:11px;line-height:1.5;color:var(--dsw-alias-label-secondary,#666);box-shadow:0 4px 14px rgba(0,0,0,.12);z-index:6;text-align:center;white-space:normal;word-break:break-word;opacity:1;transform:translateY(0);transition:opacity .18s ease,transform .22s ease,visibility .22s}`,
|
|
406
712
|
`.pk-status.hidden{opacity:0;transform:translateY(8px);visibility:hidden;pointer-events:none}`,
|
|
407
713
|
`.pk-status.warn{color:#d97706;border-color:rgba(217,119,6,.4)}`,
|
|
408
|
-
// 展开态显示悬浮条细滚动条(缩略态保持无滚动条)。
|
|
409
|
-
`[data-dsh-piano-keys]:hover .pk-strip,[data-dsh-piano-keys]:focus-within .pk-strip{scrollbar-width:thin;scrollbar-color:var(--dsw-alias-border-l2,#d7dae0) transparent}`,
|
|
410
|
-
`[data-dsh-piano-keys]:hover .pk-strip::-webkit-scrollbar,[data-dsh-piano-keys]:focus-within .pk-strip::-webkit-scrollbar{width:6px;display:block}`,
|
|
411
|
-
`[data-dsh-piano-keys]:hover .pk-strip::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l2,#d7dae0);border-radius:3px}`,
|
|
412
714
|
// 头部:从上方滑入(来源=悬浮条顶缘)——max-height/padding/opacity/transform 过渡,
|
|
413
715
|
// 不占缩略态布局。内容「标题 + ▲▼ 按钮组(并排右上)」。
|
|
414
|
-
`.pk-head{display:flex;flex:none;align-items:center;gap:4px;padding:0 6px;color:var(--dsw-alias-label-secondary,#666);font-size:10px;line-height:1.2;border-bottom:1px solid var(--dsw-alias-border-l2,#e5e5e5);max-height:0;overflow:hidden;opacity:0;transform:translateY(-6px);transition:max-height .24s ease,padding .24s ease,opacity .18s ease,transform .24s ease}`,
|
|
415
|
-
`[data-dsh-piano-keys]:hover .pk-head,[data-dsh-piano-keys]:focus-within .pk-head{max-height:30px;padding-top:5px;padding-bottom:5px;opacity:1;transform:translateY(0)}`,
|
|
716
|
+
`.pk-head{display:flex;flex:none;align-items:center;gap:4px;padding:0 6px;margin-bottom:8px;color:var(--dsw-alias-label-secondary,#666);font-size:10px;line-height:1.2;border-bottom:1px solid var(--dsw-alias-border-l2,#e5e5e5);max-height:0;overflow:hidden;opacity:0;transform:translateY(-6px);transition:max-height .24s ease,padding .24s ease,opacity .18s ease,transform .24s ease}`,
|
|
717
|
+
`[data-dsh-piano-keys]:not([data-collapse-lock]):hover .pk-head,[data-dsh-piano-keys]:focus-within .pk-head{max-height:30px;padding-top:5px;padding-bottom:5px;opacity:1;transform:translateY(0)}`,
|
|
416
718
|
`.pk-headLabel{flex:1;min-width:0;font-size:10.5px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}`,
|
|
417
719
|
`.pk-navGroup{flex:none;display:flex;gap:3px}`,
|
|
418
720
|
`.pk-navBtn{flex:none;width:20px;height:20px;border:1px solid var(--dsw-alias-border-l2,#ddd);background:var(--dsw-alias-bg-layer-1,#f5f5f5);color:var(--dsw-alias-label-primary,#222);border-radius:6px;cursor:pointer;font-size:9px;line-height:1;padding:0;transition:border-color .14s ease,color .14s ease}`,
|
|
@@ -562,6 +864,104 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
562
864
|
)
|
|
563
865
|
}
|
|
564
866
|
|
|
867
|
+
// ── 重命名对齐官方 + 卡死监控 共享辅助 ─────────────────────────────
|
|
868
|
+
/** 官方 rename RPC(log-backed session/title user 事件):本插件与官方侧栏同一数据源。 */
|
|
869
|
+
const renameViaOfficial = (conn, sessionId, title) => {
|
|
870
|
+
return conn.api.sessions.rename({ sessionId, title }).then((resp) => {
|
|
871
|
+
const res = resp && resp.result
|
|
872
|
+
if (res && res.ok === true && res.value && typeof res.value.title === 'string') return res.value.title
|
|
873
|
+
throw new Error((res && res.error && res.error.message) || STR.renameFail)
|
|
874
|
+
})
|
|
875
|
+
}
|
|
876
|
+
const canOfficialRename = (conn) =>
|
|
877
|
+
conn !== undefined && conn.api !== undefined && conn.api.sessions !== undefined && typeof conn.api.sessions.rename === 'function'
|
|
878
|
+
/** 静默时长文本({N} 占位)。 */
|
|
879
|
+
const fmtSilent = (idleMs) => {
|
|
880
|
+
if (idleMs === null || idleMs === undefined) return ''
|
|
881
|
+
const min = Math.floor(idleMs / 60000)
|
|
882
|
+
return min >= 1
|
|
883
|
+
? STR.healthSilentMin.replace('{N}', String(min))
|
|
884
|
+
: STR.healthSilentSec.replace('{N}', String(Math.max(1, Math.round(idleMs / 1000))))
|
|
885
|
+
}
|
|
886
|
+
/** 健康徽标(详情实时条 / 总览卡片共用);active/ended/unknown 不渲染(已有进行中/已结束徽标)。 */
|
|
887
|
+
const healthBadge = (health) => {
|
|
888
|
+
if (!health) return null
|
|
889
|
+
const silent = fmtSilent(health.idleMs)
|
|
890
|
+
if (health.kind === 'stalled') return h('span', { className: 'sf-badge sf-badgeStall' }, '🔴 ' + STR.healthStalled + (silent ? ' · ' + silent : ''))
|
|
891
|
+
if (health.kind === 'tool-wait') return h('span', { className: 'sf-badge sf-badgeWait' }, '🟡 ' + STR.healthToolWait + (silent ? ' · ' + silent : ''))
|
|
892
|
+
if (health.kind === 'quiet') return h('span', { className: 'sf-badge sf-badgeWait' }, '🟡 ' + STR.healthQuiet + (silent ? ' · ' + silent : ''))
|
|
893
|
+
return null
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// ── 会话页头部健康芯片(官方 conversation.session.header.actions 槽位)──
|
|
897
|
+
// 位置:官方模式标识(agent-preset label, order -10)右侧(order -9)。
|
|
898
|
+
// 数据:sessions.list 快照跟踪 running(官方真实状态,随 host 流即时翻转)→
|
|
899
|
+
// running 时 10s 轮询 tail history → host derive 健康分类(与总览探测同管线)。
|
|
900
|
+
// 点击:唤起会话流工作台并直达该会话详情(workbenchBridge 意图桥)。
|
|
901
|
+
function SessionHealthChip(props) {
|
|
902
|
+
const { sessionId, connection, sessions } = props
|
|
903
|
+
const [running, setRunning] = useState(false)
|
|
904
|
+
const [health, setHealth] = useState(null)
|
|
905
|
+
useEffect(() => {
|
|
906
|
+
if (!sessionId || !sessions || !sessions.list || typeof sessions.list.getSnapshot !== 'function') return undefined
|
|
907
|
+
const compute = () => {
|
|
908
|
+
try {
|
|
909
|
+
// sessions.list 快照 = { ids, current, byId }(byId[id] 含 running)——
|
|
910
|
+
// 不是 { items }(那是 workspace 浏览器用的另一个视图快照,曾误用导致芯片恒 null)。
|
|
911
|
+
const snap = sessions.list.getSnapshot()
|
|
912
|
+
const it = snap && snap.byId ? snap.byId[sessionId] : undefined
|
|
913
|
+
setRunning(it ? it.running === true : false)
|
|
914
|
+
} catch (e) { setRunning(false) }
|
|
915
|
+
}
|
|
916
|
+
compute()
|
|
917
|
+
if (typeof sessions.list.subscribe === 'function') return sessions.list.subscribe(compute)
|
|
918
|
+
return undefined
|
|
919
|
+
}, [sessions, sessionId])
|
|
920
|
+
useEffect(() => {
|
|
921
|
+
if (!running || !sessionId || !canOfficialRename(connection)) { setHealth(null); return undefined }
|
|
922
|
+
let alive = true
|
|
923
|
+
const probe = async () => {
|
|
924
|
+
try {
|
|
925
|
+
const hr = await connection.api.sessions.history({ sessionId, maxMessages: 3 })
|
|
926
|
+
const val = hr && hr.result && hr.result.value
|
|
927
|
+
const events = val && Array.isArray(val.events) ? val.events.map((e) => (e && e.event) || e) : []
|
|
928
|
+
if (events.length === 0) { if (alive) setHealth(null); return }
|
|
929
|
+
const d = await api('derive', { events, now: Date.now(), assumeRunning: true })
|
|
930
|
+
if (alive && d && d.ok) setHealth(d.health || null)
|
|
931
|
+
} catch (e) {}
|
|
932
|
+
}
|
|
933
|
+
probe()
|
|
934
|
+
const iv = setInterval(probe, 10000)
|
|
935
|
+
return () => { alive = false; clearInterval(iv) }
|
|
936
|
+
}, [running, sessionId, connection])
|
|
937
|
+
if (!running || !health) return null
|
|
938
|
+
const kind = health.kind
|
|
939
|
+
const silent = fmtSilent(health.idleMs)
|
|
940
|
+
// 收拢态胶囊条颜色语义:绿=活跃 / 黄=工具执行中·静默中 / 红=疑似卡死(脉冲)。
|
|
941
|
+
let text = STR.healthActive, cls = 'hc-active'
|
|
942
|
+
if (kind === 'tool-wait') { text = STR.healthToolWait; cls = 'hc-wait' }
|
|
943
|
+
else if (kind === 'quiet') { text = STR.healthQuiet; cls = 'hc-wait' }
|
|
944
|
+
else if (kind === 'stalled') { text = STR.healthStalled; cls = 'hc-stall' }
|
|
945
|
+
else if (kind !== 'active') return null // ended/unknown 不显示
|
|
946
|
+
return h('button', {
|
|
947
|
+
className: 'sf-healthChip ' + cls,
|
|
948
|
+
title: STR.healthChipTitle,
|
|
949
|
+
onClick: (e) => { e.stopPropagation(); if (props.onOpen) props.onOpen() },
|
|
950
|
+
},
|
|
951
|
+
h('span', { className: 'sf-hcBar' }),
|
|
952
|
+
h('span', { className: 'sf-hcLabel' }, text + (silent && kind !== 'active' ? ' · ' + silent : '')),
|
|
953
|
+
)
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// 工作台打开意图桥:头部芯片点击 → 唤起工作台并直达该会话详情。
|
|
957
|
+
// (工厂作用域:芯片渲染于官方槽位,与本插件视图分属不同 React 子树。)
|
|
958
|
+
const workbenchBridge = {
|
|
959
|
+
intent: null,
|
|
960
|
+
listeners: new Set(),
|
|
961
|
+
open(sid) { this.intent = sid; for (const fn of [...this.listeners]) fn(sid) },
|
|
962
|
+
subscribe(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn) },
|
|
963
|
+
}
|
|
964
|
+
|
|
565
965
|
// ── 总览页 ──────────────────────────────────────────────────────────
|
|
566
966
|
// props: { onOpen: (session) => void, onClose: () => void }
|
|
567
967
|
function SessionFlowOverview(props) {
|
|
@@ -583,28 +983,98 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
583
983
|
// M8a:当前编辑重命名的会话 id(null = 无编辑态)。
|
|
584
984
|
const [renamingId, setRenamingId] = useState(null)
|
|
585
985
|
// M8a:保存自定义标题 → 更新本地列表数据(userTitle 覆盖显示,不整页重刷)。
|
|
986
|
+
// 对齐官方(2026-08 起):非空标题且连接可用时优先走官方 session.rename RPC
|
|
987
|
+
// (log-backed,官方侧栏同步);空标题(清除)/官方拒绝(子代理 agent-busy 等)回退 legacy renames.json。
|
|
586
988
|
const saveRename = (sid, title) => {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
989
|
+
const conn = props.connection
|
|
990
|
+
const applyLocal = (userTitle) => {
|
|
991
|
+
setState((prev) => {
|
|
992
|
+
if (!prev || !prev.data) return prev
|
|
993
|
+
return {
|
|
994
|
+
...prev,
|
|
995
|
+
data: {
|
|
996
|
+
...prev.data,
|
|
997
|
+
workspaces: (prev.data.workspaces || []).map((g) => ({
|
|
998
|
+
...g,
|
|
999
|
+
sessions: (g.sessions || []).map((s) => (s.id === sid ? { ...s, userTitle } : s)),
|
|
1000
|
+
})),
|
|
1001
|
+
},
|
|
1002
|
+
}
|
|
1003
|
+
})
|
|
1004
|
+
setRenamingId(null)
|
|
1005
|
+
return true
|
|
1006
|
+
}
|
|
1007
|
+
const legacy = () => api('rename', { sessionId: sid, title }).then((json) => {
|
|
1008
|
+
if (json && json.ok) return applyLocal(json.userTitle)
|
|
605
1009
|
throw new Error((json && json.error) || STR.renameFail)
|
|
606
1010
|
})
|
|
1011
|
+
const trimmed = String(title || '').trim()
|
|
1012
|
+
// 官方 user 标题不可经本插件清空(官方服务无删除语义):阻止并提示。
|
|
1013
|
+
if (trimmed === '') {
|
|
1014
|
+
const all = ((state.data && state.data.workspaces) || []).flatMap((g) => g.sessions || [])
|
|
1015
|
+
const target = all.find((x) => x.id === sid)
|
|
1016
|
+
if (target && target.titleSource === 'user') return Promise.reject(new Error(STR.renameClearBlocked))
|
|
1017
|
+
}
|
|
1018
|
+
if (trimmed !== '' && canOfficialRename(conn)) {
|
|
1019
|
+
return renameViaOfficial(conn, sid, trimmed)
|
|
1020
|
+
.then((accepted) => applyLocal(accepted))
|
|
1021
|
+
.catch(() => legacy())
|
|
1022
|
+
}
|
|
1023
|
+
return legacy()
|
|
607
1024
|
}
|
|
1025
|
+
// ── 卡死监控:总览探测 ─────────────────────────────────────────────
|
|
1026
|
+
// 官方 sessions.list 的 running(attached agent 真实状态,比 15 分钟启发式准)+
|
|
1027
|
+
// tail history → host derive 健康分类。30s 轮询,上限 8 个、并发 3。
|
|
1028
|
+
const [healthMap, setHealthMap] = useState({})
|
|
1029
|
+
const [liveRunning, setLiveRunning] = useState(null) // null=未探测;Set<sessionId>
|
|
1030
|
+
useEffect(() => {
|
|
1031
|
+
const conn = props.connection
|
|
1032
|
+
if (!canOfficialRename(conn) || state.phase !== 'ready') return undefined
|
|
1033
|
+
let alive = true
|
|
1034
|
+
let probing = false
|
|
1035
|
+
const probe = async () => {
|
|
1036
|
+
if (probing) return
|
|
1037
|
+
probing = true
|
|
1038
|
+
try {
|
|
1039
|
+
const resp = await conn.api.sessions.list({})
|
|
1040
|
+
const items = (resp && resp.result && resp.result.value && resp.result.value.items) || []
|
|
1041
|
+
const runningIds = items.filter((i) => i.running === true).map((i) => i.sessionId)
|
|
1042
|
+
if (!alive) return
|
|
1043
|
+
setLiveRunning(new Set(runningIds))
|
|
1044
|
+
const next = {}
|
|
1045
|
+
for (let i = 0; i < Math.min(runningIds.length, 8); i += 3) {
|
|
1046
|
+
await Promise.all(runningIds.slice(i, i + 3).map(async (sid) => {
|
|
1047
|
+
try {
|
|
1048
|
+
const hr = await conn.api.sessions.history({ sessionId: sid, maxMessages: 3 })
|
|
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) : []
|
|
1051
|
+
if (events.length === 0) return
|
|
1052
|
+
const d = await api('derive', { events, now: Date.now(), assumeRunning: true })
|
|
1053
|
+
if (d && d.ok && d.health) next[sid] = d.health
|
|
1054
|
+
} catch {}
|
|
1055
|
+
}))
|
|
1056
|
+
if (!alive) return
|
|
1057
|
+
}
|
|
1058
|
+
if (alive) setHealthMap(next)
|
|
1059
|
+
} catch {} finally { probing = false }
|
|
1060
|
+
}
|
|
1061
|
+
probe()
|
|
1062
|
+
const iv = setInterval(probe, 30000)
|
|
1063
|
+
return () => { alive = false; clearInterval(iv) }
|
|
1064
|
+
}, [props.connection, state.phase])
|
|
1065
|
+
// 头部芯片直达:工作台打开意图 → 列表就绪后自动进入该会话详情(每个意图只消费一次)。
|
|
1066
|
+
// T3 修复:无论命中与否都标记消费 + 通知父级清意图——此前 Overview 卸载(进详情)后
|
|
1067
|
+
// intentConsumed ref 随之丢失,「← 返回」重挂 Overview 时旧意图被再次消费,
|
|
1068
|
+
// 表现为返回后又被拉回详情(用户实测);未命中时也消费,避免每次重挂都重试查找。
|
|
1069
|
+
const intentConsumed = useRef(null)
|
|
1070
|
+
useEffect(() => {
|
|
1071
|
+
if (!props.intentId || intentConsumed.current === props.intentId || state.phase !== 'ready') return
|
|
1072
|
+
intentConsumed.current = props.intentId
|
|
1073
|
+
if (props.onIntentConsumed) props.onIntentConsumed()
|
|
1074
|
+
const all = ((state.data && state.data.workspaces) || []).flatMap((g) => g.sessions || [])
|
|
1075
|
+
const target = all.find((x) => x.id === props.intentId)
|
|
1076
|
+
if (target) props.onOpen(target)
|
|
1077
|
+
}, [props.intentId, state.phase])
|
|
608
1078
|
useEffect(() => () => {
|
|
609
1079
|
if (fullTextTimer.current) clearTimeout(fullTextTimer.current)
|
|
610
1080
|
if (fullTextAbort.current) fullTextAbort.current.abort()
|
|
@@ -738,7 +1208,11 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
738
1208
|
}
|
|
739
1209
|
}
|
|
740
1210
|
const now = Date.now()
|
|
741
|
-
|
|
1211
|
+
// 卡死监控:官方探测过(liveRunning 非 null)则以 attached agent 真实状态为准,
|
|
1212
|
+
// 否则回退 15 分钟活动窗口启发式。
|
|
1213
|
+
const running = liveRunning === null
|
|
1214
|
+
? s.lastEventTime !== null && s.lastEventTime !== undefined && (now - s.lastEventTime) < ACTIVE_WINDOW_MS
|
|
1215
|
+
: liveRunning.has(s.id)
|
|
742
1216
|
out.push({ ...s, workspace: group.name, workspaceLabel: wsLabelOf(group), workspaceCwd: wsCwdOf(group), running, isEmpty: isSessionEmpty(s) })
|
|
743
1217
|
}
|
|
744
1218
|
}
|
|
@@ -754,7 +1228,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
754
1228
|
const ascending = sort === 'oldest'
|
|
755
1229
|
out.sort((a, b) => (ascending ? key(a) - key(b) : key(b) - key(a)))
|
|
756
1230
|
return out
|
|
757
|
-
}, [state.data, query, wsFilter, sort, showEmpty, showIssues])
|
|
1231
|
+
}, [state.data, query, wsFilter, sort, showEmpty, showIssues, liveRunning])
|
|
758
1232
|
|
|
759
1233
|
// M5b 档案统计:聚合工具出现热度与问题会话数,跟随选中的工作区(wsFilter)变化。
|
|
760
1234
|
const stats = useMemo(() => {
|
|
@@ -904,13 +1378,14 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
904
1378
|
h('div', { className: 'sf-cardTitle' },
|
|
905
1379
|
renamingId === s.id
|
|
906
1380
|
? h(RenameInline, {
|
|
907
|
-
initial: s.userTitle || s.title || '',
|
|
1381
|
+
initial: (props.liveTitles && props.liveTitles[s.id]) || s.userTitle || s.title || '',
|
|
908
1382
|
onSave: (t) => saveRename(s.id, t),
|
|
909
1383
|
onCancel: () => setRenamingId(null),
|
|
910
1384
|
})
|
|
911
1385
|
: h('span', { className: 'sf-cardText', style: { overflow: 'hidden', textOverflow: 'ellipsis' } },
|
|
912
|
-
|
|
913
|
-
|
|
1386
|
+
// 显示优先级:mux 近实时官方标题 > userTitle(档案 user/遗留 overlay)> 自动标题。
|
|
1387
|
+
(props.liveTitles && props.liveTitles[s.id]) || s.userTitle || s.title || STR.unknownTitle,
|
|
1388
|
+
(s.userTitle || (props.liveTitles && props.liveTitles[s.id])) && h('span', { className: 'sf-origTitle', title: s.title || STR.unknownTitle },
|
|
914
1389
|
STR.origTitle + ': ' + (s.title || STR.unknownTitle)),
|
|
915
1390
|
h('button', {
|
|
916
1391
|
className: 'sf-renameBtn', title: STR.rename,
|
|
@@ -919,6 +1394,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
919
1394
|
),
|
|
920
1395
|
s.isEmpty && h('span', { className: 'sf-badge sf-badgeEnd' }, STR.emptySession),
|
|
921
1396
|
s.running ? h('span', { className: 'sf-badge sf-badgeRun' }, STR.running) : h('span', { className: 'sf-badge sf-badgeEnd' }, STR.ended),
|
|
1397
|
+
healthBadge(healthMap[s.id]),
|
|
922
1398
|
s.delegationDepth > 0 && h('span', { className: 'sf-badge sf-badgeSub' }, STR.subagent + ' · d' + s.delegationDepth),
|
|
923
1399
|
s.toolErrors > 0 && h('span', { className: 'sf-badge sf-badgeErr' }, STR.errors + ': ' + s.toolErrors),
|
|
924
1400
|
),
|
|
@@ -1357,8 +1833,12 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1357
1833
|
// M8a:标题重命名(编辑态 + 保存后的显示覆盖,优先于 session.userTitle)。
|
|
1358
1834
|
const [renaming, setRenaming] = useState(false)
|
|
1359
1835
|
const [titleOverride, setTitleOverride] = useState(null)
|
|
1836
|
+
// mux 近实时官方标题(SessionFlowView 订阅 session/projection(title) 帧传入)。
|
|
1837
|
+
const liveTitleOf = (id) => (props.liveTitles && props.liveTitles[id]) || null
|
|
1838
|
+
// 对齐官方:优先官方 session.rename RPC(log-backed,双向一致);空标题/官方拒绝回退 legacy。
|
|
1360
1839
|
const saveRename = (title) => {
|
|
1361
|
-
|
|
1840
|
+
const conn = props.connection
|
|
1841
|
+
const legacy = () => api('rename', { sessionId: session.id, title }).then((json) => {
|
|
1362
1842
|
if (json && json.ok) {
|
|
1363
1843
|
setTitleOverride(json.userTitle) // null = 清除恢复原名
|
|
1364
1844
|
setRenaming(false)
|
|
@@ -1366,6 +1846,14 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1366
1846
|
}
|
|
1367
1847
|
throw new Error((json && json.error) || STR.renameFail)
|
|
1368
1848
|
})
|
|
1849
|
+
const trimmed = String(title || '').trim()
|
|
1850
|
+
if (trimmed === '' && session.titleSource === 'user') return Promise.reject(new Error(STR.renameClearBlocked))
|
|
1851
|
+
if (trimmed !== '' && canOfficialRename(conn)) {
|
|
1852
|
+
return renameViaOfficial(conn, session.id, trimmed)
|
|
1853
|
+
.then((accepted) => { setTitleOverride(accepted); setRenaming(false); return true })
|
|
1854
|
+
.catch(() => legacy())
|
|
1855
|
+
}
|
|
1856
|
+
return legacy()
|
|
1369
1857
|
}
|
|
1370
1858
|
|
|
1371
1859
|
useEffect(() => {
|
|
@@ -1514,15 +2002,15 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1514
2002
|
return
|
|
1515
2003
|
}
|
|
1516
2004
|
const events = val.events.map((entry) => (entry && entry.event) || entry)
|
|
1517
|
-
const derived = await api('derive', { events })
|
|
2005
|
+
const derived = await api('derive', { events, now: Date.now() })
|
|
1518
2006
|
if (!derived || !derived.ok) {
|
|
1519
2007
|
setLiveState((s) => ({ ...s, phase: 'error', error: (derived && derived.error) || STR.liveFail }))
|
|
1520
2008
|
return
|
|
1521
2009
|
}
|
|
1522
2010
|
const tl = (derived.timeline && derived.timeline.turns) || []
|
|
1523
|
-
// 实时视图只保留「最近
|
|
1524
|
-
// 不随 history 窗口大小变化——新回合出现时最旧的被挤出。
|
|
1525
|
-
const LIVE_MAX_HISTORY_TURNS =
|
|
2011
|
+
// 实时视图只保留「最近 N 个历史回合 + 当前运行回合」(若有),数量恒定,
|
|
2012
|
+
// 不随 history 窗口大小变化——新回合出现时最旧的被挤出。N 可配置(设置页)。
|
|
2013
|
+
const LIVE_MAX_HISTORY_TURNS = sfSettings.current.liveHistoryTurns
|
|
1526
2014
|
const liveRunning = derived.running === true
|
|
1527
2015
|
const keepCount = tl.length > LIVE_MAX_HISTORY_TURNS + (liveRunning ? 1 : 0)
|
|
1528
2016
|
? LIVE_MAX_HISTORY_TURNS + (liveRunning ? 1 : 0)
|
|
@@ -1537,6 +2025,8 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1537
2025
|
// 运行中 = 事件结构信号(未闭合回合/步骤/工具调用或流式中间态),
|
|
1538
2026
|
// 不依赖时间戳:输出间隔长(模型思考/长工具执行)时不会误判停止。
|
|
1539
2027
|
running: liveRunning,
|
|
2028
|
+
// 卡死监控:host 分类(active/tool-wait/quiet/stalled/ended)。
|
|
2029
|
+
health: derived.health || null,
|
|
1540
2030
|
})
|
|
1541
2031
|
// 只把「新出现的回合」并入折叠集合(默认折叠);已展开的回合不被收拢。
|
|
1542
2032
|
const newTurns = displayTl.filter((t) => !liveSeenTurns.current.has(t.turn))
|
|
@@ -1555,15 +2045,30 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1555
2045
|
const enterLive = () => {
|
|
1556
2046
|
setLiveOn(true)
|
|
1557
2047
|
liveSeenTurns.current.clear() // 重进实时:全新视图,首轮全部折叠
|
|
2048
|
+
tlFollowRef.current = true // T7:开播默认吸底
|
|
1558
2049
|
fetchLive.current()
|
|
1559
2050
|
if (liveTimer.current === null) {
|
|
1560
|
-
liveTimer.current = setInterval(() => { fetchLive.current() },
|
|
2051
|
+
liveTimer.current = setInterval(() => { fetchLive.current() }, sfSettings.current.livePollMs)
|
|
1561
2052
|
}
|
|
1562
2053
|
}
|
|
2054
|
+
// T7 实时吸底跟随:用户在时间线底部(距底 ≤liveFollowPx)时,每次轮询刷新后平滑滚到
|
|
2055
|
+
// 新底部;上滑读历史不跟随(onScroll 实时追踪)。阈值默认 40px(可在设置页调整)。
|
|
2056
|
+
const tlScrollRef = useRef(null)
|
|
2057
|
+
const tlFollowRef = useRef(true)
|
|
2058
|
+
const onTlScroll = () => {
|
|
2059
|
+
const el = tlScrollRef.current
|
|
2060
|
+
if (el === null) return
|
|
2061
|
+
tlFollowRef.current = el.scrollHeight - el.scrollTop - el.clientHeight <= sfSettings.current.liveFollowPx
|
|
2062
|
+
}
|
|
2063
|
+
useEffect(() => {
|
|
2064
|
+
if (!liveOn || liveState.timeline === null) return
|
|
2065
|
+
const el = tlScrollRef.current
|
|
2066
|
+
if (el !== null && tlFollowRef.current) el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' })
|
|
2067
|
+
}, [liveState.timeline, liveOn])
|
|
1563
2068
|
const exitLive = () => {
|
|
1564
2069
|
setLiveOn(false)
|
|
1565
2070
|
liveSeenTurns.current.clear()
|
|
1566
|
-
setLiveState({ phase: 'idle', error: null, timeline: null, eventCount: 0, lastActive: null, running: false })
|
|
2071
|
+
setLiveState({ phase: 'idle', error: null, timeline: null, eventCount: 0, lastActive: null, running: false, health: null })
|
|
1567
2072
|
if (liveTimer.current !== null) { clearInterval(liveTimer.current); liveTimer.current = null }
|
|
1568
2073
|
}
|
|
1569
2074
|
useEffect(() => () => { if (liveTimer.current !== null) clearInterval(liveTimer.current) }, [])
|
|
@@ -1779,13 +2284,13 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1779
2284
|
h('h2', { className: 'sf-viewTitle', style: { flex: '1 1 auto', overflow: 'hidden', textOverflow: 'ellipsis' } },
|
|
1780
2285
|
renaming
|
|
1781
2286
|
? h(RenameInline, {
|
|
1782
|
-
initial: titleOverride || session.userTitle || session.title || '',
|
|
2287
|
+
initial: titleOverride || liveTitleOf(session.id) || session.userTitle || session.title || '',
|
|
1783
2288
|
onSave: saveRename,
|
|
1784
2289
|
onCancel: () => setRenaming(false),
|
|
1785
2290
|
})
|
|
1786
2291
|
: h('span', { style: { display: 'inline-flex', alignItems: 'center', minWidth: 0 } },
|
|
1787
|
-
h('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: (titleOverride || session.userTitle) ? session.title || STR.unknownTitle : undefined },
|
|
1788
|
-
titleOverride || session.userTitle || session.title || STR.unknownTitle),
|
|
2292
|
+
h('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: (titleOverride || liveTitleOf(session.id) || session.userTitle) ? session.title || STR.unknownTitle : undefined },
|
|
2293
|
+
titleOverride || liveTitleOf(session.id) || session.userTitle || session.title || STR.unknownTitle),
|
|
1789
2294
|
h('button', {
|
|
1790
2295
|
className: 'sf-renameBtn', title: STR.rename,
|
|
1791
2296
|
onClick: () => setRenaming(true),
|
|
@@ -1795,10 +2300,15 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1795
2300
|
running ? h('span', { className: 'sf-badge sf-badgeRun' }, STR.running) : h('span', { className: 'sf-badge sf-badgeEnd' }, STR.ended),
|
|
1796
2301
|
session.delegationDepth > 0 && h('span', { className: 'sf-badge sf-badgeSub' }, STR.subagent),
|
|
1797
2302
|
h('button', { className: 'sf-btn', onClick: () => { setPendingSelect(null); setView('lineage') } }, STR.lineage),
|
|
1798
|
-
//
|
|
1799
|
-
|
|
2303
|
+
// T6:并入右栏(工作台详情页 + 嵌入标签页都显示)。嵌入模式点击后额外
|
|
2304
|
+
// 自动切回「对话」标签——右栏已接管会话流展示,中栏标签页继续停留会造成
|
|
2305
|
+
// 双视图冗余(用户指定交互:点击=打开 dock+跳回对话)。
|
|
2306
|
+
h('button', {
|
|
1800
2307
|
className: 'sf-btn',
|
|
1801
|
-
onClick: () => {
|
|
2308
|
+
onClick: () => {
|
|
2309
|
+
if (dockBridge.controller) dockBridge.controller.open()
|
|
2310
|
+
if (props.embedded) activateChatTab()
|
|
2311
|
+
},
|
|
1802
2312
|
title: STR.dockHint,
|
|
1803
2313
|
}, STR.dock),
|
|
1804
2314
|
// M6 实时通道:进行中会话可实时查看(sessions.history → derive → 折叠视图)。
|
|
@@ -1817,8 +2327,8 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1817
2327
|
state.phase === 'error' && h('div', { className: 'sf-hint', style: { color: '#d43b3b' } }, STR.detailFailed + ': ' + String(state.error)),
|
|
1818
2328
|
state.phase === 'empty' && h('div', { className: 'sf-hint' }, STR.emptyHint),
|
|
1819
2329
|
state.phase === 'ready' && h('div', { className: 'sf-detailBody' },
|
|
1820
|
-
//
|
|
1821
|
-
h('div', { className: 'sf-timeline' },
|
|
2330
|
+
// 左侧时间线(T7:ref+onScroll 供实时吸底跟随)
|
|
2331
|
+
h('div', { className: 'sf-timeline', ref: tlScrollRef, onScroll: onTlScroll },
|
|
1822
2332
|
h('div', { className: 'sf-muted', style: { flex: 'none' }, title: data.workspaceCwd || data.workspace },
|
|
1823
2333
|
(data.workspaceLabel || data.workspace || '') + ' · ' + fmtTime(sessionInfo.createdAt) +
|
|
1824
2334
|
' · ' + STR.duration + ' ' + fmtDuration((sessionInfo.lastEventTime || 0) - (sessionInfo.createdAt || 0)) +
|
|
@@ -1889,6 +2399,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1889
2399
|
// M6 实时模式:sessions.history → derive 的完整折叠时间线(轮询刷新)。
|
|
1890
2400
|
liveOn && h('div', { className: 'sf-liveBar', style: { flex: 'none' } },
|
|
1891
2401
|
h('span', { className: 'sf-badge sf-badgeRun', style: { flex: 'none' } }, STR.liveActive),
|
|
2402
|
+
healthBadge(liveState.health),
|
|
1892
2403
|
h('span', { className: 'sf-liveText' },
|
|
1893
2404
|
h('span', { className: 'sf-muted', style: { flex: 'none' } }, (liveState.eventCount || 0) + ' ' + STR.liveEvents),
|
|
1894
2405
|
liveState.lastActive && h('span', { className: 'sf-muted', style: { flex: 'none' } }, STR.liveLastActive + ' ' + fmtTime(liveState.lastActive)),
|
|
@@ -1896,7 +2407,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1896
2407
|
),
|
|
1897
2408
|
h('button', { className: 'sf-btn sf-liveExitBtn', onClick: exitLive }, STR.liveExit),
|
|
1898
2409
|
),
|
|
1899
|
-
|
|
2410
|
+
liveOn && liveState.phase === 'error' && h('div', { className: 'sf-hint', style: { color: '#d43b3b' } }, liveState.error),
|
|
1900
2411
|
liveOn && liveState.phase === 'loading' && liveState.timeline === null && h('div', { className: 'sf-hint' }, STR.liveLoading),
|
|
1901
2412
|
liveOn && liveState.timeline !== null && h(TimelineTurns, {
|
|
1902
2413
|
// 保留回合头(默认折叠可点击展开):hideHead=true 时折叠回合既无内容
|
|
@@ -2119,7 +2630,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2119
2630
|
// 内实时通道同构(结构信号运行判定 / 只折叠新回合 / 不传 hideHead),自包含实现。
|
|
2120
2631
|
function SessionFlowLiveFallback(props) {
|
|
2121
2632
|
const { sessionId, connection } = props
|
|
2122
|
-
const [state, setState] = useState({ phase: 'loading', error: null, timeline: null, eventCount: 0, running: false })
|
|
2633
|
+
const [state, setState] = useState({ phase: 'loading', error: null, timeline: null, eventCount: 0, running: false, health: null })
|
|
2123
2634
|
const [collapsed, setCollapsed] = useState(() => new Set())
|
|
2124
2635
|
const [activeTurn, setActiveTurn] = useState(null)
|
|
2125
2636
|
const liveSeenTurns = useRef(new Set())
|
|
@@ -2140,7 +2651,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2140
2651
|
return
|
|
2141
2652
|
}
|
|
2142
2653
|
const events = val.events.map((entry) => (entry && entry.event) || entry)
|
|
2143
|
-
const derived = await api('derive', { events })
|
|
2654
|
+
const derived = await api('derive', { events, now: Date.now() })
|
|
2144
2655
|
if (!derived || !derived.ok) {
|
|
2145
2656
|
setState((s) => ({ ...s, phase: 'error', error: (derived && derived.error) || STR.liveFail }))
|
|
2146
2657
|
return
|
|
@@ -2149,6 +2660,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2149
2660
|
setState({
|
|
2150
2661
|
phase: 'ready', error: null, timeline: tl, eventCount: events.length,
|
|
2151
2662
|
running: derived.running === true,
|
|
2663
|
+
health: derived.health || null,
|
|
2152
2664
|
})
|
|
2153
2665
|
// 只折叠新出现的回合(默认折叠),已展开的保持用户状态(M6 经验)。
|
|
2154
2666
|
const newTurns = tl.filter((t) => !liveSeenTurns.current.has(t.turn))
|
|
@@ -2168,7 +2680,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2168
2680
|
useEffect(() => {
|
|
2169
2681
|
liveSeenTurns.current.clear()
|
|
2170
2682
|
fetchLive.current()
|
|
2171
|
-
if (timer.current === null) timer.current = setInterval(() => { fetchLive.current() },
|
|
2683
|
+
if (timer.current === null) timer.current = setInterval(() => { fetchLive.current() }, sfSettings.current.livePollMs)
|
|
2172
2684
|
return () => { if (timer.current !== null) { clearInterval(timer.current); timer.current = null } }
|
|
2173
2685
|
}, [sessionId])
|
|
2174
2686
|
|
|
@@ -2184,6 +2696,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2184
2696
|
return h('div', { className: 'sf-view' },
|
|
2185
2697
|
h('div', { className: 'sf-viewHeader' },
|
|
2186
2698
|
h('span', { className: 'sf-badge sf-badgeRun', style: { flex: 'none' } }, STR.liveActive),
|
|
2699
|
+
healthBadge(state.health),
|
|
2187
2700
|
h('span', { className: 'sf-liveText' }, STR.liveFallbackHint),
|
|
2188
2701
|
h('button', { className: 'sf-btn sf-liveExitBtn', onClick: props.onRetry }, STR.retryArchive),
|
|
2189
2702
|
),
|
|
@@ -2202,6 +2715,32 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2202
2715
|
// ── 主视图(总览 ⇄ 详情)───────────────────────────────────────────
|
|
2203
2716
|
function SessionFlowView(props) {
|
|
2204
2717
|
const [detail, setDetail] = useState(null) // { session, searchQuery }
|
|
2718
|
+
// 头部芯片直达意图(workbenchBridge):收到会话 id → 总览数据就绪后自动进详情。
|
|
2719
|
+
const [intentId, setIntentId] = useState(workbenchBridge.intent)
|
|
2720
|
+
useEffect(() => workbenchBridge.subscribe((sid) => { setDetail(null); setIntentId(sid) }), [])
|
|
2721
|
+
// ── 官方标题近实时同步(rename 对齐增强)──────────────────────────
|
|
2722
|
+
// 自建 mux 下行流监听 session/projection(title) 帧:官方 rename / 自动标题生成后
|
|
2723
|
+
// host 向每个 mux 消费者即时广播(value 为标题字符串),秒级同步、免等重扫。
|
|
2724
|
+
const [liveTitles, setLiveTitles] = useState({}) // sessionId -> 最新官方标题
|
|
2725
|
+
useEffect(() => {
|
|
2726
|
+
const conn = props.connection
|
|
2727
|
+
if (conn === undefined || conn.api === undefined || conn.api.events === undefined || typeof conn.api.events.mux !== 'function') return undefined
|
|
2728
|
+
const ac = new AbortController()
|
|
2729
|
+
;(async () => {
|
|
2730
|
+
try {
|
|
2731
|
+
for await (const envelope of conn.api.events.mux({}, ac.signal)) {
|
|
2732
|
+
const f = (envelope && envelope.payload) || envelope
|
|
2733
|
+
if (!f || f.type !== 'session/projection' || f.key !== 'title') continue
|
|
2734
|
+
const title = typeof f.value === 'string' ? f.value : (f.value && typeof f.value.title === 'string' ? f.value.title : '')
|
|
2735
|
+
if (f.sessionId && title !== '') {
|
|
2736
|
+
const sid = f.sessionId
|
|
2737
|
+
setLiveTitles((prev) => (prev[sid] === title ? prev : { ...prev, [sid]: title }))
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
} catch (e) { /* 流断开/中止:静默(组件卸载或下次挂载时重建) */ }
|
|
2741
|
+
})()
|
|
2742
|
+
return () => ac.abort()
|
|
2743
|
+
}, [props.connection])
|
|
2205
2744
|
|
|
2206
2745
|
return detail === null
|
|
2207
2746
|
? h(SessionFlowOverview, {
|
|
@@ -2209,12 +2748,19 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2209
2748
|
onOpen: (session, searchQuery) => setDetail({ session, searchQuery }),
|
|
2210
2749
|
onClose: props.onClose,
|
|
2211
2750
|
sessions: props.sessions,
|
|
2751
|
+
connection: props.connection,
|
|
2752
|
+
liveTitles,
|
|
2753
|
+
intentId,
|
|
2754
|
+
// T3:意图消费后即清除(SessionFlowView 不随详情↔总览切换卸载,状态稳)——
|
|
2755
|
+
// 防止「← 返回」重挂 Overview 时旧意图再次消费把用户拉回详情。
|
|
2756
|
+
onIntentConsumed: () => setIntentId(null),
|
|
2212
2757
|
})
|
|
2213
2758
|
: h(SessionFlowDetail, {
|
|
2214
2759
|
session: detail.session,
|
|
2215
2760
|
initialSearchQuery: detail.searchQuery || '',
|
|
2216
2761
|
sessions: props.sessions,
|
|
2217
2762
|
connection: props.connection,
|
|
2763
|
+
liveTitles,
|
|
2218
2764
|
onBack: () => setDetail(null),
|
|
2219
2765
|
onClose: props.onClose,
|
|
2220
2766
|
})
|
|
@@ -2238,6 +2784,34 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2238
2784
|
: { open: false, listeners: new Set(), controller: null })
|
|
2239
2785
|
const PIANO_WINDOW = 15
|
|
2240
2786
|
|
|
2787
|
+
// T6:从嵌入标签页切回「对话」标签。官方 conversation.view 槽位环的标签栏没有
|
|
2788
|
+
// 公开激活 API(标准 props 仅 sessionId/useSession)——DOM 兜底:先按文案定位我们
|
|
2789
|
+
// 自己的标签按钮(「会话流」),再取其父容器(标签栏)的【直接子元素】逐个按文本
|
|
2790
|
+
// 精确匹配「对话 / Chat」——不限定 button:实测部分内置标签(对话/轨迹)不是
|
|
2791
|
+
// <button> 元素,按 button 过滤会漏掉它们、只命中插件 tab(点了会跳 Plan 图,
|
|
2792
|
+
// 实测踩坑)。命中后点击该元素或其内部首个按钮;未命中则不动作(宁可不动也
|
|
2793
|
+
// 不跳错标签)。React 合成监听挂在本事件上,click() 有效。
|
|
2794
|
+
function activateChatTab() {
|
|
2795
|
+
try {
|
|
2796
|
+
const pane = document.querySelector(CONVERSATION_SELECTOR)
|
|
2797
|
+
if (pane === null) return
|
|
2798
|
+
const btns = Array.prototype.slice.call(pane.querySelectorAll('button'))
|
|
2799
|
+
const ours = btns.find((b) => (b.textContent || '').trim().indexOf(STR.entry) >= 0)
|
|
2800
|
+
if (ours === undefined) return
|
|
2801
|
+
const bar = ours.parentElement
|
|
2802
|
+
if (bar === null) return
|
|
2803
|
+
const kids = Array.prototype.slice.call(bar.children)
|
|
2804
|
+
let target = null
|
|
2805
|
+
for (const k of kids) {
|
|
2806
|
+
if (k === ours || (typeof k.contains === 'function' && k.contains(ours))) continue
|
|
2807
|
+
if (/^(对话|Chat)$/.test((k.textContent || '').trim())) { target = k; break }
|
|
2808
|
+
}
|
|
2809
|
+
if (target === null) return
|
|
2810
|
+
const clickable = target.tagName === 'BUTTON' ? target : (target.querySelector('button') || target)
|
|
2811
|
+
clickable.click()
|
|
2812
|
+
} catch (e) {}
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2241
2815
|
// 定位目标:会话内容区。注意必须选**滚动容器** scrollBody(data-conversation-scroll):
|
|
2242
2816
|
// viewArea 在滚动场景下 rect 是「内容全尺寸」(如 y=-25687, h=26823),用它定位会把
|
|
2243
2817
|
// 竖条甩到视口外(实测踩坑)。scrollBody 才有正确的视口内矩形。
|
|
@@ -2264,16 +2838,29 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2264
2838
|
const { sessions, onOpenWorkbench } = props
|
|
2265
2839
|
const [entries, setEntries] = useState([]) // [{turn, seq, preview}] host 全量用户轮次
|
|
2266
2840
|
const [activeIdx, setActiveIdx] = useState(-1)
|
|
2267
|
-
|
|
2841
|
+
// T5 平滑滚动:连续滚动位置(浮点,单位=行)。替代原整数 winOffset 的「每格硬跳
|
|
2842
|
+
// 1 条」——滚轮增量累积(deltaMode 归一)+ 列表 transform 百分比位移(transition
|
|
2843
|
+
// 追逐),渲染窗口 = 可视 WINDOW 条 + 上下缓冲,位移按列表自身高度百分比换算,
|
|
2844
|
+
// 收拢(4px)/展开(30px)行高变化时比例自守恒,无需测量行距。
|
|
2845
|
+
const [scrollPos, setScrollPos] = useState(0)
|
|
2268
2846
|
const [kbGlobal, setKbGlobal] = useState(null)
|
|
2269
2847
|
const [detailIdx, setDetailIdx] = useState(null) // 右侧信息面板显示的条目索引
|
|
2270
2848
|
const detailTimer = useRef(null)
|
|
2271
2849
|
const rootRef = useRef(null) // 悬浮条根容器(面板定位换算基准)
|
|
2272
2850
|
const [jumpFail, setJumpFail] = useState(false) // 未加载轮次提示
|
|
2273
2851
|
const [, force] = useState(0)
|
|
2852
|
+
// detailIdx 镜像:过渡监听 effect(deps [])闭包里读取当前值,避免读到旧 state。
|
|
2853
|
+
const detailIdxRef = useRef(null)
|
|
2274
2854
|
|
|
2275
2855
|
// 定位自管理:垂直居中悬浮于会话内容区(scrollBody 矩形中心 + translateY(-50%))。
|
|
2276
2856
|
const [pos, setPos] = useState(null) // {left, top(区中心)}
|
|
2857
|
+
useEffect(() => {
|
|
2858
|
+
return () => {
|
|
2859
|
+
const lock = collapseLockRef.current
|
|
2860
|
+
if (lock.timer) clearTimeout(lock.timer)
|
|
2861
|
+
if (lock.retry) clearTimeout(lock.retry)
|
|
2862
|
+
}
|
|
2863
|
+
}, [])
|
|
2277
2864
|
useEffect(() => {
|
|
2278
2865
|
const compute = () => {
|
|
2279
2866
|
const area = pianoViewArea()
|
|
@@ -2305,7 +2892,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2305
2892
|
useEffect(() => {
|
|
2306
2893
|
if (initTailDone.current || entries.length <= WINDOW) return
|
|
2307
2894
|
initTailDone.current = true
|
|
2308
|
-
|
|
2895
|
+
setScrollPos(entries.length - WINDOW)
|
|
2309
2896
|
}, [entries])
|
|
2310
2897
|
|
|
2311
2898
|
// 当前会话 id(仅取 current,不展示跨会话列表)。
|
|
@@ -2392,14 +2979,34 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2392
2979
|
.replace(/[*_#>|~-]{1,3}/g, ' ')
|
|
2393
2980
|
.replace(/\s+/g, ' ')
|
|
2394
2981
|
.trim()
|
|
2395
|
-
//
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2982
|
+
// 行→条目映射(单调整体映射,active 定位与点击跳转共用):
|
|
2983
|
+
// 按文档序游标推进匹配用户行(「继续」类重复文本不会误配到前面条目);
|
|
2984
|
+
// 助手行未匹配时继承前一条用户行的条目(轮次归属)——视口首行为助手
|
|
2985
|
+
// 回复时 active 仍指向其所属用户轮次,不再丢失高亮(T1 修复点一)。
|
|
2986
|
+
const mapRows = () => {
|
|
2987
|
+
const rows = domRows()
|
|
2988
|
+
const idxs = new Array(rows.length).fill(-1)
|
|
2989
|
+
let cursor = 0
|
|
2990
|
+
let last = -1
|
|
2991
|
+
for (let i = 0; i < rows.length; i++) {
|
|
2992
|
+
const t = norm(textOf(rows[i]))
|
|
2993
|
+
if (t) {
|
|
2994
|
+
for (let j = cursor; j < entries.length; j++) {
|
|
2995
|
+
const core = norm(String(entries[j].preview || '').replace(/…+$/u, '')).slice(0, 50)
|
|
2996
|
+
if (core && t.includes(core.slice(0, 30))) { cursor = j + 1; last = j; break }
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
idxs[i] = last
|
|
2402
3000
|
}
|
|
3001
|
+
return { rows, idxs }
|
|
3002
|
+
}
|
|
3003
|
+
// 条目 → DOM 行:从映射中取首个归属该条目的行(用户行本身;映射按文档序
|
|
3004
|
+
// 单调推进,重复文本不会命中到前面的同文条目)。
|
|
3005
|
+
const findRow = (entry) => {
|
|
3006
|
+
const target = entries.indexOf(entry)
|
|
3007
|
+
if (target === -1) return null
|
|
3008
|
+
const { rows, idxs } = mapRows()
|
|
3009
|
+
for (let i = 0; i < rows.length; i++) if (idxs[i] === target) return rows[i]
|
|
2403
3010
|
return null
|
|
2404
3011
|
}
|
|
2405
3012
|
|
|
@@ -2413,35 +3020,85 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2413
3020
|
setTimeout(() => setJumpFail(false), 2600)
|
|
2414
3021
|
}
|
|
2415
3022
|
|
|
2416
|
-
// active
|
|
3023
|
+
// active:视口内最靠上的可见行 → 映射条目索引高亮(阅读位置跟随)。
|
|
3024
|
+
// 可见判定用 bottom 进入视口(含被顶部裁切一半的行)——此前 top>=0 会
|
|
3025
|
+
// 跳过被裁切行导致高亮偏晚一轮(T1 修复点二)。
|
|
2417
3026
|
useEffect(() => {
|
|
2418
3027
|
const compute = () => {
|
|
2419
3028
|
if (entries.length === 0) { setActiveIdx(-1); return }
|
|
2420
|
-
const rows =
|
|
2421
|
-
let target =
|
|
3029
|
+
const { rows, idxs } = mapRows()
|
|
3030
|
+
let target = -1
|
|
2422
3031
|
let bestTop = Number.POSITIVE_INFINITY
|
|
2423
|
-
for (
|
|
2424
|
-
const
|
|
2425
|
-
if (
|
|
3032
|
+
for (let i = 0; i < rows.length; i++) {
|
|
3033
|
+
const r = rows[i].getBoundingClientRect()
|
|
3034
|
+
if (r.bottom > 1 && r.top < bestTop) { bestTop = r.top; target = i }
|
|
2426
3035
|
}
|
|
2427
|
-
if (
|
|
2428
|
-
const
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
return core && t.includes(core.slice(0, 30))
|
|
2432
|
-
})
|
|
2433
|
-
setActiveIdx(idx)
|
|
3036
|
+
if (target === -1) { setActiveIdx(entries.length - 1); return }
|
|
3037
|
+
const idx = idxs[target]
|
|
3038
|
+
// idx<0(视口首行在第一条用户消息之前):保持现状,不清高亮不跳变。
|
|
3039
|
+
if (idx >= 0) setActiveIdx(idx)
|
|
2434
3040
|
}
|
|
2435
3041
|
compute()
|
|
2436
3042
|
const iv = setInterval(compute, 800)
|
|
2437
3043
|
return () => clearInterval(iv)
|
|
2438
3044
|
}, [entries])
|
|
2439
3045
|
|
|
2440
|
-
const WINDOW =
|
|
3046
|
+
const WINDOW = sfSettings.current.pianoWindow
|
|
2441
3047
|
const hidden = entries.length < 2
|
|
2442
|
-
|
|
2443
|
-
const
|
|
2444
|
-
const
|
|
3048
|
+
// 虚拟窗口:renderStart 起渲染 WINDOW+3 条(上方缓冲 1 + 下方缓冲 2)。
|
|
3049
|
+
const maxOff = Math.max(0, entries.length - WINDOW)
|
|
3050
|
+
const clampedPos = Math.max(0, Math.min(maxOff, scrollPos))
|
|
3051
|
+
const renderStart = Math.max(0, Math.floor(clampedPos) - 1)
|
|
3052
|
+
const renderCount = Math.min(WINDOW + 3, entries.length - renderStart)
|
|
3053
|
+
const windowed = entries.slice(renderStart, renderStart + renderCount)
|
|
3054
|
+
const winOffset = Math.round(clampedPos) // 逻辑口径(高亮/面板/▲▼基准)四舍五入
|
|
3055
|
+
const canUp = clampedPos > 0.01
|
|
3056
|
+
const canDown = clampedPos < maxOff - 0.01
|
|
3057
|
+
// T5 显示位(ref,非 state——逐帧 DOM 更新不过 React):renderStartRef 镜像供
|
|
3058
|
+
// 逐帧换算;displayPos 由 rAF 追逐 clampedPos(指数趋近 .22/帧 ≈ .16s 到位)。
|
|
3059
|
+
const displayPosRef = useRef(0)
|
|
3060
|
+
const renderStartRef = useRef(0)
|
|
3061
|
+
renderStartRef.current = renderStart
|
|
3062
|
+
const applyShift = () => {
|
|
3063
|
+
const list = listRef.current
|
|
3064
|
+
if (list === null || list.children.length < 2) return
|
|
3065
|
+
const pitch = list.children[1].offsetTop - list.children[0].offsetTop
|
|
3066
|
+
if (!(pitch > 0)) return
|
|
3067
|
+
list.style.transform = 'translate3d(0,' + (-(displayPosRef.current - renderStartRef.current) * pitch).toFixed(2) + 'px,0)'
|
|
3068
|
+
// strip 高度显式管理:clipper 脱流后 strip 不能靠内容撑高(塌缩=整条消失)。
|
|
3069
|
+
// 纯内容高 = 可见行数 × 实测行距 − 间隙(12×35−5=415 / 12×9−5=103);呼吸
|
|
3070
|
+
// 在 strip 外(head 下边距 8 + 根下内边距 8);键高动画期间 RO 逐帧跟随。
|
|
3071
|
+
const rows = Math.min(WINDOW, entries.length)
|
|
3072
|
+
const strip = stripRef.current
|
|
3073
|
+
if (strip !== null) strip.style.height = (rows * pitch - 5).toFixed(1) + 'px'
|
|
3074
|
+
}
|
|
3075
|
+
useEffect(() => {
|
|
3076
|
+
let raf = 0
|
|
3077
|
+
const step = () => {
|
|
3078
|
+
const target = Math.max(0, Math.min(Math.max(0, entries.length - WINDOW), scrollPos))
|
|
3079
|
+
const cur = displayPosRef.current
|
|
3080
|
+
if (Math.abs(target - cur) < 0.002) {
|
|
3081
|
+
displayPosRef.current = target
|
|
3082
|
+
applyShift()
|
|
3083
|
+
return
|
|
3084
|
+
}
|
|
3085
|
+
displayPosRef.current = cur + (target - cur) * 0.22
|
|
3086
|
+
applyShift()
|
|
3087
|
+
raf = requestAnimationFrame(step)
|
|
3088
|
+
}
|
|
3089
|
+
raf = requestAnimationFrame(step)
|
|
3090
|
+
return () => cancelAnimationFrame(raf)
|
|
3091
|
+
}, [scrollPos, entries.length])
|
|
3092
|
+
// renderStart 跳变(内容重排)后同步重设 transform(渲染后、绘制前),显示位
|
|
3093
|
+
// 不变 → 无视觉跳变;列表高度随键高变化(收拢/展开)时重算行距保持对齐。
|
|
3094
|
+
useEffect(() => { applyShift() })
|
|
3095
|
+
useEffect(() => {
|
|
3096
|
+
const list = listRef.current
|
|
3097
|
+
if (list === null || typeof ResizeObserver !== 'function') return undefined
|
|
3098
|
+
const ro = new ResizeObserver(() => applyShift())
|
|
3099
|
+
ro.observe(list)
|
|
3100
|
+
return () => ro.disconnect()
|
|
3101
|
+
}, [renderStart, entries.length])
|
|
2445
3102
|
|
|
2446
3103
|
// 悬浮条滚轮/▲▼:只翻悬浮条自身窗口,绝不影响会话区滚动(用户明确要求)。
|
|
2447
3104
|
const onKeyDown = (e) => {
|
|
@@ -2460,35 +3117,118 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2460
3117
|
}
|
|
2461
3118
|
useEffect(() => {
|
|
2462
3119
|
if (kbGlobal === null) return
|
|
2463
|
-
|
|
2464
|
-
|
|
3120
|
+
setScrollPos((o) => {
|
|
3121
|
+
const p = Math.max(0, Math.min(maxOff, o))
|
|
3122
|
+
if (kbGlobal < p) return kbGlobal
|
|
3123
|
+
if (kbGlobal >= p + WINDOW) return Math.max(0, Math.min(maxOff, kbGlobal - WINDOW + 1))
|
|
3124
|
+
return o
|
|
3125
|
+
})
|
|
2465
3126
|
}, [kbGlobal])
|
|
2466
3127
|
|
|
2467
|
-
//
|
|
3128
|
+
// 滚轮平滑滚动:React onWheel 是被动监听器(passive: true),内部 preventDefault
|
|
2468
3129
|
// 无效并刷屏告警(实测踩坑)——改原生 addEventListener('wheel', {passive:false})。
|
|
3130
|
+
// 增量累积(deltaMode 归一为像素;~0.012 行/px:一整格滚轮 ≈1.2 行,触控板小
|
|
3131
|
+
// 增量连续累积),rAF 追逐成连续位移。**静止吸附**:滚轮停 ~170ms 后取整到最近
|
|
3132
|
+
// 整行(chase 平滑滑过去)——小数位置会让顶/底条露半截(「展示了 T23 的一部
|
|
3133
|
+
// 分」实测踩坑),用户要求静止时恰好完整 12 条。
|
|
2469
3134
|
const stripRef = useRef(null)
|
|
3135
|
+
const listRef = useRef(null) // T5 虚拟滚动列表容器(键的父级,transform 平移)
|
|
3136
|
+
const snapTimerRef = useRef(0)
|
|
2470
3137
|
useEffect(() => {
|
|
2471
3138
|
const el = stripRef.current
|
|
2472
3139
|
if (el === null) return undefined
|
|
2473
3140
|
const onNativeWheel = (e) => {
|
|
2474
3141
|
e.preventDefault()
|
|
2475
3142
|
e.stopPropagation()
|
|
2476
|
-
|
|
2477
|
-
|
|
3143
|
+
let dy = e.deltaY
|
|
3144
|
+
if (e.deltaMode === 1) dy *= 33
|
|
3145
|
+
else if (e.deltaMode === 2) dy *= 300
|
|
3146
|
+
setScrollPos((o) => Math.max(0, Math.min(Math.max(0, entries.length - WINDOW), o + dy * sfSettings.current.pianoWheelSpeed)))
|
|
3147
|
+
// 静止吸附:连续滚动不断顺延;停止后吸附到最近整行。
|
|
3148
|
+
if (snapTimerRef.current !== 0) clearTimeout(snapTimerRef.current)
|
|
3149
|
+
snapTimerRef.current = setTimeout(() => {
|
|
3150
|
+
snapTimerRef.current = 0
|
|
3151
|
+
setScrollPos((o) => Math.round(o))
|
|
3152
|
+
}, sfSettings.current.pianoSnapMs)
|
|
2478
3153
|
}
|
|
2479
3154
|
el.addEventListener('wheel', onNativeWheel, { passive: false })
|
|
2480
|
-
return () =>
|
|
3155
|
+
return () => {
|
|
3156
|
+
el.removeEventListener('wheel', onNativeWheel)
|
|
3157
|
+
if (snapTimerRef.current !== 0) { clearTimeout(snapTimerRef.current); snapTimerRef.current = 0 }
|
|
3158
|
+
}
|
|
2481
3159
|
}, [entries.length])
|
|
2482
3160
|
|
|
3161
|
+
// 面板锚定刷新:detailPos 在渲染时刻从键的实时矩形计算——mouseenter 常发生在
|
|
3162
|
+
// 收拢/半展开几何(root 宽 32),面板被锚在收拢条右缘=鼠标旁,且展开完成后无
|
|
3163
|
+
// 重渲染而停留在旧坐标。监听过渡结束(React 合成事件不含 transitionend 的完整
|
|
3164
|
+
// 语义),仅几何属性(width/height/max-height)且面板显示中时强制重渲染,用最终
|
|
3165
|
+
// 几何重算(面板可见延迟 .28s > 展开 .26s,用户看到的就是正确位置)。
|
|
3166
|
+
useEffect(() => {
|
|
3167
|
+
const el = rootRef.current
|
|
3168
|
+
if (el === null) return undefined
|
|
3169
|
+
const onEnd = (e) => {
|
|
3170
|
+
if (detailIdxRef.current !== null &&
|
|
3171
|
+
(e.propertyName === 'width' || e.propertyName === 'height' || e.propertyName === 'max-height')) {
|
|
3172
|
+
force((t) => t + 1)
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
el.addEventListener('transitionend', onEnd)
|
|
3176
|
+
return () => el.removeEventListener('transitionend', onEnd)
|
|
3177
|
+
}, [])
|
|
3178
|
+
|
|
2483
3179
|
// 右侧信息面板:hover 条目/键盘聚焦时弹出该轮信息(保留弹出面板、移除原生
|
|
2484
|
-
// tooltip——tooltip
|
|
3180
|
+
// tooltip——tooltip 与面板重复且视觉凌乱);鼠标在条目间移动 250ms 宽限。
|
|
2485
3181
|
const showDetail = (idx) => {
|
|
3182
|
+
const lock = collapseLockRef.current
|
|
3183
|
+
if (Date.now() < lock.until) {
|
|
3184
|
+
// 锁检查必须先于 clearTimeout:收拢条上的合成 mouseenter(skipped)不得
|
|
3185
|
+
// 清掉待执行的隐藏定时器(实测:连续 skipped 把面板拖了 ~1s 不走)。
|
|
3186
|
+
if (lock.kind === 'enter') {
|
|
3187
|
+
// 进入护栏期(hover-guard):推迟到锁过期后重试——驻留则面板照常出现,
|
|
3188
|
+
// 快速路过则随离场作废(重试前检查 root 仍 :hover)。
|
|
3189
|
+
if (lock.retry) clearTimeout(lock.retry)
|
|
3190
|
+
lock.retry = setTimeout(() => {
|
|
3191
|
+
lock.retry = null
|
|
3192
|
+
if (rootRef.current !== null && rootRef.current.matches(':hover')) showDetail(idx)
|
|
3193
|
+
}, Math.max(20, lock.until - Date.now() + 20))
|
|
3194
|
+
return
|
|
3195
|
+
}
|
|
3196
|
+
return
|
|
3197
|
+
}
|
|
3198
|
+
if (lock.retry) { clearTimeout(lock.retry); lock.retry = null }
|
|
2486
3199
|
if (detailTimer.current) clearTimeout(detailTimer.current)
|
|
2487
3200
|
setDetailIdx(idx)
|
|
2488
3201
|
}
|
|
2489
3202
|
const hideDetail = () => {
|
|
3203
|
+
const lock = collapseLockRef.current
|
|
3204
|
+
if (lock.retry) { clearTimeout(lock.retry); lock.retry = null }
|
|
2490
3205
|
if (detailTimer.current) clearTimeout(detailTimer.current)
|
|
2491
|
-
|
|
3206
|
+
// 宽限 250ms:标题行/首键边界窄带来回移动时,面板滑出动画(.24s)频繁被
|
|
3207
|
+
// 打断重入,加长宽限减少闪动。
|
|
3208
|
+
detailTimer.current = setTimeout(() => setDetailIdx(null), 250)
|
|
3209
|
+
}
|
|
3210
|
+
const collapseLockRef = useRef({ until: 0, timer: null, retry: null, kind: '' })
|
|
3211
|
+
// hover-guard / collapse-lock 二合一:进入锁 120ms(「快速路过闪动」——锁窗内
|
|
3212
|
+
// :hover 不展开,路过即零动画;驻留 120ms 后照常展开)+离开锁 300ms(防几何
|
|
3213
|
+
// 自反馈回路:展开态越界→收拢→胶囊条弹到静止鼠标下→合成 mouseenter→再展开)。
|
|
3214
|
+
// kind 区分 showDetail 策略:enter 锁→推迟重试(驻留后面板照常),leave 锁→跳过。
|
|
3215
|
+
const armCollapseLock = (ms, kind) => {
|
|
3216
|
+
const lock = collapseLockRef.current
|
|
3217
|
+
const el = rootRef.current
|
|
3218
|
+
const w = typeof ms === 'number' ? ms : 300
|
|
3219
|
+
if (el !== null && el.getAttribute('data-collapse-lock') === null) {
|
|
3220
|
+
el.setAttribute('data-collapse-lock', '')
|
|
3221
|
+
}
|
|
3222
|
+
lock.kind = kind === 'enter' ? 'enter' : 'leave'
|
|
3223
|
+
lock.until = Date.now() + w
|
|
3224
|
+
if (lock.timer) clearTimeout(lock.timer)
|
|
3225
|
+
lock.timer = setTimeout(() => {
|
|
3226
|
+
lock.until = 0
|
|
3227
|
+
lock.kind = ''
|
|
3228
|
+
if (lock.timer) { clearTimeout(lock.timer); lock.timer = null }
|
|
3229
|
+
const el2 = rootRef.current
|
|
3230
|
+
if (el2 !== null) el2.removeAttribute('data-collapse-lock')
|
|
3231
|
+
}, w)
|
|
2492
3232
|
}
|
|
2493
3233
|
useEffect(() => () => { if (detailTimer.current) clearTimeout(detailTimer.current) }, [])
|
|
2494
3234
|
// 键盘聚焦跟随显示面板。
|
|
@@ -2496,6 +3236,9 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2496
3236
|
if (kbGlobal !== null && entries[kbGlobal]) showDetail(kbGlobal)
|
|
2497
3237
|
}, [kbGlobal])
|
|
2498
3238
|
|
|
3239
|
+
// detailIdx 镜像同步(供 deps [] 的过渡监听闭包读取当前值)。
|
|
3240
|
+
useEffect(() => { detailIdxRef.current = detailIdx }, [detailIdx])
|
|
3241
|
+
|
|
2499
3242
|
const rootStyle = pos === null ? { display: 'none' } : {
|
|
2500
3243
|
left: pos.left + 'px',
|
|
2501
3244
|
top: pos.top + 'px',
|
|
@@ -2505,12 +3248,12 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2505
3248
|
|
|
2506
3249
|
const detailEntry = detailIdx !== null ? entries[detailIdx] : null
|
|
2507
3250
|
// 面板位置:absolute 相对悬浮条容器(容器有 translateY(-50%) transform,fixed
|
|
2508
|
-
// 会退化为相对容器——必须按容器内偏移计算)。条目 =
|
|
2509
|
-
// 索引 = detailIdx -
|
|
3251
|
+
// 会退化为相对容器——必须按容器内偏移计算)。条目 = 列表(pk-list)直接子元素,
|
|
3252
|
+
// 索引 = detailIdx - renderStart(虚拟窗口含缓冲);越界修正按视口坐标换算回容器内。
|
|
2510
3253
|
let detailPos = null
|
|
2511
3254
|
const rootRect = rootRef.current ? rootRef.current.getBoundingClientRect() : null
|
|
2512
|
-
if (detailEntry && rootRect) {
|
|
2513
|
-
const el =
|
|
3255
|
+
if (detailEntry && rootRect && listRef.current) {
|
|
3256
|
+
const el = listRef.current.children[detailIdx - renderStart]
|
|
2514
3257
|
if (el instanceof Element) {
|
|
2515
3258
|
const r = el.getBoundingClientRect()
|
|
2516
3259
|
const topV = Math.min(r.top, Math.max(4, window.innerHeight - 320))
|
|
@@ -2518,37 +3261,68 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2518
3261
|
}
|
|
2519
3262
|
}
|
|
2520
3263
|
|
|
2521
|
-
return h('div', {
|
|
2522
|
-
|
|
3264
|
+
return h('div', {
|
|
3265
|
+
'data-dsh-piano-keys': '', ref: rootRef, style: rootStyle,
|
|
3266
|
+
onPointerEnter: () => {
|
|
3267
|
+
// 进入锁(hover-guard 120ms):快速路过收拢条零动画零闪动;驻留后照常展开。
|
|
3268
|
+
armCollapseLock(120, 'enter')
|
|
3269
|
+
},
|
|
3270
|
+
onPointerLeave: () => {
|
|
3271
|
+
// 面板随收拢同步消失:root 级离开=真实收拢开始,立即隐藏(淡出 .18s 与收拢
|
|
3272
|
+
// .22-.26s 同步收完)——不走 250ms 宽限+滑出拖尾 ~0.5s。
|
|
3273
|
+
if (detailIdx !== null) {
|
|
3274
|
+
if (detailTimer.current) clearTimeout(detailTimer.current)
|
|
3275
|
+
setDetailIdx(null)
|
|
3276
|
+
}
|
|
3277
|
+
// 锁只在「当前不在锁窗内」时武装:锁窗内的离开(条本就被抑制未展开)不续期,
|
|
3278
|
+
// 否则收拢条边缘快速进出让锁自续(实测 1.4s 不展开),300ms 后照常展开。
|
|
3279
|
+
if (Date.now() >= collapseLockRef.current.until) armCollapseLock()
|
|
3280
|
+
},
|
|
3281
|
+
},
|
|
3282
|
+
h('div', {
|
|
3283
|
+
className: 'pk-head',
|
|
3284
|
+
// 热区链:hover 标题行 ≡ hover 第一个键(CSS 侧高亮已链;JS 侧面板跟随)。
|
|
3285
|
+
// head↔gap↔首键连续同态,消灭「横条出现消失」的状态边界。
|
|
3286
|
+
onMouseEnter: () => showDetail(winOffset),
|
|
3287
|
+
onMouseLeave: hideDetail,
|
|
3288
|
+
},
|
|
2523
3289
|
h('span', { className: 'pk-headLabel' }, STR.pianoTitle),
|
|
2524
3290
|
h('div', { className: 'pk-navGroup' },
|
|
2525
|
-
h('button', { className: 'pk-navBtn', disabled: !canUp, onClick: () =>
|
|
2526
|
-
h('button', { className: 'pk-navBtn', disabled: !canDown, onClick: () =>
|
|
3291
|
+
h('button', { className: 'pk-navBtn', disabled: !canUp, onClick: () => { const n = Math.max(0, winOffset - 1); setScrollPos(n); showDetail(n) }, title: STR.expanded }, '▲'),
|
|
3292
|
+
h('button', { className: 'pk-navBtn', disabled: !canDown, onClick: () => { const n = Math.min(Math.max(0, entries.length - WINDOW), winOffset + 1); setScrollPos(n); showDetail(n) }, title: STR.collapsed }, '▼'),
|
|
2527
3293
|
),
|
|
2528
3294
|
),
|
|
2529
3295
|
h('div', { className: 'pk-strip', ref: stripRef, tabIndex: 0, onKeyDown, onMouseDown: (e) => { if (e.target === e.currentTarget) e.preventDefault() }, 'aria-label': STR.pianoTitle },
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
3296
|
+
// T5 裁剪框(strip 内容盒):缓冲键全裁,可见区恰好 12 行;strip 本体保留
|
|
3297
|
+
// 内边距(视觉呼吸)+ 定位(relative)。
|
|
3298
|
+
h('div', { className: 'pk-clip' },
|
|
3299
|
+
// T5 虚拟滚动列表:transform 由 JS 逐帧驱动(applyShift),不设 style
|
|
3300
|
+
// 以免 React 重渲染覆盖 JS 值。
|
|
3301
|
+
h('div', { className: 'pk-list', ref: listRef },
|
|
3302
|
+
windowed.map((entry, idx) => {
|
|
3303
|
+
const gi = renderStart + idx
|
|
3304
|
+
const act = activeIdx === gi
|
|
3305
|
+
return h('button', {
|
|
3306
|
+
key: 'turn-' + gi + '-' + entry.seq,
|
|
3307
|
+
type: 'button',
|
|
3308
|
+
className: 'pk-key' + (act ? ' pk-keyActive' : '') + (kbGlobal === gi ? ' pk-keyFocus' : ''),
|
|
3309
|
+
// 点击后主动失焦:避免 button 焦点让 :focus-within 常驻展开
|
|
3310
|
+
// (鼠标移开后悬浮条收不拢,箭头「残留」——实测踩坑)。
|
|
3311
|
+
onClick: (e) => { open(entry); e.currentTarget.blur() },
|
|
3312
|
+
onMouseEnter: () => showDetail(gi),
|
|
3313
|
+
onMouseLeave: hideDetail,
|
|
3314
|
+
onContextMenu: (e) => {
|
|
3315
|
+
e.preventDefault()
|
|
3316
|
+
if (onOpenWorkbench) onOpenWorkbench()
|
|
3317
|
+
},
|
|
2545
3318
|
},
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
)
|
|
2551
|
-
|
|
3319
|
+
// 轮次序号:展开态常驻【最左侧】独立模块(与文本模块分离,互不侵占)。
|
|
3320
|
+
h('span', { className: 'pk-turnNo' }, 'T' + entry.turn),
|
|
3321
|
+
h('span', { className: 'pk-label' }, entry.preview || '(空)'),
|
|
3322
|
+
)
|
|
3323
|
+
}),
|
|
3324
|
+
),
|
|
3325
|
+
),
|
|
2552
3326
|
),
|
|
2553
3327
|
// 信息面板:跟随 hover 条目右侧展开(fixed,位置由 detailPos 控制);
|
|
2554
3328
|
// 常驻渲染 + hidden 类做滑入/滑出;内容 key 变化触发 pkSlideIn。
|
|
@@ -2569,27 +3343,171 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2569
3343
|
function DetailsDockView(props) {
|
|
2570
3344
|
const { sessionId, connection, onExit } = props
|
|
2571
3345
|
const [state, setState] = useState({ phase: 'loading', error: null, data: null })
|
|
2572
|
-
|
|
3346
|
+
// T8-B:重挂载续播时折叠集合按钉住时间线播种(新回合照常并入折叠)。
|
|
3347
|
+
const [collapsed, setCollapsed] = useState(() => {
|
|
3348
|
+
const s = new Set()
|
|
3349
|
+
if (dockBridge.live && dockBridge.live.on && dockBridge.live.lastState) {
|
|
3350
|
+
for (const t of dockBridge.live.lastState.timeline || []) s.add('turn-' + t.turn)
|
|
3351
|
+
}
|
|
3352
|
+
return s
|
|
3353
|
+
})
|
|
2573
3354
|
const collapsedInit = useRef(false)
|
|
2574
3355
|
const [turnItems, setTurnItems] = useState(() => new Map())
|
|
2575
3356
|
const loadingTurns = useRef(new Set())
|
|
2576
3357
|
|
|
2577
|
-
|
|
2578
|
-
|
|
3358
|
+
// ── T6 dock 实时模式:复用实时通道(sessions.history → derive,与详情页 M6
|
|
3359
|
+
// 同构),但只显示「开播后」的对话——首轮记录基准回合集合,此后仅显示新出现
|
|
3360
|
+
// 的回合(含开播时正在运行的回合);不加载历史回合(档案 get 在实时期间跳过)。
|
|
3361
|
+
// ── T8-B 钉住原会话:dock 视图随会话切换整体重挂载(槽位 session 作用域
|
|
3362
|
+
// key),组件级实时状态必丢——实时状态提升到 dockBridge.live 模块级存储
|
|
3363
|
+
// (钉住 sid/基准/已见/定时器/最近状态跨挂载存活):开播即钉住当时的会话,
|
|
3364
|
+
// GUI 切走后右栏继续直播原会话并显示提示条(告知用户),可一键转播当前会话。
|
|
3365
|
+
const liveStore = dockBridge.live || (dockBridge.live = {
|
|
3366
|
+
on: false, sid: undefined, timer: null, base: null, seen: new Set(),
|
|
3367
|
+
lastState: null, conn: undefined, emit: null, tick: null,
|
|
3368
|
+
})
|
|
3369
|
+
const [liveOn, setLiveOn] = useState(() => liveStore.on)
|
|
3370
|
+
const [liveState, setLiveState] = useState(() => liveStore.lastState
|
|
3371
|
+
|| { phase: 'idle', error: null, timeline: [], running: false, eventCount: 0, health: null })
|
|
3372
|
+
const fetchLive = useRef(async () => {})
|
|
3373
|
+
fetchLive.current = async () => {
|
|
3374
|
+
const sid = liveStore.sid
|
|
3375
|
+
const conn = liveStore.conn
|
|
3376
|
+
if (sid === undefined || conn === undefined || conn.api === undefined || conn.api.sessions === undefined) {
|
|
3377
|
+
liveStore.lastState = { phase: 'error', error: STR.liveUnavailable, timeline: [], running: false, eventCount: 0, health: null }
|
|
3378
|
+
if (liveStore.emit !== null) liveStore.emit(liveStore.lastState)
|
|
3379
|
+
return
|
|
3380
|
+
}
|
|
3381
|
+
if (liveStore.emit !== null) liveStore.emit({ ...liveStateKeep(), phase: 'loading' })
|
|
3382
|
+
try {
|
|
3383
|
+
const resp = await conn.api.sessions.history({ sessionId: sid, mode: 'full', maxMessages: 400 })
|
|
3384
|
+
const val = resp && resp.result && resp.result.value
|
|
3385
|
+
if (val === undefined || !Array.isArray(val.events)) {
|
|
3386
|
+
liveStore.lastState = { ...liveStateKeep(), phase: 'error', error: STR.liveFail }
|
|
3387
|
+
if (liveStore.emit !== null) liveStore.emit(liveStore.lastState)
|
|
3388
|
+
return
|
|
3389
|
+
}
|
|
3390
|
+
const events = val.events.map((entry) => (entry && entry.event) || entry)
|
|
3391
|
+
const derived = await api('derive', { events, now: Date.now() })
|
|
3392
|
+
if (!derived || !derived.ok) {
|
|
3393
|
+
liveStore.lastState = { ...liveStateKeep(), phase: 'error', error: (derived && derived.error) || STR.liveFail }
|
|
3394
|
+
if (liveStore.emit !== null) liveStore.emit(liveStore.lastState)
|
|
3395
|
+
return
|
|
3396
|
+
}
|
|
3397
|
+
const tl = (derived.timeline && derived.timeline.turns) || []
|
|
3398
|
+
const running = derived.running === true
|
|
3399
|
+
if (liveStore.base === null) {
|
|
3400
|
+
// 开播基准:当时已存在的全部回合;运行中的那个除外(它是「正在发生的
|
|
3401
|
+
// 实时对话」,从开播起就要展示其后续流式变化)。
|
|
3402
|
+
liveStore.base = new Set(tl.map((t) => t.turn))
|
|
3403
|
+
if (running && tl.length > 0) liveStore.base.delete(tl[tl.length - 1].turn)
|
|
3404
|
+
}
|
|
3405
|
+
// 只保留:开播基准之后新出现的回合 + 开播时正在运行的那个回合。
|
|
3406
|
+
const display = tl.filter((t) => !liveStore.base.has(t.turn))
|
|
3407
|
+
const newTurns = display.filter((t) => !liveStore.seen.has(t.turn))
|
|
3408
|
+
for (const t of display) liveStore.seen.add(t.turn)
|
|
3409
|
+
if (newTurns.length > 0 && liveStore.emit !== null) {
|
|
3410
|
+
setCollapsed((prev) => {
|
|
3411
|
+
const next = new Set(prev)
|
|
3412
|
+
for (const t of newTurns) next.add('turn-' + t.turn)
|
|
3413
|
+
return next
|
|
3414
|
+
})
|
|
3415
|
+
}
|
|
3416
|
+
liveStore.lastState = { phase: 'ready', error: null, timeline: display, running, eventCount: events.length, health: derived.health || null }
|
|
3417
|
+
if (liveStore.emit !== null) liveStore.emit(liveStore.lastState)
|
|
3418
|
+
} catch (e) {
|
|
3419
|
+
liveStore.lastState = { ...liveStateKeep(), phase: 'error', error: STR.liveFail + ': ' + String(e && e.message || e) }
|
|
3420
|
+
if (liveStore.emit !== null) liveStore.emit(liveStore.lastState)
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
// 保留当前显示数据、只换 phase 的辅助(emit 前构造)。
|
|
3424
|
+
function liveStateKeep() {
|
|
3425
|
+
return liveStore.lastState || { phase: 'idle', error: null, timeline: [], running: false, eventCount: 0, health: null }
|
|
3426
|
+
}
|
|
3427
|
+
const enterLive = () => {
|
|
3428
|
+
liveStore.on = true
|
|
3429
|
+
liveStore.sid = sessionId // T8-B:开播即钉住当前会话
|
|
3430
|
+
liveStore.seen.clear()
|
|
3431
|
+
liveStore.base = null
|
|
3432
|
+
liveStore.lastState = null
|
|
3433
|
+
setLiveOn(true)
|
|
3434
|
+
liveFollowRef.current = true // T7:开播默认吸底
|
|
3435
|
+
fetchLive.current()
|
|
3436
|
+
if (liveStore.timer === null) {
|
|
3437
|
+
liveStore.timer = setInterval(() => { if (liveStore.tick !== null) liveStore.tick() }, sfSettings.current.livePollMs)
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
const exitLive = () => {
|
|
3441
|
+
liveStore.on = false
|
|
3442
|
+
liveStore.sid = undefined
|
|
3443
|
+
liveStore.seen.clear()
|
|
3444
|
+
liveStore.base = null
|
|
3445
|
+
liveStore.lastState = null
|
|
3446
|
+
if (liveStore.timer !== null) { clearInterval(liveStore.timer); liveStore.timer = null }
|
|
3447
|
+
setLiveOn(false)
|
|
3448
|
+
setLiveState({ phase: 'idle', error: null, timeline: [], running: false, eventCount: 0, health: null })
|
|
3449
|
+
// 回到档案视图:重新加载(实时期间可能已产生新回合)。
|
|
2579
3450
|
setState({ phase: 'loading', error: null, data: null })
|
|
2580
3451
|
setTurnItems(new Map())
|
|
2581
3452
|
collapsedInit.current = false
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
3453
|
+
}
|
|
3454
|
+
// T8-B:转播当前会话(提示条按钮)——重钉到当前 sessionId、基准重算。
|
|
3455
|
+
const rebindLive = () => {
|
|
3456
|
+
liveStore.sid = sessionId
|
|
3457
|
+
liveStore.seen.clear()
|
|
3458
|
+
liveStore.base = null
|
|
3459
|
+
fetchLive.current()
|
|
3460
|
+
}
|
|
3461
|
+
// 挂载注册:连接对象/状态发射器/定时器分发指向当前实例(跨重挂载续播);
|
|
3462
|
+
// 卸载时若 3s 内无新实例接管(dock 真正关闭走 onExit→exitLive),停表清理。
|
|
3463
|
+
useEffect(() => {
|
|
3464
|
+
liveStore.conn = props.connection
|
|
3465
|
+
liveStore.emit = setLiveState
|
|
3466
|
+
liveStore.tick = () => { fetchLive.current() }
|
|
3467
|
+
return () => {
|
|
3468
|
+
if (liveStore.emit === setLiveState) liveStore.emit = null
|
|
3469
|
+
if (liveStore.tick !== null) {
|
|
3470
|
+
const t = liveStore.tick
|
|
3471
|
+
setTimeout(() => { if (liveStore.tick === t && liveStore.emit === null) {
|
|
3472
|
+
if (liveStore.timer !== null) { clearInterval(liveStore.timer); liveStore.timer = null }
|
|
3473
|
+
liveStore.on = false; liveStore.tick = null
|
|
3474
|
+
} }, 500)
|
|
2589
3475
|
}
|
|
2590
|
-
}
|
|
3476
|
+
}
|
|
3477
|
+
}, [])
|
|
3478
|
+
// T7 实时吸底跟随(dock):dk-body 距底 ≤40px 时每次刷新平滑滚到新底部。
|
|
3479
|
+
const dkBodyRef = useRef(null)
|
|
3480
|
+
const liveFollowRef = useRef(true)
|
|
3481
|
+
const onDkBodyScroll = () => {
|
|
3482
|
+
const el = dkBodyRef.current
|
|
3483
|
+
if (el === null) return
|
|
3484
|
+
liveFollowRef.current = el.scrollHeight - el.scrollTop - el.clientHeight <= sfSettings.current.liveFollowPx
|
|
3485
|
+
}
|
|
3486
|
+
useEffect(() => {
|
|
3487
|
+
if (!liveOn || liveState.timeline === null || liveState.timeline.length === 0) return
|
|
3488
|
+
const el = dkBodyRef.current
|
|
3489
|
+
if (el !== null && liveFollowRef.current) el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' })
|
|
3490
|
+
}, [liveState.timeline, liveOn])
|
|
3491
|
+
|
|
3492
|
+
useEffect(() => {
|
|
3493
|
+
let alive = true
|
|
3494
|
+
// T8-B:实时钉住在 liveStore.sid(会话切换不重绑);档案加载仅非实时时进行。
|
|
3495
|
+
if (!liveOn) {
|
|
3496
|
+
setState({ phase: 'loading', error: null, data: null })
|
|
3497
|
+
setTurnItems(new Map())
|
|
3498
|
+
collapsedInit.current = false
|
|
3499
|
+
if (!sessionId) { setState({ phase: 'empty', error: null, data: null }); return }
|
|
3500
|
+
api('get', { sessionId }).then((json) => {
|
|
3501
|
+
if (!alive) return
|
|
3502
|
+
if (json && json.ok) {
|
|
3503
|
+
setState({ phase: 'ready', error: null, data: json })
|
|
3504
|
+
} else {
|
|
3505
|
+
setState({ phase: 'error', error: (json && json.error) || 'get failed', data: null })
|
|
3506
|
+
}
|
|
3507
|
+
}).catch((e) => { if (alive) setState({ phase: 'error', error: String(e), data: null }) })
|
|
3508
|
+
}
|
|
2591
3509
|
return () => { alive = false }
|
|
2592
|
-
}, [sessionId])
|
|
3510
|
+
}, [sessionId, liveOn])
|
|
2593
3511
|
|
|
2594
3512
|
useEffect(() => {
|
|
2595
3513
|
if (state.phase !== 'ready' || collapsedInit.current) return
|
|
@@ -2643,14 +3561,40 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2643
3561
|
|
|
2644
3562
|
return h('div', { className: 'dk-root' },
|
|
2645
3563
|
h('div', { className: 'dk-head' },
|
|
2646
|
-
|
|
3564
|
+
// T6:标题「会话流」+ 按钮**紧跟标题**(左侧区);spacer 吃掉右侧余量——
|
|
3565
|
+
// 彻底避开右上角 dsh-better-sidebar toggleCluster(视口级常驻浮钮区)。
|
|
3566
|
+
h('span', { className: 'dk-title', title: STR.dockHint }, STR.entry),
|
|
3567
|
+
props.connection !== undefined && h('button', {
|
|
3568
|
+
className: 'dk-btn' + (liveOn ? ' dk-btnLive' : ''),
|
|
3569
|
+
onClick: liveOn ? exitLive : enterLive,
|
|
3570
|
+
title: STR.dockLiveHint,
|
|
3571
|
+
}, liveOn ? STR.liveActive + ' ✕' : STR.live),
|
|
2647
3572
|
h('button', { className: 'dk-btn', onClick: onExit }, STR.dockExit),
|
|
3573
|
+
h('span', { className: 'dk-headSpacer' }),
|
|
2648
3574
|
),
|
|
2649
|
-
h('div', { className: 'dk-body' },
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
3575
|
+
h('div', { className: 'dk-body', ref: dkBodyRef, onScroll: onDkBodyScroll },
|
|
3576
|
+
liveOn && h('div', { className: 'sf-liveBar', style: { flex: 'none' } },
|
|
3577
|
+
h('span', { className: 'sf-badge sf-badgeRun', style: { flex: 'none' } }, STR.liveActive),
|
|
3578
|
+
healthBadge(liveState.health),
|
|
3579
|
+
h('span', { className: 'sf-liveText' },
|
|
3580
|
+
h('span', { className: 'sf-muted', style: { flex: 'none' } }, (liveState.eventCount || 0) + ' ' + STR.liveEvents),
|
|
3581
|
+
liveState.phase === 'loading' && h('span', { className: 'sf-muted', style: { flex: 'none' } }, STR.liveRefreshing),
|
|
3582
|
+
),
|
|
3583
|
+
),
|
|
3584
|
+
liveOn && liveState.phase === 'error' && h('div', { className: 'sf-hint', style: { color: '#d43b3b' } }, liveState.error),
|
|
3585
|
+
liveOn && liveStore.sid !== undefined && liveStore.sid !== sessionId && h('div', { className: 'dk-pinBar', title: liveStore.sid },
|
|
3586
|
+
h('span', { className: 'dk-pinText' }, STR.dockPinNotice),
|
|
3587
|
+
h('button', { className: 'dk-btn', onClick: rebindLive }, STR.dockPinRebind),
|
|
3588
|
+
),
|
|
3589
|
+
liveOn && liveState.phase !== 'error' && liveState.timeline.length === 0 && h('div', { className: 'sf-hint' }, STR.dockLiveEmpty),
|
|
3590
|
+
liveOn && liveState.timeline.length > 0 && h(TimelineTurns, {
|
|
3591
|
+
turns: liveState.timeline, collapsed, toggle, liveActive: liveState.running === true,
|
|
3592
|
+
activeTurn: null, setActiveTurn: () => {},
|
|
3593
|
+
}),
|
|
3594
|
+
!liveOn && state.phase === 'loading' && h('div', { className: 'sf-hint' }, STR.detailLoading),
|
|
3595
|
+
!liveOn && state.phase === 'empty' && h('div', { className: 'sf-hint' }, STR.dockEmpty),
|
|
3596
|
+
!liveOn && state.phase === 'error' && h('div', { className: 'sf-hint', style: { color: '#d43b3b' } }, STR.detailFailed + ': ' + String(state.error)),
|
|
3597
|
+
!liveOn && state.phase === 'ready' && h('div', { className: 'dk-summary' },
|
|
2654
3598
|
ruleTaskCount > 0 && h('div', { className: 'dk-summaryRow' },
|
|
2655
3599
|
h('span', { className: 'dk-summaryTag' }, STR.summaryTaskCount),
|
|
2656
3600
|
h('span', { className: 'dk-summaryText' }, ruleTaskCount),
|
|
@@ -2669,8 +3613,8 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2669
3613
|
),
|
|
2670
3614
|
data.summary && h('div', { className: 'dk-summaryFull' }, data.summary),
|
|
2671
3615
|
),
|
|
2672
|
-
state.phase === 'ready' && lightTurns.length === 0 && h('div', { className: 'sf-hint' }, STR.noTimeline),
|
|
2673
|
-
state.phase === 'ready' && h(TurnList, {
|
|
3616
|
+
!liveOn && state.phase === 'ready' && lightTurns.length === 0 && h('div', { className: 'sf-hint' }, STR.noTimeline),
|
|
3617
|
+
!liveOn && state.phase === 'ready' && h(TurnList, {
|
|
2674
3618
|
sessionId,
|
|
2675
3619
|
lightTurns,
|
|
2676
3620
|
collapsed,
|
|
@@ -2888,7 +3832,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2888
3832
|
if (!document.getElementById('dsh-session-flow-style')) {
|
|
2889
3833
|
const style = document.createElement('style')
|
|
2890
3834
|
style.id = 'dsh-session-flow-style'
|
|
2891
|
-
style.textContent = STYLE
|
|
3835
|
+
style.textContent = STYLE + '\n' + SETTINGS_STYLE
|
|
2892
3836
|
document.head.appendChild(style)
|
|
2893
3837
|
}
|
|
2894
3838
|
|
|
@@ -2947,6 +3891,48 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2947
3891
|
} catch (error) {
|
|
2948
3892
|
console.error('[dsh-session-flow] conversation.view registration failed:', error)
|
|
2949
3893
|
}
|
|
3894
|
+
// 会话健康芯片:官方 conversation.session.header.actions 槽位(ui-jobs/ui-subagent 同款),
|
|
3895
|
+
// order -9 紧随官方模式标识(agent-preset label, order -10)右侧。
|
|
3896
|
+
// 点击芯片 → workbenchBridge 意图桥直达该会话详情。
|
|
3897
|
+
try {
|
|
3898
|
+
slots.inject('conversation.session.header.actions', () => slots.register({
|
|
3899
|
+
name: 'conversation.session.header.actions',
|
|
3900
|
+
id: 'session-flow-health',
|
|
3901
|
+
order: -9,
|
|
3902
|
+
}, (slotProps) => h(SessionHealthChip, {
|
|
3903
|
+
sessionId: slotProps ? slotProps.sessionId : undefined,
|
|
3904
|
+
sessions,
|
|
3905
|
+
connection,
|
|
3906
|
+
onOpen: () => {
|
|
3907
|
+
const sid = slotProps && slotProps.sessionId
|
|
3908
|
+
if (sid) workbenchBridge.open(sid)
|
|
3909
|
+
controller.open()
|
|
3910
|
+
},
|
|
3911
|
+
})))
|
|
3912
|
+
} catch (error) {
|
|
3913
|
+
console.error('[dsh-session-flow] header health chip registration failed:', error)
|
|
3914
|
+
}
|
|
3915
|
+
// 插件设置页:settings.section 顶级 tab(order 140,排在皮肤中心 120/宠物 130 之后)。
|
|
3916
|
+
// 读写经 settingsScope(pet 同款:优先 webUiSettings 桥,回退 settingsScope);
|
|
3917
|
+
// 两者都不可用(旧宿主/未暴露)时不注册 tab。controller 闭包持有,
|
|
3918
|
+
// 卸载经 slots.inject 返回的 disposer 释放 scope 订阅。
|
|
3919
|
+
try {
|
|
3920
|
+
const settingsBinder = ctx.get('webUiSettings') ?? ctx.get('settingsScope')
|
|
3921
|
+
if (settingsBinder !== undefined && settingsBinder !== null && typeof settingsBinder.bind === 'function') {
|
|
3922
|
+
const sfSettingsCtrl = new SfSettingsController(settingsBinder.bind({ namespace: 'session-flow' }))
|
|
3923
|
+
slots.inject('settings.section', () => {
|
|
3924
|
+
const unregister = slots.register({
|
|
3925
|
+
name: 'settings.section',
|
|
3926
|
+
id: 'session-flow',
|
|
3927
|
+
order: 140,
|
|
3928
|
+
label: () => STR.settingsTitle,
|
|
3929
|
+
}, () => h(SessionFlowSettingsSection, { controller: sfSettingsCtrl }))
|
|
3930
|
+
return () => { sfSettingsCtrl.dispose(); unregister() }
|
|
3931
|
+
})
|
|
3932
|
+
}
|
|
3933
|
+
} catch (error) {
|
|
3934
|
+
console.error('[dsh-session-flow] settings.section registration failed:', error)
|
|
3935
|
+
}
|
|
2950
3936
|
}
|
|
2951
3937
|
|
|
2952
3938
|
// M12:详情并入右侧栏 —— details 槽位动态占用(priority -1 低于官方 tool-details)。
|
|
@@ -2965,66 +3951,10 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
2965
3951
|
} catch (e) {}
|
|
2966
3952
|
}
|
|
2967
3953
|
|
|
2968
|
-
// M12×aionui
|
|
2969
|
-
//
|
|
2970
|
-
//
|
|
2971
|
-
//
|
|
2972
|
-
// dock 激活期间按实际轨宽重算把手位置:三方写 frame style → MutationObserver;
|
|
2973
|
-
// 纯缩放(grid 字符串不变)→ ResizeObserver。3 轨时公式退化为官方原值,无副作用。
|
|
2974
|
-
const parseGridTracksCompat = (input) => {
|
|
2975
|
-
const tracks = []
|
|
2976
|
-
let depth = 0
|
|
2977
|
-
let current = ''
|
|
2978
|
-
for (const ch of String(input)) {
|
|
2979
|
-
if (ch === '(') depth += 1
|
|
2980
|
-
if (ch === ')') depth = Math.max(0, depth - 1)
|
|
2981
|
-
if (ch === ' ' && depth === 0) {
|
|
2982
|
-
if (current !== '') { tracks.push(current); current = '' }
|
|
2983
|
-
continue
|
|
2984
|
-
}
|
|
2985
|
-
current += ch
|
|
2986
|
-
}
|
|
2987
|
-
if (current !== '') tracks.push(current)
|
|
2988
|
-
return tracks
|
|
2989
|
-
}
|
|
2990
|
-
const detailsHandleCompat = {
|
|
2991
|
-
frame: null,
|
|
2992
|
-
observer: null,
|
|
2993
|
-
sizeObserver: null,
|
|
2994
|
-
start() {
|
|
2995
|
-
if (this.observer !== null) return
|
|
2996
|
-
const frame = document.querySelector('[data-dsh-frame]') ||
|
|
2997
|
-
(document.querySelector('[class*="sidebarCol"]') || {}).parentElement || null
|
|
2998
|
-
if (frame === null) return
|
|
2999
|
-
this.frame = frame
|
|
3000
|
-
this.observer = new MutationObserver(() => this.apply())
|
|
3001
|
-
this.observer.observe(frame, { attributes: true, attributeFilter: ['style'] })
|
|
3002
|
-
this.sizeObserver = new ResizeObserver(() => this.apply())
|
|
3003
|
-
this.sizeObserver.observe(frame)
|
|
3004
|
-
this.apply()
|
|
3005
|
-
},
|
|
3006
|
-
stop() {
|
|
3007
|
-
if (this.observer !== null) { this.observer.disconnect(); this.observer = null }
|
|
3008
|
-
if (this.sizeObserver !== null) { this.sizeObserver.disconnect(); this.sizeObserver = null }
|
|
3009
|
-
this.frame = null
|
|
3010
|
-
},
|
|
3011
|
-
apply() {
|
|
3012
|
-
const frame = this.frame
|
|
3013
|
-
if (frame === null) return
|
|
3014
|
-
const tracks = parseGridTracksCompat(frame.style.gridTemplateColumns)
|
|
3015
|
-
if (tracks.length < 3) return
|
|
3016
|
-
const trackPx = (t) => {
|
|
3017
|
-
const m = /^(-?[\d.]+)px$/.exec(String(t).trim())
|
|
3018
|
-
return m === null ? 0 : Number(m[1])
|
|
3019
|
-
}
|
|
3020
|
-
const details = trackPx(tracks[2])
|
|
3021
|
-
const preview = tracks.length >= 4 ? trackPx(tracks[3]) : 0
|
|
3022
|
-
const explorer = tracks.length >= 5 ? trackPx(tracks[4]) : 0
|
|
3023
|
-
const handle = frame.querySelector('[data-side="details"]')
|
|
3024
|
-
if (handle === null) return
|
|
3025
|
-
handle.style.left = `${Math.round(frame.getBoundingClientRect().width - details - preview - explorer)}px`
|
|
3026
|
-
},
|
|
3027
|
-
}
|
|
3954
|
+
// M12×aionui:官方 details 把手在 5 轨 grid 下的位置补偿已由上游
|
|
3955
|
+
// aionui-panel ≥0.2.0 接管(dsh-web-ui PR #311,applyGrid 每帧重算;
|
|
3956
|
+
// 3 轨时退化为官方原值)。本插件过渡兼容层 detailsHandleCompat 已移除
|
|
3957
|
+
// (2026-08-18,本机 aionui 0.2.0 实证含修复;旧版 aionui 请升级)。
|
|
3028
3958
|
const ensureColumn = () => {
|
|
3029
3959
|
if (dockBridge.open && layout !== undefined && typeof layout.openDetails === 'function') {
|
|
3030
3960
|
try { layout.openDetails() } catch (e) {}
|
|
@@ -3066,7 +3996,6 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
3066
3996
|
if (!dockBridge.open) {
|
|
3067
3997
|
dockBridge.open = true
|
|
3068
3998
|
syncDetails()
|
|
3069
|
-
detailsHandleCompat.start()
|
|
3070
3999
|
// 会话切换时官方会自动 closeDetails —— 高频守护快速重开(无动画,内容直接刷新)。
|
|
3071
4000
|
if (detailsGuard === null) {
|
|
3072
4001
|
detailsGuard = setInterval(() => { ensureColumn() }, 150)
|
|
@@ -3078,7 +4007,6 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
3078
4007
|
if (dockBridge.open) {
|
|
3079
4008
|
dockBridge.open = false
|
|
3080
4009
|
syncDetails()
|
|
3081
|
-
detailsHandleCompat.stop()
|
|
3082
4010
|
if (detailsGuard !== null) { clearInterval(detailsGuard); detailsGuard = null }
|
|
3083
4011
|
for (const fn of dockBridge.listeners) fn()
|
|
3084
4012
|
}
|
|
@@ -3132,7 +4060,6 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
3132
4060
|
}
|
|
3133
4061
|
if (detailsGuard !== null) { clearInterval(detailsGuard); detailsGuard = null }
|
|
3134
4062
|
setDockActive(false)
|
|
3135
|
-
detailsHandleCompat.stop()
|
|
3136
4063
|
if (layout !== undefined && typeof layout.closeDetails === 'function') {
|
|
3137
4064
|
try { layout.closeDetails() } catch (e) {}
|
|
3138
4065
|
}
|