dsh-conversation-navigator 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1,1483 +1,1870 @@
1
- window.__ModuleLoader__.load({
2
- id: "dsh-conversation-navigator",
3
- factory: (require) => {
4
- var module = { exports: {} };
5
- var exports = module.exports;
6
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- const React = require("react");
8
- const { Button, Tooltip, IconCloseOutline16, IconSearchOutline16 } = require("@deepseek-ai/dsh-client-ui-primitives");
9
-
10
- /* ---------- 自绘图标: Lucide (ISC License, https://lucide.dev)
11
- 24px 网格 / 2px 描边 / 圆角线帽, 与 DSH 官方描边图标同一视觉语言 ---------- */
12
- function ToggleIcon() {
13
- return React.createElement("svg", {
14
- viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
15
- stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
16
- "aria-hidden": true,
17
- },
18
- React.createElement("path", { d: "M3 5h.01" }),
19
- React.createElement("path", { d: "M3 12h.01" }),
20
- React.createElement("path", { d: "M3 19h.01" }),
21
- React.createElement("path", { d: "M8 5h13" }),
22
- React.createElement("path", { d: "M8 12h13" }),
23
- React.createElement("path", { d: "M8 19h13" }),
24
- );
25
- }
26
- function LoadEarlierIcon() {
27
- return React.createElement("svg", {
28
- viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
29
- stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
30
- "aria-hidden": true,
31
- },
32
- React.createElement("path", { d: "M5 3h14" }),
33
- React.createElement("path", { d: "m18 13-6-6-6 6" }),
34
- React.createElement("path", { d: "M12 7v14" }),
35
- );
36
- }
37
- function LoadAllIcon() {
38
- return React.createElement("svg", {
39
- viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
40
- stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
41
- "aria-hidden": true,
42
- },
43
- React.createElement("path", { d: "m17 11-5-5-5 5" }),
44
- React.createElement("path", { d: "m17 18-5-5-5 5" }),
45
- );
46
- }
47
- function JumpLatestIcon() {
48
- return React.createElement("svg", {
49
- viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
50
- stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
51
- "aria-hidden": true,
52
- },
53
- React.createElement("path", { d: "M12 17V3" }),
54
- React.createElement("path", { d: "m6 11 6 6 6-6" }),
55
- React.createElement("path", { d: "M19 21H5" }),
56
- );
57
- }
58
- function CollapseAllIcon() {
59
- return React.createElement("svg", {
60
- viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
61
- stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
62
- "aria-hidden": true,
63
- },
64
- React.createElement("path", { d: "m14 10 7-7" }),
65
- React.createElement("path", { d: "M20 10h-6V4" }),
66
- React.createElement("path", { d: "m3 21 7-7" }),
67
- React.createElement("path", { d: "M4 14h6v6" }),
68
- );
69
- }
70
-
71
- function SwitchTurnsIcon() {
72
- return React.createElement("svg", {
73
- viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
74
- stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
75
- "aria-hidden": true,
76
- },
77
- React.createElement("path", { d: "M8 3 4 7l4 4" }),
78
- React.createElement("path", { d: "M4 7h16" }),
79
- React.createElement("path", { d: "m16 21 4-4-4-4" }),
80
- React.createElement("path", { d: "M20 17H4" }),
81
- );
82
- }
83
-
84
- function PinIcon() {
85
- return React.createElement("svg", {
86
- viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
87
- stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
88
- "aria-hidden": true,
89
- },
90
- React.createElement("path", { d: "M12 17v5" }),
91
- React.createElement("path", { d: "M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z" }),
92
- );
93
- }
94
-
95
- /* ---------- own stylesheet (static packages have no styles builtin) ---------- */
96
- const CSS = `
97
- .cnvnav-panel {
98
- position: fixed; z-index: 9999; width: 288px;
99
- display: flex; flex-direction: column;
100
- pointer-events: auto;
101
- background: var(--dsw-alias-bg-overlay, #fff);
102
- border: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.35));
103
- border-radius: 12px;
104
- box-shadow: 0 16px 48px rgba(0,0,0,.22);
105
- font-family: system-ui, -apple-system, 'Segoe UI', 'Microsoft YaHei', sans-serif;
106
- font-size: 12px;
107
- color: var(--dsw-alias-label-primary, #222);
108
- overflow: hidden;
109
- transition: opacity .18s ease, transform .18s ease;
110
- }
111
- .cnvnav-panel-hidden { opacity: 0; transform: translateY(-6px) scale(.985); pointer-events: none; }
112
- .cnvnav-panel-unplaced { visibility: hidden; }
113
- .cnvnav-head {
114
- display: flex; align-items: center; gap: 8px;
115
- padding: 10px 12px;
116
- border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.35));
117
- }
118
- .cnvnav-count { color: var(--dsw-alias-label-secondary, #888); font-size: 11px; }
119
- /* 左下角拖拽缩放热区(普通面板): 仅圆角区域可拖, 无可见手柄 */
120
- .cnvnav-resize-grip {
121
- position: absolute; left: 0; bottom: 0;
122
- width: 14px; height: 14px;
123
- cursor: nesw-resize;
124
- border-bottom-left-radius: 12px;
125
- touch-action: none;
126
- background: transparent;
127
- }
128
- .cnvnav-search { flex: none; display: flex; }
129
- .cnvnav-turns-toggle { flex: none; display: flex; }
130
- .cnvnav-close { margin-left: auto; }
131
- .cnvnav-list {
132
- overflow-y: auto; padding: 6px; min-height: 0;
133
- display: flex; flex-direction: column; gap: 2px;
134
- }
135
- .cnvnav-group { display: flex; flex-direction: column; margin-bottom: 2px; }
136
- .cnvnav-group-row {
137
- display: flex; align-items: stretch;
138
- border-radius: 8px; overflow: hidden;
139
- position: relative;
140
- transition: background .16s ease;
141
- }
142
- .cnvnav-group-row:hover { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.12)); }
143
- .cnvnav-group-row-active { background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 13%, transparent); }
144
- .cnvnav-group-row-active::before {
145
- content: ''; position: absolute; left: 6px; top: 6px; bottom: 6px;
146
- width: 3px; border-radius: 2px;
147
- background: var(--dsw-alias-brand-primary, #4f46e5);
148
- }
149
- .cnvnav-group-row-active:hover { background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 18%, transparent); }
150
- .cnvnav-group-row-active .cnvnav-group-title { color: var(--dsw-alias-brand-primary, #4f46e5); }
151
- .cnvnav-group-row-active .cnvnav-group-sub { color: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 45%, var(--dsw-alias-label-secondary, #888)); }
152
- .cnvnav-group-head-wrap { flex: 1; min-width: 0; display: flex; }
153
- .cnvnav-group-head {
154
- width: 100%;
155
- display: flex; align-items: baseline; gap: 6px; text-align: left;
156
- border: none; background: transparent; cursor: pointer;
157
- padding: 5px 0 5px 17px;
158
- color: var(--dsw-alias-label-primary, #222);
159
- }
160
- .cnvnav-group-title { font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif); font-size: 14px; font-weight: 700; line-height: 20px; white-space: nowrap; transition: color .16s ease; }
161
- .cnvnav-group-head .cnvnav-badge { align-self: center; }
162
- .cnvnav-group-sub {
163
- color: var(--dsw-alias-label-secondary, #888);
164
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
165
- flex: 1; min-width: 0;
166
- }
167
- .cnvnav-chevron-wrap { flex: none; display: flex; }
168
- .cnvnav-chevron {
169
- min-width: 30px; align-self: stretch;
170
- display: flex; align-items: center; justify-content: center; gap: 3px;
171
- border: none; background: transparent; cursor: pointer;
172
- color: var(--dsw-alias-label-secondary, #888);
173
- font-size: 11px; line-height: 1; padding: 0 8px;
174
- white-space: nowrap;
175
- transition: color .16s ease, background .16s ease;
176
- }
177
- .cnvnav-chevron:hover { color: var(--dsw-alias-brand-primary, #4f46e5); background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.12)); }
178
- .cnvnav-chevron-icon { flex: none; display: block; transition: transform .2s cubic-bezier(.4, 0, .2, 1); }
179
- .cnvnav-chevron[aria-expanded="true"] .cnvnav-chevron-icon { transform: rotate(90deg); }
180
- .cnvnav-chevron-count { font-size: 11px; line-height: 1; }
181
- .cnvnav-steps { display: grid; grid-template-rows: 1fr; margin-top: 2px; transition: grid-template-rows .24s cubic-bezier(.4, 0, .2, 1), margin-top .24s cubic-bezier(.4, 0, .2, 1); }
182
- .cnvnav-steps-collapsed { grid-template-rows: 0fr; margin-top: 0; }
183
- .cnvnav-steps-inner { overflow: hidden; min-height: 0; display: flex; flex-direction: column; gap: 2px; }
184
- .cnvnav-fade-item { animation: cnvnav-fade-up .2s ease-out backwards; }
185
- @keyframes cnvnav-fade-up { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
186
- .cnvnav-item {
187
- display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
188
- border: none; background: transparent; cursor: pointer;
189
- padding: 5px 8px 5px 22px; border-radius: 8px; position: relative;
190
- color: var(--dsw-alias-label-primary, #222);
191
- transition: background .16s ease;
192
- }
193
- .cnvnav-item:hover { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.12)); }
194
- .cnvnav-item-active { background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 13%, transparent); }
195
- .cnvnav-item-active:hover { background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 18%, transparent); }
196
- .cnvnav-item-active .cnvnav-item-preview { color: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 60%, var(--dsw-alias-label-primary, #222)); }
197
- .cnvnav-item-active::before,
198
- .cnvnav-item-active::after {
199
- content: ''; position: absolute; left: 6px; width: 3px; border-radius: 2px;
200
- background: var(--dsw-alias-brand-primary, #4f46e5);
201
- }
202
- .cnvnav-item-active::before { top: 6px; height: calc(50% - 7.5px); }
203
- .cnvnav-item-active::after { bottom: 6px; height: calc(50% - 7.5px); }
204
- .cnvnav-badge {
205
- flex: none; min-width: 36px; box-sizing: border-box;
206
- display: inline-flex; align-items: center; justify-content: center;
207
- height: 19px; padding: 0 5px; border-radius: 4px;
208
- font-size: 10px; font-weight: 650; line-height: 1;
209
- letter-spacing: .035em; white-space: nowrap;
210
- }
211
- .cnvnav-badge-traj-user { color: var(--dsw-alias-state-business-primary, #4f46e5); background: var(--dsw-alias-state-business-tertiary, rgba(79,70,229,.14)); }
212
- .cnvnav-badge-traj-context { color: color-mix(in srgb, var(--dsw-alias-state-success-primary, #0e9f6e) 68%, var(--dsw-alias-label-secondary, #888)); background: var(--dsw-alias-state-success-tertiary, rgba(14,159,110,.14)); }
213
- .cnvnav-badge-traj-assistant { color: color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color, #7c3aed) 60%, var(--dsw-alias-state-error-secondary, #b91c1c)); background: color-mix(in srgb, color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color, #7c3aed) 55%, var(--dsw-alias-state-error-secondary, #b91c1c)) 15%, var(--dsw-alias-bg-layer-1, transparent)); }
214
- .cnvnav-badge-traj-tool { color: var(--dsw-alias-state-warn-label, #b45309); background: var(--dsw-alias-state-warn-tertiary, rgba(180,83,9,.14)); }
215
- .cnvnav-badge-traj-compacted { color: var(--dsw-alias-label-secondary, #888); background: var(--dsw-alias-bg-module-platform, rgba(128,128,128,.12)); }
216
- .cnvnav-badge-other-command { color: #1d4ed8; background: rgba(29,78,216,.13); }
217
- .cnvnav-badge-other-error { color: var(--dsw-alias-state-error-primary, #dc2626); background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #dc2626) 14%, transparent); }
218
- .cnvnav-badge-other-retry { color: #ea580c; background: rgba(234,88,12,.14); }
219
- .cnvnav-badge-other-neutral { color: var(--dsw-alias-label-secondary, #888); background: var(--dsw-alias-bg-module-platform, rgba(128,128,128,.12)); }
220
- .cnvnav-item-preview {
221
- flex: 1; min-width: 0; display: block;
222
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
223
- color: var(--dsw-alias-label-secondary, #888);
224
- }
225
- .cnvnav-item-time {
226
- flex: none; color: var(--dsw-alias-label-secondary, #888); font-size: 10px; opacity: .8;
227
- }
228
- .cnvnav-hl {
229
- background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f59e0b) 30%, transparent);
230
- color: inherit; border-radius: 2px; padding: 0 1px; font-weight: 600;
231
- }
232
- .cnvnav-empty { padding: 18px 10px; text-align: center; color: var(--dsw-alias-label-secondary, #888); }
233
- /* ---------- 极简模式 ---------- */
234
- .cnvnav-mini-wrap {
235
- position: fixed; z-index: 9999; right: 12px;
236
- pointer-events: auto;
237
- transition: opacity .18s ease, transform .18s ease;
238
- }
239
- .cnvnav-mini-panel {
240
- width: 18px; height: 220px; overflow: hidden;
241
- position: relative;
242
- display: flex; flex-direction: column;
243
- background: transparent;
244
- border: 1px solid transparent;
245
- border-radius: 12px;
246
- box-shadow: none;
247
- transition: width .18s ease, background .16s ease, border-color .16s ease, box-shadow .16s ease;
248
- }
249
- .cnvnav-mini-wrap:hover .cnvnav-mini-panel {
250
- width: 288px;
251
- background: var(--dsw-alias-bg-overlay, #fff);
252
- border-color: var(--dsw-alias-border-l1, rgba(128,128,128,.35));
253
- box-shadow: 0 16px 48px rgba(0,0,0,.22);
254
- }
255
- .cnvnav-mini-fab {
256
- position: absolute; bottom: 100%; right: -8px;
257
- margin-bottom: 6px;
258
- display: flex;
259
- }
260
- .cnvnav-mini-list {
261
- flex: 1; min-height: 0; width: 288px; box-sizing: border-box;
262
- overflow-y: auto; padding: 4px;
263
- display: flex; flex-direction: column; gap: 2px;
264
- }
265
- /* dsweb 式滚动条: 只显示滑块, 无滑轨无上下按钮.
266
- 面板列表与极简条统一为常显细白条(极简条收起时列表被裁剪,
267
- 悬停展开后滑块自然可见). Chromium 用 webkit 伪元素
268
- (标准属性会令其失效), Firefox 用 @supports 标准属性 */
269
- .cnvnav-list::-webkit-scrollbar,
270
- .cnvnav-mini-list::-webkit-scrollbar { width: 4px; }
271
- .cnvnav-list::-webkit-scrollbar-track,
272
- .cnvnav-mini-list::-webkit-scrollbar-track { background: transparent; }
273
- .cnvnav-list::-webkit-scrollbar-thumb,
274
- .cnvnav-mini-list::-webkit-scrollbar-thumb {
275
- background: color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 40%, transparent);
276
- border-radius: 2px;
277
- }
278
- .cnvnav-list::-webkit-scrollbar-button,
279
- .cnvnav-mini-list::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
280
- .cnvnav-list::-webkit-scrollbar-corner,
281
- .cnvnav-mini-list::-webkit-scrollbar-corner { background: transparent; }
282
- @supports (-moz-appearance: none) {
283
- .cnvnav-list,
284
- .cnvnav-mini-list {
285
- scrollbar-width: thin;
286
- scrollbar-color: color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 40%, transparent) transparent;
287
- }
288
- }
289
- .cnvnav-mini-item {
290
- flex: none; height: 28px; box-sizing: border-box; width: 100%;
291
- display: flex; align-items: center; text-align: left;
292
- border: none; background: transparent; cursor: pointer;
293
- padding: 5px 8px 5px 17px; border-radius: 8px; position: relative;
294
- color: var(--dsw-alias-label-secondary, #888);
295
- font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
296
- transition: background .16s ease;
297
- }
298
- .cnvnav-mini-item:hover { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.12)); }
299
- /* 每行都有指示竖条: 非当前=掺前景色(浅色主题掺黑/深色主题掺白, 40%), 当前=品牌色实色 */
300
- .cnvnav-mini-item::before {
301
- content: ''; position: absolute; left: 6px; top: 6px; bottom: 6px;
302
- width: 3px; border-radius: 2px;
303
- background: color-mix(in srgb, var(--dsw-alias-label-primary, #222) 40%, transparent);
304
- transition: background .16s ease;
305
- }
306
- .cnvnav-mini-item-active {
307
- background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 13%, transparent);
308
- color: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 60%, var(--dsw-alias-label-primary, #222));
309
- }
310
- .cnvnav-mini-item-active::before { background: var(--dsw-alias-brand-primary, #4f46e5); }
311
- .cnvnav-filter {
312
- flex: 1; min-width: 0; box-sizing: border-box;
313
- height: 28px; padding: 0 8px;
314
- border: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.35));
315
- border-radius: 8px;
316
- background: var(--dsw-alias-bg-layer-1, rgba(128,128,128,.06));
317
- color: var(--dsw-alias-label-primary, #222);
318
- font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif);
319
- font-size: 12px; outline: none;
320
- animation: cnvnav-search-in .16s ease-out;
321
- }
322
- .cnvnav-filter::placeholder { color: var(--dsw-alias-label-secondary, #888); }
323
- .cnvnav-filter:focus { border-color: var(--dsw-alias-brand-primary, #4f46e5); }
324
- @keyframes cnvnav-search-in {
325
- from { opacity: 0; transform: translateX(4px); }
326
- to { opacity: 1; transform: translateX(0); }
327
- }
328
- .cnvnav-topbar { display: flex; gap: 6px; margin: 6px 6px 0; }
329
- .cnvnav-footer { display: flex; gap: 6px; margin: 0 6px 6px; }
330
- .cnvnav-bar-btn { flex: 1; display: block; }
331
- .cnvnav-bar-fill { width: 100%; }
332
- /* ---------- 拖动定位与左对齐极简 ---------- */
333
- .cnvnav-head { cursor: grab; touch-action: none; }
334
- .cnvnav-dragging { cursor: grabbing; user-select: none; }
335
- .cnvnav-dragging .cnvnav-head { cursor: grabbing; }
336
- /* 模式切换按钮激活态: 与整行描边图标按钮同一视觉层级
337
- (实心 primary 填充夹在幽灵图标中间会显得"错位") */
338
- .cnvnav-head button.cnvnav-toggle-on {
339
- color: var(--dsw-alias-brand-primary, #4f46e5);
340
- background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 13%, transparent);
341
- }
342
- .cnvnav-head button.cnvnav-toggle-on:hover {
343
- background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 20%, transparent);
344
- }
345
- .cnvnav-mini-wrap-left { right: auto; }
346
- .cnvnav-mini-wrap-left .cnvnav-mini-fab { right: auto; left: -8px; }
347
- `;
348
-
349
- function apply(ctx) {
350
- const slots = ctx.get("slots");
351
- if (slots === undefined) return;
352
- const timer = ctx.get("timer");
353
- const sessions = ctx.get("sessions");
354
-
355
- /* own stylesheet with fiber-scoped cleanup */
356
- const styleEl = document.createElement("style");
357
- styleEl.setAttribute("data-dsh-cnvnav", "");
358
- styleEl.textContent = CSS;
359
- document.head.appendChild(styleEl);
360
- ctx.effect(() => () => styleEl.remove());
361
-
362
- /* ---------- shared store (toggle button + overlay panel) ---------- */
363
- const UI_KEY = "dsh-cnvnav:ui:v1";
364
- const VIEW_MODES = ["full", "hidden", "minimal", "minimal-left"];
365
- function readUi() {
366
- try {
367
- const raw = window.localStorage.getItem(UI_KEY);
368
- if (!raw) return {};
369
- const o = JSON.parse(raw);
370
- const out = {};
371
- if (VIEW_MODES.indexOf(o.viewMode) !== -1) out.viewMode = o.viewMode;
372
- if (o.docked === true || o.docked === false) out.docked = o.docked;
373
- if (typeof o.panelWidth === "number" && o.panelWidth >= 240 && o.panelWidth <= 560) out.panelWidth = o.panelWidth;
374
- if (typeof o.panelHeight === "number" && o.panelHeight >= 90 && o.panelHeight <= 2000) out.panelHeight = o.panelHeight;
375
- if (o.userPos && typeof o.userPos.left === "number" && typeof o.userPos.top === "number"
376
- && Number.isFinite(o.userPos.left) && Number.isFinite(o.userPos.top)) {
377
- out.userPos = { left: o.userPos.left, top: o.userPos.top };
378
- }
379
- return out;
380
- } catch (err) {
381
- return {};
382
- }
383
- }
384
- function persistUi() {
385
- try {
386
- window.localStorage.setItem(UI_KEY, JSON.stringify({
387
- viewMode: state.viewMode, docked: state.docked, userPos: state.userPos, panelWidth: state.panelWidth, panelHeight: state.panelHeight,
388
- }));
389
- } catch (err) { /* 隐私模式等场景忽略 */ }
390
- }
391
- const savedUi = readUi();
392
- let state = Object.assign({
393
- open: true, sessionId: null, outline: null, activeKey: null, expanded: {},
394
- hasMore: false, loadingOlder: false, loadingAll: false,
395
- viewMode: "full", docked: true, userPos: null, panelWidth: 288, panelHeight: null,
396
- }, savedUi);
397
- const listeners = new Set();
398
- function setState(patch) {
399
- state = Object.assign({}, state, patch);
400
- listeners.forEach((fn) => { try { fn(); } catch (err) { console.error(err); } });
401
- }
402
- function subscribe(fn) { listeners.add(fn); return () => { listeners.delete(fn); }; }
403
- function useStore() {
404
- const [snap, setSnap] = React.useState(state);
405
- React.useEffect(() => subscribe(() => setSnap(state)), []);
406
- return snap;
407
- }
408
-
409
- /* ---------- history paging ---------- */
410
- let loadingAll = false;
411
- function loadOlderPage() {
412
- if (sessions === undefined) return;
413
- const sid = state.sessionId;
414
- if (sid === null) return;
415
- const binding = sessions.binding(sid);
416
- if (binding === undefined) return;
417
- binding.session.loadOlder().catch((err) => console.error(err));
418
- }
419
- async function loadAllPages() {
420
- if (sessions === undefined || loadingAll) return;
421
- const sid = state.sessionId;
422
- if (sid === null) return;
423
- const binding = sessions.binding(sid);
424
- if (binding === undefined) return;
425
- loadingAll = true;
426
- setState({ loadingAll: true });
427
- try {
428
- let guard = 0;
429
- while (binding.session.getSnapshot().hasMore && guard < 500) {
430
- await binding.session.loadOlder();
431
- guard += 1;
432
- }
433
- } catch (err) {
434
- console.error(err);
435
- } finally {
436
- loadingAll = false;
437
- setState({ loadingAll: false });
438
- }
439
- }
440
-
441
- /* ---------- outline derivation from ConversationSnapshot ---------- */
442
- const KIND_META = {
443
- "user": { label: "你", cls: "traj-user" },
444
- "steering": { label: "插话", cls: "traj-user" },
445
- "context": { label: "上下文", cls: "traj-context" },
446
- "assistant-step": { label: "助手", cls: "traj-assistant" },
447
- "tool-call": { label: "工具", cls: "traj-tool" },
448
- "command": { label: "命令", cls: "other-command" },
449
- "compaction": { label: "压缩", cls: "traj-compacted" },
450
- "manual-compaction": { label: "压缩", cls: "traj-compacted" },
451
- "turn-error": { label: "出错", cls: "other-error" },
452
- "turn-max-tokens": { label: "截断", cls: "other-error" },
453
- "model-retry": { label: "重试", cls: "other-retry" },
454
- "workflow-run": { label: "流程", cls: "other-neutral" },
455
- "unknown": { label: "其他", cls: "other-neutral" },
456
- };
457
- function firstText(blocks) {
458
- if (!Array.isArray(blocks)) return null;
459
- for (const b of blocks) {
460
- if (!b || typeof b !== "object") continue;
461
- if ((b.type === "text" || b.kind === "text") && typeof b.text === "string" && b.text.trim() !== "") return b.text;
462
- }
463
- return null;
464
- }
465
- /* Searchable dialogue text: user questions + agent actual dialogue only
466
- (excludes context, tool calls, commands, compaction, reasoning, etc.) */
467
- function dialogueText(node) {
468
- const d = node && node.data;
469
- if (!d || typeof d !== "object") return null;
470
- if (node.kind === "user") return firstText(d.content);
471
- if (node.kind === "assistant-step") {
472
- if (!Array.isArray(d.blocks)) return null;
473
- const parts = [];
474
- for (const b of d.blocks) {
475
- if (!b || typeof b !== "object") continue;
476
- if ((b.type === "text" || b.kind === "text") && typeof b.text === "string" && b.text.trim() !== "") parts.push(b.text.trim());
477
- }
478
- return parts.length > 0 ? parts.join(" ") : null;
479
- }
480
- return null;
481
- }
482
- /* Full user question text (all text blocks) for the hover bubble. */
483
- function fullDialogueText(node) {
484
- const d = node && node.data;
485
- if (!d || typeof d !== "object") return null;
486
- if (node.kind !== "user") return null;
487
- if (typeof d.content === "string") {
488
- const t = d.content.trim();
489
- return t === "" ? null : t;
490
- }
491
- if (!Array.isArray(d.content)) return null;
492
- const parts = [];
493
- for (const b of d.content) {
494
- if (!b || typeof b !== "object") continue;
495
- if ((b.type === "text" || b.kind === "text") && typeof b.text === "string" && b.text.trim() !== "") parts.push(b.text.trim());
496
- }
497
- return parts.length > 0 ? parts.join("\n") : null;
498
- }
499
- function previewFor(node) {
500
- const d = node && node.data;
501
- if (!d || typeof d !== "object") return null;
502
- switch (node.kind) {
503
- case "user":
504
- case "steering":
505
- case "context":
506
- return firstText(d.content);
507
- case "assistant-step": {
508
- const text = firstText(d.blocks);
509
- if (text !== null) return text;
510
- if (Array.isArray(d.blocks) && d.blocks.some((b) => b && b.kind === "tool-call")) return "调用工具";
511
- return d.status === "running" ? "进行中…" : null;
512
- }
513
- case "tool-call": {
514
- const root = d.root;
515
- if (!root || typeof root !== "object") return null;
516
- if (root.isError === true) return "执行失败";
517
- const name = typeof root.name === "string" ? root.name
518
- : (root.call && typeof root.call.name === "string" ? root.call.name : null);
519
- return name;
520
- }
521
- case "command":
522
- return typeof d.name === "string" ? d.name + (typeof d.args === "string" && d.args !== "" ? " " + d.args : "") : null;
523
- case "compaction":
524
- return typeof d.summary === "string" ? d.summary : "上下文已压缩";
525
- case "manual-compaction": {
526
- const c = d.compaction;
527
- if (c && typeof c.summary === "string") return c.summary;
528
- return "手动压缩";
529
- }
530
- case "turn-error":
531
- return typeof d.message === "string" ? d.message : "本轮出错";
532
- case "turn-max-tokens":
533
- return "达到输出上限";
534
- case "model-retry": {
535
- const n = Array.isArray(d.attempts) ? d.attempts.length : 0;
536
- return "自动重试" + (n > 0 ? " ×" + n : "…");
537
- }
538
- case "unknown":
539
- return typeof d.type === "string" ? "未知事件 " + d.type : "未知事件";
540
- default:
541
- return null;
542
- }
543
- }
544
- function timeOf(node) {
545
- const d = node && node.data;
546
- if (!d || typeof d !== "object") return null;
547
- const t = typeof d.time === "number" ? d.time
548
- : (d.root && typeof d.root.time === "number" ? d.root.time : null);
549
- return t;
550
- }
551
- function fmtTime(ms) {
552
- if (typeof ms !== "number" || !isFinite(ms)) return "";
553
- const d = new Date(ms);
554
- const h = d.getHours();
555
- const m = d.getMinutes();
556
- return (h < 10 ? "0" + h : h) + ":" + (m < 10 ? "0" + m : m);
557
- }
558
- function turnOf(node) {
559
- const loc = node && node.location;
560
- if (!loc) return null;
561
- if (loc.kind === "step" || loc.kind === "turn") return loc.turn.turn;
562
- return null;
563
- }
564
- function truncate(text, max) {
565
- if (typeof text !== "string") return "";
566
- if (text.length <= max) return text;
567
- return text.slice(0, max) + "…";
568
- }
569
- function deriveOutline(snap) {
570
- const empty = { groups: [], count: 0, byKey: {} };
571
- if (snap === null || snap === undefined) return empty;
572
- const order = snap.chat && snap.chat.order;
573
- const nodes = snap.chat && snap.chat.nodes;
574
- if (!Array.isArray(order) || nodes === undefined) return empty;
575
- const groups = [];
576
- const byKey = {};
577
- let current = null;
578
- let count = 0;
579
- for (const key of order) {
580
- const node = nodes.get(key);
581
- if (node === undefined) continue;
582
- const meta = KIND_META[node.kind] || KIND_META.unknown;
583
- const item = {
584
- key: node.key,
585
- kind: node.kind,
586
- label: meta.label,
587
- cls: meta.cls,
588
- preview: previewFor(node),
589
- search: dialogueText(node),
590
- full: fullDialogueText(node),
591
- time: fmtTime(timeOf(node)),
592
- };
593
- const turn = turnOf(node);
594
- if (turn === null) {
595
- groups.push({ turn: null, items: [item], gid: "x" + groups.length });
596
- count += 1;
597
- current = null;
598
- continue;
599
- }
600
- if (current === null || current.turn !== turn) {
601
- current = { turn: turn, items: [], gid: "t" + turn };
602
- groups.push(current);
603
- }
604
- current.items.push(item);
605
- count += 1;
606
- }
607
- for (const g of groups) {
608
- const first = g.items[0];
609
- if (g.turn === null) {
610
- g.title = first.label;
611
- g.subtitle = truncate(first.preview || "", 60);
612
- g.headKey = first.key;
613
- g.headIsUser = false;
614
- } else {
615
- g.title = "第 " + g.turn + " 轮";
616
- const head = first.kind === "user" ? first : null;
617
- g.headKey = head ? head.key : first.key;
618
- g.headIsUser = head !== null;
619
- g.subtitle = truncate((head ? head.preview : first.preview) || "", 60);
620
- }
621
- for (const it of g.items) byKey[it.key] = g.gid;
622
- }
623
- return { groups: groups, count: count, byKey: byKey };
624
- }
625
- let cacheKey = null;
626
- let cacheValue = null;
627
- function selectOutline(snap) {
628
- if (snap !== cacheKey) {
629
- cacheKey = snap;
630
- cacheValue = deriveOutline(snap);
631
- }
632
- return cacheValue;
633
- }
634
-
635
- /* ---------- DOM helpers: jump + position tracking ---------- */
636
- function findAnchor(key) {
637
- const rows = document.querySelectorAll("[data-chat-anchor-key]");
638
- for (const row of rows) {
639
- if (row.getAttribute("data-chat-anchor-key") === key) return row;
640
- }
641
- return null;
642
- }
643
- let lastActive = null;
644
- function jumpTo(key) {
645
- try {
646
- const el = findAnchor(key);
647
- if (el === null) return false;
648
- el.scrollIntoView({ behavior: "smooth", block: "start" });
649
- lastActive = key;
650
- setState({ activeKey: key });
651
- return true;
652
- } catch (err) {
653
- console.error(err);
654
- return false;
655
- }
656
- }
657
- function computeActiveKey() {
658
- try {
659
- const scrollport = document.querySelector("[data-conversation-scroll]");
660
- if (scrollport === null) return null;
661
- const vp = scrollport.getBoundingClientRect();
662
- const rows = document.querySelectorAll("[data-chat-anchor-key]");
663
- let found = null;
664
- for (const row of rows) {
665
- const rect = row.getBoundingClientRect();
666
- if (rect.bottom > vp.top) { found = row; break; }
667
- }
668
- if (found === null && rows.length > 0) found = rows[rows.length - 1];
669
- return found === null ? null : found.getAttribute("data-chat-anchor-key");
670
- } catch (err) {
671
- console.error(err);
672
- return null;
673
- }
674
- }
675
-
676
- let panelEl = null;
677
- let listEl = null;
678
- let dragMove = null;
679
- let resizeStart = null; /* { x, y, w, h, docked } for the corner grip */
680
- const itemEls = {};
681
- const groupEls = {};
682
-
683
- /* ---------- header toggle button ---------- */
684
- function ToggleButton(props) {
685
- const snap = useStore();
686
- const outline = props.useSession(selectOutline);
687
- const hasMore = props.useSession((s) => s.hasMore);
688
- const loadingOlder = props.useSession((s) => s.loadingOlder);
689
- React.useEffect(() => {
690
- setState({ sessionId: props.sessionId, outline: outline, hasMore: hasMore, loadingOlder: loadingOlder });
691
- }, [outline, props.sessionId, hasMore, loadingOlder]);
692
- React.useEffect(() => {
693
- if (state.sessionId !== null && state.sessionId !== props.sessionId) {
694
- lastActive = null;
695
- setState({ activeKey: null, expanded: {} });
696
- }
697
- }, [props.sessionId]);
698
- return React.createElement(Tooltip, {
699
- label: snap.open ? "收起会话导航" : "打开会话导航",
700
- side: "bottom",
701
- delayMs: 500,
702
- },
703
- React.createElement("span", null,
704
- React.createElement(Button, {
705
- variant: snap.open ? "primary" : "ghost",
706
- size: "sm",
707
- icon: React.createElement(ToggleIcon),
708
- "aria-pressed": snap.open,
709
- onClick: () => setState({ open: !snap.open }),
710
- }, "导航"),
711
- ),
712
- );
713
- }
714
-
715
- /* ---------- floating navigator panel ---------- */
716
- function Panel() {
717
- const snap = useStore();
718
- const [filter, setFilter] = React.useState("");
719
- const [searchOpen, setSearchOpen] = React.useState(false);
720
- const viewMode = snap.viewMode; /* full | hidden | minimal | minimal-left */
721
- const turnsHidden = viewMode !== "full";
722
- function nextModeLabel() {
723
- return viewMode === "full" ? "隐藏轮次" : viewMode === "hidden" ? "极简·右" : viewMode === "minimal" ? "极简·左" : "显示轮次";
724
- }
725
- function cycleMode() {
726
- const next = viewMode === "full" ? "hidden" : viewMode === "hidden" ? "minimal" : viewMode === "minimal" ? "minimal-left" : "full";
727
- setState({ viewMode: next });
728
- persistUi();
729
- }
730
- function toggleSearch() {
731
- if (searchOpen) setFilter("");
732
- setSearchOpen(!searchOpen);
733
- }
734
- /* ---------- 拖动定位(仅面板形式, 极简条不参与) ---------- */
735
- function startDrag(e) {
736
- if (e.button !== 0) return;
737
- if (typeof e.target.closest === "function" && e.target.closest("button") !== null) return;
738
- const el = panelEl;
739
- if (el === null) return;
740
- dragMove = { startX: e.clientX, startY: e.clientY, baseLeft: el.offsetLeft, baseTop: el.offsetTop, el: el, dragging: false };
741
- try { e.currentTarget.setPointerCapture(e.pointerId); } catch (err) { /* 忽略捕获失败 */ }
742
- }
743
- function moveDrag(e) {
744
- const d = dragMove;
745
- if (d === null) return;
746
- const dx = e.clientX - d.startX;
747
- const dy = e.clientY - d.startY;
748
- if (!d.dragging && (Math.abs(dx) > 3 || Math.abs(dy) > 3)) {
749
- d.dragging = true;
750
- d.el.classList.add("cnvnav-dragging");
751
- }
752
- if (!d.dragging) return;
753
- const vw = document.documentElement.clientWidth || 1000;
754
- const vh = document.documentElement.clientHeight || 800;
755
- const w = d.el.offsetWidth || 288;
756
- const h = d.el.offsetHeight || 200;
757
- const left = Math.min(Math.max(8, d.baseLeft + dx), Math.max(8, vw - w - 8));
758
- const top = Math.min(Math.max(8, d.baseTop + dy), Math.max(8, vh - h - 8));
759
- d.el.style.left = Math.round(left) + "px";
760
- d.el.style.top = Math.round(top) + "px";
761
- }
762
- function endDrag() {
763
- const d = dragMove;
764
- dragMove = null;
765
- if (d === null || !d.dragging) return;
766
- d.el.classList.remove("cnvnav-dragging");
767
- const r = d.el.getBoundingClientRect();
768
- /* 拖完同步封顶(与 place() 浮动分支一致): 上边缘固定, 高度至多半屏 */
769
- const vh = document.documentElement.clientHeight || 800;
770
- d.el.style.maxHeight = Math.max(160, Math.round(Math.min(vh / 2, vh - Math.max(8, Math.round(r.top)) - 8))) + "px";
771
- setState({ docked: false, userPos: { left: Math.round(r.left), top: Math.round(r.top) } });
772
- persistUi();
773
- }
774
- /* ---------- 左下角拖拽缩放(仅普通面板) ---------- */
775
- /* 语义化最小高度: 固定区(头部/顶部按钮栏/底部栏) + 至少 3 行轮次 + 内边距余量,
776
- 不用固定 px, 任何主题/字号下都能看到对话内容 */
777
- function minPanelH() {
778
- const p = panelEl;
779
- if (p === null) return 240;
780
- let fixed = 0;
781
- const head = p.querySelector(".cnvnav-head");
782
- const topbar = p.querySelector(".cnvnav-topbar");
783
- const footer = p.querySelector(".cnvnav-footer");
784
- if (head !== null) fixed += head.offsetHeight;
785
- if (topbar !== null) fixed += topbar.offsetHeight;
786
- if (footer !== null) fixed += footer.offsetHeight;
787
- const row = p.querySelector(".cnvnav-group-row") || p.querySelector(".cnvnav-item");
788
- const rowH = row !== null ? row.offsetHeight : 34;
789
- return Math.ceil(fixed + rowH * 3 + 26);
790
- }
791
- function startResize(e) {
792
- if (e.button !== 0) return;
793
- if (resizeStart !== null) return;
794
- const el = panelEl;
795
- if (el === null) return;
796
- resizeStart = {
797
- startX: e.clientX, startY: e.clientY,
798
- baseW: el.offsetWidth || 288,
799
- baseH: el.offsetHeight || 200,
800
- baseMaxH: (function () { const v = parseInt(el.style.maxHeight, 10); return Number.isFinite(v) && v > 0 ? v : (el.offsetHeight || 200); })(),
801
- el: el,
802
- };
803
- try { e.currentTarget.setPointerCapture(e.pointerId); } catch (err) { /* 忽略 */ }
804
- }
805
- function moveResize(e) {
806
- const d = resizeStart;
807
- if (d === null) return;
808
- const vh = document.documentElement.clientHeight || 800;
809
- const dx = e.clientX - d.startX;
810
- const dy = e.clientY - d.startY;
811
- /* 高度用 maxHeight + height:auto: 实际高度 = min(内容, 上限)。
812
- 以拖拽起点为基准线性跟随鼠标(无漂移/无状态跳变):
813
- 拖小立即压缩出滚动, 拖大在内容充足时线性恢复更多行;
814
- 内容不足时面板保持内容高(拖动无感, 属预期)。 */
815
- const r = d.el.getBoundingClientRect();
816
- const sp = document.querySelector("[data-conversation-scroll]");
817
- /* 上限必须与 place() 各分支一致, 否则松手后会被 place 压回造成跳变:
818
- docked/常规 = min(容器高-24, vh-40); 自由位置 = vh-顶-8 */
819
- const sr = sp !== null ? sp.getBoundingClientRect() : null;
820
- const cap = Math.max(minPanelH(), Math.round(
821
- state.docked && sr !== null
822
- ? Math.min(sr.height - 24, vh - 40)
823
- : vh - r.top - 8
824
- ));
825
- const nw = Math.min(Math.max(240, d.baseW - dx), 560);
826
- /* 目标上限 = 起点可见高 + 总位移, 线性且可逆 */
827
- const nMaxH = Math.min(Math.max(minPanelH(), d.baseH + dy), cap);
828
- const left = Math.max(8, r.right - nw);
829
- d.el.style.width = Math.round(nw) + "px";
830
- d.el.style.left = Math.round(left) + "px";
831
- d.el.style.height = "";
832
- d.el.style.maxHeight = Math.round(nMaxH) + "px";
833
- }
834
- function endResize() {
835
- const d = resizeStart;
836
- resizeStart = null;
837
- if (d === null) return;
838
- const el = d.el;
839
- const rect = el.getBoundingClientRect();
840
- const nw = el.offsetWidth;
841
- const parsed = parseInt(el.style.maxHeight, 10);
842
- const nh = Number.isFinite(parsed) && parsed > 0 ? parsed : null;
843
- if (state.docked) {
844
- setState({ panelWidth: Math.round(nw), panelHeight: nh });
845
- } else {
846
- setState({ panelWidth: Math.round(nw), panelHeight: nh, userPos: { left: Math.round(rect.left), top: Math.round(rect.top) } });
847
- }
848
- persistUi();
849
- }
850
-
851
- React.useEffect(() => {
852
- if (!snap.open) return;
853
- let lastKey = null;
854
- let stableTicks = 0;
855
- let misses = 0;
856
- function place() {
857
- const panel = panelEl;
858
- if (panel === null) return false;
859
- if (dragMove !== null) return true;
860
- const vw = document.documentElement.clientWidth || 1000;
861
- const vh = document.documentElement.clientHeight || 800;
862
- const mini = viewMode === "minimal" || viewMode === "minimal-left";
863
- const alignLeft = viewMode === "minimal-left";
864
- const width = (state.panelWidth || 288);
865
- /* 面板宽度对非极简模式统一生效, 确保拖动(未停靠)状态下也能实时刷新 */
866
- if (!mini) panel.style.width = width + "px";
867
- /* 面板被拖走后不再自动停靠: 夹进视口、上边缘固定, 展开高度至多半屏以便下方留白 */
868
- if (!mini && !state.docked && state.userPos !== null) {
869
- const w = panel.offsetWidth || 288;
870
- const h = panel.offsetHeight || 200;
871
- const left = Math.min(Math.max(8, Math.round(state.userPos.left)), Math.max(8, vw - w - 8));
872
- const top = Math.min(Math.max(8, Math.round(state.userPos.top)), Math.max(8, vh - h - 8));
873
- const fh = state.panelHeight;
874
- const cap = Math.max(minPanelH(), Math.round(vh - top - 8));
875
- const def = Math.max(minPanelH(), Math.round(Math.min(vh / 2, vh - top - 8)));
876
- panel.style.height = "";
877
- panel.style.maxHeight = Math.max(minPanelH(), Math.min(typeof fh === "number" && fh > 0 ? fh : def, cap)) + "px";
878
- panel.style.left = left + "px";
879
- panel.style.top = top + "px";
880
- panel.classList.remove("cnvnav-panel-unplaced");
881
- return true;
882
- }
883
- const sp = document.querySelector("[data-conversation-scroll]");
884
- const MINI_H = 220;
885
- if (sp === null) {
886
- /* 会话视图可能尚未挂载(刷新/重启/切会话时面板先就位);
887
- 非会话页(设置页等)则永远没有该容器。
888
- 连续 15 次(约 6s)仍未出现才回退到视口定位, 否则保持隐藏等待校准。 */
889
- misses += 1;
890
- if (misses >= 15) {
891
- if (mini) {
892
- /* 极简: 与普通面板共用同一 DOM 根, 必须先清除非极简 resize 残留的宽高,
893
- 极简的宽度完全由 CSS 决定(收起 18px / 悬停 288px) */
894
- panel.style.width = "";
895
- panel.style.height = "";
896
- /* 水平锚定, 右对齐由 CSS right 负责, 左对齐钉在视口左侧 */
897
- if (alignLeft) {
898
- panel.style.left = "12px";
899
- panel.style.right = "auto";
900
- } else {
901
- panel.style.left = "";
902
- panel.style.right = "";
903
- }
904
- panel.style.top = Math.max(8, Math.round((vh - MINI_H) / 2)) + "px";
905
- panel.style.maxHeight = MINI_H + "px";
906
- } else {
907
- panel.style.left = Math.max(8, vw - width - 12) + "px";
908
- panel.style.top = Math.max(8, 60) + "px";
909
- const fh = state.panelHeight;
910
- const cap = Math.round(vh - 90);
911
- const def = cap;
912
- panel.style.height = "";
913
- panel.style.maxHeight = Math.max(minPanelH(), Math.min(typeof fh === "number" && fh > 0 ? fh : def, cap)) + "px";
914
- }
915
- panel.classList.remove("cnvnav-panel-unplaced");
916
- return true;
917
- }
918
- return false;
919
- }
920
- const r = sp.getBoundingClientRect();
921
- const key = mini
922
- ? [Math.round(r.left), Math.round(r.top), Math.round(r.height), Math.round(vh)].join("|")
923
- : [Math.round(r.top), Math.round(r.height), Math.round(vw), Math.round(vh)].join("|");
924
- if (key !== lastKey) {
925
- lastKey = key;
926
- if (mini) {
927
- /* 极简: 与普通面板共用同一 DOM 根, 必须先清除非极简 resize 残留的宽高,
928
- 极简的宽度完全由 CSS 决定(收起 18px / 悬停 288px) */
929
- panel.style.width = "";
930
- panel.style.height = "";
931
- /* 右对齐由 CSS(right)锚定视口右缘, 左对齐钉在对话区左缘 */
932
- if (alignLeft) {
933
- panel.style.left = Math.max(8, Math.round(r.left + 12)) + "px";
934
- panel.style.right = "auto";
935
- } else {
936
- panel.style.left = "";
937
- panel.style.right = "";
938
- }
939
- panel.style.top = Math.max(8, Math.round(r.top + (r.height - MINI_H) / 2)) + "px";
940
- panel.style.maxHeight = MINI_H + "px";
941
- } else {
942
- panel.style.left = Math.max(8, vw - width - 12) + "px";
943
- panel.style.top = Math.max(8, Math.round(r.top + 8)) + "px";
944
- const fh = state.panelHeight;
945
- const cap = Math.max(minPanelH(), Math.round(Math.min(r.height - 24, vh - 40)));
946
- const def = cap;
947
- panel.style.height = "";
948
- panel.style.maxHeight = Math.max(minPanelH(), Math.min(typeof fh === "number" && fh > 0 ? fh : def, cap)) + "px";
949
- }
950
- }
951
- panel.classList.remove("cnvnav-panel-unplaced");
952
- return true;
953
- }
954
- place();
955
- /* 每 400ms 复查: 容器出现即校准; 布局连续 3 次无变化后停止 */
956
- let id = setInterval(() => {
957
- const before = lastKey;
958
- if (!place()) return;
959
- if (lastKey !== null && lastKey === before) {
960
- stableTicks += 1;
961
- if (stableTicks >= 3) clearInterval(id);
962
- } else {
963
- stableTicks = 0;
964
- }
965
- }, 400);
966
- window.addEventListener("resize", place);
967
- /* 左对齐极简条需跟随左侧栏展开/折叠: 对话区宽度变化时重新校准 */
968
- let ro = null;
969
- const roTarget = document.querySelector("[data-conversation-scroll]");
970
- if (typeof ResizeObserver === "function" && roTarget !== null) {
971
- ro = new ResizeObserver(() => { place(); });
972
- ro.observe(roTarget);
973
- }
974
- return () => {
975
- clearInterval(id);
976
- window.removeEventListener("resize", place);
977
- if (ro !== null) ro.disconnect();
978
- };
979
- }, [snap.open, snap.sessionId, viewMode, state.docked, state.userPos, state.panelWidth, state.panelHeight]);
980
-
981
- React.useEffect(() => {
982
- if (!snap.open) return;
983
- const run = () => {
984
- const key = computeActiveKey();
985
- if (key !== lastActive) {
986
- lastActive = key;
987
- setState({ activeKey: key });
988
- }
989
- };
990
- const update = timer !== undefined ? timer.throttle(run, 120) : run;
991
- document.addEventListener("scroll", update, true);
992
- window.addEventListener("resize", update);
993
- run();
994
- return () => {
995
- document.removeEventListener("scroll", update, true);
996
- window.removeEventListener("resize", update);
997
- if (timer !== undefined && typeof update.dispose === "function") update.dispose();
998
- };
999
- }, [snap.open]);
1000
-
1001
- React.useEffect(() => {
1002
- if (!snap.open || snap.activeKey === null) return;
1003
- let target = itemEls[snap.activeKey];
1004
- if (target === undefined || target === null || (typeof target.closest === "function" && target.closest(".cnvnav-steps-collapsed") !== null)) {
1005
- const gid = snap.outline !== null ? snap.outline.byKey[snap.activeKey] : undefined;
1006
- if (gid !== undefined) target = groupEls[gid];
1007
- }
1008
- const list = listEl;
1009
- if (target !== undefined && target !== null && list !== null) {
1010
- const lr = list.getBoundingClientRect();
1011
- const er = target.getBoundingClientRect();
1012
- const rel = er.top - lr.top + list.scrollTop;
1013
- if (rel < list.scrollTop || rel + er.height > list.scrollTop + list.clientHeight) {
1014
- list.scrollTo({ top: Math.max(0, rel - list.clientHeight / 2), behavior: "smooth" });
1015
- }
1016
- }
1017
- }, [snap.open, snap.activeKey]);
1018
-
1019
- const groups = snap.outline !== null ? snap.outline.groups : [];
1020
- const count = snap.outline !== null ? snap.outline.count : 0;
1021
- const hasExpanded = Object.keys(snap.expanded).some((k) => snap.expanded[k] === true);
1022
-
1023
- /* ---------- 极简模式(右/左对齐): 只保留定位 ---------- */
1024
- if (viewMode === "minimal" || viewMode === "minimal-left") {
1025
- const miniRows = [];
1026
- for (const g of groups) {
1027
- if (g.turn === null) continue;
1028
- miniRows.push({
1029
- key: g.headKey,
1030
- text: g.subtitle,
1031
- full: g.headIsUser && g.items.length > 0 ? g.items[0].full : null,
1032
- active: g.items.some((it) => it.key === snap.activeKey),
1033
- });
1034
- }
1035
- return React.createElement("div", {
1036
- ref: (el) => { panelEl = el; },
1037
- className: "cnvnav-mini-wrap" + (viewMode === "minimal-left" ? " cnvnav-mini-wrap-left" : "") + " cnvnav-panel-unplaced" + (snap.open ? "" : " cnvnav-panel-hidden"),
1038
- role: "complementary",
1039
- "aria-label": "会话导航",
1040
- "aria-hidden": !snap.open,
1041
- },
1042
- React.createElement(Tooltip, {
1043
- label: nextModeLabel(),
1044
- side: "top",
1045
- delayMs: 500,
1046
- },
1047
- React.createElement("span", { className: "cnvnav-mini-fab" },
1048
- React.createElement(Button, {
1049
- variant: "ghost",
1050
- size: "sm",
1051
- icon: React.createElement(SwitchTurnsIcon),
1052
- "aria-pressed": true,
1053
- onClick: () => cycleMode(),
1054
- }),
1055
- ),
1056
- ),
1057
- React.createElement("div", { className: "cnvnav-mini-panel" },
1058
- React.createElement("div", {
1059
- ref: (el) => { listEl = el; },
1060
- className: "cnvnav-mini-list",
1061
- },
1062
- groups.length === 0
1063
- ? React.createElement("div", { className: "cnvnav-empty" }, "当前会话暂无对话节点")
1064
- : miniRows.map((r) => React.createElement(Tooltip, {
1065
- key: r.key,
1066
- label: r.full !== null ? r.full : "跳转到此位置",
1067
- side: "top",
1068
- delayMs: 500,
1069
- maxWidth: 340,
1070
- },
1071
- React.createElement("button", {
1072
- type: "button",
1073
- ref: (el) => { itemEls[r.key] = el; },
1074
- className: "cnvnav-mini-item" + (r.active ? " cnvnav-mini-item-active" : ""),
1075
- onClick: () => jumpTo(r.key),
1076
- }, r.text),
1077
- )),
1078
- ),
1079
- ),
1080
- );
1081
- }
1082
-
1083
- const kw = filter.trim().toLowerCase();
1084
- const filtering = kw !== "";
1085
- function hit(text) {
1086
- return typeof text === "string" && text !== "" && text.toLowerCase().indexOf(kw) !== -1;
1087
- }
1088
- /* Split text into nodes, wrapping every case-insensitive keyword match in a <mark>. */
1089
- function highlightedNodes(text, kw) {
1090
- if (typeof text !== "string" || kw === "") return [text];
1091
- const lower = text.toLowerCase();
1092
- const out = [];
1093
- let i = 0;
1094
- let m = 0;
1095
- for (;;) {
1096
- const idx = lower.indexOf(kw, i);
1097
- if (idx === -1) { if (i < text.length) out.push(text.slice(i)); break; }
1098
- if (idx > i) out.push(text.slice(i, idx));
1099
- out.push(React.createElement("mark", { key: "m" + (m++), className: "cnvnav-hl" }, text.slice(idx, idx + kw.length)));
1100
- i = idx + kw.length;
1101
- }
1102
- return out;
1103
- }
1104
- /* Window the text around the first keyword match (so the keyword is visible,
1105
- not hidden behind head-truncation), then highlight every occurrence. */
1106
- function snippetNodes(text, kw, before, after) {
1107
- if (typeof text !== "string") return [text];
1108
- const idx = text.toLowerCase().indexOf(kw);
1109
- if (idx === -1) return highlightedNodes(text, kw);
1110
- const start = Math.max(0, idx - before);
1111
- const end = Math.min(text.length, idx + kw.length + after);
1112
- const s = text.slice(start, end);
1113
- const parts = [];
1114
- if (start > 0) parts.push("…");
1115
- parts.push(...highlightedNodes(s, kw));
1116
- if (end < text.length) parts.push("…");
1117
- return parts;
1118
- }
1119
- function renderItem(item, staggerMs) {
1120
- const useSnippet = filtering && hit(item.search);
1121
- const preview = useSnippet ? snippetNodes(item.search, kw, 5, 60) : item.preview;
1122
- const hasPreview = preview !== null && preview !== "" && (!Array.isArray(preview) || preview.length > 0);
1123
- return React.createElement("button", {
1124
- key: item.key,
1125
- type: "button",
1126
- ref: (el) => { itemEls[item.key] = el; },
1127
- className: "cnvnav-item" + (item.key === snap.activeKey ? " cnvnav-item-active" : "") + (staggerMs !== undefined ? " cnvnav-fade-item" : ""),
1128
- style: staggerMs !== undefined ? { animationDelay: staggerMs + "ms" } : undefined,
1129
- onClick: () => jumpTo(item.key),
1130
- },
1131
- React.createElement("span", { className: "cnvnav-badge cnvnav-badge-" + item.cls }, item.label),
1132
- hasPreview
1133
- ? React.createElement("span", { className: "cnvnav-item-preview" }, preview)
1134
- : null,
1135
- item.time !== ""
1136
- ? React.createElement("span", { className: "cnvnav-item-time" }, item.time)
1137
- : null,
1138
- );
1139
- }
1140
- function renderGroupHead(g, groupActive, chevron) {
1141
- const headSearch = g.items.length > 0 ? g.items[0].search : null;
1142
- const headFull = g.headIsUser && g.items.length > 0 ? g.items[0].full : null;
1143
- const useSnippet = filtering && hit(headSearch);
1144
- const sub = useSnippet ? snippetNodes(headSearch, kw, 5, 55) : g.subtitle;
1145
- const hasSub = sub !== "" && (!Array.isArray(sub) || sub.length > 0);
1146
- /* 标题策略:
1147
- 显示轮次: 轮次行 = 加粗「第 N 轮」; 压缩等非轮次行 = 加粗类型标签, 视作轮次同级
1148
- 隐藏轮次: 每行统一轨迹徽标 — 轮次头 = 「用户」业务蓝(助手开头则助手紫), 非轮次行 = 各自徽标(压缩=中性灰) */
1149
- let titleNode = null;
1150
- if (g.turn === null) {
1151
- if (turnsHidden && g.items.length > 0) {
1152
- const headItem = g.items[0];
1153
- titleNode = React.createElement("span", {
1154
- className: "cnvnav-badge cnvnav-badge-" + headItem.cls,
1155
- }, headItem.label);
1156
- } else {
1157
- titleNode = React.createElement("span", { className: "cnvnav-group-title" }, g.title);
1158
- }
1159
- } else if (!turnsHidden) {
1160
- titleNode = React.createElement("span", { className: "cnvnav-group-title" }, g.title);
1161
- } else if (g.items.length > 0) {
1162
- const headItem = g.items[0];
1163
- titleNode = React.createElement("span", {
1164
- className: "cnvnav-badge cnvnav-badge-" + headItem.cls,
1165
- }, headItem.kind === "user" ? "用户" : headItem.label);
1166
- }
1167
- return React.createElement("div", {
1168
- ref: (el) => { groupEls[g.gid] = el; },
1169
- className: "cnvnav-group-row" + (groupActive ? " cnvnav-group-row-active" : ""),
1170
- },
1171
- React.createElement(Tooltip, {
1172
- label: headFull !== null ? headFull : "跳转到此位置", maxWidth: 340,
1173
- side: "top",
1174
- delayMs: 500,
1175
- },
1176
- React.createElement("span", { className: "cnvnav-group-head-wrap" },
1177
- React.createElement("button", {
1178
- type: "button",
1179
- className: "cnvnav-group-head",
1180
- onClick: () => jumpTo(g.headKey),
1181
- },
1182
- titleNode,
1183
- hasSub
1184
- ? React.createElement("span", { className: "cnvnav-group-sub" }, sub)
1185
- : null,
1186
- ),
1187
- ),
1188
- ),
1189
- chevron !== undefined ? chevron : null,
1190
- );
1191
- }
1192
-
1193
- let groupNodes;
1194
- if (filtering) {
1195
- groupNodes = [];
1196
- for (const g of groups) {
1197
- const headHit = g.headIsUser && g.items.length > 0 && hit(g.items[0].search);
1198
- const hits = g.items.filter((it, i) =>
1199
- (it.kind === "user" || it.kind === "assistant-step") &&
1200
- hit(it.search) &&
1201
- !(i === 0 && g.headIsUser)
1202
- );
1203
- if (!headHit && hits.length === 0) continue;
1204
- const groupActive = g.items.some((it) => it.key === snap.activeKey);
1205
- groupNodes.push(React.createElement("div", {
1206
- key: g.gid,
1207
- className: "cnvnav-group cnvnav-fade-item",
1208
- style: { animationDelay: (groupNodes.length * 18) + "ms" },
1209
- },
1210
- renderGroupHead(g, groupActive, null),
1211
- hits.length > 0
1212
- ? React.createElement("div", { className: "cnvnav-steps" },
1213
- React.createElement("div", { className: "cnvnav-steps-inner" },
1214
- hits.map((it, i) => renderItem(it, i * 20)),
1215
- ),
1216
- )
1217
- : null,
1218
- ));
1219
- }
1220
- } else {
1221
- groupNodes = groups.map((g) => {
1222
- const expanded = snap.expanded[g.gid] === true;
1223
- const isTurnGroup = g.turn !== null;
1224
- const stepItems = isTurnGroup ? (g.headIsUser ? g.items.slice(1) : g.items) : [];
1225
- const hasSteps = stepItems.length > 0;
1226
- const groupActive = g.items.some((it) => it.key === snap.activeKey);
1227
- const chevron = hasSteps
1228
- ? React.createElement(Tooltip, {
1229
- label: expanded ? "折叠步次" : "展开步次",
1230
- side: "top",
1231
- delayMs: 500,
1232
- },
1233
- React.createElement("span", { className: "cnvnav-chevron-wrap" },
1234
- React.createElement("button", {
1235
- type: "button",
1236
- className: "cnvnav-chevron",
1237
- "aria-expanded": expanded,
1238
- onClick: (e) => {
1239
- e.stopPropagation();
1240
- setState({ expanded: Object.assign({}, state.expanded, { [g.gid]: !expanded }) });
1241
- },
1242
- },
1243
- React.createElement("svg", {
1244
- className: "cnvnav-chevron-icon",
1245
- viewBox: "0 0 24 24", width: 12, height: 12, fill: "none",
1246
- stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
1247
- "aria-hidden": true,
1248
- },
1249
- React.createElement("path", { d: "m9 18 6-6-6-6" }),
1250
- ),
1251
- expanded ? null : React.createElement("span", { className: "cnvnav-chevron-count" }, String(stepItems.length)),
1252
- ),
1253
- ),
1254
- )
1255
- : null;
1256
- return React.createElement("div", { key: g.gid, className: "cnvnav-group" },
1257
- renderGroupHead(g, groupActive, chevron),
1258
- hasSteps
1259
- ? React.createElement("div", {
1260
- className: "cnvnav-steps" + (expanded ? "" : " cnvnav-steps-collapsed"),
1261
- "aria-hidden": !expanded,
1262
- },
1263
- React.createElement("div", { className: "cnvnav-steps-inner" },
1264
- stepItems.map(renderItem),
1265
- ),
1266
- )
1267
- : null,
1268
- );
1269
- });
1270
- }
1271
-
1272
- return React.createElement("div", {
1273
- ref: (el) => { panelEl = el; },
1274
- className: "cnvnav-panel cnvnav-panel-unplaced" + (snap.open ? "" : " cnvnav-panel-hidden"),
1275
- role: "complementary",
1276
- "aria-label": "会话导航",
1277
- "aria-hidden": !snap.open,
1278
- },
1279
- React.createElement("div", {
1280
- className: "cnvnav-head",
1281
- onPointerDown: startDrag,
1282
- onPointerMove: moveDrag,
1283
- onPointerUp: endDrag,
1284
- onPointerCancel: endDrag,
1285
- },
1286
- React.createElement(Tooltip, {
1287
- label: searchOpen ? "收起搜索" : "搜索关键词",
1288
- side: "bottom",
1289
- delayMs: 500,
1290
- },
1291
- React.createElement("span", { className: "cnvnav-search" },
1292
- React.createElement(Button, {
1293
- variant: "ghost",
1294
- size: "sm",
1295
- icon: React.createElement(IconSearchOutline16, { size: 14 }),
1296
- onClick: () => toggleSearch(),
1297
- }),
1298
- ),
1299
- ),
1300
- searchOpen
1301
- ? null
1302
- : React.createElement(Tooltip, {
1303
- label: nextModeLabel(),
1304
- side: "bottom",
1305
- delayMs: 500,
1306
- },
1307
- React.createElement("span", { className: "cnvnav-turns-toggle" },
1308
- React.createElement(Button, {
1309
- variant: "ghost",
1310
- size: "sm",
1311
- className: viewMode !== "full" ? "cnvnav-toggle-on" : "",
1312
- icon: React.createElement(SwitchTurnsIcon),
1313
- "aria-pressed": viewMode !== "full",
1314
- onClick: () => cycleMode(),
1315
- }),
1316
- ),
1317
- ),
1318
- searchOpen
1319
- ? React.createElement("input", {
1320
- type: "text",
1321
- className: "cnvnav-filter",
1322
- placeholder: "请输入关键词",
1323
- value: filter,
1324
- autoFocus: true,
1325
- onChange: (e) => setFilter(e.target.value),
1326
- })
1327
- : null,
1328
- searchOpen
1329
- ? null
1330
- : React.createElement(Tooltip, {
1331
- label: snap.docked ? "解除停靠(拖动头部定位)" : "停靠回右侧",
1332
- side: "bottom",
1333
- delayMs: 500,
1334
- },
1335
- React.createElement("span", null,
1336
- React.createElement(Button, {
1337
- variant: snap.docked ? "primary" : "ghost",
1338
- size: "sm",
1339
- icon: React.createElement(PinIcon),
1340
- "aria-pressed": snap.docked,
1341
- onClick: () => {
1342
- if (snap.docked) {
1343
- const el = panelEl;
1344
- const r = el !== null ? el.getBoundingClientRect() : null;
1345
- setState({ docked: false, userPos: r !== null ? { left: Math.round(r.left), top: Math.round(r.top) } : null });
1346
- } else {
1347
- setState({ docked: true });
1348
- }
1349
- persistUi();
1350
- },
1351
- }),
1352
- ),
1353
- ),
1354
- React.createElement("span", { className: "cnvnav-count" }, (filtering ? groupNodes.length + " / " : "") + String(count) + " 条"),
1355
- React.createElement(Tooltip, {
1356
- label: "收起面板",
1357
- side: "bottom",
1358
- delayMs: 500,
1359
- },
1360
- React.createElement("span", { className: "cnvnav-close" },
1361
- React.createElement(Button, {
1362
- variant: "ghost",
1363
- size: "sm",
1364
- icon: React.createElement(IconCloseOutline16, { size: 14 }),
1365
- onClick: () => setState({ open: false }),
1366
- }),
1367
- ),
1368
- ),
1369
- ),
1370
- React.createElement("div", { className: "cnvnav-topbar" },
1371
- React.createElement(Tooltip, {
1372
- label: "加载更早的一批轮次",
1373
- side: "bottom",
1374
- delayMs: 500,
1375
- disabled: !snap.hasMore || snap.loadingOlder,
1376
- },
1377
- React.createElement("span", { className: "cnvnav-bar-btn" },
1378
- React.createElement(Button, {
1379
- variant: "ghost",
1380
- size: "sm",
1381
- className: "cnvnav-bar-fill",
1382
- icon: React.createElement(LoadEarlierIcon),
1383
- disabled: !snap.hasMore || snap.loadingOlder,
1384
- onClick: () => loadOlderPage(),
1385
- }, "加载更早"),
1386
- ),
1387
- ),
1388
- React.createElement(Tooltip, {
1389
- label: "加载全部历史轮次",
1390
- side: "bottom",
1391
- delayMs: 500,
1392
- disabled: !snap.hasMore || snap.loadingOlder || snap.loadingAll,
1393
- },
1394
- React.createElement("span", { className: "cnvnav-bar-btn" },
1395
- React.createElement(Button, {
1396
- variant: "ghost",
1397
- size: "sm",
1398
- className: "cnvnav-bar-fill",
1399
- icon: React.createElement(LoadAllIcon),
1400
- disabled: !snap.hasMore || snap.loadingOlder || snap.loadingAll,
1401
- onClick: () => loadAllPages(),
1402
- }, "加载全部"),
1403
- ),
1404
- ),
1405
- ),
1406
- React.createElement("div", {
1407
- ref: (el) => { listEl = el; },
1408
- className: "cnvnav-list",
1409
- },
1410
- groups.length === 0
1411
- ? React.createElement("div", { className: "cnvnav-empty" }, "当前会话暂无对话节点")
1412
- : (filtering && groupNodes.length === 0
1413
- ? React.createElement("div", { className: "cnvnav-empty" }, "无匹配结果")
1414
- : groupNodes),
1415
- ),
1416
- React.createElement("div", { className: "cnvnav-footer" },
1417
- React.createElement(Tooltip, {
1418
- label: "回到对话最新位置",
1419
- side: "top",
1420
- delayMs: 500,
1421
- },
1422
- React.createElement("span", { className: "cnvnav-bar-btn" },
1423
- React.createElement(Button, {
1424
- variant: "ghost",
1425
- size: "sm",
1426
- className: "cnvnav-bar-fill",
1427
- icon: React.createElement(JumpLatestIcon),
1428
- onClick: () => {
1429
- const gs = snap.outline !== null ? snap.outline.groups : [];
1430
- if (gs.length === 0) return;
1431
- const lastGroup = gs[gs.length - 1];
1432
- const lastItem = lastGroup.items[lastGroup.items.length - 1];
1433
- if (lastItem !== undefined) jumpTo(lastItem.key);
1434
- },
1435
- }, "回到最新"),
1436
- ),
1437
- ),
1438
- React.createElement(Tooltip, {
1439
- label: hasExpanded ? "折叠所有已展开的轮次" : "当前无展开的轮次",
1440
- side: "top",
1441
- delayMs: 500,
1442
- disabled: !hasExpanded,
1443
- },
1444
- React.createElement("span", { className: "cnvnav-bar-btn" },
1445
- React.createElement(Button, {
1446
- variant: "ghost",
1447
- size: "sm",
1448
- className: "cnvnav-bar-fill",
1449
- icon: React.createElement(CollapseAllIcon),
1450
- disabled: !hasExpanded,
1451
- onClick: () => setState({ expanded: {} }),
1452
- }, "全部折叠"),
1453
- ),
1454
- ),
1455
- ),
1456
- React.createElement("span", {
1457
- className: "cnvnav-resize-grip",
1458
- onPointerDown: startResize,
1459
- onPointerMove: moveResize,
1460
- onPointerUp: endResize,
1461
- onPointerCancel: endResize,
1462
- "aria-hidden": true,
1463
- }),
1464
- );
1465
- }
1466
-
1467
- /* ---------- register ---------- */
1468
- slots.inject("conversation.session.header.utilities", () => slots.register(
1469
- { name: "conversation.session.header.utilities", id: "conversation-navigator-toggle", order: 20, label: () => "会话导航" },
1470
- (props) => React.createElement(ToggleButton, props),
1471
- ));
1472
- slots.inject("shell.overlay", () => slots.register(
1473
- { name: "shell.overlay", id: "conversation-navigator-panel", order: 10, label: () => "会话导航" },
1474
- () => React.createElement(Panel),
1475
- ));
1476
- }
1477
-
1478
- const inject = ["slots", "sessions"];
1479
- exports.apply = apply;
1480
- exports.inject = inject;
1481
- return module.exports;
1482
- }
1483
- });
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-conversation-navigator",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ const React = require("react");
8
+ const { Button, Tooltip, IconCloseOutline16, IconSearchOutline16 } = require("@deepseek-ai/dsh-client-ui-primitives");
9
+
10
+ /* ---------- 自绘图标: Lucide (ISC License, https://lucide.dev)
11
+ 24px 网格 / 2px 描边 / 圆角线帽, 与 DSH 官方描边图标同一视觉语言 ---------- */
12
+ function ToggleIcon() {
13
+ return React.createElement("svg", {
14
+ viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
15
+ stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
16
+ "aria-hidden": true,
17
+ },
18
+ React.createElement("path", { d: "M3 5h.01" }),
19
+ React.createElement("path", { d: "M3 12h.01" }),
20
+ React.createElement("path", { d: "M3 19h.01" }),
21
+ React.createElement("path", { d: "M8 5h13" }),
22
+ React.createElement("path", { d: "M8 12h13" }),
23
+ React.createElement("path", { d: "M8 19h13" }),
24
+ );
25
+ }
26
+ function LoadEarlierIcon() {
27
+ return React.createElement("svg", {
28
+ viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
29
+ stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
30
+ "aria-hidden": true,
31
+ },
32
+ React.createElement("path", { d: "M5 3h14" }),
33
+ React.createElement("path", { d: "m18 13-6-6-6 6" }),
34
+ React.createElement("path", { d: "M12 7v14" }),
35
+ );
36
+ }
37
+ function LoadAllIcon() {
38
+ return React.createElement("svg", {
39
+ viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
40
+ stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
41
+ "aria-hidden": true,
42
+ },
43
+ React.createElement("path", { d: "m17 11-5-5-5 5" }),
44
+ React.createElement("path", { d: "m17 18-5-5-5 5" }),
45
+ );
46
+ }
47
+ function JumpLatestIcon() {
48
+ return React.createElement("svg", {
49
+ viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
50
+ stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
51
+ "aria-hidden": true,
52
+ },
53
+ React.createElement("path", { d: "M12 17V3" }),
54
+ React.createElement("path", { d: "m6 11 6 6 6-6" }),
55
+ React.createElement("path", { d: "M19 21H5" }),
56
+ );
57
+ }
58
+ function CollapseAllIcon() {
59
+ return React.createElement("svg", {
60
+ viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
61
+ stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
62
+ "aria-hidden": true,
63
+ },
64
+ React.createElement("path", { d: "m14 10 7-7" }),
65
+ React.createElement("path", { d: "M20 10h-6V4" }),
66
+ React.createElement("path", { d: "m3 21 7-7" }),
67
+ React.createElement("path", { d: "M4 14h6v6" }),
68
+ );
69
+ }
70
+
71
+ function SwitchTurnsIcon() {
72
+ return React.createElement("svg", {
73
+ viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
74
+ stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
75
+ "aria-hidden": true,
76
+ },
77
+ React.createElement("path", { d: "M8 3 4 7l4 4" }),
78
+ React.createElement("path", { d: "M4 7h16" }),
79
+ React.createElement("path", { d: "m16 21 4-4-4-4" }),
80
+ React.createElement("path", { d: "M20 17H4" }),
81
+ );
82
+ }
83
+
84
+ function PinIcon() {
85
+ return React.createElement("svg", {
86
+ viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
87
+ stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
88
+ "aria-hidden": true,
89
+ },
90
+ React.createElement("path", { d: "M12 17v5" }),
91
+ React.createElement("path", { d: "M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z" }),
92
+ );
93
+ }
94
+
95
+ /* ---------- own stylesheet (static packages have no styles builtin) ---------- */
96
+ const CSS = `
97
+ .cnvnav-panel {
98
+ position: fixed; z-index: 9999; width: 288px;
99
+ display: flex; flex-direction: column;
100
+ pointer-events: auto;
101
+ background: var(--dsw-alias-bg-overlay, #fff);
102
+ border: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.35));
103
+ border-radius: 12px;
104
+ box-shadow: 0 16px 48px rgba(0,0,0,.22);
105
+ font-family: system-ui, -apple-system, 'Segoe UI', 'Microsoft YaHei', sans-serif;
106
+ font-size: 12px;
107
+ color: var(--dsw-alias-label-primary, #222);
108
+ overflow: hidden;
109
+ transition: opacity .18s ease, transform .18s ease;
110
+ }
111
+ .cnvnav-panel-hidden { opacity: 0; transform: translateY(-6px) scale(.985); pointer-events: none; }
112
+ .cnvnav-panel-unplaced { visibility: hidden; }
113
+ .cnvnav-head {
114
+ display: flex; align-items: center; gap: 8px;
115
+ padding: 10px 12px;
116
+ border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.35));
117
+ }
118
+ .cnvnav-count { color: var(--dsw-alias-label-secondary, #888); font-size: 11px; }
119
+ /* 左下角拖拽缩放热区(普通面板): 仅圆角区域可拖, 无可见手柄 */
120
+ .cnvnav-resize-grip {
121
+ position: absolute; left: 0; bottom: 0;
122
+ width: 14px; height: 14px;
123
+ cursor: nesw-resize;
124
+ border-bottom-left-radius: 12px;
125
+ touch-action: none;
126
+ background: transparent;
127
+ }
128
+ .cnvnav-search { flex: none; display: flex; }
129
+ .cnvnav-turns-toggle { flex: none; display: flex; }
130
+ .cnvnav-close { margin-left: auto; }
131
+ .cnvnav-list {
132
+ overflow-y: auto; padding: 6px; min-height: 0;
133
+ display: flex; flex-direction: column; gap: 2px;
134
+ }
135
+ .cnvnav-group { display: flex; flex-direction: column; margin-bottom: 2px; }
136
+ .cnvnav-group-row {
137
+ display: flex; align-items: stretch;
138
+ border-radius: 8px; overflow: hidden;
139
+ position: relative;
140
+ transition: background .16s ease;
141
+ }
142
+ .cnvnav-group-row:hover { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.12)); }
143
+ .cnvnav-group-row-active { background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 13%, transparent); }
144
+ .cnvnav-group-row-active::before {
145
+ content: ''; position: absolute; left: 6px; top: 6px; bottom: 6px;
146
+ width: 3px; border-radius: 2px;
147
+ background: var(--dsw-alias-brand-primary, #4f46e5);
148
+ }
149
+ .cnvnav-group-row-active:hover { background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 18%, transparent); }
150
+ .cnvnav-group-row-active .cnvnav-group-title { color: var(--dsw-alias-brand-primary, #4f46e5); }
151
+ .cnvnav-group-row-active .cnvnav-group-sub { color: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 45%, var(--dsw-alias-label-secondary, #888)); }
152
+ .cnvnav-group-head-wrap { flex: 1; min-width: 0; display: flex; }
153
+ .cnvnav-group-head {
154
+ width: 100%;
155
+ display: flex; align-items: baseline; gap: 6px; text-align: left;
156
+ border: none; background: transparent; cursor: pointer;
157
+ padding: 5px 0 5px 17px;
158
+ color: var(--dsw-alias-label-primary, #222);
159
+ }
160
+ .cnvnav-group-title { font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif); font-size: 14px; font-weight: 700; line-height: 20px; white-space: nowrap; transition: color .16s ease; }
161
+ .cnvnav-group-head .cnvnav-badge { align-self: center; }
162
+ .cnvnav-group-sub {
163
+ color: var(--dsw-alias-label-secondary, #888);
164
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
165
+ flex: 1; min-width: 0;
166
+ }
167
+ .cnvnav-chevron-wrap { flex: none; display: flex; }
168
+ .cnvnav-chevron {
169
+ min-width: 30px; align-self: stretch;
170
+ display: flex; align-items: center; justify-content: center; gap: 3px;
171
+ border: none; background: transparent; cursor: pointer;
172
+ color: var(--dsw-alias-label-secondary, #888);
173
+ font-size: 11px; line-height: 1; padding: 0 8px;
174
+ white-space: nowrap;
175
+ transition: color .16s ease, background .16s ease;
176
+ }
177
+ .cnvnav-chevron:hover { color: var(--dsw-alias-brand-primary, #4f46e5); background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.12)); }
178
+ .cnvnav-chevron-icon { flex: none; display: block; transition: transform .2s cubic-bezier(.4, 0, .2, 1); }
179
+ .cnvnav-chevron[aria-expanded="true"] .cnvnav-chevron-icon { transform: rotate(90deg); }
180
+ .cnvnav-chevron-count { font-size: 11px; line-height: 1; }
181
+ .cnvnav-steps { display: grid; grid-template-rows: 1fr; margin-top: 2px; transition: grid-template-rows .24s cubic-bezier(.4, 0, .2, 1), margin-top .24s cubic-bezier(.4, 0, .2, 1); }
182
+ .cnvnav-steps-collapsed { grid-template-rows: 0fr; margin-top: 0; }
183
+ .cnvnav-steps-inner { overflow: hidden; min-height: 0; display: flex; flex-direction: column; gap: 2px; }
184
+ .cnvnav-fade-item { animation: cnvnav-fade-up .2s ease-out backwards; }
185
+ @keyframes cnvnav-fade-up { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
186
+ .cnvnav-item {
187
+ display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
188
+ border: none; background: transparent; cursor: pointer;
189
+ padding: 5px 8px 5px 22px; border-radius: 8px; position: relative;
190
+ color: var(--dsw-alias-label-primary, #222);
191
+ transition: background .16s ease;
192
+ }
193
+ .cnvnav-item:hover { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.12)); }
194
+ .cnvnav-item-active { background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 13%, transparent); }
195
+ .cnvnav-item-active:hover { background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 18%, transparent); }
196
+ .cnvnav-item-active .cnvnav-item-preview { color: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 60%, var(--dsw-alias-label-primary, #222)); }
197
+ .cnvnav-item-active::before,
198
+ .cnvnav-item-active::after {
199
+ content: ''; position: absolute; left: 6px; width: 3px; border-radius: 2px;
200
+ background: var(--dsw-alias-brand-primary, #4f46e5);
201
+ }
202
+ .cnvnav-item-active::before { top: 6px; height: calc(50% - 7.5px); }
203
+ .cnvnav-item-active::after { bottom: 6px; height: calc(50% - 7.5px); }
204
+ .cnvnav-badge {
205
+ flex: none; min-width: 36px; box-sizing: border-box;
206
+ display: inline-flex; align-items: center; justify-content: center;
207
+ height: 19px; padding: 0 5px; border-radius: 4px;
208
+ font-size: 10px; font-weight: 650; line-height: 1;
209
+ letter-spacing: .035em; white-space: nowrap;
210
+ }
211
+ .cnvnav-badge-traj-user { color: var(--dsw-alias-state-business-primary, #4f46e5); background: var(--dsw-alias-state-business-tertiary, rgba(79,70,229,.14)); }
212
+ .cnvnav-badge-traj-context { color: color-mix(in srgb, var(--dsw-alias-state-success-primary, #0e9f6e) 68%, var(--dsw-alias-label-secondary, #888)); background: var(--dsw-alias-state-success-tertiary, rgba(14,159,110,.14)); }
213
+ .cnvnav-badge-traj-assistant { color: color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color, #7c3aed) 60%, var(--dsw-alias-state-error-secondary, #b91c1c)); background: color-mix(in srgb, color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color, #7c3aed) 55%, var(--dsw-alias-state-error-secondary, #b91c1c)) 15%, var(--dsw-alias-bg-layer-1, transparent)); }
214
+ .cnvnav-badge-traj-tool { color: var(--dsw-alias-state-warn-label, #b45309); background: var(--dsw-alias-state-warn-tertiary, rgba(180,83,9,.14)); }
215
+ .cnvnav-badge-traj-compacted { color: var(--dsw-alias-label-secondary, #888); background: var(--dsw-alias-bg-module-platform, rgba(128,128,128,.12)); }
216
+ .cnvnav-badge-other-command { color: #1d4ed8; background: rgba(29,78,216,.13); }
217
+ .cnvnav-badge-other-error { color: var(--dsw-alias-state-error-primary, #dc2626); background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #dc2626) 14%, transparent); }
218
+ .cnvnav-badge-other-retry { color: #ea580c; background: rgba(234,88,12,.14); }
219
+ .cnvnav-badge-other-neutral { color: var(--dsw-alias-label-secondary, #888); background: var(--dsw-alias-bg-module-platform, rgba(128,128,128,.12)); }
220
+ .cnvnav-item-preview {
221
+ flex: 1; min-width: 0; display: block;
222
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
223
+ color: var(--dsw-alias-label-secondary, #888);
224
+ }
225
+ .cnvnav-item-time {
226
+ flex: none; color: var(--dsw-alias-label-secondary, #888); font-size: 10px; opacity: .8;
227
+ }
228
+ .cnvnav-hl {
229
+ background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f59e0b) 30%, transparent);
230
+ color: inherit; border-radius: 2px; padding: 0 1px; font-weight: 600;
231
+ }
232
+ .cnvnav-empty { padding: 18px 10px; text-align: center; color: var(--dsw-alias-label-secondary, #888); }
233
+ /* ---------- 极简模式 ---------- */
234
+ .cnvnav-mini-wrap {
235
+ position: fixed; z-index: 9999; right: 12px;
236
+ pointer-events: auto;
237
+ transition: opacity .18s ease, transform .18s ease;
238
+ }
239
+ .cnvnav-mini-panel {
240
+ width: 18px; height: 220px; overflow: hidden;
241
+ position: relative;
242
+ display: flex; flex-direction: column;
243
+ background: transparent;
244
+ border: 1px solid transparent;
245
+ border-radius: 12px;
246
+ box-shadow: none;
247
+ transition: width .18s ease, background .16s ease, border-color .16s ease, box-shadow .16s ease;
248
+ }
249
+ .cnvnav-mini-wrap:hover .cnvnav-mini-panel {
250
+ width: 288px;
251
+ background: var(--dsw-alias-bg-overlay, #fff);
252
+ border-color: var(--dsw-alias-border-l1, rgba(128,128,128,.35));
253
+ box-shadow: 0 16px 48px rgba(0,0,0,.22);
254
+ }
255
+ .cnvnav-mini-fab {
256
+ position: absolute; bottom: 100%; right: -8px;
257
+ margin-bottom: 6px;
258
+ display: flex;
259
+ }
260
+ .cnvnav-mini-list {
261
+ flex: 1; min-height: 0; width: 288px; box-sizing: border-box;
262
+ overflow-y: auto; padding: 4px;
263
+ display: flex; flex-direction: column; gap: 2px;
264
+ }
265
+ /* dsweb 式滚动条: 只显示滑块, 无滑轨无上下按钮.
266
+ 面板列表与极简条统一为常显细白条(极简条收起时列表被裁剪,
267
+ 悬停展开后滑块自然可见). Chromium 用 webkit 伪元素
268
+ (标准属性会令其失效), Firefox 用 @supports 标准属性 */
269
+ .cnvnav-list::-webkit-scrollbar,
270
+ .cnvnav-mini-list::-webkit-scrollbar { width: 4px; }
271
+ .cnvnav-list::-webkit-scrollbar-track,
272
+ .cnvnav-mini-list::-webkit-scrollbar-track { background: transparent; }
273
+ .cnvnav-list::-webkit-scrollbar-thumb,
274
+ .cnvnav-mini-list::-webkit-scrollbar-thumb {
275
+ background: color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 40%, transparent);
276
+ border-radius: 2px;
277
+ }
278
+ .cnvnav-list::-webkit-scrollbar-button,
279
+ .cnvnav-mini-list::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
280
+ .cnvnav-list::-webkit-scrollbar-corner,
281
+ .cnvnav-mini-list::-webkit-scrollbar-corner { background: transparent; }
282
+ @supports (-moz-appearance: none) {
283
+ .cnvnav-list,
284
+ .cnvnav-mini-list {
285
+ scrollbar-width: thin;
286
+ scrollbar-color: color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 40%, transparent) transparent;
287
+ }
288
+ }
289
+ .cnvnav-mini-item {
290
+ flex: none; height: 28px; box-sizing: border-box; width: 100%;
291
+ display: flex; align-items: center; text-align: left;
292
+ border: none; background: transparent; cursor: pointer;
293
+ padding: 5px 8px 5px 17px; border-radius: 8px; position: relative;
294
+ color: var(--dsw-alias-label-secondary, #888);
295
+ font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
296
+ transition: background .16s ease;
297
+ }
298
+ .cnvnav-mini-item:hover { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.12)); }
299
+ /* 每行都有指示竖条: 非当前=掺前景色(浅色主题掺黑/深色主题掺白, 40%), 当前=品牌色实色 */
300
+ .cnvnav-mini-item::before {
301
+ content: ''; position: absolute; left: 6px; top: 6px; bottom: 6px;
302
+ width: 3px; border-radius: 2px;
303
+ background: color-mix(in srgb, var(--dsw-alias-label-primary, #222) 40%, transparent);
304
+ transition: background .16s ease;
305
+ }
306
+ .cnvnav-mini-item-active {
307
+ background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 13%, transparent);
308
+ color: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 60%, var(--dsw-alias-label-primary, #222));
309
+ }
310
+ .cnvnav-mini-item-active::before { background: var(--dsw-alias-brand-primary, #4f46e5); }
311
+ .cnvnav-filter {
312
+ flex: 1; min-width: 0; box-sizing: border-box;
313
+ height: 28px; padding: 0 8px;
314
+ border: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.35));
315
+ border-radius: 8px;
316
+ background: var(--dsw-alias-bg-layer-1, rgba(128,128,128,.06));
317
+ color: var(--dsw-alias-label-primary, #222);
318
+ font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif);
319
+ font-size: 12px; outline: none;
320
+ animation: cnvnav-search-in .16s ease-out;
321
+ }
322
+ .cnvnav-filter::placeholder { color: var(--dsw-alias-label-secondary, #888); }
323
+ .cnvnav-filter:focus { border-color: var(--dsw-alias-brand-primary, #4f46e5); }
324
+ @keyframes cnvnav-search-in {
325
+ from { opacity: 0; transform: translateX(4px); }
326
+ to { opacity: 1; transform: translateX(0); }
327
+ }
328
+ .cnvnav-topbar { display: flex; gap: 6px; margin: 6px 6px 0; }
329
+ .cnvnav-footer { display: flex; gap: 6px; margin: 0 6px 6px; }
330
+ .cnvnav-bar-btn { flex: 1; display: block; }
331
+ .cnvnav-bar-fill { width: 100%; }
332
+ /* ---------- 拖动定位与左对齐极简 ---------- */
333
+ .cnvnav-head { cursor: grab; touch-action: none; }
334
+ .cnvnav-dragging { cursor: grabbing; user-select: none; }
335
+ .cnvnav-dragging .cnvnav-head { cursor: grabbing; }
336
+ /* 模式切换按钮激活态: 与整行描边图标按钮同一视觉层级
337
+ (实心 primary 填充夹在幽灵图标中间会显得"错位") */
338
+ .cnvnav-head button.cnvnav-toggle-on {
339
+ color: var(--dsw-alias-brand-primary, #4f46e5);
340
+ background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 13%, transparent);
341
+ }
342
+ .cnvnav-head button.cnvnav-toggle-on:hover {
343
+ background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 20%, transparent);
344
+ }
345
+ .cnvnav-mini-wrap-left { right: auto; }
346
+ .cnvnav-mini-wrap-left .cnvnav-mini-fab { right: auto; left: -8px; }
347
+ `;
348
+
349
+ function apply(ctx) {
350
+ const slots = ctx.get("slots");
351
+ if (slots === undefined) return;
352
+ const timer = ctx.get("timer");
353
+ const sessions = ctx.get("sessions");
354
+ /* 宿主形态检测:官方 v0.1.2-rc.1 起引入 uiConversation 服务并把会话节点
355
+ 数据从 session 快照挪到 chat target 快照。用 feature detection,
356
+ 不硬编码版本号。 */
357
+ const uiConversation = ctx.get("uiConversation");
358
+ const NEW_HOST = uiConversation !== undefined;
359
+ /* 运行时只保证 exports.inject 声明的服务先就绪; uiConversation 未声明时
360
+ 会在"已物化/未物化"之间随机翻转(apply 竞态), 导致宿主检测时好时坏。
361
+ 这里提供实时重查, 供效果/布局在运行时使用, 不缓存负结果。 */
362
+ const getUiConv = () => ctx.get("uiConversation");
363
+ const isNewHostNow = () => getUiConv() !== undefined;
364
+
365
+ /* own stylesheet with fiber-scoped cleanup */
366
+ const styleEl = document.createElement("style");
367
+ styleEl.setAttribute("data-dsh-cnvnav", "");
368
+ styleEl.textContent = CSS;
369
+ document.head.appendChild(styleEl);
370
+ ctx.effect(() => () => {
371
+ styleEl.remove();
372
+ if (chatUnsub !== null) { chatUnsub(); chatUnsub = null; }
373
+ });
374
+
375
+ /* ---------- shared store (toggle button + overlay panel) ---------- */
376
+ /* 0.2.6: 键名升级 v2 —— 旧 v1 记录(0.2.5 时代)在新宿主下会以 viewMode:"full" 等旧值
377
+ 覆盖新的 minimal-left 默认,导致"验收不过";升级后新宿主从干净默认开始 */
378
+ const UI_KEY = "dsh-cnvnav:ui:v2";
379
+ /* 极简·右(minimal)与新宿主官方 rail 视觉重叠,先注释掉保留代码,待有更好的共存方案再恢复 */
380
+ const VIEW_MODES = ["full", "hidden", /* "minimal", */ "minimal-left"];
381
+ function readUi() {
382
+ try {
383
+ const raw = window.localStorage.getItem(UI_KEY);
384
+ if (!raw) return {};
385
+ const o = JSON.parse(raw);
386
+ const out = {};
387
+ if (VIEW_MODES.indexOf(o.viewMode) !== -1) out.viewMode = o.viewMode;
388
+ if (o.docked === true || o.docked === false) out.docked = o.docked;
389
+ /* open 不持久化: 新宿主每次启动都从默认(关闭)开始,
390
+ 避免重启/正常启动时面板被旧状态默认展开 */
391
+ if (typeof o.panelWidth === "number" && o.panelWidth >= 240 && o.panelWidth <= 560) out.panelWidth = o.panelWidth;
392
+ if (typeof o.panelHeight === "number" && o.panelHeight >= 90 && o.panelHeight <= 2000) out.panelHeight = o.panelHeight;
393
+ if (o.userPos && typeof o.userPos.left === "number" && typeof o.userPos.top === "number"
394
+ && Number.isFinite(o.userPos.left) && Number.isFinite(o.userPos.top)) {
395
+ out.userPos = { left: o.userPos.left, top: o.userPos.top };
396
+ }
397
+ return out;
398
+ } catch (err) {
399
+ return {};
400
+ }
401
+ }
402
+ function persistUi() {
403
+ try {
404
+ window.localStorage.setItem(UI_KEY, JSON.stringify({
405
+ viewMode: state.viewMode, docked: state.docked, userPos: state.userPos, panelWidth: state.panelWidth, panelHeight: state.panelHeight, open: state.open,
406
+ }));
407
+ } catch (err) { /* 隐私模式等场景忽略 */ }
408
+ }
409
+ const savedUi = readUi();
410
+ let state = Object.assign({
411
+ /* 双形态默认值:旧宿主保持全功能导航器(默认展开);
412
+ 新宿主(官方内置 TurnNavigator 常显)默认关闭 + 极简·左,
413
+ 与官方右侧 rail 零重叠,由用户主动呼出。 */
414
+ open: NEW_HOST ? false : true, sessionId: null, outline: null, activeKey: null, expanded: {},
415
+ hasMore: false, loadingOlder: false, loadingAll: false,
416
+ viewMode: NEW_HOST ? "minimal-left" : "full", docked: true, userPos: null, panelWidth: 288, panelHeight: null,
417
+ }, savedUi);
418
+ const listeners = new Set();
419
+ function setState(patch) {
420
+ state = Object.assign({}, state, patch);
421
+ listeners.forEach((fn) => { try { fn(); } catch (err) { console.error(err); } });
422
+ }
423
+ function subscribe(fn) { listeners.add(fn); return () => { listeners.delete(fn); }; }
424
+ function useStore() {
425
+ const [snap, setSnap] = React.useState(state);
426
+ React.useEffect(() => subscribe(() => setSnap(state)), []);
427
+ return snap;
428
+ }
429
+
430
+ /* ---------- history paging ---------- */
431
+ let loadingAll = false;
432
+ function loadOlderPage() {
433
+ if (sessions === undefined) return;
434
+ const sid = state.sessionId;
435
+ if (sid === null) return;
436
+ const binding = sessions.binding(sid);
437
+ if (binding === undefined) return;
438
+ binding.session.loadOlder().catch((err) => console.error(err));
439
+ }
440
+ async function loadAllPages() {
441
+ if (sessions === undefined || loadingAll) return;
442
+ const sid = state.sessionId;
443
+ if (sid === null) return;
444
+ const binding = sessions.binding(sid);
445
+ if (binding === undefined) return;
446
+ loadingAll = true;
447
+ setState({ loadingAll: true });
448
+ try {
449
+ let guard = 0;
450
+ while (binding.session.getSnapshot().hasMore && guard < 500) {
451
+ await binding.session.loadOlder();
452
+ guard += 1;
453
+ }
454
+ } catch (err) {
455
+ console.error(err);
456
+ } finally {
457
+ loadingAll = false;
458
+ setState({ loadingAll: false });
459
+ }
460
+ }
461
+
462
+ /* ---------- outline derivation from ConversationSnapshot ---------- */
463
+ const KIND_META = {
464
+ "user": { label: "你", cls: "traj-user" },
465
+ "steering": { label: "插话", cls: "traj-user" },
466
+ "context": { label: "上下文", cls: "traj-context" },
467
+ "assistant-step": { label: "助手", cls: "traj-assistant" },
468
+ "tool-call": { label: "工具", cls: "traj-tool" },
469
+ "command": { label: "命令", cls: "other-command" },
470
+ "compaction": { label: "压缩", cls: "traj-compacted" },
471
+ "manual-compaction": { label: "压缩", cls: "traj-compacted" },
472
+ "turn-error": { label: "出错", cls: "other-error" },
473
+ "turn-max-tokens": { label: "截断", cls: "other-error" },
474
+ "model-retry": { label: "重试", cls: "other-retry" },
475
+ "workflow-run": { label: "流程", cls: "other-neutral" },
476
+ "unknown": { label: "其他", cls: "other-neutral" },
477
+ };
478
+ function firstText(blocks) {
479
+ if (!Array.isArray(blocks)) return null;
480
+ for (const b of blocks) {
481
+ /* 官方 user.content 为嵌套数组(多 part),递归展平提取 */
482
+ if (Array.isArray(b)) {
483
+ const inner = firstText(b);
484
+ if (inner !== null) return inner;
485
+ continue;
486
+ }
487
+ if (!b || typeof b !== "object") continue;
488
+ if ((b.type === "text" || b.kind === "text") && typeof b.text === "string" && b.text.trim() !== "") return b.text;
489
+ }
490
+ return null;
491
+ }
492
+ /* Searchable dialogue text: user questions + agent actual dialogue only
493
+ (excludes context, tool calls, commands, compaction, reasoning, etc.) */
494
+ function dialogueText(node) {
495
+ const d = node && node.data;
496
+ if (!d || typeof d !== "object") return null;
497
+ if (node.kind === "user" || node.kind === "input-message") return firstText(d.content);
498
+ if (node.kind === "assistant-step") {
499
+ if (!Array.isArray(d.blocks)) return null;
500
+ const parts = [];
501
+ /* 官方 assistant.blocks 同样可能嵌套,先展平 */
502
+ const flat = d.blocks.flatMap((x) => Array.isArray(x) ? x : [x]);
503
+ for (const b of flat) {
504
+ if (!b || typeof b !== "object") continue;
505
+ if ((b.type === "text" || b.kind === "text") && typeof b.text === "string" && b.text.trim() !== "") parts.push(b.text.trim());
506
+ }
507
+ return parts.length > 0 ? parts.join(" ") : null;
508
+ }
509
+ return null;
510
+ }
511
+ /* Full user question text (all text blocks) for the hover bubble. */
512
+ function fullDialogueText(node) {
513
+ const d = node && node.data;
514
+ if (!d || typeof d !== "object") return null;
515
+ if (node.kind !== "user" && node.kind !== "input-message") return null;
516
+ if (typeof d.content === "string") {
517
+ const t = d.content.trim();
518
+ return t === "" ? null : t;
519
+ }
520
+ if (!Array.isArray(d.content)) return null;
521
+ const parts = [];
522
+ /* 官方 user.content 为嵌套数组(多 part),先展平 */
523
+ const flat = d.content.flatMap((x) => Array.isArray(x) ? x : [x]);
524
+ for (const b of flat) {
525
+ if (!b || typeof b !== "object") continue;
526
+ if ((b.type === "text" || b.kind === "text") && typeof b.text === "string" && b.text.trim() !== "") parts.push(b.text.trim());
527
+ }
528
+ return parts.length > 0 ? parts.join("\n") : null;
529
+ }
530
+ function previewFor(node) {
531
+ const d = node && node.data;
532
+ if (!d || typeof d !== "object") return null;
533
+ switch (node.kind) {
534
+ case "user":
535
+ case "steering":
536
+ case "context":
537
+ return firstText(d.content);
538
+ case "assistant-step": {
539
+ const text = firstText(d.blocks);
540
+ if (text !== null) return text;
541
+ if (Array.isArray(d.blocks) && d.blocks.some((b) => b && b.kind === "tool-call")) return "调用工具";
542
+ return d.status === "running" ? "进行中…" : null;
543
+ }
544
+ case "tool-call": {
545
+ const root = d.root;
546
+ if (!root || typeof root !== "object") return null;
547
+ if (root.isError === true) return "执行失败";
548
+ const name = typeof root.name === "string" ? root.name
549
+ : (root.call && typeof root.call.name === "string" ? root.call.name : null);
550
+ return name;
551
+ }
552
+ case "command":
553
+ return typeof d.name === "string" ? d.name + (typeof d.args === "string" && d.args !== "" ? " " + d.args : "") : null;
554
+ case "compaction":
555
+ return typeof d.summary === "string" ? d.summary : "上下文已压缩";
556
+ case "manual-compaction": {
557
+ const c = d.compaction;
558
+ if (c && typeof c.summary === "string") return c.summary;
559
+ return "手动压缩";
560
+ }
561
+ case "turn-error":
562
+ return typeof d.message === "string" ? d.message : "本轮出错";
563
+ case "turn-max-tokens":
564
+ return "达到输出上限";
565
+ case "model-retry": {
566
+ const n = Array.isArray(d.attempts) ? d.attempts.length : 0;
567
+ return "自动重试" + (n > 0 ? " ×" + n : "…");
568
+ }
569
+ case "unknown":
570
+ return typeof d.type === "string" ? "未知事件 " + d.type : "未知事件";
571
+ default:
572
+ return null;
573
+ }
574
+ }
575
+ function timeOf(node) {
576
+ const d = node && node.data;
577
+ if (!d || typeof d !== "object") return null;
578
+ const t = typeof d.time === "number" ? d.time
579
+ : (d.root && typeof d.root.time === "number" ? d.root.time : null);
580
+ return t;
581
+ }
582
+ function fmtTime(ms) {
583
+ if (typeof ms !== "number" || !isFinite(ms)) return "";
584
+ const d = new Date(ms);
585
+ const h = d.getHours();
586
+ const m = d.getMinutes();
587
+ return (h < 10 ? "0" + h : h) + ":" + (m < 10 ? "0" + m : m);
588
+ }
589
+ function turnOf(node) {
590
+ const loc = node && node.location;
591
+ if (!loc) return null;
592
+ if (loc.kind === "step" || loc.kind === "turn") return loc.turn.turn;
593
+ return null;
594
+ }
595
+ function truncate(text, max) {
596
+ if (typeof text !== "string") return "";
597
+ if (text.length <= max) return text;
598
+ return text.slice(0, max) + "…";
599
+ }
600
+ function deriveOutline(snap) {
601
+ const empty = { groups: [], count: 0, byKey: {} };
602
+ if (snap === null || snap === undefined) return empty;
603
+ const order = snap.chat && snap.chat.order;
604
+ const nodes = snap.chat && snap.chat.nodes;
605
+ if (!Array.isArray(order) || nodes === undefined) return empty;
606
+ const groups = [];
607
+ const byKey = {};
608
+ let current = null;
609
+ let count = 0;
610
+ for (const key of order) {
611
+ const node = nodes.get(key);
612
+ if (node === undefined) continue;
613
+ const meta = KIND_META[node.kind] || KIND_META.unknown;
614
+ const item = {
615
+ key: node.key,
616
+ kind: node.kind,
617
+ label: meta.label,
618
+ cls: meta.cls,
619
+ preview: previewFor(node),
620
+ search: dialogueText(node),
621
+ full: fullDialogueText(node),
622
+ time: fmtTime(timeOf(node)),
623
+ };
624
+ const turn = turnOf(node);
625
+ if (turn === null) {
626
+ groups.push({ turn: null, items: [item], gid: "x" + groups.length });
627
+ count += 1;
628
+ current = null;
629
+ continue;
630
+ }
631
+ if (current === null || current.turn !== turn) {
632
+ current = { turn: turn, items: [], gid: "t" + turn };
633
+ groups.push(current);
634
+ }
635
+ current.items.push(item);
636
+ count += 1;
637
+ }
638
+ for (const g of groups) {
639
+ const first = g.items[0];
640
+ if (g.turn === null) {
641
+ g.title = first.label;
642
+ g.subtitle = truncate(first.preview || "", 60);
643
+ g.headKey = first.key;
644
+ g.headIsUser = false;
645
+ } else {
646
+ g.title = "第 " + g.turn + " 轮";
647
+ const head = first.kind === "user" ? first : null;
648
+ g.headKey = head ? head.key : first.key;
649
+ g.headIsUser = head !== null;
650
+ g.subtitle = truncate((head ? head.preview : first.preview) || "", 60);
651
+ }
652
+ for (const it of g.items) byKey[it.key] = g.gid;
653
+ }
654
+ return { groups: groups, count: count, byKey: byKey };
655
+ }
656
+ let cacheKey = null;
657
+ let cacheValue = null;
658
+ function selectOutline(snap) {
659
+ if (snap !== cacheKey) {
660
+ cacheKey = snap;
661
+ cacheValue = deriveOutline(snap);
662
+ }
663
+ return cacheValue;
664
+ }
665
+
666
+ /* ---------- 新宿主大纲:官方 v0.1.2-rc.1 起会话节点从 session 快照
667
+ 挪到 chat target 快照(uiConversation.binding(...).target("chat"))。
668
+ 走其 legacy 兼容投影(有序 nodes + turnEnds 轮次边界)派生大纲。 ---------- */
669
+ const KIND_META_NEW = {
670
+ "user": { label: "你", cls: "traj-user" },
671
+ "input-message": { label: "你", cls: "traj-user" },
672
+ "steering": { label: "插话", cls: "traj-user" },
673
+ "context": { label: "上下文", cls: "traj-context" },
674
+ "assistant": { label: "助手", cls: "traj-assistant" },
675
+ "assistant-step": { label: "助手", cls: "traj-assistant" },
676
+ "tool-call": { label: "工具", cls: "traj-tool" },
677
+ "tool-result": { label: "工具", cls: "traj-tool" },
678
+ "request-prompt": { label: "请求", cls: "other-neutral" },
679
+ "command": { label: "命令", cls: "other-command" },
680
+ "compaction": { label: "压缩", cls: "traj-compacted" },
681
+ "manual-compaction": { label: "压缩", cls: "traj-compacted" },
682
+ "model-retry": { label: "重试", cls: "other-retry" },
683
+ "turn-error": { label: "出错", cls: "other-error" },
684
+ "turn-max-tokens": { label: "截断", cls: "other-error" },
685
+ /* 官方新节点类型(0.1.2 引入):turn-process 回合过程折叠、turn-tail 轮尾、system-prompt 系统提示 */
686
+ "turn-process": { label: "过程", cls: "traj-process" },
687
+ "turn-tail": { label: "轮尾", cls: "other-neutral" },
688
+ "system-prompt": { label: "系统提示", cls: "other-neutral" },
689
+ "unknown-surface": { label: "其他", cls: "other-neutral" },
690
+ "unknown": { label: "其他", cls: "other-neutral" },
691
+ };
692
+ function previewForNew(node) {
693
+ const d = node && node.data;
694
+ if (!d || typeof d !== "object") return null;
695
+ switch (node.kind) {
696
+ case "user":
697
+ case "input-message":
698
+ case "steering":
699
+ case "context":
700
+ return firstText(d.content);
701
+ case "assistant":
702
+ case "assistant-step": {
703
+ const text = firstText(d.blocks);
704
+ if (text !== null) return text;
705
+ if (Array.isArray(d.blocks) && d.blocks.some((b) => b && (b.kind === "tool-call" || b.kind === "tool-result"))) return "调用工具";
706
+ return null;
707
+ }
708
+ case "tool-result": {
709
+ if (d.isError === true) return "执行失败";
710
+ return d.call && typeof d.call.name === "string" ? d.call.name : "工具结果";
711
+ }
712
+ case "command":
713
+ return typeof d.name === "string" ? d.name + (typeof d.args === "string" && d.args !== "" ? " " + d.args : "") : "命令";
714
+ case "compaction":
715
+ return typeof d.summary === "string" ? d.summary : "上下文已压缩";
716
+ case "model-retry": {
717
+ const n = Array.isArray(d.attempts) ? d.attempts.length : 0;
718
+ return "自动重试" + (n > 0 ? " ×" + n : "…");
719
+ }
720
+ case "turn-error":
721
+ return typeof d.message === "string" ? d.message : "本轮出错";
722
+ case "turn-max-tokens":
723
+ return "达到输出上限";
724
+ case "unknown":
725
+ case "unknown-surface":
726
+ return typeof d.type === "string" ? "未知事件 " + d.type : "未知事件";
727
+ default:
728
+ return null;
729
+ }
730
+ }
731
+ function deriveOutlineNew(snap) {
732
+ const empty = { groups: [], count: 0, byKey: {} };
733
+ if (snap === null || snap === undefined) return empty;
734
+ /* 分组使用官方权威映射: timeline.turnOrder + locations.getTurn(turn) + nodes.get(key)。
735
+ 旧实现按 legacy 节点 seq 与 turnEnds(全局事件 seq)比较,两者不同源导致
736
+ 所有节点都判入第一轮(展开却见数百条)。 */
737
+ const turnOrder = (snap.timeline !== null && snap.timeline !== undefined && Array.isArray(snap.timeline.turnOrder)) ? snap.timeline.turnOrder : [];
738
+ const locations = snap.locations;
739
+ const nodesMap = snap.nodes;
740
+ if (locations === undefined || nodesMap === undefined) return empty;
741
+ /* 解析官方节点自带 location 的轮次(仿 locationCoordinates:
742
+ turn / step 两类可解析, unresolved 返回 null) */
743
+ function turnOfNode(node) {
744
+ const loc = node !== null && node !== undefined ? node.location : undefined;
745
+ if (loc === undefined || loc === null || typeof loc !== "object") return null;
746
+ if (loc.kind === "turn") return typeof loc.turn === "number" ? loc.turn : null;
747
+ if (loc.kind === "step") {
748
+ const t = loc.turn;
749
+ if (t !== undefined && t !== null && typeof t === "object" && typeof t.turn === "number") return t.turn;
750
+ }
751
+ return null;
752
+ }
753
+ /* 纯 seq 归轮:轮次边界(start/end seq)来自官方 timeline.turns,
754
+ 节点 location 即使为 unresolved 也能定位 —— 取"最大的 start.seq <= seq"。 */
755
+ function turnBySeq(timeline, seq) {
756
+ if (timeline === null || timeline === undefined) return null;
757
+ const turns = timeline.turns;
758
+ if (turns === undefined || turns === null || typeof turns.get !== "function") return null;
759
+ let best = null;
760
+ for (const turn of timeline.turnOrder) {
761
+ const loc = turns.get(turn);
762
+ if (loc === undefined || loc === null) continue;
763
+ const startSeq = loc.start !== undefined && loc.start !== null && typeof loc.start.seq === "number" ? loc.start.seq : null;
764
+ if (startSeq === null) continue;
765
+ if (startSeq > seq) break; /* turnOrder 升序, 之后都更晚 */
766
+ const endSeq = loc.end !== undefined && loc.end !== null && typeof loc.end.seq === "number" ? loc.end.seq : null;
767
+ if (endSeq !== null && endSeq < seq) continue;
768
+ best = turn;
769
+ }
770
+ return best;
771
+ }
772
+ const groups = [];
773
+ const byKey = {};
774
+ const seen = new Set();
775
+ let count = 0;
776
+ const mkItem = (node) => {
777
+ const meta = KIND_META_NEW[node.kind] || KIND_META_NEW.unknown;
778
+ /* 官方节点自带 {kind, data} 形状,直接复用文本辅助函数 */
779
+ const view = { kind: node.kind, data: node.data };
780
+ return {
781
+ key: String(node.key !== undefined && node.key !== null ? node.key : node.anchorSeq),
782
+ kind: node.kind,
783
+ label: meta.label,
784
+ cls: meta.cls,
785
+ preview: previewForNew(view),
786
+ search: dialogueText(view),
787
+ full: fullDialogueText(view),
788
+ time: null,
789
+ seq: typeof node.anchorSeq === "number" ? node.anchorSeq : null,
790
+ };
791
+ };
792
+ /* 主通道:官方 locations 索引 (turnOrder -> key[]) */
793
+ for (const turn of turnOrder) {
794
+ const keys = locations.getTurn(turn);
795
+ if (!Array.isArray(keys) || keys.length === 0) continue;
796
+ const items = [];
797
+ for (const key of keys) {
798
+ const node = nodesMap.get(key);
799
+ if (node === undefined || node === null) continue;
800
+ const item = mkItem(node);
801
+ items.push(item);
802
+ seen.add(item.key);
803
+ }
804
+ if (items.length === 0) continue;
805
+ groups.push({ turn: turn, items: items, gid: "t" + turn });
806
+ count += items.length;
807
+ }
808
+ /* 兜底:官方 locations 索引会对部分节点(例如被组装器排除在
809
+ order/locations 之外的 user 消息)漏挂; snap.nodes 是完整节点存储
810
+ (ChatNodeStore: get/values), 依节点自带 location 或 anchorSeq
811
+ 对照轮次边界补进对应轮次, 保证大纲完整。 */
812
+ if (typeof nodesMap.values === "function") {
813
+ for (const node of nodesMap.values()) {
814
+ if (node === undefined || node === null) continue;
815
+ const itemKey = String(node.key !== undefined && node.key !== null ? node.key : node.anchorSeq);
816
+ if (seen.has(itemKey)) continue;
817
+ let turn = turnOfNode(node);
818
+ if (turn === null && typeof node.anchorSeq === "number") turn = turnBySeq(snap.timeline, node.anchorSeq);
819
+ if (turn === null) continue;
820
+ let g = null;
821
+ for (const cand of groups) { if (cand.turn === turn) { g = cand; break; } }
822
+ if (g === null) {
823
+ g = { turn: turn, items: [], gid: "t" + turn };
824
+ groups.push(g);
825
+ }
826
+ g.items.push(mkItem(node));
827
+ byKey[itemKey] = g.gid;
828
+ seen.add(itemKey);
829
+ count += 1;
830
+ }
831
+ }
832
+ /* 汇总:轮次升序; 组内排序人类消息(你/插话)优先打头(与官方 rail 一致),
833
+ 其余按 anchorSeq(系统提示锚定在轮次起始但不应压过用户消息) */
834
+ groups.sort((a, b) => a.turn - b.turn);
835
+ for (const g of groups) {
836
+ const humanKind = (it) => it.kind === "user" || it.kind === "steering" || it.kind === "input-message";
837
+ g.items.sort((a, b) => {
838
+ const ha = humanKind(a) ? 0 : 1;
839
+ const hb = humanKind(b) ? 0 : 1;
840
+ if (ha !== hb) return ha - hb;
841
+ return ((a.seq === null ? 0 : a.seq) - (b.seq === null ? 0 : b.seq));
842
+ });
843
+ const first = g.items[0];
844
+ g.title = "第 " + g.turn + " 轮";
845
+ const head = humanKind(first) ? first : null;
846
+ g.headKey = head ? head.key : first.key;
847
+ g.headIsUser = head !== null;
848
+ g.subtitle = truncate((head ? head.preview : first.preview) || "", 60);
849
+ for (const it of g.items) byKey[it.key] = g.gid;
850
+ }
851
+ return { groups: groups, count: count, byKey: byKey };
852
+ }
853
+ /* 投影合并:官方 turnOutline 投影(全会话 {turn, seq, prompt, response} 摘要),
854
+ 与官方 rail 的 mergeTurnRailItems 同源。用它为缺失用户消息的轮次补一个
855
+ 合成"你"条目, 并为加载窗口外的轮次生成纯投影分组。 */
856
+ function mergeOutlineWithProjection(outline, proj) {
857
+ if (outline === null || outline === undefined) return null;
858
+ if (!Array.isArray(proj) || proj.length === 0) return null;
859
+ const byTurn = {};
860
+ for (const e of proj) {
861
+ if (e === null || typeof e !== "object") continue;
862
+ if (typeof e.turn !== "number" || !Number.isSafeInteger(e.turn)) continue;
863
+ byTurn[e.turn] = e;
864
+ }
865
+ const groups = [];
866
+ const byKey = {};
867
+ let count = 0;
868
+ let changed = false;
869
+ for (const g of outline.groups) {
870
+ const items = g.items.slice();
871
+ const projE = byTurn[g.turn];
872
+ const headIsUser = items.length > 0 && (items[0].kind === "user" || items[0].kind === "input-message");
873
+ if (projE !== undefined && typeof projE.prompt === "string" && projE.prompt !== "" && !headIsUser) {
874
+ items.unshift({
875
+ key: "proj-" + g.turn + "-user",
876
+ kind: "user", label: "你", cls: "traj-user",
877
+ preview: truncate(projE.prompt, 120),
878
+ search: projE.prompt,
879
+ full: projE.prompt,
880
+ time: null, seq: null, synthetic: true,
881
+ });
882
+ changed = true;
883
+ }
884
+ const first = items[0];
885
+ const ng = {
886
+ turn: g.turn, items: items, gid: g.gid,
887
+ title: "第 " + g.turn + " 轮",
888
+ headKey: first !== undefined ? first.key : g.headKey,
889
+ headIsUser: first !== undefined && (first.kind === "user" || first.kind === "input-message"),
890
+ subtitle: truncate((first ? first.preview : null) || "", 60),
891
+ };
892
+ groups.push(ng);
893
+ count += ng.items.length;
894
+ }
895
+ for (const numStr of Object.keys(byTurn)) {
896
+ const num = Number(numStr);
897
+ if (groups.some((g) => g.turn === num)) continue;
898
+ const projE = byTurn[num];
899
+ const items = [];
900
+ if (typeof projE.prompt === "string" && projE.prompt !== "") {
901
+ items.push({ key: "proj-" + num + "-user", kind: "user", label: "你", cls: "traj-user", preview: truncate(projE.prompt, 120), search: projE.prompt, full: projE.prompt, time: null, seq: null, synthetic: true });
902
+ }
903
+ if (typeof projE.response === "string" && projE.response !== "") {
904
+ items.push({ key: "proj-" + num + "-resp", kind: "assistant", label: "助手", cls: "traj-assistant", preview: truncate(projE.response, 120), search: projE.response, full: projE.response, time: null, seq: null, synthetic: true });
905
+ }
906
+ if (items.length === 0) continue;
907
+ const first = items[0];
908
+ groups.push({ turn: num, items: items, gid: "t" + num, title: "第 " + num + " 轮", headKey: first.key, headIsUser: true, subtitle: truncate(first.preview || "", 60) });
909
+ count += items.length;
910
+ changed = true;
911
+ }
912
+ if (!changed) return null;
913
+ groups.sort((a, b) => a.turn - b.turn);
914
+ for (const g of groups) for (const it of g.items) byKey[it.key] = g.gid;
915
+ return { groups: groups, count: count, byKey: byKey };
916
+ }
917
+ /* 新宿主 chat target 订阅:随 loadOlder 等更新自动重派生大纲 */
918
+ let chatUnsub = null;
919
+ let projUnsub = null;
920
+
921
+ /* ---------- DOM helpers: jump + position tracking ---------- */
922
+ function findAnchor(key) {
923
+ const rows = document.querySelectorAll("[data-chat-anchor-key]");
924
+ for (const row of rows) {
925
+ if (row.getAttribute("data-chat-anchor-key") === key) return row;
926
+ }
927
+ return null;
928
+ }
929
+ let lastActive = null;
930
+ function jumpTo(key) {
931
+ try {
932
+ const el = findAnchor(key);
933
+ if (el === null) return false;
934
+ el.scrollIntoView({ behavior: "smooth", block: "start" });
935
+ lastActive = key;
936
+ setState({ activeKey: key });
937
+ return true;
938
+ } catch (err) {
939
+ console.error(err);
940
+ return false;
941
+ }
942
+ }
943
+ function computeActiveKey() {
944
+ try {
945
+ const scrollport = document.querySelector("[data-conversation-scroll]");
946
+ if (scrollport === null) return null;
947
+ const vp = scrollport.getBoundingClientRect();
948
+ const rows = document.querySelectorAll("[data-chat-anchor-key]");
949
+ let found = null;
950
+ for (const row of rows) {
951
+ const rect = row.getBoundingClientRect();
952
+ if (rect.bottom > vp.top) { found = row; break; }
953
+ }
954
+ if (found === null && rows.length > 0) found = rows[rows.length - 1];
955
+ return found === null ? null : found.getAttribute("data-chat-anchor-key");
956
+ } catch (err) {
957
+ console.error(err);
958
+ return null;
959
+ }
960
+ }
961
+
962
+ let panelEl = null;
963
+ let listEl = null;
964
+ let dragMove = null;
965
+ let resizeStart = null; /* { x, y, w, h, docked } for the corner grip */
966
+ const itemEls = {};
967
+ const groupEls = {};
968
+
969
+ /* ---------- header toggle button ---------- */
970
+ function ToggleButton(props) {
971
+ const snap = useStore();
972
+ const outline = props.useSession(selectOutline);
973
+ const hasMore = props.useSession((s) => s.hasMore);
974
+ const loadingOlder = props.useSession((s) => s.loadingOlder);
975
+ /* 投影通道 B:slot 标准 props 的 useProjection(官方 rail 同款) */
976
+ const turnOutlineProj = typeof props.useProjection === "function" ? props.useProjection("turnOutline") : undefined;
977
+ React.useEffect(() => {
978
+ if (!NEW_HOST || turnOutlineProj === undefined || !Array.isArray(turnOutlineProj) || turnOutlineProj.length === 0) return;
979
+ try {
980
+ const merged = mergeOutlineWithProjection(state.outline, turnOutlineProj);
981
+ if (merged !== null) setState({ outline: merged });
982
+ } catch (err) {
983
+ console.error("[cnvnav] projection(use) merge failed", err);
984
+ }
985
+ }, [turnOutlineProj]);
986
+ React.useEffect(() => {
987
+ /* 旧宿主:大纲来自 useSession 的 chat 快照;新宿主:大纲由下方
988
+ chat target 订阅独立驱动,此处只同步会话级字段。 */
989
+ setState(Object.assign({
990
+ sessionId: props.sessionId, hasMore: hasMore, loadingOlder: loadingOlder,
991
+ }, NEW_HOST ? {} : { outline: outline }));
992
+ }, [outline, props.sessionId, hasMore, loadingOlder]);
993
+ React.useEffect(() => {
994
+ /* 实时重查宿主(apply 时 uiConversation 可能尚未物化, 不依赖 NEW_HOST 常量) */
995
+ if (getUiConv() === undefined) {
996
+ /* 旧宿主:大纲由上方 effect 的 props.useSession 派生, 无需此处订阅 */
997
+ return;
998
+ }
999
+ if (chatUnsub !== null) { chatUnsub(); chatUnsub = null; }
1000
+ if (projUnsub !== null) { projUnsub(); projUnsub = null; }
1001
+ /* 会话恢复的时序竞争修复:binding/target 未物化时不再静默放弃,
1002
+ 订阅 sessions.list + 定时兜底自动重试(重启后导航空的根因) */
1003
+ const trySubscribe = () => {
1004
+ if (props.sessionId == null || sessions === undefined) {
1005
+ setState({ outline: null });
1006
+ return true; /* 无会话:状态已处理,无需重试 */
1007
+ }
1008
+ try {
1009
+ const binding = sessions.binding(props.sessionId);
1010
+ if (binding === undefined) {
1011
+ return false;
1012
+ }
1013
+ const uiConv = getUiConv(); /* 兜底重查, 不依赖 apply 时刻的快照 */
1014
+ if (uiConv === undefined) {
1015
+ return false;
1016
+ }
1017
+ const target = uiConv.binding(binding).target("chat");
1018
+ if (target === undefined) {
1019
+ return false;
1020
+ }
1021
+ /* 投影兜底:官方 turnOutline 投影(全会话轮次摘要, 含加载窗口外轮次)
1022
+ 与官方 rail 的 mergeTurnRailItems 同源 —— 用它补上被组装器
1023
+ 漏掉的用户消息与窗口外轮次。 */
1024
+ const projStore = sessions.projectionStore !== undefined && typeof sessions.projectionStore === "function"
1025
+ ? sessions.projectionStore(props.sessionId) : undefined;
1026
+ const mergeProj = () => {
1027
+ try {
1028
+ const proj = projStore !== undefined ? projStore.get("turnOutline") : undefined;
1029
+ const merged = mergeOutlineWithProjection(state.outline, proj);
1030
+ if (merged !== null) setState({ outline: merged });
1031
+ } catch (err) {
1032
+ console.error("[cnvnav] projection merge failed", err);
1033
+ }
1034
+ };
1035
+ const update = () => {
1036
+ try {
1037
+ const snap = target.getSnapshot();
1038
+ const outline = deriveOutlineNew(snap);
1039
+ setState({ outline: outline });
1040
+ mergeProj();
1041
+ } catch (err) {
1042
+ console.error("[cnvnav] derive failed", err);
1043
+ setState({ outline: null });
1044
+ }
1045
+ };
1046
+ chatUnsub = target.subscribe(update);
1047
+ update();
1048
+ if (projStore !== undefined) projUnsub = projStore.subscribeAny(mergeProj);
1049
+ mergeProj();
1050
+ return true;
1051
+ } catch (err) {
1052
+ console.error("[cnvnav] subscribe failed", err);
1053
+ return false;
1054
+ }
1055
+ };
1056
+ if (trySubscribe()) return;
1057
+ let dispose = null;
1058
+ let timer = null;
1059
+ let stopped = false;
1060
+ const retry = () => {
1061
+ if (stopped || chatUnsub !== null) return;
1062
+ if (trySubscribe()) cleanup();
1063
+ };
1064
+ const cleanup = () => {
1065
+ stopped = true;
1066
+ if (dispose !== null) { dispose(); dispose = null; }
1067
+ if (timer !== null) { window.clearInterval(timer); timer = null; }
1068
+ };
1069
+ dispose = sessions.list.subscribe(retry);
1070
+ timer = window.setInterval(retry, 400);
1071
+ return cleanup;
1072
+ }, [props.sessionId]);
1073
+ React.useEffect(() => {
1074
+ if (state.sessionId !== null && state.sessionId !== props.sessionId) {
1075
+ lastActive = null;
1076
+ setState({ activeKey: null, expanded: {} });
1077
+ }
1078
+ }, [props.sessionId]);
1079
+ return React.createElement(Tooltip, {
1080
+ label: snap.open ? "收起会话导航" : "打开会话导航",
1081
+ side: "bottom",
1082
+ delayMs: 500,
1083
+ },
1084
+ React.createElement("span", null,
1085
+ React.createElement(Button, {
1086
+ variant: snap.open ? "primary" : "ghost",
1087
+ size: "sm",
1088
+ icon: React.createElement(ToggleIcon),
1089
+ "aria-pressed": snap.open,
1090
+ onClick: () => setState({ open: !snap.open }),
1091
+ }, "导航"),
1092
+ ),
1093
+ );
1094
+ }
1095
+
1096
+ /* ---------- floating navigator panel ---------- */
1097
+ function Panel() {
1098
+ const snap = useStore();
1099
+ const [filter, setFilter] = React.useState("");
1100
+ /* 新宿主搜索优先:面板打开时搜索框常驻;旧宿主保持隐藏搜索图标 */
1101
+ const [searchOpen, setSearchOpen] = React.useState(false);
1102
+ const viewMode = snap.viewMode; /* full | hidden | minimal-left (极简·右已注释) */
1103
+ const turnsHidden = viewMode !== "full";
1104
+ function nextModeLabel() {
1105
+ return viewMode === "full" ? "隐藏轮次" : viewMode === "hidden" ? "极简·左" : "显示轮次";
1106
+ }
1107
+ function cycleMode() {
1108
+ const next = viewMode === "full" ? "hidden" : viewMode === "hidden" ? "minimal-left" : "full";
1109
+ setState({ viewMode: next });
1110
+ persistUi();
1111
+ }
1112
+ function toggleSearch() {
1113
+ if (searchOpen) setFilter("");
1114
+ setSearchOpen(!searchOpen);
1115
+ }
1116
+ /* ---------- 拖动定位(仅面板形式, 极简条不参与) ---------- */
1117
+ function startDrag(e) {
1118
+ if (e.button !== 0) return;
1119
+ if (typeof e.target.closest === "function" && e.target.closest("button") !== null) return;
1120
+ const el = panelEl;
1121
+ if (el === null) return;
1122
+ dragMove = { startX: e.clientX, startY: e.clientY, baseLeft: el.offsetLeft, baseTop: el.offsetTop, el: el, dragging: false };
1123
+ try { e.currentTarget.setPointerCapture(e.pointerId); } catch (err) { /* 忽略捕获失败 */ }
1124
+ }
1125
+ function moveDrag(e) {
1126
+ const d = dragMove;
1127
+ if (d === null) return;
1128
+ const dx = e.clientX - d.startX;
1129
+ const dy = e.clientY - d.startY;
1130
+ if (!d.dragging && (Math.abs(dx) > 3 || Math.abs(dy) > 3)) {
1131
+ d.dragging = true;
1132
+ d.el.classList.add("cnvnav-dragging");
1133
+ }
1134
+ if (!d.dragging) return;
1135
+ const vw = document.documentElement.clientWidth || 1000;
1136
+ const vh = document.documentElement.clientHeight || 800;
1137
+ const w = d.el.offsetWidth || 288;
1138
+ const h = d.el.offsetHeight || 200;
1139
+ const left = Math.min(Math.max(8, d.baseLeft + dx), Math.max(8, vw - w - 8));
1140
+ const top = Math.min(Math.max(8, d.baseTop + dy), Math.max(8, vh - h - 8));
1141
+ d.el.style.left = Math.round(left) + "px";
1142
+ d.el.style.top = Math.round(top) + "px";
1143
+ }
1144
+ function endDrag() {
1145
+ const d = dragMove;
1146
+ dragMove = null;
1147
+ if (d === null || !d.dragging) return;
1148
+ d.el.classList.remove("cnvnav-dragging");
1149
+ const r = d.el.getBoundingClientRect();
1150
+ /* 拖完同步封顶(与 place() 浮动分支一致): 上边缘固定, 高度至多半屏 */
1151
+ const vh = document.documentElement.clientHeight || 800;
1152
+ d.el.style.maxHeight = Math.max(160, Math.round(Math.min(vh / 2, vh - Math.max(8, Math.round(r.top)) - 8))) + "px";
1153
+ setState({ docked: false, userPos: { left: Math.round(r.left), top: Math.round(r.top) } });
1154
+ persistUi();
1155
+ }
1156
+ /* ---------- 左下角拖拽缩放(仅普通面板) ---------- */
1157
+ /* 语义化最小高度: 固定区(头部/顶部按钮栏/底部栏) + 至少 3 行轮次 + 内边距余量,
1158
+ 不用固定 px, 任何主题/字号下都能看到对话内容 */
1159
+ function minPanelH() {
1160
+ const p = panelEl;
1161
+ if (p === null) return 240;
1162
+ let fixed = 0;
1163
+ const head = p.querySelector(".cnvnav-head");
1164
+ const topbar = p.querySelector(".cnvnav-topbar");
1165
+ const footer = p.querySelector(".cnvnav-footer");
1166
+ if (head !== null) fixed += head.offsetHeight;
1167
+ if (topbar !== null) fixed += topbar.offsetHeight;
1168
+ if (footer !== null) fixed += footer.offsetHeight;
1169
+ const row = p.querySelector(".cnvnav-group-row") || p.querySelector(".cnvnav-item");
1170
+ const rowH = row !== null ? row.offsetHeight : 34;
1171
+ return Math.ceil(fixed + rowH * 3 + 26);
1172
+ }
1173
+ function startResize(e) {
1174
+ if (e.button !== 0) return;
1175
+ if (resizeStart !== null) return;
1176
+ const el = panelEl;
1177
+ if (el === null) return;
1178
+ resizeStart = {
1179
+ startX: e.clientX, startY: e.clientY,
1180
+ baseW: el.offsetWidth || 288,
1181
+ baseH: el.offsetHeight || 200,
1182
+ baseMaxH: (function () { const v = parseInt(el.style.maxHeight, 10); return Number.isFinite(v) && v > 0 ? v : (el.offsetHeight || 200); })(),
1183
+ el: el,
1184
+ };
1185
+ try { e.currentTarget.setPointerCapture(e.pointerId); } catch (err) { /* 忽略 */ }
1186
+ }
1187
+ function moveResize(e) {
1188
+ const d = resizeStart;
1189
+ if (d === null) return;
1190
+ const vh = document.documentElement.clientHeight || 800;
1191
+ const dx = e.clientX - d.startX;
1192
+ const dy = e.clientY - d.startY;
1193
+ /* 高度用 maxHeight + height:auto: 实际高度 = min(内容, 上限)。
1194
+ 以拖拽起点为基准线性跟随鼠标(无漂移/无状态跳变):
1195
+ 拖小立即压缩出滚动, 拖大在内容充足时线性恢复更多行;
1196
+ 内容不足时面板保持内容高(拖动无感, 属预期)。 */
1197
+ const r = d.el.getBoundingClientRect();
1198
+ const sp = document.querySelector("[data-conversation-scroll]");
1199
+ /* 上限必须与 place() 各分支一致, 否则松手后会被 place 压回造成跳变:
1200
+ docked/常规 = min(容器高-24, vh-40); 自由位置 = vh-顶-8 */
1201
+ const sr = sp !== null ? sp.getBoundingClientRect() : null;
1202
+ const cap = Math.max(minPanelH(), Math.round(
1203
+ state.docked && sr !== null
1204
+ ? Math.min(sr.height - 24, vh - 40)
1205
+ : vh - r.top - 8
1206
+ ));
1207
+ const nw = Math.min(Math.max(240, d.baseW - dx), 560);
1208
+ /* 目标上限 = 起点可见高 + 总位移, 线性且可逆 */
1209
+ const nMaxH = Math.min(Math.max(minPanelH(), d.baseH + dy), cap);
1210
+ const left = Math.max(8, r.right - nw);
1211
+ d.el.style.width = Math.round(nw) + "px";
1212
+ d.el.style.left = Math.round(left) + "px";
1213
+ d.el.style.height = "";
1214
+ d.el.style.maxHeight = Math.round(nMaxH) + "px";
1215
+ }
1216
+ function endResize() {
1217
+ const d = resizeStart;
1218
+ resizeStart = null;
1219
+ if (d === null) return;
1220
+ const el = d.el;
1221
+ const rect = el.getBoundingClientRect();
1222
+ const nw = el.offsetWidth;
1223
+ const parsed = parseInt(el.style.maxHeight, 10);
1224
+ const nh = Number.isFinite(parsed) && parsed > 0 ? parsed : null;
1225
+ if (state.docked) {
1226
+ setState({ panelWidth: Math.round(nw), panelHeight: nh });
1227
+ } else {
1228
+ setState({ panelWidth: Math.round(nw), panelHeight: nh, userPos: { left: Math.round(rect.left), top: Math.round(rect.top) } });
1229
+ }
1230
+ persistUi();
1231
+ }
1232
+
1233
+ React.useEffect(() => {
1234
+ if (!snap.open) return;
1235
+ let lastKey = null;
1236
+ let stableTicks = 0;
1237
+ let misses = 0;
1238
+ function place() {
1239
+ const panel = panelEl;
1240
+ if (panel === null) return false;
1241
+ if (dragMove !== null) return true;
1242
+ const vw = document.documentElement.clientWidth || 1000;
1243
+ const vh = document.documentElement.clientHeight || 800;
1244
+ /* 新宿主 dock 右侧需让出官方 rail 的 28px 竖带(right: calc(12px - clearance) + width:28px),
1245
+ 留 40px 总避让;旧宿主维持 12px。运行时实时重查, 不依赖 apply 时刻的检测结果。 */
1246
+ const railClearance = isNewHostNow() ? 40 : 12;
1247
+ const mini = /* viewMode === "minimal" || */ viewMode === "minimal-left";
1248
+ const alignLeft = viewMode === "minimal-left";
1249
+ const width = (state.panelWidth || 288);
1250
+ /* 面板宽度对非极简模式统一生效, 确保拖动(未停靠)状态下也能实时刷新 */
1251
+ if (!mini) panel.style.width = width + "px";
1252
+ /* 面板被拖走后不再自动停靠: 夹进视口、上边缘固定, 展开高度至多半屏以便下方留白 */
1253
+ if (!mini && !state.docked && state.userPos !== null) {
1254
+ const w = panel.offsetWidth || 288;
1255
+ const h = panel.offsetHeight || 200;
1256
+ const left = Math.min(Math.max(8, Math.round(state.userPos.left)), Math.max(8, vw - w - 8));
1257
+ const top = Math.min(Math.max(8, Math.round(state.userPos.top)), Math.max(8, vh - h - 8));
1258
+ const fh = state.panelHeight;
1259
+ const cap = Math.max(minPanelH(), Math.round(vh - top - 8));
1260
+ const def = Math.max(minPanelH(), Math.round(Math.min(vh / 2, vh - top - 8)));
1261
+ panel.style.height = "";
1262
+ panel.style.maxHeight = Math.max(minPanelH(), Math.min(typeof fh === "number" && fh > 0 ? fh : def, cap)) + "px";
1263
+ panel.style.left = left + "px";
1264
+ panel.style.top = top + "px";
1265
+ panel.classList.remove("cnvnav-panel-unplaced");
1266
+ return true;
1267
+ }
1268
+ const sp = document.querySelector("[data-conversation-scroll]");
1269
+ const MINI_H = 220;
1270
+ if (sp === null) {
1271
+ /* 会话视图可能尚未挂载(刷新/重启/切会话时面板先就位);
1272
+ 非会话页(设置页等)则永远没有该容器。
1273
+ 连续 15 次(约 6s)仍未出现才回退到视口定位, 否则保持隐藏等待校准。 */
1274
+ misses += 1;
1275
+ if (misses >= 15) {
1276
+ if (mini) {
1277
+ /* 极简: 与普通面板共用同一 DOM 根, 必须先清除非极简 resize 残留的宽高,
1278
+ 极简的宽度完全由 CSS 决定(收起 18px / 悬停 288px) */
1279
+ panel.style.width = "";
1280
+ panel.style.height = "";
1281
+ /* 水平锚定, 右对齐由 CSS right 负责, 左对齐钉在视口左侧 */
1282
+ if (alignLeft) {
1283
+ panel.style.left = "12px";
1284
+ panel.style.right = "auto";
1285
+ } else {
1286
+ panel.style.left = "";
1287
+ panel.style.right = "";
1288
+ }
1289
+ panel.style.top = Math.max(8, Math.round((vh - MINI_H) / 2)) + "px";
1290
+ panel.style.maxHeight = MINI_H + "px";
1291
+ } else {
1292
+ panel.style.left = Math.max(8, vw - width - railClearance) + "px";
1293
+ panel.style.top = Math.max(8, 60) + "px";
1294
+ const fh = state.panelHeight;
1295
+ const cap = Math.round(vh - 90);
1296
+ const def = cap;
1297
+ panel.style.height = "";
1298
+ panel.style.maxHeight = Math.max(minPanelH(), Math.min(typeof fh === "number" && fh > 0 ? fh : def, cap)) + "px";
1299
+ }
1300
+ panel.classList.remove("cnvnav-panel-unplaced");
1301
+ return true;
1302
+ }
1303
+ return false;
1304
+ }
1305
+ const r = sp.getBoundingClientRect();
1306
+ const key = mini
1307
+ ? [Math.round(r.left), Math.round(r.top), Math.round(r.height), Math.round(vh)].join("|")
1308
+ : [Math.round(r.top), Math.round(r.height), Math.round(vw), Math.round(vh)].join("|");
1309
+ if (key !== lastKey) {
1310
+ lastKey = key;
1311
+ if (mini) {
1312
+ /* 极简: 与普通面板共用同一 DOM 根, 必须先清除非极简 resize 残留的宽高,
1313
+ 极简的宽度完全由 CSS 决定(收起 18px / 悬停 288px) */
1314
+ panel.style.width = "";
1315
+ panel.style.height = "";
1316
+ /* 右对齐由 CSS(right)锚定视口右缘, 左对齐钉在对话区左缘 */
1317
+ if (alignLeft) {
1318
+ panel.style.left = Math.max(8, Math.round(r.left + 12)) + "px";
1319
+ panel.style.right = "auto";
1320
+ } else {
1321
+ panel.style.left = "";
1322
+ panel.style.right = "";
1323
+ }
1324
+ panel.style.top = Math.max(8, Math.round(r.top + (r.height - MINI_H) / 2)) + "px";
1325
+ panel.style.maxHeight = MINI_H + "px";
1326
+ } else {
1327
+ panel.style.left = Math.max(8, vw - width - railClearance) + "px";
1328
+ panel.style.top = Math.max(8, Math.round(r.top + 8)) + "px";
1329
+ const fh = state.panelHeight;
1330
+ const cap = Math.max(minPanelH(), Math.round(Math.min(r.height - 24, vh - 40)));
1331
+ const def = cap;
1332
+ panel.style.height = "";
1333
+ panel.style.maxHeight = Math.max(minPanelH(), Math.min(typeof fh === "number" && fh > 0 ? fh : def, cap)) + "px";
1334
+ }
1335
+ }
1336
+ panel.classList.remove("cnvnav-panel-unplaced");
1337
+ return true;
1338
+ }
1339
+ place();
1340
+ /* 每 400ms 复查: 容器出现即校准; 布局连续 3 次无变化后停止 */
1341
+ let id = setInterval(() => {
1342
+ const before = lastKey;
1343
+ if (!place()) return;
1344
+ if (lastKey !== null && lastKey === before) {
1345
+ stableTicks += 1;
1346
+ if (stableTicks >= 3) clearInterval(id);
1347
+ } else {
1348
+ stableTicks = 0;
1349
+ }
1350
+ }, 400);
1351
+ window.addEventListener("resize", place);
1352
+ /* 左对齐极简条需跟随左侧栏展开/折叠: 对话区宽度变化时重新校准 */
1353
+ let ro = null;
1354
+ const roTarget = document.querySelector("[data-conversation-scroll]");
1355
+ if (typeof ResizeObserver === "function" && roTarget !== null) {
1356
+ ro = new ResizeObserver(() => { place(); });
1357
+ ro.observe(roTarget);
1358
+ }
1359
+ return () => {
1360
+ clearInterval(id);
1361
+ window.removeEventListener("resize", place);
1362
+ if (ro !== null) ro.disconnect();
1363
+ };
1364
+ }, [snap.open, snap.sessionId, viewMode, state.docked, state.userPos, state.panelWidth, state.panelHeight]);
1365
+
1366
+ React.useEffect(() => {
1367
+ if (!snap.open) return;
1368
+ const run = () => {
1369
+ const key = computeActiveKey();
1370
+ if (key !== lastActive) {
1371
+ lastActive = key;
1372
+ setState({ activeKey: key });
1373
+ }
1374
+ };
1375
+ const update = timer !== undefined ? timer.throttle(run, 120) : run;
1376
+ document.addEventListener("scroll", update, true);
1377
+ window.addEventListener("resize", update);
1378
+ run();
1379
+ return () => {
1380
+ document.removeEventListener("scroll", update, true);
1381
+ window.removeEventListener("resize", update);
1382
+ if (timer !== undefined && typeof update.dispose === "function") update.dispose();
1383
+ };
1384
+ }, [snap.open]);
1385
+
1386
+ React.useEffect(() => {
1387
+ if (!snap.open || snap.activeKey === null) return;
1388
+ let target = itemEls[snap.activeKey];
1389
+ if (target === undefined || target === null || (typeof target.closest === "function" && target.closest(".cnvnav-steps-collapsed") !== null)) {
1390
+ const gid = snap.outline !== null ? snap.outline.byKey[snap.activeKey] : undefined;
1391
+ if (gid !== undefined) target = groupEls[gid];
1392
+ }
1393
+ const list = listEl;
1394
+ if (target !== undefined && target !== null && list !== null) {
1395
+ const lr = list.getBoundingClientRect();
1396
+ const er = target.getBoundingClientRect();
1397
+ const rel = er.top - lr.top + list.scrollTop;
1398
+ if (rel < list.scrollTop || rel + er.height > list.scrollTop + list.clientHeight) {
1399
+ list.scrollTo({ top: Math.max(0, rel - list.clientHeight / 2), behavior: "smooth" });
1400
+ }
1401
+ }
1402
+ }, [snap.open, snap.activeKey]);
1403
+
1404
+ const groups = snap.outline !== null ? snap.outline.groups : [];
1405
+ const count = snap.outline !== null ? snap.outline.count : 0;
1406
+ const hasExpanded = Object.keys(snap.expanded).some((k) => snap.expanded[k] === true);
1407
+
1408
+ /* ---------- 极简模式(左对齐): 只保留定位 (极简·右已注释,待共存方案) ---------- */
1409
+ if (/* viewMode === "minimal" || */ viewMode === "minimal-left") {
1410
+ const miniRows = [];
1411
+ for (const g of groups) {
1412
+ if (g.turn === null) continue;
1413
+ miniRows.push({
1414
+ key: g.headKey,
1415
+ text: g.subtitle,
1416
+ full: g.headIsUser && g.items.length > 0 ? g.items[0].full : null,
1417
+ active: g.items.some((it) => it.key === snap.activeKey),
1418
+ });
1419
+ }
1420
+ return React.createElement("div", {
1421
+ ref: (el) => { panelEl = el; },
1422
+ className: "cnvnav-mini-wrap" + (viewMode === "minimal-left" ? " cnvnav-mini-wrap-left" : "") + " cnvnav-panel-unplaced" + (snap.open ? "" : " cnvnav-panel-hidden"),
1423
+ role: "complementary",
1424
+ "aria-label": "会话导航",
1425
+ "aria-hidden": !snap.open,
1426
+ },
1427
+ React.createElement(Tooltip, {
1428
+ label: nextModeLabel(),
1429
+ side: "top",
1430
+ delayMs: 500,
1431
+ },
1432
+ React.createElement("span", { className: "cnvnav-mini-fab" },
1433
+ React.createElement(Button, {
1434
+ variant: "ghost",
1435
+ size: "sm",
1436
+ icon: React.createElement(SwitchTurnsIcon),
1437
+ "aria-pressed": true,
1438
+ onClick: () => cycleMode(),
1439
+ }),
1440
+ ),
1441
+ ),
1442
+ React.createElement("div", { className: "cnvnav-mini-panel" },
1443
+ React.createElement("div", {
1444
+ ref: (el) => { listEl = el; },
1445
+ className: "cnvnav-mini-list",
1446
+ },
1447
+ groups.length === 0
1448
+ ? React.createElement("div", { className: "cnvnav-empty" }, "当前会话暂无对话节点")
1449
+ : miniRows.map((r) => React.createElement(Tooltip, {
1450
+ key: r.key,
1451
+ label: r.full !== null ? r.full : "跳转到此位置",
1452
+ side: "top",
1453
+ delayMs: 500,
1454
+ maxWidth: 340,
1455
+ },
1456
+ React.createElement("button", {
1457
+ type: "button",
1458
+ ref: (el) => { itemEls[r.key] = el; },
1459
+ className: "cnvnav-mini-item" + (r.active ? " cnvnav-mini-item-active" : ""),
1460
+ onClick: () => jumpTo(r.key),
1461
+ }, r.text),
1462
+ )),
1463
+ ),
1464
+ ),
1465
+ );
1466
+ }
1467
+
1468
+ const kw = filter.trim().toLowerCase();
1469
+ const filtering = kw !== "";
1470
+ function hit(text) {
1471
+ return typeof text === "string" && text !== "" && text.toLowerCase().indexOf(kw) !== -1;
1472
+ }
1473
+ /* Split text into nodes, wrapping every case-insensitive keyword match in a <mark>. */
1474
+ function highlightedNodes(text, kw) {
1475
+ if (typeof text !== "string" || kw === "") return [text];
1476
+ const lower = text.toLowerCase();
1477
+ const out = [];
1478
+ let i = 0;
1479
+ let m = 0;
1480
+ for (;;) {
1481
+ const idx = lower.indexOf(kw, i);
1482
+ if (idx === -1) { if (i < text.length) out.push(text.slice(i)); break; }
1483
+ if (idx > i) out.push(text.slice(i, idx));
1484
+ out.push(React.createElement("mark", { key: "m" + (m++), className: "cnvnav-hl" }, text.slice(idx, idx + kw.length)));
1485
+ i = idx + kw.length;
1486
+ }
1487
+ return out;
1488
+ }
1489
+ /* Window the text around the first keyword match (so the keyword is visible,
1490
+ not hidden behind head-truncation), then highlight every occurrence. */
1491
+ function snippetNodes(text, kw, before, after) {
1492
+ if (typeof text !== "string") return [text];
1493
+ const idx = text.toLowerCase().indexOf(kw);
1494
+ if (idx === -1) return highlightedNodes(text, kw);
1495
+ const start = Math.max(0, idx - before);
1496
+ const end = Math.min(text.length, idx + kw.length + after);
1497
+ const s = text.slice(start, end);
1498
+ const parts = [];
1499
+ if (start > 0) parts.push("…");
1500
+ parts.push(...highlightedNodes(s, kw));
1501
+ if (end < text.length) parts.push("…");
1502
+ return parts;
1503
+ }
1504
+ function renderItem(item, staggerMs) {
1505
+ const useSnippet = filtering && hit(item.search);
1506
+ const preview = useSnippet ? snippetNodes(item.search, kw, 5, 60) : item.preview;
1507
+ const hasPreview = preview !== null && preview !== "" && (!Array.isArray(preview) || preview.length > 0);
1508
+ return React.createElement("button", {
1509
+ key: item.key,
1510
+ type: "button",
1511
+ ref: (el) => { itemEls[item.key] = el; },
1512
+ className: "cnvnav-item" + (item.key === snap.activeKey ? " cnvnav-item-active" : "") + (staggerMs !== undefined ? " cnvnav-fade-item" : ""),
1513
+ style: staggerMs !== undefined ? { animationDelay: staggerMs + "ms" } : undefined,
1514
+ onClick: () => jumpTo(item.key),
1515
+ },
1516
+ React.createElement("span", { className: "cnvnav-badge cnvnav-badge-" + item.cls }, item.label),
1517
+ hasPreview
1518
+ ? React.createElement("span", { className: "cnvnav-item-preview" }, preview)
1519
+ : null,
1520
+ item.time !== ""
1521
+ ? React.createElement("span", { className: "cnvnav-item-time" }, item.time)
1522
+ : null,
1523
+ );
1524
+ }
1525
+ function renderGroupHead(g, groupActive, chevron) {
1526
+ const headSearch = g.items.length > 0 ? g.items[0].search : null;
1527
+ const headFull = g.headIsUser && g.items.length > 0 ? g.items[0].full : null;
1528
+ const useSnippet = filtering && hit(headSearch);
1529
+ const sub = useSnippet ? snippetNodes(headSearch, kw, 5, 55) : g.subtitle;
1530
+ const hasSub = sub !== "" && (!Array.isArray(sub) || sub.length > 0);
1531
+ /* 标题策略:
1532
+ 显示轮次: 轮次行 = 加粗「第 N 轮」; 压缩等非轮次行 = 加粗类型标签, 视作轮次同级
1533
+ 隐藏轮次: 每行统一轨迹徽标 — 轮次头 = 「用户」业务蓝(助手开头则助手紫), 非轮次行 = 各自徽标(压缩=中性灰) */
1534
+ let titleNode = null;
1535
+ if (g.turn === null) {
1536
+ if (turnsHidden && g.items.length > 0) {
1537
+ const headItem = g.items[0];
1538
+ titleNode = React.createElement("span", {
1539
+ className: "cnvnav-badge cnvnav-badge-" + headItem.cls,
1540
+ }, headItem.label);
1541
+ } else {
1542
+ titleNode = React.createElement("span", { className: "cnvnav-group-title" }, g.title);
1543
+ }
1544
+ } else if (!turnsHidden) {
1545
+ titleNode = React.createElement("span", { className: "cnvnav-group-title" }, g.title);
1546
+ } else if (g.items.length > 0) {
1547
+ /* 组头徽标优先取用户节点(官方节点顺序里 turn-process 常在 user 之前,
1548
+ 直接取 items[0] 会显示「过程」而非「用户」) */
1549
+ const headItem = g.items.find((it) => it.kind === "user" || it.kind === "input-message") ?? g.items[0];
1550
+ titleNode = React.createElement("span", {
1551
+ className: "cnvnav-badge cnvnav-badge-" + headItem.cls,
1552
+ }, headItem.kind === "user" ? "用户" : headItem.label);
1553
+ }
1554
+ return React.createElement("div", {
1555
+ ref: (el) => { groupEls[g.gid] = el; },
1556
+ className: "cnvnav-group-row" + (groupActive ? " cnvnav-group-row-active" : ""),
1557
+ },
1558
+ React.createElement(Tooltip, {
1559
+ label: headFull !== null ? headFull : "跳转到此位置", maxWidth: 340,
1560
+ side: "top",
1561
+ delayMs: 500,
1562
+ },
1563
+ React.createElement("span", { className: "cnvnav-group-head-wrap" },
1564
+ React.createElement("button", {
1565
+ type: "button",
1566
+ className: "cnvnav-group-head",
1567
+ onClick: () => jumpTo(g.headKey),
1568
+ },
1569
+ titleNode,
1570
+ hasSub
1571
+ ? React.createElement("span", { className: "cnvnav-group-sub" }, sub)
1572
+ : null,
1573
+ ),
1574
+ ),
1575
+ ),
1576
+ chevron !== undefined ? chevron : null,
1577
+ );
1578
+ }
1579
+
1580
+ let groupNodes;
1581
+ if (filtering) {
1582
+ groupNodes = [];
1583
+ for (const g of groups) {
1584
+ const headHit = g.headIsUser && g.items.length > 0 && hit(g.items[0].search);
1585
+ const hits = g.items.filter((it, i) =>
1586
+ (it.kind === "user" || it.kind === "assistant-step") &&
1587
+ hit(it.search) &&
1588
+ !(i === 0 && g.headIsUser)
1589
+ );
1590
+ if (!headHit && hits.length === 0) continue;
1591
+ const groupActive = g.items.some((it) => it.key === snap.activeKey);
1592
+ groupNodes.push(React.createElement("div", {
1593
+ key: g.gid,
1594
+ className: "cnvnav-group cnvnav-fade-item",
1595
+ style: { animationDelay: (groupNodes.length * 18) + "ms" },
1596
+ },
1597
+ renderGroupHead(g, groupActive, null),
1598
+ hits.length > 0
1599
+ ? React.createElement("div", { className: "cnvnav-steps" },
1600
+ React.createElement("div", { className: "cnvnav-steps-inner" },
1601
+ hits.map((it, i) => renderItem(it, i * 20)),
1602
+ ),
1603
+ )
1604
+ : null,
1605
+ ));
1606
+ }
1607
+ } else {
1608
+ groupNodes = groups.map((g) => {
1609
+ const expanded = snap.expanded[g.gid] === true;
1610
+ const isTurnGroup = g.turn !== null;
1611
+ const stepItems = isTurnGroup ? (g.headIsUser ? g.items.slice(1) : g.items) : [];
1612
+ const hasSteps = stepItems.length > 0;
1613
+ const groupActive = g.items.some((it) => it.key === snap.activeKey);
1614
+ const chevron = hasSteps
1615
+ ? React.createElement(Tooltip, {
1616
+ label: expanded ? "折叠步次" : "展开步次",
1617
+ side: "top",
1618
+ delayMs: 500,
1619
+ },
1620
+ React.createElement("span", { className: "cnvnav-chevron-wrap" },
1621
+ React.createElement("button", {
1622
+ type: "button",
1623
+ className: "cnvnav-chevron",
1624
+ "aria-expanded": expanded,
1625
+ onClick: (e) => {
1626
+ e.stopPropagation();
1627
+ setState({ expanded: Object.assign({}, state.expanded, { [g.gid]: !expanded }) });
1628
+ },
1629
+ },
1630
+ React.createElement("svg", {
1631
+ className: "cnvnav-chevron-icon",
1632
+ viewBox: "0 0 24 24", width: 12, height: 12, fill: "none",
1633
+ stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
1634
+ "aria-hidden": true,
1635
+ },
1636
+ React.createElement("path", { d: "m9 18 6-6-6-6" }),
1637
+ ),
1638
+ expanded ? null : React.createElement("span", { className: "cnvnav-chevron-count" }, String(stepItems.length)),
1639
+ ),
1640
+ ),
1641
+ )
1642
+ : null;
1643
+ return React.createElement("div", { key: g.gid, className: "cnvnav-group" },
1644
+ renderGroupHead(g, groupActive, chevron),
1645
+ hasSteps
1646
+ ? React.createElement("div", {
1647
+ className: "cnvnav-steps" + (expanded ? "" : " cnvnav-steps-collapsed"),
1648
+ "aria-hidden": !expanded,
1649
+ },
1650
+ React.createElement("div", { className: "cnvnav-steps-inner" },
1651
+ stepItems.map(renderItem),
1652
+ ),
1653
+ )
1654
+ : null,
1655
+ );
1656
+ });
1657
+ }
1658
+
1659
+ return React.createElement("div", {
1660
+ ref: (el) => { panelEl = el; },
1661
+ className: "cnvnav-panel cnvnav-panel-unplaced" + (snap.open ? "" : " cnvnav-panel-hidden"),
1662
+ role: "complementary",
1663
+ "aria-label": "会话导航",
1664
+ "aria-hidden": !snap.open,
1665
+ },
1666
+ React.createElement("div", {
1667
+ className: "cnvnav-head",
1668
+ onPointerDown: startDrag,
1669
+ onPointerMove: moveDrag,
1670
+ onPointerUp: endDrag,
1671
+ onPointerCancel: endDrag,
1672
+ },
1673
+ React.createElement(Tooltip, {
1674
+ label: searchOpen ? "收起搜索" : "搜索关键词",
1675
+ side: "bottom",
1676
+ delayMs: 500,
1677
+ },
1678
+ React.createElement("span", { className: "cnvnav-search" },
1679
+ React.createElement(Button, {
1680
+ variant: "ghost",
1681
+ size: "sm",
1682
+ icon: React.createElement(IconSearchOutline16, { size: 14 }),
1683
+ onClick: () => toggleSearch(),
1684
+ }),
1685
+ ),
1686
+ ),
1687
+ searchOpen
1688
+ ? null
1689
+ : React.createElement(Tooltip, {
1690
+ label: nextModeLabel(),
1691
+ side: "bottom",
1692
+ delayMs: 500,
1693
+ },
1694
+ React.createElement("span", { className: "cnvnav-turns-toggle" },
1695
+ React.createElement(Button, {
1696
+ variant: "ghost",
1697
+ size: "sm",
1698
+ className: viewMode !== "full" ? "cnvnav-toggle-on" : "",
1699
+ icon: React.createElement(SwitchTurnsIcon),
1700
+ "aria-pressed": viewMode !== "full",
1701
+ onClick: () => cycleMode(),
1702
+ }),
1703
+ ),
1704
+ ),
1705
+ searchOpen
1706
+ ? React.createElement("input", {
1707
+ type: "text",
1708
+ className: "cnvnav-filter",
1709
+ placeholder: "请输入关键词",
1710
+ value: filter,
1711
+ autoFocus: true,
1712
+ onChange: (e) => setFilter(e.target.value),
1713
+ })
1714
+ : null,
1715
+ searchOpen
1716
+ ? null
1717
+ : React.createElement(Tooltip, {
1718
+ label: snap.docked ? "解除停靠(拖动头部定位)" : "停靠回右侧",
1719
+ side: "bottom",
1720
+ delayMs: 500,
1721
+ },
1722
+ React.createElement("span", null,
1723
+ React.createElement(Button, {
1724
+ variant: snap.docked ? "primary" : "ghost",
1725
+ size: "sm",
1726
+ icon: React.createElement(PinIcon),
1727
+ "aria-pressed": snap.docked,
1728
+ onClick: () => {
1729
+ if (snap.docked) {
1730
+ const el = panelEl;
1731
+ const r = el !== null ? el.getBoundingClientRect() : null;
1732
+ setState({ docked: false, userPos: r !== null ? { left: Math.round(r.left), top: Math.round(r.top) } : null });
1733
+ } else {
1734
+ setState({ docked: true });
1735
+ }
1736
+ persistUi();
1737
+ },
1738
+ }),
1739
+ ),
1740
+ ),
1741
+ React.createElement("span", { className: "cnvnav-count" }, (filtering ? groupNodes.length + " / " : "") + String(count) + " 条"),
1742
+ React.createElement(Tooltip, {
1743
+ label: "收起面板",
1744
+ side: "bottom",
1745
+ delayMs: 500,
1746
+ },
1747
+ React.createElement("span", { className: "cnvnav-close" },
1748
+ React.createElement(Button, {
1749
+ variant: "ghost",
1750
+ size: "sm",
1751
+ icon: React.createElement(IconCloseOutline16, { size: 14 }),
1752
+ onClick: () => setState({ open: false }),
1753
+ }),
1754
+ ),
1755
+ ),
1756
+ ),
1757
+ React.createElement("div", { className: "cnvnav-topbar" },
1758
+ React.createElement(Tooltip, {
1759
+ label: "加载更早的一批轮次",
1760
+ side: "bottom",
1761
+ delayMs: 500,
1762
+ disabled: !snap.hasMore || snap.loadingOlder,
1763
+ },
1764
+ React.createElement("span", { className: "cnvnav-bar-btn" },
1765
+ React.createElement(Button, {
1766
+ variant: "ghost",
1767
+ size: "sm",
1768
+ className: "cnvnav-bar-fill",
1769
+ icon: React.createElement(LoadEarlierIcon),
1770
+ disabled: !snap.hasMore || snap.loadingOlder,
1771
+ onClick: () => loadOlderPage(),
1772
+ }, "加载更早"),
1773
+ ),
1774
+ ),
1775
+ React.createElement(Tooltip, {
1776
+ label: "加载全部历史轮次",
1777
+ side: "bottom",
1778
+ delayMs: 500,
1779
+ disabled: !snap.hasMore || snap.loadingOlder || snap.loadingAll,
1780
+ },
1781
+ React.createElement("span", { className: "cnvnav-bar-btn" },
1782
+ React.createElement(Button, {
1783
+ variant: "ghost",
1784
+ size: "sm",
1785
+ className: "cnvnav-bar-fill",
1786
+ icon: React.createElement(LoadAllIcon),
1787
+ disabled: !snap.hasMore || snap.loadingOlder || snap.loadingAll,
1788
+ onClick: () => loadAllPages(),
1789
+ }, "加载全部"),
1790
+ ),
1791
+ ),
1792
+ ),
1793
+ React.createElement("div", {
1794
+ ref: (el) => { listEl = el; },
1795
+ className: "cnvnav-list",
1796
+ },
1797
+ groups.length === 0
1798
+ ? React.createElement("div", { className: "cnvnav-empty" }, "当前会话暂无对话节点")
1799
+ : (filtering && groupNodes.length === 0
1800
+ ? React.createElement("div", { className: "cnvnav-empty" }, "无匹配结果")
1801
+ : groupNodes),
1802
+ ),
1803
+ React.createElement("div", { className: "cnvnav-footer" },
1804
+ React.createElement(Tooltip, {
1805
+ label: "回到对话最新位置",
1806
+ side: "top",
1807
+ delayMs: 500,
1808
+ },
1809
+ React.createElement("span", { className: "cnvnav-bar-btn" },
1810
+ React.createElement(Button, {
1811
+ variant: "ghost",
1812
+ size: "sm",
1813
+ className: "cnvnav-bar-fill",
1814
+ icon: React.createElement(JumpLatestIcon),
1815
+ onClick: () => {
1816
+ const gs = snap.outline !== null ? snap.outline.groups : [];
1817
+ if (gs.length === 0) return;
1818
+ const lastGroup = gs[gs.length - 1];
1819
+ const lastItem = lastGroup.items[lastGroup.items.length - 1];
1820
+ if (lastItem !== undefined) jumpTo(lastItem.key);
1821
+ },
1822
+ }, "回到最新"),
1823
+ ),
1824
+ ),
1825
+ React.createElement(Tooltip, {
1826
+ label: hasExpanded ? "折叠所有已展开的轮次" : "当前无展开的轮次",
1827
+ side: "top",
1828
+ delayMs: 500,
1829
+ disabled: !hasExpanded,
1830
+ },
1831
+ React.createElement("span", { className: "cnvnav-bar-btn" },
1832
+ React.createElement(Button, {
1833
+ variant: "ghost",
1834
+ size: "sm",
1835
+ className: "cnvnav-bar-fill",
1836
+ icon: React.createElement(CollapseAllIcon),
1837
+ disabled: !hasExpanded,
1838
+ onClick: () => setState({ expanded: {} }),
1839
+ }, "全部折叠"),
1840
+ ),
1841
+ ),
1842
+ ),
1843
+ React.createElement("span", {
1844
+ className: "cnvnav-resize-grip",
1845
+ onPointerDown: startResize,
1846
+ onPointerMove: moveResize,
1847
+ onPointerUp: endResize,
1848
+ onPointerCancel: endResize,
1849
+ "aria-hidden": true,
1850
+ }),
1851
+ );
1852
+ }
1853
+
1854
+ /* ---------- register ---------- */
1855
+ slots.inject("conversation.session.header.utilities", () => slots.register(
1856
+ { name: "conversation.session.header.utilities", id: "conversation-navigator-toggle", order: 20, label: () => "会话导航" },
1857
+ (props) => React.createElement(ToggleButton, props),
1858
+ ));
1859
+ slots.inject("shell.overlay", () => slots.register(
1860
+ { name: "shell.overlay", id: "conversation-navigator-panel", order: 10, label: () => "会话导航" },
1861
+ () => React.createElement(Panel),
1862
+ ));
1863
+ }
1864
+
1865
+ const inject = ["slots", "sessions", "uiConversation"];
1866
+ exports.apply = apply;
1867
+ exports.inject = inject;
1868
+ return module.exports;
1869
+ }
1870
+ });