dsh-conversation-overview 1.3.1 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/client.js +1681 -121
- package/cordis.patch.yml +1 -0
- package/index.js +1 -0
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -5,6 +5,18 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
const { jsx, jsxs } = require("react/jsx-runtime");
|
|
6
6
|
const h = React.createElement;
|
|
7
7
|
|
|
8
|
+
/* The chat surface renders Markdown with the harness' own MarkdownText, so a
|
|
9
|
+
document opened by this plugin borrows that exact component when it can: same
|
|
10
|
+
fonts, spacing, code banners and tables as a message in the conversation. */
|
|
11
|
+
const ChatMarkdown = (() => {
|
|
12
|
+
try {
|
|
13
|
+
const primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
14
|
+
return primitives && typeof primitives.MarkdownText === "function" ? primitives.MarkdownText : null;
|
|
15
|
+
} catch (error) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
})();
|
|
19
|
+
|
|
8
20
|
function cx(...parts) {
|
|
9
21
|
return parts.filter((part) => typeof part === "string" && part.length > 0).join(" ");
|
|
10
22
|
}
|
|
@@ -166,7 +178,7 @@ window.__ModuleLoader__.load({
|
|
|
166
178
|
}
|
|
167
179
|
const PLACEMENTS = ["conversation", "sidebar", "both"];
|
|
168
180
|
const TIME_MODES = ["clock", "elapsed"];
|
|
169
|
-
const CONFIG_FIELDS = ["showProgress", "showArtifacts", "showReferences", "showStats", "timeMode", "stepAnchors", "placement", "sectionFullHeight", "todoPrompt", "todoPromptTemplate", "quickNav", "quickNavLink", "vikingPreview", "vikingUrl", "vikingAccount", "vikingUser"];
|
|
181
|
+
const CONFIG_FIELDS = ["showProgress", "showArtifacts", "showReferences", "showStats", "timeMode", "stepAnchors", "placement", "sectionFullHeight", "todoPrompt", "todoPromptTemplate", "quickNav", "quickNavLink", "vikingPreview", "vikingUrl", "vikingAccount", "vikingUser", "conclusionTab"];
|
|
170
182
|
const VIKING_TAB_ID = "dsh-conversation-overview/viking";
|
|
171
183
|
const VIKING_TAB_KIND = "viking-memory";
|
|
172
184
|
const VIKING_ADDRESS_PREFIX = "dsh-resource://viking-memory/";
|
|
@@ -194,6 +206,10 @@ window.__ModuleLoader__.load({
|
|
|
194
206
|
"progress.emptyTurn": "本轮未创建任务计划",
|
|
195
207
|
"progress.recentSteps": "本轮关键步骤(最近 6 条)",
|
|
196
208
|
"step.compact": "上下文压缩",
|
|
209
|
+
"step.approvalApproved": "已批准",
|
|
210
|
+
"step.approvalPending": "待审批",
|
|
211
|
+
"step.approvalGated": "含审批",
|
|
212
|
+
"step.approvalDenied": "已拒绝",
|
|
197
213
|
"step.approval": "用户审批",
|
|
198
214
|
"progress.allSteps": "本轮关键步骤(全部)",
|
|
199
215
|
"progress.showEarlierSteps": "显示更早的步骤",
|
|
@@ -249,13 +265,22 @@ window.__ModuleLoader__.load({
|
|
|
249
265
|
"settings.quickNavRow": "对话轮次导航条",
|
|
250
266
|
"settings.quickNavLinkRow": "导航联动概览",
|
|
251
267
|
"settings.quickNavNote": "对话区右侧的轮次刻度条:悬停预览该轮问答,点击跳转;开启后窄窗口下也保持显示,刻度悬停可见轮次号。",
|
|
268
|
+
"markdown.copy": "复制",
|
|
269
|
+
"markdown.copied": "已复制",
|
|
270
|
+
"markdown.code": "代码",
|
|
271
|
+
"markdown.wrap": "自动换行",
|
|
272
|
+
"markdown.unwrap": "取消换行",
|
|
273
|
+
"markdown.footnotes": "脚注",
|
|
252
274
|
"settings.quickNavLinkNote": "点击导航刻度跳转对话时,已打开的概览同步切换到对应轮次。",
|
|
253
275
|
"settings.interaction": "交互",
|
|
254
|
-
"settings.todoPromptRow": "
|
|
255
|
-
"settings.
|
|
256
|
-
"settings.
|
|
257
|
-
"settings.
|
|
276
|
+
"settings.todoPromptRow": "双击待办",
|
|
277
|
+
"settings.todoPromptOn": "提示词",
|
|
278
|
+
"settings.todoPromptOff": "关闭",
|
|
279
|
+
"settings.todoPromptHint": "查看内置默认模版",
|
|
280
|
+
"settings.todoPromptDefault": "内置默认模版",
|
|
281
|
+
"settings.todoPromptPlaceholder": "双击进展中的待办行,把重执行提示词填入输入框。\n模板留空则使用内置提示词;支持占位符 {index}(步骤序号)与 {content}(步骤内容)。",
|
|
258
282
|
"settings.placementRow": "概览显示位置",
|
|
283
|
+
"settings.placementNote": "对话下方=在对话里显示一块概览卡片;右侧边栏=在右边栏单独开一页;两处=同时显示,内容一致。",
|
|
259
284
|
"settings.viking": "OpenViking 记忆预览",
|
|
260
285
|
"settings.vikingEnable": "启用记忆预览",
|
|
261
286
|
"settings.vikingUrl": "服务地址",
|
|
@@ -274,9 +299,11 @@ window.__ModuleLoader__.load({
|
|
|
274
299
|
"viking.field.keywords": "关键词",
|
|
275
300
|
"viking.field.source": "来源",
|
|
276
301
|
"reference.sort.time": "时间",
|
|
277
|
-
"reference.sort.count": "
|
|
302
|
+
"reference.sort.count": "轮次",
|
|
303
|
+
"reference.sort.hits": "次数",
|
|
278
304
|
"reference.sort.time.title": "按最近引用排序",
|
|
279
|
-
"reference.sort.count.title": "
|
|
305
|
+
"reference.sort.count.title": "按出现的轮次数排序",
|
|
306
|
+
"reference.sort.hits.title": "按本轮出现次数排序",
|
|
280
307
|
"reference.sort.cancel": "点击取消排序",
|
|
281
308
|
"reference.previewHint": "点击预览注入的内容快照",
|
|
282
309
|
"scope.label": "概览范围",
|
|
@@ -287,9 +314,25 @@ window.__ModuleLoader__.load({
|
|
|
287
314
|
"viking.revealHint": "打开目录",
|
|
288
315
|
"viking.revealFailed": "打开目录失败",
|
|
289
316
|
"viking.revealRestart": "宿主路由未就绪,请重启 DSH 后重试",
|
|
317
|
+
"artifact.conclusion": "结论",
|
|
318
|
+
"artifact.conclusionTurn": "第 {n} 轮结论",
|
|
319
|
+
"artifact.conclusionChars": "{n} 字",
|
|
320
|
+
"artifact.conclusionHint": "本轮结论文档,点击在新标签中打开",
|
|
321
|
+
"context.subtitleConclusion": "本轮结论 · Markdown 文档",
|
|
322
|
+
"settings.conclusionTabRow": "本轮结束后自动打开结论",
|
|
323
|
+
"settings.conclusionTabOn": "新标签页",
|
|
324
|
+
"settings.conclusionTabOff": "关闭",
|
|
325
|
+
"settings.conclusionTabNote": "一轮对话结束时,把最终答复作为 Markdown 文档自动开在新标签里;之后仍可在「产物」里点开。",
|
|
326
|
+
"settings.conclusionTabOffNote": "不自动打开标签;本轮结论仍会出现在「产物」里,点一下即可查看。",
|
|
327
|
+
"viking.eventMemory": "记忆",
|
|
328
|
+
"viking.prefMemory": "偏好",
|
|
329
|
+
"viking.entityMemory": "实体",
|
|
330
|
+
"viking.sessionMemory": "会话记忆",
|
|
331
|
+
"viking.genericMemory": "记忆",
|
|
290
332
|
"viking.revealed": "已在系统中打开",
|
|
291
333
|
"context.tab": "上下文预览",
|
|
292
334
|
"context.subtitle": "注入内容快照",
|
|
335
|
+
"context.subtitlePlan": "计划内容快照",
|
|
293
336
|
"context.unavailable": "内容快照不可用(页面刷新后需回到对应会话重新加载)",
|
|
294
337
|
"skill.revealTitle": "在系统中打开技能 {name} 的目录",
|
|
295
338
|
"skill.revealFailed": "打开技能目录失败",
|
|
@@ -330,6 +373,7 @@ window.__ModuleLoader__.load({
|
|
|
330
373
|
"goal.blocked": "受阻",
|
|
331
374
|
"goal.complete": "已完成",
|
|
332
375
|
"artifact.presented": "交付",
|
|
376
|
+
"artifact.plan": "计划",
|
|
333
377
|
"artifact.memory": "记忆",
|
|
334
378
|
"artifact.menu.reveal": "在上层文件夹中显示",
|
|
335
379
|
"artifact.menu.openWith": "打开方式",
|
|
@@ -357,8 +401,32 @@ window.__ModuleLoader__.load({
|
|
|
357
401
|
"reference.producer.agentInstructions": "Agent 指令",
|
|
358
402
|
"reference.more": "+{n} 条",
|
|
359
403
|
"reference.times": "×{n}",
|
|
404
|
+
"reference.turns": "{n} 轮",
|
|
405
|
+
"reference.turnsTitle": "出现在 {turns} 轮对话中,共 {times} 次",
|
|
406
|
+
"reference.timesTitle": "本轮出现 {times} 次",
|
|
360
407
|
"action.openInSidebar": "在侧边栏打开",
|
|
361
408
|
"state.loadingHistory": "正在载入更早的记录…",
|
|
409
|
+
"state.reloadHistory": "重新加载",
|
|
410
|
+
"state.loadingStalled": "更早的记录未能载入",
|
|
411
|
+
"state.reloadHistory.title": "重新开始载入更早的记录",
|
|
412
|
+
"intro.newTurn": "第 {n} 轮对话开始",
|
|
413
|
+
"progress.stepCommand": "命令 / 参数",
|
|
414
|
+
"progress.stepResult": "输出",
|
|
415
|
+
"progress.stepError": "错误",
|
|
416
|
+
"progress.stepDuration": "耗时 {v}",
|
|
417
|
+
"progress.stepChars": "{n} 字符",
|
|
418
|
+
"progress.stepTruncated": "输出较长,仅显示前段",
|
|
419
|
+
"progress.stepSkipped": "输出过大,未保留内容",
|
|
420
|
+
"progress.expandStep": "展开查看命令与输出",
|
|
421
|
+
"progress.collapseStep": "收起命令与输出",
|
|
422
|
+
"role.main": "主代理",
|
|
423
|
+
"role.subagent": "子代理",
|
|
424
|
+
"role.children": "子代理 {n}",
|
|
425
|
+
"role.childrenToggle": "展开/收起本会话的子代理",
|
|
426
|
+
"role.switchTitle": "切换到该子代理的概览",
|
|
427
|
+
"role.openParent": "查看主会话",
|
|
428
|
+
"role.openParentTitle": "切换到父会话的概览",
|
|
429
|
+
"role.parentPrefix": "·",
|
|
362
430
|
"state.noSession": "当前没有打开的会话",
|
|
363
431
|
"state.error": "概览渲染失败",
|
|
364
432
|
};
|
|
@@ -375,6 +443,10 @@ window.__ModuleLoader__.load({
|
|
|
375
443
|
"progress.emptyTurn": "No task plan was created in this turn",
|
|
376
444
|
"progress.recentSteps": "Key steps in this turn (latest 6)",
|
|
377
445
|
"step.compact": "Context compaction",
|
|
446
|
+
"step.approvalApproved": "Approved",
|
|
447
|
+
"step.approvalPending": "Awaiting approval",
|
|
448
|
+
"step.approvalGated": "Approval",
|
|
449
|
+
"step.approvalDenied": "Denied",
|
|
378
450
|
"step.approval": "User approval",
|
|
379
451
|
"progress.allSteps": "Key steps in this turn (all)",
|
|
380
452
|
"progress.showEarlierSteps": "Show earlier steps",
|
|
@@ -430,13 +502,22 @@ window.__ModuleLoader__.load({
|
|
|
430
502
|
"settings.quickNavRow": "Conversation turn rail",
|
|
431
503
|
"settings.quickNavLinkRow": "Link rail with overview",
|
|
432
504
|
"settings.quickNavNote": "The turn rail on the right edge of the chat: hover for a turn preview, click to jump. When enabled it stays visible in narrow windows too, and ticks show their turn number on hover.",
|
|
505
|
+
"markdown.copy": "Copy",
|
|
506
|
+
"markdown.copied": "Copied",
|
|
507
|
+
"markdown.code": "Code",
|
|
508
|
+
"markdown.wrap": "Wrap",
|
|
509
|
+
"markdown.unwrap": "No wrap",
|
|
510
|
+
"markdown.footnotes": "Footnotes",
|
|
433
511
|
"settings.quickNavLinkNote": "When you click a rail tick to jump, an open overview switches to that turn as well.",
|
|
434
512
|
"settings.interaction": "Interaction",
|
|
435
|
-
"settings.todoPromptRow": "Double-click todo
|
|
436
|
-
"settings.
|
|
437
|
-
"settings.
|
|
438
|
-
"settings.
|
|
513
|
+
"settings.todoPromptRow": "Double-click todo",
|
|
514
|
+
"settings.todoPromptOn": "Prompt",
|
|
515
|
+
"settings.todoPromptOff": "Off",
|
|
516
|
+
"settings.todoPromptHint": "Show the built-in default template",
|
|
517
|
+
"settings.todoPromptDefault": "Built-in default template",
|
|
518
|
+
"settings.todoPromptPlaceholder": "Double-click a todo row in Progress to fill the rerun prompt into the composer.\nLeave empty for the built-in prompt; placeholders: {index} (step number), {content} (step text).",
|
|
439
519
|
"settings.placementRow": "Show the overview in",
|
|
520
|
+
"settings.placementNote": "Below the conversation = one overview card inside the chat; Right sidebar = its own page in the right sidebar; Both = shown in both places with the same content.",
|
|
440
521
|
"settings.viking": "OpenViking memory preview",
|
|
441
522
|
"settings.vikingEnable": "Enable memory preview",
|
|
442
523
|
"settings.vikingUrl": "Server URL",
|
|
@@ -455,9 +536,11 @@ window.__ModuleLoader__.load({
|
|
|
455
536
|
"viking.field.keywords": "Keywords",
|
|
456
537
|
"viking.field.source": "Source",
|
|
457
538
|
"reference.sort.time": "Time",
|
|
458
|
-
"reference.sort.count": "
|
|
539
|
+
"reference.sort.count": "Turns",
|
|
540
|
+
"reference.sort.hits": "Count",
|
|
459
541
|
"reference.sort.time.title": "Sort by most recently referenced",
|
|
460
|
-
"reference.sort.count.title": "Sort by
|
|
542
|
+
"reference.sort.count.title": "Sort by how many turns mention it",
|
|
543
|
+
"reference.sort.hits.title": "Sort by mentions in this turn",
|
|
461
544
|
"reference.sort.cancel": "Click again to clear",
|
|
462
545
|
"reference.previewHint": "Click to preview the injected content snapshot",
|
|
463
546
|
"scope.label": "Scope",
|
|
@@ -468,9 +551,25 @@ window.__ModuleLoader__.load({
|
|
|
468
551
|
"viking.revealHint": "Open directory",
|
|
469
552
|
"viking.revealFailed": "Could not open the directory",
|
|
470
553
|
"viking.revealRestart": "Host route not ready — restart DSH and try again",
|
|
554
|
+
"artifact.conclusion": "Conclusion",
|
|
555
|
+
"artifact.conclusionTurn": "Turn {n} conclusion",
|
|
556
|
+
"artifact.conclusionChars": "{n} chars",
|
|
557
|
+
"artifact.conclusionHint": "This turn's closing answer as a document — click to open in a tab",
|
|
558
|
+
"context.subtitleConclusion": "Turn conclusion · Markdown document",
|
|
559
|
+
"settings.conclusionTabRow": "Open the conclusion when a turn ends",
|
|
560
|
+
"settings.conclusionTabOn": "New tab",
|
|
561
|
+
"settings.conclusionTabOff": "Off",
|
|
562
|
+
"settings.conclusionTabNote": "When a turn finishes, its closing answer opens as a Markdown tab; it also stays one click away in Artifacts.",
|
|
563
|
+
"settings.conclusionTabOffNote": "No tab opens. The turn conclusion still appears in Artifacts, one click away.",
|
|
564
|
+
"viking.eventMemory": "Memory",
|
|
565
|
+
"viking.prefMemory": "Preference",
|
|
566
|
+
"viking.entityMemory": "Entity",
|
|
567
|
+
"viking.sessionMemory": "Session memory",
|
|
568
|
+
"viking.genericMemory": "Memory",
|
|
471
569
|
"viking.revealed": "Opened in the system",
|
|
472
570
|
"context.tab": "Context preview",
|
|
473
571
|
"context.subtitle": "Injected content snapshot",
|
|
572
|
+
"context.subtitlePlan": "Plan snapshot",
|
|
474
573
|
"context.unavailable": "Content snapshot unavailable (return to the session to reload after a page refresh)",
|
|
475
574
|
"skill.revealTitle": "Open the directory of skill {name} in the system",
|
|
476
575
|
"skill.revealFailed": "Could not open the skill directory",
|
|
@@ -511,6 +610,7 @@ window.__ModuleLoader__.load({
|
|
|
511
610
|
"goal.blocked": "Blocked",
|
|
512
611
|
"goal.complete": "Completed",
|
|
513
612
|
"artifact.presented": "Delivered",
|
|
613
|
+
"artifact.plan": "Plan",
|
|
514
614
|
"artifact.memory": "Memory",
|
|
515
615
|
"artifact.menu.reveal": "Show in Enclosing Folder",
|
|
516
616
|
"artifact.menu.openWith": "Open With",
|
|
@@ -538,8 +638,32 @@ window.__ModuleLoader__.load({
|
|
|
538
638
|
"reference.producer.agentInstructions": "Agent instructions",
|
|
539
639
|
"reference.more": "+{n} more",
|
|
540
640
|
"reference.times": "×{n}",
|
|
641
|
+
"reference.turns": "{n} turns",
|
|
642
|
+
"reference.turnsTitle": "Seen in {turns} turns · {times} mentions",
|
|
643
|
+
"reference.timesTitle": "{times} mentions in this turn",
|
|
541
644
|
"action.openInSidebar": "Open in sidebar",
|
|
542
645
|
"state.loadingHistory": "Loading earlier history…",
|
|
646
|
+
"state.reloadHistory": "Reload",
|
|
647
|
+
"state.loadingStalled": "Earlier history could not be loaded",
|
|
648
|
+
"state.reloadHistory.title": "Restart loading earlier history",
|
|
649
|
+
"intro.newTurn": "Turn {n} started",
|
|
650
|
+
"progress.stepCommand": "Command / args",
|
|
651
|
+
"progress.stepResult": "Output",
|
|
652
|
+
"progress.stepError": "Error",
|
|
653
|
+
"progress.stepDuration": "took {v}",
|
|
654
|
+
"progress.stepChars": "{n} chars",
|
|
655
|
+
"progress.stepTruncated": "long output, showing the head",
|
|
656
|
+
"progress.stepSkipped": "output too large, not kept",
|
|
657
|
+
"progress.expandStep": "Show command and output",
|
|
658
|
+
"progress.collapseStep": "Hide command and output",
|
|
659
|
+
"role.main": "Main agent",
|
|
660
|
+
"role.subagent": "Subagent",
|
|
661
|
+
"role.children": "Subagents {n}",
|
|
662
|
+
"role.childrenToggle": "Show/hide this session's subagents",
|
|
663
|
+
"role.switchTitle": "Switch to this subagent's overview",
|
|
664
|
+
"role.openParent": "Open main session",
|
|
665
|
+
"role.openParentTitle": "Switch to the parent session's overview",
|
|
666
|
+
"role.parentPrefix": "·",
|
|
543
667
|
"state.noSession": "No session is open",
|
|
544
668
|
"state.error": "Overview failed to render",
|
|
545
669
|
};
|
|
@@ -577,6 +701,7 @@ window.__ModuleLoader__.load({
|
|
|
577
701
|
quickNav: typeof source.quickNav === "boolean" ? source.quickNav : true,
|
|
578
702
|
quickNavLink: typeof source.quickNavLink === "boolean" ? source.quickNavLink : true,
|
|
579
703
|
vikingPreview: typeof source.vikingPreview === "boolean" ? source.vikingPreview : true,
|
|
704
|
+
conclusionTab: typeof source.conclusionTab === "boolean" ? source.conclusionTab : true,
|
|
580
705
|
vikingUrl: text(source.vikingUrl, VIKING_DEFAULTS.vikingUrl).replace(/\/+$/, ""),
|
|
581
706
|
vikingAccount: text(source.vikingAccount, VIKING_DEFAULTS.vikingAccount),
|
|
582
707
|
vikingUser: text(source.vikingUser, VIKING_DEFAULTS.vikingUser),
|
|
@@ -1068,9 +1193,35 @@ window.__ModuleLoader__.load({
|
|
|
1068
1193
|
return match !== null ? match[1] : null;
|
|
1069
1194
|
}
|
|
1070
1195
|
|
|
1196
|
+
/** Which memory tree a viking:// URI lives in; a locale key for its label prefix. */
|
|
1197
|
+
function vikingMemoryBucket(uri) {
|
|
1198
|
+
const value = String(uri);
|
|
1199
|
+
if (/\/memories\/preferences(\/|$)/i.test(value)) return "viking.prefMemory";
|
|
1200
|
+
if (/\/memories\/entities(\/|$)/i.test(value)) return "viking.entityMemory";
|
|
1201
|
+
if (/\/memories\/sessions(\/|$)/i.test(value) || /^viking:\/\/(?:user\/[^/]+|agent)\/sessions(\/|$)/i.test(value)) return "viking.sessionMemory";
|
|
1202
|
+
if (/\/memories\/events(\/|$)/i.test(value)) return "viking.eventMemory";
|
|
1203
|
+
if (/\/memories(\/|$)/i.test(value)) return "viking.genericMemory";
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/** Display name for a viking:// URI, read from the memory tree it lives in: a dated
|
|
1208
|
+
folder must not show up as its last segment ("29"). */
|
|
1071
1209
|
function vikingLabel(uri) {
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1210
|
+
const value = String(uri).replace(/\/+$/, "");
|
|
1211
|
+
const skill = vikingSkillName(value);
|
|
1212
|
+
if (skill !== null) return skill;
|
|
1213
|
+
const dated = /\/memories\/events\/(\d{4})\/(\d{2})\/(\d{2})(\/.+)?$/i.exec(value);
|
|
1214
|
+
if (dated !== null) {
|
|
1215
|
+
const day = `${dated[1]}-${dated[2]}-${dated[3]}`;
|
|
1216
|
+
const tail = typeof dated[4] === "string" ? vikingName(dated[4]) : "";
|
|
1217
|
+
if (tail !== "" && tail !== day) return `${tail} · ${day}`;
|
|
1218
|
+
return `${translate("viking.eventMemory")} · ${day}`;
|
|
1219
|
+
}
|
|
1220
|
+
const bucket = vikingMemoryBucket(value);
|
|
1221
|
+
const name = vikingName(value);
|
|
1222
|
+
if (bucket === null) return name;
|
|
1223
|
+
/* entities keep a category folder above the leaf; show only the leaf, prefixed */
|
|
1224
|
+
return `${translate(bucket)} · ${name}`;
|
|
1074
1225
|
}
|
|
1075
1226
|
|
|
1076
1227
|
/** Ref kind: anything under a skills/ root reads as SKILL, the rest stays a memory file. */
|
|
@@ -1261,6 +1412,7 @@ window.__ModuleLoader__.load({
|
|
|
1261
1412
|
const lines = escapeHtml(markdown).split("\n");
|
|
1262
1413
|
const out = [];
|
|
1263
1414
|
let fence = null;
|
|
1415
|
+
let fenceLang = "";
|
|
1264
1416
|
let list = null; // "ul" | "ol"
|
|
1265
1417
|
let paragraph = [];
|
|
1266
1418
|
const closeList = () => {
|
|
@@ -1279,8 +1431,15 @@ window.__ModuleLoader__.load({
|
|
|
1279
1431
|
const trimmed = line.trim();
|
|
1280
1432
|
if (fence !== null) {
|
|
1281
1433
|
if (trimmed.startsWith("```")) {
|
|
1282
|
-
|
|
1434
|
+
const body = fence.join("\n");
|
|
1435
|
+
if (fenceLang === "mermaid") {
|
|
1436
|
+
/* the inner <pre> is the fallback view until mermaid replaces it */
|
|
1437
|
+
out.push(`<div class="dsov-mermaid" data-src="${body}"><pre class="dsov-md-pre"><code>${body}</code></pre></div>`);
|
|
1438
|
+
} else {
|
|
1439
|
+
out.push(`<pre class="dsov-md-pre"><code>${body}</code></pre>`);
|
|
1440
|
+
}
|
|
1283
1441
|
fence = null;
|
|
1442
|
+
fenceLang = "";
|
|
1284
1443
|
} else {
|
|
1285
1444
|
fence.push(line);
|
|
1286
1445
|
}
|
|
@@ -1290,6 +1449,7 @@ window.__ModuleLoader__.load({
|
|
|
1290
1449
|
flushParagraph();
|
|
1291
1450
|
closeList();
|
|
1292
1451
|
fence = [];
|
|
1452
|
+
fenceLang = trimmed.slice(3).trim().toLowerCase();
|
|
1293
1453
|
continue;
|
|
1294
1454
|
}
|
|
1295
1455
|
if (trimmed === "") {
|
|
@@ -1348,7 +1508,82 @@ window.__ModuleLoader__.load({
|
|
|
1348
1508
|
return root === "" ? normalized : `${root}/${normalized}`;
|
|
1349
1509
|
}
|
|
1350
1510
|
|
|
1511
|
+
/** Open the host's own plan viewer — the exact page the turn-tail plan banner
|
|
1512
|
+
opens. The plan plugin re-resolves the markdown from the session log by
|
|
1513
|
+
tool-call id, so this address stays valid across reloads. */
|
|
1514
|
+
function openNativePlan(sessionId, callId) {
|
|
1515
|
+
if (typeof sessionId !== "string" || sessionId === "" || typeof callId !== "string" || callId === "") return false;
|
|
1516
|
+
const sidebar = runtime && runtime.ctx ? runtime.ctx.sidebarRight : undefined;
|
|
1517
|
+
if (sidebar === undefined || typeof sidebar.openResource !== "function") return false;
|
|
1518
|
+
let segments;
|
|
1519
|
+
try {
|
|
1520
|
+
const sessions = runtime.ctx.sessions;
|
|
1521
|
+
const child = sessions !== undefined && typeof sessions.subagentAddress === "function" ? sessions.subagentAddress(sessionId) : undefined;
|
|
1522
|
+
segments =
|
|
1523
|
+
child === undefined
|
|
1524
|
+
? [sessionId, callId]
|
|
1525
|
+
: ["subagent", child.parentSessionId, child.childSessionId, child.mode, callId];
|
|
1526
|
+
} catch {
|
|
1527
|
+
segments = [sessionId, callId]; /* uninjected ctx access throws; plain sessions are the common case */
|
|
1528
|
+
}
|
|
1529
|
+
try {
|
|
1530
|
+
sidebar.openResource(`dsh-resource://plan/${segments.map(encodeURIComponent).join("/")}`);
|
|
1531
|
+
return true;
|
|
1532
|
+
} catch {
|
|
1533
|
+
return false;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
/* The plan plugin's preview page is also the product's read-only Markdown
|
|
1538
|
+
document page: besides logged plans it serves a temporary address whose
|
|
1539
|
+
body renders caller-supplied Markdown through the harness' own MarkdownText
|
|
1540
|
+
and chrome — the very page a turn-tail plan banner's 「打开」 reveals. Sending
|
|
1541
|
+
our documents there makes 「点击打开」 the same page instead of a lookalike. */
|
|
1542
|
+
const DOCUMENT_ADDRESS_PREFIX = "dsh-resource://plan-review/";
|
|
1543
|
+
|
|
1544
|
+
/** Address of the host document page for one caller-supplied Markdown snapshot. */
|
|
1545
|
+
function documentAddress(sessionId, docKey) {
|
|
1546
|
+
if (typeof sessionId !== "string" || sessionId === "" || typeof docKey !== "string" || docKey === "") return undefined;
|
|
1547
|
+
return `${DOCUMENT_ADDRESS_PREFIX}${encodeURIComponent(sessionId)}/${encodeURIComponent(docKey)}`;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/** Open a Markdown snapshot on the host's own document page; false when unavailable. */
|
|
1551
|
+
function openNativeDocument(sessionId, docKey, title, markdown) {
|
|
1552
|
+
const address = documentAddress(sessionId, docKey);
|
|
1553
|
+
if (address === undefined || typeof markdown !== "string" || markdown === "") return false;
|
|
1554
|
+
const sidebar = contextSidebar();
|
|
1555
|
+
if (sidebar === null) return false;
|
|
1556
|
+
try {
|
|
1557
|
+
sidebar.openResource(address, { params: { planReview: { markdown, title: typeof title === "string" && title !== "" ? title : docKey } } });
|
|
1558
|
+
return true;
|
|
1559
|
+
} catch (_unavailable) {
|
|
1560
|
+
/* no tab type claims the address (plan plugin disabled): caller falls back */
|
|
1561
|
+
return false;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1351
1565
|
function openArtifact(binding, artifact, stats) {
|
|
1566
|
+
const sessionId = binding ? binding.sessionId : undefined;
|
|
1567
|
+
if (artifact.kind === "conclusion") {
|
|
1568
|
+
/* The plugin's own tab is restorable: its body is mirrored into localStorage,
|
|
1569
|
+
while the host's plan-review page keeps its payload in tab params only and
|
|
1570
|
+
turns into the expired "计划" page after a reload. */
|
|
1571
|
+
if (typeof artifact.previewKey === "string" && artifact.previewKey !== "" && openContextPreview(artifact.previewKey)) return;
|
|
1572
|
+
const entry = typeof artifact.previewKey === "string" ? readContextPreview(artifact.previewKey) : null;
|
|
1573
|
+
const docKey = `conclusion:${typeof artifact.turn === "number" ? artifact.turn : artifact.previewKey}`;
|
|
1574
|
+
openNativeDocument(sessionId, docKey, artifact.title, entry === null ? undefined : entry.text);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
if (artifact.plan === true) {
|
|
1578
|
+
if (openNativePlan(sessionId, artifact.planCallId)) return;
|
|
1579
|
+
/* host plan viewer unavailable (plugin disabled, no sidebar): the document
|
|
1580
|
+
page over the same snapshot, then the plugin's own tab */
|
|
1581
|
+
if (typeof artifact.previewKey === "string" && artifact.previewKey !== "" && openContextPreview(artifact.previewKey)) return;
|
|
1582
|
+
const entry = typeof artifact.previewKey === "string" ? readContextPreview(artifact.previewKey) : null;
|
|
1583
|
+
const docKey = `plan:${typeof artifact.planCallId === "string" && artifact.planCallId !== "" ? artifact.planCallId : artifact.previewKey}`;
|
|
1584
|
+
openNativeDocument(sessionId, docKey, entry !== null ? entry.title : artifact.planTitle, entry === null ? undefined : entry.text);
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1352
1587
|
if (typeof artifact.path === "string" && artifact.path.startsWith("viking://")) {
|
|
1353
1588
|
openVikingPreview(artifact.path);
|
|
1354
1589
|
return;
|
|
@@ -1524,6 +1759,7 @@ window.__ModuleLoader__.load({
|
|
|
1524
1759
|
teamTasks: new Map(),
|
|
1525
1760
|
subagents: new Map(),
|
|
1526
1761
|
stats: createStats(),
|
|
1762
|
+
conclusion: null,
|
|
1527
1763
|
changed: false,
|
|
1528
1764
|
};
|
|
1529
1765
|
}
|
|
@@ -1535,7 +1771,7 @@ window.__ModuleLoader__.load({
|
|
|
1535
1771
|
}
|
|
1536
1772
|
|
|
1537
1773
|
function createTurnBucket() {
|
|
1538
|
-
return { artifacts: new Map(), refs: new Map(), refOrder: [], changes: [], steps: [], anchors: [], members: new Map(), teamTasks: new Map(), subagents: new Map(), stats: createStats() };
|
|
1774
|
+
return { artifacts: new Map(), refs: new Map(), refOrder: [], changes: [], steps: [], anchors: [], members: new Map(), teamTasks: new Map(), subagents: new Map(), stats: createStats(), conclusion: null, lastReply: null, ended: false };
|
|
1539
1775
|
}
|
|
1540
1776
|
|
|
1541
1777
|
const EMPTY_BUCKET = Object.freeze({ artifacts: new Map(), refs: new Map(), refOrder: [], changes: [], steps: [], anchors: [], members: new Map(), teamTasks: new Map(), subagents: new Map(), stats: Object.freeze(createStats()) });
|
|
@@ -1554,10 +1790,77 @@ window.__ModuleLoader__.load({
|
|
|
1554
1790
|
const CONTEXT_PREVIEW_LIMIT = 400;
|
|
1555
1791
|
const CONTEXT_TEXT_LIMIT = 120000;
|
|
1556
1792
|
|
|
1557
|
-
|
|
1793
|
+
/* Mermaid is loaded lazily from a CDN the first time a preview contains a
|
|
1794
|
+
diagram; if the load fails (offline / blocked) the raw source <pre>
|
|
1795
|
+
fallback inside every .dsov-mermaid block simply stays visible. */
|
|
1796
|
+
let mermaidPromise = null;
|
|
1797
|
+
let mermaidSeq = 0;
|
|
1798
|
+
function loadMermaid() {
|
|
1799
|
+
if (mermaidPromise === null) {
|
|
1800
|
+
mermaidPromise = import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs")
|
|
1801
|
+
.then((mod) => {
|
|
1802
|
+
const mermaid = mod !== null && typeof mod === "object" && typeof mod.default === "object" ? mod.default : mod;
|
|
1803
|
+
const dark = typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches === true;
|
|
1804
|
+
mermaid.initialize({ startOnLoad: false, theme: dark === true ? "dark" : "default", fontFamily: "inherit" });
|
|
1805
|
+
return mermaid;
|
|
1806
|
+
})
|
|
1807
|
+
.catch((error) => {
|
|
1808
|
+
mermaidPromise = null;
|
|
1809
|
+
throw error;
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
return mermaidPromise;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
async function renderMermaidNodes(root) {
|
|
1816
|
+
if (root === null || typeof root.querySelectorAll !== "function") return;
|
|
1817
|
+
const nodes = root.querySelectorAll(".dsov-mermaid[data-src]");
|
|
1818
|
+
if (nodes.length === 0) return;
|
|
1819
|
+
let mermaid;
|
|
1820
|
+
try {
|
|
1821
|
+
mermaid = await loadMermaid();
|
|
1822
|
+
} catch {
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
for (const node of nodes) {
|
|
1826
|
+
if (node.getAttribute("data-rendered") === "true") continue;
|
|
1827
|
+
const source = node.getAttribute("data-src") || "";
|
|
1828
|
+
if (source.trim() === "") continue;
|
|
1829
|
+
try {
|
|
1830
|
+
mermaidSeq += 1;
|
|
1831
|
+
const result = await mermaid.render(`dsov-mmd-${mermaidSeq}`, source);
|
|
1832
|
+
const svg = result !== null && typeof result === "object" && typeof result.svg === "string" ? result.svg : "";
|
|
1833
|
+
if (svg === "") continue;
|
|
1834
|
+
node.innerHTML = svg;
|
|
1835
|
+
node.setAttribute("data-rendered", "true");
|
|
1836
|
+
} catch {
|
|
1837
|
+
node.classList.add("dsov-mermaidFailed");
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
/** Markdown block that upgrades ```mermaid fences to rendered diagrams after mount. */
|
|
1843
|
+
function MarkdownBody({ text }) {
|
|
1844
|
+
const ref = React.useRef(null);
|
|
1845
|
+
const html = React.useMemo(() => mdToHtml(typeof text === "string" ? text : ""), [text]);
|
|
1846
|
+
React.useEffect(() => {
|
|
1847
|
+
const node = ref.current;
|
|
1848
|
+
if (node === null) return undefined;
|
|
1849
|
+
const timer = setTimeout(() => renderMermaidNodes(node), 0);
|
|
1850
|
+
return () => clearTimeout(timer);
|
|
1851
|
+
}, [html]);
|
|
1852
|
+
return h("div", { className: "dsov-md", ref, dangerouslySetInnerHTML: { __html: html } });
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
function rememberContextPreview(sessionId, refKey, title, text, kind) {
|
|
1558
1856
|
if (typeof sessionId !== "string" || sessionId === "" || typeof text !== "string" || text === "") return undefined;
|
|
1559
1857
|
const globalKey = `${sessionId}|${refKey}`;
|
|
1560
|
-
|
|
1858
|
+
const entry = { title: typeof title === "string" && title !== "" ? title : refKey, text: text.length > CONTEXT_TEXT_LIMIT ? `${text.slice(0, CONTEXT_TEXT_LIMIT)}\n…` : text, ...(typeof kind === "string" && kind !== "" ? { kind } : {}) };
|
|
1859
|
+
contextPreviews.set(globalKey, entry);
|
|
1860
|
+
/* Documents (turn conclusion, plan snapshot) open as tabs the host restores
|
|
1861
|
+
after a reload, and their body lives only here — mirror them into
|
|
1862
|
+
localStorage so a refreshed tab does not render the "unavailable" state. */
|
|
1863
|
+
if (entry.kind === "conclusion" || entry.kind === "plan") persistDocPreview(globalKey, entry);
|
|
1561
1864
|
while (contextPreviews.size > CONTEXT_PREVIEW_LIMIT) {
|
|
1562
1865
|
const oldest = contextPreviews.keys().next();
|
|
1563
1866
|
if (oldest.done === true) break;
|
|
@@ -1566,9 +1869,43 @@ window.__ModuleLoader__.load({
|
|
|
1566
1869
|
return globalKey;
|
|
1567
1870
|
}
|
|
1568
1871
|
|
|
1872
|
+
const DOC_PREVIEW_STORE_KEY = "dsh.conversationOverview.docPreviews";
|
|
1873
|
+
const DOC_PREVIEW_STORE_LIMIT = 8;
|
|
1874
|
+
const DOC_PREVIEW_TEXT_LIMIT = 20000;
|
|
1875
|
+
|
|
1876
|
+
function readDocPreviewStore() {
|
|
1877
|
+
try {
|
|
1878
|
+
const raw = window.localStorage.getItem(DOC_PREVIEW_STORE_KEY);
|
|
1879
|
+
const parsed = typeof raw === "string" ? JSON.parse(raw) : null;
|
|
1880
|
+
if (!Array.isArray(parsed)) return [];
|
|
1881
|
+
return parsed.filter((item) => Array.isArray(item) && typeof item[0] === "string" && isRecord(item[1]));
|
|
1882
|
+
} catch {
|
|
1883
|
+
return [];
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
function persistDocPreview(globalKey, entry) {
|
|
1888
|
+
try {
|
|
1889
|
+
const kept = readDocPreviewStore().filter((item) => item[0] !== globalKey);
|
|
1890
|
+
const text = entry.text.length > DOC_PREVIEW_TEXT_LIMIT ? `${entry.text.slice(0, DOC_PREVIEW_TEXT_LIMIT)}\n…` : entry.text;
|
|
1891
|
+
kept.push([globalKey, { title: entry.title, text, kind: entry.kind }]);
|
|
1892
|
+
window.localStorage.setItem(DOC_PREVIEW_STORE_KEY, JSON.stringify(kept.slice(-DOC_PREVIEW_STORE_LIMIT)));
|
|
1893
|
+
} catch {
|
|
1894
|
+
/* storage full or unavailable: the in-memory copy still serves this page */
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1569
1898
|
function readContextPreview(globalKey) {
|
|
1570
1899
|
const entry = contextPreviews.get(globalKey);
|
|
1571
|
-
|
|
1900
|
+
if (entry !== undefined) return entry;
|
|
1901
|
+
/* a tab restored after a reload finds its body here */
|
|
1902
|
+
const stored = readDocPreviewStore().find((item) => item[0] === globalKey);
|
|
1903
|
+
if (stored === undefined) return null;
|
|
1904
|
+
const revived = isRecord(stored[1]) && typeof stored[1].text === "string" ? stored[1] : null;
|
|
1905
|
+
if (revived === null || revived.text === "") return null;
|
|
1906
|
+
const restored = { title: typeof revived.title === "string" && revived.title !== "" ? revived.title : globalKey, text: revived.text, ...(typeof revived.kind === "string" && revived.kind !== "" ? { kind: revived.kind } : {}) };
|
|
1907
|
+
contextPreviews.set(globalKey, restored);
|
|
1908
|
+
return restored;
|
|
1572
1909
|
}
|
|
1573
1910
|
|
|
1574
1911
|
const CONTEXT_ADDRESS_PREFIX = "dsh-resource://overview-context/";
|
|
@@ -1587,10 +1924,58 @@ window.__ModuleLoader__.load({
|
|
|
1587
1924
|
}
|
|
1588
1925
|
}
|
|
1589
1926
|
|
|
1927
|
+
/** The host surface that holds preview tabs; null when this context has none. */
|
|
1928
|
+
function contextSidebar() {
|
|
1929
|
+
try {
|
|
1930
|
+
const sidebar = runtime && runtime.ctx ? runtime.ctx.sidebarRight : undefined;
|
|
1931
|
+
return sidebar && typeof sidebar.openResource === "function" ? sidebar : null;
|
|
1932
|
+
} catch {
|
|
1933
|
+
return null;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1590
1937
|
function openContextPreview(globalKey) {
|
|
1591
|
-
const sidebar =
|
|
1592
|
-
if (
|
|
1593
|
-
|
|
1938
|
+
const sidebar = contextSidebar();
|
|
1939
|
+
if (sidebar === null) return false;
|
|
1940
|
+
try {
|
|
1941
|
+
sidebar.openResource(contextPreviewAddress(globalKey));
|
|
1942
|
+
return true;
|
|
1943
|
+
} catch (_unavailable) {
|
|
1944
|
+
/* no tab type claims our address (plugin half not mounted): caller falls back */
|
|
1945
|
+
return false;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
/* An overview can be mounted twice (inline card + sidebar tab); only the first
|
|
1950
|
+
instance may pop the turn-conclusion tab. */
|
|
1951
|
+
const conclusionAutoOpened = new Set();
|
|
1952
|
+
|
|
1953
|
+
function claimConclusionTab(key) {
|
|
1954
|
+
if (conclusionAutoOpened.has(key)) return false;
|
|
1955
|
+
if (conclusionAutoOpened.size > 200) conclusionAutoOpened.clear();
|
|
1956
|
+
conclusionAutoOpened.add(key);
|
|
1957
|
+
return true;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
/* Auto-open decision for the turn-conclusion tab. Guard shape:
|
|
1961
|
+
{ ready, opened:Set, sawRunning }. A conclusion only earns a tab when this
|
|
1962
|
+
overview watched the session run at some point after mounting — history that
|
|
1963
|
+
is replayed on mount/refresh (even when it arrives in several flushes, after
|
|
1964
|
+
the first paint) must never pop a tab. Returns the key to open, else null. */
|
|
1965
|
+
function conclusionAutoOpenKey(guard, conclusion, sessionId, sawRunning) {
|
|
1966
|
+
if (!isRecord(guard) || !(guard.opened instanceof Set)) return null;
|
|
1967
|
+
const record = isRecord(conclusion) ? conclusion : null;
|
|
1968
|
+
const key = `${sessionId}|${record === null ? "" : record.turn}`;
|
|
1969
|
+
if (guard.ready !== true) {
|
|
1970
|
+
/* first paint: whatever already exists is history, never re-opened */
|
|
1971
|
+
guard.ready = true;
|
|
1972
|
+
if (record !== null) guard.opened.add(key);
|
|
1973
|
+
return null;
|
|
1974
|
+
}
|
|
1975
|
+
if (record === null || record.ended !== true || guard.opened.has(key)) return null;
|
|
1976
|
+
guard.opened.add(key);
|
|
1977
|
+
if (sawRunning !== true) return null;
|
|
1978
|
+
return key;
|
|
1594
1979
|
}
|
|
1595
1980
|
|
|
1596
1981
|
/* Quick-nav linkage: a click on the built-in turn rail broadcasts a scope request
|
|
@@ -1721,16 +2106,51 @@ window.__ModuleLoader__.load({
|
|
|
1721
2106
|
return React.useSyncExternalStore(subscribe, read);
|
|
1722
2107
|
}
|
|
1723
2108
|
|
|
2109
|
+
/** True while the session is actively producing a turn (running flag of sessionStatus). */
|
|
2110
|
+
function useSessionRunning(sessionId) {
|
|
2111
|
+
const source = readSessionStatusSource();
|
|
2112
|
+
const subscribe = React.useCallback(
|
|
2113
|
+
(onChange) => (source !== undefined && typeof source.subscribe === "function" ? source.subscribe(onChange) : () => {}),
|
|
2114
|
+
[source]
|
|
2115
|
+
);
|
|
2116
|
+
const read = React.useCallback(() => {
|
|
2117
|
+
if (source === undefined || typeof source.getSnapshot !== "function" || typeof sessionId !== "string") return false;
|
|
2118
|
+
try {
|
|
2119
|
+
const snapshot = source.getSnapshot();
|
|
2120
|
+
const status = snapshot !== null && snapshot !== undefined && typeof snapshot.get === "function" ? snapshot.get(sessionId) : undefined;
|
|
2121
|
+
return status !== null && status !== undefined && status.running === true;
|
|
2122
|
+
} catch {
|
|
2123
|
+
return false;
|
|
2124
|
+
}
|
|
2125
|
+
}, [source, sessionId]);
|
|
2126
|
+
return React.useSyncExternalStore(subscribe, read);
|
|
2127
|
+
}
|
|
2128
|
+
|
|
1724
2129
|
function touch(folder) {
|
|
1725
2130
|
folder.changed = true;
|
|
1726
2131
|
}
|
|
1727
2132
|
|
|
2133
|
+
/** First heading (or first non-empty line) of a plan markdown, trimmed for row display. */
|
|
2134
|
+
function planTitleOf(text) {
|
|
2135
|
+
for (const line of String(text).split("\n")) {
|
|
2136
|
+
const trimmed = line.trim();
|
|
2137
|
+
if (trimmed === "") continue;
|
|
2138
|
+
const heading = /^#{1,6}\s+(.*)$/.exec(trimmed);
|
|
2139
|
+
return (heading ? heading[1] : trimmed).replace(/[#*`]/g, "").trim().slice(0, 80);
|
|
2140
|
+
}
|
|
2141
|
+
return "";
|
|
2142
|
+
}
|
|
2143
|
+
|
|
1728
2144
|
function upsertArtifact(folder, bucket, path, update) {
|
|
1729
2145
|
const existing = folder.artifacts.get(path);
|
|
1730
2146
|
if (existing) {
|
|
1731
2147
|
if (typeof update.description === "string" && update.description.length > 0) existing.description = update.description;
|
|
1732
2148
|
if (update.presented) existing.presented = true;
|
|
1733
2149
|
if (update.produced) existing.produced = true;
|
|
2150
|
+
if (update.plan === true) existing.plan = true;
|
|
2151
|
+
if (typeof update.planTitle === "string" && update.planTitle !== "") existing.planTitle = update.planTitle;
|
|
2152
|
+
if (typeof update.previewKey === "string" && update.previewKey !== "") existing.previewKey = update.previewKey;
|
|
2153
|
+
if (typeof update.planCallId === "string" && update.planCallId !== "") existing.planCallId = update.planCallId;
|
|
1734
2154
|
if (typeof update.time === "number" && update.time > 0) existing.time = update.time;
|
|
1735
2155
|
if (typeof update.seq === "number") existing.seq = Math.max(existing.seq, update.seq);
|
|
1736
2156
|
const bucketExisting = bucket.artifacts.get(path);
|
|
@@ -1738,6 +2158,10 @@ window.__ModuleLoader__.load({
|
|
|
1738
2158
|
if (typeof update.description === "string" && update.description.length > 0) bucketExisting.description = update.description;
|
|
1739
2159
|
if (update.presented) bucketExisting.presented = true;
|
|
1740
2160
|
if (update.produced) bucketExisting.produced = true;
|
|
2161
|
+
if (update.plan === true) bucketExisting.plan = true;
|
|
2162
|
+
if (typeof update.planTitle === "string" && update.planTitle !== "") bucketExisting.planTitle = update.planTitle;
|
|
2163
|
+
if (typeof update.previewKey === "string" && update.previewKey !== "") bucketExisting.previewKey = update.previewKey;
|
|
2164
|
+
if (typeof update.planCallId === "string" && update.planCallId !== "") bucketExisting.planCallId = update.planCallId;
|
|
1741
2165
|
if (typeof update.time === "number" && update.time > 0) bucketExisting.time = update.time;
|
|
1742
2166
|
if (typeof update.seq === "number") bucketExisting.seq = Math.max(bucketExisting.seq, update.seq);
|
|
1743
2167
|
} else {
|
|
@@ -1751,6 +2175,10 @@ window.__ModuleLoader__.load({
|
|
|
1751
2175
|
description: typeof update.description === "string" ? update.description : undefined,
|
|
1752
2176
|
presented: update.presented === true,
|
|
1753
2177
|
produced: update.produced === true,
|
|
2178
|
+
...(update.plan === true ? { plan: true } : {}),
|
|
2179
|
+
...(typeof update.planTitle === "string" && update.planTitle !== "" ? { planTitle: update.planTitle } : {}),
|
|
2180
|
+
...(typeof update.previewKey === "string" && update.previewKey !== "" ? { previewKey: update.previewKey } : {}),
|
|
2181
|
+
...(typeof update.planCallId === "string" && update.planCallId !== "" ? { planCallId: update.planCallId } : {}),
|
|
1754
2182
|
time: typeof update.time === "number" ? update.time : 0,
|
|
1755
2183
|
firstTime: typeof update.time === "number" ? update.time : 0,
|
|
1756
2184
|
seq: typeof update.seq === "number" ? update.seq : 0,
|
|
@@ -1761,6 +2189,21 @@ window.__ModuleLoader__.load({
|
|
|
1761
2189
|
}
|
|
1762
2190
|
|
|
1763
2191
|
/** Record that one global ref appeared in the folder's current turn (unique, ascending). */
|
|
2192
|
+
/** A turn bucket keeps its own mention count and turn list: the folder entry spans
|
|
2193
|
+
every turn, while one bucket must answer "in THIS turn" for the per-turn view. */
|
|
2194
|
+
function withOwnTurns(stored, folder, current) {
|
|
2195
|
+
const turn = folder.currentTurn;
|
|
2196
|
+
const at = isRecord(current) && typeof current.time === "number" && current.time > 0 ? current.time : 0;
|
|
2197
|
+
const seq = isRecord(current) && typeof current.seq === "number" ? current.seq : null;
|
|
2198
|
+
return {
|
|
2199
|
+
...stored,
|
|
2200
|
+
count: 1,
|
|
2201
|
+
turns: typeof turn === "number" && turn >= 1 ? [turn] : [],
|
|
2202
|
+
...(at > 0 ? { time: at, firstTime: at } : {}),
|
|
2203
|
+
...(seq === null ? {} : { seq }),
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
|
|
1764
2207
|
function noteRefTurn(stored, folder) {
|
|
1765
2208
|
const turn = folder.currentTurn;
|
|
1766
2209
|
if (typeof turn !== "number" || turn < 1) return;
|
|
@@ -1782,13 +2225,14 @@ window.__ModuleLoader__.load({
|
|
|
1782
2225
|
const bucketExisting = bucket.refs.get(ref.key);
|
|
1783
2226
|
if (bucketExisting) {
|
|
1784
2227
|
bucketExisting.count += 1;
|
|
2228
|
+
noteRefTurn(bucketExisting, folder);
|
|
1785
2229
|
if (typeof ref.time === "number" && ref.time >= bucketExisting.time) {
|
|
1786
2230
|
bucketExisting.time = ref.time;
|
|
1787
2231
|
bucketExisting.seq = ref.seq;
|
|
1788
2232
|
}
|
|
1789
2233
|
if (typeof ref.previewKey === "string") bucketExisting.previewKey = ref.previewKey;
|
|
1790
2234
|
} else {
|
|
1791
|
-
bucket.refs.set(ref.key,
|
|
2235
|
+
bucket.refs.set(ref.key, withOwnTurns(existing, folder, ref));
|
|
1792
2236
|
bucket.refOrder.push(ref.key);
|
|
1793
2237
|
}
|
|
1794
2238
|
touch(folder);
|
|
@@ -1798,7 +2242,7 @@ window.__ModuleLoader__.load({
|
|
|
1798
2242
|
noteRefTurn(stored, folder);
|
|
1799
2243
|
folder.refs.set(ref.key, stored);
|
|
1800
2244
|
folder.refOrder.push(ref.key);
|
|
1801
|
-
bucket.refs.set(ref.key,
|
|
2245
|
+
bucket.refs.set(ref.key, withOwnTurns(stored, folder, ref));
|
|
1802
2246
|
bucket.refOrder.push(ref.key);
|
|
1803
2247
|
touch(folder);
|
|
1804
2248
|
}
|
|
@@ -1918,6 +2362,26 @@ window.__ModuleLoader__.load({
|
|
|
1918
2362
|
return text.length > 4000 ? text.slice(0, 4000) : text;
|
|
1919
2363
|
}
|
|
1920
2364
|
|
|
2365
|
+
/** Shorter assistant messages are asides (status lines, "done."), not documents. */
|
|
2366
|
+
const CONCLUSION_MIN_CHARS = 32;
|
|
2367
|
+
/** Chars kept per step for the expanded detail (kept small: every step carries one). */
|
|
2368
|
+
const STEP_RESULT_LIMIT = 1600;
|
|
2369
|
+
/** Stop collecting once a result is clearly huge; only the size is remembered. */
|
|
2370
|
+
const STEP_RESULT_SKIP = 40000;
|
|
2371
|
+
function stepResultOf(content) {
|
|
2372
|
+
if (!Array.isArray(content)) return null;
|
|
2373
|
+
let text = "";
|
|
2374
|
+
let chars = 0;
|
|
2375
|
+
for (const part of content) {
|
|
2376
|
+
if (!isRecord(part) || part.type !== "text" || typeof part.text !== "string") continue;
|
|
2377
|
+
chars += part.text.length;
|
|
2378
|
+
if (chars > STEP_RESULT_SKIP) return { text: "", chars, truncated: true, skipped: true };
|
|
2379
|
+
if (text.length < STEP_RESULT_LIMIT) text += part.text.slice(0, STEP_RESULT_LIMIT - text.length);
|
|
2380
|
+
}
|
|
2381
|
+
if (chars === 0) return null;
|
|
2382
|
+
return { text, chars, truncated: chars > text.length, skipped: false };
|
|
2383
|
+
}
|
|
2384
|
+
|
|
1921
2385
|
const ANCHOR_CAP = 8;
|
|
1922
2386
|
function addStep(folder, bucket, step) {
|
|
1923
2387
|
if (typeof step.teamKey === "string") {
|
|
@@ -1956,6 +2420,12 @@ window.__ModuleLoader__.load({
|
|
|
1956
2420
|
function foldEvent(folder, event, cwd) {
|
|
1957
2421
|
if (!isRecord(event) || typeof event.type !== "string") return;
|
|
1958
2422
|
const data = event.data;
|
|
2423
|
+
/* Tool work after a reply proves that reply was not the turn's closing word:
|
|
2424
|
+
the candidate stays provisional until a fresh reply replaces it. */
|
|
2425
|
+
if ((event.type === "tool/call" || event.type === "tool/result") && folder.currentTurn >= 1) {
|
|
2426
|
+
const running = folder.byTurn.get(folder.currentTurn);
|
|
2427
|
+
if (running !== undefined && isRecord(running.lastReply)) running.lastReply.provisional = true;
|
|
2428
|
+
}
|
|
1959
2429
|
if (typeof event.time === "number" && Number.isFinite(event.time)) {
|
|
1960
2430
|
bumpStatTime(folder.stats, event.time);
|
|
1961
2431
|
const statsTurn =
|
|
@@ -1975,6 +2445,45 @@ window.__ModuleLoader__.load({
|
|
|
1975
2445
|
touch(folder);
|
|
1976
2446
|
return;
|
|
1977
2447
|
}
|
|
2448
|
+
case "assistant/message": {
|
|
2449
|
+
/* A reply is only a candidate while the turn runs: 「本轮结论」 is published
|
|
2450
|
+
at turn/end, so 产物 never shows a provisional document mid-turn. */
|
|
2451
|
+
if (!isRecord(data) || !isAppendSurfaceEvent(event)) return;
|
|
2452
|
+
/* Two shapes carry the reply: plugin-level {content} and the durable log's
|
|
2453
|
+
{message:{content}}. Reading only the first left every real turn without a
|
|
2454
|
+
conclusion document, so the auto-open never fired. */
|
|
2455
|
+
const body = isRecord(data.message) ? data.message : data;
|
|
2456
|
+
const text = (typeof body.text === "string" && body.text !== "" ? body.text : textOf(body.content)).trim();
|
|
2457
|
+
if (text === "") return;
|
|
2458
|
+
const turn = folder.currentTurn >= 1 ? folder.currentTurn : Number.isSafeInteger(data.turn) && data.turn >= 1 ? data.turn : 0;
|
|
2459
|
+
const bucket = folder.byTurn.get(turn) ?? turnBucket(folder, folder.currentTurn);
|
|
2460
|
+
bucket.lastReply = {
|
|
2461
|
+
turn,
|
|
2462
|
+
text,
|
|
2463
|
+
seq: typeof event.seq === "number" ? event.seq : 0,
|
|
2464
|
+
time: typeof event.time === "number" ? event.time : 0,
|
|
2465
|
+
provisional: false,
|
|
2466
|
+
};
|
|
2467
|
+
touch(folder);
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
case "turn/end": {
|
|
2471
|
+
if (!isRecord(data) || typeof data.turn !== "number" || !Number.isSafeInteger(data.turn) || data.turn < 1) return;
|
|
2472
|
+
const bucket = folder.byTurn.get(data.turn);
|
|
2473
|
+
if (bucket === undefined) return;
|
|
2474
|
+
bucket.ended = true;
|
|
2475
|
+
const reply = isRecord(bucket.lastReply) ? bucket.lastReply : null;
|
|
2476
|
+
/* Publish the closing reply only now that the turn is over. A provisional
|
|
2477
|
+
reply (tool work followed it) still counts when it is substantive, so a
|
|
2478
|
+
turn whose last action was a tool call keeps its readable document. */
|
|
2479
|
+
if (reply !== null && typeof reply.text === "string" && reply.text !== "" && (reply.provisional !== true || reply.text.length >= CONCLUSION_MIN_CHARS)) {
|
|
2480
|
+
const record = { turn: data.turn, text: reply.text, seq: reply.seq, time: reply.time, ended: true };
|
|
2481
|
+
bucket.conclusion = record;
|
|
2482
|
+
folder.conclusion = record;
|
|
2483
|
+
}
|
|
2484
|
+
touch(folder);
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
1978
2487
|
case "todo/write": {
|
|
1979
2488
|
if (!isRecord(data) || !Array.isArray(data.todos)) return;
|
|
1980
2489
|
const todos = [];
|
|
@@ -2048,8 +2557,13 @@ window.__ModuleLoader__.load({
|
|
|
2048
2557
|
addStep(folder, approvalBucket, {
|
|
2049
2558
|
callId: `approval-${data.id}`,
|
|
2050
2559
|
name: "approval",
|
|
2560
|
+
/* the tool call this gate blocks: the approval renders nested under it so
|
|
2561
|
+
a child never appears to finish before its own parent */
|
|
2562
|
+
...(typeof data.callId === "string" && data.callId !== "" ? { parentCallId: data.callId } : {}),
|
|
2051
2563
|
detail: toolName,
|
|
2052
2564
|
full: typeof data.reason === "string" && data.reason !== "" ? `${toolName} · ${data.reason}` : toolName,
|
|
2565
|
+
approvalTool: toolName,
|
|
2566
|
+
...(typeof data.reason === "string" && data.reason !== "" ? { approvalReason: data.reason } : {}),
|
|
2053
2567
|
milestone: true,
|
|
2054
2568
|
done: false,
|
|
2055
2569
|
seq: event.seq,
|
|
@@ -2061,6 +2575,7 @@ window.__ModuleLoader__.load({
|
|
|
2061
2575
|
if (!isRecord(data) || typeof data.id !== "string" || data.id === "") return;
|
|
2062
2576
|
const outcome = typeof data.outcome === "string" ? data.outcome : "";
|
|
2063
2577
|
const approvalStep = folder.stepByCall.get(`approval-${data.id}`);
|
|
2578
|
+
if (approvalStep !== undefined && outcome !== "") approvalStep.approvalOutcome = outcome;
|
|
2064
2579
|
if (approvalStep !== undefined && outcome !== "" && typeof approvalStep.detail === "string" && !approvalStep.detail.includes(outcome)) {
|
|
2065
2580
|
approvalStep.detail = approvalStep.detail === "" ? outcome : `${approvalStep.detail} · ${outcome}`;
|
|
2066
2581
|
}
|
|
@@ -2112,6 +2627,10 @@ window.__ModuleLoader__.load({
|
|
|
2112
2627
|
folder.pending.set(callId, { type: "artifact", path: producedPath });
|
|
2113
2628
|
return;
|
|
2114
2629
|
}
|
|
2630
|
+
if (name === "exit_plan_mode") {
|
|
2631
|
+
folder.pending.set(callId, { type: "plan", callId, text: args !== null && isRecord(args) && typeof args.plan === "string" ? args.plan : "" });
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2115
2634
|
if (name === "skill" && args && typeof args.name === "string" && args.name.length > 0) {
|
|
2116
2635
|
folder.pending.set(callId, {
|
|
2117
2636
|
type: "reference",
|
|
@@ -2188,6 +2707,16 @@ window.__ModuleLoader__.load({
|
|
|
2188
2707
|
const callId = isRecord(source) && typeof source.callId === "string" ? source.callId : undefined;
|
|
2189
2708
|
if (callId === undefined) return;
|
|
2190
2709
|
markStepDone(folder, callId, event.time, message.isError === true, message.isError === true ? textOf(message.content) : undefined);
|
|
2710
|
+
const resultStep = folder.stepByCall.get(callId);
|
|
2711
|
+
if (resultStep !== undefined && resultStep.result === undefined) {
|
|
2712
|
+
const captured = stepResultOf(message.content);
|
|
2713
|
+
if (captured !== null) {
|
|
2714
|
+
resultStep.result = captured.text;
|
|
2715
|
+
resultStep.resultChars = captured.chars;
|
|
2716
|
+
resultStep.resultTruncated = captured.truncated;
|
|
2717
|
+
resultStep.resultSkipped = captured.skipped;
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2191
2720
|
if (message.isError === true) {
|
|
2192
2721
|
folder.stats.errors += 1;
|
|
2193
2722
|
const errorBucket = folder.byTurn.get(folder.currentTurn);
|
|
@@ -2205,6 +2734,18 @@ window.__ModuleLoader__.load({
|
|
|
2205
2734
|
seq: event.seq,
|
|
2206
2735
|
time: event.time,
|
|
2207
2736
|
});
|
|
2737
|
+
} else if (pending.type === "plan" && typeof pending.text === "string" && pending.text.trim() !== "") {
|
|
2738
|
+
const planTitle = planTitleOf(pending.text);
|
|
2739
|
+
const planRefKey = `plan|${typeof event.seq === "number" ? event.seq : folder.currentTurn}`;
|
|
2740
|
+
const previewKey = rememberContextPreview(folder.sessionId, planRefKey, planTitle, pending.text, "plan");
|
|
2741
|
+
upsertArtifact(folder, bucket, `plan:${planTitle === "" ? planRefKey : planTitle}`, {
|
|
2742
|
+
plan: true,
|
|
2743
|
+
...(planTitle === "" ? {} : { planTitle }),
|
|
2744
|
+
...(previewKey === undefined ? {} : { previewKey }),
|
|
2745
|
+
...(typeof pending.callId === "string" && pending.callId !== "" ? { planCallId: pending.callId } : {}),
|
|
2746
|
+
seq: event.seq,
|
|
2747
|
+
time: event.time,
|
|
2748
|
+
});
|
|
2208
2749
|
} else {
|
|
2209
2750
|
const resultText = textOf(message.content);
|
|
2210
2751
|
for (const ref of pending.refs) {
|
|
@@ -2338,16 +2879,41 @@ window.__ModuleLoader__.load({
|
|
|
2338
2879
|
todosTime = own !== null ? own.time : 0;
|
|
2339
2880
|
}
|
|
2340
2881
|
const artifacts = [...source.artifacts.values()].sort((left, right) => (right.time || right.seq) - (left.time || left.seq) || right.seq - left.seq);
|
|
2882
|
+
/* 「本轮结论」: the closing reply is offered as a document, newest scope first. */
|
|
2883
|
+
const rawConclusion = isRecord(source.conclusion) ? source.conclusion : null;
|
|
2884
|
+
let conclusion = null;
|
|
2885
|
+
if (rawConclusion !== null && typeof rawConclusion.text === "string" && rawConclusion.text !== "") {
|
|
2886
|
+
const title = translate("artifact.conclusionTurn", { n: rawConclusion.turn });
|
|
2887
|
+
const previewKey = rememberContextPreview(folder.sessionId, `conclusion|${rawConclusion.turn}`, title, rawConclusion.text, "conclusion");
|
|
2888
|
+
conclusion = {
|
|
2889
|
+
path: `conclusion:${rawConclusion.turn}`,
|
|
2890
|
+
kind: "conclusion",
|
|
2891
|
+
title,
|
|
2892
|
+
description: "",
|
|
2893
|
+
chars: rawConclusion.text.length,
|
|
2894
|
+
turn: rawConclusion.turn,
|
|
2895
|
+
seq: rawConclusion.seq,
|
|
2896
|
+
time: rawConclusion.time,
|
|
2897
|
+
ended: rawConclusion.ended === true,
|
|
2898
|
+
...(previewKey === undefined ? {} : { previewKey }),
|
|
2899
|
+
};
|
|
2900
|
+
artifacts.unshift(conclusion);
|
|
2901
|
+
}
|
|
2341
2902
|
/* A ref is ubiquitous when it shows up in at least two turns covering >= 60% of
|
|
2342
2903
|
all observed turns — the per-turn OpenViking/runtime injections match this. */
|
|
2343
2904
|
const turnCount = turns.length;
|
|
2344
2905
|
const ubiquitousFrom = turnCount >= 2 ? Math.max(2, Math.ceil(turnCount * 0.6)) : Infinity;
|
|
2345
2906
|
const references = [];
|
|
2346
2907
|
for (let index = source.refOrder.length - 1; index >= 0; index -= 1) {
|
|
2347
|
-
const
|
|
2908
|
+
const key = source.refOrder[index];
|
|
2909
|
+
const ref = source.refs.get(key);
|
|
2348
2910
|
if (ref === undefined) continue;
|
|
2349
2911
|
const refTurns = Array.isArray(ref.turns) ? ref.turns.length : 0;
|
|
2350
|
-
|
|
2912
|
+
/* "common" is a session-wide property: a ref injected every turn stays common
|
|
2913
|
+
even while the view is narrowed to one turn. */
|
|
2914
|
+
const global = source === folder ? ref : folder.refs.get(key);
|
|
2915
|
+
const globalTurns = global !== undefined && Array.isArray(global.turns) ? global.turns.length : refTurns;
|
|
2916
|
+
references.push({ ...ref, refTurns, ubiquitous: globalTurns >= 2 && globalTurns >= ubiquitousFrom });
|
|
2351
2917
|
}
|
|
2352
2918
|
const st = isRecord(source.stats) ? source.stats : EMPTY_BUCKET.stats;
|
|
2353
2919
|
const stats = {
|
|
@@ -2396,10 +2962,13 @@ window.__ModuleLoader__.load({
|
|
|
2396
2962
|
turns,
|
|
2397
2963
|
latestTurn,
|
|
2398
2964
|
scope,
|
|
2965
|
+
/* how many turns this scope spans: "all" covers the session, a turn view spans 1 */
|
|
2966
|
+
scopeTurns: source === folder ? turns.length : 1,
|
|
2967
|
+
conclusion,
|
|
2399
2968
|
};
|
|
2400
2969
|
}
|
|
2401
2970
|
|
|
2402
|
-
const EMPTY_STATE = { todos: null, todosTime: 0, artifacts: [], references: [], steps: [], stepsAll: [], team: { members: [], tasks: [], subagents: [] }, stats: { durationMs: 0, toolCalls: 0, commands: 0, errors: 0, files: 0 }, turnStart: null, turnEnd: null, milestones: [], todoHistory: [], changeTurns: [], hasMore: false, localeRev: 0, turns: [], latestTurn: null, scope: "latest" };
|
|
2971
|
+
const EMPTY_STATE = { todos: null, todosTime: 0, artifacts: [], references: [], steps: [], stepsAll: [], team: { members: [], tasks: [], subagents: [] }, stats: { durationMs: 0, toolCalls: 0, commands: 0, errors: 0, files: 0 }, turnStart: null, turnEnd: null, milestones: [], todoHistory: [], changeTurns: [], hasMore: false, localeRev: 0, turns: [], latestTurn: null, scope: "latest", scopeTurns: 1, conclusion: null };
|
|
2403
2972
|
|
|
2404
2973
|
function createSummaryStore(binding) {
|
|
2405
2974
|
const source = binding.eventSource;
|
|
@@ -2562,33 +3131,82 @@ window.__ModuleLoader__.load({
|
|
|
2562
3131
|
return isRecord(goal) && typeof goal.objective === "string" ? goal : null;
|
|
2563
3132
|
}
|
|
2564
3133
|
|
|
2565
|
-
|
|
3134
|
+
/** Backfills older history. Returns true once several consecutive loads made no
|
|
3135
|
+
progress, so the UI can surface 「重新加载」 instead of an endless spinner. */
|
|
3136
|
+
function useBackfill(binding, hasMore, retryToken) {
|
|
3137
|
+
const [stalled, setStalled] = React.useState(false);
|
|
2566
3138
|
React.useEffect(() => {
|
|
3139
|
+
setStalled(false);
|
|
2567
3140
|
if (!binding || !hasMore) return undefined;
|
|
2568
3141
|
let cancelled = false;
|
|
2569
3142
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3143
|
+
/* every host read is guarded: a throwing getSnapshot would kill the loop and
|
|
3144
|
+
leave the status line up with no way back */
|
|
3145
|
+
const readSnapshot = () => {
|
|
3146
|
+
try {
|
|
3147
|
+
const source = binding.eventSource;
|
|
3148
|
+
return source !== undefined && typeof source.getSnapshot === "function" ? source.getSnapshot() : null;
|
|
3149
|
+
} catch {
|
|
3150
|
+
return null;
|
|
3151
|
+
}
|
|
3152
|
+
};
|
|
3153
|
+
const sizeOf = (snapshot) => (isRecord(snapshot) && Array.isArray(snapshot.entries) ? snapshot.entries.length : 0);
|
|
3154
|
+
const session = binding.session;
|
|
3155
|
+
const canLoadThrough = isRecord(session) && typeof session.loadThrough === "function";
|
|
3156
|
+
const canLoadOlder = isRecord(session) && typeof session.loadOlder === "function";
|
|
2570
3157
|
const run = async () => {
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
3158
|
+
if (!canLoadThrough && !canLoadOlder) {
|
|
3159
|
+
/* no loader at all: this can never converge, so say so instead of spinning */
|
|
3160
|
+
setStalled(true);
|
|
3161
|
+
return;
|
|
3162
|
+
}
|
|
3163
|
+
/* The jump loader pages back inside one call until the window covers the
|
|
3164
|
+
target seq. Event seqs are dense and ZERO-based, so the target must be
|
|
3165
|
+
0: aiming at 1 leaves baseSeq stuck at 1, and from then on the host's
|
|
3166
|
+
own `baseSeq <= seq` guard makes every retry a permanent no-op. */
|
|
3167
|
+
let lastSize = sizeOf(readSnapshot());
|
|
3168
|
+
let noProgress = 0;
|
|
3169
|
+
let usedOlder = 0;
|
|
3170
|
+
let resynced = false;
|
|
3171
|
+
const deadline = Date.now() + 45000;
|
|
3172
|
+
for (let attempt = 0; attempt < 400 && !cancelled && Date.now() < deadline; attempt += 1) {
|
|
3173
|
+
const snapshot = readSnapshot();
|
|
2576
3174
|
if (!isRecord(snapshot) || snapshot.hasMore !== true) return;
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
3175
|
+
try {
|
|
3176
|
+
/* A no-op page means the jump loader is blocked (another loader holds
|
|
3177
|
+
the lock, or the seq target is already covered): loadOlder() is
|
|
3178
|
+
seq-free and still prepends a real page, so alternate to it. If even
|
|
3179
|
+
that stalls, rebuild the history source once. */
|
|
3180
|
+
if (usedOlder < 2 || !canLoadOlder) {
|
|
3181
|
+
if (canLoadThrough) await session.loadThrough(0);
|
|
3182
|
+
else await session.loadOlder();
|
|
3183
|
+
} else if (!resynced && typeof session.resync === "function") {
|
|
3184
|
+
resynced = true;
|
|
3185
|
+
await session.resync();
|
|
3186
|
+
} else {
|
|
3187
|
+
await session.loadOlder();
|
|
3188
|
+
}
|
|
3189
|
+
} catch {
|
|
3190
|
+
/* a rejected page must not kill the loop; count it as no progress */
|
|
3191
|
+
}
|
|
2581
3192
|
if (cancelled) return;
|
|
2582
|
-
const after =
|
|
3193
|
+
const after = readSnapshot();
|
|
2583
3194
|
if (isRecord(after) && after.hasMore !== true) return;
|
|
2584
|
-
|
|
3195
|
+
const size = sizeOf(after);
|
|
3196
|
+
const grew = size > lastSize;
|
|
3197
|
+
lastSize = size;
|
|
3198
|
+
noProgress = grew ? 0 : noProgress + 1;
|
|
3199
|
+
usedOlder = grew ? 0 : usedOlder + 1;
|
|
3200
|
+
if (noProgress === 4) setStalled(true);
|
|
3201
|
+
await sleep(grew ? 60 : 200);
|
|
2585
3202
|
}
|
|
2586
3203
|
};
|
|
2587
3204
|
run().catch(() => {});
|
|
2588
3205
|
return () => {
|
|
2589
3206
|
cancelled = true;
|
|
2590
3207
|
};
|
|
2591
|
-
}, [binding, hasMore]);
|
|
3208
|
+
}, [binding, hasMore, retryToken]);
|
|
3209
|
+
return stalled;
|
|
2592
3210
|
}
|
|
2593
3211
|
|
|
2594
3212
|
/** Class map mirroring the copied FileTypeIcon.module.css, renamed under dsov-ft-*. */
|
|
@@ -3319,6 +3937,49 @@ function FileTypeIcon(props) {
|
|
|
3319
3937
|
]);
|
|
3320
3938
|
}
|
|
3321
3939
|
|
|
3940
|
+
function IconPlan({ size = 16, className }) {
|
|
3941
|
+
return svg(size, className, [
|
|
3942
|
+
h("rect", { key: "a", x: 3.2, y: 2.2, width: 9.6, height: 11.6, rx: 1.5, stroke: "currentColor", fill: "none" }),
|
|
3943
|
+
h("path", { key: "b", d: "M5.6 5.6h4.8M5.6 8.1h4.8M5.6 10.6h2.9", stroke: "currentColor", strokeLinecap: "round" }),
|
|
3944
|
+
]);
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
function IconInfo({ size = 14, className }) {
|
|
3948
|
+
return svg(size, className, [
|
|
3949
|
+
h("circle", { key: "a", cx: 8, cy: 8, r: 6.1, stroke: "currentColor", fill: "none" }),
|
|
3950
|
+
h("path", { key: "b", d: "M8 7.2v3.9", stroke: "currentColor", strokeLinecap: "round" }),
|
|
3951
|
+
h("circle", { key: "c", cx: 8, cy: 5.1, r: 0.85, fill: "currentColor" }),
|
|
3952
|
+
], "0 0 16 16");
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
/** Chrome copy the chat Markdown component expects (code banner, footnotes). */
|
|
3956
|
+
function chatMarkdownLabels(t) {
|
|
3957
|
+
return {
|
|
3958
|
+
code: {
|
|
3959
|
+
copyLabel: t("markdown.copy"),
|
|
3960
|
+
copiedLabel: t("markdown.copied"),
|
|
3961
|
+
toolbarLabels: {
|
|
3962
|
+
codeLabel: t("markdown.code"),
|
|
3963
|
+
wrapLabel: t("markdown.wrap"),
|
|
3964
|
+
unwrapLabel: t("markdown.unwrap"),
|
|
3965
|
+
},
|
|
3966
|
+
},
|
|
3967
|
+
footnotes: t("markdown.footnotes"),
|
|
3968
|
+
};
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
/** Turn conclusion: a written page with a sealed tick on its corner. */
|
|
3972
|
+
function IconConclusion({ size = 16, className }) {
|
|
3973
|
+
return svg(size, className, [
|
|
3974
|
+
h("path", { key: "a", d: "M9.2 1.6H4.3c-.85 0-1.5.65-1.5 1.5v9.8c0 .85.65 1.5 1.5 1.5h4.6", stroke: "currentColor", strokeLinejoin: "round", fill: "none", strokeWidth: 1.25 }),
|
|
3975
|
+
h("path", { key: "b", d: "M9.1 1.8v3.7h3.9", stroke: "currentColor", strokeLinejoin: "round", fill: "none", strokeWidth: 1.25 }),
|
|
3976
|
+
h("path", { key: "c", d: "M9.1 1.8v3.7h3.6V5.6L9.1 1.8Z", stroke: "currentColor", strokeLinejoin: "round", fill: "none", strokeWidth: 1.25 }),
|
|
3977
|
+
h("path", { key: "d", d: "M5.2 7.3h4.7M5.2 9.4h3", stroke: "currentColor", strokeLinecap: "round", strokeWidth: 1.25 }),
|
|
3978
|
+
h("circle", { key: "e", cx: 11.7, cy: 12.1, r: 2.55, fill: "currentColor" }),
|
|
3979
|
+
h("path", { key: "f", d: "M10.5 12.1l.85.9 1.6-1.8", stroke: "#fff", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.15, fill: "none" }),
|
|
3980
|
+
], "0 0 16 16");
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3322
3983
|
function IconSkill({ size = 16, className }) {
|
|
3323
3984
|
return svg(size, className, [
|
|
3324
3985
|
h("path", { key: "a", d: "M8.8 1.6 3.4 9h4l-.8 5.4L12.6 7h-4.2l.4-5.4Z", stroke: "currentColor", strokeLinejoin: "round" }),
|
|
@@ -3563,13 +4224,34 @@ function FileTypeIcon(props) {
|
|
|
3563
4224
|
}
|
|
3564
4225
|
|
|
3565
4226
|
/** Incoming order is newest-first; kind/count modes re-rank, breaking ties by recency. */
|
|
3566
|
-
|
|
4227
|
+
/** Mentions of one reference: folds can count one turn twice, so this is not turn coverage. */
|
|
4228
|
+
function refHitsOf(ref) {
|
|
4229
|
+
return isRecord(ref) && typeof ref.count === "number" && ref.count > 0 ? ref.count : 1;
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
/** Distinct turns that mentioned one reference; a single turn can fold it twice. */
|
|
4233
|
+
function refTurnsOf(ref) {
|
|
4234
|
+
if (isRecord(ref) && typeof ref.refTurns === "number") return ref.refTurns;
|
|
4235
|
+
return isRecord(ref) && Array.isArray(ref.turns) ? ref.turns.length : 0;
|
|
4236
|
+
}
|
|
4237
|
+
|
|
4238
|
+
function sortReferences(refs, mode, multiTurn) {
|
|
3567
4239
|
const byRecency = (left, right) => (right.time || right.seq || 0) - (left.time || left.seq || 0) || (right.seq || 0) - (left.seq || 0);
|
|
3568
|
-
if (mode === "count")
|
|
4240
|
+
if (mode === "count") {
|
|
4241
|
+
if (multiTurn !== true) return refs.slice().sort((left, right) => refHitsOf(right) - refHitsOf(left) || byRecency(left, right));
|
|
4242
|
+
return refs.slice().sort((left, right) => refTurnsOf(right) - refTurnsOf(left) || refHitsOf(right) - refHitsOf(left) || byRecency(left, right));
|
|
4243
|
+
}
|
|
3569
4244
|
return refs;
|
|
3570
4245
|
}
|
|
3571
4246
|
|
|
3572
|
-
|
|
4247
|
+
/** Alternating animation names let the CSS entrance replay without remounting
|
|
4248
|
+
(a keyed remount would drop collapse/scroll state on every new turn). */
|
|
4249
|
+
function sectionIntroProps(introSeq, delayMs) {
|
|
4250
|
+
if (!Number.isFinite(introSeq) || introSeq <= 0) return {};
|
|
4251
|
+
return { introAttr: introSeq % 2 === 0 ? "even" : "odd", introDelay: delayMs };
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
function Section({ id, icon, title, badge, empty, action, children, introAttr, introDelay }) {
|
|
3573
4255
|
const [open, setOpen] = React.useState(() => collapsedSections.get(id) !== true);
|
|
3574
4256
|
const cfg = useConfig();
|
|
3575
4257
|
const kids = Array.isArray(children) ? children.filter((child) => child !== null && child !== undefined && child !== false) : children ? [children] : [];
|
|
@@ -3580,7 +4262,12 @@ function FileTypeIcon(props) {
|
|
|
3580
4262
|
};
|
|
3581
4263
|
return h(
|
|
3582
4264
|
"section",
|
|
3583
|
-
{
|
|
4265
|
+
{
|
|
4266
|
+
className: "dsov-section",
|
|
4267
|
+
"data-full": cfg.sectionFullHeight === true ? "true" : "false",
|
|
4268
|
+
"data-intro": introAttr,
|
|
4269
|
+
style: typeof introDelay === "number" ? { animationDelay: `${introDelay}ms` } : undefined,
|
|
4270
|
+
},
|
|
3584
4271
|
h(
|
|
3585
4272
|
"div",
|
|
3586
4273
|
{ className: "dsov-sectionHead" },
|
|
@@ -3626,7 +4313,7 @@ function FileTypeIcon(props) {
|
|
|
3626
4313
|
complete: "success",
|
|
3627
4314
|
};
|
|
3628
4315
|
|
|
3629
|
-
function ProgressSection({ summary, goal, t, waiting, cfg, asOf }) {
|
|
4316
|
+
function ProgressSection({ summary, goal, t, waiting, cfg, asOf, introSeq }) {
|
|
3630
4317
|
const fmtTime = (ts) => formatTime(ts, cfg.timeMode, summary.turnStart);
|
|
3631
4318
|
const timeTitle = (ts) => (cfg.timeMode === "elapsed" ? formatTime(ts) : undefined);
|
|
3632
4319
|
/* timeline scrubbing: only what already happened at `asOf` is shown */
|
|
@@ -3739,14 +4426,59 @@ function FileTypeIcon(props) {
|
|
|
3739
4426
|
if (!stepAt(candidate)) continue;
|
|
3740
4427
|
const key = milestoneKeyOf(candidate);
|
|
3741
4428
|
if (baseKeys.has(key)) continue;
|
|
4429
|
+
if (typeof candidate.callId === "string" && nestedApprovalCallIds.has(candidate.callId)) continue;
|
|
3742
4430
|
baseKeys.add(key);
|
|
3743
4431
|
milestoneExtras.push(stepClamped(candidate));
|
|
3744
4432
|
}
|
|
3745
4433
|
milestoneExtras.sort((left, right) => (left.time || 0) - (right.time || 0));
|
|
3746
4434
|
}
|
|
3747
4435
|
const displaySteps = milestoneExtras.length > 0 ? [...milestoneExtras, ...(stepsOpen ? expandedSteps : steps)] : stepsOpen ? expandedSteps : steps;
|
|
4436
|
+
/* An approval gates one tool call; render it directly under that call so a
|
|
4437
|
+
child row can never look like it finished before its own parent. */
|
|
4438
|
+
const approvalChildren = new Map();
|
|
4439
|
+
const nestedApprovalKeys = new Set();
|
|
4440
|
+
const nestedApprovalCallIds = new Set();
|
|
4441
|
+
{
|
|
4442
|
+
const keyByCall = new Map();
|
|
4443
|
+
displaySteps.forEach((step, index) => {
|
|
4444
|
+
if (typeof step.callId === "string" && step.callId !== "") keyByCall.set(step.callId, stepKeyOf(step, index));
|
|
4445
|
+
});
|
|
4446
|
+
displaySteps.forEach((step, index) => {
|
|
4447
|
+
if (step.name !== "approval" || typeof step.parentCallId !== "string" || step.parentCallId === "") return;
|
|
4448
|
+
const parentKey = keyByCall.get(step.parentCallId);
|
|
4449
|
+
const ownKey = stepKeyOf(step, index);
|
|
4450
|
+
if (parentKey === undefined || parentKey === ownKey) return;
|
|
4451
|
+
const list = approvalChildren.get(parentKey);
|
|
4452
|
+
if (list === undefined) approvalChildren.set(parentKey, [step]);
|
|
4453
|
+
else list.push(step);
|
|
4454
|
+
nestedApprovalKeys.add(ownKey);
|
|
4455
|
+
if (typeof step.callId === "string") nestedApprovalCallIds.add(step.callId);
|
|
4456
|
+
});
|
|
4457
|
+
}
|
|
4458
|
+
const renderSteps = nestedApprovalKeys.size === 0 ? displaySteps : displaySteps.filter((step, index) => !nestedApprovalKeys.has(stepKeyOf(step, index)));
|
|
4459
|
+
/* A step the user expanded while it ran closes itself once it succeeds; a failed
|
|
4460
|
+
step stays open so the error stays readable. */
|
|
4461
|
+
const runningKeys = [];
|
|
4462
|
+
const stepByKey = new Map();
|
|
4463
|
+
renderSteps.forEach((step, index) => {
|
|
4464
|
+
const key = stepKeyOf(step, index);
|
|
4465
|
+
stepByKey.set(key, step);
|
|
4466
|
+
if (step.done !== true && step.error !== true) runningKeys.push(key);
|
|
4467
|
+
});
|
|
4468
|
+
const runningSig = runningKeys.join("|");
|
|
4469
|
+
const prevRunningKeys = React.useRef(new Set());
|
|
4470
|
+
React.useEffect(() => {
|
|
4471
|
+
const now = new Set(runningKeys);
|
|
4472
|
+
for (const key of prevRunningKeys.current) {
|
|
4473
|
+
if (now.has(key)) continue;
|
|
4474
|
+
const step = stepByKey.get(key);
|
|
4475
|
+
if (step === undefined || step.done !== true || step.error === true) continue;
|
|
4476
|
+
if (overrideOf(`step:${key}`, false)) toggleKey(`step:${key}`, false);
|
|
4477
|
+
}
|
|
4478
|
+
prevRunningKeys.current = now;
|
|
4479
|
+
}, [runningSig]);
|
|
3748
4480
|
if (seenSteps.current === null || seenSteps.current.scope !== summary.scope) {
|
|
3749
|
-
seenSteps.current = { scope: summary.scope, keys: new Set(
|
|
4481
|
+
seenSteps.current = { scope: summary.scope, keys: new Set(renderSteps.map(stepKeyOf)) };
|
|
3750
4482
|
}
|
|
3751
4483
|
React.useEffect(
|
|
3752
4484
|
() => () => {
|
|
@@ -3959,6 +4691,106 @@ function FileTypeIcon(props) {
|
|
|
3959
4691
|
target.push(subRow(sub, 0));
|
|
3960
4692
|
});
|
|
3961
4693
|
};
|
|
4694
|
+
/* What an expanded step reveals: the full command/arg text plus the result. */
|
|
4695
|
+
const stepBodyOf = (step) => {
|
|
4696
|
+
if (step.name === "approval") return null;
|
|
4697
|
+
const command = typeof step.full === "string" ? step.full.trim() : "";
|
|
4698
|
+
const rawResult = step.error === true && typeof step.err === "string" && step.err !== "" ? step.err : typeof step.result === "string" ? step.result : "";
|
|
4699
|
+
const result = rawResult.trim();
|
|
4700
|
+
if (command === "" && result === "") return null;
|
|
4701
|
+
const duration = typeof step.endTime === "number" && typeof step.time === "number" ? Math.max(0, step.endTime - step.time) : null;
|
|
4702
|
+
return {
|
|
4703
|
+
command,
|
|
4704
|
+
result,
|
|
4705
|
+
error: step.error === true,
|
|
4706
|
+
duration,
|
|
4707
|
+
chars: typeof step.resultChars === "number" ? step.resultChars : null,
|
|
4708
|
+
truncated: step.resultTruncated === true,
|
|
4709
|
+
skipped: step.resultSkipped === true,
|
|
4710
|
+
};
|
|
4711
|
+
};
|
|
4712
|
+
/* 审批 is part of the step it gates, not a neighbour: it leads the detail panel
|
|
4713
|
+
so the gate reads before the command it unlocked, and the row carries a chip. */
|
|
4714
|
+
const approvalToneOf = (child) => {
|
|
4715
|
+
if (child.done !== true && child.error !== true) return "warn";
|
|
4716
|
+
return /denied|reject|deny/i.test(typeof child.approvalOutcome === "string" ? child.approvalOutcome : "") ? "err" : "ok";
|
|
4717
|
+
};
|
|
4718
|
+
const approvalTitleOf = (approvals, tr) => {
|
|
4719
|
+
const parts = [];
|
|
4720
|
+
for (const child of approvals) {
|
|
4721
|
+
const tool = typeof child.approvalTool === "string" && child.approvalTool !== "" ? child.approvalTool : typeof child.detail === "string" ? child.detail : "";
|
|
4722
|
+
const reason = typeof child.approvalReason === "string" ? child.approvalReason : "";
|
|
4723
|
+
const outcome = typeof child.approvalOutcome === "string" ? child.approvalOutcome : "";
|
|
4724
|
+
const tone = approvalToneOf(child);
|
|
4725
|
+
const state = tone === "warn" ? tr("step.approvalPending") : outcome;
|
|
4726
|
+
const line = [tool, reason, state].filter((part) => typeof part === "string" && part !== "").join(" · ");
|
|
4727
|
+
if (line !== "") parts.push(line);
|
|
4728
|
+
}
|
|
4729
|
+
return parts.length === 0 ? undefined : parts.join("\n");
|
|
4730
|
+
};
|
|
4731
|
+
const approvalBlock = (approvals) => {
|
|
4732
|
+
if (!Array.isArray(approvals) || approvals.length === 0) return null;
|
|
4733
|
+
return h(
|
|
4734
|
+
"div",
|
|
4735
|
+
{ className: "dsov-stepApproval", key: "approval" },
|
|
4736
|
+
approvals.map((child) => {
|
|
4737
|
+
const tone = approvalToneOf(child);
|
|
4738
|
+
const tool = typeof child.approvalTool === "string" && child.approvalTool !== "" ? child.approvalTool : typeof child.detail === "string" ? child.detail : "";
|
|
4739
|
+
const reason = typeof child.approvalReason === "string" ? child.approvalReason : "";
|
|
4740
|
+
const outcome = typeof child.approvalOutcome === "string" ? child.approvalOutcome : "";
|
|
4741
|
+
const label = tone === "warn" ? t("step.approvalPending") : tone === "err" ? t("step.approvalDenied") : t("step.approvalApproved");
|
|
4742
|
+
const stamp = typeof child.endTime === "number" ? child.endTime : child.time;
|
|
4743
|
+
return h(
|
|
4744
|
+
"div",
|
|
4745
|
+
{ className: "dsov-stepApprovalItem", key: typeof child.callId === "string" ? child.callId : "approval", "data-tone": tone },
|
|
4746
|
+
h(
|
|
4747
|
+
"div",
|
|
4748
|
+
{ className: "dsov-stepApprovalHead" },
|
|
4749
|
+
h("span", { className: "dsov-stepApprovalIcon" }, h(IconTarget, { size: 11 })),
|
|
4750
|
+
h("span", { className: "dsov-stepApprovalName" }, t("step.approval")),
|
|
4751
|
+
tool !== "" ? h("span", { className: "dsov-stepApprovalTool" }, tool) : null,
|
|
4752
|
+
h("span", { className: "dsov-stepApprovalBadge", "data-tone": tone }, label),
|
|
4753
|
+
h("span", { className: "dsov-stepApprovalTime" }, fmtTime(stamp))
|
|
4754
|
+
),
|
|
4755
|
+
reason !== "" ? h("div", { className: "dsov-stepApprovalReason" }, reason) : null,
|
|
4756
|
+
outcome !== "" && tone !== "warn" ? h("div", { className: "dsov-stepApprovalOutcome" }, outcome) : null
|
|
4757
|
+
);
|
|
4758
|
+
})
|
|
4759
|
+
);
|
|
4760
|
+
};
|
|
4761
|
+
const stepBodyBlock = (step, key, approvals) => {
|
|
4762
|
+
const body = stepBodyOf(step);
|
|
4763
|
+
const gated = approvalBlock(approvals);
|
|
4764
|
+
if (body === null && gated === null) return null;
|
|
4765
|
+
const meta = [];
|
|
4766
|
+
if (body !== null && body.duration !== null) meta.push(t("progress.stepDuration", { v: formatElapsed(body.duration) }));
|
|
4767
|
+
if (body !== null && body.chars !== null && body.chars > 0) meta.push(t("progress.stepChars", { n: body.chars }));
|
|
4768
|
+
if (body !== null && body.skipped) meta.push(t("progress.stepSkipped"));
|
|
4769
|
+
else if (body !== null && body.truncated) meta.push(t("progress.stepTruncated"));
|
|
4770
|
+
return h(
|
|
4771
|
+
"div",
|
|
4772
|
+
{ className: "dsov-stepBody", key: `${key}-body` },
|
|
4773
|
+
gated,
|
|
4774
|
+
body !== null && body.command !== ""
|
|
4775
|
+
? h(
|
|
4776
|
+
"div",
|
|
4777
|
+
{ className: "dsov-stepBodySec" },
|
|
4778
|
+
h("div", { className: "dsov-stepBodyLabel" }, t("progress.stepCommand")),
|
|
4779
|
+
h("pre", { className: "dsov-stepBodyText" }, body.command)
|
|
4780
|
+
)
|
|
4781
|
+
: null,
|
|
4782
|
+
body !== null && body.result !== ""
|
|
4783
|
+
? h(
|
|
4784
|
+
"div",
|
|
4785
|
+
{ className: "dsov-stepBodySec" },
|
|
4786
|
+
h("div", { className: "dsov-stepBodyLabel" }, t(body.error ? "progress.stepError" : "progress.stepResult")),
|
|
4787
|
+
h("pre", { className: body.error ? "dsov-stepBodyText dsov-stepBodyErr" : "dsov-stepBodyText" }, body.result)
|
|
4788
|
+
)
|
|
4789
|
+
: null,
|
|
4790
|
+
meta.length > 0 ? h("div", { className: "dsov-stepBodyMeta" }, meta.join(" · ")) : null
|
|
4791
|
+
);
|
|
4792
|
+
};
|
|
4793
|
+
|
|
3962
4794
|
const stepRow = (step, key, extraProps, isWaiting) => {
|
|
3963
4795
|
const stepRunning = step.done !== true;
|
|
3964
4796
|
const failed = step.error === true;
|
|
@@ -3967,8 +4799,79 @@ function FileTypeIcon(props) {
|
|
|
3967
4799
|
const full = typeof step.full === "string" && step.full !== "" ? step.full : detail !== "" ? detail : typeof step.name === "string" ? step.name : "";
|
|
3968
4800
|
const flash = stepFlash !== null && isRecord(stepFlash) && stepFlash.key === (typeof step.callId === "string" && step.callId !== "" ? step.callId : `${step.name}|${step.seq}`) ? stepFlash.state : null;
|
|
3969
4801
|
const attached = stepAttachments.get(key);
|
|
3970
|
-
const
|
|
3971
|
-
const
|
|
4802
|
+
const approvals = approvalChildren.get(key);
|
|
4803
|
+
const dying = extraProps !== undefined && extraProps["data-exit"] === "true";
|
|
4804
|
+
const body = dying ? null : stepBodyOf(step);
|
|
4805
|
+
const gated = !dying && Array.isArray(approvals) && approvals.length > 0;
|
|
4806
|
+
const pendingApproval = gated && approvals.some((child) => approvalToneOf(child) === "warn");
|
|
4807
|
+
const expandable = (Array.isArray(attached) && attached.length > 0) || body !== null || gated;
|
|
4808
|
+
/* team rows keep their chevron visible; a plain step reveals it on hover only */
|
|
4809
|
+
const persistentChevron = Array.isArray(attached) && attached.length > 0;
|
|
4810
|
+
const open = expandable ? overrideOf(`step:${key}`, (Array.isArray(attached) && attached.some((item) => item.unfinished)) || pendingApproval) : false;
|
|
4811
|
+
const statusIcon =
|
|
4812
|
+
step.name === "compact-checkpoint"
|
|
4813
|
+
? h("span", { className: "dsov-todoIcon dsov-compactIcon" }, h(IconCompact, { size: 15 }))
|
|
4814
|
+
: waitingRow
|
|
4815
|
+
? h(
|
|
4816
|
+
"span",
|
|
4817
|
+
{ className: "dsov-todoIcon", "data-status": "waiting" },
|
|
4818
|
+
h(
|
|
4819
|
+
"svg",
|
|
4820
|
+
{ width: 15, height: 15, viewBox: "0 0 15 15", fill: "none", "aria-hidden": "true" },
|
|
4821
|
+
h("circle", { cx: 7.5, cy: 7.5, r: 6, stroke: "currentColor", strokeWidth: 1.3, opacity: 0.45 }),
|
|
4822
|
+
h("circle", { cx: 7.5, cy: 7.5, r: 2.4, fill: "currentColor", className: "dsov-todoPulse" })
|
|
4823
|
+
)
|
|
4824
|
+
)
|
|
4825
|
+
: h(
|
|
4826
|
+
"span",
|
|
4827
|
+
{ className: "dsov-todoIcon", "data-status": stepRunning ? "in_progress" : "completed" },
|
|
4828
|
+
h(TodoStatusIcon, { status: stepRunning ? "in_progress" : "completed" })
|
|
4829
|
+
);
|
|
4830
|
+
/* The expand affordance is the status icon itself: hovering swaps it for a
|
|
4831
|
+
chevron in place, so nothing shifts and no second column appears. Team rows
|
|
4832
|
+
keep their own always-visible chevron ahead of the status icon. */
|
|
4833
|
+
const swapChevron = h(
|
|
4834
|
+
"span",
|
|
4835
|
+
{ className: "dsov-chevron dsov-stepSwapChevron", "data-open": open ? "true" : "false", "aria-hidden": "true" },
|
|
4836
|
+
h(IconChevron, { size: 12 })
|
|
4837
|
+
);
|
|
4838
|
+
const leading = !expandable
|
|
4839
|
+
? statusIcon
|
|
4840
|
+
: persistentChevron
|
|
4841
|
+
? h(
|
|
4842
|
+
React.Fragment,
|
|
4843
|
+
{ key: "lead" },
|
|
4844
|
+
h(
|
|
4845
|
+
"span",
|
|
4846
|
+
{
|
|
4847
|
+
className: "dsov-chevron dsov-stepChevron",
|
|
4848
|
+
key: "chevron",
|
|
4849
|
+
"data-open": open ? "true" : "false",
|
|
4850
|
+
title: t(open ? "progress.collapseTeam" : "progress.expandTeam"),
|
|
4851
|
+
onClick: (event) => {
|
|
4852
|
+
event.stopPropagation();
|
|
4853
|
+
toggleKey(`step:${key}`, !open);
|
|
4854
|
+
},
|
|
4855
|
+
},
|
|
4856
|
+
h(IconChevron, { size: 11 })
|
|
4857
|
+
),
|
|
4858
|
+
statusIcon
|
|
4859
|
+
)
|
|
4860
|
+
: h(
|
|
4861
|
+
"span",
|
|
4862
|
+
{
|
|
4863
|
+
className: "dsov-stepIconSwap",
|
|
4864
|
+
key: "swap",
|
|
4865
|
+
"data-open": open ? "true" : "false",
|
|
4866
|
+
title: t(open ? "progress.collapseStep" : "progress.expandStep"),
|
|
4867
|
+
onClick: (event) => {
|
|
4868
|
+
event.stopPropagation();
|
|
4869
|
+
toggleKey(`step:${key}`, !open);
|
|
4870
|
+
},
|
|
4871
|
+
},
|
|
4872
|
+
statusIcon,
|
|
4873
|
+
swapChevron
|
|
4874
|
+
);
|
|
3972
4875
|
const row = h(
|
|
3973
4876
|
"div",
|
|
3974
4877
|
{
|
|
@@ -3981,24 +4884,67 @@ function FileTypeIcon(props) {
|
|
|
3981
4884
|
...(extraProps === undefined ? {} : extraProps),
|
|
3982
4885
|
onClick: () => onStepClick(step),
|
|
3983
4886
|
},
|
|
3984
|
-
|
|
4887
|
+
leading,
|
|
4888
|
+
h(
|
|
4889
|
+
"span",
|
|
4890
|
+
{ className: "dsov-stepName" },
|
|
4891
|
+
step.name === "compact-checkpoint" ? t("step.compact") : step.name === "approval" ? t("step.approval") : typeof step.name === "string" ? step.name : ""
|
|
4892
|
+
),
|
|
4893
|
+
h("span", { className: "dsov-stepDetail" }, detail),
|
|
4894
|
+
gated
|
|
3985
4895
|
? h(
|
|
3986
4896
|
"span",
|
|
3987
4897
|
{
|
|
3988
|
-
className: "dsov-
|
|
3989
|
-
"data-
|
|
3990
|
-
title:
|
|
3991
|
-
onClick: (event) => {
|
|
3992
|
-
event.stopPropagation();
|
|
3993
|
-
toggleKey(`step:${key}`, !open);
|
|
3994
|
-
},
|
|
4898
|
+
className: "dsov-stepChip",
|
|
4899
|
+
"data-tone": approvalToneOf(approvals[0]),
|
|
4900
|
+
title: approvalTitleOf(approvals, t),
|
|
3995
4901
|
},
|
|
3996
|
-
|
|
4902
|
+
approvalToneOf(approvals[0]) === "warn" ? t("step.approvalPending") : t("step.approvalGated")
|
|
3997
4903
|
)
|
|
3998
4904
|
: null,
|
|
3999
|
-
step.
|
|
4000
|
-
|
|
4001
|
-
|
|
4905
|
+
h("span", { className: "dsov-time", title: timeTitle(typeof step.endTime === "number" ? step.endTime : step.time) }, fmtTime(typeof step.endTime === "number" ? step.endTime : step.time))
|
|
4906
|
+
);
|
|
4907
|
+
if (!open) return row;
|
|
4908
|
+
const extras = [];
|
|
4909
|
+
if (body !== null || gated) {
|
|
4910
|
+
const block = stepBodyBlock(step, key, approvals);
|
|
4911
|
+
if (block !== null) extras.push(block);
|
|
4912
|
+
}
|
|
4913
|
+
if (Array.isArray(attached)) for (const item of attached) extras.push(item.row);
|
|
4914
|
+
return extras.length === 0 ? row : [row, ...extras];
|
|
4915
|
+
};
|
|
4916
|
+
/* Nested approval row: its own two-line layout, because reusing the flat step
|
|
4917
|
+
row squeezed the reason into the ellipsised detail column (~38px in a 332px
|
|
4918
|
+
sidebar) and the text looked clipped away. The reason now wraps on its own
|
|
4919
|
+
line; the outcome becomes a small badge instead of trailing detail text. */
|
|
4920
|
+
const approvalChildRow = (step, key, isWaiting) => {
|
|
4921
|
+
const running = step.done !== true;
|
|
4922
|
+
const failed = step.error === true;
|
|
4923
|
+
const waitingRow = isWaiting === true && running && !failed;
|
|
4924
|
+
const tool = typeof step.approvalTool === "string" && step.approvalTool !== "" ? step.approvalTool : typeof step.detail === "string" ? step.detail : "";
|
|
4925
|
+
const reason = typeof step.approvalReason === "string" ? step.approvalReason : "";
|
|
4926
|
+
const outcome = typeof step.approvalOutcome === "string" ? step.approvalOutcome : "";
|
|
4927
|
+
const outcomeText =
|
|
4928
|
+
outcome === ""
|
|
4929
|
+
? ""
|
|
4930
|
+
: /approved|allow/i.test(outcome)
|
|
4931
|
+
? t("step.approvalApproved")
|
|
4932
|
+
: /denied|reject|deny/i.test(outcome)
|
|
4933
|
+
? t("step.approvalDenied")
|
|
4934
|
+
: outcome;
|
|
4935
|
+
const outcomeOk = !/denied|reject|deny/i.test(outcome);
|
|
4936
|
+
const stamp = typeof step.endTime === "number" ? step.endTime : step.time;
|
|
4937
|
+
const hoverText = [tool, reason, outcome].filter((part) => typeof part === "string" && part !== "").join(" · ");
|
|
4938
|
+
return h(
|
|
4939
|
+
"div",
|
|
4940
|
+
{
|
|
4941
|
+
className: "dsov-step dsov-stepChild",
|
|
4942
|
+
key,
|
|
4943
|
+
"data-error": failed ? "true" : "false",
|
|
4944
|
+
"data-waiting": waitingRow ? "true" : "false",
|
|
4945
|
+
title: hoverText === "" ? undefined : hoverText,
|
|
4946
|
+
},
|
|
4947
|
+
waitingRow
|
|
4002
4948
|
? h(
|
|
4003
4949
|
"span",
|
|
4004
4950
|
{ className: "dsov-todoIcon", "data-status": "waiting" },
|
|
@@ -4009,17 +4955,29 @@ function FileTypeIcon(props) {
|
|
|
4009
4955
|
h("circle", { cx: 7.5, cy: 7.5, r: 2.4, fill: "currentColor", className: "dsov-todoPulse" })
|
|
4010
4956
|
)
|
|
4011
4957
|
)
|
|
4012
|
-
: h(
|
|
4958
|
+
: h(
|
|
4959
|
+
"span",
|
|
4960
|
+
{ className: "dsov-todoIcon", "data-status": running ? "in_progress" : "completed" },
|
|
4961
|
+
h(TodoStatusIcon, { status: running ? "in_progress" : "completed" })
|
|
4962
|
+
),
|
|
4013
4963
|
h(
|
|
4014
|
-
"
|
|
4015
|
-
{ className: "dsov-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4964
|
+
"div",
|
|
4965
|
+
{ className: "dsov-stepChildBody" },
|
|
4966
|
+
h(
|
|
4967
|
+
"div",
|
|
4968
|
+
{ className: "dsov-stepChildTop" },
|
|
4969
|
+
h("span", { className: "dsov-stepName" }, t("step.approval")),
|
|
4970
|
+
tool !== "" ? h("span", { className: "dsov-stepChildTool" }, tool) : null,
|
|
4971
|
+
outcomeText !== ""
|
|
4972
|
+
? h("span", { className: "dsov-stepChildOutcome", "data-ok": outcomeOk ? "true" : "false" }, outcomeText)
|
|
4973
|
+
: null,
|
|
4974
|
+
h("span", { className: "dsov-time", title: timeTitle(stamp) }, fmtTime(stamp))
|
|
4975
|
+
),
|
|
4976
|
+
reason !== "" ? h("div", { className: "dsov-stepChildNote" }, reason) : null
|
|
4977
|
+
)
|
|
4020
4978
|
);
|
|
4021
|
-
return expandable && open ? [row, ...attached.map((item) => item.row)] : row;
|
|
4022
4979
|
};
|
|
4980
|
+
|
|
4023
4981
|
const pushStepRow = (target, step, key, extraProps, isWaiting) => {
|
|
4024
4982
|
const rendered = stepRow(step, key, extraProps, isWaiting);
|
|
4025
4983
|
if (Array.isArray(rendered)) target.push(...rendered);
|
|
@@ -4142,10 +5100,17 @@ function FileTypeIcon(props) {
|
|
|
4142
5100
|
if (hasTeam && !showSteps) pushFlatTeam(kids);
|
|
4143
5101
|
if (showSteps) {
|
|
4144
5102
|
const markWaiting = waiting === true && summary.scope === "latest";
|
|
5103
|
+
/* a step blocked on an approval is not the thing being waited on: the
|
|
5104
|
+
nested approval row carries the waiting state instead */
|
|
5105
|
+
const approvalBlocked = new Set();
|
|
5106
|
+
for (const [parentKey, list] of approvalChildren) {
|
|
5107
|
+
if (list.some((child) => child.done !== true && child.error !== true)) approvalBlocked.add(parentKey);
|
|
5108
|
+
}
|
|
4145
5109
|
let waitingIndex = -1;
|
|
4146
5110
|
if (markWaiting) {
|
|
4147
|
-
for (let index =
|
|
4148
|
-
if (
|
|
5111
|
+
for (let index = renderSteps.length - 1; index >= 0; index -= 1) {
|
|
5112
|
+
if (approvalBlocked.has(stepKeyOf(renderSteps[index], index))) continue;
|
|
5113
|
+
if (renderSteps[index].done !== true && renderSteps[index].error !== true) {
|
|
4149
5114
|
waitingIndex = index;
|
|
4150
5115
|
break;
|
|
4151
5116
|
}
|
|
@@ -4153,13 +5118,18 @@ function FileTypeIcon(props) {
|
|
|
4153
5118
|
}
|
|
4154
5119
|
const rows = [];
|
|
4155
5120
|
let freshCount = 0;
|
|
4156
|
-
|
|
5121
|
+
renderSteps.forEach((step, index) => {
|
|
4157
5122
|
const key = stepKeyOf(step, index);
|
|
4158
5123
|
const fresh = asOfTime === null && !seenSteps.current.keys.has(key);
|
|
4159
5124
|
if (fresh) {
|
|
4160
5125
|
seenSteps.current.keys.add(key);
|
|
4161
5126
|
freshCount += 1;
|
|
4162
5127
|
}
|
|
5128
|
+
if (step.name === "approval") {
|
|
5129
|
+
/* an approval whose tool call fell outside the window still needs a row */
|
|
5130
|
+
rows.push(approvalChildRow(step, key, step.done !== true && step.error !== true));
|
|
5131
|
+
return;
|
|
5132
|
+
}
|
|
4163
5133
|
pushStepRow(rows, step, key, fresh ? { "data-enter": "true", style: { animationDelay: `${(freshCount - 1) * 45}ms` } } : undefined, index === waitingIndex);
|
|
4164
5134
|
});
|
|
4165
5135
|
if (ghost !== null && isRecord(ghost) && !stepsOpen) {
|
|
@@ -4188,10 +5158,14 @@ function FileTypeIcon(props) {
|
|
|
4188
5158
|
);
|
|
4189
5159
|
kids.push(h("div", { className: "dsov-steps", key: "steps" }, rows));
|
|
4190
5160
|
}
|
|
4191
|
-
return h(
|
|
5161
|
+
return h(
|
|
5162
|
+
Section,
|
|
5163
|
+
{ id: "progress", ...sectionIntroProps(introSeq, 0), icon: h(IconProgress, { size: 15 }), title: t("section.progress"), empty: t(summary.scope === "all" ? "progress.empty" : "progress.emptyTurn") },
|
|
5164
|
+
kids
|
|
5165
|
+
);
|
|
4192
5166
|
}
|
|
4193
5167
|
|
|
4194
|
-
function ArtifactsSection({ summary, t, binding, stats, asOf }) {
|
|
5168
|
+
function ArtifactsSection({ summary, t, binding, stats, asOf, introSeq }) {
|
|
4195
5169
|
const cfg = useConfig();
|
|
4196
5170
|
const asOfTime = typeof asOf === "number" && asOf > 0 ? asOf : null;
|
|
4197
5171
|
const visibleArtifacts = asOfTime === null ? summary.artifacts : summary.artifacts.filter((artifact) => (artifact.firstTime || artifact.time || 0) <= asOfTime);
|
|
@@ -4233,24 +5207,35 @@ function FileTypeIcon(props) {
|
|
|
4233
5207
|
const stat = stats ? stats.get(absPath(cwd, artifact.path)) : undefined;
|
|
4234
5208
|
const showCounts = stat !== undefined && !stat.binary && !stat.oversized && (stat.added > 0 || stat.deleted > 0);
|
|
4235
5209
|
const isMemory = typeof artifact.path === "string" && artifact.path.startsWith("viking://");
|
|
5210
|
+
const isConclusion = artifact.kind === "conclusion";
|
|
5211
|
+
const isPlan = artifact.plan === true;
|
|
5212
|
+
const planName = isPlan ? (typeof artifact.planTitle === "string" && artifact.planTitle !== "" ? artifact.planTitle : t("artifact.plan")) : "";
|
|
4236
5213
|
return h(
|
|
4237
5214
|
"button",
|
|
4238
5215
|
{
|
|
4239
5216
|
type: "button",
|
|
4240
5217
|
className: "dsov-row",
|
|
4241
5218
|
key: `${artifact.path}#${index}`,
|
|
4242
|
-
title: artifact.description ? `${artifact.path} — ${artifact.description}` : artifact.path,
|
|
5219
|
+
title: isConclusion ? `${artifact.title} · ${t("artifact.conclusionHint")}` : isPlan ? planName : artifact.description ? `${artifact.path} — ${artifact.description}` : artifact.path,
|
|
4243
5220
|
onClick: () => openArtifact(binding, artifact, stats),
|
|
4244
5221
|
onContextMenu:
|
|
4245
|
-
canFileMenu && !isMemory
|
|
5222
|
+
canFileMenu && !isMemory && !isPlan && !isConclusion
|
|
4246
5223
|
? (event) => {
|
|
4247
5224
|
event.preventDefault();
|
|
4248
5225
|
setFileMenu({ x: event.clientX, y: event.clientY, path: absPath(cwd, artifact.path) });
|
|
4249
5226
|
}
|
|
4250
5227
|
: undefined,
|
|
4251
5228
|
},
|
|
4252
|
-
h(
|
|
4253
|
-
|
|
5229
|
+
h(
|
|
5230
|
+
"span",
|
|
5231
|
+
{ className: "dsov-fileIcon" },
|
|
5232
|
+
isConclusion
|
|
5233
|
+
? h("span", { className: "dsov-fileMark", "data-kind": "conclusion" }, h(IconConclusion, { size: 15 }))
|
|
5234
|
+
: isPlan
|
|
5235
|
+
? h(IconPlan, { size: 20 })
|
|
5236
|
+
: h(FileTypeIcon, { path: artifact.path, size: 20 })
|
|
5237
|
+
),
|
|
5238
|
+
h("span", { className: "dsov-artifactName" }, isConclusion ? artifact.title : isPlan ? planName : basename(artifact.path)),
|
|
4254
5239
|
showCounts
|
|
4255
5240
|
? h(
|
|
4256
5241
|
"span",
|
|
@@ -4263,6 +5248,9 @@ function FileTypeIcon(props) {
|
|
|
4263
5248
|
: null,
|
|
4264
5249
|
isMemory ? h("span", { className: "dsov-badge", "data-tone": "quiet" }, t("artifact.memory")) : null,
|
|
4265
5250
|
artifact.presented ? h("span", { className: "dsov-badge", "data-tone": "brand" }, t("artifact.presented")) : null,
|
|
5251
|
+
isPlan ? h("span", { className: "dsov-badge", "data-tone": "plan" }, t("artifact.plan")) : null,
|
|
5252
|
+
isConclusion ? h("span", { className: "dsov-badge", "data-tone": "brand" }, t("artifact.conclusion")) : null,
|
|
5253
|
+
isConclusion && typeof artifact.chars === "number" ? h("span", { className: "dsov-badge", "data-tone": "quiet" }, t("artifact.conclusionChars", { n: artifact.chars })) : null,
|
|
4266
5254
|
h("span", { className: "dsov-rowTail" }, h("span", { className: "dsov-time", title: timeTitle(artifact.time) }, fmtTime(artifact.time)))
|
|
4267
5255
|
);
|
|
4268
5256
|
});
|
|
@@ -4271,6 +5259,7 @@ function FileTypeIcon(props) {
|
|
|
4271
5259
|
Section,
|
|
4272
5260
|
{
|
|
4273
5261
|
id: "artifacts",
|
|
5262
|
+
...sectionIntroProps(introSeq, 70),
|
|
4274
5263
|
icon: h(IconArtifact, { size: 15 }),
|
|
4275
5264
|
title: t("section.artifacts"),
|
|
4276
5265
|
badge: visibleArtifacts.length > 0 ? String(visibleArtifacts.length) : undefined,
|
|
@@ -4388,7 +5377,23 @@ function FileTypeIcon(props) {
|
|
|
4388
5377
|
return h(RefIcon, { kind: ref.kind, size: 14 });
|
|
4389
5378
|
}
|
|
4390
5379
|
|
|
4391
|
-
|
|
5380
|
+
/* per-session memory of reference keys whose entrance already animated
|
|
5381
|
+
(page lifetime): collapsing, scope switching and tab remounts never replay */
|
|
5382
|
+
const seenRefKeys = new Map();
|
|
5383
|
+
function seenRefKeysFor(sessionId) {
|
|
5384
|
+
const storeKey = typeof sessionId === "string" && sessionId !== "" ? sessionId : "-";
|
|
5385
|
+
let set = seenRefKeys.get(storeKey);
|
|
5386
|
+
if (set === undefined) {
|
|
5387
|
+
set = new Set();
|
|
5388
|
+
seenRefKeys.set(storeKey, set);
|
|
5389
|
+
}
|
|
5390
|
+
return set;
|
|
5391
|
+
}
|
|
5392
|
+
function refKeyOf(ref) {
|
|
5393
|
+
return isRecord(ref) && typeof ref.key === "string" && ref.key !== "" ? ref.key : `label:${isRecord(ref) ? ref.label : ""}`;
|
|
5394
|
+
}
|
|
5395
|
+
|
|
5396
|
+
function ReferencesSection({ summary, t, sessionId, asOf, introSeq }) {
|
|
4392
5397
|
const cfg = useConfig();
|
|
4393
5398
|
const asOfTime = typeof asOf === "number" && asOf > 0 ? asOf : null;
|
|
4394
5399
|
const allRefs = asOfTime === null ? summary.references : summary.references.filter((ref) => (ref.firstTime || ref.time || 0) <= asOfTime);
|
|
@@ -4396,6 +5401,11 @@ function FileTypeIcon(props) {
|
|
|
4396
5401
|
const timeTitle = (ts) => (cfg.timeMode === "elapsed" ? formatTime(ts) : undefined);
|
|
4397
5402
|
const sortMode = useRefSort();
|
|
4398
5403
|
const hideCommon = useHideCommonRefs();
|
|
5404
|
+
/* "all" spans the session (count turns); a turn view reports that turn's mentions. */
|
|
5405
|
+
const multiTurn = summary.scope === "all";
|
|
5406
|
+
/* "count" means turn coverage across the session, or mentions inside one turn. */
|
|
5407
|
+
const sortLabelOf = (mode) =>
|
|
5408
|
+
mode === "count" && multiTurn === false ? t("reference.sort.hits.title") : t(`reference.sort.${mode}.title`);
|
|
4399
5409
|
const typeFilter = useRefTypes();
|
|
4400
5410
|
const [revealState, setRevealState] = React.useState({});
|
|
4401
5411
|
const revealTimers = React.useRef({});
|
|
@@ -4432,7 +5442,32 @@ function FileTypeIcon(props) {
|
|
|
4432
5442
|
const scoped = hideCommon ? allRefs.filter((ref) => ref.ubiquitous !== true) : allRefs;
|
|
4433
5443
|
const selectedTypes = Array.isArray(typeFilter) ? typeFilter : [];
|
|
4434
5444
|
const filtered = selectedTypes.length === 0 ? scoped : scoped.filter((ref) => selectedTypes.indexOf(refDisplayType(ref)) !== -1);
|
|
4435
|
-
const sorted = sortReferences(filtered, sortMode);
|
|
5445
|
+
const sorted = sortReferences(filtered, sortMode, multiTurn);
|
|
5446
|
+
/* first-appearance animation: brand-new rows rise in with a small stagger;
|
|
5447
|
+
scrubbing the timeline never animates (rows would flicker in and out) */
|
|
5448
|
+
const seenRefs = seenRefKeysFor(sessionId);
|
|
5449
|
+
const enterDelays = new Map();
|
|
5450
|
+
if (asOfTime === null) {
|
|
5451
|
+
let batch = 0;
|
|
5452
|
+
for (const ref of sorted) {
|
|
5453
|
+
const refKey = refKeyOf(ref);
|
|
5454
|
+
if (seenRefs.has(refKey)) continue;
|
|
5455
|
+
enterDelays.set(refKey, Math.min(batch, 11) * 45);
|
|
5456
|
+
batch += 1;
|
|
5457
|
+
}
|
|
5458
|
+
}
|
|
5459
|
+
React.useEffect(() => {
|
|
5460
|
+
if (asOfTime !== null) {
|
|
5461
|
+
for (const ref of sorted) seenRefs.add(refKeyOf(ref));
|
|
5462
|
+
return undefined;
|
|
5463
|
+
}
|
|
5464
|
+
/* mark after the longest animation finished so quick re-renders keep the
|
|
5465
|
+
attribute (a CSS animation never restarts while the attribute stays) */
|
|
5466
|
+
const timer = setTimeout(() => {
|
|
5467
|
+
for (const ref of sorted) seenRefs.add(refKeyOf(ref));
|
|
5468
|
+
}, 900);
|
|
5469
|
+
return () => clearTimeout(timer);
|
|
5470
|
+
});
|
|
4436
5471
|
const rows = sorted.map((ref, index) => {
|
|
4437
5472
|
const detail = Array.isArray(ref.detail) ? ref.detail : [];
|
|
4438
5473
|
const shown = detail.slice(0, 8);
|
|
@@ -4449,7 +5484,19 @@ function FileTypeIcon(props) {
|
|
|
4449
5484
|
h("span", { className: "dsov-rowName" }, refTitle(ref, t)),
|
|
4450
5485
|
h("span", { className: "dsov-badge" }, t(KIND_KEYS[ref.kind] ?? "reference.kind.context")),
|
|
4451
5486
|
ref.form ? h("span", { className: "dsov-badge", "data-tone": "quiet" }, t(`reference.form.${ref.form}`) === `reference.form.${ref.form}` ? ref.form : t(`reference.form.${ref.form}`)) : null,
|
|
4452
|
-
|
|
5487
|
+
(multiTurn ? refTurnsOf(ref) > 1 : refHitsOf(ref) > 1)
|
|
5488
|
+
? h(
|
|
5489
|
+
"span",
|
|
5490
|
+
{
|
|
5491
|
+
className: "dsov-badge",
|
|
5492
|
+
"data-tone": "quiet",
|
|
5493
|
+
title: multiTurn
|
|
5494
|
+
? t("reference.turnsTitle", { turns: refTurnsOf(ref), times: refHitsOf(ref) })
|
|
5495
|
+
: t("reference.timesTitle", { times: refHitsOf(ref) }),
|
|
5496
|
+
},
|
|
5497
|
+
multiTurn ? t("reference.turns", { n: refTurnsOf(ref) }) : t("reference.times", { n: refHitsOf(ref) })
|
|
5498
|
+
)
|
|
5499
|
+
: null
|
|
4453
5500
|
),
|
|
4454
5501
|
shown.length > 0
|
|
4455
5502
|
? h(
|
|
@@ -4493,13 +5540,15 @@ function FileTypeIcon(props) {
|
|
|
4493
5540
|
h("span", { className: "dsov-time", key: "time", title: timeTitle(ref.time) }, fmtTime(ref.time)),
|
|
4494
5541
|
];
|
|
4495
5542
|
const isFile = ref.kind === "file";
|
|
5543
|
+
const enterDelay = enterDelays.get(refKeyOf(ref));
|
|
5544
|
+
const enterProps = enterDelay === undefined ? { key: `ref-${refKeyOf(ref)}` } : { key: `ref-${refKeyOf(ref)}`, "data-enter": "true", style: { animationDelay: `${enterDelay}ms` } };
|
|
4496
5545
|
if (isFile) {
|
|
4497
5546
|
return h(
|
|
4498
5547
|
"button",
|
|
4499
5548
|
{
|
|
4500
5549
|
type: "button",
|
|
4501
5550
|
className: "dsov-row",
|
|
4502
|
-
|
|
5551
|
+
...enterProps,
|
|
4503
5552
|
title: ref.label,
|
|
4504
5553
|
onClick: () => openSessionFile(sessionId, ref.label),
|
|
4505
5554
|
},
|
|
@@ -4512,7 +5561,7 @@ function FileTypeIcon(props) {
|
|
|
4512
5561
|
{
|
|
4513
5562
|
type: "button",
|
|
4514
5563
|
className: "dsov-row",
|
|
4515
|
-
|
|
5564
|
+
...enterProps,
|
|
4516
5565
|
title: ref.uri,
|
|
4517
5566
|
onClick: () => openVikingPreview(ref.uri),
|
|
4518
5567
|
},
|
|
@@ -4525,7 +5574,7 @@ function FileTypeIcon(props) {
|
|
|
4525
5574
|
{
|
|
4526
5575
|
type: "button",
|
|
4527
5576
|
className: "dsov-row",
|
|
4528
|
-
|
|
5577
|
+
...enterProps,
|
|
4529
5578
|
title: t("reference.previewHint"),
|
|
4530
5579
|
onClick: () => openContextPreview(ref.previewKey),
|
|
4531
5580
|
},
|
|
@@ -4534,7 +5583,7 @@ function FileTypeIcon(props) {
|
|
|
4534
5583
|
}
|
|
4535
5584
|
return h(
|
|
4536
5585
|
"div",
|
|
4537
|
-
{ className: "dsov-row dsov-rowStatic",
|
|
5586
|
+
{ className: "dsov-row dsov-rowStatic", ...enterProps, title: isVikingUri(ref.uri) ? ref.uri : ref.label },
|
|
4538
5587
|
...content
|
|
4539
5588
|
);
|
|
4540
5589
|
});
|
|
@@ -4571,6 +5620,7 @@ function FileTypeIcon(props) {
|
|
|
4571
5620
|
Section,
|
|
4572
5621
|
{
|
|
4573
5622
|
id: "references",
|
|
5623
|
+
...sectionIntroProps(introSeq, 140),
|
|
4574
5624
|
icon: h(IconReference, { size: 15 }),
|
|
4575
5625
|
title: t("section.references"),
|
|
4576
5626
|
badge: filtered.length > 0 ? String(filtered.length) : undefined,
|
|
@@ -4608,10 +5658,11 @@ function FileTypeIcon(props) {
|
|
|
4608
5658
|
key: mode,
|
|
4609
5659
|
className: "dsov-segBtn",
|
|
4610
5660
|
"data-active": sortMode === mode ? "true" : "false",
|
|
4611
|
-
title:
|
|
5661
|
+
title:
|
|
5662
|
+
(sortMode === mode ? `${sortLabelOf(mode)} · ${t("reference.sort.cancel")}` : sortLabelOf(mode)),
|
|
4612
5663
|
onClick: () => setRefSort(sortMode === mode ? "" : mode),
|
|
4613
5664
|
},
|
|
4614
|
-
t(`reference.sort.${mode}`)
|
|
5665
|
+
mode === "count" && multiTurn === false ? t("reference.sort.hits") : t(`reference.sort.${mode}`)
|
|
4615
5666
|
)
|
|
4616
5667
|
)
|
|
4617
5668
|
)
|
|
@@ -4646,6 +5697,19 @@ function FileTypeIcon(props) {
|
|
|
4646
5697
|
const [errPop, setErrPop] = React.useState(null);
|
|
4647
5698
|
const errOpen = isRecord(errPop);
|
|
4648
5699
|
const errRef = React.useRef(null);
|
|
5700
|
+
const errBtnRef = React.useRef(null);
|
|
5701
|
+
/* Place the fixed popover under its pill, clamped into the viewport. */
|
|
5702
|
+
const placePopover = React.useCallback(() => {
|
|
5703
|
+
const button = errBtnRef.current;
|
|
5704
|
+
if (button === null || button.isConnected !== true) return false;
|
|
5705
|
+
const rect = button.getBoundingClientRect();
|
|
5706
|
+
if (rect.bottom < 0 || rect.top > window.innerHeight) return false;
|
|
5707
|
+
setErrPop({
|
|
5708
|
+
top: Math.min(rect.bottom + 5, Math.max(8, window.innerHeight - 292)),
|
|
5709
|
+
right: Math.max(8, window.innerWidth - rect.right),
|
|
5710
|
+
});
|
|
5711
|
+
return true;
|
|
5712
|
+
}, []);
|
|
4649
5713
|
React.useEffect(() => {
|
|
4650
5714
|
if (!errOpen) return undefined;
|
|
4651
5715
|
const close = () => setErrPop(null);
|
|
@@ -4655,17 +5719,30 @@ function FileTypeIcon(props) {
|
|
|
4655
5719
|
const onKey = (event) => {
|
|
4656
5720
|
if (event.key === "Escape") close();
|
|
4657
5721
|
};
|
|
5722
|
+
/* Following the anchor instead of dismissing: the chat auto-scrolls on every
|
|
5723
|
+
streamed event, so closing on scroll made the popover impossible to read
|
|
5724
|
+
while a turn was running. Reposition, and only close once the pill itself
|
|
5725
|
+
left the viewport (or unmounted). */
|
|
5726
|
+
let frame = 0;
|
|
5727
|
+
const follow = () => {
|
|
5728
|
+
if (frame !== 0) return;
|
|
5729
|
+
frame = requestAnimationFrame(() => {
|
|
5730
|
+
frame = 0;
|
|
5731
|
+
if (!placePopover()) setErrPop(null);
|
|
5732
|
+
});
|
|
5733
|
+
};
|
|
4658
5734
|
document.addEventListener("mousedown", onDown);
|
|
4659
5735
|
document.addEventListener("keydown", onKey);
|
|
4660
|
-
window.addEventListener("scroll",
|
|
4661
|
-
window.addEventListener("resize",
|
|
5736
|
+
window.addEventListener("scroll", follow, true);
|
|
5737
|
+
window.addEventListener("resize", follow);
|
|
4662
5738
|
return () => {
|
|
5739
|
+
if (frame !== 0) cancelAnimationFrame(frame);
|
|
4663
5740
|
document.removeEventListener("mousedown", onDown);
|
|
4664
5741
|
document.removeEventListener("keydown", onKey);
|
|
4665
|
-
window.removeEventListener("scroll",
|
|
4666
|
-
window.removeEventListener("resize",
|
|
5742
|
+
window.removeEventListener("scroll", follow, true);
|
|
5743
|
+
window.removeEventListener("resize", follow);
|
|
4667
5744
|
};
|
|
4668
|
-
}, [errOpen]);
|
|
5745
|
+
}, [errOpen, placePopover]);
|
|
4669
5746
|
if (!isRecord(stats)) return null;
|
|
4670
5747
|
const duration = typeof stats.durationMs === "number" ? stats.durationMs : 0;
|
|
4671
5748
|
const toolCalls = typeof stats.toolCalls === "number" ? stats.toolCalls : 0;
|
|
@@ -4720,20 +5797,17 @@ function FileTypeIcon(props) {
|
|
|
4720
5797
|
className: "dsov-stat dsov-statBtn",
|
|
4721
5798
|
"data-tone": "err",
|
|
4722
5799
|
"data-active": errOpen ? "true" : undefined,
|
|
5800
|
+
ref: errBtnRef,
|
|
4723
5801
|
disabled: failed.length === 0,
|
|
4724
5802
|
title: failed.length > 0 ? t("stats.errors.popTitle") : t("stats.errors.title"),
|
|
4725
5803
|
onClick:
|
|
4726
5804
|
failed.length > 0
|
|
4727
|
-
? (
|
|
5805
|
+
? () => {
|
|
4728
5806
|
if (errOpen) {
|
|
4729
5807
|
setErrPop(null);
|
|
4730
5808
|
return;
|
|
4731
5809
|
}
|
|
4732
|
-
|
|
4733
|
-
setErrPop({
|
|
4734
|
-
top: Math.min(rect.bottom + 5, Math.max(8, window.innerHeight - 292)),
|
|
4735
|
-
right: Math.max(8, window.innerWidth - rect.right),
|
|
4736
|
-
});
|
|
5810
|
+
if (!placePopover()) setErrPop(null);
|
|
4737
5811
|
}
|
|
4738
5812
|
: undefined,
|
|
4739
5813
|
},
|
|
@@ -4777,6 +5851,202 @@ function FileTypeIcon(props) {
|
|
|
4777
5851
|
);
|
|
4778
5852
|
}
|
|
4779
5853
|
|
|
5854
|
+
/** Which side of a subagent lineage the bound session is on, read from catalog
|
|
5855
|
+
metadata only (never an Agent-bound RPC). */
|
|
5856
|
+
function sessionRoleOf(sessionId) {
|
|
5857
|
+
const empty = { kind: "main", parentId: null, parentTitle: "", mode: "", children: [] };
|
|
5858
|
+
if (typeof sessionId !== "string" || sessionId === "") return empty;
|
|
5859
|
+
let ctx = null;
|
|
5860
|
+
try {
|
|
5861
|
+
ctx = runtime === null ? null : runtime.ctx;
|
|
5862
|
+
} catch {
|
|
5863
|
+
ctx = null;
|
|
5864
|
+
}
|
|
5865
|
+
let snapshot = null;
|
|
5866
|
+
try {
|
|
5867
|
+
snapshot = ctx === null || ctx === undefined ? null : ctx.sessions.list.getSnapshot();
|
|
5868
|
+
} catch {
|
|
5869
|
+
snapshot = null;
|
|
5870
|
+
}
|
|
5871
|
+
let address = null;
|
|
5872
|
+
try {
|
|
5873
|
+
address = ctx === null || ctx === undefined ? null : ctx.sessions.subagentAddress(sessionId);
|
|
5874
|
+
} catch {
|
|
5875
|
+
address = null;
|
|
5876
|
+
}
|
|
5877
|
+
const byId = isRecord(snapshot) && isRecord(snapshot.byId) ? snapshot.byId : {};
|
|
5878
|
+
const self = isRecord(byId[sessionId]) ? byId[sessionId] : null;
|
|
5879
|
+
const entryTitle = (entry, fallback) =>
|
|
5880
|
+
entry !== null && typeof entry.title === "string" && entry.title !== ""
|
|
5881
|
+
? entry.title
|
|
5882
|
+
: entry !== null && typeof entry.displayTitle === "string" && entry.displayTitle !== ""
|
|
5883
|
+
? entry.displayTitle
|
|
5884
|
+
: fallback;
|
|
5885
|
+
const entryParent = (entry) =>
|
|
5886
|
+
typeof entry.parentSessionId === "string" && entry.parentSessionId !== ""
|
|
5887
|
+
? entry.parentSessionId
|
|
5888
|
+
: typeof entry.parentId === "string" && entry.parentId !== ""
|
|
5889
|
+
? entry.parentId
|
|
5890
|
+
: null;
|
|
5891
|
+
/* `origin: "subagent"` is the durable marker. A branched/forked conversation
|
|
5892
|
+
also carries a parentSessionId (and the host even hands out a subagent
|
|
5893
|
+
address for it), but it is a main session with lineage — not a subagent. */
|
|
5894
|
+
const isSubagent = self !== null && self.origin === "subagent";
|
|
5895
|
+
const parentId = isSubagent
|
|
5896
|
+
? (isRecord(address) && typeof address.parentSessionId === "string" && address.parentSessionId !== "" ? address.parentSessionId : entryParent(self))
|
|
5897
|
+
: null;
|
|
5898
|
+
const parent = parentId !== null && isRecord(byId[parentId]) ? byId[parentId] : null;
|
|
5899
|
+
const ids = isRecord(snapshot) && Array.isArray(snapshot.ids) ? snapshot.ids : [];
|
|
5900
|
+
const children = [];
|
|
5901
|
+
const seen = new Set();
|
|
5902
|
+
for (const id of ids) {
|
|
5903
|
+
const entry = isRecord(byId[id]) ? byId[id] : null;
|
|
5904
|
+
if (entry === null || entry.origin !== "subagent" || entryParent(entry) !== sessionId) continue;
|
|
5905
|
+
seen.add(id);
|
|
5906
|
+
children.push({ id, title: entryTitle(entry, id), running: entry.running === true });
|
|
5907
|
+
}
|
|
5908
|
+
/* A child can exist in the parent's catalog projection before the list is
|
|
5909
|
+
projected; the catalog is the same metadata the host itself reads. */
|
|
5910
|
+
const projections = isRecord(snapshot) && isRecord(snapshot.projectionsBySession) ? snapshot.projectionsBySession : null;
|
|
5911
|
+
const parentProjection = projections !== null && isRecord(projections[sessionId]) ? projections[sessionId] : null;
|
|
5912
|
+
const catalog = parentProjection !== null && isRecord(parentProjection.values) && Array.isArray(parentProjection.values.subagentCatalog) ? parentProjection.values.subagentCatalog : [];
|
|
5913
|
+
for (const entry of catalog) {
|
|
5914
|
+
if (!isRecord(entry) || typeof entry.id !== "string" || entry.id === "" || seen.has(entry.id)) continue;
|
|
5915
|
+
if (entry.mode === "external") continue;
|
|
5916
|
+
seen.add(entry.id);
|
|
5917
|
+
const listed = isRecord(byId[entry.id]) ? byId[entry.id] : null;
|
|
5918
|
+
const label = typeof entry.title === "string" && entry.title !== "" ? entry.title : typeof entry.label === "string" ? entry.label : "";
|
|
5919
|
+
children.push({
|
|
5920
|
+
id: entry.id,
|
|
5921
|
+
title:
|
|
5922
|
+
listed !== null && typeof listed.displayTitle === "string" && listed.displayTitle !== ""
|
|
5923
|
+
? listed.displayTitle
|
|
5924
|
+
: label !== ""
|
|
5925
|
+
? label
|
|
5926
|
+
: entry.id,
|
|
5927
|
+
running: listed !== null ? listed.running === true : false,
|
|
5928
|
+
});
|
|
5929
|
+
}
|
|
5930
|
+
if (parentId === null) return { ...empty, children };
|
|
5931
|
+
return {
|
|
5932
|
+
kind: "sub",
|
|
5933
|
+
parentId,
|
|
5934
|
+
parentTitle: parent !== null ? entryTitle(parent, "") : "",
|
|
5935
|
+
mode: isRecord(address) && typeof address.mode === "string" ? address.mode : "",
|
|
5936
|
+
children,
|
|
5937
|
+
};
|
|
5938
|
+
}
|
|
5939
|
+
|
|
5940
|
+
/** Whether the workspace service can move the main conversation to another session. */
|
|
5941
|
+
function canNavigateSession() {
|
|
5942
|
+
try {
|
|
5943
|
+
const ctx = runtime === null ? null : runtime.ctx;
|
|
5944
|
+
const workspace = ctx === null || ctx === undefined ? null : ctx.uiWorkspace;
|
|
5945
|
+
return isRecord(workspace) && typeof workspace.openSession === "function";
|
|
5946
|
+
} catch {
|
|
5947
|
+
return false;
|
|
5948
|
+
}
|
|
5949
|
+
}
|
|
5950
|
+
|
|
5951
|
+
function openSessionById(sessionId) {
|
|
5952
|
+
if (typeof sessionId !== "string" || sessionId === "") return false;
|
|
5953
|
+
try {
|
|
5954
|
+
const ctx = runtime === null ? null : runtime.ctx;
|
|
5955
|
+
const workspace = ctx === null || ctx === undefined ? null : ctx.uiWorkspace;
|
|
5956
|
+
if (!isRecord(workspace) || typeof workspace.openSession !== "function") return false;
|
|
5957
|
+
workspace.openSession(sessionId);
|
|
5958
|
+
return true;
|
|
5959
|
+
} catch {
|
|
5960
|
+
return false;
|
|
5961
|
+
}
|
|
5962
|
+
}
|
|
5963
|
+
|
|
5964
|
+
/* Whether this session's visible scope actually involved subagents: agent-team
|
|
5965
|
+
members, subagent-catalog records, or real subagent/spawn tool calls. A
|
|
5966
|
+
context-compaction milestone also carries a teamKey, so teamKey alone is not
|
|
5967
|
+
evidence (that used to announce a subagent in single-agent conversations). */
|
|
5968
|
+
function subagentWorkInScope(summary) {
|
|
5969
|
+
const team = isRecord(summary) && isRecord(summary.team) ? summary.team : null;
|
|
5970
|
+
if (team !== null && Array.isArray(team.members) && team.members.length > 0) return true;
|
|
5971
|
+
if (team !== null && Array.isArray(team.subagents) && team.subagents.length > 0) return true;
|
|
5972
|
+
const steps = isRecord(summary) && Array.isArray(summary.stepsAll) ? summary.stepsAll : [];
|
|
5973
|
+
return steps.some(
|
|
5974
|
+
(step) =>
|
|
5975
|
+
isRecord(step) &&
|
|
5976
|
+
(step.name === "subagent" || step.name === "subagent_fork" || step.name === "spawn_teammate" || step.name === "team_task_create")
|
|
5977
|
+
);
|
|
5978
|
+
}
|
|
5979
|
+
|
|
5980
|
+
/** One compact line that says whether this overview belongs to the main agent or to
|
|
5981
|
+
a subagent, and offers to switch to the other side of that lineage. */
|
|
5982
|
+
function SessionRoleBar({ t, sessionId, hasWork }) {
|
|
5983
|
+
const role = sessionRoleOf(sessionId);
|
|
5984
|
+
const [open, setOpen] = React.useState(false);
|
|
5985
|
+
const canSwitch = canNavigateSession();
|
|
5986
|
+
const children =
|
|
5987
|
+
role.children.length === 0
|
|
5988
|
+
? null
|
|
5989
|
+
: h(
|
|
5990
|
+
"button",
|
|
5991
|
+
{
|
|
5992
|
+
type: "button",
|
|
5993
|
+
className: "dsov-roleBtn",
|
|
5994
|
+
"data-open": open ? "true" : "false",
|
|
5995
|
+
title: t("role.childrenToggle"),
|
|
5996
|
+
onClick: () => setOpen((value) => !value),
|
|
5997
|
+
},
|
|
5998
|
+
t("role.children", { n: role.children.length }),
|
|
5999
|
+
h(IconChevron, { size: 11, className: "dsov-roleChevron" })
|
|
6000
|
+
);
|
|
6001
|
+
const list =
|
|
6002
|
+
!open || role.children.length === 0
|
|
6003
|
+
? null
|
|
6004
|
+
: h(
|
|
6005
|
+
"div",
|
|
6006
|
+
{ className: "dsov-roleList" },
|
|
6007
|
+
role.children.map((child) =>
|
|
6008
|
+
h(
|
|
6009
|
+
"button",
|
|
6010
|
+
{
|
|
6011
|
+
type: "button",
|
|
6012
|
+
className: "dsov-roleItem",
|
|
6013
|
+
key: child.id,
|
|
6014
|
+
title: canSwitch ? t("role.switchTitle") : child.title,
|
|
6015
|
+
disabled: !canSwitch,
|
|
6016
|
+
onClick: canSwitch ? () => openSessionById(child.id) : undefined,
|
|
6017
|
+
},
|
|
6018
|
+
h("span", { className: "dsov-roleDot", "data-running": child.running ? "true" : "false" }),
|
|
6019
|
+
h("span", { className: "dsov-roleItemTitle" }, child.title)
|
|
6020
|
+
)
|
|
6021
|
+
)
|
|
6022
|
+
);
|
|
6023
|
+
if (role.kind === "sub") {
|
|
6024
|
+
const parentName = role.parentTitle !== "" ? role.parentTitle : role.parentId;
|
|
6025
|
+
return h(
|
|
6026
|
+
"div",
|
|
6027
|
+
{ className: "dsov-role", "data-kind": "sub" },
|
|
6028
|
+
h("span", { className: "dsov-roleBadge" }, t("role.subagent")),
|
|
6029
|
+
role.mode !== "" && role.mode !== "unknown" ? h("span", { className: "dsov-roleMode" }, role.mode) : null,
|
|
6030
|
+
canSwitch
|
|
6031
|
+
? h(
|
|
6032
|
+
"button",
|
|
6033
|
+
{ type: "button", className: "dsov-roleBtn", "data-link": "true", title: t("role.openParentTitle"), onClick: () => openSessionById(role.parentId) },
|
|
6034
|
+
t("role.openParent"),
|
|
6035
|
+
h("span", { className: "dsov-roleParent" }, parentName)
|
|
6036
|
+
)
|
|
6037
|
+
: h("span", { className: "dsov-roleHint" }, t("role.openParent"), h("span", { className: "dsov-roleParent" }, parentName)),
|
|
6038
|
+
children,
|
|
6039
|
+
list
|
|
6040
|
+
);
|
|
6041
|
+
}
|
|
6042
|
+
/* A plain main session with no subagents in this scope has no identity to
|
|
6043
|
+
announce: the badge, the child list, and the parent link are all noise. */
|
|
6044
|
+
/* A finished child session from an older turn is lineage, not status: only an
|
|
6045
|
+
actually running subagent (or subagent work in the visible scope) is announced. */
|
|
6046
|
+
if (role.children.length === 0 || hasWork === false) return null;
|
|
6047
|
+
return h("div", { className: "dsov-role", "data-kind": "main" }, h("span", { className: "dsov-roleBadge" }, t("role.main")), children, list);
|
|
6048
|
+
}
|
|
6049
|
+
|
|
4780
6050
|
function OverviewContent({ binding, t, variant }) {
|
|
4781
6051
|
const [selection, setSelection] = React.useState("latest");
|
|
4782
6052
|
/* timeline scrubbing: a null asOf always means "live/latest state" */
|
|
@@ -4796,11 +6066,41 @@ function FileTypeIcon(props) {
|
|
|
4796
6066
|
setAsOf(null);
|
|
4797
6067
|
}, [selection]);
|
|
4798
6068
|
const summary = useSummary(binding, selection);
|
|
4799
|
-
|
|
6069
|
+
/* 「重新加载」restarts the backfill loop from zero — the escape hatch when the
|
|
6070
|
+
host loader silently no-ops (another loader holds the lock) or a page fails. */
|
|
6071
|
+
const [historyRetry, setHistoryRetry] = React.useState(0);
|
|
6072
|
+
const historyStalled = useBackfill(binding, summary.hasMore, historyRetry);
|
|
4800
6073
|
const goal = useGoal(binding);
|
|
4801
6074
|
const stats = useArtifactStats(binding, summary.changeTurns);
|
|
4802
6075
|
const cfg = useConfig();
|
|
4803
6076
|
const sessionId = binding ? binding.sessionId : undefined;
|
|
6077
|
+
/* 本轮结论 as a document: only a turn this overview actually watched finish
|
|
6078
|
+
pops its closing reply into a tab, once. A conclusion replayed from history
|
|
6079
|
+
on mount/refresh — even when it arrives in a late flush after the first
|
|
6080
|
+
paint — is never opened. */
|
|
6081
|
+
const sessionRunning = useSessionRunning(binding ? binding.sessionId : undefined);
|
|
6082
|
+
const conclusionGuard = React.useRef({ ready: false, opened: new Set(), sawRunning: false });
|
|
6083
|
+
React.useEffect(() => {
|
|
6084
|
+
if (sessionRunning === true) conclusionGuard.current.sawRunning = true;
|
|
6085
|
+
}, [sessionRunning]);
|
|
6086
|
+
React.useEffect(() => {
|
|
6087
|
+
const conclusion = isRecord(summary.conclusion) ? summary.conclusion : null;
|
|
6088
|
+
const guard = conclusionGuard.current;
|
|
6089
|
+
const key = conclusionAutoOpenKey(guard, conclusion, sessionId, guard.sawRunning);
|
|
6090
|
+
if (key === null) return;
|
|
6091
|
+
if (cfg.conclusionTab !== true) return;
|
|
6092
|
+
if (typeof conclusion.previewKey !== "string" || conclusion.previewKey === "") return;
|
|
6093
|
+
if (contextSidebar() === null) return;
|
|
6094
|
+
if (!claimConclusionTab(key)) return;
|
|
6095
|
+
/* a failed open must not eat the claim: another instance may still show the tab */
|
|
6096
|
+
const entry = readContextPreview(conclusion.previewKey);
|
|
6097
|
+
const docKey = `conclusion:${typeof conclusion.turn === "number" ? conclusion.turn : conclusion.previewKey}`;
|
|
6098
|
+
const opened = openContextPreview(conclusion.previewKey) || openNativeDocument(sessionId, docKey, conclusion.title, entry === null ? undefined : entry.text);
|
|
6099
|
+
if (!opened) conclusionAutoOpened.delete(key);
|
|
6100
|
+
}, [summary, cfg.conclusionTab, sessionId, sessionRunning]);
|
|
6101
|
+
/* Agent identity only earns its row when a subagent actually took part in the
|
|
6102
|
+
visible scope (see subagentWorkInScope). */
|
|
6103
|
+
const roleWork = React.useMemo(() => subagentWorkInScope(summary), [summary]);
|
|
4804
6104
|
const rootClass = variant === "panel" ? "dsov-panel" : "dsov-view";
|
|
4805
6105
|
const rootRef = React.useRef(null);
|
|
4806
6106
|
const scope = summary.scope === selection || selection === "latest" ? summary.scope : selection;
|
|
@@ -4835,6 +6135,29 @@ function FileTypeIcon(props) {
|
|
|
4835
6135
|
errors: list.filter((step) => step.error === true && (step.endTime || step.time || 0) <= asOf).length,
|
|
4836
6136
|
};
|
|
4837
6137
|
}, [asOf, summary]);
|
|
6138
|
+
/* A new turn gets a staged entrance for the three sections plus a short
|
|
6139
|
+
「第 N 轮对话开始」 cue. First paint of an already-running turn stays silent,
|
|
6140
|
+
and nothing replays on re-renders, scope switches or tab remounts. */
|
|
6141
|
+
const [introSeq, setIntroSeq] = React.useState(0);
|
|
6142
|
+
const [introBanner, setIntroBanner] = React.useState(null);
|
|
6143
|
+
const introTurn = React.useRef(null);
|
|
6144
|
+
React.useEffect(() => {
|
|
6145
|
+
const turn = Number.isFinite(summary.latestTurn) ? summary.latestTurn : null;
|
|
6146
|
+
if (turn === null || asOf !== null) return;
|
|
6147
|
+
if (introTurn.current === null) {
|
|
6148
|
+
introTurn.current = turn;
|
|
6149
|
+
return;
|
|
6150
|
+
}
|
|
6151
|
+
if (turn === introTurn.current) return;
|
|
6152
|
+
introTurn.current = turn;
|
|
6153
|
+
setIntroSeq((value) => value + 1);
|
|
6154
|
+
setIntroBanner(turn);
|
|
6155
|
+
}, [summary.latestTurn, asOf]);
|
|
6156
|
+
React.useEffect(() => {
|
|
6157
|
+
if (introBanner === null) return undefined;
|
|
6158
|
+
const timer = setTimeout(() => setIntroBanner(null), 2200);
|
|
6159
|
+
return () => clearTimeout(timer);
|
|
6160
|
+
}, [introBanner, introSeq]);
|
|
4838
6161
|
const canTimeline = typeof summary.turnStart === "number" && typeof summary.turnEnd === "number" && summary.turnEnd > summary.turnStart;
|
|
4839
6162
|
/* the errors pill drills into these: failed steps of the scope, honoring the scrub point */
|
|
4840
6163
|
const failedSteps = React.useMemo(() => {
|
|
@@ -5013,10 +6336,40 @@ function FileTypeIcon(props) {
|
|
|
5013
6336
|
),
|
|
5014
6337
|
timelineRow
|
|
5015
6338
|
),
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
6339
|
+
binding && typeof binding.sessionId === "string" && binding.sessionId !== "" ? h(SessionRoleBar, { key: "roleBar", t, sessionId: binding.sessionId, hasWork: roleWork }) : null,
|
|
6340
|
+
summary.hasMore
|
|
6341
|
+
? h(
|
|
6342
|
+
"div",
|
|
6343
|
+
{ className: "dsov-loading" },
|
|
6344
|
+
h("span", { className: "dsov-loadingText" }, t(historyStalled ? "state.loadingStalled" : "state.loadingHistory")),
|
|
6345
|
+
h(
|
|
6346
|
+
"button",
|
|
6347
|
+
{
|
|
6348
|
+
type: "button",
|
|
6349
|
+
className: "dsov-loadingReload",
|
|
6350
|
+
title: t("state.reloadHistory.title"),
|
|
6351
|
+
onClick: () => {
|
|
6352
|
+
/* Re-arming alone is not enough: the loop would issue the same
|
|
6353
|
+
call that just no-oped. Pull one page directly first, using the
|
|
6354
|
+
seq-free loader when available, then let the loop take over. */
|
|
6355
|
+
const session = binding ? binding.session : undefined;
|
|
6356
|
+
try {
|
|
6357
|
+
if (isRecord(session) && typeof session.loadOlder === "function") Promise.resolve(session.loadOlder()).catch(() => {});
|
|
6358
|
+
else if (isRecord(session) && typeof session.loadThrough === "function") Promise.resolve(session.loadThrough(0)).catch(() => {});
|
|
6359
|
+
} catch {
|
|
6360
|
+
/* the re-arm below still gives the loop another chance */
|
|
6361
|
+
}
|
|
6362
|
+
setHistoryRetry((value) => value + 1);
|
|
6363
|
+
},
|
|
6364
|
+
},
|
|
6365
|
+
t("state.reloadHistory")
|
|
6366
|
+
)
|
|
6367
|
+
)
|
|
6368
|
+
: null,
|
|
6369
|
+
introBanner !== null ? h("div", { className: "dsov-turnStart", key: `turnStart-${introSeq}` }, h("span", { className: "dsov-turnStartDot" }), t("intro.newTurn", { n: introBanner })) : null,
|
|
6370
|
+
cfg.showProgress ? h(ProgressSection, { summary, goal, t, waiting: sessionWaiting && asOf === null, cfg, asOf, introSeq }) : null,
|
|
6371
|
+
cfg.showArtifacts ? h(ArtifactsSection, { summary, t, binding, stats, asOf, introSeq }) : null,
|
|
6372
|
+
cfg.showReferences ? h(ReferencesSection, { summary, t, sessionId, asOf, introSeq }) : null
|
|
5020
6373
|
);
|
|
5021
6374
|
}
|
|
5022
6375
|
|
|
@@ -5200,7 +6553,7 @@ function FileTypeIcon(props) {
|
|
|
5200
6553
|
view.status === "ready" && view.mode === "file" && source === true ? h("pre", { className: "dsov-vikingSource" }, view.text) : null,
|
|
5201
6554
|
view.status === "ready" && view.mode === "file" && source !== true && parsed ? renderFrontmatterFields(parsed, t) : null,
|
|
5202
6555
|
view.status === "ready" && view.mode === "file" && source !== true && parsed
|
|
5203
|
-
? h(
|
|
6556
|
+
? h(MarkdownBody, { text: parsed.body })
|
|
5204
6557
|
: null
|
|
5205
6558
|
);
|
|
5206
6559
|
}
|
|
@@ -5308,24 +6661,56 @@ function FileTypeIcon(props) {
|
|
|
5308
6661
|
if (entry === null) {
|
|
5309
6662
|
return h("div", { className: "dsov-viking" }, h("div", { className: "dsov-vikingError" }, t("context.unavailable")));
|
|
5310
6663
|
}
|
|
6664
|
+
if (entry.kind === "conclusion") {
|
|
6665
|
+
/* Just the turn's closing answer, rendered exactly as the chat renders it;
|
|
6666
|
+
the page geometry mirrors the host's own Markdown document page so the
|
|
6667
|
+
fallback tab reads the same as the banner-opened one. */
|
|
6668
|
+
return h(
|
|
6669
|
+
"section",
|
|
6670
|
+
{ className: "dsov-docPage", "data-doc-preview": "conclusion", "aria-label": entry.title },
|
|
6671
|
+
h(
|
|
6672
|
+
"div",
|
|
6673
|
+
{ className: "dsov-docDocument" },
|
|
6674
|
+
ChatMarkdown
|
|
6675
|
+
? h(ChatMarkdown, { text: entry.text, labels: chatMarkdownLabels(t) })
|
|
6676
|
+
: h(MarkdownBody, { text: entry.text })
|
|
6677
|
+
)
|
|
6678
|
+
);
|
|
6679
|
+
}
|
|
5311
6680
|
return h(
|
|
5312
6681
|
"div",
|
|
5313
6682
|
{ className: "dsov-viking" },
|
|
5314
6683
|
h(
|
|
5315
6684
|
"div",
|
|
5316
6685
|
{ className: "dsov-vikingHead" },
|
|
5317
|
-
h("span", { className: "dsov-vikingIcon" }, h(IconReference, { size: 15 })),
|
|
6686
|
+
h("span", { className: "dsov-vikingIcon" }, entry.kind === "plan" ? h(IconPlan, { size: 15 }) : h(IconReference, { size: 15 })),
|
|
5318
6687
|
h(
|
|
5319
6688
|
"span",
|
|
5320
6689
|
{ className: "dsov-vikingHeadText" },
|
|
5321
6690
|
h("span", { className: "dsov-vikingName" }, entry.title),
|
|
5322
|
-
h(
|
|
6691
|
+
h(
|
|
6692
|
+
"span",
|
|
6693
|
+
{ className: "dsov-vikingPathText" },
|
|
6694
|
+
entry.kind === "plan" ? t("context.subtitlePlan") : t("context.subtitle")
|
|
6695
|
+
)
|
|
5323
6696
|
)
|
|
5324
6697
|
),
|
|
5325
|
-
h(
|
|
6698
|
+
h(MarkdownBody, { text: entry.text })
|
|
5326
6699
|
);
|
|
5327
6700
|
}
|
|
5328
6701
|
|
|
6702
|
+
/** Tab chip for a captured snapshot: the file glyph the host's document tabs carry. */
|
|
6703
|
+
function ContextTabTitle(props) {
|
|
6704
|
+
const t = typeof props.t === "function" ? props.t : translate;
|
|
6705
|
+
const info = typeof props.useTabInfo === "function" ? props.useTabInfo() : null;
|
|
6706
|
+
const key = info && isRecord(info.tab) ? parseContextAddress(info.tab.contentId) : undefined;
|
|
6707
|
+
const entry = key === undefined ? null : readContextPreview(key);
|
|
6708
|
+
const title = entry !== null ? entry.title : t("context.tab");
|
|
6709
|
+
/* only documents carry the glyph; reference snapshots keep a plain caption */
|
|
6710
|
+
if (entry === null || (entry.kind !== "conclusion" && entry.kind !== "plan")) return title;
|
|
6711
|
+
return h(React.Fragment, null, h(FileTypeIcon, { kind: "other", size: 16, className: "dsov-docTitleIcon" }), title);
|
|
6712
|
+
}
|
|
6713
|
+
|
|
5329
6714
|
/* ------------------------------------------------------------------ */
|
|
5330
6715
|
/* Settings form (settings page + Plugins row config slot) */
|
|
5331
6716
|
/* ------------------------------------------------------------------ */
|
|
@@ -5380,7 +6765,7 @@ function FileTypeIcon(props) {
|
|
|
5380
6765
|
}
|
|
5381
6766
|
|
|
5382
6767
|
/* dsh-context style settings rows: label left, pill-select control right. */
|
|
5383
|
-
function SettingSelect({ label, value, options, onPick }) {
|
|
6768
|
+
function SettingSelect({ label, value, options, onPick, hint }) {
|
|
5384
6769
|
const [open, setOpen] = React.useState(false);
|
|
5385
6770
|
const rootRef = React.useRef(null);
|
|
5386
6771
|
React.useEffect(() => {
|
|
@@ -5396,7 +6781,7 @@ function FileTypeIcon(props) {
|
|
|
5396
6781
|
return h(
|
|
5397
6782
|
"div",
|
|
5398
6783
|
{ className: "dsov-set-row" },
|
|
5399
|
-
h("span", { className: "dsov-set-label" }, label),
|
|
6784
|
+
h("span", { className: "dsov-set-label" }, h("span", { className: "dsov-set-labelText" }, label), hint ?? null),
|
|
5400
6785
|
h(
|
|
5401
6786
|
"div",
|
|
5402
6787
|
{ className: "dsov-set-control", ref: rootRef },
|
|
@@ -5489,7 +6874,7 @@ function FileTypeIcon(props) {
|
|
|
5489
6874
|
return h(
|
|
5490
6875
|
"div",
|
|
5491
6876
|
{ className: "dsov-set-row dsov-set-arearow" },
|
|
5492
|
-
h("span", { className: "dsov-set-label" }, label),
|
|
6877
|
+
label === "" ? null : h("span", { className: "dsov-set-label" }, label),
|
|
5493
6878
|
h("textarea", {
|
|
5494
6879
|
className: "dsov-set-textarea",
|
|
5495
6880
|
rows: 7,
|
|
@@ -5511,6 +6896,7 @@ function FileTypeIcon(props) {
|
|
|
5511
6896
|
function SettingsForm(props) {
|
|
5512
6897
|
const t = typeof props.t === "function" ? props.t : translate;
|
|
5513
6898
|
const hookForm = useSettingsForm();
|
|
6899
|
+
const [showDefaultPrompt, setShowDefaultPrompt] = React.useState(false);
|
|
5514
6900
|
if (props.view === "summary") {
|
|
5515
6901
|
return h("span", null, t("settings.summary"));
|
|
5516
6902
|
}
|
|
@@ -5540,6 +6926,14 @@ function FileTypeIcon(props) {
|
|
|
5540
6926
|
{ id: "clock", label: t("settings.timeMode.clock") },
|
|
5541
6927
|
{ id: "elapsed", label: t("settings.timeMode.elapsed") },
|
|
5542
6928
|
];
|
|
6929
|
+
const todoPromptOptions = [
|
|
6930
|
+
{ id: "prompt", label: t("settings.todoPromptOn") },
|
|
6931
|
+
{ id: "off", label: t("settings.todoPromptOff") },
|
|
6932
|
+
];
|
|
6933
|
+
const conclusionTabOptions = [
|
|
6934
|
+
{ id: "newtab", label: t("settings.conclusionTabOn") },
|
|
6935
|
+
{ id: "off", label: t("settings.conclusionTabOff") },
|
|
6936
|
+
];
|
|
5543
6937
|
const boolRow = (label, field) =>
|
|
5544
6938
|
h(SettingSelect, {
|
|
5545
6939
|
label,
|
|
@@ -5568,23 +6962,53 @@ function FileTypeIcon(props) {
|
|
|
5568
6962
|
note(t("settings.timeModeNote")),
|
|
5569
6963
|
boolRow(t("settings.stepAnchors"), "stepAnchors"),
|
|
5570
6964
|
note(t("settings.stepAnchorsNote")),
|
|
5571
|
-
|
|
6965
|
+
h(SettingSelect, {
|
|
6966
|
+
label: t("settings.todoPromptRow"),
|
|
6967
|
+
value: cfg.todoPrompt ? "prompt" : "off",
|
|
6968
|
+
options: todoPromptOptions,
|
|
6969
|
+
onPick: (id) => set("todoPrompt", id === "prompt"),
|
|
6970
|
+
hint: h(
|
|
6971
|
+
"button",
|
|
6972
|
+
{
|
|
6973
|
+
type: "button",
|
|
6974
|
+
className: "dsov-set-hint",
|
|
6975
|
+
title: t("settings.todoPromptHint"),
|
|
6976
|
+
"aria-label": t("settings.todoPromptHint"),
|
|
6977
|
+
"data-active": showDefaultPrompt ? "true" : "false",
|
|
6978
|
+
onClick: () => setShowDefaultPrompt(!showDefaultPrompt),
|
|
6979
|
+
},
|
|
6980
|
+
h(IconInfo, { size: 13 })
|
|
6981
|
+
),
|
|
6982
|
+
}),
|
|
5572
6983
|
cfg.todoPrompt
|
|
5573
6984
|
? h(SettingTextarea, {
|
|
5574
|
-
label:
|
|
6985
|
+
label: "",
|
|
5575
6986
|
value: cfg.todoPromptTemplate,
|
|
5576
|
-
placeholder:
|
|
6987
|
+
placeholder: t("settings.todoPromptPlaceholder"),
|
|
5577
6988
|
onCommit: (value) => set("todoPromptTemplate", value),
|
|
5578
6989
|
})
|
|
6990
|
+
: h("div", { className: "dsov-set-note dsov-set-noteWrap" }, t("settings.todoPromptPlaceholder")),
|
|
6991
|
+
showDefaultPrompt
|
|
6992
|
+
? h(
|
|
6993
|
+
"div",
|
|
6994
|
+
{ className: "dsov-set-defaultPrompt" },
|
|
6995
|
+
h("div", { className: "dsov-set-defaultPromptHead" }, t("settings.todoPromptDefault")),
|
|
6996
|
+
h("pre", { className: "dsov-set-defaultPromptBody" }, DEFAULT_TODO_TEMPLATE.trim())
|
|
6997
|
+
)
|
|
5579
6998
|
: null,
|
|
5580
|
-
|
|
5581
|
-
|
|
6999
|
+
h(SettingSelect, {
|
|
7000
|
+
label: t("settings.conclusionTabRow"),
|
|
7001
|
+
value: cfg.conclusionTab ? "newtab" : "off",
|
|
7002
|
+
options: conclusionTabOptions,
|
|
7003
|
+
onPick: (id) => set("conclusionTab", id === "newtab"),
|
|
7004
|
+
}),
|
|
7005
|
+
note(t(cfg.conclusionTab ? "settings.conclusionTabNote" : "settings.conclusionTabOffNote"))
|
|
5582
7006
|
),
|
|
5583
7007
|
group(
|
|
5584
7008
|
t("settings.quickNav"),
|
|
5585
7009
|
boolRow(t("settings.quickNavRow"), "quickNav"),
|
|
5586
|
-
boolRow(t("settings.quickNavLinkRow"), "quickNavLink"),
|
|
5587
7010
|
note(t("settings.quickNavNote")),
|
|
7011
|
+
boolRow(t("settings.quickNavLinkRow"), "quickNavLink"),
|
|
5588
7012
|
note(t("settings.quickNavLinkNote"))
|
|
5589
7013
|
),
|
|
5590
7014
|
group(
|
|
@@ -5595,6 +7019,7 @@ function FileTypeIcon(props) {
|
|
|
5595
7019
|
options: placeOptions,
|
|
5596
7020
|
onPick: (id) => set("placement", id),
|
|
5597
7021
|
}),
|
|
7022
|
+
note(t("settings.placementNote")),
|
|
5598
7023
|
boolRow(t("settings.fullHeightRow"), "sectionFullHeight"),
|
|
5599
7024
|
note(t("settings.fullHeightNote"))
|
|
5600
7025
|
),
|
|
@@ -5664,7 +7089,7 @@ function FileTypeIcon(props) {
|
|
|
5664
7089
|
.dsov-empty{font-size:12.5px;line-height:1.6;color:var(--dsw-alias-label-secondary);padding:4px 2px}
|
|
5665
7090
|
.dsov-secBody{max-height:320px;overflow-y:auto;overflow-x:hidden;margin:0 -12px 0 -8px;padding:0 12px 0 8px}
|
|
5666
7091
|
.dsov-section[data-full=true] .dsov-secBody{max-height:none;overflow:visible;margin:0;padding:0}
|
|
5667
|
-
.dsov-loading{font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
7092
|
+
.dsov-loading{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
5668
7093
|
.dsov-goal{display:flex;align-items:flex-start;gap:8px;font-size:13px;color:var(--dsw-alias-label-primary);padding:2px}
|
|
5669
7094
|
.dsov-goalIcon{flex:none;display:inline-flex;margin-top:2px;color:var(--dsw-alias-brand-primary)}
|
|
5670
7095
|
.dsov-goalObjective{flex:1;min-width:0;line-height:1.55;overflow-wrap:anywhere}
|
|
@@ -5716,7 +7141,39 @@ function FileTypeIcon(props) {
|
|
|
5716
7141
|
.dsov-todo[data-depth]::before{content:"";position:absolute;left:-15px;top:-4px;bottom:50%;width:9px;border-left:1px solid var(--dsw-alias-border-l2);border-bottom:1px solid var(--dsw-alias-border-l2);border-bottom-left-radius:6px}
|
|
5717
7142
|
.dsov-member[data-expandable=false]{cursor:default}
|
|
5718
7143
|
.dsov-member[data-expandable=false]:hover{background:transparent}
|
|
5719
|
-
.dsov-stepChevron{flex:none;margin-right:-2px}
|
|
7144
|
+
.dsov-stepChevron{flex:none;margin-right:-2px;position:relative}
|
|
7145
|
+
.dsov-stepChevron::after{content:"";position:absolute;inset:-5px -4px}
|
|
7146
|
+
.dsov-stepIconSwap{position:relative;flex:none;display:inline-flex;align-items:center;justify-content:center;width:15px;height:15px;cursor:pointer;border-radius:5px}
|
|
7147
|
+
.dsov-stepIconSwap::after{content:"";position:absolute;inset:-4px}
|
|
7148
|
+
.dsov-stepIconSwap>.dsov-todoIcon{transition:opacity .12s ease}
|
|
7149
|
+
.dsov-stepSwapChevron{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .12s ease,transform .15s ease;pointer-events:none}
|
|
7150
|
+
.dsov-step:hover .dsov-stepIconSwap>.dsov-todoIcon,.dsov-stepIconSwap:focus-visible>.dsov-todoIcon{opacity:0}
|
|
7151
|
+
.dsov-step:hover .dsov-stepSwapChevron{opacity:1}
|
|
7152
|
+
.dsov-stepIconSwap[data-open=true]>.dsov-todoIcon{opacity:0}
|
|
7153
|
+
.dsov-stepIconSwap[data-open=true] .dsov-stepSwapChevron{opacity:1}
|
|
7154
|
+
.dsov-stepBody{display:flex;flex-direction:column;gap:6px;margin:1px 6px 5px 21px;padding:7px 9px;border-radius:9px;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l1)}
|
|
7155
|
+
.dsov-stepBodySec{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
7156
|
+
.dsov-stepBodyLabel{font-size:10px;line-height:14px;letter-spacing:.02em;color:var(--dsw-alias-label-tertiary)}
|
|
7157
|
+
.dsov-stepBodyText{margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;line-height:1.5;color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;max-height:168px;overflow-y:auto;overflow-x:hidden}
|
|
7158
|
+
.dsov-stepBodyErr{color:var(--dsw-alias-state-error-primary,#ff453a)}
|
|
7159
|
+
.dsov-stepBodyMeta{font-size:10px;line-height:14px;color:var(--dsw-alias-label-tertiary)}
|
|
7160
|
+
.dsov-stepChip{flex:none;display:inline-flex;align-items:center;height:16px;padding:0 5px;border-radius:5px;font-size:10px;line-height:1;letter-spacing:.01em;white-space:nowrap}
|
|
7161
|
+
.dsov-stepChip[data-tone=warn]{color:var(--dsw-alias-state-warn-primary,#ff7d00);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#ff7d00) 13%,transparent)}
|
|
7162
|
+
.dsov-stepChip[data-tone=ok]{color:var(--dsw-alias-state-success-primary,#00b42a);background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#00b42a) 12%,transparent)}
|
|
7163
|
+
.dsov-stepChip[data-tone=err]{color:var(--dsw-alias-state-error-primary,#f53f3f);background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#f53f3f) 12%,transparent)}
|
|
7164
|
+
.dsov-stepApproval{display:flex;flex-direction:column;gap:4px;padding:6px 8px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#ff7d00) 7%,transparent);border:.5px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary,#ff7d00) 26%,transparent)}
|
|
7165
|
+
.dsov-stepApprovalItem[data-tone=ok]{background:transparent}
|
|
7166
|
+
.dsov-stepApprovalHead{display:flex;align-items:center;gap:5px;min-width:0}
|
|
7167
|
+
.dsov-stepApprovalIcon{flex:none;display:inline-flex;color:var(--dsw-alias-label-secondary)}
|
|
7168
|
+
.dsov-stepApprovalName{flex:none;font-size:11px;font-weight:500;color:var(--dsw-alias-label-primary)}
|
|
7169
|
+
.dsov-stepApprovalTool{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10.5px;color:var(--dsw-alias-label-tertiary)}
|
|
7170
|
+
.dsov-stepApprovalBadge{flex:none;padding:0 5px;height:15px;display:inline-flex;align-items:center;border-radius:4px;font-size:10px;white-space:nowrap}
|
|
7171
|
+
.dsov-stepApprovalBadge[data-tone=warn]{color:var(--dsw-alias-state-warn-primary,#ff7d00);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#ff7d00) 16%,transparent)}
|
|
7172
|
+
.dsov-stepApprovalBadge[data-tone=ok]{color:var(--dsw-alias-state-success-primary,#00b42a);background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#00b42a) 14%,transparent)}
|
|
7173
|
+
.dsov-stepApprovalBadge[data-tone=err]{color:var(--dsw-alias-state-error-primary,#f53f3f);background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#f53f3f) 14%,transparent)}
|
|
7174
|
+
.dsov-stepApprovalTime{flex:none;margin-left:auto;font-size:10px;color:var(--dsw-alias-label-tertiary)}
|
|
7175
|
+
.dsov-stepApprovalReason{margin-left:16px;font-size:11px;line-height:1.5;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;overflow-wrap:anywhere}
|
|
7176
|
+
.dsov-stepApprovalOutcome{margin-left:16px;font-size:10.5px;line-height:1.45;color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;overflow-wrap:anywhere;max-height:60px;overflow-y:auto}
|
|
5720
7177
|
.dsov-compactIcon{color:var(--dsw-alias-label-secondary)}
|
|
5721
7178
|
.dsov-todoIconSlot{position:relative;flex:none;display:inline-flex;margin-top:2px}
|
|
5722
7179
|
.dsov-todoIconSlot .dsov-todoIcon{margin-top:0}
|
|
@@ -5728,7 +7185,8 @@ function FileTypeIcon(props) {
|
|
|
5728
7185
|
.dsov-rowStatic{cursor:default}
|
|
5729
7186
|
.dsov-rowStatic:hover{background:transparent}
|
|
5730
7187
|
.dsov-tile{flex:none;width:28px;height:28px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l1);display:flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-secondary)}
|
|
5731
|
-
.dsov-fileIcon{flex:none;width:22px;display:inline-flex;justify-content:center}
|
|
7188
|
+
.dsov-fileIcon{flex:none;width:22px;display:inline-flex;justify-content:center;align-items:center}
|
|
7189
|
+
.dsov-fileMark{display:inline-flex;align-items:center;justify-content:center;flex:none;width:20px;height:20px;border-radius:6px;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 14%,transparent);color:var(--dsw-alias-brand-primary)}
|
|
5732
7190
|
.dsov-fileIcon svg{display:block}
|
|
5733
7191
|
.dsov-artifactName{flex:1;min-width:0;font-size:13px;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
5734
7192
|
.dsov-counts{flex:none;display:inline-flex;gap:6px;font-family:var(--ds-font-family-code,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace);font-size:11.5px;font-variant-numeric:tabular-nums}
|
|
@@ -5751,7 +7209,8 @@ function FileTypeIcon(props) {
|
|
|
5751
7209
|
.dsov-set-link:hover{text-decoration:underline}
|
|
5752
7210
|
.dsov-set-group-title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:18px;letter-spacing:.02em;margin-bottom:10px}
|
|
5753
7211
|
.dsov-set-row{align-items:center;gap:10px;padding:8px 0;display:flex}
|
|
5754
|
-
.dsov-set-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:400;line-height:20px}
|
|
7212
|
+
.dsov-set-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:400;line-height:20px;display:flex;align-items:center;gap:6px}
|
|
7213
|
+
.dsov-set-labelText{min-width:0;overflow-wrap:anywhere}
|
|
5755
7214
|
.dsov-set-control{position:relative;flex:none;display:inline-flex}
|
|
5756
7215
|
.dsov-set-select{background:var(--dsw-alias-bg-module-platform,var(--dsw-alias-bg-layer-2));height:32px;font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;transition:background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border:none;border-radius:16px;align-items:center;gap:10px;padding:0 12px;font-size:13px;line-height:20px;display:inline-flex}
|
|
5757
7216
|
.dsov-set-select:hover{background:var(--dsw-alias-interactive-bg-hover,color-mix(in srgb,var(--dsw-alias-label-primary) 8%,transparent))}
|
|
@@ -5764,6 +7223,13 @@ function FileTypeIcon(props) {
|
|
|
5764
7223
|
.dsov-set-menuItem[data-selected=true]{color:var(--dsw-alias-brand-primary);font-weight:500}
|
|
5765
7224
|
.dsov-set-note{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));margin:4px 0 0;font-size:12px;line-height:1.55}
|
|
5766
7225
|
.dsov-set-note+.dsov-set-note{margin-top:2px}
|
|
7226
|
+
.dsov-set-labelText{min-width:0}
|
|
7227
|
+
.dsov-set-hint{display:inline-flex;align-items:center;justify-content:center;flex:none;width:16px;height:16px;margin:0;padding:0;border:none;border-radius:50%;background:transparent;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));cursor:pointer;transition:color .15s ease,background-color .15s ease}
|
|
7228
|
+
.dsov-set-noteWrap{white-space:pre-line}
|
|
7229
|
+
.dsov-set-hint:hover,.dsov-set-hint[data-active=true]{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}
|
|
7230
|
+
.dsov-set-defaultPrompt{margin:2px 0 6px;border:.5px solid var(--dsw-alias-border-l4,var(--dsw-alias-border-l2));border-radius:var(--dsw-radius-md,8px);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));padding:8px 12px}
|
|
7231
|
+
.dsov-set-defaultPromptHead{font-size:11.5px;font-weight:600;color:var(--dsw-alias-label-secondary);margin-bottom:4px}
|
|
7232
|
+
.dsov-set-defaultPromptBody{max-height:220px;overflow-y:auto;margin:0;white-space:pre-wrap;overflow-wrap:anywhere;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;line-height:1.6;color:var(--dsw-alias-label-secondary)}
|
|
5767
7233
|
.dsov-set-textrow{flex-direction:column;align-items:stretch;gap:6px}
|
|
5768
7234
|
.dsov-set-input{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l4,var(--dsw-alias-border-l2));border-radius:var(--dsw-radius-md,8px);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));width:100%;color:var(--dsw-alias-label-primary);font:inherit;padding:6px 12px;font-size:13px;line-height:1.5}
|
|
5769
7235
|
.dsov-set-input:focus{outline:none;border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 55%,transparent)}
|
|
@@ -5775,6 +7241,10 @@ function FileTypeIcon(props) {
|
|
|
5775
7241
|
html[data-dsov-quicknav="on"] nav:has(button[data-index][aria-label]),html[data-dsov-quicknav="on"] nav[class*="_frame"]{display:block!important}
|
|
5776
7242
|
html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data-dsov-quicknav="off"] nav[class*="_frame"]{display:none!important}
|
|
5777
7243
|
.dsov-viking{box-sizing:border-box;height:100%;min-height:0;overflow-y:auto;padding:14px 16px 28px;display:flex;flex-direction:column;gap:10px}
|
|
7244
|
+
.dsov-docPage{box-sizing:border-box;height:100%;padding:20px 24px 40px;position:relative;overflow:auto}
|
|
7245
|
+
.dsov-docDocument{min-width:0;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere;font-size:14px;line-height:1.75}
|
|
7246
|
+
.dsov-docTitleIcon{flex:none}
|
|
7247
|
+
@media (width<=767px){.dsov-docPage{padding:16px 18px 32px}}
|
|
5778
7248
|
.dsov-vikingHead{display:flex;align-items:flex-start;gap:8px}
|
|
5779
7249
|
.dsov-vikingIcon{flex:none;display:inline-flex;margin-top:2px;color:var(--dsw-alias-brand-primary)}
|
|
5780
7250
|
.dsov-vikingHeadText{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
|
|
@@ -5854,6 +7324,51 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
5854
7324
|
.dsov-errPopTime{color:var(--dsw-alias-label-tertiary);font-size:10px;white-space:nowrap}
|
|
5855
7325
|
.dsov-errPopDetail{color:var(--dsw-alias-label-secondary);word-break:break-all;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
5856
7326
|
.dsov-errPopMsg{margin-top:2px;color:var(--dsw-alias-state-error-primary,#ff453a);white-space:pre-wrap;word-break:break-all;font-size:10.5px;max-height:76px;overflow-y:auto}
|
|
7327
|
+
.dsov-role{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:0 2px;font-size:10.5px;line-height:16px;color:var(--dsw-alias-label-secondary)}
|
|
7328
|
+
.dsov-roleBadge{flex:none;display:inline-flex;align-items:center;padding:1px 7px;border-radius:999px;font-weight:500;color:var(--dsw-alias-brand-primary,#247bbf);background:color-mix(in srgb,var(--dsw-alias-brand-primary,#247bbf) 12%,transparent)}
|
|
7329
|
+
.dsov-role[data-kind=sub] .dsov-roleBadge{color:var(--dsw-alias-state-warn-primary,#ff9f0a);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#ff9f0a) 15%,transparent)}
|
|
7330
|
+
.dsov-roleMode{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px;color:var(--dsw-alias-label-tertiary)}
|
|
7331
|
+
.dsov-roleBtn{display:inline-flex;align-items:center;gap:3px;flex:none;max-width:100%;min-width:0;border:none;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;padding:1px 6px;border-radius:999px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:background-color .16s ease,color .16s ease}
|
|
7332
|
+
.dsov-roleBtn:hover{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary)}
|
|
7333
|
+
.dsov-roleBtn[data-link=true]{color:var(--dsw-alias-brand-primary,#247bbf)}
|
|
7334
|
+
.dsov-roleBtn:disabled{cursor:default;opacity:.65}
|
|
7335
|
+
.dsov-roleChevron{flex:none;transition:transform .16s ease}
|
|
7336
|
+
.dsov-roleBtn[data-open=true] .dsov-roleChevron{transform:rotate(90deg)}
|
|
7337
|
+
.dsov-roleParent{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary)}
|
|
7338
|
+
.dsov-roleHint{display:inline-flex;align-items:center;gap:4px;min-width:0;color:var(--dsw-alias-label-tertiary)}
|
|
7339
|
+
.dsov-roleList{display:flex;flex-direction:column;gap:1px;width:100%;margin:1px 0 0}
|
|
7340
|
+
.dsov-roleItem{display:flex;align-items:center;gap:6px;width:100%;min-width:0;border:none;background:transparent;font:inherit;color:inherit;text-align:left;cursor:pointer;padding:3px 6px;border-radius:8px}
|
|
7341
|
+
.dsov-roleItem:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-label-primary) 6%,transparent)}
|
|
7342
|
+
.dsov-roleItem:disabled{cursor:default}
|
|
7343
|
+
.dsov-roleDot{flex:none;width:5px;height:5px;border-radius:50%;background:color-mix(in srgb,var(--dsw-alias-label-secondary) 45%,transparent)}
|
|
7344
|
+
.dsov-roleDot[data-running=true]{background:var(--dsw-alias-state-success-primary,#00b42a)}
|
|
7345
|
+
.dsov-roleItemTitle{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
7346
|
+
.dsov-loadingReload{border:.5px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:1.4;padding:2px 8px;border-radius:999px;cursor:pointer;transition:background-color .16s ease,color .16s ease}
|
|
7347
|
+
.dsov-loadingReload:hover{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary)}
|
|
7348
|
+
@keyframes dsov-secIntroA{0%{opacity:0;transform:translateY(9px);box-shadow:0 0 0 1px color-mix(in srgb,var(--dsw-alias-brand-primary,#247bbf) 60%,transparent)}55%{opacity:1;transform:none;box-shadow:0 0 0 1px color-mix(in srgb,var(--dsw-alias-brand-primary,#247bbf) 45%,transparent)}100%{opacity:1;transform:none;box-shadow:0 0 0 0 transparent}}
|
|
7349
|
+
@keyframes dsov-secIntroB{0%{opacity:0;transform:translateY(9px);box-shadow:0 0 0 1px color-mix(in srgb,var(--dsw-alias-brand-primary,#247bbf) 60%,transparent)}55%{opacity:1;transform:none;box-shadow:0 0 0 1px color-mix(in srgb,var(--dsw-alias-brand-primary,#247bbf) 45%,transparent)}100%{opacity:1;transform:none;box-shadow:0 0 0 0 transparent}}
|
|
7350
|
+
.dsov-section[data-intro=even]{animation:dsov-secIntroA .44s cubic-bezier(.22,.61,.36,1) both}
|
|
7351
|
+
.dsov-section[data-intro=odd]{animation:dsov-secIntroB .44s cubic-bezier(.22,.61,.36,1) both}
|
|
7352
|
+
@keyframes dsov-turnStart{0%{opacity:0;transform:translateY(-4px)}12%{opacity:1;transform:none}80%{opacity:1;transform:none}100%{opacity:0;transform:translateY(-2px)}}
|
|
7353
|
+
.dsov-turnStart{display:flex;align-items:center;gap:6px;font-size:11px;line-height:1.5;color:var(--dsw-alias-brand-primary,#247bbf);padding:0 2px;animation:dsov-turnStart 2.2s ease both}
|
|
7354
|
+
.dsov-turnStartDot{flex:none;width:6px;height:6px;border-radius:50%;background:currentColor;box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary,#247bbf) 20%,transparent)}
|
|
7355
|
+
.dsov-stepChild{margin-left:21px;padding:3px 6px;border-radius:8px;align-items:flex-start;cursor:default;font-size:11.5px}
|
|
7356
|
+
.dsov-stepChild:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 3.5%,transparent)}
|
|
7357
|
+
.dsov-stepChild[data-waiting=true]{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#ff9f0a) 8%,transparent)}
|
|
7358
|
+
.dsov-stepChild .dsov-stepName{font-weight:400;color:var(--dsw-alias-label-secondary)}
|
|
7359
|
+
.dsov-stepChild .dsov-time{font-size:10.5px;color:var(--dsw-alias-label-tertiary)}
|
|
7360
|
+
.dsov-stepChild>.dsov-todoIcon{margin-top:1px;flex:none}
|
|
7361
|
+
.dsov-stepChildBody{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
|
|
7362
|
+
.dsov-stepChildTop{display:flex;align-items:center;gap:6px;min-width:0}
|
|
7363
|
+
.dsov-stepChildTool{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10.5px;color:var(--dsw-alias-label-tertiary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
7364
|
+
.dsov-stepChildOutcome{flex:none;font-size:10px;line-height:1.6;padding:0 5px;border-radius:999px;color:var(--dsw-alias-state-success-primary,#00b42a);background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#00b42a) 13%,transparent)}
|
|
7365
|
+
.dsov-stepChildOutcome[data-ok=false]{color:var(--dsw-alias-state-error-primary,#ff453a);background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#ff453a) 13%,transparent)}
|
|
7366
|
+
.dsov-stepChildNote{font-size:11px;line-height:1.5;color:var(--dsw-alias-label-secondary);white-space:normal;overflow-wrap:anywhere;word-break:break-word}
|
|
7367
|
+
.dsov-mermaid{margin:6px 0;text-align:center;overflow-x:auto}
|
|
7368
|
+
.dsov-mermaid svg{max-width:100%;height:auto}
|
|
7369
|
+
.dsov-mermaid .dsov-md-pre{text-align:left}
|
|
7370
|
+
.dsov-mermaidFailed>.dsov-md-pre{border-left:2px solid var(--dsw-alias-state-warning-primary,#e6a23c)}
|
|
7371
|
+
.dsov-badge[data-tone=plan]{color:var(--dsw-alias-state-success-primary,#2ea36a);border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary,#2ea36a) 45%,transparent)}
|
|
5857
7372
|
.dsov-renderErr{margin:8px 12px 0;padding:6px 8px;border-radius:6px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-error-primary,#ff453a);font-size:11px;line-height:1.5;white-space:pre-wrap;word-break:break-all;text-align:left;max-height:140px;overflow:auto}
|
|
5858
7373
|
.dsov-turnSelect{flex:none;max-width:150px;height:24px;padding:0 22px 0 9px;border:.5px solid var(--dsw-alias-border-l2);border-radius:12px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;cursor:pointer;appearance:none;-webkit-appearance:none;background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,transparent 50%);background-position:calc(100% - 12px) 10px,calc(100% - 8px) 10px;background-size:4px 4px,4px 4px;background-repeat:no-repeat}
|
|
5859
7374
|
.dsov-turnSelect:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3,var(--dsw-alias-border-l2))}
|
|
@@ -5861,11 +7376,13 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
5861
7376
|
@keyframes dsov-pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
|
5862
7377
|
@keyframes dsov-stepIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
|
|
5863
7378
|
@keyframes dsov-stepOut{from{opacity:1;transform:none}to{opacity:0;transform:translateY(-6px)}}
|
|
7379
|
+
@keyframes dsov-refIn{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}
|
|
7380
|
+
.dsov-row[data-enter=true]{animation:dsov-refIn .32s ease both}
|
|
5864
7381
|
.dsov-step[data-enter=true]{animation:dsov-stepIn .34s ease both}
|
|
5865
7382
|
.dsov-step[data-exit=true]{position:absolute;top:0;left:0;right:0;animation:dsov-stepOut .4s ease both;pointer-events:none}
|
|
5866
7383
|
.dsov-todoSpin{transform-origin:7.5px 7.5px;animation:dsov-spin 1.1s linear infinite}
|
|
5867
7384
|
.dsov-todoPulse{animation:dsov-pulse 1.4s ease-in-out infinite}
|
|
5868
|
-
@media (prefers-reduced-motion: reduce){.dsov-todoSpin,.dsov-todoPulse,.dsov-step[data-enter=true]{animation:none}.dsov-step[data-exit=true]{animation:none;opacity:0}}
|
|
7385
|
+
@media (prefers-reduced-motion: reduce){.dsov-todoSpin,.dsov-todoPulse,.dsov-step[data-enter=true],.dsov-row[data-enter=true],.dsov-section[data-intro],.dsov-turnStart{animation:none}.dsov-step[data-exit=true]{animation:none;opacity:0}}
|
|
5869
7386
|
.dsov-vikingMode{align-self:flex-start;margin-left:auto}
|
|
5870
7387
|
.dsov-seg{flex:none;display:inline-flex;gap:2px;padding:2px;border-radius:8px;background:var(--dsw-alias-bg-layer-2)}
|
|
5871
7388
|
.dsov-segBtn{border:none;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;line-height:18px;padding:0 8px;border-radius:6px;cursor:pointer;white-space:nowrap}
|
|
@@ -6107,6 +7624,7 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
6107
7624
|
kind: CONTEXT_TAB_KIND,
|
|
6108
7625
|
patterns: [`${CONTEXT_ADDRESS_PREFIX}**`],
|
|
6109
7626
|
priority: "builtin",
|
|
7627
|
+
multiple: true,
|
|
6110
7628
|
canOpen: (address) => parseContextAddress(address) !== undefined,
|
|
6111
7629
|
title: (address) => {
|
|
6112
7630
|
const key = parseContextAddress(address);
|
|
@@ -6135,6 +7653,22 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
6135
7653
|
"conversation-overview: context tab body"
|
|
6136
7654
|
);
|
|
6137
7655
|
|
|
7656
|
+
ctx.effect(
|
|
7657
|
+
() =>
|
|
7658
|
+
ctx.slots.inject(
|
|
7659
|
+
"sidebar.right.pane.tab.title",
|
|
7660
|
+
() =>
|
|
7661
|
+
ctx.slots.register(
|
|
7662
|
+
{
|
|
7663
|
+
name: "sidebar.right.pane.tab.title",
|
|
7664
|
+
key: CONTEXT_TAB_ID,
|
|
7665
|
+
},
|
|
7666
|
+
ContextTabTitle
|
|
7667
|
+
)
|
|
7668
|
+
),
|
|
7669
|
+
"conversation-overview: context tab title"
|
|
7670
|
+
);
|
|
7671
|
+
|
|
6138
7672
|
ctx.effect(() => {
|
|
6139
7673
|
if (typeof document === "undefined" || !document.documentElement) return undefined;
|
|
6140
7674
|
const applyAttr = () => {
|
|
@@ -6290,6 +7824,11 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
6290
7824
|
changesReviewAddress,
|
|
6291
7825
|
absPath,
|
|
6292
7826
|
normalizeConfig,
|
|
7827
|
+
conclusionAutoOpenKey,
|
|
7828
|
+
subagentWorkInScope,
|
|
7829
|
+
persistDocPreview,
|
|
7830
|
+
readDocPreviewStore,
|
|
7831
|
+
DOC_PREVIEW_STORE_KEY,
|
|
6293
7832
|
setFormConfig,
|
|
6294
7833
|
setOverlayConfig,
|
|
6295
7834
|
clearOverlayConfig,
|
|
@@ -6297,10 +7836,31 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
6297
7836
|
ProgressSection,
|
|
6298
7837
|
ArtifactsSection,
|
|
6299
7838
|
ReferencesSection,
|
|
7839
|
+
ContextPreviewBody,
|
|
7840
|
+
SettingsForm,
|
|
7841
|
+
chatMarkdown: ChatMarkdown,
|
|
7842
|
+
chatMarkdownLabels,
|
|
6300
7843
|
StatsBar,
|
|
6301
7844
|
ArtifactFileMenu,
|
|
6302
7845
|
OverviewGlyph,
|
|
6303
7846
|
OverviewContent,
|
|
7847
|
+
MarkdownBody,
|
|
7848
|
+
mdToHtml,
|
|
7849
|
+
Section,
|
|
7850
|
+
sectionIntroProps,
|
|
7851
|
+
SessionRoleBar,
|
|
7852
|
+
sessionRoleOf,
|
|
7853
|
+
canNavigateSession,
|
|
7854
|
+
openSessionById,
|
|
7855
|
+
planTitleOf,
|
|
7856
|
+
openNativePlan,
|
|
7857
|
+
documentAddress,
|
|
7858
|
+
openNativeDocument,
|
|
7859
|
+
openArtifact,
|
|
7860
|
+
ContextTabTitle,
|
|
7861
|
+
__setRuntime: (value) => {
|
|
7862
|
+
runtime = value;
|
|
7863
|
+
},
|
|
6304
7864
|
OverviewBoundary,
|
|
6305
7865
|
SettingsForm,
|
|
6306
7866
|
SettingsSection,
|