dsh-session-flow 0.1.0 → 1.0.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 +46 -99
- package/README.md +44 -97
- package/assets/screenshots/dock-right.png +0 -0
- package/assets/screenshots/folded-detail.png +0 -0
- package/assets/screenshots/fulltext-search.png +0 -0
- package/assets/screenshots/overview.png +0 -0
- package/assets/screenshots/session-flow-tab.png +0 -0
- package/assets/screenshots/turn-rail.png +0 -0
- package/lib/archive.js +40 -2
- package/lib/client.js +1256 -13
- package/lib/host.js +144 -3
- package/lib/index-store.js +1 -1
- package/lib/renames.js +80 -0
- package/package.json +4 -3
package/lib/client.js
CHANGED
|
@@ -36,6 +36,11 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
36
36
|
|
|
37
37
|
const STR = LOCALE === 'zh' ? {
|
|
38
38
|
entry: '会话流', title: '会话流', rescan: '重新扫描', scanning: '扫描中…',
|
|
39
|
+
pianoTitle: '轮次', pianoNew: '新建会话', pianoFav: '收藏', pianoUnfav: '取消收藏', pianoFavs: '收藏', pianoOpenInFlow: '在会话流中打开',
|
|
40
|
+
pianoJumpFail: '该轮次尚未加载:请在会话区向上滚动加载历史后再点击',
|
|
41
|
+
fullText: '全文检索', fullTextSearching: '搜索中…', fullTextPartial: '已扫描 {N}/{M} 个会话(结果未完整)', fullTextNone: '全文无命中', fullTextFail: '全文检索失败',
|
|
42
|
+
dock: '并入右栏', dockExit: '退出并入', dockHint: '会话流已并入右侧栏 · 随当前会话切换', dockEmpty: '打开会话后可在此查看会话流',
|
|
43
|
+
rename: '重命名', renamePlaceholder: '输入新标题…', renameFail: '重命名失败', origTitle: '原名',
|
|
39
44
|
search: '搜索标题/工具/文件… 支持 tool: pwsh · file: src · err:', allWorkspaces: '全部工作区',
|
|
40
45
|
sortRecent: '最近运行', sortNewest: '最近创建', sortOldest: '最早创建', sortTools: '工具最多', sortLongest: '耗时最长',
|
|
41
46
|
noMatch: '没有匹配的会话', noData: '暂无会话数据',
|
|
@@ -44,10 +49,11 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
44
49
|
duration: '耗时', created: '创建', size: '大小', records: '记录',
|
|
45
50
|
jumpFailed: '无法跳转(会话不在当前列表)', unknownTitle: '(未命名会话)',
|
|
46
51
|
loadFailed: '加载失败',
|
|
47
|
-
back: '返回', close: '关闭', jumpNative: '跳转原生会话', expandAll: '全部展开', collapseAll: '全部折叠',
|
|
52
|
+
back: '返回', openWorkbench: '打开完整工作台', close: '关闭', jumpNative: '跳转原生会话', expandAll: '全部展开', collapseAll: '全部折叠',
|
|
48
53
|
exportMd: '导出报告 (ZIP)', exporting: '导出中…', exportFail: '导出失败',
|
|
49
54
|
live: '实时', liveActive: '实时 · 进行中', liveExit: '退出实时', liveLoading: '实时加载…',
|
|
50
55
|
liveFail: '实时不可用', liveUnavailable: '(实时通道不可用:连接不可用)',
|
|
56
|
+
liveFallbackHint: '会话尚未落盘 · 实时视图(3s 轮询)', retryArchive: '重试档案视图',
|
|
51
57
|
liveEvents: '事件', liveRefreshing: '刷新…', liveLastActive: '最后活动',
|
|
52
58
|
liveTurn: '运行中', liveGenerating: '正在生成…',
|
|
53
59
|
detailLoading: '正在解析会话…', noTimeline: '无可展示的时间线', artifacts: '产物',
|
|
@@ -55,8 +61,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
55
61
|
args: '参数', result: '结果', thinking: '思考', expanded: '展开', collapsed: '收起',
|
|
56
62
|
detailFailed: '会话详情加载失败', showEmpty: '显示空会话', emptySession: '空会话',
|
|
57
63
|
emptyHint: '此会话已创建但尚未开始对话', userNav: '用户发言', noUserNav: '(无用户发言)',
|
|
58
|
-
conclusion: '结论', injected: '注入', lineage: '血缘', noLineage: '(无子代理血缘信息)',
|
|
59
|
-
liveTree: '运行时血缘', offlineTree: '档案血缘', modeOneShot: '一次性', modeContinuable: '可续',
|
|
64
|
+
conclusion: '结论', injected: '注入', lineage: '血缘', noLineage: '(无子代理血缘信息)', task: '任务', liveTree: '运行时血缘', offlineTree: '档案血缘', modeOneShot: '一次性', modeContinuable: '可续',
|
|
60
65
|
subagentDetail: '子代理详情', loadDetail: '加载子代理详情…', noDetail: '该子代理无可见事件',
|
|
61
66
|
viewSubagent: '查看子代理', toolTop: '工具 Top', issues: '问题会话', issuesHint: '只显示有错误记录的会话',
|
|
62
67
|
searchTab: '检索', searchInPlaceholder: '会话内检索…', noMatches: '无匹配位置', matches: '命中', navUsers: '用户',
|
|
@@ -69,6 +74,11 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
69
74
|
cacheHint: '仅清理本插件的缓存(可自动重建),不影响 DSH 会话存档与任何其他数据', cacheLimit: '时间线缓存上限',
|
|
70
75
|
} : {
|
|
71
76
|
entry: 'Session Flow', title: 'Session Flow', rescan: 'Rescan', scanning: 'Scanning…',
|
|
77
|
+
pianoTitle: 'Turns', pianoNew: 'New session', pianoFav: 'Favorite', pianoUnfav: 'Unfavorite', pianoFavs: 'Favorites', pianoOpenInFlow: 'Open in Session Flow',
|
|
78
|
+
pianoJumpFail: 'Turn not loaded — scroll up in the conversation to load history, then retry',
|
|
79
|
+
fullText: 'Full-text', fullTextSearching: 'Searching…', fullTextPartial: 'Scanned {N}/{M} sessions (partial)', fullTextNone: 'No full-text matches', fullTextFail: 'Full-text search failed',
|
|
80
|
+
dock: 'Dock right', dockExit: 'Undock', dockHint: 'Session Flow docked · follows current session', dockEmpty: 'Open a session to view its flow here',
|
|
81
|
+
rename: 'Rename', renamePlaceholder: 'New title…', renameFail: 'Rename failed', origTitle: 'Original',
|
|
72
82
|
search: 'Search title / tools / files… use tool: · file: · err:', allWorkspaces: 'All workspaces',
|
|
73
83
|
sortRecent: 'Recently run', sortNewest: 'Recently created', sortOldest: 'Oldest first', sortTools: 'Most tools', sortLongest: 'Longest',
|
|
74
84
|
noMatch: 'No matching sessions', noData: 'No session data yet',
|
|
@@ -77,10 +87,11 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
77
87
|
duration: 'duration', created: 'created', size: 'size', records: 'records',
|
|
78
88
|
jumpFailed: 'Cannot open (session not in live list)', unknownTitle: '(untitled session)',
|
|
79
89
|
loadFailed: 'Load failed',
|
|
80
|
-
back: 'Back', close: 'Close', jumpNative: 'Open in native view', expandAll: 'Expand all', collapseAll: 'Collapse all',
|
|
90
|
+
back: 'Back', openWorkbench: 'Open workbench', close: 'Close', jumpNative: 'Open in native view', expandAll: 'Expand all', collapseAll: 'Collapse all',
|
|
81
91
|
exportMd: 'Export report (ZIP)', exporting: 'Exporting…', exportFail: 'Export failed',
|
|
82
92
|
live: 'Live', liveActive: 'Live · running', liveExit: 'Exit live', liveLoading: 'Loading live…',
|
|
83
93
|
liveFail: 'Live unavailable', liveUnavailable: '(live channel unavailable: no connection)',
|
|
94
|
+
liveFallbackHint: 'Session not persisted yet · live view (3s poll)', retryArchive: 'Retry archive view',
|
|
84
95
|
liveEvents: 'events', liveRefreshing: 'refreshing…', liveLastActive: 'last active',
|
|
85
96
|
liveTurn: 'Running', liveGenerating: 'Generating…',
|
|
86
97
|
detailLoading: 'Parsing session…', noTimeline: 'No timeline to show', artifacts: 'Artifacts',
|
|
@@ -88,7 +99,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
88
99
|
args: 'Args', result: 'Result', thinking: 'Thinking', expanded: 'Expanded', collapsed: 'Collapsed',
|
|
89
100
|
detailFailed: 'Failed to load session detail', showEmpty: 'Show empty sessions', emptySession: 'Empty',
|
|
90
101
|
emptyHint: 'This session was created but has no conversation yet', userNav: 'User turns', noUserNav: '(no user turns)',
|
|
91
|
-
conclusion: 'Conclusion', injected: 'Injected', lineage: 'Lineage', noLineage: '(no subagent lineage)',
|
|
102
|
+
conclusion: 'Conclusion', injected: 'Injected', lineage: 'Lineage', noLineage: '(no subagent lineage)', task: 'Task',
|
|
92
103
|
liveTree: 'Live lineage', offlineTree: 'Archive lineage', modeOneShot: 'one-shot', modeContinuable: 'continuable',
|
|
93
104
|
subagentDetail: 'Subagent detail', loadDetail: 'Loading subagent detail…', noDetail: 'No visible events for this subagent',
|
|
94
105
|
viewSubagent: 'View subagent', toolTop: 'Tool Top', issues: 'Problem sessions', issuesHint: 'Only sessions with errors',
|
|
@@ -144,6 +155,19 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
144
155
|
`.sf-card:hover{border-color:var(--dsw-specific-accent,#4a7dff);box-shadow:0 1px 4px rgba(0,0,0,.06)}`,
|
|
145
156
|
`.sf-cardTitle{font-weight:600;margin-bottom:4px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}`,
|
|
146
157
|
`.sf-cardText{color:var(--dsw-alias-label-primary,#222)}`,
|
|
158
|
+
// M8a 会话重命名:hover 显示 ✏️;编辑态行内输入;自定义标题时显示「原名」小字。
|
|
159
|
+
`.sf-renameBtn{flex:none;border:none;background:none;padding:1px 4px;margin-left:2px;border-radius:6px;cursor:pointer;color:var(--dsw-alias-label-secondary,#888);font-size:11px;line-height:16px;opacity:0;transition:opacity .12s,color .12s}`,
|
|
160
|
+
`.sf-card:hover .sf-renameBtn,.sf-renameBtn:focus-visible,.sf-renameBtn[data-active='true']{opacity:1}`,
|
|
161
|
+
`.sf-renameBtn:hover{color:var(--dsw-specific-accent,#4a7dff);background:rgba(90,140,255,.1)}`,
|
|
162
|
+
`.sf-renameWrap{flex:1 1 100%;min-width:0;display:inline-flex;align-items:center;gap:6px}`,
|
|
163
|
+
`.sf-renameInput{flex:1 1 auto;min-width:0;border:1px solid var(--dsw-specific-accent,#4a7dff);border-radius:6px;padding:3px 8px;font-size:12.5px;background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#222);outline:none}`,
|
|
164
|
+
`.sf-renameErr{color:#d43b3b;font-size:11px;white-space:nowrap}`,
|
|
165
|
+
`.sf-origTitle{flex:none;font-size:11px;color:var(--dsw-alias-label-secondary,#888);font-weight:400;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:180px;margin-left:8px}`,
|
|
166
|
+
// 候选 B:卡片结论摘要行(最近结论主行 + 首个任务小字,单行 ellipsis,hover 悬浮全文)。
|
|
167
|
+
`.sf-cardConclusion,.sf-cardTask{display:flex;align-items:center;gap:6px;min-width:0;margin-top:3px;font-size:11.5px;line-height:1.5;color:var(--dsw-alias-label-secondary,#666)}`,
|
|
168
|
+
`.sf-cardConclusionText,.sf-cardTaskText{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}`,
|
|
169
|
+
`.sf-cardTag{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}`,
|
|
170
|
+
`.sf-viewTitle .sf-renameBtn{opacity:.55}.sf-viewTitle:hover .sf-renameBtn{opacity:1}`,
|
|
147
171
|
`.sf-muted{color:var(--dsw-alias-label-secondary,#888);font-size:12px}`,
|
|
148
172
|
`.sf-mono{font-family:ui-monospace,Consolas,monospace;font-size:12px}`,
|
|
149
173
|
`.sf-badge{flex:none;border-radius:6px;padding:1px 7px;font-size:11px;line-height:16px}`,
|
|
@@ -272,6 +296,31 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
272
296
|
`.sf-cacheRow{display:flex;align-items:center;gap:8px;min-width:0}`,
|
|
273
297
|
`.sf-cacheLabel{flex:none;font-weight:600;color:var(--dsw-alias-label-secondary,#888)}`,
|
|
274
298
|
`.sf-cacheValue{font-variant-numeric:tabular-nums;font-family:ui-monospace,Consolas,monospace}`,
|
|
299
|
+
// 方向 A:跨会话全文检索结果区(与元数据筛选结果分区展示)。
|
|
300
|
+
`.sf-fulltext{flex:none;border:1px solid rgba(90,140,255,.3);background:rgba(90,140,255,.05);border-radius:10px;padding:8px 10px;display:flex;flex-direction:column;gap:4px;max-height:220px;overflow:auto}`,
|
|
301
|
+
`.sf-fulltextHead{display:flex;align-items:center;gap:8px;flex:none}`,
|
|
302
|
+
`.sf-fulltextTitle{font-weight:700;font-size:12px;color:var(--dsw-alias-label-primary,#222)}`,
|
|
303
|
+
`.sf-fulltextHit{border-radius:8px;padding:5px 8px;cursor:pointer;display:flex;flex-direction:column;gap:2px;min-width:0}`,
|
|
304
|
+
`.sf-fulltextHit:hover{background:rgba(90,140,255,.1)}`,
|
|
305
|
+
`.sf-fulltextHitTitle{display:flex;align-items:center;gap:8px;min-width:0}`,
|
|
306
|
+
`.sf-fulltextName{flex:1;min-width:0;font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary,#222)}`,
|
|
307
|
+
`.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
|
+
`.sf-fulltext .sf-muted{font-size:11px;color:var(--dsw-alias-label-secondary,#666)}`,
|
|
309
|
+
// M12:详情并入右侧栏(details 槽位轻量视图)。
|
|
310
|
+
`.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:1;min-width:0;font-size:13px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}`,
|
|
313
|
+
`.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}`,
|
|
314
|
+
`.dk-btn:hover{border-color:var(--dsw-alias-brand-primary,#4a7dff);color:var(--dsw-alias-brand-primary,#4a7dff)}`,
|
|
315
|
+
`.dk-body{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:6px}`,
|
|
316
|
+
`.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
|
+
`.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
|
+
`.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
|
+
`.dk-summaryText{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}`,
|
|
320
|
+
`.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}`,
|
|
321
|
+
// M12:并入期间禁用 layout grid 列宽过渡——官方会话切换会 closeDetails(宽 360→0),
|
|
322
|
+
// 守护重开(0→360)若带过渡动画会产生「关→开」闪烁;禁用后切换直接刷新内容(实测踩坑)。
|
|
323
|
+
`html[data-dsh-dock-active] [class*=frame],html[data-dsh-dock-active] [data-side=details]{transition:none!important}`,
|
|
275
324
|
// ── M5 会话摘要卡 ──
|
|
276
325
|
`.sf-summaryCard{flex:none;display:flex;flex-direction:column;gap:5px;border:1px solid var(--dsw-alias-border-l2,#e5e5e5);border-radius:10px;background:var(--dsw-specific-input-major,#fafafa);padding:9px 12px;font-size:12px}`,
|
|
277
326
|
`.sf-summaryHead{display:flex;align-items:center;gap:8px}`,
|
|
@@ -306,14 +355,79 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
306
355
|
`.sf-navKind.assistant{background:rgba(120,120,120,.14);color:#666}`,
|
|
307
356
|
`@keyframes sfFlashBg{0%{background:rgba(90,140,255,.4)}100%{background:transparent}}`,
|
|
308
357
|
`.sf-flash{animation:sfFlashBg 1.5s ease}`,
|
|
358
|
+
// ── M11 本会话轮次导航(会话页左侧常驻悬浮条目条,DeepSeek 悬浮导航风格)──
|
|
359
|
+
// 只显示【当前会话】的对话轮次(用户消息),不跨会话/不跨工作区;
|
|
360
|
+
// DOM 锚点驱动(data-time-hover-root,dsh-navbar 同款,零 RPC)。
|
|
361
|
+
// 形态:默认收拢为一列短横条(胶囊线,极简不占空间),悬浮/聚焦展开为文字条目卡;
|
|
362
|
+
// 垂直居中悬浮于会话内容区中间(top 由组件算区中心 + translateY(-50%))。
|
|
363
|
+
// 动画体系:一切组件的产生/消失都有「来源与目标」——display 硬切全部改为
|
|
364
|
+
// 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) .22s}`,
|
|
366
|
+
// 收拢时序:离开时宽度过渡延迟 .22s——信息面板先滑出(.24s),悬浮条主体再收拢
|
|
367
|
+
// (用户要求:收拢第一步先收起详情信息框);进入(hover/focus)无延迟即时展开。
|
|
368
|
+
`[data-dsh-piano-keys]:hover,[data-dsh-piano-keys]:focus-within{width:200px;transition-delay:0s}`,
|
|
369
|
+
`html[${ACTIVE_ATTR}] [data-dsh-piano-keys]{display:none!important}`,
|
|
370
|
+
`.pk-strip{flex:1;min-height:0;max-height:calc(60vh - 60px);overflow-y:auto;overflow-x:hidden;display:flex;flex-direction:column;gap:5px;padding:8px 4px;scrollbar-width:none;outline:none}`,
|
|
371
|
+
`.pk-strip::-webkit-scrollbar{display:none}`,
|
|
372
|
+
// 收拢态:短横条(胶囊线);overflow:hidden 使 label 文字被高度裁剪(来源=胶囊);
|
|
373
|
+
// ::before 延伸命中区(±4px,点击/悬浮无死区)。
|
|
374
|
+
`.pk-key{position:relative;flex:none;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
|
+
`.pk-key::before{content:'';position:absolute;left:0;right:0;top:-4px;bottom:-4px}`,
|
|
376
|
+
`.pk-key:hover{background:var(--dsw-alias-brand-primary,#4a7dff)}`,
|
|
377
|
+
`.pk-key:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4a7dff);outline-offset:-1px}`,
|
|
378
|
+
`.pk-keyActive{background:var(--dsw-alias-brand-primary,#4a7dff)}`,
|
|
379
|
+
`.pk-keyFocus{background:var(--dsw-alias-brand-primary,#4a7dff)}`,
|
|
380
|
+
// 展开态(悬浮/聚焦):条目变文字卡片;label 常驻(缩略态被条目高度裁剪),
|
|
381
|
+
// 展开时随高度过渡露出文字(来源=胶囊内部,目标=卡片全文)。
|
|
382
|
+
`[data-dsh-piano-keys]:hover .pk-key,[data-dsh-piano-keys]:focus-within .pk-key{display:flex;align-items:center;gap:6px;height:auto;min-height:28px;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)}`,
|
|
383
|
+
`[data-dsh-piano-keys]:hover .pk-key:hover,[data-dsh-piano-keys]:focus-within .pk-key:hover{background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 96%,transparent);border-color:var(--dsw-alias-brand-primary,#4a7dff)}`,
|
|
384
|
+
`[data-dsh-piano-keys]: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
|
+
`.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}`,
|
|
387
|
+
// 轮次序号:缩略态折叠(宽度 0 + 透明),展开态从左侧展开露出(来源=左缘)。
|
|
388
|
+
`.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}`,
|
|
390
|
+
// hover 条目:在【该条目右侧】展开信息面板(.pk-detail)——absolute 相对悬浮条容器,
|
|
391
|
+
// 坐标按条目位置换算为容器内偏移(注意:容器有 transform:translateY(-50%),fixed 子元素
|
|
392
|
+
// 会被 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)}`,
|
|
394
|
+
// 展开时序:详情框延迟 .28s 再滑入——等悬浮条展开动画完成、条目就位后,
|
|
395
|
+
// 位置才准确(第一次展开时条目仍在变形,立即计算会错位,实测踩坑);
|
|
396
|
+
// 隐藏则立即(无延迟)。
|
|
397
|
+
`.pk-detail{position:absolute;left:0;top:0;width:264px;max-height:calc(60vh - 40px);display:flex;flex-direction:column;gap:6px;background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 76%,transparent);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border:1px solid var(--dsw-alias-border-l2,#e4e6eb);border-radius:12px;box-shadow:0 8px 28px rgba(0,0,0,.18);padding:10px 12px;z-index:6;overflow:hidden;opacity:1;transform:translateX(0);transition:opacity .18s ease .28s,transform .24s cubic-bezier(.25,1.1,.4,1) .28s,visibility .24s .28s}`,
|
|
398
|
+
`.pk-detail.hidden{opacity:0;transform:translateX(14px);visibility:hidden;pointer-events:none;transition:opacity .18s ease,transform .24s cubic-bezier(.25,1.1,.4,1),visibility .24s}`,
|
|
399
|
+
`.pk-detailHead{flex:none;font-size:10.5px;font-weight:700;color:var(--dsw-alias-label-secondary,#666);font-family:ui-monospace,Consolas,monospace;letter-spacing:.2px}`,
|
|
400
|
+
// 面板内容随条目切换:新内容从右轻滑入(key 变化重新挂载触发 animation)。
|
|
401
|
+
`.pk-detailBody{min-height:0;font-size:12.5px;line-height:1.6;color:var(--dsw-alias-label-primary,#222);white-space:pre-wrap;word-break:break-word;overflow-y:auto;max-height:320px;animation:pkSlideIn .22s ease}`,
|
|
402
|
+
`@keyframes pkSlideIn{from{opacity:0;transform:translateX(10px)}to{opacity:1;transform:translateX(0)}}`,
|
|
403
|
+
// 跳转状态提示条(悬浮条下方,与悬浮条等宽):文字在条宽内换行增高显示完整
|
|
404
|
+
// 文案(不加宽),从下方淡入/淡出(来源=悬浮条底缘);毛玻璃。
|
|
405
|
+
`.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
|
+
`.pk-status.hidden{opacity:0;transform:translateY(8px);visibility:hidden;pointer-events:none}`,
|
|
407
|
+
`.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
|
+
// 头部:从上方滑入(来源=悬浮条顶缘)——max-height/padding/opacity/transform 过渡,
|
|
413
|
+
// 不占缩略态布局。内容「标题 + ▲▼ 按钮组(并排右上)」。
|
|
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)}`,
|
|
416
|
+
`.pk-headLabel{flex:1;min-width:0;font-size:10.5px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}`,
|
|
417
|
+
`.pk-navGroup{flex:none;display:flex;gap:3px}`,
|
|
418
|
+
`.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}`,
|
|
419
|
+
`.pk-navBtn:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary,#4a7dff);color:var(--dsw-alias-brand-primary,#4a7dff)}`,
|
|
420
|
+
`.pk-navBtn:disabled{opacity:.35;cursor:default}`,
|
|
421
|
+
`@keyframes sfPianoPulse{0%,100%{opacity:.35}50%{opacity:1}}`,
|
|
309
422
|
].join('\n')
|
|
310
423
|
|
|
311
424
|
// ── API ─────────────────────────────────────────────────────────────
|
|
312
|
-
function api(method, params) {
|
|
425
|
+
function api(method, params, signal) {
|
|
313
426
|
return fetch('/api/session-flow', {
|
|
314
427
|
method: 'POST',
|
|
315
428
|
headers: { 'Content-Type': 'application/json' },
|
|
316
429
|
body: JSON.stringify(Object.assign({ method }, params || {})),
|
|
430
|
+
signal,
|
|
317
431
|
}).then((r) => r.json())
|
|
318
432
|
}
|
|
319
433
|
|
|
@@ -404,6 +518,50 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
404
518
|
return (bytes / 1024 / 1024).toFixed(1) + ' MiB'
|
|
405
519
|
}
|
|
406
520
|
|
|
521
|
+
// ── M8a 会话重命名:行内编辑(总览卡片 / 详情页标题复用)─────────────
|
|
522
|
+
// props: { initial, onSave: (title) => Promise, onCancel, placeholder }
|
|
523
|
+
// 保存成功(ok truthy / 无返回)由父组件退出编辑态;失败(reject / ok===false)留在编辑态显示错误。
|
|
524
|
+
function RenameInline(props) {
|
|
525
|
+
const [value, setValue] = useState(props.initial || '')
|
|
526
|
+
const [saving, setSaving] = useState(false)
|
|
527
|
+
const [err, setErr] = useState('')
|
|
528
|
+
const inputRef = useRef(null)
|
|
529
|
+
useEffect(() => {
|
|
530
|
+
const el = inputRef.current
|
|
531
|
+
if (el) { el.focus(); el.select() }
|
|
532
|
+
}, [])
|
|
533
|
+
const commit = () => {
|
|
534
|
+
if (saving) return
|
|
535
|
+
const v = value.trim()
|
|
536
|
+
if (v === String(props.initial || '').trim()) { props.onCancel(); return }
|
|
537
|
+
setSaving(true)
|
|
538
|
+
setErr('')
|
|
539
|
+
Promise.resolve(props.onSave(v)).then((ok) => {
|
|
540
|
+
if (ok === false) setSaving(false)
|
|
541
|
+
}).catch((e) => {
|
|
542
|
+
setSaving(false)
|
|
543
|
+
setErr(String(e && e.message || e))
|
|
544
|
+
})
|
|
545
|
+
}
|
|
546
|
+
return h('span', { className: 'sf-renameWrap' },
|
|
547
|
+
h('input', {
|
|
548
|
+
ref: inputRef,
|
|
549
|
+
className: 'sf-renameInput',
|
|
550
|
+
value,
|
|
551
|
+
placeholder: props.placeholder || STR.renamePlaceholder,
|
|
552
|
+
disabled: saving,
|
|
553
|
+
onInput: (e) => setValue(e.target.value),
|
|
554
|
+
onKeyDown: (e) => {
|
|
555
|
+
if (e.key === 'Enter') commit()
|
|
556
|
+
else if (e.key === 'Escape') { e.stopPropagation(); props.onCancel() }
|
|
557
|
+
},
|
|
558
|
+
onBlur: commit,
|
|
559
|
+
onClick: (e) => e.stopPropagation(),
|
|
560
|
+
}),
|
|
561
|
+
err && h('span', { className: 'sf-renameErr' }, err),
|
|
562
|
+
)
|
|
563
|
+
}
|
|
564
|
+
|
|
407
565
|
// ── 总览页 ──────────────────────────────────────────────────────────
|
|
408
566
|
// props: { onOpen: (session) => void, onClose: () => void }
|
|
409
567
|
function SessionFlowOverview(props) {
|
|
@@ -418,6 +576,79 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
418
576
|
const [cacheInfo, setCacheInfo] = useState(null)
|
|
419
577
|
const [cacheBusy, setCacheBusy] = useState(false)
|
|
420
578
|
const [cacheMsg, setCacheMsg] = useState('')
|
|
579
|
+
// 方向 A:跨会话全文检索(防抖 500ms;AbortController 取消;结果区渲染)。
|
|
580
|
+
const [fullText, setFullText] = useState(null) // {phase:'searching'|'done'|'error', query, results, scanned, total, hasMore, error}
|
|
581
|
+
const fullTextAbort = useRef(null)
|
|
582
|
+
const fullTextTimer = useRef(null)
|
|
583
|
+
// M8a:当前编辑重命名的会话 id(null = 无编辑态)。
|
|
584
|
+
const [renamingId, setRenamingId] = useState(null)
|
|
585
|
+
// M8a:保存自定义标题 → 更新本地列表数据(userTitle 覆盖显示,不整页重刷)。
|
|
586
|
+
const saveRename = (sid, title) => {
|
|
587
|
+
return api('rename', { sessionId: sid, title }).then((json) => {
|
|
588
|
+
if (json && json.ok) {
|
|
589
|
+
setState((prev) => {
|
|
590
|
+
if (!prev || !prev.data) return prev
|
|
591
|
+
return {
|
|
592
|
+
...prev,
|
|
593
|
+
data: {
|
|
594
|
+
...prev.data,
|
|
595
|
+
workspaces: (prev.data.workspaces || []).map((g) => ({
|
|
596
|
+
...g,
|
|
597
|
+
sessions: (g.sessions || []).map((s) => (s.id === sid ? { ...s, userTitle: json.userTitle } : s)),
|
|
598
|
+
})),
|
|
599
|
+
},
|
|
600
|
+
}
|
|
601
|
+
})
|
|
602
|
+
setRenamingId(null)
|
|
603
|
+
return true
|
|
604
|
+
}
|
|
605
|
+
throw new Error((json && json.error) || STR.renameFail)
|
|
606
|
+
})
|
|
607
|
+
}
|
|
608
|
+
useEffect(() => () => {
|
|
609
|
+
if (fullTextTimer.current) clearTimeout(fullTextTimer.current)
|
|
610
|
+
if (fullTextAbort.current) fullTextAbort.current.abort()
|
|
611
|
+
}, [])
|
|
612
|
+
useEffect(() => {
|
|
613
|
+
const q = query.trim()
|
|
614
|
+
const isStructured = /^(tool|file|path|err|error):/i.test(q)
|
|
615
|
+
if (fullTextTimer.current) clearTimeout(fullTextTimer.current)
|
|
616
|
+
if (fullTextAbort.current) { fullTextAbort.current.abort(); fullTextAbort.current = null }
|
|
617
|
+
// 结构化前缀走元数据筛选(已有);自由文本 ≥2 字符才触发全文检索。
|
|
618
|
+
if (q.length < 2 || isStructured) { setFullText(null); return }
|
|
619
|
+
fullTextTimer.current = setTimeout(() => {
|
|
620
|
+
const ctrl = new AbortController()
|
|
621
|
+
fullTextAbort.current = ctrl
|
|
622
|
+
setFullText({ phase: 'searching', query: q, results: null, scanned: 0, total: 0, hasMore: false, error: '' })
|
|
623
|
+
api('searchAll', { query: q, workspace: wsFilter || undefined }, ctrl.signal).then((json) => {
|
|
624
|
+
if (json && json.ok) {
|
|
625
|
+
setFullText({ phase: 'done', query: q, results: json.results || [], scanned: json.scanned || 0, total: json.total || 0, hasMore: json.hasMore === true, error: '' })
|
|
626
|
+
} else {
|
|
627
|
+
setFullText((p) => p && p.query === q ? { phase: 'error', query: q, results: null, scanned: 0, total: 0, hasMore: false, error: (json && json.error) || STR.fullTextFail } : p)
|
|
628
|
+
}
|
|
629
|
+
}).catch((e) => {
|
|
630
|
+
if (e && e.name === 'AbortError') return
|
|
631
|
+
setFullText((p) => p && p.query === q ? { phase: 'error', query: q, results: null, scanned: 0, total: 0, hasMore: false, error: String(e && e.message || e) } : p)
|
|
632
|
+
})
|
|
633
|
+
}, 500)
|
|
634
|
+
}, [query, wsFilter])
|
|
635
|
+
// 命中跳转:localStorage 桥(会话流标签页读取后执行会话内检索定位)+ 原生打开会话。
|
|
636
|
+
const openFullTextHit = (hit) => {
|
|
637
|
+
try {
|
|
638
|
+
// 预写目标会话的 chat store view='session-flow':会话打开时(store 按会话重建并
|
|
639
|
+
// rehydrate localStorage)自动激活「会话流」标签页,而非停在默认 Chat 标签。
|
|
640
|
+
const storeKey = 'dsh.conversation.chat.' + hit.sessionId
|
|
641
|
+
let st = {}
|
|
642
|
+
try { const raw = localStorage.getItem(storeKey); if (raw) st = JSON.parse(raw) } catch (e) {}
|
|
643
|
+
st.view = 'session-flow'
|
|
644
|
+
localStorage.setItem(storeKey, JSON.stringify(st))
|
|
645
|
+
localStorage.setItem(PENDING_SEARCH_KEY, JSON.stringify({ sessionId: hit.sessionId, query: query.trim() }))
|
|
646
|
+
} catch (e) {}
|
|
647
|
+
try { props.sessions.open(hit.sessionId) } catch (e) { console.warn('[dsh-session-flow] open failed', hit.sessionId, e) }
|
|
648
|
+
// 关闭工作台(中栏交还给会话),用户直接看到目标会话的会话流标签页——
|
|
649
|
+
// 否则工作台仍覆盖中栏,需手动点侧边栏才可见(实测踩坑)。
|
|
650
|
+
try { if (props.onClose) props.onClose() } catch (e) {}
|
|
651
|
+
}
|
|
421
652
|
const fmtBytes = (b) => {
|
|
422
653
|
if (b === null || b === undefined) return '—'
|
|
423
654
|
if (b < 1024) return b + ' B'
|
|
@@ -575,6 +806,27 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
575
806
|
h('button', { className: 'sf-btn' + (cacheOpen ? ' sf-btnActive' : ''), onClick: () => setCacheOpen(!cacheOpen) }, STR.cache),
|
|
576
807
|
h('button', { className: 'sf-btn', onClick: props.onClose }, '✕ ' + STR.close),
|
|
577
808
|
),
|
|
809
|
+
// 方向 A:跨会话全文检索结果区(防抖触发;与元数据筛选结果分区展示)。
|
|
810
|
+
fullText && h('div', { className: 'sf-fulltext' },
|
|
811
|
+
h('div', { className: 'sf-fulltextHead' },
|
|
812
|
+
h('span', { className: 'sf-fulltextTitle' }, STR.fullText + '「' + fullText.query + '」'),
|
|
813
|
+
fullText.phase === 'searching' && h('span', { className: 'sf-muted' }, STR.fullTextSearching),
|
|
814
|
+
fullText.phase === 'done' && fullText.hasMore && h('span', { className: 'sf-muted' },
|
|
815
|
+
STR.fullTextPartial.replace('{N}', String(fullText.scanned)).replace('{M}', String(fullText.total))),
|
|
816
|
+
),
|
|
817
|
+
fullText.phase === 'error' && h('div', { className: 'sf-hint', style: { color: '#d43b3b' } }, STR.fullTextFail + ': ' + fullText.error),
|
|
818
|
+
fullText.phase === 'done' && fullText.results.length === 0 && h('div', { className: 'sf-hint' }, STR.fullTextNone),
|
|
819
|
+
fullText.phase === 'done' && fullText.results.map((hit) =>
|
|
820
|
+
h('div', { key: hit.sessionId, className: 'sf-fulltextHit', onClick: () => openFullTextHit(hit) },
|
|
821
|
+
h('div', { className: 'sf-fulltextHitTitle' },
|
|
822
|
+
h('span', { className: 'sf-fulltextName' }, hit.userTitle || hit.title),
|
|
823
|
+
h('span', { className: 'sf-fulltextCount' }, hit.matchCount),
|
|
824
|
+
),
|
|
825
|
+
h('div', { className: 'sf-muted', style: { marginTop: 2 } },
|
|
826
|
+
(hit.matches[0] ? hit.matches[0].preview : '') + ' · ' + hit.workspace),
|
|
827
|
+
),
|
|
828
|
+
),
|
|
829
|
+
),
|
|
578
830
|
// 缓存管理面板:仅本插件缓存,查看体积 + 分类清理(清理后自动重建,无需重启)。
|
|
579
831
|
cacheOpen && h('div', { className: 'sf-cachePanel' },
|
|
580
832
|
h('div', { className: 'sf-muted', style: { flex: 'none' } }, STR.cacheHint),
|
|
@@ -650,12 +902,35 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
650
902
|
h('span', { className: 'sf-cardWsFilter', dangerouslySetInnerHTML: { __html: FILTER_ICON } }),
|
|
651
903
|
),
|
|
652
904
|
h('div', { className: 'sf-cardTitle' },
|
|
653
|
-
|
|
905
|
+
renamingId === s.id
|
|
906
|
+
? h(RenameInline, {
|
|
907
|
+
initial: s.userTitle || s.title || '',
|
|
908
|
+
onSave: (t) => saveRename(s.id, t),
|
|
909
|
+
onCancel: () => setRenamingId(null),
|
|
910
|
+
})
|
|
911
|
+
: h('span', { className: 'sf-cardText', style: { overflow: 'hidden', textOverflow: 'ellipsis' } },
|
|
912
|
+
s.userTitle || s.title || STR.unknownTitle,
|
|
913
|
+
s.userTitle && h('span', { className: 'sf-origTitle', title: s.title || STR.unknownTitle },
|
|
914
|
+
STR.origTitle + ': ' + (s.title || STR.unknownTitle)),
|
|
915
|
+
h('button', {
|
|
916
|
+
className: 'sf-renameBtn', title: STR.rename,
|
|
917
|
+
onClick: (e) => { e.stopPropagation(); setRenamingId(s.id) },
|
|
918
|
+
}, '✎'),
|
|
919
|
+
),
|
|
654
920
|
s.isEmpty && h('span', { className: 'sf-badge sf-badgeEnd' }, STR.emptySession),
|
|
655
921
|
s.running ? h('span', { className: 'sf-badge sf-badgeRun' }, STR.running) : h('span', { className: 'sf-badge sf-badgeEnd' }, STR.ended),
|
|
656
922
|
s.delegationDepth > 0 && h('span', { className: 'sf-badge sf-badgeSub' }, STR.subagent + ' · d' + s.delegationDepth),
|
|
657
923
|
s.toolErrors > 0 && h('span', { className: 'sf-badge sf-badgeErr' }, STR.errors + ': ' + s.toolErrors),
|
|
658
924
|
),
|
|
925
|
+
// 候选 B:最近结论(主行)+ 首个任务(小字);无值不渲染;hover 悬浮全文。
|
|
926
|
+
s.lastConclusion && h('div', { className: 'sf-cardConclusion', title: s.lastConclusion },
|
|
927
|
+
h('span', { className: 'sf-cardTag' }, STR.conclusion),
|
|
928
|
+
h('span', { className: 'sf-cardConclusionText' }, s.lastConclusion),
|
|
929
|
+
),
|
|
930
|
+
s.firstTask && h('div', { className: 'sf-cardTask', title: s.firstTask },
|
|
931
|
+
h('span', { className: 'sf-cardTag' }, STR.task),
|
|
932
|
+
h('span', { className: 'sf-cardTaskText' }, s.firstTask),
|
|
933
|
+
),
|
|
659
934
|
h('div', { className: 'sf-muted' },
|
|
660
935
|
h('span', {}, fmtTime(s.createdAt)),
|
|
661
936
|
h('span', { style: { marginLeft: 8 } }, STR.duration + ' ' + fmtDuration(s.lastEventTime - s.createdAt)),
|
|
@@ -1007,7 +1282,8 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1007
1282
|
const renderNode = (node, depth, isLive) => {
|
|
1008
1283
|
const active = selected !== null && selected.id === node.id
|
|
1009
1284
|
const hasChildren = (node.children && node.children.length > 0)
|
|
1010
|
-
|
|
1285
|
+
// M8a:自定义标题优先(离线节点 host 已透传 userTitle;live 节点无此字段)。
|
|
1286
|
+
const label = node.userTitle || node.label || node.title || node.id
|
|
1011
1287
|
const meta = isLive
|
|
1012
1288
|
? ((node.mode === 'continuable' ? STR.modeContinuable : STR.modeOneShot) + ' · ' + (node.activity === 'running' ? STR.running : STR.ended))
|
|
1013
1289
|
: ((node.toolCalls || 0) + ' ' + STR.tools + (node.toolErrors ? ' · ' + node.toolErrors + ' ' + STR.errors : ''))
|
|
@@ -1078,6 +1354,19 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1078
1354
|
// 折叠状态:键集合,has(key) = 折叠。turn-N / tool-<callId> / msg-<seq>
|
|
1079
1355
|
const [collapsed, setCollapsed] = useState(() => new Set())
|
|
1080
1356
|
const collapsedInit = useRef(false)
|
|
1357
|
+
// M8a:标题重命名(编辑态 + 保存后的显示覆盖,优先于 session.userTitle)。
|
|
1358
|
+
const [renaming, setRenaming] = useState(false)
|
|
1359
|
+
const [titleOverride, setTitleOverride] = useState(null)
|
|
1360
|
+
const saveRename = (title) => {
|
|
1361
|
+
return api('rename', { sessionId: session.id, title }).then((json) => {
|
|
1362
|
+
if (json && json.ok) {
|
|
1363
|
+
setTitleOverride(json.userTitle) // null = 清除恢复原名
|
|
1364
|
+
setRenaming(false)
|
|
1365
|
+
return true
|
|
1366
|
+
}
|
|
1367
|
+
throw new Error((json && json.error) || STR.renameFail)
|
|
1368
|
+
})
|
|
1369
|
+
}
|
|
1081
1370
|
|
|
1082
1371
|
useEffect(() => {
|
|
1083
1372
|
// 空会话(新建后未对话):不请求详情,直接显示占位提示。
|
|
@@ -1317,6 +1606,61 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1317
1606
|
}, 120)
|
|
1318
1607
|
}
|
|
1319
1608
|
|
|
1609
|
+
// ── M7 遗留 L1:跨视图 inspect 握手 ──────────────────────────────
|
|
1610
|
+
// chat 视图工具调用行「Inspect」按钮写入共享 store({callId}),会话流标签
|
|
1611
|
+
// 作为 conversation.view 占用者收到 owner props:inspect/onInspectDone。
|
|
1612
|
+
// 消费:toolStats 里定位该 callId 所属回合 → 展开回合(按需加载)→ 滚动定位
|
|
1613
|
+
// → 闪烁高亮 → onInspectDone() 清除(参照 ui-trajectory 的 inspect 消费方式)。
|
|
1614
|
+
// 时序处理:inspect 可能在数据未就绪时到达(依赖 phase 重跑);liveOn/血缘视图
|
|
1615
|
+
// 时先退出再定位;同一 callId 只处理一次(inspectHandled ref,store 清除后重置)。
|
|
1616
|
+
const inspectHandled = useRef(null)
|
|
1617
|
+
const jumpToCall = (turn, callId) => {
|
|
1618
|
+
setActiveTurn(turn)
|
|
1619
|
+
setCollapsed((prev) => {
|
|
1620
|
+
const next = new Set(prev)
|
|
1621
|
+
next.delete('turn-' + turn)
|
|
1622
|
+
return next
|
|
1623
|
+
})
|
|
1624
|
+
ensureTurn(turn)
|
|
1625
|
+
setTimeout(() => {
|
|
1626
|
+
const el = document.getElementById('sf-tc-' + callId)
|
|
1627
|
+
if (el) {
|
|
1628
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
1629
|
+
el.classList.add('sf-flash')
|
|
1630
|
+
setTimeout(() => el.classList.remove('sf-flash'), 1600)
|
|
1631
|
+
}
|
|
1632
|
+
}, 120)
|
|
1633
|
+
}
|
|
1634
|
+
const applyInspect = (callId) => {
|
|
1635
|
+
if (!callId) return
|
|
1636
|
+
let turn = null
|
|
1637
|
+
for (const t of toolStats) {
|
|
1638
|
+
const c = (t.calls || []).find((x) => x.callId === callId)
|
|
1639
|
+
if (c) { turn = c.turn; break }
|
|
1640
|
+
}
|
|
1641
|
+
if (turn === null) {
|
|
1642
|
+
// toolStats 未命中(理论上不应发生):searchIn 按 callId 全文兜底。
|
|
1643
|
+
api('searchIn', { sessionId: session.id, query: callId }).then((json) => {
|
|
1644
|
+
const m = json && json.ok ? (json.matches || []).find((x) => x.callId === callId) : null
|
|
1645
|
+
if (m && typeof m.turn === 'number') jumpToCall(m.turn, callId)
|
|
1646
|
+
if (props.onInspectDone) props.onInspectDone()
|
|
1647
|
+
}).catch(() => { if (props.onInspectDone) props.onInspectDone() })
|
|
1648
|
+
return
|
|
1649
|
+
}
|
|
1650
|
+
jumpToCall(turn, callId)
|
|
1651
|
+
if (props.onInspectDone) props.onInspectDone()
|
|
1652
|
+
}
|
|
1653
|
+
useEffect(() => {
|
|
1654
|
+
// store 清除(inspect → null)后重置防重标记,允许同一 callId 再次检视。
|
|
1655
|
+
if (!props.inspect || !props.inspect.callId) { inspectHandled.current = null; return }
|
|
1656
|
+
if (state.phase !== 'ready') return
|
|
1657
|
+
if (inspectHandled.current === props.inspect.callId) return
|
|
1658
|
+
inspectHandled.current = props.inspect.callId
|
|
1659
|
+
if (liveOn) exitLive()
|
|
1660
|
+
if (view === 'lineage') setView('timeline')
|
|
1661
|
+
applyInspect(props.inspect.callId)
|
|
1662
|
+
}, [props.inspect, state.phase])
|
|
1663
|
+
|
|
1320
1664
|
// 定位工具/错误调用:展开所在回合(按需加载)→ 滚动到调用行;同一种再次点击循环。
|
|
1321
1665
|
const locateCall = (list, cursorKey) => {
|
|
1322
1666
|
if (!list || list.length === 0) return
|
|
@@ -1431,12 +1775,32 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1431
1775
|
|
|
1432
1776
|
return h('div', { className: 'sf-view' },
|
|
1433
1777
|
h('div', { className: 'sf-viewHeader' },
|
|
1434
|
-
h('button', { className: 'sf-btn', onClick: props.onBack }, '← ' + STR.back),
|
|
1778
|
+
h('button', { className: 'sf-btn', onClick: props.onBack }, '← ' + (props.backLabel || STR.back)),
|
|
1435
1779
|
h('h2', { className: 'sf-viewTitle', style: { flex: '1 1 auto', overflow: 'hidden', textOverflow: 'ellipsis' } },
|
|
1436
|
-
|
|
1780
|
+
renaming
|
|
1781
|
+
? h(RenameInline, {
|
|
1782
|
+
initial: titleOverride || session.userTitle || session.title || '',
|
|
1783
|
+
onSave: saveRename,
|
|
1784
|
+
onCancel: () => setRenaming(false),
|
|
1785
|
+
})
|
|
1786
|
+
: 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),
|
|
1789
|
+
h('button', {
|
|
1790
|
+
className: 'sf-renameBtn', title: STR.rename,
|
|
1791
|
+
onClick: () => setRenaming(true),
|
|
1792
|
+
}, '✎'),
|
|
1793
|
+
),
|
|
1794
|
+
),
|
|
1437
1795
|
running ? h('span', { className: 'sf-badge sf-badgeRun' }, STR.running) : h('span', { className: 'sf-badge sf-badgeEnd' }, STR.ended),
|
|
1438
1796
|
session.delegationDepth > 0 && h('span', { className: 'sf-badge sf-badgeSub' }, STR.subagent),
|
|
1439
1797
|
h('button', { className: 'sf-btn', onClick: () => { setPendingSelect(null); setView('lineage') } }, STR.lineage),
|
|
1798
|
+
// M12:并入右栏(仅工作台详情页;嵌入标签页不显示——避免右栏与标签页重复)。
|
|
1799
|
+
!props.embedded && h('button', {
|
|
1800
|
+
className: 'sf-btn',
|
|
1801
|
+
onClick: () => { if (dockBridge.controller) dockBridge.controller.open() },
|
|
1802
|
+
title: STR.dockHint,
|
|
1803
|
+
}, STR.dock),
|
|
1440
1804
|
// M6 实时通道:进行中会话可实时查看(sessions.history → derive → 折叠视图)。
|
|
1441
1805
|
props.connection !== undefined && h('button', {
|
|
1442
1806
|
className: 'sf-btn' + (liveOn ? ' sf-btnActive' : ''),
|
|
@@ -1444,7 +1808,8 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1444
1808
|
disabled: liveOn && liveState.phase === 'loading',
|
|
1445
1809
|
}, liveOn ? STR.liveActive + ' ✕' : STR.live),
|
|
1446
1810
|
h('button', { className: 'sf-btn', onClick: exportSessionMd, disabled: exporting }, exporting ? STR.exporting : STR.exportMd),
|
|
1447
|
-
|
|
1811
|
+
// 嵌入原生会话页模式:无「跳转原生会话」(本页即原生会话页)。
|
|
1812
|
+
!props.embedded && h('button', { className: 'sf-btn', onClick: jumpNative }, STR.jumpNative),
|
|
1448
1813
|
h('button', { className: 'sf-btn', onClick: expandAll }, STR.expandAll),
|
|
1449
1814
|
h('button', { className: 'sf-btn', onClick: collapseAll }, STR.collapseAll),
|
|
1450
1815
|
),
|
|
@@ -1651,6 +2016,189 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1651
2016
|
)
|
|
1652
2017
|
}
|
|
1653
2018
|
|
|
2019
|
+
// ── 二期:嵌入原生会话页(conversation.view 槽位标签页)────────────
|
|
2020
|
+
// 槽位环为会话作用域:每个原生会话页内一个「会话流」标签页,框架按
|
|
2021
|
+
// key={sessionId} 重挂载,天然跟随当前会话切换。组件收到的框架标准
|
|
2022
|
+
// props:sessionId + useSession(会话快照);数据源与一期详情页一致
|
|
2023
|
+
// (host get/getTurn/searchIn/summarize/exportMd/lineage + 实时通道)。
|
|
2024
|
+
// 先取 host get 的轻量元信息(标题/计数,用于详情页空会话判定),
|
|
2025
|
+
// 再复用 SessionFlowDetail 完整详情视图(摘要/统计/折叠时间线/四标签导航)。
|
|
2026
|
+
// 嵌入模式差异:返回按钮 →「打开完整工作台」(唤起侧边栏工作台);
|
|
2027
|
+
// 隐藏「跳转原生会话」(本页即原生会话页);onClose 无操作。
|
|
2028
|
+
function SessionFlowTab(props) {
|
|
2029
|
+
const { sessionId } = props
|
|
2030
|
+
const [meta, setMeta] = useState(null) // {id, title, userMessages, toolCalls, turns}
|
|
2031
|
+
const [loadErr, setLoadErr] = useState('')
|
|
2032
|
+
// M7 遗留 L2:档案未命中(会话尚未落盘)时进入实时兜底视图,而非纯报错。
|
|
2033
|
+
const [liveFallback, setLiveFallback] = useState(false)
|
|
2034
|
+
const [retryKey, setRetryKey] = useState(0) // 「重试档案视图」触发重新 get
|
|
2035
|
+
// 方向 A:全文检索命中跳转——读取 pendingSearch 桥(总览命中点击写入),
|
|
2036
|
+
// 匹配当前会话则把检索词带入详情页(initialSearchQuery → 自动执行会话内 searchIn)。
|
|
2037
|
+
const [pendingQuery, setPendingQuery] = useState('')
|
|
2038
|
+
useEffect(() => {
|
|
2039
|
+
if (!sessionId) return
|
|
2040
|
+
try {
|
|
2041
|
+
const raw = localStorage.getItem(PENDING_SEARCH_KEY)
|
|
2042
|
+
if (!raw) return
|
|
2043
|
+
const p = JSON.parse(raw)
|
|
2044
|
+
if (p && p.sessionId === sessionId && p.query) {
|
|
2045
|
+
localStorage.removeItem(PENDING_SEARCH_KEY)
|
|
2046
|
+
setPendingQuery(p.query)
|
|
2047
|
+
}
|
|
2048
|
+
} catch (e) {}
|
|
2049
|
+
}, [sessionId])
|
|
2050
|
+
|
|
2051
|
+
useEffect(() => {
|
|
2052
|
+
let alive = true
|
|
2053
|
+
setMeta(null)
|
|
2054
|
+
setLoadErr('')
|
|
2055
|
+
setLiveFallback(false)
|
|
2056
|
+
if (!sessionId) return undefined
|
|
2057
|
+
const conn = props.connection
|
|
2058
|
+
const canLive = conn !== undefined && conn.api !== undefined && conn.api.sessions !== undefined
|
|
2059
|
+
const toFallback = () => { if (alive && canLive) setLiveFallback(true) }
|
|
2060
|
+
api('get', { sessionId }).then((json) => {
|
|
2061
|
+
if (!alive) return
|
|
2062
|
+
if (json && json.ok) {
|
|
2063
|
+
const counts = json.counts || {}
|
|
2064
|
+
const t = json.title
|
|
2065
|
+
setMeta({
|
|
2066
|
+
id: sessionId,
|
|
2067
|
+
// entry.parsed.title 是 {title, source} 对象(已知陷阱),取 .title。
|
|
2068
|
+
title: t && typeof t === 'object' ? String(t.title || '') : String(t || ''),
|
|
2069
|
+
// M8a:自定义标题覆盖显示(userTitle || title)。
|
|
2070
|
+
userTitle: json.userTitle || null,
|
|
2071
|
+
userMessages: counts['user/message'] || 0,
|
|
2072
|
+
toolCalls: counts['tool/call'] || 0,
|
|
2073
|
+
turns: counts['turn/start'] || 0,
|
|
2074
|
+
})
|
|
2075
|
+
} else {
|
|
2076
|
+
if (canLive) setLiveFallback(true)
|
|
2077
|
+
else setLoadErr((json && json.error) || STR.detailFailed)
|
|
2078
|
+
}
|
|
2079
|
+
}).catch((e) => {
|
|
2080
|
+
if (!alive) return
|
|
2081
|
+
if (canLive) setLiveFallback(true)
|
|
2082
|
+
else setLoadErr(String(e && e.message || e))
|
|
2083
|
+
})
|
|
2084
|
+
return () => { alive = false }
|
|
2085
|
+
}, [sessionId, retryKey])
|
|
2086
|
+
|
|
2087
|
+
if (liveFallback) {
|
|
2088
|
+
return h(SessionFlowLiveFallback, {
|
|
2089
|
+
sessionId,
|
|
2090
|
+
connection: props.connection,
|
|
2091
|
+
onRetry: () => setRetryKey((k) => k + 1),
|
|
2092
|
+
})
|
|
2093
|
+
}
|
|
2094
|
+
if (loadErr) {
|
|
2095
|
+
return h('div', { className: 'sf-view' },
|
|
2096
|
+
h('div', { className: 'sf-hint', style: { color: '#d43b3b' } }, STR.detailFailed + ': ' + loadErr))
|
|
2097
|
+
}
|
|
2098
|
+
if (meta === null) {
|
|
2099
|
+
return h('div', { className: 'sf-view' }, h('div', { className: 'sf-hint' }, STR.detailLoading))
|
|
2100
|
+
}
|
|
2101
|
+
return h(SessionFlowDetail, {
|
|
2102
|
+
session: meta,
|
|
2103
|
+
sessions: props.sessions,
|
|
2104
|
+
connection: props.connection,
|
|
2105
|
+
onBack: props.onOpenWorkbench,
|
|
2106
|
+
backLabel: STR.openWorkbench,
|
|
2107
|
+
embedded: true,
|
|
2108
|
+
onClose: () => {},
|
|
2109
|
+
initialSearchQuery: pendingQuery,
|
|
2110
|
+
// M7 遗留 L1:透传跨视图 inspect 握手(chat Inspect 按钮 → 会话流定位)。
|
|
2111
|
+
inspect: props.inspect,
|
|
2112
|
+
onInspectDone: props.onInspectDone,
|
|
2113
|
+
})
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// ── M7 遗留 L2:会话未落盘时的实时兜底视图 ────────────────────────
|
|
2117
|
+
// 档案通道未命中(host get 404)时,用实时通道(sessions.history → derive)
|
|
2118
|
+
// 渲染折叠时间线 + 3s 轮询,并保留「重试档案视图」入口。逻辑与 SessionFlowDetail
|
|
2119
|
+
// 内实时通道同构(结构信号运行判定 / 只折叠新回合 / 不传 hideHead),自包含实现。
|
|
2120
|
+
function SessionFlowLiveFallback(props) {
|
|
2121
|
+
const { sessionId, connection } = props
|
|
2122
|
+
const [state, setState] = useState({ phase: 'loading', error: null, timeline: null, eventCount: 0, running: false })
|
|
2123
|
+
const [collapsed, setCollapsed] = useState(() => new Set())
|
|
2124
|
+
const [activeTurn, setActiveTurn] = useState(null)
|
|
2125
|
+
const liveSeenTurns = useRef(new Set())
|
|
2126
|
+
const timer = useRef(null)
|
|
2127
|
+
|
|
2128
|
+
const fetchLive = useRef(async () => {})
|
|
2129
|
+
fetchLive.current = async () => {
|
|
2130
|
+
if (connection === undefined || connection.api === undefined || connection.api.sessions === undefined) {
|
|
2131
|
+
setState((s) => ({ ...s, phase: 'error', error: STR.liveUnavailable }))
|
|
2132
|
+
return
|
|
2133
|
+
}
|
|
2134
|
+
setState((s) => ({ ...s, phase: s.timeline === null ? 'loading' : 'ready' }))
|
|
2135
|
+
try {
|
|
2136
|
+
const resp = await connection.api.sessions.history({ sessionId, mode: 'full', maxMessages: 400 })
|
|
2137
|
+
const val = resp && resp.result && resp.result.value
|
|
2138
|
+
if (val === undefined || !Array.isArray(val.events)) {
|
|
2139
|
+
setState((s) => ({ ...s, phase: 'error', error: STR.liveFail }))
|
|
2140
|
+
return
|
|
2141
|
+
}
|
|
2142
|
+
const events = val.events.map((entry) => (entry && entry.event) || entry)
|
|
2143
|
+
const derived = await api('derive', { events })
|
|
2144
|
+
if (!derived || !derived.ok) {
|
|
2145
|
+
setState((s) => ({ ...s, phase: 'error', error: (derived && derived.error) || STR.liveFail }))
|
|
2146
|
+
return
|
|
2147
|
+
}
|
|
2148
|
+
const tl = (derived.timeline && derived.timeline.turns) || []
|
|
2149
|
+
setState({
|
|
2150
|
+
phase: 'ready', error: null, timeline: tl, eventCount: events.length,
|
|
2151
|
+
running: derived.running === true,
|
|
2152
|
+
})
|
|
2153
|
+
// 只折叠新出现的回合(默认折叠),已展开的保持用户状态(M6 经验)。
|
|
2154
|
+
const newTurns = tl.filter((t) => !liveSeenTurns.current.has(t.turn))
|
|
2155
|
+
for (const t of tl) liveSeenTurns.current.add(t.turn)
|
|
2156
|
+
if (newTurns.length > 0) {
|
|
2157
|
+
setCollapsed((prev) => {
|
|
2158
|
+
const next = new Set(prev)
|
|
2159
|
+
for (const t of newTurns) next.add('turn-' + t.turn)
|
|
2160
|
+
return next
|
|
2161
|
+
})
|
|
2162
|
+
}
|
|
2163
|
+
} catch (e) {
|
|
2164
|
+
setState((s) => ({ ...s, phase: 'error', error: STR.liveFail + ': ' + String(e && e.message || e) }))
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
useEffect(() => {
|
|
2169
|
+
liveSeenTurns.current.clear()
|
|
2170
|
+
fetchLive.current()
|
|
2171
|
+
if (timer.current === null) timer.current = setInterval(() => { fetchLive.current() }, 3000)
|
|
2172
|
+
return () => { if (timer.current !== null) { clearInterval(timer.current); timer.current = null } }
|
|
2173
|
+
}, [sessionId])
|
|
2174
|
+
|
|
2175
|
+
const toggle = (key) => {
|
|
2176
|
+
setCollapsed((prev) => {
|
|
2177
|
+
const next = new Set(prev)
|
|
2178
|
+
if (next.has(key)) next.delete(key)
|
|
2179
|
+
else next.add(key)
|
|
2180
|
+
return next
|
|
2181
|
+
})
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
return h('div', { className: 'sf-view' },
|
|
2185
|
+
h('div', { className: 'sf-viewHeader' },
|
|
2186
|
+
h('span', { className: 'sf-badge sf-badgeRun', style: { flex: 'none' } }, STR.liveActive),
|
|
2187
|
+
h('span', { className: 'sf-liveText' }, STR.liveFallbackHint),
|
|
2188
|
+
h('button', { className: 'sf-btn sf-liveExitBtn', onClick: props.onRetry }, STR.retryArchive),
|
|
2189
|
+
),
|
|
2190
|
+
state.phase === 'loading' && h('div', { className: 'sf-hint' }, STR.liveLoading),
|
|
2191
|
+
state.phase === 'error' && h('div', { className: 'sf-hint', style: { color: '#d43b3b' } }, state.error),
|
|
2192
|
+
state.phase === 'ready' && state.timeline !== null && state.timeline.length === 0 && h('div', { className: 'sf-hint' }, STR.noTimeline),
|
|
2193
|
+
state.phase === 'ready' && state.timeline !== null && state.timeline.length > 0 && h(TimelineTurns, {
|
|
2194
|
+
// 不传 hideHead:折叠回合需保留展开入口(M6 经验)。
|
|
2195
|
+
turns: state.timeline, collapsed, toggle,
|
|
2196
|
+
liveActive: state.running === true,
|
|
2197
|
+
activeTurn, setActiveTurn,
|
|
2198
|
+
}),
|
|
2199
|
+
)
|
|
2200
|
+
}
|
|
2201
|
+
|
|
1654
2202
|
// ── 主视图(总览 ⇄ 详情)───────────────────────────────────────────
|
|
1655
2203
|
function SessionFlowView(props) {
|
|
1656
2204
|
const [detail, setDetail] = useState(null) // { session, searchQuery }
|
|
@@ -1660,6 +2208,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1660
2208
|
// M5c 方案C:总览检索词随会话带入详情页,自动执行会话内检索。
|
|
1661
2209
|
onOpen: (session, searchQuery) => setDetail({ session, searchQuery }),
|
|
1662
2210
|
onClose: props.onClose,
|
|
2211
|
+
sessions: props.sessions,
|
|
1663
2212
|
})
|
|
1664
2213
|
: h(SessionFlowDetail, {
|
|
1665
2214
|
session: detail.session,
|
|
@@ -1671,9 +2220,522 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1671
2220
|
})
|
|
1672
2221
|
}
|
|
1673
2222
|
|
|
2223
|
+
// ── M11:钢琴键会话快切(会话页左侧常驻竖条)─────────────────────
|
|
2224
|
+
// 数据:ctx.sessions.list(ObservableSnapshot 订阅)→ 过滤(排除 subagent、隐藏 blank)
|
|
2225
|
+
// → 排序(updatedAt 降序,收藏置顶)→ 分组(连续 cwd 同段,段间黑白交替)。
|
|
2226
|
+
// 点击 sessions.open(id) 切换;会话切换由框架重挂载会话组件(M7 已验证)。
|
|
2227
|
+
// 错误角标:挂载时拉一次 host list 索引(toolErrors 映射)。
|
|
2228
|
+
// 预览卡:hover 200ms 防抖 → host get 单个会话(缓存秒回)→ fixed 浮层(不裁剪)。
|
|
2229
|
+
// 交互:滚轮/箭头滚动窗口(默认 15 键,兼容大量会话)、键盘 ↑↓/Enter、
|
|
2230
|
+
// 收藏星标(localStorage)、右键「在会话流中打开」(工作台唤起)。
|
|
2231
|
+
const PIANO_FAVS_KEY = 'dsh.sessionFlow.pianoFavs' // 预留(收藏键 v2 迁移用;当前轮次导航不使用)
|
|
2232
|
+
const PENDING_SEARCH_KEY = 'dsh.sessionFlow.pendingSearch' // 方向 A:全文命中跳转桥
|
|
2233
|
+
// M12:并入右栏状态桥——挂 window 全局共享(跨 factory 实例/热重载稳定;
|
|
2234
|
+
// 曾用模块闭包 const dockState,热重载后新 apply 闭包访问不到 → ReferenceError 踩坑)。
|
|
2235
|
+
const dockBridge = (typeof window !== 'undefined' && window.__dshSessionFlowDock__) ||
|
|
2236
|
+
(typeof window !== 'undefined'
|
|
2237
|
+
? (window.__dshSessionFlowDock__ = { open: false, listeners: new Set(), controller: null })
|
|
2238
|
+
: { open: false, listeners: new Set(), controller: null })
|
|
2239
|
+
const PIANO_WINDOW = 15
|
|
2240
|
+
|
|
2241
|
+
// 定位目标:会话内容区。注意必须选**滚动容器** scrollBody(data-conversation-scroll):
|
|
2242
|
+
// viewArea 在滚动场景下 rect 是「内容全尺寸」(如 y=-25687, h=26823),用它定位会把
|
|
2243
|
+
// 竖条甩到视口外(实测踩坑)。scrollBody 才有正确的视口内矩形。
|
|
2244
|
+
// 优先级:scrollBody(滚动容器)→ viewArea(仅无 scrollBody 时兜底)。
|
|
2245
|
+
function pianoViewArea() {
|
|
2246
|
+
const col = document.querySelector(CONVERSATION_SELECTOR)
|
|
2247
|
+
if (col === null) return undefined
|
|
2248
|
+
const body = col.querySelector('[data-conversation-scroll]')
|
|
2249
|
+
if (body instanceof Element) return body
|
|
2250
|
+
const area = col.querySelector('[class*=viewArea]')
|
|
2251
|
+
if (area instanceof Element) return area
|
|
2252
|
+
return undefined
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
// ── M11:本会话轮次导航(会话页左侧常驻悬浮条目条,v3)──────────
|
|
2256
|
+
// 范围:只显示【当前会话】的对话轮次(用户消息),不跨会话/不跨工作区。
|
|
2257
|
+
// 数据:官方消息行锚点 DOM 驱动(data-time-hover-root,dsh-navbar 同款机制,
|
|
2258
|
+
// 零 RPC 实时):用户行 = 无 data-turn-tail + 含 [class*=bubble]。
|
|
2259
|
+
// 轮次 → 结论预览:按顺序索引映射 host get 的 lightTurns(用户消息摊平)。
|
|
2260
|
+
// 交互:点击滚动定位该轮、active 跟随视口第一条、窗口裁剪 + 滚轮/▲▼、
|
|
2261
|
+
// 键盘 ↑↓/Enter、hover 预览卡(该轮用户消息 + 结论)、右键唤起会话流工作台。
|
|
2262
|
+
// 样式:DeepSeek 网页版右侧悬浮消息导航风格(圆角卡片 + 单行截断文字),移到左侧。
|
|
2263
|
+
function TurnKeys(props) {
|
|
2264
|
+
const { sessions, onOpenWorkbench } = props
|
|
2265
|
+
const [entries, setEntries] = useState([]) // [{turn, seq, preview}] host 全量用户轮次
|
|
2266
|
+
const [activeIdx, setActiveIdx] = useState(-1)
|
|
2267
|
+
const [winOffset, setWinOffset] = useState(0)
|
|
2268
|
+
const [kbGlobal, setKbGlobal] = useState(null)
|
|
2269
|
+
const [detailIdx, setDetailIdx] = useState(null) // 右侧信息面板显示的条目索引
|
|
2270
|
+
const detailTimer = useRef(null)
|
|
2271
|
+
const rootRef = useRef(null) // 悬浮条根容器(面板定位换算基准)
|
|
2272
|
+
const [jumpFail, setJumpFail] = useState(false) // 未加载轮次提示
|
|
2273
|
+
const [, force] = useState(0)
|
|
2274
|
+
|
|
2275
|
+
// 定位自管理:垂直居中悬浮于会话内容区(scrollBody 矩形中心 + translateY(-50%))。
|
|
2276
|
+
const [pos, setPos] = useState(null) // {left, top(区中心)}
|
|
2277
|
+
useEffect(() => {
|
|
2278
|
+
const compute = () => {
|
|
2279
|
+
const area = pianoViewArea()
|
|
2280
|
+
if (area === undefined) { setPos(null); return }
|
|
2281
|
+
const r = area.getBoundingClientRect()
|
|
2282
|
+
if (r.width === 0 && r.height === 0) return
|
|
2283
|
+
const seat = area.querySelector('[data-composer-seat]')
|
|
2284
|
+
const seatH = seat ? seat.getBoundingClientRect().height : 0
|
|
2285
|
+
setPos({ left: r.left, top: r.top + Math.max(0, r.height - seatH) / 2 })
|
|
2286
|
+
}
|
|
2287
|
+
compute()
|
|
2288
|
+
window.addEventListener('resize', compute)
|
|
2289
|
+
const iv = setInterval(compute, 2000)
|
|
2290
|
+
return () => { window.removeEventListener('resize', compute); clearInterval(iv) }
|
|
2291
|
+
}, [])
|
|
2292
|
+
|
|
2293
|
+
// 订阅会话列表变化:切换会话(current 变化)→ 重渲染刷新条目。
|
|
2294
|
+
useEffect(() => {
|
|
2295
|
+
if (sessions && sessions.list && typeof sessions.list.subscribe === 'function') {
|
|
2296
|
+
return sessions.list.subscribe(() => force((n) => n + 1))
|
|
2297
|
+
}
|
|
2298
|
+
return undefined
|
|
2299
|
+
}, [sessions])
|
|
2300
|
+
|
|
2301
|
+
// 默认定位到【最新一轮】:首次拿到条目后窗口初始 offset 指向末尾
|
|
2302
|
+
// (此前默认从最旧开始,看最新轮次需翻页——已修正)。之后用户手动
|
|
2303
|
+
// 翻页/新消息到达都不再重置(不打断用户当前窗口位置)。
|
|
2304
|
+
const initTailDone = useRef(false)
|
|
2305
|
+
useEffect(() => {
|
|
2306
|
+
if (initTailDone.current || entries.length <= WINDOW) return
|
|
2307
|
+
initTailDone.current = true
|
|
2308
|
+
setWinOffset(entries.length - WINDOW)
|
|
2309
|
+
}, [entries])
|
|
2310
|
+
|
|
2311
|
+
// 当前会话 id(仅取 current,不展示跨会话列表)。
|
|
2312
|
+
const currentId = (() => {
|
|
2313
|
+
try {
|
|
2314
|
+
const s = sessions && sessions.list && typeof sessions.list.getSnapshot === 'function' ? sessions.list.getSnapshot() : null
|
|
2315
|
+
return s && s.current ? s.current : null
|
|
2316
|
+
} catch (e) { return null }
|
|
2317
|
+
})()
|
|
2318
|
+
|
|
2319
|
+
// 条目数据:host get lightTurns 全量用户轮次(含往期,不受 DOM 虚拟列表窗口限制)。
|
|
2320
|
+
// 轮询降载(PERF-ANALYSIS §4.1):大会话 get 每轮 ~383ms(缓存 JSON.parse)——
|
|
2321
|
+
// 观察消息流 DOM 变化(新消息/流式渲染)→ 立即刷新并切高频(3s);
|
|
2322
|
+
// 空闲时低频兜底(15s),避免常驻高频轮询空转。
|
|
2323
|
+
useEffect(() => {
|
|
2324
|
+
if (!currentId) { setEntries([]); return undefined }
|
|
2325
|
+
let alive = true
|
|
2326
|
+
// 切换会话后重置「已定位」标记,下次加载重新定位到新会话末尾。
|
|
2327
|
+
initTailDone.current = false
|
|
2328
|
+
const refresh = () => {
|
|
2329
|
+
api('get', { sessionId: currentId }).then((json) => {
|
|
2330
|
+
if (!alive || !json || !json.ok) return
|
|
2331
|
+
const flat = []
|
|
2332
|
+
for (const lt of json.lightTurns || []) {
|
|
2333
|
+
for (const u of lt.userMessages || []) {
|
|
2334
|
+
flat.push({ turn: lt.turn, seq: u.seq, preview: u.preview })
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
// 防御:同一 seq 只保留一条(避免同一条消息重复渲染 = 「最后一条显示两次」)。
|
|
2338
|
+
const seen = new Set()
|
|
2339
|
+
const dedup = flat.filter((e) => {
|
|
2340
|
+
if (seen.has(e.seq)) return false
|
|
2341
|
+
seen.add(e.seq)
|
|
2342
|
+
return true
|
|
2343
|
+
})
|
|
2344
|
+
setEntries((prev) => {
|
|
2345
|
+
if (prev.length === dedup.length && prev.length > 0 && prev[prev.length - 1].seq === dedup[dedup.length - 1].seq) return prev
|
|
2346
|
+
return dedup
|
|
2347
|
+
})
|
|
2348
|
+
}).catch(() => {})
|
|
2349
|
+
}
|
|
2350
|
+
const ACTIVE_MS = 3000
|
|
2351
|
+
const IDLE_MS = 15000
|
|
2352
|
+
let iv = null
|
|
2353
|
+
const schedule = (delay) => {
|
|
2354
|
+
if (iv) clearInterval(iv)
|
|
2355
|
+
iv = setInterval(refresh, delay)
|
|
2356
|
+
}
|
|
2357
|
+
// DOM 变化(新消息/流式渲染):立即刷新 + 切高频;1s 节流防虚拟列表滚动刷屏。
|
|
2358
|
+
let flowObs = null
|
|
2359
|
+
let lastDomRefresh = 0
|
|
2360
|
+
const onDomChange = () => {
|
|
2361
|
+
if (!alive) return
|
|
2362
|
+
const now = Date.now()
|
|
2363
|
+
if (now - lastDomRefresh < 1000) return
|
|
2364
|
+
lastDomRefresh = now
|
|
2365
|
+
refresh()
|
|
2366
|
+
schedule(ACTIVE_MS)
|
|
2367
|
+
}
|
|
2368
|
+
const flowEl = document.querySelector('[data-chat-flow=""]') || document.querySelector('[data-focus-flow=""]')
|
|
2369
|
+
if (flowEl) {
|
|
2370
|
+
flowObs = new MutationObserver(onDomChange)
|
|
2371
|
+
flowObs.observe(flowEl, { childList: true, subtree: true })
|
|
2372
|
+
}
|
|
2373
|
+
refresh()
|
|
2374
|
+
schedule(IDLE_MS)
|
|
2375
|
+
return () => { alive = false; if (iv) clearInterval(iv); if (flowObs) flowObs.disconnect() }
|
|
2376
|
+
}, [currentId])
|
|
2377
|
+
|
|
2378
|
+
// DOM 行工具:官方消息行锚点(dsh-navbar 同款)。
|
|
2379
|
+
const domRows = () => [...document.querySelectorAll('[data-time-hover-root]')]
|
|
2380
|
+
.filter((r) => !r.hasAttribute('data-pending-steering'))
|
|
2381
|
+
.filter((r) => !r.hasAttribute('data-turn-tail') && r.querySelector('[class*=bubble]') !== null)
|
|
2382
|
+
const textOf = (row) => {
|
|
2383
|
+
const b = row.querySelector('[class*=bubble]')
|
|
2384
|
+
return b ? String(b.textContent || '').replace(/\s+/g, ' ').trim() : ''
|
|
2385
|
+
}
|
|
2386
|
+
// 归一化:剥离 markdown 标记(**加粗**/行内代码/链接/符号),使 host 原始
|
|
2387
|
+
// preview 与 DOM 渲染后的 textContent 可比(否则 startsWith 永远失败 →
|
|
2388
|
+
// 近轮次误判「未找到」+ 加载循环误匹配其他轮次,实测踩坑)。
|
|
2389
|
+
const norm = (s) => String(s || '')
|
|
2390
|
+
.replace(/`{1,3}[^`]*`{1,3}/g, ' ')
|
|
2391
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
2392
|
+
.replace(/[*_#>|~-]{1,3}/g, ' ')
|
|
2393
|
+
.replace(/\s+/g, ' ')
|
|
2394
|
+
.trim()
|
|
2395
|
+
// 条目 → DOM 行匹配:preview 归一化截断前缀与行文本归一化后 includes 比较。
|
|
2396
|
+
const findRow = (entry) => {
|
|
2397
|
+
const core = norm(String(entry.preview || '').replace(/…+$/u, '')).slice(0, 50)
|
|
2398
|
+
if (!core) return null // 无文字内容(纯图片/文件消息)无法文本定位
|
|
2399
|
+
for (const row of domRows()) {
|
|
2400
|
+
const t = norm(textOf(row))
|
|
2401
|
+
if (t && t.includes(core.slice(0, 30))) return row
|
|
2402
|
+
}
|
|
2403
|
+
return null
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
// 点击定位:先查该轮次是否已加载(DOM 中能否匹配到对应行)——
|
|
2407
|
+
// 已加载 → 平滑滚动定位;未加载 → 只提示、不跳转(自动加载循环会让消息流
|
|
2408
|
+
// 乱滚/跳错位置,实测踩坑;用户需先在会话区向上滚动加载历史再点击)。
|
|
2409
|
+
const open = (entry) => {
|
|
2410
|
+
const row = findRow(entry)
|
|
2411
|
+
if (row) { row.scrollIntoView({ behavior: 'smooth', block: 'start' }); return }
|
|
2412
|
+
setJumpFail(true)
|
|
2413
|
+
setTimeout(() => setJumpFail(false), 2600)
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
// active:视口内第一条 DOM 用户行 → 文本匹配条目索引高亮(阅读位置跟随)。
|
|
2417
|
+
useEffect(() => {
|
|
2418
|
+
const compute = () => {
|
|
2419
|
+
if (entries.length === 0) { setActiveIdx(-1); return }
|
|
2420
|
+
const rows = domRows()
|
|
2421
|
+
let target = null
|
|
2422
|
+
let bestTop = Number.POSITIVE_INFINITY
|
|
2423
|
+
for (const row of rows) {
|
|
2424
|
+
const top = row.getBoundingClientRect().top
|
|
2425
|
+
if (top >= 0 && top < bestTop) { bestTop = top; target = row }
|
|
2426
|
+
}
|
|
2427
|
+
if (!target) { setActiveIdx(entries.length - 1); return }
|
|
2428
|
+
const t = norm(textOf(target))
|
|
2429
|
+
const idx = entries.findIndex((e) => {
|
|
2430
|
+
const core = norm(String(e.preview || '').replace(/…+$/u, '')).slice(0, 50)
|
|
2431
|
+
return core && t.includes(core.slice(0, 30))
|
|
2432
|
+
})
|
|
2433
|
+
setActiveIdx(idx)
|
|
2434
|
+
}
|
|
2435
|
+
compute()
|
|
2436
|
+
const iv = setInterval(compute, 800)
|
|
2437
|
+
return () => clearInterval(iv)
|
|
2438
|
+
}, [entries])
|
|
2439
|
+
|
|
2440
|
+
const WINDOW = 12
|
|
2441
|
+
const hidden = entries.length < 2
|
|
2442
|
+
const windowed = entries.slice(winOffset, winOffset + WINDOW)
|
|
2443
|
+
const canUp = winOffset > 0
|
|
2444
|
+
const canDown = winOffset + WINDOW < entries.length
|
|
2445
|
+
|
|
2446
|
+
// 悬浮条滚轮/▲▼:只翻悬浮条自身窗口,绝不影响会话区滚动(用户明确要求)。
|
|
2447
|
+
const onKeyDown = (e) => {
|
|
2448
|
+
if (entries.length === 0) return
|
|
2449
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
2450
|
+
e.preventDefault()
|
|
2451
|
+
const dir = e.key === 'ArrowDown' ? 1 : -1
|
|
2452
|
+
setKbGlobal((prev) => {
|
|
2453
|
+
const base = prev === null ? (dir > 0 ? -1 : entries.length) : prev
|
|
2454
|
+
return Math.max(0, Math.min(entries.length - 1, base + dir))
|
|
2455
|
+
})
|
|
2456
|
+
} else if (e.key === 'Enter' && kbGlobal !== null && entries[kbGlobal]) {
|
|
2457
|
+
e.preventDefault()
|
|
2458
|
+
open(entries[kbGlobal])
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
useEffect(() => {
|
|
2462
|
+
if (kbGlobal === null) return
|
|
2463
|
+
if (kbGlobal < winOffset) setWinOffset(kbGlobal)
|
|
2464
|
+
else if (kbGlobal >= winOffset + WINDOW) setWinOffset(kbGlobal - WINDOW + 1)
|
|
2465
|
+
}, [kbGlobal])
|
|
2466
|
+
|
|
2467
|
+
// 滚轮翻页:React onWheel 是被动监听器(passive: true),内部 preventDefault
|
|
2468
|
+
// 无效并刷屏告警(实测踩坑)——改原生 addEventListener('wheel', {passive:false})。
|
|
2469
|
+
const stripRef = useRef(null)
|
|
2470
|
+
useEffect(() => {
|
|
2471
|
+
const el = stripRef.current
|
|
2472
|
+
if (el === null) return undefined
|
|
2473
|
+
const onNativeWheel = (e) => {
|
|
2474
|
+
e.preventDefault()
|
|
2475
|
+
e.stopPropagation()
|
|
2476
|
+
if (e.deltaY > 0) setWinOffset((o) => Math.min(Math.max(0, entries.length - WINDOW), o + 1))
|
|
2477
|
+
else setWinOffset((o) => Math.max(0, o - 1))
|
|
2478
|
+
}
|
|
2479
|
+
el.addEventListener('wheel', onNativeWheel, { passive: false })
|
|
2480
|
+
return () => el.removeEventListener('wheel', onNativeWheel)
|
|
2481
|
+
}, [entries.length])
|
|
2482
|
+
|
|
2483
|
+
// 右侧信息面板:hover 条目/键盘聚焦时弹出该轮信息(保留弹出面板、移除原生
|
|
2484
|
+
// tooltip——tooltip 与面板重复且视觉凌乱);鼠标在条目与面板间移动 150ms 宽限。
|
|
2485
|
+
const showDetail = (idx) => {
|
|
2486
|
+
if (detailTimer.current) clearTimeout(detailTimer.current)
|
|
2487
|
+
setDetailIdx(idx)
|
|
2488
|
+
}
|
|
2489
|
+
const hideDetail = () => {
|
|
2490
|
+
if (detailTimer.current) clearTimeout(detailTimer.current)
|
|
2491
|
+
detailTimer.current = setTimeout(() => setDetailIdx(null), 150)
|
|
2492
|
+
}
|
|
2493
|
+
useEffect(() => () => { if (detailTimer.current) clearTimeout(detailTimer.current) }, [])
|
|
2494
|
+
// 键盘聚焦跟随显示面板。
|
|
2495
|
+
useEffect(() => {
|
|
2496
|
+
if (kbGlobal !== null && entries[kbGlobal]) showDetail(kbGlobal)
|
|
2497
|
+
}, [kbGlobal])
|
|
2498
|
+
|
|
2499
|
+
const rootStyle = pos === null ? { display: 'none' } : {
|
|
2500
|
+
left: pos.left + 'px',
|
|
2501
|
+
top: pos.top + 'px',
|
|
2502
|
+
transform: 'translateY(-50%)',
|
|
2503
|
+
display: hidden ? 'none' : undefined,
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
const detailEntry = detailIdx !== null ? entries[detailIdx] : null
|
|
2507
|
+
// 面板位置:absolute 相对悬浮条容器(容器有 translateY(-50%) transform,fixed
|
|
2508
|
+
// 会退化为相对容器——必须按容器内偏移计算)。条目 = strip 直接子元素,
|
|
2509
|
+
// 索引 = detailIdx - winOffset;越界修正按视口坐标换算回容器内。
|
|
2510
|
+
let detailPos = null
|
|
2511
|
+
const rootRect = rootRef.current ? rootRef.current.getBoundingClientRect() : null
|
|
2512
|
+
if (detailEntry && rootRect) {
|
|
2513
|
+
const el = stripRef.current ? stripRef.current.children[detailIdx - winOffset] : null
|
|
2514
|
+
if (el instanceof Element) {
|
|
2515
|
+
const r = el.getBoundingClientRect()
|
|
2516
|
+
const topV = Math.min(r.top, Math.max(4, window.innerHeight - 320))
|
|
2517
|
+
detailPos = { left: r.right - rootRect.left + 8, top: topV - rootRect.top }
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
return h('div', { 'data-dsh-piano-keys': '', ref: rootRef, style: rootStyle },
|
|
2522
|
+
h('div', { className: 'pk-head' },
|
|
2523
|
+
h('span', { className: 'pk-headLabel' }, STR.pianoTitle),
|
|
2524
|
+
h('div', { className: 'pk-navGroup' },
|
|
2525
|
+
h('button', { className: 'pk-navBtn', disabled: !canUp, onClick: () => setWinOffset((o) => Math.max(0, o - 1)), title: STR.expanded }, '▲'),
|
|
2526
|
+
h('button', { className: 'pk-navBtn', disabled: !canDown, onClick: () => setWinOffset((o) => Math.min(Math.max(0, entries.length - WINDOW), o + 1)), title: STR.collapsed }, '▼'),
|
|
2527
|
+
),
|
|
2528
|
+
),
|
|
2529
|
+
h('div', { className: 'pk-strip', ref: stripRef, tabIndex: 0, onKeyDown, onMouseDown: (e) => { if (e.target === e.currentTarget) e.preventDefault() }, 'aria-label': STR.pianoTitle },
|
|
2530
|
+
windowed.map((entry, idx) => {
|
|
2531
|
+
const gi = winOffset + idx
|
|
2532
|
+
const act = activeIdx === gi
|
|
2533
|
+
return h('button', {
|
|
2534
|
+
key: 'turn-' + gi + '-' + entry.seq,
|
|
2535
|
+
type: 'button',
|
|
2536
|
+
className: 'pk-key' + (act ? ' pk-keyActive' : '') + (kbGlobal === gi ? ' pk-keyFocus' : ''),
|
|
2537
|
+
// 点击后主动失焦:避免 button 焦点让 :focus-within 常驻展开
|
|
2538
|
+
// (鼠标移开后悬浮条收不拢,箭头「残留」——实测踩坑)。
|
|
2539
|
+
onClick: (e) => { open(entry); e.currentTarget.blur() },
|
|
2540
|
+
onMouseEnter: () => showDetail(gi),
|
|
2541
|
+
onMouseLeave: hideDetail,
|
|
2542
|
+
onContextMenu: (e) => {
|
|
2543
|
+
e.preventDefault()
|
|
2544
|
+
if (onOpenWorkbench) onOpenWorkbench()
|
|
2545
|
+
},
|
|
2546
|
+
},
|
|
2547
|
+
// 轮次序号:展开态常驻【最左侧】独立模块(与文本模块分离,互不侵占)。
|
|
2548
|
+
h('span', { className: 'pk-turnNo' }, 'T' + entry.turn),
|
|
2549
|
+
h('span', { className: 'pk-label' }, entry.preview || '(空)'),
|
|
2550
|
+
)
|
|
2551
|
+
}),
|
|
2552
|
+
),
|
|
2553
|
+
// 信息面板:跟随 hover 条目右侧展开(fixed,位置由 detailPos 控制);
|
|
2554
|
+
// 常驻渲染 + hidden 类做滑入/滑出;内容 key 变化触发 pkSlideIn。
|
|
2555
|
+
h('div', { className: 'pk-detail' + (detailEntry ? '' : ' hidden'), style: detailPos, onMouseEnter: () => showDetail(detailIdx), onMouseLeave: hideDetail },
|
|
2556
|
+
detailEntry && h('div', { key: 'h' + detailEntry.turn, className: 'pk-detailHead' }, 'T' + detailEntry.turn + ' · ' + STR.user),
|
|
2557
|
+
detailEntry && h('div', { key: 'b' + detailEntry.turn, className: 'pk-detailBody' }, detailEntry.preview || '(空)'),
|
|
2558
|
+
),
|
|
2559
|
+
// 跳转状态提示:常驻渲染 + hidden 类(从下方淡入/淡出);未加载轮次提示。
|
|
2560
|
+
h('div', { className: 'pk-status' + (jumpFail ? '' : ' hidden') + (jumpFail ? ' warn' : '') },
|
|
2561
|
+
jumpFail ? STR.pianoJumpFail : '',
|
|
2562
|
+
),
|
|
2563
|
+
)
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
// ── M12:详情并入右侧栏(details 槽位轻量视图)────────────────────
|
|
2567
|
+
// 随会话自动切换(槽位 session 作用域 key 重挂载);数据复用 host get/getTurn;
|
|
2568
|
+
// 回合列表直接复用 TurnList(展开 → TimelineTurns 完整渲染)。
|
|
2569
|
+
function DetailsDockView(props) {
|
|
2570
|
+
const { sessionId, connection, onExit } = props
|
|
2571
|
+
const [state, setState] = useState({ phase: 'loading', error: null, data: null })
|
|
2572
|
+
const [collapsed, setCollapsed] = useState(() => new Set())
|
|
2573
|
+
const collapsedInit = useRef(false)
|
|
2574
|
+
const [turnItems, setTurnItems] = useState(() => new Map())
|
|
2575
|
+
const loadingTurns = useRef(new Set())
|
|
2576
|
+
|
|
2577
|
+
useEffect(() => {
|
|
2578
|
+
let alive = true
|
|
2579
|
+
setState({ phase: 'loading', error: null, data: null })
|
|
2580
|
+
setTurnItems(new Map())
|
|
2581
|
+
collapsedInit.current = false
|
|
2582
|
+
if (!sessionId) { setState({ phase: 'empty', error: null, data: null }); return }
|
|
2583
|
+
api('get', { sessionId }).then((json) => {
|
|
2584
|
+
if (!alive) return
|
|
2585
|
+
if (json && json.ok) {
|
|
2586
|
+
setState({ phase: 'ready', error: null, data: json })
|
|
2587
|
+
} else {
|
|
2588
|
+
setState({ phase: 'error', error: (json && json.error) || 'get failed', data: null })
|
|
2589
|
+
}
|
|
2590
|
+
}).catch((e) => { if (alive) setState({ phase: 'error', error: String(e), data: null }) })
|
|
2591
|
+
return () => { alive = false }
|
|
2592
|
+
}, [sessionId])
|
|
2593
|
+
|
|
2594
|
+
useEffect(() => {
|
|
2595
|
+
if (state.phase !== 'ready' || collapsedInit.current) return
|
|
2596
|
+
collapsedInit.current = true
|
|
2597
|
+
const all = new Set()
|
|
2598
|
+
for (const lt of state.data.lightTurns || []) all.add('turn-' + lt.turn)
|
|
2599
|
+
setCollapsed(all)
|
|
2600
|
+
}, [state.phase])
|
|
2601
|
+
|
|
2602
|
+
const toggle = (key) => {
|
|
2603
|
+
setCollapsed((prev) => {
|
|
2604
|
+
const next = new Set(prev)
|
|
2605
|
+
if (next.has(key)) next.delete(key)
|
|
2606
|
+
else next.add(key)
|
|
2607
|
+
return next
|
|
2608
|
+
})
|
|
2609
|
+
}
|
|
2610
|
+
const ensureTurn = async (turnNo) => {
|
|
2611
|
+
if (turnItems.has(turnNo) || loadingTurns.current.has(turnNo)) return
|
|
2612
|
+
loadingTurns.current.add(turnNo)
|
|
2613
|
+
try {
|
|
2614
|
+
const json = await api('getTurn', { sessionId, turn: turnNo })
|
|
2615
|
+
if (json && json.ok && json.turn) {
|
|
2616
|
+
setTurnItems((prev) => new Map(prev).set(turnNo, json.turn))
|
|
2617
|
+
}
|
|
2618
|
+
} catch (e) {
|
|
2619
|
+
console.warn('[dsh-session-flow] getTurn failed', turnNo, e)
|
|
2620
|
+
} finally {
|
|
2621
|
+
loadingTurns.current.delete(turnNo)
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
const data = state.data
|
|
2626
|
+
const lightTurns = (data && data.lightTurns) || []
|
|
2627
|
+
const toolStats = (data && data.toolStats) || []
|
|
2628
|
+
// 规则摘要(与详情页同口径,精简版)。
|
|
2629
|
+
const ruleTaskCount = lightTurns.filter((lt) => lt.userMessages && lt.userMessages.length > 0).length
|
|
2630
|
+
const ruleGoal = (() => {
|
|
2631
|
+
for (const lt of lightTurns) {
|
|
2632
|
+
if (lt.userMessages && lt.userMessages.length > 0) return String(lt.userMessages[0].preview).slice(0, 150)
|
|
2633
|
+
}
|
|
2634
|
+
return ''
|
|
2635
|
+
})()
|
|
2636
|
+
const ruleConclusion = (() => {
|
|
2637
|
+
for (let i = lightTurns.length - 1; i >= 0; i--) {
|
|
2638
|
+
if (lightTurns[i].conclusionPreview) return String(lightTurns[i].conclusionPreview).slice(0, 220)
|
|
2639
|
+
}
|
|
2640
|
+
return ''
|
|
2641
|
+
})()
|
|
2642
|
+
const ruleTools = toolStats.slice(0, 6).map((t) => t.name + '·' + t.count).join(' ')
|
|
2643
|
+
|
|
2644
|
+
return h('div', { className: 'dk-root' },
|
|
2645
|
+
h('div', { className: 'dk-head' },
|
|
2646
|
+
h('span', { className: 'dk-title' }, STR.dockHint),
|
|
2647
|
+
h('button', { className: 'dk-btn', onClick: onExit }, STR.dockExit),
|
|
2648
|
+
),
|
|
2649
|
+
h('div', { className: 'dk-body' },
|
|
2650
|
+
state.phase === 'loading' && h('div', { className: 'sf-hint' }, STR.detailLoading),
|
|
2651
|
+
state.phase === 'empty' && h('div', { className: 'sf-hint' }, STR.dockEmpty),
|
|
2652
|
+
state.phase === 'error' && h('div', { className: 'sf-hint', style: { color: '#d43b3b' } }, STR.detailFailed + ': ' + String(state.error)),
|
|
2653
|
+
state.phase === 'ready' && h('div', { className: 'dk-summary' },
|
|
2654
|
+
ruleTaskCount > 0 && h('div', { className: 'dk-summaryRow' },
|
|
2655
|
+
h('span', { className: 'dk-summaryTag' }, STR.summaryTaskCount),
|
|
2656
|
+
h('span', { className: 'dk-summaryText' }, ruleTaskCount),
|
|
2657
|
+
),
|
|
2658
|
+
ruleGoal && h('div', { className: 'dk-summaryRow' },
|
|
2659
|
+
h('span', { className: 'dk-summaryTag' }, STR.summaryGoal),
|
|
2660
|
+
h('span', { className: 'dk-summaryText' }, ruleGoal),
|
|
2661
|
+
),
|
|
2662
|
+
ruleConclusion && h('div', { className: 'dk-summaryRow' },
|
|
2663
|
+
h('span', { className: 'dk-summaryTag' }, STR.summaryConclusion),
|
|
2664
|
+
h('span', { className: 'dk-summaryText' }, ruleConclusion),
|
|
2665
|
+
),
|
|
2666
|
+
ruleTools && h('div', { className: 'dk-summaryRow' },
|
|
2667
|
+
h('span', { className: 'dk-summaryTag' }, STR.summaryTools),
|
|
2668
|
+
h('span', { className: 'dk-summaryText' }, ruleTools),
|
|
2669
|
+
),
|
|
2670
|
+
data.summary && h('div', { className: 'dk-summaryFull' }, data.summary),
|
|
2671
|
+
),
|
|
2672
|
+
state.phase === 'ready' && lightTurns.length === 0 && h('div', { className: 'sf-hint' }, STR.noTimeline),
|
|
2673
|
+
state.phase === 'ready' && h(TurnList, {
|
|
2674
|
+
sessionId,
|
|
2675
|
+
lightTurns,
|
|
2676
|
+
collapsed,
|
|
2677
|
+
toggle,
|
|
2678
|
+
ensureTurn,
|
|
2679
|
+
turnItems,
|
|
2680
|
+
}),
|
|
2681
|
+
),
|
|
2682
|
+
)
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
// ── M11:钢琴键竖条挂载(DOM 注入 + 自愈)────────────────────────
|
|
2686
|
+
// 容器挂 document.body(dsh-navbar 同款模式):完全脱离会话区布局树,
|
|
2687
|
+
// 不干扰 conversation 的 flex/grid 布局;fixed 定位由组件自管理。
|
|
2688
|
+
// MutationObserver 自愈:容器丢失(布局重建)时自动恢复。
|
|
2689
|
+
function mountPianoKeys(controller, sessions, connection) {
|
|
2690
|
+
let root = undefined
|
|
2691
|
+
let container = undefined
|
|
2692
|
+
let waitObserver = null
|
|
2693
|
+
|
|
2694
|
+
// M11×视图:轮次条只在「对话」tab(ChatView)激活时显示——官方 viewArea
|
|
2695
|
+
// 只渲染激活视图(only: active.id),轨迹/Plan图/会话流 tab 下 ChatView 卸载,
|
|
2696
|
+
// `[data-chat-flow]` 随之消失(会话流标签页有自己的 sf-view,不冲突)。
|
|
2697
|
+
const syncChatVisibility = () => {
|
|
2698
|
+
if (container === undefined) return
|
|
2699
|
+
const chatFlow = document.querySelector('[data-chat-flow]')
|
|
2700
|
+
container.style.display = chatFlow !== null ? '' : 'none'
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
const ensure = () => {
|
|
2704
|
+
if (container !== undefined) {
|
|
2705
|
+
if (container.isConnected) return
|
|
2706
|
+
if (root) { try { root.unmount() } catch (e) {} root = undefined }
|
|
2707
|
+
container = undefined
|
|
2708
|
+
}
|
|
2709
|
+
container = document.createElement('div')
|
|
2710
|
+
document.body.appendChild(container)
|
|
2711
|
+
try {
|
|
2712
|
+
root = createRoot(container)
|
|
2713
|
+
root.render(h(TurnKeys, {
|
|
2714
|
+
sessions,
|
|
2715
|
+
connection,
|
|
2716
|
+
onOpenWorkbench: () => controller.open(),
|
|
2717
|
+
}))
|
|
2718
|
+
syncChatVisibility()
|
|
2719
|
+
} catch (e) {
|
|
2720
|
+
console.error('[dsh-session-flow:piano] render failed:', e)
|
|
2721
|
+
if (root) { try { root.unmount() } catch (e2) {} root = undefined }
|
|
2722
|
+
if (container) { container.remove(); container = undefined }
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
waitObserver = new MutationObserver(() => { ensure(); syncChatVisibility() })
|
|
2727
|
+
waitObserver.observe(document.body, { childList: true, subtree: true })
|
|
2728
|
+
ensure()
|
|
2729
|
+
|
|
2730
|
+
return () => {
|
|
2731
|
+
waitObserver.disconnect()
|
|
2732
|
+
if (root) { try { root.unmount() } catch (e) {} root = undefined }
|
|
2733
|
+
if (container) { container.remove(); container = undefined }
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
|
|
1674
2737
|
// ── 侧边栏入口(DOM 级注入 + 自愈)────────────────────────────────
|
|
1675
2738
|
const ICON = `<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 3.5h11M2.5 8h11M2.5 12.5h7"/></svg>`
|
|
1676
|
-
|
|
1677
2739
|
function sidebarRoot() {
|
|
1678
2740
|
const column = document.querySelector('[data-pane="sidebar"], [class*="sidebarCol"]')
|
|
1679
2741
|
if (column === null) return undefined
|
|
@@ -1818,7 +2880,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1818
2880
|
}
|
|
1819
2881
|
|
|
1820
2882
|
// ── 插件体 ─────────────────────────────────────────────────────────
|
|
1821
|
-
const inject = ['sessions', 'connection']
|
|
2883
|
+
const inject = ['sessions', 'connection', 'slots', 'layout']
|
|
1822
2884
|
|
|
1823
2885
|
function apply(ctx) {
|
|
1824
2886
|
try {
|
|
@@ -1850,6 +2912,13 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1850
2912
|
for (const fn of state.listeners) fn()
|
|
1851
2913
|
}
|
|
1852
2914
|
},
|
|
2915
|
+
open: () => {
|
|
2916
|
+
// 二期:嵌入标签页「打开完整工作台」→ 唤起侧边栏工作台。
|
|
2917
|
+
if (!state.open) {
|
|
2918
|
+
state.open = true
|
|
2919
|
+
for (const fn of state.listeners) fn()
|
|
2920
|
+
}
|
|
2921
|
+
},
|
|
1853
2922
|
}
|
|
1854
2923
|
|
|
1855
2924
|
const onActivate = (event) => {
|
|
@@ -1857,6 +2926,166 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1857
2926
|
}
|
|
1858
2927
|
document.addEventListener(ACTIVATE_EVENT, onActivate)
|
|
1859
2928
|
|
|
2929
|
+
// 二期:嵌入原生会话页 —— conversation.view 槽位环(会话作用域标签页)。
|
|
2930
|
+
// 参照 ui-trajectory(order 10) / plan-graph(order 20) 的注册模式:
|
|
2931
|
+
// slots.inject 自带生命周期(插件卸载自动移除标签页),无需手动清理。
|
|
2932
|
+
// label 支持字符串或惰性求值函数;order 越大越靠后(对话/轨迹/Plan图 之后)。
|
|
2933
|
+
const slots = ctx.get('slots')
|
|
2934
|
+
if (slots !== undefined && typeof slots.inject === 'function' && typeof slots.register === 'function') {
|
|
2935
|
+
try {
|
|
2936
|
+
slots.inject('conversation.view', () => slots.register({
|
|
2937
|
+
name: 'conversation.view',
|
|
2938
|
+
id: 'session-flow',
|
|
2939
|
+
order: 30,
|
|
2940
|
+
label: STR.entry,
|
|
2941
|
+
}, (props) => h(SessionFlowTab, {
|
|
2942
|
+
sessionId: props ? props.sessionId : undefined,
|
|
2943
|
+
sessions,
|
|
2944
|
+
connection,
|
|
2945
|
+
onOpenWorkbench: () => controller.open(),
|
|
2946
|
+
})))
|
|
2947
|
+
} catch (error) {
|
|
2948
|
+
console.error('[dsh-session-flow] conversation.view registration failed:', error)
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
// M12:详情并入右侧栏 —— details 槽位动态占用(priority -1 低于官方 tool-details)。
|
|
2953
|
+
// 并入开启时注册 + **打开官方 details 列(layout.openDetails——列默认宽度 0 不可见,
|
|
2954
|
+
// plan-graph 同款;曾漏开列导致「无响应」,实测踩坑)**;退出/卸载注销 + closeDetails。
|
|
2955
|
+
// 官方在会话切换时自动 closeDetails(无法阻止)——并入期间:
|
|
2956
|
+
// ① html 打 data-dsh-dock-active 标记 → CSS 禁用 grid 列宽过渡(切换无「关→开」动画);
|
|
2957
|
+
// ② 高频守护(150ms)快速重开列 → 视觉上右栏内容直接刷新为新会话。
|
|
2958
|
+
const layout = ctx.get('layout')
|
|
2959
|
+
let detailsRegistration = null
|
|
2960
|
+
let detailsGuard = null
|
|
2961
|
+
const setDockActive = (on) => {
|
|
2962
|
+
try {
|
|
2963
|
+
if (on) document.documentElement.setAttribute('data-dsh-dock-active', '')
|
|
2964
|
+
else document.documentElement.removeAttribute('data-dsh-dock-active')
|
|
2965
|
+
} catch (e) {}
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
// M12×aionui 兼容层:官方 details 把手按 3 轨模型定位(left = frameWidth - details,
|
|
2969
|
+
// 假设 details 是最后一轨);aionui-panel 把 frame grid 扩为 5 轨
|
|
2970
|
+
// (sidebar|center|details|preview|explorer)后,真实边界左移 preview+explorer px,
|
|
2971
|
+
// 把手却停在旧位置(悬浮于预览列上方,见 docs/02-design/LAYOUT-COEXIST.md)。
|
|
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
|
+
}
|
|
3028
|
+
const ensureColumn = () => {
|
|
3029
|
+
if (dockBridge.open && layout !== undefined && typeof layout.openDetails === 'function') {
|
|
3030
|
+
try { layout.openDetails() } catch (e) {}
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
const syncDetails = () => {
|
|
3034
|
+
if (dockBridge.open && detailsRegistration === null && slots !== undefined && typeof slots.inject === 'function') {
|
|
3035
|
+
try {
|
|
3036
|
+
detailsRegistration = slots.inject('details', () => slots.register({
|
|
3037
|
+
name: 'details',
|
|
3038
|
+
priority: -1,
|
|
3039
|
+
}, (props) => h(DetailsDockView, {
|
|
3040
|
+
sessionId: props ? props.sessionId : undefined,
|
|
3041
|
+
connection,
|
|
3042
|
+
onExit: () => dockBridge.controller.close(),
|
|
3043
|
+
})))
|
|
3044
|
+
setDockActive(true)
|
|
3045
|
+
ensureColumn()
|
|
3046
|
+
} catch (error) {
|
|
3047
|
+
console.error('[dsh-session-flow] details dock registration failed:', error)
|
|
3048
|
+
detailsRegistration = null
|
|
3049
|
+
}
|
|
3050
|
+
} else if (!dockBridge.open && detailsRegistration !== null) {
|
|
3051
|
+
try { detailsRegistration() } catch (e) {}
|
|
3052
|
+
detailsRegistration = null
|
|
3053
|
+
setDockActive(false)
|
|
3054
|
+
if (layout !== undefined && typeof layout.closeDetails === 'function') {
|
|
3055
|
+
try { layout.closeDetails() } catch (e) {}
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
dockBridge.controller = {
|
|
3060
|
+
getSnapshot: () => ({ open: dockBridge.open }),
|
|
3061
|
+
subscribe: (fn) => {
|
|
3062
|
+
dockBridge.listeners.add(fn)
|
|
3063
|
+
return () => dockBridge.listeners.delete(fn)
|
|
3064
|
+
},
|
|
3065
|
+
open: () => {
|
|
3066
|
+
if (!dockBridge.open) {
|
|
3067
|
+
dockBridge.open = true
|
|
3068
|
+
syncDetails()
|
|
3069
|
+
detailsHandleCompat.start()
|
|
3070
|
+
// 会话切换时官方会自动 closeDetails —— 高频守护快速重开(无动画,内容直接刷新)。
|
|
3071
|
+
if (detailsGuard === null) {
|
|
3072
|
+
detailsGuard = setInterval(() => { ensureColumn() }, 150)
|
|
3073
|
+
}
|
|
3074
|
+
for (const fn of dockBridge.listeners) fn()
|
|
3075
|
+
}
|
|
3076
|
+
},
|
|
3077
|
+
close: () => {
|
|
3078
|
+
if (dockBridge.open) {
|
|
3079
|
+
dockBridge.open = false
|
|
3080
|
+
syncDetails()
|
|
3081
|
+
detailsHandleCompat.stop()
|
|
3082
|
+
if (detailsGuard !== null) { clearInterval(detailsGuard); detailsGuard = null }
|
|
3083
|
+
for (const fn of dockBridge.listeners) fn()
|
|
3084
|
+
}
|
|
3085
|
+
},
|
|
3086
|
+
}
|
|
3087
|
+
syncDetails()
|
|
3088
|
+
|
|
1860
3089
|
// 点击侧边栏会话/工作区行 → 交还中栏给会话。
|
|
1861
3090
|
const onClickSidebarRow = (event) => {
|
|
1862
3091
|
if (!state.open) return
|
|
@@ -1889,11 +3118,25 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
|
|
|
1889
3118
|
try {
|
|
1890
3119
|
disposers.push(mountSidebarEntry(controller))
|
|
1891
3120
|
disposers.push(mountBoard(controller, sessions, connection))
|
|
3121
|
+
// M11:钢琴键会话快切(会话页左侧常驻竖条);挂载失败不影响其他功能。
|
|
3122
|
+
disposers.push(mountPianoKeys(controller, sessions, connection))
|
|
1892
3123
|
} catch (error) {
|
|
1893
3124
|
console.error('[dsh-session-flow] mount failed:', error)
|
|
1894
3125
|
}
|
|
1895
3126
|
|
|
1896
3127
|
const teardown = () => {
|
|
3128
|
+
// M12:卸载时注销 details 槽位占用(恢复官方 tool-details)+ 关列 + 清守护 + 移除标记。
|
|
3129
|
+
if (detailsRegistration) {
|
|
3130
|
+
try { detailsRegistration() } catch (e) {}
|
|
3131
|
+
detailsRegistration = null
|
|
3132
|
+
}
|
|
3133
|
+
if (detailsGuard !== null) { clearInterval(detailsGuard); detailsGuard = null }
|
|
3134
|
+
setDockActive(false)
|
|
3135
|
+
detailsHandleCompat.stop()
|
|
3136
|
+
if (layout !== undefined && typeof layout.closeDetails === 'function') {
|
|
3137
|
+
try { layout.closeDetails() } catch (e) {}
|
|
3138
|
+
}
|
|
3139
|
+
dockBridge.open = false
|
|
1897
3140
|
document.removeEventListener(ACTIVATE_EVENT, onActivate)
|
|
1898
3141
|
document.removeEventListener('click', onClickSidebarRow, true)
|
|
1899
3142
|
document.removeEventListener('scroll', onAnyScroll, true)
|