dsh-conversation-navigator 0.2.2 → 0.2.3

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.
Files changed (4) hide show
  1. package/README.en.md +107 -106
  2. package/README.md +103 -102
  3. package/lib/client.js +1460 -1365
  4. package/package.json +47 -47
package/lib/client.js CHANGED
@@ -1,1365 +1,1460 @@
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
- .cnvnav-search { flex: none; display: flex; }
120
- .cnvnav-turns-toggle { flex: none; display: flex; }
121
- .cnvnav-close { margin-left: auto; }
122
- .cnvnav-list {
123
- overflow-y: auto; padding: 6px; min-height: 0;
124
- display: flex; flex-direction: column; gap: 2px;
125
- }
126
- .cnvnav-group { display: flex; flex-direction: column; margin-bottom: 2px; }
127
- .cnvnav-group-row {
128
- display: flex; align-items: stretch;
129
- border-radius: 8px; overflow: hidden;
130
- position: relative;
131
- transition: background .16s ease;
132
- }
133
- .cnvnav-group-row:hover { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.12)); }
134
- .cnvnav-group-row-active { background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 13%, transparent); }
135
- .cnvnav-group-row-active::before {
136
- content: ''; position: absolute; left: 6px; top: 6px; bottom: 6px;
137
- width: 3px; border-radius: 2px;
138
- background: var(--dsw-alias-brand-primary, #4f46e5);
139
- }
140
- .cnvnav-group-row-active:hover { background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 18%, transparent); }
141
- .cnvnav-group-row-active .cnvnav-group-title { color: var(--dsw-alias-brand-primary, #4f46e5); }
142
- .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)); }
143
- .cnvnav-group-head-wrap { flex: 1; min-width: 0; display: flex; }
144
- .cnvnav-group-head {
145
- width: 100%;
146
- display: flex; align-items: baseline; gap: 6px; text-align: left;
147
- border: none; background: transparent; cursor: pointer;
148
- padding: 5px 0 5px 17px;
149
- color: var(--dsw-alias-label-primary, #222);
150
- }
151
- .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; }
152
- .cnvnav-group-head .cnvnav-badge { align-self: center; }
153
- .cnvnav-group-sub {
154
- color: var(--dsw-alias-label-secondary, #888);
155
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
156
- flex: 1; min-width: 0;
157
- }
158
- .cnvnav-chevron-wrap { flex: none; display: flex; }
159
- .cnvnav-chevron {
160
- min-width: 30px; align-self: stretch;
161
- display: flex; align-items: center; justify-content: center; gap: 3px;
162
- border: none; background: transparent; cursor: pointer;
163
- color: var(--dsw-alias-label-secondary, #888);
164
- font-size: 11px; line-height: 1; padding: 0 8px;
165
- white-space: nowrap;
166
- transition: color .16s ease, background .16s ease;
167
- }
168
- .cnvnav-chevron:hover { color: var(--dsw-alias-brand-primary, #4f46e5); background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.12)); }
169
- .cnvnav-chevron-icon { flex: none; display: block; transition: transform .2s cubic-bezier(.4, 0, .2, 1); }
170
- .cnvnav-chevron[aria-expanded="true"] .cnvnav-chevron-icon { transform: rotate(90deg); }
171
- .cnvnav-chevron-count { font-size: 11px; line-height: 1; }
172
- .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); }
173
- .cnvnav-steps-collapsed { grid-template-rows: 0fr; margin-top: 0; }
174
- .cnvnav-steps-inner { overflow: hidden; min-height: 0; display: flex; flex-direction: column; gap: 2px; }
175
- .cnvnav-fade-item { animation: cnvnav-fade-up .2s ease-out backwards; }
176
- @keyframes cnvnav-fade-up { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
177
- .cnvnav-item {
178
- display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
179
- border: none; background: transparent; cursor: pointer;
180
- padding: 5px 8px 5px 22px; border-radius: 8px; position: relative;
181
- color: var(--dsw-alias-label-primary, #222);
182
- transition: background .16s ease;
183
- }
184
- .cnvnav-item:hover { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.12)); }
185
- .cnvnav-item-active { background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 13%, transparent); }
186
- .cnvnav-item-active:hover { background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 18%, transparent); }
187
- .cnvnav-item-active .cnvnav-item-preview { color: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 60%, var(--dsw-alias-label-primary, #222)); }
188
- .cnvnav-item-active::before,
189
- .cnvnav-item-active::after {
190
- content: ''; position: absolute; left: 6px; width: 3px; border-radius: 2px;
191
- background: var(--dsw-alias-brand-primary, #4f46e5);
192
- }
193
- .cnvnav-item-active::before { top: 6px; height: calc(50% - 7.5px); }
194
- .cnvnav-item-active::after { bottom: 6px; height: calc(50% - 7.5px); }
195
- .cnvnav-badge {
196
- flex: none; min-width: 36px; box-sizing: border-box;
197
- display: inline-flex; align-items: center; justify-content: center;
198
- height: 19px; padding: 0 5px; border-radius: 4px;
199
- font-size: 10px; font-weight: 650; line-height: 1;
200
- letter-spacing: .035em; white-space: nowrap;
201
- }
202
- .cnvnav-badge-traj-user { color: var(--dsw-alias-state-business-primary, #4f46e5); background: var(--dsw-alias-state-business-tertiary, rgba(79,70,229,.14)); }
203
- .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)); }
204
- .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)); }
205
- .cnvnav-badge-traj-tool { color: var(--dsw-alias-state-warn-label, #b45309); background: var(--dsw-alias-state-warn-tertiary, rgba(180,83,9,.14)); }
206
- .cnvnav-badge-traj-compacted { color: var(--dsw-alias-label-secondary, #888); background: var(--dsw-alias-bg-module-platform, rgba(128,128,128,.12)); }
207
- .cnvnav-badge-other-command { color: #1d4ed8; background: rgba(29,78,216,.13); }
208
- .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); }
209
- .cnvnav-badge-other-retry { color: #ea580c; background: rgba(234,88,12,.14); }
210
- .cnvnav-badge-other-neutral { color: var(--dsw-alias-label-secondary, #888); background: var(--dsw-alias-bg-module-platform, rgba(128,128,128,.12)); }
211
- .cnvnav-item-preview {
212
- flex: 1; min-width: 0; display: block;
213
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
214
- color: var(--dsw-alias-label-secondary, #888);
215
- }
216
- .cnvnav-item-time {
217
- flex: none; color: var(--dsw-alias-label-secondary, #888); font-size: 10px; opacity: .8;
218
- }
219
- .cnvnav-hl {
220
- background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f59e0b) 30%, transparent);
221
- color: inherit; border-radius: 2px; padding: 0 1px; font-weight: 600;
222
- }
223
- .cnvnav-empty { padding: 18px 10px; text-align: center; color: var(--dsw-alias-label-secondary, #888); }
224
- /* ---------- 极简模式 ---------- */
225
- .cnvnav-mini-wrap {
226
- position: fixed; z-index: 9999; right: 12px;
227
- pointer-events: auto;
228
- transition: opacity .18s ease, transform .18s ease;
229
- }
230
- .cnvnav-mini-panel {
231
- width: 18px; height: 220px; overflow: hidden;
232
- position: relative;
233
- display: flex; flex-direction: column;
234
- background: transparent;
235
- border: 1px solid transparent;
236
- border-radius: 12px;
237
- box-shadow: none;
238
- transition: width .18s ease, background .16s ease, border-color .16s ease, box-shadow .16s ease;
239
- }
240
- .cnvnav-mini-wrap:hover .cnvnav-mini-panel {
241
- width: 288px;
242
- background: var(--dsw-alias-bg-overlay, #fff);
243
- border-color: var(--dsw-alias-border-l1, rgba(128,128,128,.35));
244
- box-shadow: 0 16px 48px rgba(0,0,0,.22);
245
- }
246
- .cnvnav-mini-fab {
247
- position: absolute; bottom: 100%; right: -8px;
248
- margin-bottom: 6px;
249
- display: flex;
250
- }
251
- .cnvnav-mini-list {
252
- flex: 1; min-height: 0; width: 288px; box-sizing: border-box;
253
- overflow-y: auto; padding: 4px;
254
- display: flex; flex-direction: column; gap: 2px;
255
- }
256
- /* dsweb 式滚动条: 只显示滑块, 无滑轨无上下按钮.
257
- 面板列表与极简条统一为常显细白条(极简条收起时列表被裁剪,
258
- 悬停展开后滑块自然可见). Chromium 用 webkit 伪元素
259
- (标准属性会令其失效), Firefox 用 @supports 标准属性 */
260
- .cnvnav-list::-webkit-scrollbar,
261
- .cnvnav-mini-list::-webkit-scrollbar { width: 4px; }
262
- .cnvnav-list::-webkit-scrollbar-track,
263
- .cnvnav-mini-list::-webkit-scrollbar-track { background: transparent; }
264
- .cnvnav-list::-webkit-scrollbar-thumb,
265
- .cnvnav-mini-list::-webkit-scrollbar-thumb {
266
- background: color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 40%, transparent);
267
- border-radius: 2px;
268
- }
269
- .cnvnav-list::-webkit-scrollbar-button,
270
- .cnvnav-mini-list::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
271
- .cnvnav-list::-webkit-scrollbar-corner,
272
- .cnvnav-mini-list::-webkit-scrollbar-corner { background: transparent; }
273
- @supports (-moz-appearance: none) {
274
- .cnvnav-list,
275
- .cnvnav-mini-list {
276
- scrollbar-width: thin;
277
- scrollbar-color: color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 40%, transparent) transparent;
278
- }
279
- }
280
- .cnvnav-mini-item {
281
- flex: none; height: 28px; box-sizing: border-box; width: 100%;
282
- display: flex; align-items: center; text-align: left;
283
- border: none; background: transparent; cursor: pointer;
284
- padding: 5px 8px 5px 17px; border-radius: 8px; position: relative;
285
- color: var(--dsw-alias-label-secondary, #888);
286
- font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
287
- transition: background .16s ease;
288
- }
289
- .cnvnav-mini-item:hover { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.12)); }
290
- /* 每行都有指示竖条: 非当前=掺前景色(浅色主题掺黑/深色主题掺白, 40%), 当前=品牌色实色 */
291
- .cnvnav-mini-item::before {
292
- content: ''; position: absolute; left: 6px; top: 6px; bottom: 6px;
293
- width: 3px; border-radius: 2px;
294
- background: color-mix(in srgb, var(--dsw-alias-label-primary, #222) 40%, transparent);
295
- transition: background .16s ease;
296
- }
297
- .cnvnav-mini-item-active {
298
- background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 13%, transparent);
299
- color: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 60%, var(--dsw-alias-label-primary, #222));
300
- }
301
- .cnvnav-mini-item-active::before { background: var(--dsw-alias-brand-primary, #4f46e5); }
302
- .cnvnav-filter {
303
- flex: 1; min-width: 0; box-sizing: border-box;
304
- height: 28px; padding: 0 8px;
305
- border: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.35));
306
- border-radius: 8px;
307
- background: var(--dsw-alias-bg-layer-1, rgba(128,128,128,.06));
308
- color: var(--dsw-alias-label-primary, #222);
309
- font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif);
310
- font-size: 12px; outline: none;
311
- animation: cnvnav-search-in .16s ease-out;
312
- }
313
- .cnvnav-filter::placeholder { color: var(--dsw-alias-label-secondary, #888); }
314
- .cnvnav-filter:focus { border-color: var(--dsw-alias-brand-primary, #4f46e5); }
315
- @keyframes cnvnav-search-in {
316
- from { opacity: 0; transform: translateX(4px); }
317
- to { opacity: 1; transform: translateX(0); }
318
- }
319
- .cnvnav-topbar { display: flex; gap: 6px; margin: 6px 6px 0; }
320
- .cnvnav-footer { display: flex; gap: 6px; margin: 0 6px 6px; }
321
- .cnvnav-bar-btn { flex: 1; display: block; }
322
- .cnvnav-bar-fill { width: 100%; }
323
- /* ---------- 拖动定位与左对齐极简 ---------- */
324
- .cnvnav-head { cursor: grab; touch-action: none; }
325
- .cnvnav-dragging { cursor: grabbing; user-select: none; }
326
- .cnvnav-dragging .cnvnav-head { cursor: grabbing; }
327
- /* 模式切换按钮激活态: 与整行描边图标按钮同一视觉层级
328
- (实心 primary 填充夹在幽灵图标中间会显得"错位") */
329
- .cnvnav-head button.cnvnav-toggle-on {
330
- color: var(--dsw-alias-brand-primary, #4f46e5);
331
- background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 13%, transparent);
332
- }
333
- .cnvnav-head button.cnvnav-toggle-on:hover {
334
- background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 20%, transparent);
335
- }
336
- .cnvnav-mini-wrap-left { right: auto; }
337
- .cnvnav-mini-wrap-left .cnvnav-mini-fab { right: auto; left: -8px; }
338
- `;
339
-
340
- function apply(ctx) {
341
- const slots = ctx.get("slots");
342
- if (slots === undefined) return;
343
- const timer = ctx.get("timer");
344
- const sessions = ctx.get("sessions");
345
-
346
- /* own stylesheet with fiber-scoped cleanup */
347
- const styleEl = document.createElement("style");
348
- styleEl.setAttribute("data-dsh-cnvnav", "");
349
- styleEl.textContent = CSS;
350
- document.head.appendChild(styleEl);
351
- ctx.effect(() => () => styleEl.remove());
352
-
353
- /* ---------- shared store (toggle button + overlay panel) ---------- */
354
- const UI_KEY = "dsh-cnvnav:ui:v1";
355
- const VIEW_MODES = ["full", "hidden", "minimal", "minimal-left"];
356
- function readUi() {
357
- try {
358
- const raw = window.localStorage.getItem(UI_KEY);
359
- if (!raw) return {};
360
- const o = JSON.parse(raw);
361
- const out = {};
362
- if (VIEW_MODES.indexOf(o.viewMode) !== -1) out.viewMode = o.viewMode;
363
- if (o.docked === true || o.docked === false) out.docked = o.docked;
364
- if (o.userPos && typeof o.userPos.left === "number" && typeof o.userPos.top === "number"
365
- && Number.isFinite(o.userPos.left) && Number.isFinite(o.userPos.top)) {
366
- out.userPos = { left: o.userPos.left, top: o.userPos.top };
367
- }
368
- return out;
369
- } catch (err) {
370
- return {};
371
- }
372
- }
373
- function persistUi() {
374
- try {
375
- window.localStorage.setItem(UI_KEY, JSON.stringify({
376
- viewMode: state.viewMode, docked: state.docked, userPos: state.userPos,
377
- }));
378
- } catch (err) { /* 隐私模式等场景忽略 */ }
379
- }
380
- const savedUi = readUi();
381
- let state = Object.assign({
382
- open: true, sessionId: null, outline: null, activeKey: null, expanded: {},
383
- hasMore: false, loadingOlder: false, loadingAll: false,
384
- viewMode: "full", docked: true, userPos: null,
385
- }, savedUi);
386
- const listeners = new Set();
387
- function setState(patch) {
388
- state = Object.assign({}, state, patch);
389
- listeners.forEach((fn) => { try { fn(); } catch (err) { console.error(err); } });
390
- }
391
- function subscribe(fn) { listeners.add(fn); return () => { listeners.delete(fn); }; }
392
- function useStore() {
393
- const [snap, setSnap] = React.useState(state);
394
- React.useEffect(() => subscribe(() => setSnap(state)), []);
395
- return snap;
396
- }
397
-
398
- /* ---------- history paging ---------- */
399
- let loadingAll = false;
400
- function loadOlderPage() {
401
- if (sessions === undefined) return;
402
- const sid = state.sessionId;
403
- if (sid === null) return;
404
- const binding = sessions.binding(sid);
405
- if (binding === undefined) return;
406
- binding.session.loadOlder().catch((err) => console.error(err));
407
- }
408
- async function loadAllPages() {
409
- if (sessions === undefined || loadingAll) return;
410
- const sid = state.sessionId;
411
- if (sid === null) return;
412
- const binding = sessions.binding(sid);
413
- if (binding === undefined) return;
414
- loadingAll = true;
415
- setState({ loadingAll: true });
416
- try {
417
- let guard = 0;
418
- while (binding.session.getSnapshot().hasMore && guard < 500) {
419
- await binding.session.loadOlder();
420
- guard += 1;
421
- }
422
- } catch (err) {
423
- console.error(err);
424
- } finally {
425
- loadingAll = false;
426
- setState({ loadingAll: false });
427
- }
428
- }
429
-
430
- /* ---------- outline derivation from ConversationSnapshot ---------- */
431
- const KIND_META = {
432
- "user": { label: "你", cls: "traj-user" },
433
- "steering": { label: "插话", cls: "traj-user" },
434
- "context": { label: "上下文", cls: "traj-context" },
435
- "assistant-step": { label: "助手", cls: "traj-assistant" },
436
- "tool-call": { label: "工具", cls: "traj-tool" },
437
- "command": { label: "命令", cls: "other-command" },
438
- "compaction": { label: "压缩", cls: "traj-compacted" },
439
- "manual-compaction": { label: "压缩", cls: "traj-compacted" },
440
- "turn-error": { label: "出错", cls: "other-error" },
441
- "turn-max-tokens": { label: "截断", cls: "other-error" },
442
- "model-retry": { label: "重试", cls: "other-retry" },
443
- "workflow-run": { label: "流程", cls: "other-neutral" },
444
- "unknown": { label: "其他", cls: "other-neutral" },
445
- };
446
- function firstText(blocks) {
447
- if (!Array.isArray(blocks)) return null;
448
- for (const b of blocks) {
449
- if (!b || typeof b !== "object") continue;
450
- if ((b.type === "text" || b.kind === "text") && typeof b.text === "string" && b.text.trim() !== "") return b.text;
451
- }
452
- return null;
453
- }
454
- /* Searchable dialogue text: user questions + agent actual dialogue only
455
- (excludes context, tool calls, commands, compaction, reasoning, etc.) */
456
- function dialogueText(node) {
457
- const d = node && node.data;
458
- if (!d || typeof d !== "object") return null;
459
- if (node.kind === "user") return firstText(d.content);
460
- if (node.kind === "assistant-step") {
461
- if (!Array.isArray(d.blocks)) return null;
462
- const parts = [];
463
- for (const b of d.blocks) {
464
- if (!b || typeof b !== "object") continue;
465
- if ((b.type === "text" || b.kind === "text") && typeof b.text === "string" && b.text.trim() !== "") parts.push(b.text.trim());
466
- }
467
- return parts.length > 0 ? parts.join(" ") : null;
468
- }
469
- return null;
470
- }
471
- /* Full user question text (all text blocks) for the hover bubble. */
472
- function fullDialogueText(node) {
473
- const d = node && node.data;
474
- if (!d || typeof d !== "object") return null;
475
- if (node.kind !== "user") return null;
476
- if (typeof d.content === "string") {
477
- const t = d.content.trim();
478
- return t === "" ? null : t;
479
- }
480
- if (!Array.isArray(d.content)) return null;
481
- const parts = [];
482
- for (const b of d.content) {
483
- if (!b || typeof b !== "object") continue;
484
- if ((b.type === "text" || b.kind === "text") && typeof b.text === "string" && b.text.trim() !== "") parts.push(b.text.trim());
485
- }
486
- return parts.length > 0 ? parts.join("\n") : null;
487
- }
488
- function previewFor(node) {
489
- const d = node && node.data;
490
- if (!d || typeof d !== "object") return null;
491
- switch (node.kind) {
492
- case "user":
493
- case "steering":
494
- case "context":
495
- return firstText(d.content);
496
- case "assistant-step": {
497
- const text = firstText(d.blocks);
498
- if (text !== null) return text;
499
- if (Array.isArray(d.blocks) && d.blocks.some((b) => b && b.kind === "tool-call")) return "调用工具";
500
- return d.status === "running" ? "进行中…" : null;
501
- }
502
- case "tool-call": {
503
- const root = d.root;
504
- if (!root || typeof root !== "object") return null;
505
- if (root.isError === true) return "执行失败";
506
- const name = typeof root.name === "string" ? root.name
507
- : (root.call && typeof root.call.name === "string" ? root.call.name : null);
508
- return name;
509
- }
510
- case "command":
511
- return typeof d.name === "string" ? d.name + (typeof d.args === "string" && d.args !== "" ? " " + d.args : "") : null;
512
- case "compaction":
513
- return typeof d.summary === "string" ? d.summary : "上下文已压缩";
514
- case "manual-compaction": {
515
- const c = d.compaction;
516
- if (c && typeof c.summary === "string") return c.summary;
517
- return "手动压缩";
518
- }
519
- case "turn-error":
520
- return typeof d.message === "string" ? d.message : "本轮出错";
521
- case "turn-max-tokens":
522
- return "达到输出上限";
523
- case "model-retry": {
524
- const n = Array.isArray(d.attempts) ? d.attempts.length : 0;
525
- return "自动重试" + (n > 0 ? " ×" + n : "…");
526
- }
527
- case "unknown":
528
- return typeof d.type === "string" ? "未知事件 " + d.type : "未知事件";
529
- default:
530
- return null;
531
- }
532
- }
533
- function timeOf(node) {
534
- const d = node && node.data;
535
- if (!d || typeof d !== "object") return null;
536
- const t = typeof d.time === "number" ? d.time
537
- : (d.root && typeof d.root.time === "number" ? d.root.time : null);
538
- return t;
539
- }
540
- function fmtTime(ms) {
541
- if (typeof ms !== "number" || !isFinite(ms)) return "";
542
- const d = new Date(ms);
543
- const h = d.getHours();
544
- const m = d.getMinutes();
545
- return (h < 10 ? "0" + h : h) + ":" + (m < 10 ? "0" + m : m);
546
- }
547
- function turnOf(node) {
548
- const loc = node && node.location;
549
- if (!loc) return null;
550
- if (loc.kind === "step" || loc.kind === "turn") return loc.turn.turn;
551
- return null;
552
- }
553
- function truncate(text, max) {
554
- if (typeof text !== "string") return "";
555
- if (text.length <= max) return text;
556
- return text.slice(0, max) + "";
557
- }
558
- function deriveOutline(snap) {
559
- const empty = { groups: [], count: 0, byKey: {} };
560
- if (snap === null || snap === undefined) return empty;
561
- const order = snap.chat && snap.chat.order;
562
- const nodes = snap.chat && snap.chat.nodes;
563
- if (!Array.isArray(order) || nodes === undefined) return empty;
564
- const groups = [];
565
- const byKey = {};
566
- let current = null;
567
- let count = 0;
568
- for (const key of order) {
569
- const node = nodes.get(key);
570
- if (node === undefined) continue;
571
- const meta = KIND_META[node.kind] || KIND_META.unknown;
572
- const item = {
573
- key: node.key,
574
- kind: node.kind,
575
- label: meta.label,
576
- cls: meta.cls,
577
- preview: previewFor(node),
578
- search: dialogueText(node),
579
- full: fullDialogueText(node),
580
- time: fmtTime(timeOf(node)),
581
- };
582
- const turn = turnOf(node);
583
- if (turn === null) {
584
- groups.push({ turn: null, items: [item], gid: "x" + groups.length });
585
- count += 1;
586
- current = null;
587
- continue;
588
- }
589
- if (current === null || current.turn !== turn) {
590
- current = { turn: turn, items: [], gid: "t" + turn };
591
- groups.push(current);
592
- }
593
- current.items.push(item);
594
- count += 1;
595
- }
596
- for (const g of groups) {
597
- const first = g.items[0];
598
- if (g.turn === null) {
599
- g.title = first.label;
600
- g.subtitle = truncate(first.preview || "", 60);
601
- g.headKey = first.key;
602
- g.headIsUser = false;
603
- } else {
604
- g.title = "第 " + g.turn + " 轮";
605
- const head = first.kind === "user" ? first : null;
606
- g.headKey = head ? head.key : first.key;
607
- g.headIsUser = head !== null;
608
- g.subtitle = truncate((head ? head.preview : first.preview) || "", 60);
609
- }
610
- for (const it of g.items) byKey[it.key] = g.gid;
611
- }
612
- return { groups: groups, count: count, byKey: byKey };
613
- }
614
- let cacheKey = null;
615
- let cacheValue = null;
616
- function selectOutline(snap) {
617
- if (snap !== cacheKey) {
618
- cacheKey = snap;
619
- cacheValue = deriveOutline(snap);
620
- }
621
- return cacheValue;
622
- }
623
-
624
- /* ---------- DOM helpers: jump + position tracking ---------- */
625
- function findAnchor(key) {
626
- const rows = document.querySelectorAll("[data-chat-anchor-key]");
627
- for (const row of rows) {
628
- if (row.getAttribute("data-chat-anchor-key") === key) return row;
629
- }
630
- return null;
631
- }
632
- let lastActive = null;
633
- function jumpTo(key) {
634
- try {
635
- const el = findAnchor(key);
636
- if (el === null) return false;
637
- el.scrollIntoView({ behavior: "smooth", block: "start" });
638
- lastActive = key;
639
- setState({ activeKey: key });
640
- return true;
641
- } catch (err) {
642
- console.error(err);
643
- return false;
644
- }
645
- }
646
- function computeActiveKey() {
647
- try {
648
- const scrollport = document.querySelector("[data-conversation-scroll]");
649
- if (scrollport === null) return null;
650
- const vp = scrollport.getBoundingClientRect();
651
- const rows = document.querySelectorAll("[data-chat-anchor-key]");
652
- let found = null;
653
- for (const row of rows) {
654
- const rect = row.getBoundingClientRect();
655
- if (rect.bottom > vp.top) { found = row; break; }
656
- }
657
- if (found === null && rows.length > 0) found = rows[rows.length - 1];
658
- return found === null ? null : found.getAttribute("data-chat-anchor-key");
659
- } catch (err) {
660
- console.error(err);
661
- return null;
662
- }
663
- }
664
-
665
- let panelEl = null;
666
- let listEl = null;
667
- let dragMove = null;
668
- const itemEls = {};
669
- const groupEls = {};
670
-
671
- /* ---------- header toggle button ---------- */
672
- function ToggleButton(props) {
673
- const snap = useStore();
674
- const outline = props.useSession(selectOutline);
675
- const hasMore = props.useSession((s) => s.hasMore);
676
- const loadingOlder = props.useSession((s) => s.loadingOlder);
677
- React.useEffect(() => {
678
- setState({ sessionId: props.sessionId, outline: outline, hasMore: hasMore, loadingOlder: loadingOlder });
679
- }, [outline, props.sessionId, hasMore, loadingOlder]);
680
- React.useEffect(() => {
681
- if (state.sessionId !== null && state.sessionId !== props.sessionId) {
682
- lastActive = null;
683
- setState({ activeKey: null, expanded: {} });
684
- }
685
- }, [props.sessionId]);
686
- return React.createElement(Tooltip, {
687
- label: snap.open ? "收起会话导航" : "打开会话导航",
688
- side: "bottom",
689
- delayMs: 500,
690
- },
691
- React.createElement("span", null,
692
- React.createElement(Button, {
693
- variant: snap.open ? "primary" : "ghost",
694
- size: "sm",
695
- icon: React.createElement(ToggleIcon),
696
- "aria-pressed": snap.open,
697
- onClick: () => setState({ open: !snap.open }),
698
- }, "导航"),
699
- ),
700
- );
701
- }
702
-
703
- /* ---------- floating navigator panel ---------- */
704
- function Panel() {
705
- const snap = useStore();
706
- const [filter, setFilter] = React.useState("");
707
- const [searchOpen, setSearchOpen] = React.useState(false);
708
- const viewMode = snap.viewMode; /* full | hidden | minimal | minimal-left */
709
- const turnsHidden = viewMode !== "full";
710
- function nextModeLabel() {
711
- return viewMode === "full" ? "隐藏轮次" : viewMode === "hidden" ? "极简·右" : viewMode === "minimal" ? "极简·左" : "显示轮次";
712
- }
713
- function cycleMode() {
714
- const next = viewMode === "full" ? "hidden" : viewMode === "hidden" ? "minimal" : viewMode === "minimal" ? "minimal-left" : "full";
715
- setState({ viewMode: next });
716
- persistUi();
717
- }
718
- function toggleSearch() {
719
- if (searchOpen) setFilter("");
720
- setSearchOpen(!searchOpen);
721
- }
722
- /* ---------- 拖动定位(仅面板形式, 极简条不参与) ---------- */
723
- function startDrag(e) {
724
- if (e.button !== 0) return;
725
- if (typeof e.target.closest === "function" && e.target.closest("button") !== null) return;
726
- const el = panelEl;
727
- if (el === null) return;
728
- dragMove = { startX: e.clientX, startY: e.clientY, baseLeft: el.offsetLeft, baseTop: el.offsetTop, el: el, dragging: false };
729
- try { e.currentTarget.setPointerCapture(e.pointerId); } catch (err) { /* 忽略捕获失败 */ }
730
- }
731
- function moveDrag(e) {
732
- const d = dragMove;
733
- if (d === null) return;
734
- const dx = e.clientX - d.startX;
735
- const dy = e.clientY - d.startY;
736
- if (!d.dragging && (Math.abs(dx) > 3 || Math.abs(dy) > 3)) {
737
- d.dragging = true;
738
- d.el.classList.add("cnvnav-dragging");
739
- }
740
- if (!d.dragging) return;
741
- const vw = document.documentElement.clientWidth || 1000;
742
- const vh = document.documentElement.clientHeight || 800;
743
- const w = d.el.offsetWidth || 288;
744
- const h = d.el.offsetHeight || 200;
745
- const left = Math.min(Math.max(8, d.baseLeft + dx), Math.max(8, vw - w - 8));
746
- const top = Math.min(Math.max(8, d.baseTop + dy), Math.max(8, vh - h - 8));
747
- d.el.style.left = Math.round(left) + "px";
748
- d.el.style.top = Math.round(top) + "px";
749
- }
750
- function endDrag() {
751
- const d = dragMove;
752
- dragMove = null;
753
- if (d === null || !d.dragging) return;
754
- d.el.classList.remove("cnvnav-dragging");
755
- const r = d.el.getBoundingClientRect();
756
- /* 拖完同步封顶(与 place() 浮动分支一致): 上边缘固定, 高度至多半屏 */
757
- const vh = document.documentElement.clientHeight || 800;
758
- d.el.style.maxHeight = Math.max(160, Math.round(Math.min(vh / 2, vh - Math.max(8, Math.round(r.top)) - 8))) + "px";
759
- setState({ docked: false, userPos: { left: Math.round(r.left), top: Math.round(r.top) } });
760
- persistUi();
761
- }
762
-
763
- React.useEffect(() => {
764
- if (!snap.open) return;
765
- let lastKey = null;
766
- let stableTicks = 0;
767
- let misses = 0;
768
- function place() {
769
- const panel = panelEl;
770
- if (panel === null) return false;
771
- if (dragMove !== null) return true;
772
- const vw = document.documentElement.clientWidth || 1000;
773
- const vh = document.documentElement.clientHeight || 800;
774
- const mini = viewMode === "minimal" || viewMode === "minimal-left";
775
- const alignLeft = viewMode === "minimal-left";
776
- /* 面板被拖走后不再自动停靠: 夹进视口、上边缘固定, 展开高度至多半屏以便下方留白 */
777
- if (!mini && !state.docked && state.userPos !== null) {
778
- const w = panel.offsetWidth || 288;
779
- const h = panel.offsetHeight || 200;
780
- const left = Math.min(Math.max(8, Math.round(state.userPos.left)), Math.max(8, vw - w - 8));
781
- const top = Math.min(Math.max(8, Math.round(state.userPos.top)), Math.max(8, vh - h - 8));
782
- panel.style.maxHeight = Math.max(160, Math.round(Math.min(vh / 2, vh - top - 8))) + "px";
783
- panel.style.left = left + "px";
784
- panel.style.top = top + "px";
785
- panel.classList.remove("cnvnav-panel-unplaced");
786
- return true;
787
- }
788
- const sp = document.querySelector("[data-conversation-scroll]");
789
- const width = 288;
790
- const MINI_H = 220;
791
- if (sp === null) {
792
- /* 会话视图可能尚未挂载(刷新/重启/切会话时面板先就位);
793
- 非会话页(设置页等)则永远没有该容器。
794
- 连续 15 次(约 6s)仍未出现才回退到视口定位, 否则保持隐藏等待校准。 */
795
- misses += 1;
796
- if (misses >= 15) {
797
- if (mini) {
798
- /* 极简: 水平锚定, 右对齐由 CSS right 负责, 左对齐钉在视口左侧 */
799
- if (alignLeft) {
800
- panel.style.left = "12px";
801
- panel.style.right = "auto";
802
- } else {
803
- panel.style.left = "";
804
- panel.style.right = "";
805
- }
806
- panel.style.top = Math.max(8, Math.round((vh - MINI_H) / 2)) + "px";
807
- panel.style.maxHeight = MINI_H + "px";
808
- } else {
809
- panel.style.left = Math.max(8, vw - width - 12) + "px";
810
- panel.style.top = Math.max(8, 60) + "px";
811
- panel.style.maxHeight = Math.round(vh - 90) + "px";
812
- }
813
- panel.classList.remove("cnvnav-panel-unplaced");
814
- return true;
815
- }
816
- return false;
817
- }
818
- const r = sp.getBoundingClientRect();
819
- const key = mini
820
- ? [Math.round(r.left), Math.round(r.top), Math.round(r.height), Math.round(vh)].join("|")
821
- : [Math.round(r.top), Math.round(r.height), Math.round(vw), Math.round(vh)].join("|");
822
- if (key !== lastKey) {
823
- lastKey = key;
824
- if (mini) {
825
- /* 极简: 右对齐由 CSS(right)锚定视口右缘, 左对齐钉在对话区左缘 */
826
- if (alignLeft) {
827
- panel.style.left = Math.max(8, Math.round(r.left + 12)) + "px";
828
- panel.style.right = "auto";
829
- } else {
830
- panel.style.left = "";
831
- panel.style.right = "";
832
- }
833
- panel.style.top = Math.max(8, Math.round(r.top + (r.height - MINI_H) / 2)) + "px";
834
- panel.style.maxHeight = MINI_H + "px";
835
- } else {
836
- panel.style.left = Math.max(8, vw - width - 12) + "px";
837
- panel.style.top = Math.max(8, Math.round(r.top + 8)) + "px";
838
- panel.style.maxHeight = Math.round(Math.min(r.height - 24, vh - 40)) + "px";
839
- }
840
- }
841
- panel.classList.remove("cnvnav-panel-unplaced");
842
- return true;
843
- }
844
- place();
845
- /* 400ms 复查: 容器出现即校准; 布局连续 3 次无变化后停止 */
846
- let id = setInterval(() => {
847
- const before = lastKey;
848
- if (!place()) return;
849
- if (lastKey !== null && lastKey === before) {
850
- stableTicks += 1;
851
- if (stableTicks >= 3) clearInterval(id);
852
- } else {
853
- stableTicks = 0;
854
- }
855
- }, 400);
856
- window.addEventListener("resize", place);
857
- /* 左对齐极简条需跟随左侧栏展开/折叠: 对话区宽度变化时重新校准 */
858
- let ro = null;
859
- const roTarget = document.querySelector("[data-conversation-scroll]");
860
- if (typeof ResizeObserver === "function" && roTarget !== null) {
861
- ro = new ResizeObserver(() => { place(); });
862
- ro.observe(roTarget);
863
- }
864
- return () => {
865
- clearInterval(id);
866
- window.removeEventListener("resize", place);
867
- if (ro !== null) ro.disconnect();
868
- };
869
- }, [snap.open, snap.sessionId, viewMode, state.docked, state.userPos]);
870
-
871
- React.useEffect(() => {
872
- if (!snap.open) return;
873
- const run = () => {
874
- const key = computeActiveKey();
875
- if (key !== lastActive) {
876
- lastActive = key;
877
- setState({ activeKey: key });
878
- }
879
- };
880
- const update = timer !== undefined ? timer.throttle(run, 120) : run;
881
- document.addEventListener("scroll", update, true);
882
- window.addEventListener("resize", update);
883
- run();
884
- return () => {
885
- document.removeEventListener("scroll", update, true);
886
- window.removeEventListener("resize", update);
887
- if (timer !== undefined && typeof update.dispose === "function") update.dispose();
888
- };
889
- }, [snap.open]);
890
-
891
- React.useEffect(() => {
892
- if (!snap.open || snap.activeKey === null) return;
893
- let target = itemEls[snap.activeKey];
894
- if (target === undefined || target === null || (typeof target.closest === "function" && target.closest(".cnvnav-steps-collapsed") !== null)) {
895
- const gid = snap.outline !== null ? snap.outline.byKey[snap.activeKey] : undefined;
896
- if (gid !== undefined) target = groupEls[gid];
897
- }
898
- const list = listEl;
899
- if (target !== undefined && target !== null && list !== null) {
900
- const lr = list.getBoundingClientRect();
901
- const er = target.getBoundingClientRect();
902
- const rel = er.top - lr.top + list.scrollTop;
903
- if (rel < list.scrollTop || rel + er.height > list.scrollTop + list.clientHeight) {
904
- list.scrollTo({ top: Math.max(0, rel - list.clientHeight / 2), behavior: "smooth" });
905
- }
906
- }
907
- }, [snap.open, snap.activeKey]);
908
-
909
- const groups = snap.outline !== null ? snap.outline.groups : [];
910
- const count = snap.outline !== null ? snap.outline.count : 0;
911
- const hasExpanded = Object.keys(snap.expanded).some((k) => snap.expanded[k] === true);
912
-
913
- /* ---------- 极简模式(右/左对齐): 只保留定位 ---------- */
914
- if (viewMode === "minimal" || viewMode === "minimal-left") {
915
- const miniRows = [];
916
- for (const g of groups) {
917
- if (g.turn === null) continue;
918
- miniRows.push({
919
- key: g.headKey,
920
- text: g.subtitle,
921
- full: g.headIsUser && g.items.length > 0 ? g.items[0].full : null,
922
- active: g.items.some((it) => it.key === snap.activeKey),
923
- });
924
- }
925
- return React.createElement("div", {
926
- ref: (el) => { panelEl = el; },
927
- className: "cnvnav-mini-wrap" + (viewMode === "minimal-left" ? " cnvnav-mini-wrap-left" : "") + " cnvnav-panel-unplaced" + (snap.open ? "" : " cnvnav-panel-hidden"),
928
- role: "complementary",
929
- "aria-label": "会话导航",
930
- "aria-hidden": !snap.open,
931
- },
932
- React.createElement(Tooltip, {
933
- label: nextModeLabel(),
934
- side: "top",
935
- delayMs: 500,
936
- },
937
- React.createElement("span", { className: "cnvnav-mini-fab" },
938
- React.createElement(Button, {
939
- variant: "ghost",
940
- size: "sm",
941
- icon: React.createElement(SwitchTurnsIcon),
942
- "aria-pressed": true,
943
- onClick: () => cycleMode(),
944
- }),
945
- ),
946
- ),
947
- React.createElement("div", { className: "cnvnav-mini-panel" },
948
- React.createElement("div", {
949
- ref: (el) => { listEl = el; },
950
- className: "cnvnav-mini-list",
951
- },
952
- groups.length === 0
953
- ? React.createElement("div", { className: "cnvnav-empty" }, "当前会话暂无对话节点")
954
- : miniRows.map((r) => React.createElement(Tooltip, {
955
- key: r.key,
956
- label: r.full !== null ? r.full : "跳转到此位置",
957
- side: "top",
958
- delayMs: 500,
959
- maxWidth: 340,
960
- },
961
- React.createElement("button", {
962
- type: "button",
963
- ref: (el) => { itemEls[r.key] = el; },
964
- className: "cnvnav-mini-item" + (r.active ? " cnvnav-mini-item-active" : ""),
965
- onClick: () => jumpTo(r.key),
966
- }, r.text),
967
- )),
968
- ),
969
- ),
970
- );
971
- }
972
-
973
- const kw = filter.trim().toLowerCase();
974
- const filtering = kw !== "";
975
- function hit(text) {
976
- return typeof text === "string" && text !== "" && text.toLowerCase().indexOf(kw) !== -1;
977
- }
978
- /* Split text into nodes, wrapping every case-insensitive keyword match in a <mark>. */
979
- function highlightedNodes(text, kw) {
980
- if (typeof text !== "string" || kw === "") return [text];
981
- const lower = text.toLowerCase();
982
- const out = [];
983
- let i = 0;
984
- let m = 0;
985
- for (;;) {
986
- const idx = lower.indexOf(kw, i);
987
- if (idx === -1) { if (i < text.length) out.push(text.slice(i)); break; }
988
- if (idx > i) out.push(text.slice(i, idx));
989
- out.push(React.createElement("mark", { key: "m" + (m++), className: "cnvnav-hl" }, text.slice(idx, idx + kw.length)));
990
- i = idx + kw.length;
991
- }
992
- return out;
993
- }
994
- /* Window the text around the first keyword match (so the keyword is visible,
995
- not hidden behind head-truncation), then highlight every occurrence. */
996
- function snippetNodes(text, kw, before, after) {
997
- if (typeof text !== "string") return [text];
998
- const idx = text.toLowerCase().indexOf(kw);
999
- if (idx === -1) return highlightedNodes(text, kw);
1000
- const start = Math.max(0, idx - before);
1001
- const end = Math.min(text.length, idx + kw.length + after);
1002
- const s = text.slice(start, end);
1003
- const parts = [];
1004
- if (start > 0) parts.push("…");
1005
- parts.push(...highlightedNodes(s, kw));
1006
- if (end < text.length) parts.push("…");
1007
- return parts;
1008
- }
1009
- function renderItem(item, staggerMs) {
1010
- const useSnippet = filtering && hit(item.search);
1011
- const preview = useSnippet ? snippetNodes(item.search, kw, 5, 60) : item.preview;
1012
- const hasPreview = preview !== null && preview !== "" && (!Array.isArray(preview) || preview.length > 0);
1013
- return React.createElement("button", {
1014
- key: item.key,
1015
- type: "button",
1016
- ref: (el) => { itemEls[item.key] = el; },
1017
- className: "cnvnav-item" + (item.key === snap.activeKey ? " cnvnav-item-active" : "") + (staggerMs !== undefined ? " cnvnav-fade-item" : ""),
1018
- style: staggerMs !== undefined ? { animationDelay: staggerMs + "ms" } : undefined,
1019
- onClick: () => jumpTo(item.key),
1020
- },
1021
- React.createElement("span", { className: "cnvnav-badge cnvnav-badge-" + item.cls }, item.label),
1022
- hasPreview
1023
- ? React.createElement("span", { className: "cnvnav-item-preview" }, preview)
1024
- : null,
1025
- item.time !== ""
1026
- ? React.createElement("span", { className: "cnvnav-item-time" }, item.time)
1027
- : null,
1028
- );
1029
- }
1030
- function renderGroupHead(g, groupActive, chevron) {
1031
- const headSearch = g.items.length > 0 ? g.items[0].search : null;
1032
- const headFull = g.headIsUser && g.items.length > 0 ? g.items[0].full : null;
1033
- const useSnippet = filtering && hit(headSearch);
1034
- const sub = useSnippet ? snippetNodes(headSearch, kw, 5, 55) : g.subtitle;
1035
- const hasSub = sub !== "" && (!Array.isArray(sub) || sub.length > 0);
1036
- /* 标题策略:
1037
- 显示轮次: 轮次行 = 加粗「第 N 轮」; 压缩等非轮次行 = 加粗类型标签, 视作轮次同级
1038
- 隐藏轮次: 每行统一轨迹徽标 — 轮次头 = 「用户」业务蓝(助手开头则助手紫), 非轮次行 = 各自徽标(压缩=中性灰) */
1039
- let titleNode = null;
1040
- if (g.turn === null) {
1041
- if (turnsHidden && g.items.length > 0) {
1042
- const headItem = g.items[0];
1043
- titleNode = React.createElement("span", {
1044
- className: "cnvnav-badge cnvnav-badge-" + headItem.cls,
1045
- }, headItem.label);
1046
- } else {
1047
- titleNode = React.createElement("span", { className: "cnvnav-group-title" }, g.title);
1048
- }
1049
- } else if (!turnsHidden) {
1050
- titleNode = React.createElement("span", { className: "cnvnav-group-title" }, g.title);
1051
- } else if (g.items.length > 0) {
1052
- const headItem = g.items[0];
1053
- titleNode = React.createElement("span", {
1054
- className: "cnvnav-badge cnvnav-badge-" + headItem.cls,
1055
- }, headItem.kind === "user" ? "用户" : headItem.label);
1056
- }
1057
- return React.createElement("div", {
1058
- ref: (el) => { groupEls[g.gid] = el; },
1059
- className: "cnvnav-group-row" + (groupActive ? " cnvnav-group-row-active" : ""),
1060
- },
1061
- React.createElement(Tooltip, {
1062
- label: headFull !== null ? headFull : "跳转到此位置", maxWidth: 340,
1063
- side: "top",
1064
- delayMs: 500,
1065
- },
1066
- React.createElement("span", { className: "cnvnav-group-head-wrap" },
1067
- React.createElement("button", {
1068
- type: "button",
1069
- className: "cnvnav-group-head",
1070
- onClick: () => jumpTo(g.headKey),
1071
- },
1072
- titleNode,
1073
- hasSub
1074
- ? React.createElement("span", { className: "cnvnav-group-sub" }, sub)
1075
- : null,
1076
- ),
1077
- ),
1078
- ),
1079
- chevron !== undefined ? chevron : null,
1080
- );
1081
- }
1082
-
1083
- let groupNodes;
1084
- if (filtering) {
1085
- groupNodes = [];
1086
- for (const g of groups) {
1087
- const headHit = g.headIsUser && g.items.length > 0 && hit(g.items[0].search);
1088
- const hits = g.items.filter((it, i) =>
1089
- (it.kind === "user" || it.kind === "assistant-step") &&
1090
- hit(it.search) &&
1091
- !(i === 0 && g.headIsUser)
1092
- );
1093
- if (!headHit && hits.length === 0) continue;
1094
- const groupActive = g.items.some((it) => it.key === snap.activeKey);
1095
- groupNodes.push(React.createElement("div", {
1096
- key: g.gid,
1097
- className: "cnvnav-group cnvnav-fade-item",
1098
- style: { animationDelay: (groupNodes.length * 18) + "ms" },
1099
- },
1100
- renderGroupHead(g, groupActive, null),
1101
- hits.length > 0
1102
- ? React.createElement("div", { className: "cnvnav-steps" },
1103
- React.createElement("div", { className: "cnvnav-steps-inner" },
1104
- hits.map((it, i) => renderItem(it, i * 20)),
1105
- ),
1106
- )
1107
- : null,
1108
- ));
1109
- }
1110
- } else {
1111
- groupNodes = groups.map((g) => {
1112
- const expanded = snap.expanded[g.gid] === true;
1113
- const isTurnGroup = g.turn !== null;
1114
- const stepItems = isTurnGroup ? (g.headIsUser ? g.items.slice(1) : g.items) : [];
1115
- const hasSteps = stepItems.length > 0;
1116
- const groupActive = g.items.some((it) => it.key === snap.activeKey);
1117
- const chevron = hasSteps
1118
- ? React.createElement(Tooltip, {
1119
- label: expanded ? "折叠步次" : "展开步次",
1120
- side: "top",
1121
- delayMs: 500,
1122
- },
1123
- React.createElement("span", { className: "cnvnav-chevron-wrap" },
1124
- React.createElement("button", {
1125
- type: "button",
1126
- className: "cnvnav-chevron",
1127
- "aria-expanded": expanded,
1128
- onClick: (e) => {
1129
- e.stopPropagation();
1130
- setState({ expanded: Object.assign({}, state.expanded, { [g.gid]: !expanded }) });
1131
- },
1132
- },
1133
- React.createElement("svg", {
1134
- className: "cnvnav-chevron-icon",
1135
- viewBox: "0 0 24 24", width: 12, height: 12, fill: "none",
1136
- stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
1137
- "aria-hidden": true,
1138
- },
1139
- React.createElement("path", { d: "m9 18 6-6-6-6" }),
1140
- ),
1141
- expanded ? null : React.createElement("span", { className: "cnvnav-chevron-count" }, String(stepItems.length)),
1142
- ),
1143
- ),
1144
- )
1145
- : null;
1146
- return React.createElement("div", { key: g.gid, className: "cnvnav-group" },
1147
- renderGroupHead(g, groupActive, chevron),
1148
- hasSteps
1149
- ? React.createElement("div", {
1150
- className: "cnvnav-steps" + (expanded ? "" : " cnvnav-steps-collapsed"),
1151
- "aria-hidden": !expanded,
1152
- },
1153
- React.createElement("div", { className: "cnvnav-steps-inner" },
1154
- stepItems.map(renderItem),
1155
- ),
1156
- )
1157
- : null,
1158
- );
1159
- });
1160
- }
1161
-
1162
- return React.createElement("div", {
1163
- ref: (el) => { panelEl = el; },
1164
- className: "cnvnav-panel cnvnav-panel-unplaced" + (snap.open ? "" : " cnvnav-panel-hidden"),
1165
- role: "complementary",
1166
- "aria-label": "会话导航",
1167
- "aria-hidden": !snap.open,
1168
- },
1169
- React.createElement("div", {
1170
- className: "cnvnav-head",
1171
- onPointerDown: startDrag,
1172
- onPointerMove: moveDrag,
1173
- onPointerUp: endDrag,
1174
- onPointerCancel: endDrag,
1175
- },
1176
- React.createElement(Tooltip, {
1177
- label: searchOpen ? "收起搜索" : "搜索关键词",
1178
- side: "bottom",
1179
- delayMs: 500,
1180
- },
1181
- React.createElement("span", { className: "cnvnav-search" },
1182
- React.createElement(Button, {
1183
- variant: "ghost",
1184
- size: "sm",
1185
- icon: React.createElement(IconSearchOutline16, { size: 14 }),
1186
- onClick: () => toggleSearch(),
1187
- }),
1188
- ),
1189
- ),
1190
- searchOpen
1191
- ? null
1192
- : React.createElement(Tooltip, {
1193
- label: nextModeLabel(),
1194
- side: "bottom",
1195
- delayMs: 500,
1196
- },
1197
- React.createElement("span", { className: "cnvnav-turns-toggle" },
1198
- React.createElement(Button, {
1199
- variant: "ghost",
1200
- size: "sm",
1201
- className: viewMode !== "full" ? "cnvnav-toggle-on" : "",
1202
- icon: React.createElement(SwitchTurnsIcon),
1203
- "aria-pressed": viewMode !== "full",
1204
- onClick: () => cycleMode(),
1205
- }),
1206
- ),
1207
- ),
1208
- searchOpen
1209
- ? React.createElement("input", {
1210
- type: "text",
1211
- className: "cnvnav-filter",
1212
- placeholder: "请输入关键词",
1213
- value: filter,
1214
- autoFocus: true,
1215
- onChange: (e) => setFilter(e.target.value),
1216
- })
1217
- : null,
1218
- searchOpen
1219
- ? null
1220
- : React.createElement(Tooltip, {
1221
- label: snap.docked ? "解除停靠(拖动头部定位)" : "停靠回右侧",
1222
- side: "bottom",
1223
- delayMs: 500,
1224
- },
1225
- React.createElement("span", null,
1226
- React.createElement(Button, {
1227
- variant: snap.docked ? "primary" : "ghost",
1228
- size: "sm",
1229
- icon: React.createElement(PinIcon),
1230
- "aria-pressed": snap.docked,
1231
- onClick: () => {
1232
- if (snap.docked) {
1233
- const el = panelEl;
1234
- const r = el !== null ? el.getBoundingClientRect() : null;
1235
- setState({ docked: false, userPos: r !== null ? { left: Math.round(r.left), top: Math.round(r.top) } : null });
1236
- } else {
1237
- setState({ docked: true });
1238
- }
1239
- persistUi();
1240
- },
1241
- }),
1242
- ),
1243
- ),
1244
- React.createElement("span", { className: "cnvnav-count" }, (filtering ? groupNodes.length + " / " : "") + String(count) + " 条"),
1245
- React.createElement(Tooltip, {
1246
- label: "收起面板",
1247
- side: "bottom",
1248
- delayMs: 500,
1249
- },
1250
- React.createElement("span", { className: "cnvnav-close" },
1251
- React.createElement(Button, {
1252
- variant: "ghost",
1253
- size: "sm",
1254
- icon: React.createElement(IconCloseOutline16, { size: 14 }),
1255
- onClick: () => setState({ open: false }),
1256
- }),
1257
- ),
1258
- ),
1259
- ),
1260
- React.createElement("div", { className: "cnvnav-topbar" },
1261
- React.createElement(Tooltip, {
1262
- label: "加载更早的一批轮次",
1263
- side: "bottom",
1264
- delayMs: 500,
1265
- disabled: !snap.hasMore || snap.loadingOlder,
1266
- },
1267
- React.createElement("span", { className: "cnvnav-bar-btn" },
1268
- React.createElement(Button, {
1269
- variant: "ghost",
1270
- size: "sm",
1271
- className: "cnvnav-bar-fill",
1272
- icon: React.createElement(LoadEarlierIcon),
1273
- disabled: !snap.hasMore || snap.loadingOlder,
1274
- onClick: () => loadOlderPage(),
1275
- }, "加载更早"),
1276
- ),
1277
- ),
1278
- React.createElement(Tooltip, {
1279
- label: "加载全部历史轮次",
1280
- side: "bottom",
1281
- delayMs: 500,
1282
- disabled: !snap.hasMore || snap.loadingOlder || snap.loadingAll,
1283
- },
1284
- React.createElement("span", { className: "cnvnav-bar-btn" },
1285
- React.createElement(Button, {
1286
- variant: "ghost",
1287
- size: "sm",
1288
- className: "cnvnav-bar-fill",
1289
- icon: React.createElement(LoadAllIcon),
1290
- disabled: !snap.hasMore || snap.loadingOlder || snap.loadingAll,
1291
- onClick: () => loadAllPages(),
1292
- }, "加载全部"),
1293
- ),
1294
- ),
1295
- ),
1296
- React.createElement("div", {
1297
- ref: (el) => { listEl = el; },
1298
- className: "cnvnav-list",
1299
- },
1300
- groups.length === 0
1301
- ? React.createElement("div", { className: "cnvnav-empty" }, "当前会话暂无对话节点")
1302
- : (filtering && groupNodes.length === 0
1303
- ? React.createElement("div", { className: "cnvnav-empty" }, "无匹配结果")
1304
- : groupNodes),
1305
- ),
1306
- React.createElement("div", { className: "cnvnav-footer" },
1307
- React.createElement(Tooltip, {
1308
- label: "回到对话最新位置",
1309
- side: "top",
1310
- delayMs: 500,
1311
- },
1312
- React.createElement("span", { className: "cnvnav-bar-btn" },
1313
- React.createElement(Button, {
1314
- variant: "ghost",
1315
- size: "sm",
1316
- className: "cnvnav-bar-fill",
1317
- icon: React.createElement(JumpLatestIcon),
1318
- onClick: () => {
1319
- const gs = snap.outline !== null ? snap.outline.groups : [];
1320
- if (gs.length === 0) return;
1321
- const lastGroup = gs[gs.length - 1];
1322
- const lastItem = lastGroup.items[lastGroup.items.length - 1];
1323
- if (lastItem !== undefined) jumpTo(lastItem.key);
1324
- },
1325
- }, "回到最新"),
1326
- ),
1327
- ),
1328
- React.createElement(Tooltip, {
1329
- label: hasExpanded ? "折叠所有已展开的轮次" : "当前无展开的轮次",
1330
- side: "top",
1331
- delayMs: 500,
1332
- disabled: !hasExpanded,
1333
- },
1334
- React.createElement("span", { className: "cnvnav-bar-btn" },
1335
- React.createElement(Button, {
1336
- variant: "ghost",
1337
- size: "sm",
1338
- className: "cnvnav-bar-fill",
1339
- icon: React.createElement(CollapseAllIcon),
1340
- disabled: !hasExpanded,
1341
- onClick: () => setState({ expanded: {} }),
1342
- }, "全部折叠"),
1343
- ),
1344
- ),
1345
- ),
1346
- );
1347
- }
1348
-
1349
- /* ---------- register ---------- */
1350
- slots.inject("conversation.session.header.utilities", () => slots.register(
1351
- { name: "conversation.session.header.utilities", id: "conversation-navigator-toggle", order: 20, label: () => "会话导航" },
1352
- (props) => React.createElement(ToggleButton, props),
1353
- ));
1354
- slots.inject("shell.overlay", () => slots.register(
1355
- { name: "shell.overlay", id: "conversation-navigator-panel", order: 10, label: () => "会话导航" },
1356
- () => React.createElement(Panel),
1357
- ));
1358
- }
1359
-
1360
- const inject = ["slots", "sessions"];
1361
- exports.apply = apply;
1362
- exports.inject = inject;
1363
- return module.exports;
1364
- }
1365
- });
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
+ const MIN_PANEL_H = 90; /* 约 3 轮 */
776
+ function startResize(e) {
777
+ if (e.button !== 0) return;
778
+ if (resizeStart !== null) return;
779
+ const el = panelEl;
780
+ if (el === null) return;
781
+ resizeStart = {
782
+ startX: e.clientX, startY: e.clientY,
783
+ baseW: el.offsetWidth || 288,
784
+ baseH: el.offsetHeight || 200,
785
+ baseMaxH: (function () { const v = parseInt(el.style.maxHeight, 10); return Number.isFinite(v) && v > 0 ? v : (el.offsetHeight || 200); })(),
786
+ el: el,
787
+ };
788
+ try { e.currentTarget.setPointerCapture(e.pointerId); } catch (err) { /* 忽略 */ }
789
+ }
790
+ function moveResize(e) {
791
+ const d = resizeStart;
792
+ if (d === null) return;
793
+ const vh = document.documentElement.clientHeight || 800;
794
+ const dx = e.clientX - d.startX;
795
+ const dy = e.clientY - d.startY;
796
+ /* 高度用 maxHeight + height:auto: 实际高度 = min(内容, 上限)
797
+ 以拖拽起点为基准线性跟随鼠标(无漂移/无状态跳变):
798
+ 拖小立即压缩出滚动, 拖大在内容充足时线性恢复更多行;
799
+ 内容不足时面板保持内容高(拖动无感, 属预期) */
800
+ const r = d.el.getBoundingClientRect();
801
+ const sp = document.querySelector("[data-conversation-scroll]");
802
+ /* 上限必须与 place() 各分支一致, 否则松手后会被 place 压回造成跳变:
803
+ docked/常规 = min(容器高-24, vh-40); 自由位置 = vh-顶-8 */
804
+ const sr = sp !== null ? sp.getBoundingClientRect() : null;
805
+ const cap = Math.max(MIN_PANEL_H, Math.round(
806
+ state.docked && sr !== null
807
+ ? Math.min(sr.height - 24, vh - 40)
808
+ : vh - r.top - 8
809
+ ));
810
+ const nw = Math.min(Math.max(240, d.baseW - dx), 560);
811
+ /* 目标上限 = 起点可见高 + 总位移, 线性且可逆 */
812
+ const nMaxH = Math.min(Math.max(MIN_PANEL_H, d.baseH + dy), cap);
813
+ const left = Math.max(8, r.right - nw);
814
+ d.el.style.width = Math.round(nw) + "px";
815
+ d.el.style.left = Math.round(left) + "px";
816
+ d.el.style.height = "";
817
+ d.el.style.maxHeight = Math.round(nMaxH) + "px";
818
+ }
819
+ function endResize() {
820
+ const d = resizeStart;
821
+ resizeStart = null;
822
+ if (d === null) return;
823
+ const el = d.el;
824
+ const rect = el.getBoundingClientRect();
825
+ const nw = el.offsetWidth;
826
+ const parsed = parseInt(el.style.maxHeight, 10);
827
+ const nh = Number.isFinite(parsed) && parsed > 0 ? parsed : null;
828
+ if (state.docked) {
829
+ setState({ panelWidth: Math.round(nw), panelHeight: nh });
830
+ } else {
831
+ setState({ panelWidth: Math.round(nw), panelHeight: nh, userPos: { left: Math.round(rect.left), top: Math.round(rect.top) } });
832
+ }
833
+ persistUi();
834
+ }
835
+
836
+ React.useEffect(() => {
837
+ if (!snap.open) return;
838
+ let lastKey = null;
839
+ let stableTicks = 0;
840
+ let misses = 0;
841
+ function place() {
842
+ const panel = panelEl;
843
+ if (panel === null) return false;
844
+ if (dragMove !== null) return true;
845
+ const vw = document.documentElement.clientWidth || 1000;
846
+ const vh = document.documentElement.clientHeight || 800;
847
+ const mini = viewMode === "minimal" || viewMode === "minimal-left";
848
+ const alignLeft = viewMode === "minimal-left";
849
+ const width = (state.panelWidth || 288);
850
+ /* 面板宽度对非极简模式统一生效, 确保拖动(未停靠)状态下也能实时刷新 */
851
+ if (!mini) panel.style.width = width + "px";
852
+ /* 面板被拖走后不再自动停靠: 夹进视口、上边缘固定, 展开高度至多半屏以便下方留白 */
853
+ if (!mini && !state.docked && state.userPos !== null) {
854
+ const w = panel.offsetWidth || 288;
855
+ const h = panel.offsetHeight || 200;
856
+ const left = Math.min(Math.max(8, Math.round(state.userPos.left)), Math.max(8, vw - w - 8));
857
+ const top = Math.min(Math.max(8, Math.round(state.userPos.top)), Math.max(8, vh - h - 8));
858
+ const fh = state.panelHeight;
859
+ const cap = Math.max(MIN_PANEL_H, Math.round(vh - top - 8));
860
+ const def = Math.max(MIN_PANEL_H, Math.round(Math.min(vh / 2, vh - top - 8)));
861
+ panel.style.height = "";
862
+ panel.style.maxHeight = Math.max(MIN_PANEL_H, Math.min(typeof fh === "number" && fh > 0 ? fh : def, cap)) + "px";
863
+ panel.style.left = left + "px";
864
+ panel.style.top = top + "px";
865
+ panel.classList.remove("cnvnav-panel-unplaced");
866
+ return true;
867
+ }
868
+ const sp = document.querySelector("[data-conversation-scroll]");
869
+ const MINI_H = 220;
870
+ if (sp === null) {
871
+ /* 会话视图可能尚未挂载(刷新/重启/切会话时面板先就位);
872
+ 非会话页(设置页等)则永远没有该容器。
873
+ 连续 15 次(约 6s)仍未出现才回退到视口定位, 否则保持隐藏等待校准。 */
874
+ misses += 1;
875
+ if (misses >= 15) {
876
+ if (mini) {
877
+ /* 极简: 水平锚定, 右对齐由 CSS right 负责, 左对齐钉在视口左侧 */
878
+ if (alignLeft) {
879
+ panel.style.left = "12px";
880
+ panel.style.right = "auto";
881
+ } else {
882
+ panel.style.left = "";
883
+ panel.style.right = "";
884
+ }
885
+ panel.style.top = Math.max(8, Math.round((vh - MINI_H) / 2)) + "px";
886
+ panel.style.maxHeight = MINI_H + "px";
887
+ } else {
888
+ panel.style.left = Math.max(8, vw - width - 12) + "px";
889
+ panel.style.top = Math.max(8, 60) + "px";
890
+ const fh = state.panelHeight;
891
+ const cap = Math.round(vh - 90);
892
+ const def = cap;
893
+ panel.style.height = "";
894
+ panel.style.maxHeight = Math.max(MIN_PANEL_H, Math.min(typeof fh === "number" && fh > 0 ? fh : def, cap)) + "px";
895
+ }
896
+ panel.classList.remove("cnvnav-panel-unplaced");
897
+ return true;
898
+ }
899
+ return false;
900
+ }
901
+ const r = sp.getBoundingClientRect();
902
+ const key = mini
903
+ ? [Math.round(r.left), Math.round(r.top), Math.round(r.height), Math.round(vh)].join("|")
904
+ : [Math.round(r.top), Math.round(r.height), Math.round(vw), Math.round(vh)].join("|");
905
+ if (key !== lastKey) {
906
+ lastKey = key;
907
+ if (mini) {
908
+ /* 极简: 右对齐由 CSS(right)锚定视口右缘, 左对齐钉在对话区左缘 */
909
+ if (alignLeft) {
910
+ panel.style.left = Math.max(8, Math.round(r.left + 12)) + "px";
911
+ panel.style.right = "auto";
912
+ } else {
913
+ panel.style.left = "";
914
+ panel.style.right = "";
915
+ }
916
+ panel.style.top = Math.max(8, Math.round(r.top + (r.height - MINI_H) / 2)) + "px";
917
+ panel.style.maxHeight = MINI_H + "px";
918
+ } else {
919
+ panel.style.left = Math.max(8, vw - width - 12) + "px";
920
+ panel.style.top = Math.max(8, Math.round(r.top + 8)) + "px";
921
+ const fh = state.panelHeight;
922
+ const cap = Math.max(MIN_PANEL_H, Math.round(Math.min(r.height - 24, vh - 40)));
923
+ const def = cap;
924
+ panel.style.height = "";
925
+ panel.style.maxHeight = Math.max(MIN_PANEL_H, Math.min(typeof fh === "number" && fh > 0 ? fh : def, cap)) + "px";
926
+ }
927
+ }
928
+ panel.classList.remove("cnvnav-panel-unplaced");
929
+ return true;
930
+ }
931
+ place();
932
+ /* 每 400ms 复查: 容器出现即校准; 布局连续 3 次无变化后停止 */
933
+ let id = setInterval(() => {
934
+ const before = lastKey;
935
+ if (!place()) return;
936
+ if (lastKey !== null && lastKey === before) {
937
+ stableTicks += 1;
938
+ if (stableTicks >= 3) clearInterval(id);
939
+ } else {
940
+ stableTicks = 0;
941
+ }
942
+ }, 400);
943
+ window.addEventListener("resize", place);
944
+ /* 左对齐极简条需跟随左侧栏展开/折叠: 对话区宽度变化时重新校准 */
945
+ let ro = null;
946
+ const roTarget = document.querySelector("[data-conversation-scroll]");
947
+ if (typeof ResizeObserver === "function" && roTarget !== null) {
948
+ ro = new ResizeObserver(() => { place(); });
949
+ ro.observe(roTarget);
950
+ }
951
+ return () => {
952
+ clearInterval(id);
953
+ window.removeEventListener("resize", place);
954
+ if (ro !== null) ro.disconnect();
955
+ };
956
+ }, [snap.open, snap.sessionId, viewMode, state.docked, state.userPos, state.panelWidth, state.panelHeight]);
957
+
958
+ React.useEffect(() => {
959
+ if (!snap.open) return;
960
+ const run = () => {
961
+ const key = computeActiveKey();
962
+ if (key !== lastActive) {
963
+ lastActive = key;
964
+ setState({ activeKey: key });
965
+ }
966
+ };
967
+ const update = timer !== undefined ? timer.throttle(run, 120) : run;
968
+ document.addEventListener("scroll", update, true);
969
+ window.addEventListener("resize", update);
970
+ run();
971
+ return () => {
972
+ document.removeEventListener("scroll", update, true);
973
+ window.removeEventListener("resize", update);
974
+ if (timer !== undefined && typeof update.dispose === "function") update.dispose();
975
+ };
976
+ }, [snap.open]);
977
+
978
+ React.useEffect(() => {
979
+ if (!snap.open || snap.activeKey === null) return;
980
+ let target = itemEls[snap.activeKey];
981
+ if (target === undefined || target === null || (typeof target.closest === "function" && target.closest(".cnvnav-steps-collapsed") !== null)) {
982
+ const gid = snap.outline !== null ? snap.outline.byKey[snap.activeKey] : undefined;
983
+ if (gid !== undefined) target = groupEls[gid];
984
+ }
985
+ const list = listEl;
986
+ if (target !== undefined && target !== null && list !== null) {
987
+ const lr = list.getBoundingClientRect();
988
+ const er = target.getBoundingClientRect();
989
+ const rel = er.top - lr.top + list.scrollTop;
990
+ if (rel < list.scrollTop || rel + er.height > list.scrollTop + list.clientHeight) {
991
+ list.scrollTo({ top: Math.max(0, rel - list.clientHeight / 2), behavior: "smooth" });
992
+ }
993
+ }
994
+ }, [snap.open, snap.activeKey]);
995
+
996
+ const groups = snap.outline !== null ? snap.outline.groups : [];
997
+ const count = snap.outline !== null ? snap.outline.count : 0;
998
+ const hasExpanded = Object.keys(snap.expanded).some((k) => snap.expanded[k] === true);
999
+
1000
+ /* ---------- 极简模式(右/左对齐): 只保留定位 ---------- */
1001
+ if (viewMode === "minimal" || viewMode === "minimal-left") {
1002
+ const miniRows = [];
1003
+ for (const g of groups) {
1004
+ if (g.turn === null) continue;
1005
+ miniRows.push({
1006
+ key: g.headKey,
1007
+ text: g.subtitle,
1008
+ full: g.headIsUser && g.items.length > 0 ? g.items[0].full : null,
1009
+ active: g.items.some((it) => it.key === snap.activeKey),
1010
+ });
1011
+ }
1012
+ return React.createElement("div", {
1013
+ ref: (el) => { panelEl = el; },
1014
+ className: "cnvnav-mini-wrap" + (viewMode === "minimal-left" ? " cnvnav-mini-wrap-left" : "") + " cnvnav-panel-unplaced" + (snap.open ? "" : " cnvnav-panel-hidden"),
1015
+ role: "complementary",
1016
+ "aria-label": "会话导航",
1017
+ "aria-hidden": !snap.open,
1018
+ },
1019
+ React.createElement(Tooltip, {
1020
+ label: nextModeLabel(),
1021
+ side: "top",
1022
+ delayMs: 500,
1023
+ },
1024
+ React.createElement("span", { className: "cnvnav-mini-fab" },
1025
+ React.createElement(Button, {
1026
+ variant: "ghost",
1027
+ size: "sm",
1028
+ icon: React.createElement(SwitchTurnsIcon),
1029
+ "aria-pressed": true,
1030
+ onClick: () => cycleMode(),
1031
+ }),
1032
+ ),
1033
+ ),
1034
+ React.createElement("div", { className: "cnvnav-mini-panel" },
1035
+ React.createElement("div", {
1036
+ ref: (el) => { listEl = el; },
1037
+ className: "cnvnav-mini-list",
1038
+ },
1039
+ groups.length === 0
1040
+ ? React.createElement("div", { className: "cnvnav-empty" }, "当前会话暂无对话节点")
1041
+ : miniRows.map((r) => React.createElement(Tooltip, {
1042
+ key: r.key,
1043
+ label: r.full !== null ? r.full : "跳转到此位置",
1044
+ side: "top",
1045
+ delayMs: 500,
1046
+ maxWidth: 340,
1047
+ },
1048
+ React.createElement("button", {
1049
+ type: "button",
1050
+ ref: (el) => { itemEls[r.key] = el; },
1051
+ className: "cnvnav-mini-item" + (r.active ? " cnvnav-mini-item-active" : ""),
1052
+ onClick: () => jumpTo(r.key),
1053
+ }, r.text),
1054
+ )),
1055
+ ),
1056
+ ),
1057
+ );
1058
+ }
1059
+
1060
+ const kw = filter.trim().toLowerCase();
1061
+ const filtering = kw !== "";
1062
+ function hit(text) {
1063
+ return typeof text === "string" && text !== "" && text.toLowerCase().indexOf(kw) !== -1;
1064
+ }
1065
+ /* Split text into nodes, wrapping every case-insensitive keyword match in a <mark>. */
1066
+ function highlightedNodes(text, kw) {
1067
+ if (typeof text !== "string" || kw === "") return [text];
1068
+ const lower = text.toLowerCase();
1069
+ const out = [];
1070
+ let i = 0;
1071
+ let m = 0;
1072
+ for (;;) {
1073
+ const idx = lower.indexOf(kw, i);
1074
+ if (idx === -1) { if (i < text.length) out.push(text.slice(i)); break; }
1075
+ if (idx > i) out.push(text.slice(i, idx));
1076
+ out.push(React.createElement("mark", { key: "m" + (m++), className: "cnvnav-hl" }, text.slice(idx, idx + kw.length)));
1077
+ i = idx + kw.length;
1078
+ }
1079
+ return out;
1080
+ }
1081
+ /* Window the text around the first keyword match (so the keyword is visible,
1082
+ not hidden behind head-truncation), then highlight every occurrence. */
1083
+ function snippetNodes(text, kw, before, after) {
1084
+ if (typeof text !== "string") return [text];
1085
+ const idx = text.toLowerCase().indexOf(kw);
1086
+ if (idx === -1) return highlightedNodes(text, kw);
1087
+ const start = Math.max(0, idx - before);
1088
+ const end = Math.min(text.length, idx + kw.length + after);
1089
+ const s = text.slice(start, end);
1090
+ const parts = [];
1091
+ if (start > 0) parts.push("…");
1092
+ parts.push(...highlightedNodes(s, kw));
1093
+ if (end < text.length) parts.push("…");
1094
+ return parts;
1095
+ }
1096
+ function renderItem(item, staggerMs) {
1097
+ const useSnippet = filtering && hit(item.search);
1098
+ const preview = useSnippet ? snippetNodes(item.search, kw, 5, 60) : item.preview;
1099
+ const hasPreview = preview !== null && preview !== "" && (!Array.isArray(preview) || preview.length > 0);
1100
+ return React.createElement("button", {
1101
+ key: item.key,
1102
+ type: "button",
1103
+ ref: (el) => { itemEls[item.key] = el; },
1104
+ className: "cnvnav-item" + (item.key === snap.activeKey ? " cnvnav-item-active" : "") + (staggerMs !== undefined ? " cnvnav-fade-item" : ""),
1105
+ style: staggerMs !== undefined ? { animationDelay: staggerMs + "ms" } : undefined,
1106
+ onClick: () => jumpTo(item.key),
1107
+ },
1108
+ React.createElement("span", { className: "cnvnav-badge cnvnav-badge-" + item.cls }, item.label),
1109
+ hasPreview
1110
+ ? React.createElement("span", { className: "cnvnav-item-preview" }, preview)
1111
+ : null,
1112
+ item.time !== ""
1113
+ ? React.createElement("span", { className: "cnvnav-item-time" }, item.time)
1114
+ : null,
1115
+ );
1116
+ }
1117
+ function renderGroupHead(g, groupActive, chevron) {
1118
+ const headSearch = g.items.length > 0 ? g.items[0].search : null;
1119
+ const headFull = g.headIsUser && g.items.length > 0 ? g.items[0].full : null;
1120
+ const useSnippet = filtering && hit(headSearch);
1121
+ const sub = useSnippet ? snippetNodes(headSearch, kw, 5, 55) : g.subtitle;
1122
+ const hasSub = sub !== "" && (!Array.isArray(sub) || sub.length > 0);
1123
+ /* 标题策略:
1124
+ 显示轮次: 轮次行 = 加粗「第 N 轮」; 压缩等非轮次行 = 加粗类型标签, 视作轮次同级
1125
+ 隐藏轮次: 每行统一轨迹徽标 — 轮次头 = 「用户」业务蓝(助手开头则助手紫), 非轮次行 = 各自徽标(压缩=中性灰) */
1126
+ let titleNode = null;
1127
+ if (g.turn === null) {
1128
+ if (turnsHidden && g.items.length > 0) {
1129
+ const headItem = g.items[0];
1130
+ titleNode = React.createElement("span", {
1131
+ className: "cnvnav-badge cnvnav-badge-" + headItem.cls,
1132
+ }, headItem.label);
1133
+ } else {
1134
+ titleNode = React.createElement("span", { className: "cnvnav-group-title" }, g.title);
1135
+ }
1136
+ } else if (!turnsHidden) {
1137
+ titleNode = React.createElement("span", { className: "cnvnav-group-title" }, g.title);
1138
+ } else if (g.items.length > 0) {
1139
+ const headItem = g.items[0];
1140
+ titleNode = React.createElement("span", {
1141
+ className: "cnvnav-badge cnvnav-badge-" + headItem.cls,
1142
+ }, headItem.kind === "user" ? "用户" : headItem.label);
1143
+ }
1144
+ return React.createElement("div", {
1145
+ ref: (el) => { groupEls[g.gid] = el; },
1146
+ className: "cnvnav-group-row" + (groupActive ? " cnvnav-group-row-active" : ""),
1147
+ },
1148
+ React.createElement(Tooltip, {
1149
+ label: headFull !== null ? headFull : "跳转到此位置", maxWidth: 340,
1150
+ side: "top",
1151
+ delayMs: 500,
1152
+ },
1153
+ React.createElement("span", { className: "cnvnav-group-head-wrap" },
1154
+ React.createElement("button", {
1155
+ type: "button",
1156
+ className: "cnvnav-group-head",
1157
+ onClick: () => jumpTo(g.headKey),
1158
+ },
1159
+ titleNode,
1160
+ hasSub
1161
+ ? React.createElement("span", { className: "cnvnav-group-sub" }, sub)
1162
+ : null,
1163
+ ),
1164
+ ),
1165
+ ),
1166
+ chevron !== undefined ? chevron : null,
1167
+ );
1168
+ }
1169
+
1170
+ let groupNodes;
1171
+ if (filtering) {
1172
+ groupNodes = [];
1173
+ for (const g of groups) {
1174
+ const headHit = g.headIsUser && g.items.length > 0 && hit(g.items[0].search);
1175
+ const hits = g.items.filter((it, i) =>
1176
+ (it.kind === "user" || it.kind === "assistant-step") &&
1177
+ hit(it.search) &&
1178
+ !(i === 0 && g.headIsUser)
1179
+ );
1180
+ if (!headHit && hits.length === 0) continue;
1181
+ const groupActive = g.items.some((it) => it.key === snap.activeKey);
1182
+ groupNodes.push(React.createElement("div", {
1183
+ key: g.gid,
1184
+ className: "cnvnav-group cnvnav-fade-item",
1185
+ style: { animationDelay: (groupNodes.length * 18) + "ms" },
1186
+ },
1187
+ renderGroupHead(g, groupActive, null),
1188
+ hits.length > 0
1189
+ ? React.createElement("div", { className: "cnvnav-steps" },
1190
+ React.createElement("div", { className: "cnvnav-steps-inner" },
1191
+ hits.map((it, i) => renderItem(it, i * 20)),
1192
+ ),
1193
+ )
1194
+ : null,
1195
+ ));
1196
+ }
1197
+ } else {
1198
+ groupNodes = groups.map((g) => {
1199
+ const expanded = snap.expanded[g.gid] === true;
1200
+ const isTurnGroup = g.turn !== null;
1201
+ const stepItems = isTurnGroup ? (g.headIsUser ? g.items.slice(1) : g.items) : [];
1202
+ const hasSteps = stepItems.length > 0;
1203
+ const groupActive = g.items.some((it) => it.key === snap.activeKey);
1204
+ const chevron = hasSteps
1205
+ ? React.createElement(Tooltip, {
1206
+ label: expanded ? "折叠步次" : "展开步次",
1207
+ side: "top",
1208
+ delayMs: 500,
1209
+ },
1210
+ React.createElement("span", { className: "cnvnav-chevron-wrap" },
1211
+ React.createElement("button", {
1212
+ type: "button",
1213
+ className: "cnvnav-chevron",
1214
+ "aria-expanded": expanded,
1215
+ onClick: (e) => {
1216
+ e.stopPropagation();
1217
+ setState({ expanded: Object.assign({}, state.expanded, { [g.gid]: !expanded }) });
1218
+ },
1219
+ },
1220
+ React.createElement("svg", {
1221
+ className: "cnvnav-chevron-icon",
1222
+ viewBox: "0 0 24 24", width: 12, height: 12, fill: "none",
1223
+ stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
1224
+ "aria-hidden": true,
1225
+ },
1226
+ React.createElement("path", { d: "m9 18 6-6-6-6" }),
1227
+ ),
1228
+ expanded ? null : React.createElement("span", { className: "cnvnav-chevron-count" }, String(stepItems.length)),
1229
+ ),
1230
+ ),
1231
+ )
1232
+ : null;
1233
+ return React.createElement("div", { key: g.gid, className: "cnvnav-group" },
1234
+ renderGroupHead(g, groupActive, chevron),
1235
+ hasSteps
1236
+ ? React.createElement("div", {
1237
+ className: "cnvnav-steps" + (expanded ? "" : " cnvnav-steps-collapsed"),
1238
+ "aria-hidden": !expanded,
1239
+ },
1240
+ React.createElement("div", { className: "cnvnav-steps-inner" },
1241
+ stepItems.map(renderItem),
1242
+ ),
1243
+ )
1244
+ : null,
1245
+ );
1246
+ });
1247
+ }
1248
+
1249
+ return React.createElement("div", {
1250
+ ref: (el) => { panelEl = el; },
1251
+ className: "cnvnav-panel cnvnav-panel-unplaced" + (snap.open ? "" : " cnvnav-panel-hidden"),
1252
+ role: "complementary",
1253
+ "aria-label": "会话导航",
1254
+ "aria-hidden": !snap.open,
1255
+ },
1256
+ React.createElement("div", {
1257
+ className: "cnvnav-head",
1258
+ onPointerDown: startDrag,
1259
+ onPointerMove: moveDrag,
1260
+ onPointerUp: endDrag,
1261
+ onPointerCancel: endDrag,
1262
+ },
1263
+ React.createElement(Tooltip, {
1264
+ label: searchOpen ? "收起搜索" : "搜索关键词",
1265
+ side: "bottom",
1266
+ delayMs: 500,
1267
+ },
1268
+ React.createElement("span", { className: "cnvnav-search" },
1269
+ React.createElement(Button, {
1270
+ variant: "ghost",
1271
+ size: "sm",
1272
+ icon: React.createElement(IconSearchOutline16, { size: 14 }),
1273
+ onClick: () => toggleSearch(),
1274
+ }),
1275
+ ),
1276
+ ),
1277
+ searchOpen
1278
+ ? null
1279
+ : React.createElement(Tooltip, {
1280
+ label: nextModeLabel(),
1281
+ side: "bottom",
1282
+ delayMs: 500,
1283
+ },
1284
+ React.createElement("span", { className: "cnvnav-turns-toggle" },
1285
+ React.createElement(Button, {
1286
+ variant: "ghost",
1287
+ size: "sm",
1288
+ className: viewMode !== "full" ? "cnvnav-toggle-on" : "",
1289
+ icon: React.createElement(SwitchTurnsIcon),
1290
+ "aria-pressed": viewMode !== "full",
1291
+ onClick: () => cycleMode(),
1292
+ }),
1293
+ ),
1294
+ ),
1295
+ searchOpen
1296
+ ? React.createElement("input", {
1297
+ type: "text",
1298
+ className: "cnvnav-filter",
1299
+ placeholder: "请输入关键词",
1300
+ value: filter,
1301
+ autoFocus: true,
1302
+ onChange: (e) => setFilter(e.target.value),
1303
+ })
1304
+ : null,
1305
+ searchOpen
1306
+ ? null
1307
+ : React.createElement(Tooltip, {
1308
+ label: snap.docked ? "解除停靠(拖动头部定位)" : "停靠回右侧",
1309
+ side: "bottom",
1310
+ delayMs: 500,
1311
+ },
1312
+ React.createElement("span", null,
1313
+ React.createElement(Button, {
1314
+ variant: snap.docked ? "primary" : "ghost",
1315
+ size: "sm",
1316
+ icon: React.createElement(PinIcon),
1317
+ "aria-pressed": snap.docked,
1318
+ onClick: () => {
1319
+ if (snap.docked) {
1320
+ const el = panelEl;
1321
+ const r = el !== null ? el.getBoundingClientRect() : null;
1322
+ setState({ docked: false, userPos: r !== null ? { left: Math.round(r.left), top: Math.round(r.top) } : null });
1323
+ } else {
1324
+ setState({ docked: true });
1325
+ }
1326
+ persistUi();
1327
+ },
1328
+ }),
1329
+ ),
1330
+ ),
1331
+ React.createElement("span", { className: "cnvnav-count" }, (filtering ? groupNodes.length + " / " : "") + String(count) + " 条"),
1332
+ React.createElement(Tooltip, {
1333
+ label: "收起面板",
1334
+ side: "bottom",
1335
+ delayMs: 500,
1336
+ },
1337
+ React.createElement("span", { className: "cnvnav-close" },
1338
+ React.createElement(Button, {
1339
+ variant: "ghost",
1340
+ size: "sm",
1341
+ icon: React.createElement(IconCloseOutline16, { size: 14 }),
1342
+ onClick: () => setState({ open: false }),
1343
+ }),
1344
+ ),
1345
+ ),
1346
+ ),
1347
+ React.createElement("div", { className: "cnvnav-topbar" },
1348
+ React.createElement(Tooltip, {
1349
+ label: "加载更早的一批轮次",
1350
+ side: "bottom",
1351
+ delayMs: 500,
1352
+ disabled: !snap.hasMore || snap.loadingOlder,
1353
+ },
1354
+ React.createElement("span", { className: "cnvnav-bar-btn" },
1355
+ React.createElement(Button, {
1356
+ variant: "ghost",
1357
+ size: "sm",
1358
+ className: "cnvnav-bar-fill",
1359
+ icon: React.createElement(LoadEarlierIcon),
1360
+ disabled: !snap.hasMore || snap.loadingOlder,
1361
+ onClick: () => loadOlderPage(),
1362
+ }, "加载更早"),
1363
+ ),
1364
+ ),
1365
+ React.createElement(Tooltip, {
1366
+ label: "加载全部历史轮次",
1367
+ side: "bottom",
1368
+ delayMs: 500,
1369
+ disabled: !snap.hasMore || snap.loadingOlder || snap.loadingAll,
1370
+ },
1371
+ React.createElement("span", { className: "cnvnav-bar-btn" },
1372
+ React.createElement(Button, {
1373
+ variant: "ghost",
1374
+ size: "sm",
1375
+ className: "cnvnav-bar-fill",
1376
+ icon: React.createElement(LoadAllIcon),
1377
+ disabled: !snap.hasMore || snap.loadingOlder || snap.loadingAll,
1378
+ onClick: () => loadAllPages(),
1379
+ }, "加载全部"),
1380
+ ),
1381
+ ),
1382
+ ),
1383
+ React.createElement("div", {
1384
+ ref: (el) => { listEl = el; },
1385
+ className: "cnvnav-list",
1386
+ },
1387
+ groups.length === 0
1388
+ ? React.createElement("div", { className: "cnvnav-empty" }, "当前会话暂无对话节点")
1389
+ : (filtering && groupNodes.length === 0
1390
+ ? React.createElement("div", { className: "cnvnav-empty" }, "无匹配结果")
1391
+ : groupNodes),
1392
+ ),
1393
+ React.createElement("div", { className: "cnvnav-footer" },
1394
+ React.createElement(Tooltip, {
1395
+ label: "回到对话最新位置",
1396
+ side: "top",
1397
+ delayMs: 500,
1398
+ },
1399
+ React.createElement("span", { className: "cnvnav-bar-btn" },
1400
+ React.createElement(Button, {
1401
+ variant: "ghost",
1402
+ size: "sm",
1403
+ className: "cnvnav-bar-fill",
1404
+ icon: React.createElement(JumpLatestIcon),
1405
+ onClick: () => {
1406
+ const gs = snap.outline !== null ? snap.outline.groups : [];
1407
+ if (gs.length === 0) return;
1408
+ const lastGroup = gs[gs.length - 1];
1409
+ const lastItem = lastGroup.items[lastGroup.items.length - 1];
1410
+ if (lastItem !== undefined) jumpTo(lastItem.key);
1411
+ },
1412
+ }, "回到最新"),
1413
+ ),
1414
+ ),
1415
+ React.createElement(Tooltip, {
1416
+ label: hasExpanded ? "折叠所有已展开的轮次" : "当前无展开的轮次",
1417
+ side: "top",
1418
+ delayMs: 500,
1419
+ disabled: !hasExpanded,
1420
+ },
1421
+ React.createElement("span", { className: "cnvnav-bar-btn" },
1422
+ React.createElement(Button, {
1423
+ variant: "ghost",
1424
+ size: "sm",
1425
+ className: "cnvnav-bar-fill",
1426
+ icon: React.createElement(CollapseAllIcon),
1427
+ disabled: !hasExpanded,
1428
+ onClick: () => setState({ expanded: {} }),
1429
+ }, "全部折叠"),
1430
+ ),
1431
+ ),
1432
+ ),
1433
+ React.createElement("span", {
1434
+ className: "cnvnav-resize-grip",
1435
+ onPointerDown: startResize,
1436
+ onPointerMove: moveResize,
1437
+ onPointerUp: endResize,
1438
+ onPointerCancel: endResize,
1439
+ "aria-hidden": true,
1440
+ }),
1441
+ );
1442
+ }
1443
+
1444
+ /* ---------- register ---------- */
1445
+ slots.inject("conversation.session.header.utilities", () => slots.register(
1446
+ { name: "conversation.session.header.utilities", id: "conversation-navigator-toggle", order: 20, label: () => "会话导航" },
1447
+ (props) => React.createElement(ToggleButton, props),
1448
+ ));
1449
+ slots.inject("shell.overlay", () => slots.register(
1450
+ { name: "shell.overlay", id: "conversation-navigator-panel", order: 10, label: () => "会话导航" },
1451
+ () => React.createElement(Panel),
1452
+ ));
1453
+ }
1454
+
1455
+ const inject = ["slots", "sessions"];
1456
+ exports.apply = apply;
1457
+ exports.inject = inject;
1458
+ return module.exports;
1459
+ }
1460
+ });