dsh-conversation-overview 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/client.js +1572 -120
- 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,79 @@ 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
|
+
const entry = typeof artifact.previewKey === "string" ? readContextPreview(artifact.previewKey) : null;
|
|
1569
|
+
const docKey = `conclusion:${typeof artifact.turn === "number" ? artifact.turn : artifact.previewKey}`;
|
|
1570
|
+
if (openNativeDocument(sessionId, docKey, artifact.title, entry === null ? undefined : entry.text)) return;
|
|
1571
|
+
if (typeof artifact.previewKey === "string" && artifact.previewKey !== "") openContextPreview(artifact.previewKey);
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
if (artifact.plan === true) {
|
|
1575
|
+
if (openNativePlan(sessionId, artifact.planCallId)) return;
|
|
1576
|
+
/* host plan viewer unavailable (plugin disabled, no sidebar): the document
|
|
1577
|
+
page over the same snapshot, then the plugin's own tab */
|
|
1578
|
+
const entry = typeof artifact.previewKey === "string" ? readContextPreview(artifact.previewKey) : null;
|
|
1579
|
+
const docKey = `plan:${typeof artifact.planCallId === "string" && artifact.planCallId !== "" ? artifact.planCallId : artifact.previewKey}`;
|
|
1580
|
+
if (openNativeDocument(sessionId, docKey, entry !== null ? entry.title : artifact.planTitle, entry === null ? undefined : entry.text)) return;
|
|
1581
|
+
if (typeof artifact.previewKey === "string" && artifact.previewKey !== "") openContextPreview(artifact.previewKey);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1352
1584
|
if (typeof artifact.path === "string" && artifact.path.startsWith("viking://")) {
|
|
1353
1585
|
openVikingPreview(artifact.path);
|
|
1354
1586
|
return;
|
|
@@ -1524,6 +1756,7 @@ window.__ModuleLoader__.load({
|
|
|
1524
1756
|
teamTasks: new Map(),
|
|
1525
1757
|
subagents: new Map(),
|
|
1526
1758
|
stats: createStats(),
|
|
1759
|
+
conclusion: null,
|
|
1527
1760
|
changed: false,
|
|
1528
1761
|
};
|
|
1529
1762
|
}
|
|
@@ -1535,7 +1768,7 @@ window.__ModuleLoader__.load({
|
|
|
1535
1768
|
}
|
|
1536
1769
|
|
|
1537
1770
|
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() };
|
|
1771
|
+
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
1772
|
}
|
|
1540
1773
|
|
|
1541
1774
|
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 +1787,72 @@ window.__ModuleLoader__.load({
|
|
|
1554
1787
|
const CONTEXT_PREVIEW_LIMIT = 400;
|
|
1555
1788
|
const CONTEXT_TEXT_LIMIT = 120000;
|
|
1556
1789
|
|
|
1557
|
-
|
|
1790
|
+
/* Mermaid is loaded lazily from a CDN the first time a preview contains a
|
|
1791
|
+
diagram; if the load fails (offline / blocked) the raw source <pre>
|
|
1792
|
+
fallback inside every .dsov-mermaid block simply stays visible. */
|
|
1793
|
+
let mermaidPromise = null;
|
|
1794
|
+
let mermaidSeq = 0;
|
|
1795
|
+
function loadMermaid() {
|
|
1796
|
+
if (mermaidPromise === null) {
|
|
1797
|
+
mermaidPromise = import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs")
|
|
1798
|
+
.then((mod) => {
|
|
1799
|
+
const mermaid = mod !== null && typeof mod === "object" && typeof mod.default === "object" ? mod.default : mod;
|
|
1800
|
+
const dark = typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches === true;
|
|
1801
|
+
mermaid.initialize({ startOnLoad: false, theme: dark === true ? "dark" : "default", fontFamily: "inherit" });
|
|
1802
|
+
return mermaid;
|
|
1803
|
+
})
|
|
1804
|
+
.catch((error) => {
|
|
1805
|
+
mermaidPromise = null;
|
|
1806
|
+
throw error;
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
return mermaidPromise;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
async function renderMermaidNodes(root) {
|
|
1813
|
+
if (root === null || typeof root.querySelectorAll !== "function") return;
|
|
1814
|
+
const nodes = root.querySelectorAll(".dsov-mermaid[data-src]");
|
|
1815
|
+
if (nodes.length === 0) return;
|
|
1816
|
+
let mermaid;
|
|
1817
|
+
try {
|
|
1818
|
+
mermaid = await loadMermaid();
|
|
1819
|
+
} catch {
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
for (const node of nodes) {
|
|
1823
|
+
if (node.getAttribute("data-rendered") === "true") continue;
|
|
1824
|
+
const source = node.getAttribute("data-src") || "";
|
|
1825
|
+
if (source.trim() === "") continue;
|
|
1826
|
+
try {
|
|
1827
|
+
mermaidSeq += 1;
|
|
1828
|
+
const result = await mermaid.render(`dsov-mmd-${mermaidSeq}`, source);
|
|
1829
|
+
const svg = result !== null && typeof result === "object" && typeof result.svg === "string" ? result.svg : "";
|
|
1830
|
+
if (svg === "") continue;
|
|
1831
|
+
node.innerHTML = svg;
|
|
1832
|
+
node.setAttribute("data-rendered", "true");
|
|
1833
|
+
} catch {
|
|
1834
|
+
node.classList.add("dsov-mermaidFailed");
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
/** Markdown block that upgrades ```mermaid fences to rendered diagrams after mount. */
|
|
1840
|
+
function MarkdownBody({ text }) {
|
|
1841
|
+
const ref = React.useRef(null);
|
|
1842
|
+
const html = React.useMemo(() => mdToHtml(typeof text === "string" ? text : ""), [text]);
|
|
1843
|
+
React.useEffect(() => {
|
|
1844
|
+
const node = ref.current;
|
|
1845
|
+
if (node === null) return undefined;
|
|
1846
|
+
const timer = setTimeout(() => renderMermaidNodes(node), 0);
|
|
1847
|
+
return () => clearTimeout(timer);
|
|
1848
|
+
}, [html]);
|
|
1849
|
+
return h("div", { className: "dsov-md", ref, dangerouslySetInnerHTML: { __html: html } });
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
function rememberContextPreview(sessionId, refKey, title, text, kind) {
|
|
1558
1853
|
if (typeof sessionId !== "string" || sessionId === "" || typeof text !== "string" || text === "") return undefined;
|
|
1559
1854
|
const globalKey = `${sessionId}|${refKey}`;
|
|
1560
|
-
contextPreviews.set(globalKey, { title: typeof title === "string" && title !== "" ? title : refKey, text: text.length > CONTEXT_TEXT_LIMIT ? `${text.slice(0, CONTEXT_TEXT_LIMIT)}\n…` : text });
|
|
1855
|
+
contextPreviews.set(globalKey, { 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 } : {}) });
|
|
1561
1856
|
while (contextPreviews.size > CONTEXT_PREVIEW_LIMIT) {
|
|
1562
1857
|
const oldest = contextPreviews.keys().next();
|
|
1563
1858
|
if (oldest.done === true) break;
|
|
@@ -1587,10 +1882,32 @@ window.__ModuleLoader__.load({
|
|
|
1587
1882
|
}
|
|
1588
1883
|
}
|
|
1589
1884
|
|
|
1885
|
+
/** The host surface that holds preview tabs; null when this context has none. */
|
|
1886
|
+
function contextSidebar() {
|
|
1887
|
+
try {
|
|
1888
|
+
const sidebar = runtime && runtime.ctx ? runtime.ctx.sidebarRight : undefined;
|
|
1889
|
+
return sidebar && typeof sidebar.openResource === "function" ? sidebar : null;
|
|
1890
|
+
} catch {
|
|
1891
|
+
return null;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1590
1895
|
function openContextPreview(globalKey) {
|
|
1591
|
-
const sidebar =
|
|
1592
|
-
if (
|
|
1896
|
+
const sidebar = contextSidebar();
|
|
1897
|
+
if (sidebar === null) return false;
|
|
1593
1898
|
sidebar.openResource(contextPreviewAddress(globalKey));
|
|
1899
|
+
return true;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
/* An overview can be mounted twice (inline card + sidebar tab); only the first
|
|
1903
|
+
instance may pop the turn-conclusion tab. */
|
|
1904
|
+
const conclusionAutoOpened = new Set();
|
|
1905
|
+
|
|
1906
|
+
function claimConclusionTab(key) {
|
|
1907
|
+
if (conclusionAutoOpened.has(key)) return false;
|
|
1908
|
+
if (conclusionAutoOpened.size > 200) conclusionAutoOpened.clear();
|
|
1909
|
+
conclusionAutoOpened.add(key);
|
|
1910
|
+
return true;
|
|
1594
1911
|
}
|
|
1595
1912
|
|
|
1596
1913
|
/* Quick-nav linkage: a click on the built-in turn rail broadcasts a scope request
|
|
@@ -1725,12 +2042,27 @@ window.__ModuleLoader__.load({
|
|
|
1725
2042
|
folder.changed = true;
|
|
1726
2043
|
}
|
|
1727
2044
|
|
|
2045
|
+
/** First heading (or first non-empty line) of a plan markdown, trimmed for row display. */
|
|
2046
|
+
function planTitleOf(text) {
|
|
2047
|
+
for (const line of String(text).split("\n")) {
|
|
2048
|
+
const trimmed = line.trim();
|
|
2049
|
+
if (trimmed === "") continue;
|
|
2050
|
+
const heading = /^#{1,6}\s+(.*)$/.exec(trimmed);
|
|
2051
|
+
return (heading ? heading[1] : trimmed).replace(/[#*`]/g, "").trim().slice(0, 80);
|
|
2052
|
+
}
|
|
2053
|
+
return "";
|
|
2054
|
+
}
|
|
2055
|
+
|
|
1728
2056
|
function upsertArtifact(folder, bucket, path, update) {
|
|
1729
2057
|
const existing = folder.artifacts.get(path);
|
|
1730
2058
|
if (existing) {
|
|
1731
2059
|
if (typeof update.description === "string" && update.description.length > 0) existing.description = update.description;
|
|
1732
2060
|
if (update.presented) existing.presented = true;
|
|
1733
2061
|
if (update.produced) existing.produced = true;
|
|
2062
|
+
if (update.plan === true) existing.plan = true;
|
|
2063
|
+
if (typeof update.planTitle === "string" && update.planTitle !== "") existing.planTitle = update.planTitle;
|
|
2064
|
+
if (typeof update.previewKey === "string" && update.previewKey !== "") existing.previewKey = update.previewKey;
|
|
2065
|
+
if (typeof update.planCallId === "string" && update.planCallId !== "") existing.planCallId = update.planCallId;
|
|
1734
2066
|
if (typeof update.time === "number" && update.time > 0) existing.time = update.time;
|
|
1735
2067
|
if (typeof update.seq === "number") existing.seq = Math.max(existing.seq, update.seq);
|
|
1736
2068
|
const bucketExisting = bucket.artifacts.get(path);
|
|
@@ -1738,6 +2070,10 @@ window.__ModuleLoader__.load({
|
|
|
1738
2070
|
if (typeof update.description === "string" && update.description.length > 0) bucketExisting.description = update.description;
|
|
1739
2071
|
if (update.presented) bucketExisting.presented = true;
|
|
1740
2072
|
if (update.produced) bucketExisting.produced = true;
|
|
2073
|
+
if (update.plan === true) bucketExisting.plan = true;
|
|
2074
|
+
if (typeof update.planTitle === "string" && update.planTitle !== "") bucketExisting.planTitle = update.planTitle;
|
|
2075
|
+
if (typeof update.previewKey === "string" && update.previewKey !== "") bucketExisting.previewKey = update.previewKey;
|
|
2076
|
+
if (typeof update.planCallId === "string" && update.planCallId !== "") bucketExisting.planCallId = update.planCallId;
|
|
1741
2077
|
if (typeof update.time === "number" && update.time > 0) bucketExisting.time = update.time;
|
|
1742
2078
|
if (typeof update.seq === "number") bucketExisting.seq = Math.max(bucketExisting.seq, update.seq);
|
|
1743
2079
|
} else {
|
|
@@ -1751,6 +2087,10 @@ window.__ModuleLoader__.load({
|
|
|
1751
2087
|
description: typeof update.description === "string" ? update.description : undefined,
|
|
1752
2088
|
presented: update.presented === true,
|
|
1753
2089
|
produced: update.produced === true,
|
|
2090
|
+
...(update.plan === true ? { plan: true } : {}),
|
|
2091
|
+
...(typeof update.planTitle === "string" && update.planTitle !== "" ? { planTitle: update.planTitle } : {}),
|
|
2092
|
+
...(typeof update.previewKey === "string" && update.previewKey !== "" ? { previewKey: update.previewKey } : {}),
|
|
2093
|
+
...(typeof update.planCallId === "string" && update.planCallId !== "" ? { planCallId: update.planCallId } : {}),
|
|
1754
2094
|
time: typeof update.time === "number" ? update.time : 0,
|
|
1755
2095
|
firstTime: typeof update.time === "number" ? update.time : 0,
|
|
1756
2096
|
seq: typeof update.seq === "number" ? update.seq : 0,
|
|
@@ -1761,6 +2101,21 @@ window.__ModuleLoader__.load({
|
|
|
1761
2101
|
}
|
|
1762
2102
|
|
|
1763
2103
|
/** Record that one global ref appeared in the folder's current turn (unique, ascending). */
|
|
2104
|
+
/** A turn bucket keeps its own mention count and turn list: the folder entry spans
|
|
2105
|
+
every turn, while one bucket must answer "in THIS turn" for the per-turn view. */
|
|
2106
|
+
function withOwnTurns(stored, folder, current) {
|
|
2107
|
+
const turn = folder.currentTurn;
|
|
2108
|
+
const at = isRecord(current) && typeof current.time === "number" && current.time > 0 ? current.time : 0;
|
|
2109
|
+
const seq = isRecord(current) && typeof current.seq === "number" ? current.seq : null;
|
|
2110
|
+
return {
|
|
2111
|
+
...stored,
|
|
2112
|
+
count: 1,
|
|
2113
|
+
turns: typeof turn === "number" && turn >= 1 ? [turn] : [],
|
|
2114
|
+
...(at > 0 ? { time: at, firstTime: at } : {}),
|
|
2115
|
+
...(seq === null ? {} : { seq }),
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
|
|
1764
2119
|
function noteRefTurn(stored, folder) {
|
|
1765
2120
|
const turn = folder.currentTurn;
|
|
1766
2121
|
if (typeof turn !== "number" || turn < 1) return;
|
|
@@ -1782,13 +2137,14 @@ window.__ModuleLoader__.load({
|
|
|
1782
2137
|
const bucketExisting = bucket.refs.get(ref.key);
|
|
1783
2138
|
if (bucketExisting) {
|
|
1784
2139
|
bucketExisting.count += 1;
|
|
2140
|
+
noteRefTurn(bucketExisting, folder);
|
|
1785
2141
|
if (typeof ref.time === "number" && ref.time >= bucketExisting.time) {
|
|
1786
2142
|
bucketExisting.time = ref.time;
|
|
1787
2143
|
bucketExisting.seq = ref.seq;
|
|
1788
2144
|
}
|
|
1789
2145
|
if (typeof ref.previewKey === "string") bucketExisting.previewKey = ref.previewKey;
|
|
1790
2146
|
} else {
|
|
1791
|
-
bucket.refs.set(ref.key,
|
|
2147
|
+
bucket.refs.set(ref.key, withOwnTurns(existing, folder, ref));
|
|
1792
2148
|
bucket.refOrder.push(ref.key);
|
|
1793
2149
|
}
|
|
1794
2150
|
touch(folder);
|
|
@@ -1798,7 +2154,7 @@ window.__ModuleLoader__.load({
|
|
|
1798
2154
|
noteRefTurn(stored, folder);
|
|
1799
2155
|
folder.refs.set(ref.key, stored);
|
|
1800
2156
|
folder.refOrder.push(ref.key);
|
|
1801
|
-
bucket.refs.set(ref.key,
|
|
2157
|
+
bucket.refs.set(ref.key, withOwnTurns(stored, folder, ref));
|
|
1802
2158
|
bucket.refOrder.push(ref.key);
|
|
1803
2159
|
touch(folder);
|
|
1804
2160
|
}
|
|
@@ -1918,6 +2274,26 @@ window.__ModuleLoader__.load({
|
|
|
1918
2274
|
return text.length > 4000 ? text.slice(0, 4000) : text;
|
|
1919
2275
|
}
|
|
1920
2276
|
|
|
2277
|
+
/** Shorter assistant messages are asides (status lines, "done."), not documents. */
|
|
2278
|
+
const CONCLUSION_MIN_CHARS = 32;
|
|
2279
|
+
/** Chars kept per step for the expanded detail (kept small: every step carries one). */
|
|
2280
|
+
const STEP_RESULT_LIMIT = 1600;
|
|
2281
|
+
/** Stop collecting once a result is clearly huge; only the size is remembered. */
|
|
2282
|
+
const STEP_RESULT_SKIP = 40000;
|
|
2283
|
+
function stepResultOf(content) {
|
|
2284
|
+
if (!Array.isArray(content)) return null;
|
|
2285
|
+
let text = "";
|
|
2286
|
+
let chars = 0;
|
|
2287
|
+
for (const part of content) {
|
|
2288
|
+
if (!isRecord(part) || part.type !== "text" || typeof part.text !== "string") continue;
|
|
2289
|
+
chars += part.text.length;
|
|
2290
|
+
if (chars > STEP_RESULT_SKIP) return { text: "", chars, truncated: true, skipped: true };
|
|
2291
|
+
if (text.length < STEP_RESULT_LIMIT) text += part.text.slice(0, STEP_RESULT_LIMIT - text.length);
|
|
2292
|
+
}
|
|
2293
|
+
if (chars === 0) return null;
|
|
2294
|
+
return { text, chars, truncated: chars > text.length, skipped: false };
|
|
2295
|
+
}
|
|
2296
|
+
|
|
1921
2297
|
const ANCHOR_CAP = 8;
|
|
1922
2298
|
function addStep(folder, bucket, step) {
|
|
1923
2299
|
if (typeof step.teamKey === "string") {
|
|
@@ -1956,6 +2332,12 @@ window.__ModuleLoader__.load({
|
|
|
1956
2332
|
function foldEvent(folder, event, cwd) {
|
|
1957
2333
|
if (!isRecord(event) || typeof event.type !== "string") return;
|
|
1958
2334
|
const data = event.data;
|
|
2335
|
+
/* Tool work after a reply proves that reply was not the turn's closing word:
|
|
2336
|
+
the candidate stays provisional until a fresh reply replaces it. */
|
|
2337
|
+
if ((event.type === "tool/call" || event.type === "tool/result") && folder.currentTurn >= 1) {
|
|
2338
|
+
const running = folder.byTurn.get(folder.currentTurn);
|
|
2339
|
+
if (running !== undefined && isRecord(running.lastReply)) running.lastReply.provisional = true;
|
|
2340
|
+
}
|
|
1959
2341
|
if (typeof event.time === "number" && Number.isFinite(event.time)) {
|
|
1960
2342
|
bumpStatTime(folder.stats, event.time);
|
|
1961
2343
|
const statsTurn =
|
|
@@ -1975,6 +2357,45 @@ window.__ModuleLoader__.load({
|
|
|
1975
2357
|
touch(folder);
|
|
1976
2358
|
return;
|
|
1977
2359
|
}
|
|
2360
|
+
case "assistant/message": {
|
|
2361
|
+
/* A reply is only a candidate while the turn runs: 「本轮结论」 is published
|
|
2362
|
+
at turn/end, so 产物 never shows a provisional document mid-turn. */
|
|
2363
|
+
if (!isRecord(data) || !isAppendSurfaceEvent(event)) return;
|
|
2364
|
+
/* Two shapes carry the reply: plugin-level {content} and the durable log's
|
|
2365
|
+
{message:{content}}. Reading only the first left every real turn without a
|
|
2366
|
+
conclusion document, so the auto-open never fired. */
|
|
2367
|
+
const body = isRecord(data.message) ? data.message : data;
|
|
2368
|
+
const text = (typeof body.text === "string" && body.text !== "" ? body.text : textOf(body.content)).trim();
|
|
2369
|
+
if (text === "") return;
|
|
2370
|
+
const turn = folder.currentTurn >= 1 ? folder.currentTurn : Number.isSafeInteger(data.turn) && data.turn >= 1 ? data.turn : 0;
|
|
2371
|
+
const bucket = folder.byTurn.get(turn) ?? turnBucket(folder, folder.currentTurn);
|
|
2372
|
+
bucket.lastReply = {
|
|
2373
|
+
turn,
|
|
2374
|
+
text,
|
|
2375
|
+
seq: typeof event.seq === "number" ? event.seq : 0,
|
|
2376
|
+
time: typeof event.time === "number" ? event.time : 0,
|
|
2377
|
+
provisional: false,
|
|
2378
|
+
};
|
|
2379
|
+
touch(folder);
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
case "turn/end": {
|
|
2383
|
+
if (!isRecord(data) || typeof data.turn !== "number" || !Number.isSafeInteger(data.turn) || data.turn < 1) return;
|
|
2384
|
+
const bucket = folder.byTurn.get(data.turn);
|
|
2385
|
+
if (bucket === undefined) return;
|
|
2386
|
+
bucket.ended = true;
|
|
2387
|
+
const reply = isRecord(bucket.lastReply) ? bucket.lastReply : null;
|
|
2388
|
+
/* Publish the closing reply only now that the turn is over. A provisional
|
|
2389
|
+
reply (tool work followed it) still counts when it is substantive, so a
|
|
2390
|
+
turn whose last action was a tool call keeps its readable document. */
|
|
2391
|
+
if (reply !== null && typeof reply.text === "string" && reply.text !== "" && (reply.provisional !== true || reply.text.length >= CONCLUSION_MIN_CHARS)) {
|
|
2392
|
+
const record = { turn: data.turn, text: reply.text, seq: reply.seq, time: reply.time, ended: true };
|
|
2393
|
+
bucket.conclusion = record;
|
|
2394
|
+
folder.conclusion = record;
|
|
2395
|
+
}
|
|
2396
|
+
touch(folder);
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
1978
2399
|
case "todo/write": {
|
|
1979
2400
|
if (!isRecord(data) || !Array.isArray(data.todos)) return;
|
|
1980
2401
|
const todos = [];
|
|
@@ -2048,8 +2469,13 @@ window.__ModuleLoader__.load({
|
|
|
2048
2469
|
addStep(folder, approvalBucket, {
|
|
2049
2470
|
callId: `approval-${data.id}`,
|
|
2050
2471
|
name: "approval",
|
|
2472
|
+
/* the tool call this gate blocks: the approval renders nested under it so
|
|
2473
|
+
a child never appears to finish before its own parent */
|
|
2474
|
+
...(typeof data.callId === "string" && data.callId !== "" ? { parentCallId: data.callId } : {}),
|
|
2051
2475
|
detail: toolName,
|
|
2052
2476
|
full: typeof data.reason === "string" && data.reason !== "" ? `${toolName} · ${data.reason}` : toolName,
|
|
2477
|
+
approvalTool: toolName,
|
|
2478
|
+
...(typeof data.reason === "string" && data.reason !== "" ? { approvalReason: data.reason } : {}),
|
|
2053
2479
|
milestone: true,
|
|
2054
2480
|
done: false,
|
|
2055
2481
|
seq: event.seq,
|
|
@@ -2061,6 +2487,7 @@ window.__ModuleLoader__.load({
|
|
|
2061
2487
|
if (!isRecord(data) || typeof data.id !== "string" || data.id === "") return;
|
|
2062
2488
|
const outcome = typeof data.outcome === "string" ? data.outcome : "";
|
|
2063
2489
|
const approvalStep = folder.stepByCall.get(`approval-${data.id}`);
|
|
2490
|
+
if (approvalStep !== undefined && outcome !== "") approvalStep.approvalOutcome = outcome;
|
|
2064
2491
|
if (approvalStep !== undefined && outcome !== "" && typeof approvalStep.detail === "string" && !approvalStep.detail.includes(outcome)) {
|
|
2065
2492
|
approvalStep.detail = approvalStep.detail === "" ? outcome : `${approvalStep.detail} · ${outcome}`;
|
|
2066
2493
|
}
|
|
@@ -2112,6 +2539,10 @@ window.__ModuleLoader__.load({
|
|
|
2112
2539
|
folder.pending.set(callId, { type: "artifact", path: producedPath });
|
|
2113
2540
|
return;
|
|
2114
2541
|
}
|
|
2542
|
+
if (name === "exit_plan_mode") {
|
|
2543
|
+
folder.pending.set(callId, { type: "plan", callId, text: args !== null && isRecord(args) && typeof args.plan === "string" ? args.plan : "" });
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2115
2546
|
if (name === "skill" && args && typeof args.name === "string" && args.name.length > 0) {
|
|
2116
2547
|
folder.pending.set(callId, {
|
|
2117
2548
|
type: "reference",
|
|
@@ -2188,6 +2619,16 @@ window.__ModuleLoader__.load({
|
|
|
2188
2619
|
const callId = isRecord(source) && typeof source.callId === "string" ? source.callId : undefined;
|
|
2189
2620
|
if (callId === undefined) return;
|
|
2190
2621
|
markStepDone(folder, callId, event.time, message.isError === true, message.isError === true ? textOf(message.content) : undefined);
|
|
2622
|
+
const resultStep = folder.stepByCall.get(callId);
|
|
2623
|
+
if (resultStep !== undefined && resultStep.result === undefined) {
|
|
2624
|
+
const captured = stepResultOf(message.content);
|
|
2625
|
+
if (captured !== null) {
|
|
2626
|
+
resultStep.result = captured.text;
|
|
2627
|
+
resultStep.resultChars = captured.chars;
|
|
2628
|
+
resultStep.resultTruncated = captured.truncated;
|
|
2629
|
+
resultStep.resultSkipped = captured.skipped;
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2191
2632
|
if (message.isError === true) {
|
|
2192
2633
|
folder.stats.errors += 1;
|
|
2193
2634
|
const errorBucket = folder.byTurn.get(folder.currentTurn);
|
|
@@ -2205,6 +2646,18 @@ window.__ModuleLoader__.load({
|
|
|
2205
2646
|
seq: event.seq,
|
|
2206
2647
|
time: event.time,
|
|
2207
2648
|
});
|
|
2649
|
+
} else if (pending.type === "plan" && typeof pending.text === "string" && pending.text.trim() !== "") {
|
|
2650
|
+
const planTitle = planTitleOf(pending.text);
|
|
2651
|
+
const planRefKey = `plan|${typeof event.seq === "number" ? event.seq : folder.currentTurn}`;
|
|
2652
|
+
const previewKey = rememberContextPreview(folder.sessionId, planRefKey, planTitle, pending.text, "plan");
|
|
2653
|
+
upsertArtifact(folder, bucket, `plan:${planTitle === "" ? planRefKey : planTitle}`, {
|
|
2654
|
+
plan: true,
|
|
2655
|
+
...(planTitle === "" ? {} : { planTitle }),
|
|
2656
|
+
...(previewKey === undefined ? {} : { previewKey }),
|
|
2657
|
+
...(typeof pending.callId === "string" && pending.callId !== "" ? { planCallId: pending.callId } : {}),
|
|
2658
|
+
seq: event.seq,
|
|
2659
|
+
time: event.time,
|
|
2660
|
+
});
|
|
2208
2661
|
} else {
|
|
2209
2662
|
const resultText = textOf(message.content);
|
|
2210
2663
|
for (const ref of pending.refs) {
|
|
@@ -2338,16 +2791,41 @@ window.__ModuleLoader__.load({
|
|
|
2338
2791
|
todosTime = own !== null ? own.time : 0;
|
|
2339
2792
|
}
|
|
2340
2793
|
const artifacts = [...source.artifacts.values()].sort((left, right) => (right.time || right.seq) - (left.time || left.seq) || right.seq - left.seq);
|
|
2794
|
+
/* 「本轮结论」: the closing reply is offered as a document, newest scope first. */
|
|
2795
|
+
const rawConclusion = isRecord(source.conclusion) ? source.conclusion : null;
|
|
2796
|
+
let conclusion = null;
|
|
2797
|
+
if (rawConclusion !== null && typeof rawConclusion.text === "string" && rawConclusion.text !== "") {
|
|
2798
|
+
const title = translate("artifact.conclusionTurn", { n: rawConclusion.turn });
|
|
2799
|
+
const previewKey = rememberContextPreview(folder.sessionId, `conclusion|${rawConclusion.turn}`, title, rawConclusion.text, "conclusion");
|
|
2800
|
+
conclusion = {
|
|
2801
|
+
path: `conclusion:${rawConclusion.turn}`,
|
|
2802
|
+
kind: "conclusion",
|
|
2803
|
+
title,
|
|
2804
|
+
description: "",
|
|
2805
|
+
chars: rawConclusion.text.length,
|
|
2806
|
+
turn: rawConclusion.turn,
|
|
2807
|
+
seq: rawConclusion.seq,
|
|
2808
|
+
time: rawConclusion.time,
|
|
2809
|
+
ended: rawConclusion.ended === true,
|
|
2810
|
+
...(previewKey === undefined ? {} : { previewKey }),
|
|
2811
|
+
};
|
|
2812
|
+
artifacts.unshift(conclusion);
|
|
2813
|
+
}
|
|
2341
2814
|
/* A ref is ubiquitous when it shows up in at least two turns covering >= 60% of
|
|
2342
2815
|
all observed turns — the per-turn OpenViking/runtime injections match this. */
|
|
2343
2816
|
const turnCount = turns.length;
|
|
2344
2817
|
const ubiquitousFrom = turnCount >= 2 ? Math.max(2, Math.ceil(turnCount * 0.6)) : Infinity;
|
|
2345
2818
|
const references = [];
|
|
2346
2819
|
for (let index = source.refOrder.length - 1; index >= 0; index -= 1) {
|
|
2347
|
-
const
|
|
2820
|
+
const key = source.refOrder[index];
|
|
2821
|
+
const ref = source.refs.get(key);
|
|
2348
2822
|
if (ref === undefined) continue;
|
|
2349
2823
|
const refTurns = Array.isArray(ref.turns) ? ref.turns.length : 0;
|
|
2350
|
-
|
|
2824
|
+
/* "common" is a session-wide property: a ref injected every turn stays common
|
|
2825
|
+
even while the view is narrowed to one turn. */
|
|
2826
|
+
const global = source === folder ? ref : folder.refs.get(key);
|
|
2827
|
+
const globalTurns = global !== undefined && Array.isArray(global.turns) ? global.turns.length : refTurns;
|
|
2828
|
+
references.push({ ...ref, refTurns, ubiquitous: globalTurns >= 2 && globalTurns >= ubiquitousFrom });
|
|
2351
2829
|
}
|
|
2352
2830
|
const st = isRecord(source.stats) ? source.stats : EMPTY_BUCKET.stats;
|
|
2353
2831
|
const stats = {
|
|
@@ -2396,10 +2874,13 @@ window.__ModuleLoader__.load({
|
|
|
2396
2874
|
turns,
|
|
2397
2875
|
latestTurn,
|
|
2398
2876
|
scope,
|
|
2877
|
+
/* how many turns this scope spans: "all" covers the session, a turn view spans 1 */
|
|
2878
|
+
scopeTurns: source === folder ? turns.length : 1,
|
|
2879
|
+
conclusion,
|
|
2399
2880
|
};
|
|
2400
2881
|
}
|
|
2401
2882
|
|
|
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" };
|
|
2883
|
+
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
2884
|
|
|
2404
2885
|
function createSummaryStore(binding) {
|
|
2405
2886
|
const source = binding.eventSource;
|
|
@@ -2562,33 +3043,82 @@ window.__ModuleLoader__.load({
|
|
|
2562
3043
|
return isRecord(goal) && typeof goal.objective === "string" ? goal : null;
|
|
2563
3044
|
}
|
|
2564
3045
|
|
|
2565
|
-
|
|
3046
|
+
/** Backfills older history. Returns true once several consecutive loads made no
|
|
3047
|
+
progress, so the UI can surface 「重新加载」 instead of an endless spinner. */
|
|
3048
|
+
function useBackfill(binding, hasMore, retryToken) {
|
|
3049
|
+
const [stalled, setStalled] = React.useState(false);
|
|
2566
3050
|
React.useEffect(() => {
|
|
3051
|
+
setStalled(false);
|
|
2567
3052
|
if (!binding || !hasMore) return undefined;
|
|
2568
3053
|
let cancelled = false;
|
|
2569
3054
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3055
|
+
/* every host read is guarded: a throwing getSnapshot would kill the loop and
|
|
3056
|
+
leave the status line up with no way back */
|
|
3057
|
+
const readSnapshot = () => {
|
|
3058
|
+
try {
|
|
3059
|
+
const source = binding.eventSource;
|
|
3060
|
+
return source !== undefined && typeof source.getSnapshot === "function" ? source.getSnapshot() : null;
|
|
3061
|
+
} catch {
|
|
3062
|
+
return null;
|
|
3063
|
+
}
|
|
3064
|
+
};
|
|
3065
|
+
const sizeOf = (snapshot) => (isRecord(snapshot) && Array.isArray(snapshot.entries) ? snapshot.entries.length : 0);
|
|
3066
|
+
const session = binding.session;
|
|
3067
|
+
const canLoadThrough = isRecord(session) && typeof session.loadThrough === "function";
|
|
3068
|
+
const canLoadOlder = isRecord(session) && typeof session.loadOlder === "function";
|
|
2570
3069
|
const run = async () => {
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
3070
|
+
if (!canLoadThrough && !canLoadOlder) {
|
|
3071
|
+
/* no loader at all: this can never converge, so say so instead of spinning */
|
|
3072
|
+
setStalled(true);
|
|
3073
|
+
return;
|
|
3074
|
+
}
|
|
3075
|
+
/* The jump loader pages back inside one call until the window covers the
|
|
3076
|
+
target seq. Event seqs are dense and ZERO-based, so the target must be
|
|
3077
|
+
0: aiming at 1 leaves baseSeq stuck at 1, and from then on the host's
|
|
3078
|
+
own `baseSeq <= seq` guard makes every retry a permanent no-op. */
|
|
3079
|
+
let lastSize = sizeOf(readSnapshot());
|
|
3080
|
+
let noProgress = 0;
|
|
3081
|
+
let usedOlder = 0;
|
|
3082
|
+
let resynced = false;
|
|
3083
|
+
const deadline = Date.now() + 45000;
|
|
3084
|
+
for (let attempt = 0; attempt < 400 && !cancelled && Date.now() < deadline; attempt += 1) {
|
|
3085
|
+
const snapshot = readSnapshot();
|
|
2576
3086
|
if (!isRecord(snapshot) || snapshot.hasMore !== true) return;
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
3087
|
+
try {
|
|
3088
|
+
/* A no-op page means the jump loader is blocked (another loader holds
|
|
3089
|
+
the lock, or the seq target is already covered): loadOlder() is
|
|
3090
|
+
seq-free and still prepends a real page, so alternate to it. If even
|
|
3091
|
+
that stalls, rebuild the history source once. */
|
|
3092
|
+
if (usedOlder < 2 || !canLoadOlder) {
|
|
3093
|
+
if (canLoadThrough) await session.loadThrough(0);
|
|
3094
|
+
else await session.loadOlder();
|
|
3095
|
+
} else if (!resynced && typeof session.resync === "function") {
|
|
3096
|
+
resynced = true;
|
|
3097
|
+
await session.resync();
|
|
3098
|
+
} else {
|
|
3099
|
+
await session.loadOlder();
|
|
3100
|
+
}
|
|
3101
|
+
} catch {
|
|
3102
|
+
/* a rejected page must not kill the loop; count it as no progress */
|
|
3103
|
+
}
|
|
2581
3104
|
if (cancelled) return;
|
|
2582
|
-
const after =
|
|
3105
|
+
const after = readSnapshot();
|
|
2583
3106
|
if (isRecord(after) && after.hasMore !== true) return;
|
|
2584
|
-
|
|
3107
|
+
const size = sizeOf(after);
|
|
3108
|
+
const grew = size > lastSize;
|
|
3109
|
+
lastSize = size;
|
|
3110
|
+
noProgress = grew ? 0 : noProgress + 1;
|
|
3111
|
+
usedOlder = grew ? 0 : usedOlder + 1;
|
|
3112
|
+
if (noProgress === 4) setStalled(true);
|
|
3113
|
+
await sleep(grew ? 60 : 200);
|
|
2585
3114
|
}
|
|
2586
3115
|
};
|
|
2587
3116
|
run().catch(() => {});
|
|
2588
3117
|
return () => {
|
|
2589
3118
|
cancelled = true;
|
|
2590
3119
|
};
|
|
2591
|
-
}, [binding, hasMore]);
|
|
3120
|
+
}, [binding, hasMore, retryToken]);
|
|
3121
|
+
return stalled;
|
|
2592
3122
|
}
|
|
2593
3123
|
|
|
2594
3124
|
/** Class map mirroring the copied FileTypeIcon.module.css, renamed under dsov-ft-*. */
|
|
@@ -3319,6 +3849,49 @@ function FileTypeIcon(props) {
|
|
|
3319
3849
|
]);
|
|
3320
3850
|
}
|
|
3321
3851
|
|
|
3852
|
+
function IconPlan({ size = 16, className }) {
|
|
3853
|
+
return svg(size, className, [
|
|
3854
|
+
h("rect", { key: "a", x: 3.2, y: 2.2, width: 9.6, height: 11.6, rx: 1.5, stroke: "currentColor", fill: "none" }),
|
|
3855
|
+
h("path", { key: "b", d: "M5.6 5.6h4.8M5.6 8.1h4.8M5.6 10.6h2.9", stroke: "currentColor", strokeLinecap: "round" }),
|
|
3856
|
+
]);
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
function IconInfo({ size = 14, className }) {
|
|
3860
|
+
return svg(size, className, [
|
|
3861
|
+
h("circle", { key: "a", cx: 8, cy: 8, r: 6.1, stroke: "currentColor", fill: "none" }),
|
|
3862
|
+
h("path", { key: "b", d: "M8 7.2v3.9", stroke: "currentColor", strokeLinecap: "round" }),
|
|
3863
|
+
h("circle", { key: "c", cx: 8, cy: 5.1, r: 0.85, fill: "currentColor" }),
|
|
3864
|
+
], "0 0 16 16");
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
/** Chrome copy the chat Markdown component expects (code banner, footnotes). */
|
|
3868
|
+
function chatMarkdownLabels(t) {
|
|
3869
|
+
return {
|
|
3870
|
+
code: {
|
|
3871
|
+
copyLabel: t("markdown.copy"),
|
|
3872
|
+
copiedLabel: t("markdown.copied"),
|
|
3873
|
+
toolbarLabels: {
|
|
3874
|
+
codeLabel: t("markdown.code"),
|
|
3875
|
+
wrapLabel: t("markdown.wrap"),
|
|
3876
|
+
unwrapLabel: t("markdown.unwrap"),
|
|
3877
|
+
},
|
|
3878
|
+
},
|
|
3879
|
+
footnotes: t("markdown.footnotes"),
|
|
3880
|
+
};
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
/** Turn conclusion: a written page with a sealed tick on its corner. */
|
|
3884
|
+
function IconConclusion({ size = 16, className }) {
|
|
3885
|
+
return svg(size, className, [
|
|
3886
|
+
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 }),
|
|
3887
|
+
h("path", { key: "b", d: "M9.1 1.8v3.7h3.9", stroke: "currentColor", strokeLinejoin: "round", fill: "none", strokeWidth: 1.25 }),
|
|
3888
|
+
h("path", { key: "c", d: "M9.1 1.8v3.7h3.6V5.6L9.1 1.8Z", stroke: "currentColor", strokeLinejoin: "round", fill: "none", strokeWidth: 1.25 }),
|
|
3889
|
+
h("path", { key: "d", d: "M5.2 7.3h4.7M5.2 9.4h3", stroke: "currentColor", strokeLinecap: "round", strokeWidth: 1.25 }),
|
|
3890
|
+
h("circle", { key: "e", cx: 11.7, cy: 12.1, r: 2.55, fill: "currentColor" }),
|
|
3891
|
+
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" }),
|
|
3892
|
+
], "0 0 16 16");
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3322
3895
|
function IconSkill({ size = 16, className }) {
|
|
3323
3896
|
return svg(size, className, [
|
|
3324
3897
|
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 +4136,34 @@ function FileTypeIcon(props) {
|
|
|
3563
4136
|
}
|
|
3564
4137
|
|
|
3565
4138
|
/** Incoming order is newest-first; kind/count modes re-rank, breaking ties by recency. */
|
|
3566
|
-
|
|
4139
|
+
/** Mentions of one reference: folds can count one turn twice, so this is not turn coverage. */
|
|
4140
|
+
function refHitsOf(ref) {
|
|
4141
|
+
return isRecord(ref) && typeof ref.count === "number" && ref.count > 0 ? ref.count : 1;
|
|
4142
|
+
}
|
|
4143
|
+
|
|
4144
|
+
/** Distinct turns that mentioned one reference; a single turn can fold it twice. */
|
|
4145
|
+
function refTurnsOf(ref) {
|
|
4146
|
+
if (isRecord(ref) && typeof ref.refTurns === "number") return ref.refTurns;
|
|
4147
|
+
return isRecord(ref) && Array.isArray(ref.turns) ? ref.turns.length : 0;
|
|
4148
|
+
}
|
|
4149
|
+
|
|
4150
|
+
function sortReferences(refs, mode, multiTurn) {
|
|
3567
4151
|
const byRecency = (left, right) => (right.time || right.seq || 0) - (left.time || left.seq || 0) || (right.seq || 0) - (left.seq || 0);
|
|
3568
|
-
if (mode === "count")
|
|
4152
|
+
if (mode === "count") {
|
|
4153
|
+
if (multiTurn !== true) return refs.slice().sort((left, right) => refHitsOf(right) - refHitsOf(left) || byRecency(left, right));
|
|
4154
|
+
return refs.slice().sort((left, right) => refTurnsOf(right) - refTurnsOf(left) || refHitsOf(right) - refHitsOf(left) || byRecency(left, right));
|
|
4155
|
+
}
|
|
3569
4156
|
return refs;
|
|
3570
4157
|
}
|
|
3571
4158
|
|
|
3572
|
-
|
|
4159
|
+
/** Alternating animation names let the CSS entrance replay without remounting
|
|
4160
|
+
(a keyed remount would drop collapse/scroll state on every new turn). */
|
|
4161
|
+
function sectionIntroProps(introSeq, delayMs) {
|
|
4162
|
+
if (!Number.isFinite(introSeq) || introSeq <= 0) return {};
|
|
4163
|
+
return { introAttr: introSeq % 2 === 0 ? "even" : "odd", introDelay: delayMs };
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
function Section({ id, icon, title, badge, empty, action, children, introAttr, introDelay }) {
|
|
3573
4167
|
const [open, setOpen] = React.useState(() => collapsedSections.get(id) !== true);
|
|
3574
4168
|
const cfg = useConfig();
|
|
3575
4169
|
const kids = Array.isArray(children) ? children.filter((child) => child !== null && child !== undefined && child !== false) : children ? [children] : [];
|
|
@@ -3580,7 +4174,12 @@ function FileTypeIcon(props) {
|
|
|
3580
4174
|
};
|
|
3581
4175
|
return h(
|
|
3582
4176
|
"section",
|
|
3583
|
-
{
|
|
4177
|
+
{
|
|
4178
|
+
className: "dsov-section",
|
|
4179
|
+
"data-full": cfg.sectionFullHeight === true ? "true" : "false",
|
|
4180
|
+
"data-intro": introAttr,
|
|
4181
|
+
style: typeof introDelay === "number" ? { animationDelay: `${introDelay}ms` } : undefined,
|
|
4182
|
+
},
|
|
3584
4183
|
h(
|
|
3585
4184
|
"div",
|
|
3586
4185
|
{ className: "dsov-sectionHead" },
|
|
@@ -3626,7 +4225,7 @@ function FileTypeIcon(props) {
|
|
|
3626
4225
|
complete: "success",
|
|
3627
4226
|
};
|
|
3628
4227
|
|
|
3629
|
-
function ProgressSection({ summary, goal, t, waiting, cfg, asOf }) {
|
|
4228
|
+
function ProgressSection({ summary, goal, t, waiting, cfg, asOf, introSeq }) {
|
|
3630
4229
|
const fmtTime = (ts) => formatTime(ts, cfg.timeMode, summary.turnStart);
|
|
3631
4230
|
const timeTitle = (ts) => (cfg.timeMode === "elapsed" ? formatTime(ts) : undefined);
|
|
3632
4231
|
/* timeline scrubbing: only what already happened at `asOf` is shown */
|
|
@@ -3739,14 +4338,59 @@ function FileTypeIcon(props) {
|
|
|
3739
4338
|
if (!stepAt(candidate)) continue;
|
|
3740
4339
|
const key = milestoneKeyOf(candidate);
|
|
3741
4340
|
if (baseKeys.has(key)) continue;
|
|
4341
|
+
if (typeof candidate.callId === "string" && nestedApprovalCallIds.has(candidate.callId)) continue;
|
|
3742
4342
|
baseKeys.add(key);
|
|
3743
4343
|
milestoneExtras.push(stepClamped(candidate));
|
|
3744
4344
|
}
|
|
3745
4345
|
milestoneExtras.sort((left, right) => (left.time || 0) - (right.time || 0));
|
|
3746
4346
|
}
|
|
3747
4347
|
const displaySteps = milestoneExtras.length > 0 ? [...milestoneExtras, ...(stepsOpen ? expandedSteps : steps)] : stepsOpen ? expandedSteps : steps;
|
|
4348
|
+
/* An approval gates one tool call; render it directly under that call so a
|
|
4349
|
+
child row can never look like it finished before its own parent. */
|
|
4350
|
+
const approvalChildren = new Map();
|
|
4351
|
+
const nestedApprovalKeys = new Set();
|
|
4352
|
+
const nestedApprovalCallIds = new Set();
|
|
4353
|
+
{
|
|
4354
|
+
const keyByCall = new Map();
|
|
4355
|
+
displaySteps.forEach((step, index) => {
|
|
4356
|
+
if (typeof step.callId === "string" && step.callId !== "") keyByCall.set(step.callId, stepKeyOf(step, index));
|
|
4357
|
+
});
|
|
4358
|
+
displaySteps.forEach((step, index) => {
|
|
4359
|
+
if (step.name !== "approval" || typeof step.parentCallId !== "string" || step.parentCallId === "") return;
|
|
4360
|
+
const parentKey = keyByCall.get(step.parentCallId);
|
|
4361
|
+
const ownKey = stepKeyOf(step, index);
|
|
4362
|
+
if (parentKey === undefined || parentKey === ownKey) return;
|
|
4363
|
+
const list = approvalChildren.get(parentKey);
|
|
4364
|
+
if (list === undefined) approvalChildren.set(parentKey, [step]);
|
|
4365
|
+
else list.push(step);
|
|
4366
|
+
nestedApprovalKeys.add(ownKey);
|
|
4367
|
+
if (typeof step.callId === "string") nestedApprovalCallIds.add(step.callId);
|
|
4368
|
+
});
|
|
4369
|
+
}
|
|
4370
|
+
const renderSteps = nestedApprovalKeys.size === 0 ? displaySteps : displaySteps.filter((step, index) => !nestedApprovalKeys.has(stepKeyOf(step, index)));
|
|
4371
|
+
/* A step the user expanded while it ran closes itself once it succeeds; a failed
|
|
4372
|
+
step stays open so the error stays readable. */
|
|
4373
|
+
const runningKeys = [];
|
|
4374
|
+
const stepByKey = new Map();
|
|
4375
|
+
renderSteps.forEach((step, index) => {
|
|
4376
|
+
const key = stepKeyOf(step, index);
|
|
4377
|
+
stepByKey.set(key, step);
|
|
4378
|
+
if (step.done !== true && step.error !== true) runningKeys.push(key);
|
|
4379
|
+
});
|
|
4380
|
+
const runningSig = runningKeys.join("|");
|
|
4381
|
+
const prevRunningKeys = React.useRef(new Set());
|
|
4382
|
+
React.useEffect(() => {
|
|
4383
|
+
const now = new Set(runningKeys);
|
|
4384
|
+
for (const key of prevRunningKeys.current) {
|
|
4385
|
+
if (now.has(key)) continue;
|
|
4386
|
+
const step = stepByKey.get(key);
|
|
4387
|
+
if (step === undefined || step.done !== true || step.error === true) continue;
|
|
4388
|
+
if (overrideOf(`step:${key}`, false)) toggleKey(`step:${key}`, false);
|
|
4389
|
+
}
|
|
4390
|
+
prevRunningKeys.current = now;
|
|
4391
|
+
}, [runningSig]);
|
|
3748
4392
|
if (seenSteps.current === null || seenSteps.current.scope !== summary.scope) {
|
|
3749
|
-
seenSteps.current = { scope: summary.scope, keys: new Set(
|
|
4393
|
+
seenSteps.current = { scope: summary.scope, keys: new Set(renderSteps.map(stepKeyOf)) };
|
|
3750
4394
|
}
|
|
3751
4395
|
React.useEffect(
|
|
3752
4396
|
() => () => {
|
|
@@ -3959,6 +4603,106 @@ function FileTypeIcon(props) {
|
|
|
3959
4603
|
target.push(subRow(sub, 0));
|
|
3960
4604
|
});
|
|
3961
4605
|
};
|
|
4606
|
+
/* What an expanded step reveals: the full command/arg text plus the result. */
|
|
4607
|
+
const stepBodyOf = (step) => {
|
|
4608
|
+
if (step.name === "approval") return null;
|
|
4609
|
+
const command = typeof step.full === "string" ? step.full.trim() : "";
|
|
4610
|
+
const rawResult = step.error === true && typeof step.err === "string" && step.err !== "" ? step.err : typeof step.result === "string" ? step.result : "";
|
|
4611
|
+
const result = rawResult.trim();
|
|
4612
|
+
if (command === "" && result === "") return null;
|
|
4613
|
+
const duration = typeof step.endTime === "number" && typeof step.time === "number" ? Math.max(0, step.endTime - step.time) : null;
|
|
4614
|
+
return {
|
|
4615
|
+
command,
|
|
4616
|
+
result,
|
|
4617
|
+
error: step.error === true,
|
|
4618
|
+
duration,
|
|
4619
|
+
chars: typeof step.resultChars === "number" ? step.resultChars : null,
|
|
4620
|
+
truncated: step.resultTruncated === true,
|
|
4621
|
+
skipped: step.resultSkipped === true,
|
|
4622
|
+
};
|
|
4623
|
+
};
|
|
4624
|
+
/* 审批 is part of the step it gates, not a neighbour: it leads the detail panel
|
|
4625
|
+
so the gate reads before the command it unlocked, and the row carries a chip. */
|
|
4626
|
+
const approvalToneOf = (child) => {
|
|
4627
|
+
if (child.done !== true && child.error !== true) return "warn";
|
|
4628
|
+
return /denied|reject|deny/i.test(typeof child.approvalOutcome === "string" ? child.approvalOutcome : "") ? "err" : "ok";
|
|
4629
|
+
};
|
|
4630
|
+
const approvalTitleOf = (approvals, tr) => {
|
|
4631
|
+
const parts = [];
|
|
4632
|
+
for (const child of approvals) {
|
|
4633
|
+
const tool = typeof child.approvalTool === "string" && child.approvalTool !== "" ? child.approvalTool : typeof child.detail === "string" ? child.detail : "";
|
|
4634
|
+
const reason = typeof child.approvalReason === "string" ? child.approvalReason : "";
|
|
4635
|
+
const outcome = typeof child.approvalOutcome === "string" ? child.approvalOutcome : "";
|
|
4636
|
+
const tone = approvalToneOf(child);
|
|
4637
|
+
const state = tone === "warn" ? tr("step.approvalPending") : outcome;
|
|
4638
|
+
const line = [tool, reason, state].filter((part) => typeof part === "string" && part !== "").join(" · ");
|
|
4639
|
+
if (line !== "") parts.push(line);
|
|
4640
|
+
}
|
|
4641
|
+
return parts.length === 0 ? undefined : parts.join("\n");
|
|
4642
|
+
};
|
|
4643
|
+
const approvalBlock = (approvals) => {
|
|
4644
|
+
if (!Array.isArray(approvals) || approvals.length === 0) return null;
|
|
4645
|
+
return h(
|
|
4646
|
+
"div",
|
|
4647
|
+
{ className: "dsov-stepApproval", key: "approval" },
|
|
4648
|
+
approvals.map((child) => {
|
|
4649
|
+
const tone = approvalToneOf(child);
|
|
4650
|
+
const tool = typeof child.approvalTool === "string" && child.approvalTool !== "" ? child.approvalTool : typeof child.detail === "string" ? child.detail : "";
|
|
4651
|
+
const reason = typeof child.approvalReason === "string" ? child.approvalReason : "";
|
|
4652
|
+
const outcome = typeof child.approvalOutcome === "string" ? child.approvalOutcome : "";
|
|
4653
|
+
const label = tone === "warn" ? t("step.approvalPending") : tone === "err" ? t("step.approvalDenied") : t("step.approvalApproved");
|
|
4654
|
+
const stamp = typeof child.endTime === "number" ? child.endTime : child.time;
|
|
4655
|
+
return h(
|
|
4656
|
+
"div",
|
|
4657
|
+
{ className: "dsov-stepApprovalItem", key: typeof child.callId === "string" ? child.callId : "approval", "data-tone": tone },
|
|
4658
|
+
h(
|
|
4659
|
+
"div",
|
|
4660
|
+
{ className: "dsov-stepApprovalHead" },
|
|
4661
|
+
h("span", { className: "dsov-stepApprovalIcon" }, h(IconTarget, { size: 11 })),
|
|
4662
|
+
h("span", { className: "dsov-stepApprovalName" }, t("step.approval")),
|
|
4663
|
+
tool !== "" ? h("span", { className: "dsov-stepApprovalTool" }, tool) : null,
|
|
4664
|
+
h("span", { className: "dsov-stepApprovalBadge", "data-tone": tone }, label),
|
|
4665
|
+
h("span", { className: "dsov-stepApprovalTime" }, fmtTime(stamp))
|
|
4666
|
+
),
|
|
4667
|
+
reason !== "" ? h("div", { className: "dsov-stepApprovalReason" }, reason) : null,
|
|
4668
|
+
outcome !== "" && tone !== "warn" ? h("div", { className: "dsov-stepApprovalOutcome" }, outcome) : null
|
|
4669
|
+
);
|
|
4670
|
+
})
|
|
4671
|
+
);
|
|
4672
|
+
};
|
|
4673
|
+
const stepBodyBlock = (step, key, approvals) => {
|
|
4674
|
+
const body = stepBodyOf(step);
|
|
4675
|
+
const gated = approvalBlock(approvals);
|
|
4676
|
+
if (body === null && gated === null) return null;
|
|
4677
|
+
const meta = [];
|
|
4678
|
+
if (body !== null && body.duration !== null) meta.push(t("progress.stepDuration", { v: formatElapsed(body.duration) }));
|
|
4679
|
+
if (body !== null && body.chars !== null && body.chars > 0) meta.push(t("progress.stepChars", { n: body.chars }));
|
|
4680
|
+
if (body !== null && body.skipped) meta.push(t("progress.stepSkipped"));
|
|
4681
|
+
else if (body !== null && body.truncated) meta.push(t("progress.stepTruncated"));
|
|
4682
|
+
return h(
|
|
4683
|
+
"div",
|
|
4684
|
+
{ className: "dsov-stepBody", key: `${key}-body` },
|
|
4685
|
+
gated,
|
|
4686
|
+
body !== null && body.command !== ""
|
|
4687
|
+
? h(
|
|
4688
|
+
"div",
|
|
4689
|
+
{ className: "dsov-stepBodySec" },
|
|
4690
|
+
h("div", { className: "dsov-stepBodyLabel" }, t("progress.stepCommand")),
|
|
4691
|
+
h("pre", { className: "dsov-stepBodyText" }, body.command)
|
|
4692
|
+
)
|
|
4693
|
+
: null,
|
|
4694
|
+
body !== null && body.result !== ""
|
|
4695
|
+
? h(
|
|
4696
|
+
"div",
|
|
4697
|
+
{ className: "dsov-stepBodySec" },
|
|
4698
|
+
h("div", { className: "dsov-stepBodyLabel" }, t(body.error ? "progress.stepError" : "progress.stepResult")),
|
|
4699
|
+
h("pre", { className: body.error ? "dsov-stepBodyText dsov-stepBodyErr" : "dsov-stepBodyText" }, body.result)
|
|
4700
|
+
)
|
|
4701
|
+
: null,
|
|
4702
|
+
meta.length > 0 ? h("div", { className: "dsov-stepBodyMeta" }, meta.join(" · ")) : null
|
|
4703
|
+
);
|
|
4704
|
+
};
|
|
4705
|
+
|
|
3962
4706
|
const stepRow = (step, key, extraProps, isWaiting) => {
|
|
3963
4707
|
const stepRunning = step.done !== true;
|
|
3964
4708
|
const failed = step.error === true;
|
|
@@ -3967,8 +4711,79 @@ function FileTypeIcon(props) {
|
|
|
3967
4711
|
const full = typeof step.full === "string" && step.full !== "" ? step.full : detail !== "" ? detail : typeof step.name === "string" ? step.name : "";
|
|
3968
4712
|
const flash = stepFlash !== null && isRecord(stepFlash) && stepFlash.key === (typeof step.callId === "string" && step.callId !== "" ? step.callId : `${step.name}|${step.seq}`) ? stepFlash.state : null;
|
|
3969
4713
|
const attached = stepAttachments.get(key);
|
|
3970
|
-
const
|
|
3971
|
-
const
|
|
4714
|
+
const approvals = approvalChildren.get(key);
|
|
4715
|
+
const dying = extraProps !== undefined && extraProps["data-exit"] === "true";
|
|
4716
|
+
const body = dying ? null : stepBodyOf(step);
|
|
4717
|
+
const gated = !dying && Array.isArray(approvals) && approvals.length > 0;
|
|
4718
|
+
const pendingApproval = gated && approvals.some((child) => approvalToneOf(child) === "warn");
|
|
4719
|
+
const expandable = (Array.isArray(attached) && attached.length > 0) || body !== null || gated;
|
|
4720
|
+
/* team rows keep their chevron visible; a plain step reveals it on hover only */
|
|
4721
|
+
const persistentChevron = Array.isArray(attached) && attached.length > 0;
|
|
4722
|
+
const open = expandable ? overrideOf(`step:${key}`, (Array.isArray(attached) && attached.some((item) => item.unfinished)) || pendingApproval) : false;
|
|
4723
|
+
const statusIcon =
|
|
4724
|
+
step.name === "compact-checkpoint"
|
|
4725
|
+
? h("span", { className: "dsov-todoIcon dsov-compactIcon" }, h(IconCompact, { size: 15 }))
|
|
4726
|
+
: waitingRow
|
|
4727
|
+
? h(
|
|
4728
|
+
"span",
|
|
4729
|
+
{ className: "dsov-todoIcon", "data-status": "waiting" },
|
|
4730
|
+
h(
|
|
4731
|
+
"svg",
|
|
4732
|
+
{ width: 15, height: 15, viewBox: "0 0 15 15", fill: "none", "aria-hidden": "true" },
|
|
4733
|
+
h("circle", { cx: 7.5, cy: 7.5, r: 6, stroke: "currentColor", strokeWidth: 1.3, opacity: 0.45 }),
|
|
4734
|
+
h("circle", { cx: 7.5, cy: 7.5, r: 2.4, fill: "currentColor", className: "dsov-todoPulse" })
|
|
4735
|
+
)
|
|
4736
|
+
)
|
|
4737
|
+
: h(
|
|
4738
|
+
"span",
|
|
4739
|
+
{ className: "dsov-todoIcon", "data-status": stepRunning ? "in_progress" : "completed" },
|
|
4740
|
+
h(TodoStatusIcon, { status: stepRunning ? "in_progress" : "completed" })
|
|
4741
|
+
);
|
|
4742
|
+
/* The expand affordance is the status icon itself: hovering swaps it for a
|
|
4743
|
+
chevron in place, so nothing shifts and no second column appears. Team rows
|
|
4744
|
+
keep their own always-visible chevron ahead of the status icon. */
|
|
4745
|
+
const swapChevron = h(
|
|
4746
|
+
"span",
|
|
4747
|
+
{ className: "dsov-chevron dsov-stepSwapChevron", "data-open": open ? "true" : "false", "aria-hidden": "true" },
|
|
4748
|
+
h(IconChevron, { size: 12 })
|
|
4749
|
+
);
|
|
4750
|
+
const leading = !expandable
|
|
4751
|
+
? statusIcon
|
|
4752
|
+
: persistentChevron
|
|
4753
|
+
? h(
|
|
4754
|
+
React.Fragment,
|
|
4755
|
+
{ key: "lead" },
|
|
4756
|
+
h(
|
|
4757
|
+
"span",
|
|
4758
|
+
{
|
|
4759
|
+
className: "dsov-chevron dsov-stepChevron",
|
|
4760
|
+
key: "chevron",
|
|
4761
|
+
"data-open": open ? "true" : "false",
|
|
4762
|
+
title: t(open ? "progress.collapseTeam" : "progress.expandTeam"),
|
|
4763
|
+
onClick: (event) => {
|
|
4764
|
+
event.stopPropagation();
|
|
4765
|
+
toggleKey(`step:${key}`, !open);
|
|
4766
|
+
},
|
|
4767
|
+
},
|
|
4768
|
+
h(IconChevron, { size: 11 })
|
|
4769
|
+
),
|
|
4770
|
+
statusIcon
|
|
4771
|
+
)
|
|
4772
|
+
: h(
|
|
4773
|
+
"span",
|
|
4774
|
+
{
|
|
4775
|
+
className: "dsov-stepIconSwap",
|
|
4776
|
+
key: "swap",
|
|
4777
|
+
"data-open": open ? "true" : "false",
|
|
4778
|
+
title: t(open ? "progress.collapseStep" : "progress.expandStep"),
|
|
4779
|
+
onClick: (event) => {
|
|
4780
|
+
event.stopPropagation();
|
|
4781
|
+
toggleKey(`step:${key}`, !open);
|
|
4782
|
+
},
|
|
4783
|
+
},
|
|
4784
|
+
statusIcon,
|
|
4785
|
+
swapChevron
|
|
4786
|
+
);
|
|
3972
4787
|
const row = h(
|
|
3973
4788
|
"div",
|
|
3974
4789
|
{
|
|
@@ -3981,24 +4796,67 @@ function FileTypeIcon(props) {
|
|
|
3981
4796
|
...(extraProps === undefined ? {} : extraProps),
|
|
3982
4797
|
onClick: () => onStepClick(step),
|
|
3983
4798
|
},
|
|
3984
|
-
|
|
4799
|
+
leading,
|
|
4800
|
+
h(
|
|
4801
|
+
"span",
|
|
4802
|
+
{ className: "dsov-stepName" },
|
|
4803
|
+
step.name === "compact-checkpoint" ? t("step.compact") : step.name === "approval" ? t("step.approval") : typeof step.name === "string" ? step.name : ""
|
|
4804
|
+
),
|
|
4805
|
+
h("span", { className: "dsov-stepDetail" }, detail),
|
|
4806
|
+
gated
|
|
3985
4807
|
? h(
|
|
3986
4808
|
"span",
|
|
3987
4809
|
{
|
|
3988
|
-
className: "dsov-
|
|
3989
|
-
"data-
|
|
3990
|
-
title:
|
|
3991
|
-
onClick: (event) => {
|
|
3992
|
-
event.stopPropagation();
|
|
3993
|
-
toggleKey(`step:${key}`, !open);
|
|
3994
|
-
},
|
|
4810
|
+
className: "dsov-stepChip",
|
|
4811
|
+
"data-tone": approvalToneOf(approvals[0]),
|
|
4812
|
+
title: approvalTitleOf(approvals, t),
|
|
3995
4813
|
},
|
|
3996
|
-
|
|
4814
|
+
approvalToneOf(approvals[0]) === "warn" ? t("step.approvalPending") : t("step.approvalGated")
|
|
3997
4815
|
)
|
|
3998
4816
|
: null,
|
|
3999
|
-
step.
|
|
4000
|
-
|
|
4001
|
-
|
|
4817
|
+
h("span", { className: "dsov-time", title: timeTitle(typeof step.endTime === "number" ? step.endTime : step.time) }, fmtTime(typeof step.endTime === "number" ? step.endTime : step.time))
|
|
4818
|
+
);
|
|
4819
|
+
if (!open) return row;
|
|
4820
|
+
const extras = [];
|
|
4821
|
+
if (body !== null || gated) {
|
|
4822
|
+
const block = stepBodyBlock(step, key, approvals);
|
|
4823
|
+
if (block !== null) extras.push(block);
|
|
4824
|
+
}
|
|
4825
|
+
if (Array.isArray(attached)) for (const item of attached) extras.push(item.row);
|
|
4826
|
+
return extras.length === 0 ? row : [row, ...extras];
|
|
4827
|
+
};
|
|
4828
|
+
/* Nested approval row: its own two-line layout, because reusing the flat step
|
|
4829
|
+
row squeezed the reason into the ellipsised detail column (~38px in a 332px
|
|
4830
|
+
sidebar) and the text looked clipped away. The reason now wraps on its own
|
|
4831
|
+
line; the outcome becomes a small badge instead of trailing detail text. */
|
|
4832
|
+
const approvalChildRow = (step, key, isWaiting) => {
|
|
4833
|
+
const running = step.done !== true;
|
|
4834
|
+
const failed = step.error === true;
|
|
4835
|
+
const waitingRow = isWaiting === true && running && !failed;
|
|
4836
|
+
const tool = typeof step.approvalTool === "string" && step.approvalTool !== "" ? step.approvalTool : typeof step.detail === "string" ? step.detail : "";
|
|
4837
|
+
const reason = typeof step.approvalReason === "string" ? step.approvalReason : "";
|
|
4838
|
+
const outcome = typeof step.approvalOutcome === "string" ? step.approvalOutcome : "";
|
|
4839
|
+
const outcomeText =
|
|
4840
|
+
outcome === ""
|
|
4841
|
+
? ""
|
|
4842
|
+
: /approved|allow/i.test(outcome)
|
|
4843
|
+
? t("step.approvalApproved")
|
|
4844
|
+
: /denied|reject|deny/i.test(outcome)
|
|
4845
|
+
? t("step.approvalDenied")
|
|
4846
|
+
: outcome;
|
|
4847
|
+
const outcomeOk = !/denied|reject|deny/i.test(outcome);
|
|
4848
|
+
const stamp = typeof step.endTime === "number" ? step.endTime : step.time;
|
|
4849
|
+
const hoverText = [tool, reason, outcome].filter((part) => typeof part === "string" && part !== "").join(" · ");
|
|
4850
|
+
return h(
|
|
4851
|
+
"div",
|
|
4852
|
+
{
|
|
4853
|
+
className: "dsov-step dsov-stepChild",
|
|
4854
|
+
key,
|
|
4855
|
+
"data-error": failed ? "true" : "false",
|
|
4856
|
+
"data-waiting": waitingRow ? "true" : "false",
|
|
4857
|
+
title: hoverText === "" ? undefined : hoverText,
|
|
4858
|
+
},
|
|
4859
|
+
waitingRow
|
|
4002
4860
|
? h(
|
|
4003
4861
|
"span",
|
|
4004
4862
|
{ className: "dsov-todoIcon", "data-status": "waiting" },
|
|
@@ -4009,17 +4867,29 @@ function FileTypeIcon(props) {
|
|
|
4009
4867
|
h("circle", { cx: 7.5, cy: 7.5, r: 2.4, fill: "currentColor", className: "dsov-todoPulse" })
|
|
4010
4868
|
)
|
|
4011
4869
|
)
|
|
4012
|
-
: h(
|
|
4870
|
+
: h(
|
|
4871
|
+
"span",
|
|
4872
|
+
{ className: "dsov-todoIcon", "data-status": running ? "in_progress" : "completed" },
|
|
4873
|
+
h(TodoStatusIcon, { status: running ? "in_progress" : "completed" })
|
|
4874
|
+
),
|
|
4013
4875
|
h(
|
|
4014
|
-
"
|
|
4015
|
-
{ className: "dsov-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4876
|
+
"div",
|
|
4877
|
+
{ className: "dsov-stepChildBody" },
|
|
4878
|
+
h(
|
|
4879
|
+
"div",
|
|
4880
|
+
{ className: "dsov-stepChildTop" },
|
|
4881
|
+
h("span", { className: "dsov-stepName" }, t("step.approval")),
|
|
4882
|
+
tool !== "" ? h("span", { className: "dsov-stepChildTool" }, tool) : null,
|
|
4883
|
+
outcomeText !== ""
|
|
4884
|
+
? h("span", { className: "dsov-stepChildOutcome", "data-ok": outcomeOk ? "true" : "false" }, outcomeText)
|
|
4885
|
+
: null,
|
|
4886
|
+
h("span", { className: "dsov-time", title: timeTitle(stamp) }, fmtTime(stamp))
|
|
4887
|
+
),
|
|
4888
|
+
reason !== "" ? h("div", { className: "dsov-stepChildNote" }, reason) : null
|
|
4889
|
+
)
|
|
4020
4890
|
);
|
|
4021
|
-
return expandable && open ? [row, ...attached.map((item) => item.row)] : row;
|
|
4022
4891
|
};
|
|
4892
|
+
|
|
4023
4893
|
const pushStepRow = (target, step, key, extraProps, isWaiting) => {
|
|
4024
4894
|
const rendered = stepRow(step, key, extraProps, isWaiting);
|
|
4025
4895
|
if (Array.isArray(rendered)) target.push(...rendered);
|
|
@@ -4142,10 +5012,17 @@ function FileTypeIcon(props) {
|
|
|
4142
5012
|
if (hasTeam && !showSteps) pushFlatTeam(kids);
|
|
4143
5013
|
if (showSteps) {
|
|
4144
5014
|
const markWaiting = waiting === true && summary.scope === "latest";
|
|
5015
|
+
/* a step blocked on an approval is not the thing being waited on: the
|
|
5016
|
+
nested approval row carries the waiting state instead */
|
|
5017
|
+
const approvalBlocked = new Set();
|
|
5018
|
+
for (const [parentKey, list] of approvalChildren) {
|
|
5019
|
+
if (list.some((child) => child.done !== true && child.error !== true)) approvalBlocked.add(parentKey);
|
|
5020
|
+
}
|
|
4145
5021
|
let waitingIndex = -1;
|
|
4146
5022
|
if (markWaiting) {
|
|
4147
|
-
for (let index =
|
|
4148
|
-
if (
|
|
5023
|
+
for (let index = renderSteps.length - 1; index >= 0; index -= 1) {
|
|
5024
|
+
if (approvalBlocked.has(stepKeyOf(renderSteps[index], index))) continue;
|
|
5025
|
+
if (renderSteps[index].done !== true && renderSteps[index].error !== true) {
|
|
4149
5026
|
waitingIndex = index;
|
|
4150
5027
|
break;
|
|
4151
5028
|
}
|
|
@@ -4153,13 +5030,18 @@ function FileTypeIcon(props) {
|
|
|
4153
5030
|
}
|
|
4154
5031
|
const rows = [];
|
|
4155
5032
|
let freshCount = 0;
|
|
4156
|
-
|
|
5033
|
+
renderSteps.forEach((step, index) => {
|
|
4157
5034
|
const key = stepKeyOf(step, index);
|
|
4158
5035
|
const fresh = asOfTime === null && !seenSteps.current.keys.has(key);
|
|
4159
5036
|
if (fresh) {
|
|
4160
5037
|
seenSteps.current.keys.add(key);
|
|
4161
5038
|
freshCount += 1;
|
|
4162
5039
|
}
|
|
5040
|
+
if (step.name === "approval") {
|
|
5041
|
+
/* an approval whose tool call fell outside the window still needs a row */
|
|
5042
|
+
rows.push(approvalChildRow(step, key, step.done !== true && step.error !== true));
|
|
5043
|
+
return;
|
|
5044
|
+
}
|
|
4163
5045
|
pushStepRow(rows, step, key, fresh ? { "data-enter": "true", style: { animationDelay: `${(freshCount - 1) * 45}ms` } } : undefined, index === waitingIndex);
|
|
4164
5046
|
});
|
|
4165
5047
|
if (ghost !== null && isRecord(ghost) && !stepsOpen) {
|
|
@@ -4188,10 +5070,14 @@ function FileTypeIcon(props) {
|
|
|
4188
5070
|
);
|
|
4189
5071
|
kids.push(h("div", { className: "dsov-steps", key: "steps" }, rows));
|
|
4190
5072
|
}
|
|
4191
|
-
return h(
|
|
5073
|
+
return h(
|
|
5074
|
+
Section,
|
|
5075
|
+
{ id: "progress", ...sectionIntroProps(introSeq, 0), icon: h(IconProgress, { size: 15 }), title: t("section.progress"), empty: t(summary.scope === "all" ? "progress.empty" : "progress.emptyTurn") },
|
|
5076
|
+
kids
|
|
5077
|
+
);
|
|
4192
5078
|
}
|
|
4193
5079
|
|
|
4194
|
-
function ArtifactsSection({ summary, t, binding, stats, asOf }) {
|
|
5080
|
+
function ArtifactsSection({ summary, t, binding, stats, asOf, introSeq }) {
|
|
4195
5081
|
const cfg = useConfig();
|
|
4196
5082
|
const asOfTime = typeof asOf === "number" && asOf > 0 ? asOf : null;
|
|
4197
5083
|
const visibleArtifacts = asOfTime === null ? summary.artifacts : summary.artifacts.filter((artifact) => (artifact.firstTime || artifact.time || 0) <= asOfTime);
|
|
@@ -4233,24 +5119,35 @@ function FileTypeIcon(props) {
|
|
|
4233
5119
|
const stat = stats ? stats.get(absPath(cwd, artifact.path)) : undefined;
|
|
4234
5120
|
const showCounts = stat !== undefined && !stat.binary && !stat.oversized && (stat.added > 0 || stat.deleted > 0);
|
|
4235
5121
|
const isMemory = typeof artifact.path === "string" && artifact.path.startsWith("viking://");
|
|
5122
|
+
const isConclusion = artifact.kind === "conclusion";
|
|
5123
|
+
const isPlan = artifact.plan === true;
|
|
5124
|
+
const planName = isPlan ? (typeof artifact.planTitle === "string" && artifact.planTitle !== "" ? artifact.planTitle : t("artifact.plan")) : "";
|
|
4236
5125
|
return h(
|
|
4237
5126
|
"button",
|
|
4238
5127
|
{
|
|
4239
5128
|
type: "button",
|
|
4240
5129
|
className: "dsov-row",
|
|
4241
5130
|
key: `${artifact.path}#${index}`,
|
|
4242
|
-
title: artifact.description ? `${artifact.path} — ${artifact.description}` : artifact.path,
|
|
5131
|
+
title: isConclusion ? `${artifact.title} · ${t("artifact.conclusionHint")}` : isPlan ? planName : artifact.description ? `${artifact.path} — ${artifact.description}` : artifact.path,
|
|
4243
5132
|
onClick: () => openArtifact(binding, artifact, stats),
|
|
4244
5133
|
onContextMenu:
|
|
4245
|
-
canFileMenu && !isMemory
|
|
5134
|
+
canFileMenu && !isMemory && !isPlan && !isConclusion
|
|
4246
5135
|
? (event) => {
|
|
4247
5136
|
event.preventDefault();
|
|
4248
5137
|
setFileMenu({ x: event.clientX, y: event.clientY, path: absPath(cwd, artifact.path) });
|
|
4249
5138
|
}
|
|
4250
5139
|
: undefined,
|
|
4251
5140
|
},
|
|
4252
|
-
h(
|
|
4253
|
-
|
|
5141
|
+
h(
|
|
5142
|
+
"span",
|
|
5143
|
+
{ className: "dsov-fileIcon" },
|
|
5144
|
+
isConclusion
|
|
5145
|
+
? h("span", { className: "dsov-fileMark", "data-kind": "conclusion" }, h(IconConclusion, { size: 15 }))
|
|
5146
|
+
: isPlan
|
|
5147
|
+
? h(IconPlan, { size: 20 })
|
|
5148
|
+
: h(FileTypeIcon, { path: artifact.path, size: 20 })
|
|
5149
|
+
),
|
|
5150
|
+
h("span", { className: "dsov-artifactName" }, isConclusion ? artifact.title : isPlan ? planName : basename(artifact.path)),
|
|
4254
5151
|
showCounts
|
|
4255
5152
|
? h(
|
|
4256
5153
|
"span",
|
|
@@ -4263,6 +5160,9 @@ function FileTypeIcon(props) {
|
|
|
4263
5160
|
: null,
|
|
4264
5161
|
isMemory ? h("span", { className: "dsov-badge", "data-tone": "quiet" }, t("artifact.memory")) : null,
|
|
4265
5162
|
artifact.presented ? h("span", { className: "dsov-badge", "data-tone": "brand" }, t("artifact.presented")) : null,
|
|
5163
|
+
isPlan ? h("span", { className: "dsov-badge", "data-tone": "plan" }, t("artifact.plan")) : null,
|
|
5164
|
+
isConclusion ? h("span", { className: "dsov-badge", "data-tone": "brand" }, t("artifact.conclusion")) : null,
|
|
5165
|
+
isConclusion && typeof artifact.chars === "number" ? h("span", { className: "dsov-badge", "data-tone": "quiet" }, t("artifact.conclusionChars", { n: artifact.chars })) : null,
|
|
4266
5166
|
h("span", { className: "dsov-rowTail" }, h("span", { className: "dsov-time", title: timeTitle(artifact.time) }, fmtTime(artifact.time)))
|
|
4267
5167
|
);
|
|
4268
5168
|
});
|
|
@@ -4271,6 +5171,7 @@ function FileTypeIcon(props) {
|
|
|
4271
5171
|
Section,
|
|
4272
5172
|
{
|
|
4273
5173
|
id: "artifacts",
|
|
5174
|
+
...sectionIntroProps(introSeq, 70),
|
|
4274
5175
|
icon: h(IconArtifact, { size: 15 }),
|
|
4275
5176
|
title: t("section.artifacts"),
|
|
4276
5177
|
badge: visibleArtifacts.length > 0 ? String(visibleArtifacts.length) : undefined,
|
|
@@ -4388,7 +5289,23 @@ function FileTypeIcon(props) {
|
|
|
4388
5289
|
return h(RefIcon, { kind: ref.kind, size: 14 });
|
|
4389
5290
|
}
|
|
4390
5291
|
|
|
4391
|
-
|
|
5292
|
+
/* per-session memory of reference keys whose entrance already animated
|
|
5293
|
+
(page lifetime): collapsing, scope switching and tab remounts never replay */
|
|
5294
|
+
const seenRefKeys = new Map();
|
|
5295
|
+
function seenRefKeysFor(sessionId) {
|
|
5296
|
+
const storeKey = typeof sessionId === "string" && sessionId !== "" ? sessionId : "-";
|
|
5297
|
+
let set = seenRefKeys.get(storeKey);
|
|
5298
|
+
if (set === undefined) {
|
|
5299
|
+
set = new Set();
|
|
5300
|
+
seenRefKeys.set(storeKey, set);
|
|
5301
|
+
}
|
|
5302
|
+
return set;
|
|
5303
|
+
}
|
|
5304
|
+
function refKeyOf(ref) {
|
|
5305
|
+
return isRecord(ref) && typeof ref.key === "string" && ref.key !== "" ? ref.key : `label:${isRecord(ref) ? ref.label : ""}`;
|
|
5306
|
+
}
|
|
5307
|
+
|
|
5308
|
+
function ReferencesSection({ summary, t, sessionId, asOf, introSeq }) {
|
|
4392
5309
|
const cfg = useConfig();
|
|
4393
5310
|
const asOfTime = typeof asOf === "number" && asOf > 0 ? asOf : null;
|
|
4394
5311
|
const allRefs = asOfTime === null ? summary.references : summary.references.filter((ref) => (ref.firstTime || ref.time || 0) <= asOfTime);
|
|
@@ -4396,6 +5313,11 @@ function FileTypeIcon(props) {
|
|
|
4396
5313
|
const timeTitle = (ts) => (cfg.timeMode === "elapsed" ? formatTime(ts) : undefined);
|
|
4397
5314
|
const sortMode = useRefSort();
|
|
4398
5315
|
const hideCommon = useHideCommonRefs();
|
|
5316
|
+
/* "all" spans the session (count turns); a turn view reports that turn's mentions. */
|
|
5317
|
+
const multiTurn = summary.scope === "all";
|
|
5318
|
+
/* "count" means turn coverage across the session, or mentions inside one turn. */
|
|
5319
|
+
const sortLabelOf = (mode) =>
|
|
5320
|
+
mode === "count" && multiTurn === false ? t("reference.sort.hits.title") : t(`reference.sort.${mode}.title`);
|
|
4399
5321
|
const typeFilter = useRefTypes();
|
|
4400
5322
|
const [revealState, setRevealState] = React.useState({});
|
|
4401
5323
|
const revealTimers = React.useRef({});
|
|
@@ -4432,7 +5354,32 @@ function FileTypeIcon(props) {
|
|
|
4432
5354
|
const scoped = hideCommon ? allRefs.filter((ref) => ref.ubiquitous !== true) : allRefs;
|
|
4433
5355
|
const selectedTypes = Array.isArray(typeFilter) ? typeFilter : [];
|
|
4434
5356
|
const filtered = selectedTypes.length === 0 ? scoped : scoped.filter((ref) => selectedTypes.indexOf(refDisplayType(ref)) !== -1);
|
|
4435
|
-
const sorted = sortReferences(filtered, sortMode);
|
|
5357
|
+
const sorted = sortReferences(filtered, sortMode, multiTurn);
|
|
5358
|
+
/* first-appearance animation: brand-new rows rise in with a small stagger;
|
|
5359
|
+
scrubbing the timeline never animates (rows would flicker in and out) */
|
|
5360
|
+
const seenRefs = seenRefKeysFor(sessionId);
|
|
5361
|
+
const enterDelays = new Map();
|
|
5362
|
+
if (asOfTime === null) {
|
|
5363
|
+
let batch = 0;
|
|
5364
|
+
for (const ref of sorted) {
|
|
5365
|
+
const refKey = refKeyOf(ref);
|
|
5366
|
+
if (seenRefs.has(refKey)) continue;
|
|
5367
|
+
enterDelays.set(refKey, Math.min(batch, 11) * 45);
|
|
5368
|
+
batch += 1;
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
React.useEffect(() => {
|
|
5372
|
+
if (asOfTime !== null) {
|
|
5373
|
+
for (const ref of sorted) seenRefs.add(refKeyOf(ref));
|
|
5374
|
+
return undefined;
|
|
5375
|
+
}
|
|
5376
|
+
/* mark after the longest animation finished so quick re-renders keep the
|
|
5377
|
+
attribute (a CSS animation never restarts while the attribute stays) */
|
|
5378
|
+
const timer = setTimeout(() => {
|
|
5379
|
+
for (const ref of sorted) seenRefs.add(refKeyOf(ref));
|
|
5380
|
+
}, 900);
|
|
5381
|
+
return () => clearTimeout(timer);
|
|
5382
|
+
});
|
|
4436
5383
|
const rows = sorted.map((ref, index) => {
|
|
4437
5384
|
const detail = Array.isArray(ref.detail) ? ref.detail : [];
|
|
4438
5385
|
const shown = detail.slice(0, 8);
|
|
@@ -4449,7 +5396,19 @@ function FileTypeIcon(props) {
|
|
|
4449
5396
|
h("span", { className: "dsov-rowName" }, refTitle(ref, t)),
|
|
4450
5397
|
h("span", { className: "dsov-badge" }, t(KIND_KEYS[ref.kind] ?? "reference.kind.context")),
|
|
4451
5398
|
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
|
-
|
|
5399
|
+
(multiTurn ? refTurnsOf(ref) > 1 : refHitsOf(ref) > 1)
|
|
5400
|
+
? h(
|
|
5401
|
+
"span",
|
|
5402
|
+
{
|
|
5403
|
+
className: "dsov-badge",
|
|
5404
|
+
"data-tone": "quiet",
|
|
5405
|
+
title: multiTurn
|
|
5406
|
+
? t("reference.turnsTitle", { turns: refTurnsOf(ref), times: refHitsOf(ref) })
|
|
5407
|
+
: t("reference.timesTitle", { times: refHitsOf(ref) }),
|
|
5408
|
+
},
|
|
5409
|
+
multiTurn ? t("reference.turns", { n: refTurnsOf(ref) }) : t("reference.times", { n: refHitsOf(ref) })
|
|
5410
|
+
)
|
|
5411
|
+
: null
|
|
4453
5412
|
),
|
|
4454
5413
|
shown.length > 0
|
|
4455
5414
|
? h(
|
|
@@ -4493,13 +5452,15 @@ function FileTypeIcon(props) {
|
|
|
4493
5452
|
h("span", { className: "dsov-time", key: "time", title: timeTitle(ref.time) }, fmtTime(ref.time)),
|
|
4494
5453
|
];
|
|
4495
5454
|
const isFile = ref.kind === "file";
|
|
5455
|
+
const enterDelay = enterDelays.get(refKeyOf(ref));
|
|
5456
|
+
const enterProps = enterDelay === undefined ? { key: `ref-${refKeyOf(ref)}` } : { key: `ref-${refKeyOf(ref)}`, "data-enter": "true", style: { animationDelay: `${enterDelay}ms` } };
|
|
4496
5457
|
if (isFile) {
|
|
4497
5458
|
return h(
|
|
4498
5459
|
"button",
|
|
4499
5460
|
{
|
|
4500
5461
|
type: "button",
|
|
4501
5462
|
className: "dsov-row",
|
|
4502
|
-
|
|
5463
|
+
...enterProps,
|
|
4503
5464
|
title: ref.label,
|
|
4504
5465
|
onClick: () => openSessionFile(sessionId, ref.label),
|
|
4505
5466
|
},
|
|
@@ -4512,7 +5473,7 @@ function FileTypeIcon(props) {
|
|
|
4512
5473
|
{
|
|
4513
5474
|
type: "button",
|
|
4514
5475
|
className: "dsov-row",
|
|
4515
|
-
|
|
5476
|
+
...enterProps,
|
|
4516
5477
|
title: ref.uri,
|
|
4517
5478
|
onClick: () => openVikingPreview(ref.uri),
|
|
4518
5479
|
},
|
|
@@ -4525,7 +5486,7 @@ function FileTypeIcon(props) {
|
|
|
4525
5486
|
{
|
|
4526
5487
|
type: "button",
|
|
4527
5488
|
className: "dsov-row",
|
|
4528
|
-
|
|
5489
|
+
...enterProps,
|
|
4529
5490
|
title: t("reference.previewHint"),
|
|
4530
5491
|
onClick: () => openContextPreview(ref.previewKey),
|
|
4531
5492
|
},
|
|
@@ -4534,7 +5495,7 @@ function FileTypeIcon(props) {
|
|
|
4534
5495
|
}
|
|
4535
5496
|
return h(
|
|
4536
5497
|
"div",
|
|
4537
|
-
{ className: "dsov-row dsov-rowStatic",
|
|
5498
|
+
{ className: "dsov-row dsov-rowStatic", ...enterProps, title: isVikingUri(ref.uri) ? ref.uri : ref.label },
|
|
4538
5499
|
...content
|
|
4539
5500
|
);
|
|
4540
5501
|
});
|
|
@@ -4571,6 +5532,7 @@ function FileTypeIcon(props) {
|
|
|
4571
5532
|
Section,
|
|
4572
5533
|
{
|
|
4573
5534
|
id: "references",
|
|
5535
|
+
...sectionIntroProps(introSeq, 140),
|
|
4574
5536
|
icon: h(IconReference, { size: 15 }),
|
|
4575
5537
|
title: t("section.references"),
|
|
4576
5538
|
badge: filtered.length > 0 ? String(filtered.length) : undefined,
|
|
@@ -4608,10 +5570,11 @@ function FileTypeIcon(props) {
|
|
|
4608
5570
|
key: mode,
|
|
4609
5571
|
className: "dsov-segBtn",
|
|
4610
5572
|
"data-active": sortMode === mode ? "true" : "false",
|
|
4611
|
-
title:
|
|
5573
|
+
title:
|
|
5574
|
+
(sortMode === mode ? `${sortLabelOf(mode)} · ${t("reference.sort.cancel")}` : sortLabelOf(mode)),
|
|
4612
5575
|
onClick: () => setRefSort(sortMode === mode ? "" : mode),
|
|
4613
5576
|
},
|
|
4614
|
-
t(`reference.sort.${mode}`)
|
|
5577
|
+
mode === "count" && multiTurn === false ? t("reference.sort.hits") : t(`reference.sort.${mode}`)
|
|
4615
5578
|
)
|
|
4616
5579
|
)
|
|
4617
5580
|
)
|
|
@@ -4646,6 +5609,19 @@ function FileTypeIcon(props) {
|
|
|
4646
5609
|
const [errPop, setErrPop] = React.useState(null);
|
|
4647
5610
|
const errOpen = isRecord(errPop);
|
|
4648
5611
|
const errRef = React.useRef(null);
|
|
5612
|
+
const errBtnRef = React.useRef(null);
|
|
5613
|
+
/* Place the fixed popover under its pill, clamped into the viewport. */
|
|
5614
|
+
const placePopover = React.useCallback(() => {
|
|
5615
|
+
const button = errBtnRef.current;
|
|
5616
|
+
if (button === null || button.isConnected !== true) return false;
|
|
5617
|
+
const rect = button.getBoundingClientRect();
|
|
5618
|
+
if (rect.bottom < 0 || rect.top > window.innerHeight) return false;
|
|
5619
|
+
setErrPop({
|
|
5620
|
+
top: Math.min(rect.bottom + 5, Math.max(8, window.innerHeight - 292)),
|
|
5621
|
+
right: Math.max(8, window.innerWidth - rect.right),
|
|
5622
|
+
});
|
|
5623
|
+
return true;
|
|
5624
|
+
}, []);
|
|
4649
5625
|
React.useEffect(() => {
|
|
4650
5626
|
if (!errOpen) return undefined;
|
|
4651
5627
|
const close = () => setErrPop(null);
|
|
@@ -4655,17 +5631,30 @@ function FileTypeIcon(props) {
|
|
|
4655
5631
|
const onKey = (event) => {
|
|
4656
5632
|
if (event.key === "Escape") close();
|
|
4657
5633
|
};
|
|
5634
|
+
/* Following the anchor instead of dismissing: the chat auto-scrolls on every
|
|
5635
|
+
streamed event, so closing on scroll made the popover impossible to read
|
|
5636
|
+
while a turn was running. Reposition, and only close once the pill itself
|
|
5637
|
+
left the viewport (or unmounted). */
|
|
5638
|
+
let frame = 0;
|
|
5639
|
+
const follow = () => {
|
|
5640
|
+
if (frame !== 0) return;
|
|
5641
|
+
frame = requestAnimationFrame(() => {
|
|
5642
|
+
frame = 0;
|
|
5643
|
+
if (!placePopover()) setErrPop(null);
|
|
5644
|
+
});
|
|
5645
|
+
};
|
|
4658
5646
|
document.addEventListener("mousedown", onDown);
|
|
4659
5647
|
document.addEventListener("keydown", onKey);
|
|
4660
|
-
window.addEventListener("scroll",
|
|
4661
|
-
window.addEventListener("resize",
|
|
5648
|
+
window.addEventListener("scroll", follow, true);
|
|
5649
|
+
window.addEventListener("resize", follow);
|
|
4662
5650
|
return () => {
|
|
5651
|
+
if (frame !== 0) cancelAnimationFrame(frame);
|
|
4663
5652
|
document.removeEventListener("mousedown", onDown);
|
|
4664
5653
|
document.removeEventListener("keydown", onKey);
|
|
4665
|
-
window.removeEventListener("scroll",
|
|
4666
|
-
window.removeEventListener("resize",
|
|
5654
|
+
window.removeEventListener("scroll", follow, true);
|
|
5655
|
+
window.removeEventListener("resize", follow);
|
|
4667
5656
|
};
|
|
4668
|
-
}, [errOpen]);
|
|
5657
|
+
}, [errOpen, placePopover]);
|
|
4669
5658
|
if (!isRecord(stats)) return null;
|
|
4670
5659
|
const duration = typeof stats.durationMs === "number" ? stats.durationMs : 0;
|
|
4671
5660
|
const toolCalls = typeof stats.toolCalls === "number" ? stats.toolCalls : 0;
|
|
@@ -4720,20 +5709,17 @@ function FileTypeIcon(props) {
|
|
|
4720
5709
|
className: "dsov-stat dsov-statBtn",
|
|
4721
5710
|
"data-tone": "err",
|
|
4722
5711
|
"data-active": errOpen ? "true" : undefined,
|
|
5712
|
+
ref: errBtnRef,
|
|
4723
5713
|
disabled: failed.length === 0,
|
|
4724
5714
|
title: failed.length > 0 ? t("stats.errors.popTitle") : t("stats.errors.title"),
|
|
4725
5715
|
onClick:
|
|
4726
5716
|
failed.length > 0
|
|
4727
|
-
? (
|
|
5717
|
+
? () => {
|
|
4728
5718
|
if (errOpen) {
|
|
4729
5719
|
setErrPop(null);
|
|
4730
5720
|
return;
|
|
4731
5721
|
}
|
|
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
|
-
});
|
|
5722
|
+
if (!placePopover()) setErrPop(null);
|
|
4737
5723
|
}
|
|
4738
5724
|
: undefined,
|
|
4739
5725
|
},
|
|
@@ -4777,6 +5763,175 @@ function FileTypeIcon(props) {
|
|
|
4777
5763
|
);
|
|
4778
5764
|
}
|
|
4779
5765
|
|
|
5766
|
+
/** Which side of a subagent lineage the bound session is on, read from catalog
|
|
5767
|
+
metadata only (never an Agent-bound RPC). */
|
|
5768
|
+
function sessionRoleOf(sessionId) {
|
|
5769
|
+
const empty = { kind: "main", parentId: null, parentTitle: "", mode: "", children: [] };
|
|
5770
|
+
if (typeof sessionId !== "string" || sessionId === "") return empty;
|
|
5771
|
+
let ctx = null;
|
|
5772
|
+
try {
|
|
5773
|
+
ctx = runtime === null ? null : runtime.ctx;
|
|
5774
|
+
} catch {
|
|
5775
|
+
ctx = null;
|
|
5776
|
+
}
|
|
5777
|
+
let snapshot = null;
|
|
5778
|
+
try {
|
|
5779
|
+
snapshot = ctx === null || ctx === undefined ? null : ctx.sessions.list.getSnapshot();
|
|
5780
|
+
} catch {
|
|
5781
|
+
snapshot = null;
|
|
5782
|
+
}
|
|
5783
|
+
let address = null;
|
|
5784
|
+
try {
|
|
5785
|
+
address = ctx === null || ctx === undefined ? null : ctx.sessions.subagentAddress(sessionId);
|
|
5786
|
+
} catch {
|
|
5787
|
+
address = null;
|
|
5788
|
+
}
|
|
5789
|
+
const byId = isRecord(snapshot) && isRecord(snapshot.byId) ? snapshot.byId : {};
|
|
5790
|
+
const self = isRecord(byId[sessionId]) ? byId[sessionId] : null;
|
|
5791
|
+
const parentId =
|
|
5792
|
+
isRecord(address) && typeof address.parentSessionId === "string" && address.parentSessionId !== ""
|
|
5793
|
+
? address.parentSessionId
|
|
5794
|
+
: self !== null && typeof self.parentId === "string" && self.parentId !== ""
|
|
5795
|
+
? self.parentId
|
|
5796
|
+
: null;
|
|
5797
|
+
const parent = parentId !== null && isRecord(byId[parentId]) ? byId[parentId] : null;
|
|
5798
|
+
const ids = isRecord(snapshot) && Array.isArray(snapshot.ids) ? snapshot.ids : [];
|
|
5799
|
+
const children = [];
|
|
5800
|
+
const seen = new Set();
|
|
5801
|
+
for (const id of ids) {
|
|
5802
|
+
const entry = isRecord(byId[id]) ? byId[id] : null;
|
|
5803
|
+
if (entry === null || entry.parentId !== sessionId) continue;
|
|
5804
|
+
seen.add(id);
|
|
5805
|
+
children.push({
|
|
5806
|
+
id,
|
|
5807
|
+
title: typeof entry.displayTitle === "string" && entry.displayTitle !== "" ? entry.displayTitle : id,
|
|
5808
|
+
running: entry.running === true,
|
|
5809
|
+
});
|
|
5810
|
+
}
|
|
5811
|
+
/* A child can exist in the parent's catalog projection before the list is
|
|
5812
|
+
projected; the catalog is the same metadata the host itself reads. */
|
|
5813
|
+
const projections = isRecord(snapshot) && isRecord(snapshot.projectionsBySession) ? snapshot.projectionsBySession : null;
|
|
5814
|
+
const parentProjection = projections !== null && isRecord(projections[sessionId]) ? projections[sessionId] : null;
|
|
5815
|
+
const catalog = parentProjection !== null && isRecord(parentProjection.values) && Array.isArray(parentProjection.values.subagentCatalog) ? parentProjection.values.subagentCatalog : [];
|
|
5816
|
+
for (const entry of catalog) {
|
|
5817
|
+
if (!isRecord(entry) || typeof entry.id !== "string" || entry.id === "" || seen.has(entry.id)) continue;
|
|
5818
|
+
if (entry.mode === "external") continue;
|
|
5819
|
+
seen.add(entry.id);
|
|
5820
|
+
const listed = isRecord(byId[entry.id]) ? byId[entry.id] : null;
|
|
5821
|
+
const label = typeof entry.title === "string" && entry.title !== "" ? entry.title : typeof entry.label === "string" ? entry.label : "";
|
|
5822
|
+
children.push({
|
|
5823
|
+
id: entry.id,
|
|
5824
|
+
title:
|
|
5825
|
+
listed !== null && typeof listed.displayTitle === "string" && listed.displayTitle !== ""
|
|
5826
|
+
? listed.displayTitle
|
|
5827
|
+
: label !== ""
|
|
5828
|
+
? label
|
|
5829
|
+
: entry.id,
|
|
5830
|
+
running: listed !== null ? listed.running === true : false,
|
|
5831
|
+
});
|
|
5832
|
+
}
|
|
5833
|
+
if (parentId === null) return { ...empty, children };
|
|
5834
|
+
return {
|
|
5835
|
+
kind: "sub",
|
|
5836
|
+
parentId,
|
|
5837
|
+
parentTitle: parent !== null && typeof parent.displayTitle === "string" ? parent.displayTitle : "",
|
|
5838
|
+
mode: isRecord(address) && typeof address.mode === "string" ? address.mode : "",
|
|
5839
|
+
children,
|
|
5840
|
+
};
|
|
5841
|
+
}
|
|
5842
|
+
|
|
5843
|
+
/** Whether the workspace service can move the main conversation to another session. */
|
|
5844
|
+
function canNavigateSession() {
|
|
5845
|
+
try {
|
|
5846
|
+
const ctx = runtime === null ? null : runtime.ctx;
|
|
5847
|
+
const workspace = ctx === null || ctx === undefined ? null : ctx.uiWorkspace;
|
|
5848
|
+
return isRecord(workspace) && typeof workspace.openSession === "function";
|
|
5849
|
+
} catch {
|
|
5850
|
+
return false;
|
|
5851
|
+
}
|
|
5852
|
+
}
|
|
5853
|
+
|
|
5854
|
+
function openSessionById(sessionId) {
|
|
5855
|
+
if (typeof sessionId !== "string" || sessionId === "") return false;
|
|
5856
|
+
try {
|
|
5857
|
+
const ctx = runtime === null ? null : runtime.ctx;
|
|
5858
|
+
const workspace = ctx === null || ctx === undefined ? null : ctx.uiWorkspace;
|
|
5859
|
+
if (!isRecord(workspace) || typeof workspace.openSession !== "function") return false;
|
|
5860
|
+
workspace.openSession(sessionId);
|
|
5861
|
+
return true;
|
|
5862
|
+
} catch {
|
|
5863
|
+
return false;
|
|
5864
|
+
}
|
|
5865
|
+
}
|
|
5866
|
+
|
|
5867
|
+
/** One compact line that says whether this overview belongs to the main agent or to
|
|
5868
|
+
a subagent, and offers to switch to the other side of that lineage. */
|
|
5869
|
+
function SessionRoleBar({ t, sessionId, hasWork }) {
|
|
5870
|
+
const role = sessionRoleOf(sessionId);
|
|
5871
|
+
const [open, setOpen] = React.useState(false);
|
|
5872
|
+
const canSwitch = canNavigateSession();
|
|
5873
|
+
const children =
|
|
5874
|
+
role.children.length === 0
|
|
5875
|
+
? null
|
|
5876
|
+
: h(
|
|
5877
|
+
"button",
|
|
5878
|
+
{
|
|
5879
|
+
type: "button",
|
|
5880
|
+
className: "dsov-roleBtn",
|
|
5881
|
+
"data-open": open ? "true" : "false",
|
|
5882
|
+
title: t("role.childrenToggle"),
|
|
5883
|
+
onClick: () => setOpen((value) => !value),
|
|
5884
|
+
},
|
|
5885
|
+
t("role.children", { n: role.children.length }),
|
|
5886
|
+
h(IconChevron, { size: 11, className: "dsov-roleChevron" })
|
|
5887
|
+
);
|
|
5888
|
+
const list =
|
|
5889
|
+
!open || role.children.length === 0
|
|
5890
|
+
? null
|
|
5891
|
+
: h(
|
|
5892
|
+
"div",
|
|
5893
|
+
{ className: "dsov-roleList" },
|
|
5894
|
+
role.children.map((child) =>
|
|
5895
|
+
h(
|
|
5896
|
+
"button",
|
|
5897
|
+
{
|
|
5898
|
+
type: "button",
|
|
5899
|
+
className: "dsov-roleItem",
|
|
5900
|
+
key: child.id,
|
|
5901
|
+
title: canSwitch ? t("role.switchTitle") : child.title,
|
|
5902
|
+
disabled: !canSwitch,
|
|
5903
|
+
onClick: canSwitch ? () => openSessionById(child.id) : undefined,
|
|
5904
|
+
},
|
|
5905
|
+
h("span", { className: "dsov-roleDot", "data-running": child.running ? "true" : "false" }),
|
|
5906
|
+
h("span", { className: "dsov-roleItemTitle" }, child.title)
|
|
5907
|
+
)
|
|
5908
|
+
)
|
|
5909
|
+
);
|
|
5910
|
+
if (role.kind === "sub") {
|
|
5911
|
+
const parentName = role.parentTitle !== "" ? role.parentTitle : role.parentId;
|
|
5912
|
+
return h(
|
|
5913
|
+
"div",
|
|
5914
|
+
{ className: "dsov-role", "data-kind": "sub" },
|
|
5915
|
+
h("span", { className: "dsov-roleBadge" }, t("role.subagent")),
|
|
5916
|
+
role.mode !== "" && role.mode !== "unknown" ? h("span", { className: "dsov-roleMode" }, role.mode) : null,
|
|
5917
|
+
canSwitch
|
|
5918
|
+
? h(
|
|
5919
|
+
"button",
|
|
5920
|
+
{ type: "button", className: "dsov-roleBtn", "data-link": "true", title: t("role.openParentTitle"), onClick: () => openSessionById(role.parentId) },
|
|
5921
|
+
t("role.openParent"),
|
|
5922
|
+
h("span", { className: "dsov-roleParent" }, parentName)
|
|
5923
|
+
)
|
|
5924
|
+
: h("span", { className: "dsov-roleHint" }, t("role.openParent"), h("span", { className: "dsov-roleParent" }, parentName)),
|
|
5925
|
+
children,
|
|
5926
|
+
list
|
|
5927
|
+
);
|
|
5928
|
+
}
|
|
5929
|
+
/* A plain main session with no subagents in this scope has no identity to
|
|
5930
|
+
announce: the badge, the child list, and the parent link are all noise. */
|
|
5931
|
+
if (role.children.length === 0 || hasWork === false) return null;
|
|
5932
|
+
return h("div", { className: "dsov-role", "data-kind": "main" }, h("span", { className: "dsov-roleBadge" }, t("role.main")), children, list);
|
|
5933
|
+
}
|
|
5934
|
+
|
|
4780
5935
|
function OverviewContent({ binding, t, variant }) {
|
|
4781
5936
|
const [selection, setSelection] = React.useState("latest");
|
|
4782
5937
|
/* timeline scrubbing: a null asOf always means "live/latest state" */
|
|
@@ -4796,11 +5951,53 @@ function FileTypeIcon(props) {
|
|
|
4796
5951
|
setAsOf(null);
|
|
4797
5952
|
}, [selection]);
|
|
4798
5953
|
const summary = useSummary(binding, selection);
|
|
4799
|
-
|
|
5954
|
+
/* 「重新加载」restarts the backfill loop from zero — the escape hatch when the
|
|
5955
|
+
host loader silently no-ops (another loader holds the lock) or a page fails. */
|
|
5956
|
+
const [historyRetry, setHistoryRetry] = React.useState(0);
|
|
5957
|
+
const historyStalled = useBackfill(binding, summary.hasMore, historyRetry);
|
|
4800
5958
|
const goal = useGoal(binding);
|
|
4801
5959
|
const stats = useArtifactStats(binding, summary.changeTurns);
|
|
4802
5960
|
const cfg = useConfig();
|
|
4803
5961
|
const sessionId = binding ? binding.sessionId : undefined;
|
|
5962
|
+
/* 本轮结论 as a document: a turn that finishes while this overview is open pops
|
|
5963
|
+
its closing reply into a tab once. A turn that had already finished before the
|
|
5964
|
+
overview mounted is never re-opened (no tab storm on refresh or scope switch). */
|
|
5965
|
+
const conclusionGuard = React.useRef({ ready: false, opened: new Set() });
|
|
5966
|
+
React.useEffect(() => {
|
|
5967
|
+
const conclusion = isRecord(summary.conclusion) ? summary.conclusion : null;
|
|
5968
|
+
const key = `${sessionId}|${conclusion === null ? "" : conclusion.turn}`;
|
|
5969
|
+
const guard = conclusionGuard.current;
|
|
5970
|
+
if (guard.ready !== true) {
|
|
5971
|
+
/* first paint: a turn that had already finished is never re-opened */
|
|
5972
|
+
guard.ready = true;
|
|
5973
|
+
if (conclusion !== null && conclusion.ended === true) guard.opened.add(key);
|
|
5974
|
+
return;
|
|
5975
|
+
}
|
|
5976
|
+
if (conclusion === null || conclusion.ended !== true || guard.opened.has(key)) return;
|
|
5977
|
+
guard.opened.add(key);
|
|
5978
|
+
if (cfg.conclusionTab !== true) return;
|
|
5979
|
+
if (typeof conclusion.previewKey !== "string" || conclusion.previewKey === "") return;
|
|
5980
|
+
if (contextSidebar() === null) return;
|
|
5981
|
+
if (!claimConclusionTab(key)) return;
|
|
5982
|
+
/* a failed open must not eat the claim: another instance may still show the tab */
|
|
5983
|
+
const entry = readContextPreview(conclusion.previewKey);
|
|
5984
|
+
const docKey = `conclusion:${typeof conclusion.turn === "number" ? conclusion.turn : conclusion.previewKey}`;
|
|
5985
|
+
const opened = openNativeDocument(sessionId, docKey, conclusion.title, entry === null ? undefined : entry.text) || openContextPreview(conclusion.previewKey);
|
|
5986
|
+
if (!opened) conclusionAutoOpened.delete(key);
|
|
5987
|
+
}, [summary, cfg.conclusionTab, sessionId]);
|
|
5988
|
+
/* Agent identity only earns its row when a subagent actually took part in the
|
|
5989
|
+
visible scope — otherwise the overview is a single-agent conversation. */
|
|
5990
|
+
const roleWork = React.useMemo(() => {
|
|
5991
|
+
const team = isRecord(summary.team) ? summary.team : null;
|
|
5992
|
+
if (team !== null && (Array.isArray(team.subagents) && team.subagents.length > 0)) return true;
|
|
5993
|
+
if (team !== null && (Array.isArray(team.members) && team.members.length > 0)) return true;
|
|
5994
|
+
const steps = Array.isArray(summary.stepsAll) ? summary.stepsAll : [];
|
|
5995
|
+
return steps.some(
|
|
5996
|
+
(step) =>
|
|
5997
|
+
isRecord(step) &&
|
|
5998
|
+
(step.name === "subagent" || step.name === "subagent_fork" || step.name === "spawn_teammate" || step.name === "team_task_create" || typeof step.teamKey === "string")
|
|
5999
|
+
);
|
|
6000
|
+
}, [summary]);
|
|
4804
6001
|
const rootClass = variant === "panel" ? "dsov-panel" : "dsov-view";
|
|
4805
6002
|
const rootRef = React.useRef(null);
|
|
4806
6003
|
const scope = summary.scope === selection || selection === "latest" ? summary.scope : selection;
|
|
@@ -4835,6 +6032,29 @@ function FileTypeIcon(props) {
|
|
|
4835
6032
|
errors: list.filter((step) => step.error === true && (step.endTime || step.time || 0) <= asOf).length,
|
|
4836
6033
|
};
|
|
4837
6034
|
}, [asOf, summary]);
|
|
6035
|
+
/* A new turn gets a staged entrance for the three sections plus a short
|
|
6036
|
+
「第 N 轮对话开始」 cue. First paint of an already-running turn stays silent,
|
|
6037
|
+
and nothing replays on re-renders, scope switches or tab remounts. */
|
|
6038
|
+
const [introSeq, setIntroSeq] = React.useState(0);
|
|
6039
|
+
const [introBanner, setIntroBanner] = React.useState(null);
|
|
6040
|
+
const introTurn = React.useRef(null);
|
|
6041
|
+
React.useEffect(() => {
|
|
6042
|
+
const turn = Number.isFinite(summary.latestTurn) ? summary.latestTurn : null;
|
|
6043
|
+
if (turn === null || asOf !== null) return;
|
|
6044
|
+
if (introTurn.current === null) {
|
|
6045
|
+
introTurn.current = turn;
|
|
6046
|
+
return;
|
|
6047
|
+
}
|
|
6048
|
+
if (turn === introTurn.current) return;
|
|
6049
|
+
introTurn.current = turn;
|
|
6050
|
+
setIntroSeq((value) => value + 1);
|
|
6051
|
+
setIntroBanner(turn);
|
|
6052
|
+
}, [summary.latestTurn, asOf]);
|
|
6053
|
+
React.useEffect(() => {
|
|
6054
|
+
if (introBanner === null) return undefined;
|
|
6055
|
+
const timer = setTimeout(() => setIntroBanner(null), 2200);
|
|
6056
|
+
return () => clearTimeout(timer);
|
|
6057
|
+
}, [introBanner, introSeq]);
|
|
4838
6058
|
const canTimeline = typeof summary.turnStart === "number" && typeof summary.turnEnd === "number" && summary.turnEnd > summary.turnStart;
|
|
4839
6059
|
/* the errors pill drills into these: failed steps of the scope, honoring the scrub point */
|
|
4840
6060
|
const failedSteps = React.useMemo(() => {
|
|
@@ -5013,10 +6233,40 @@ function FileTypeIcon(props) {
|
|
|
5013
6233
|
),
|
|
5014
6234
|
timelineRow
|
|
5015
6235
|
),
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
6236
|
+
binding && typeof binding.sessionId === "string" && binding.sessionId !== "" ? h(SessionRoleBar, { key: "roleBar", t, sessionId: binding.sessionId, hasWork: roleWork }) : null,
|
|
6237
|
+
summary.hasMore
|
|
6238
|
+
? h(
|
|
6239
|
+
"div",
|
|
6240
|
+
{ className: "dsov-loading" },
|
|
6241
|
+
h("span", { className: "dsov-loadingText" }, t(historyStalled ? "state.loadingStalled" : "state.loadingHistory")),
|
|
6242
|
+
h(
|
|
6243
|
+
"button",
|
|
6244
|
+
{
|
|
6245
|
+
type: "button",
|
|
6246
|
+
className: "dsov-loadingReload",
|
|
6247
|
+
title: t("state.reloadHistory.title"),
|
|
6248
|
+
onClick: () => {
|
|
6249
|
+
/* Re-arming alone is not enough: the loop would issue the same
|
|
6250
|
+
call that just no-oped. Pull one page directly first, using the
|
|
6251
|
+
seq-free loader when available, then let the loop take over. */
|
|
6252
|
+
const session = binding ? binding.session : undefined;
|
|
6253
|
+
try {
|
|
6254
|
+
if (isRecord(session) && typeof session.loadOlder === "function") Promise.resolve(session.loadOlder()).catch(() => {});
|
|
6255
|
+
else if (isRecord(session) && typeof session.loadThrough === "function") Promise.resolve(session.loadThrough(0)).catch(() => {});
|
|
6256
|
+
} catch {
|
|
6257
|
+
/* the re-arm below still gives the loop another chance */
|
|
6258
|
+
}
|
|
6259
|
+
setHistoryRetry((value) => value + 1);
|
|
6260
|
+
},
|
|
6261
|
+
},
|
|
6262
|
+
t("state.reloadHistory")
|
|
6263
|
+
)
|
|
6264
|
+
)
|
|
6265
|
+
: null,
|
|
6266
|
+
introBanner !== null ? h("div", { className: "dsov-turnStart", key: `turnStart-${introSeq}` }, h("span", { className: "dsov-turnStartDot" }), t("intro.newTurn", { n: introBanner })) : null,
|
|
6267
|
+
cfg.showProgress ? h(ProgressSection, { summary, goal, t, waiting: sessionWaiting && asOf === null, cfg, asOf, introSeq }) : null,
|
|
6268
|
+
cfg.showArtifacts ? h(ArtifactsSection, { summary, t, binding, stats, asOf, introSeq }) : null,
|
|
6269
|
+
cfg.showReferences ? h(ReferencesSection, { summary, t, sessionId, asOf, introSeq }) : null
|
|
5020
6270
|
);
|
|
5021
6271
|
}
|
|
5022
6272
|
|
|
@@ -5200,7 +6450,7 @@ function FileTypeIcon(props) {
|
|
|
5200
6450
|
view.status === "ready" && view.mode === "file" && source === true ? h("pre", { className: "dsov-vikingSource" }, view.text) : null,
|
|
5201
6451
|
view.status === "ready" && view.mode === "file" && source !== true && parsed ? renderFrontmatterFields(parsed, t) : null,
|
|
5202
6452
|
view.status === "ready" && view.mode === "file" && source !== true && parsed
|
|
5203
|
-
? h(
|
|
6453
|
+
? h(MarkdownBody, { text: parsed.body })
|
|
5204
6454
|
: null
|
|
5205
6455
|
);
|
|
5206
6456
|
}
|
|
@@ -5308,24 +6558,56 @@ function FileTypeIcon(props) {
|
|
|
5308
6558
|
if (entry === null) {
|
|
5309
6559
|
return h("div", { className: "dsov-viking" }, h("div", { className: "dsov-vikingError" }, t("context.unavailable")));
|
|
5310
6560
|
}
|
|
6561
|
+
if (entry.kind === "conclusion") {
|
|
6562
|
+
/* Just the turn's closing answer, rendered exactly as the chat renders it;
|
|
6563
|
+
the page geometry mirrors the host's own Markdown document page so the
|
|
6564
|
+
fallback tab reads the same as the banner-opened one. */
|
|
6565
|
+
return h(
|
|
6566
|
+
"section",
|
|
6567
|
+
{ className: "dsov-docPage", "data-doc-preview": "conclusion", "aria-label": entry.title },
|
|
6568
|
+
h(
|
|
6569
|
+
"div",
|
|
6570
|
+
{ className: "dsov-docDocument" },
|
|
6571
|
+
ChatMarkdown
|
|
6572
|
+
? h(ChatMarkdown, { text: entry.text, labels: chatMarkdownLabels(t) })
|
|
6573
|
+
: h(MarkdownBody, { text: entry.text })
|
|
6574
|
+
)
|
|
6575
|
+
);
|
|
6576
|
+
}
|
|
5311
6577
|
return h(
|
|
5312
6578
|
"div",
|
|
5313
6579
|
{ className: "dsov-viking" },
|
|
5314
6580
|
h(
|
|
5315
6581
|
"div",
|
|
5316
6582
|
{ className: "dsov-vikingHead" },
|
|
5317
|
-
h("span", { className: "dsov-vikingIcon" }, h(IconReference, { size: 15 })),
|
|
6583
|
+
h("span", { className: "dsov-vikingIcon" }, entry.kind === "plan" ? h(IconPlan, { size: 15 }) : h(IconReference, { size: 15 })),
|
|
5318
6584
|
h(
|
|
5319
6585
|
"span",
|
|
5320
6586
|
{ className: "dsov-vikingHeadText" },
|
|
5321
6587
|
h("span", { className: "dsov-vikingName" }, entry.title),
|
|
5322
|
-
h(
|
|
6588
|
+
h(
|
|
6589
|
+
"span",
|
|
6590
|
+
{ className: "dsov-vikingPathText" },
|
|
6591
|
+
entry.kind === "plan" ? t("context.subtitlePlan") : t("context.subtitle")
|
|
6592
|
+
)
|
|
5323
6593
|
)
|
|
5324
6594
|
),
|
|
5325
|
-
h(
|
|
6595
|
+
h(MarkdownBody, { text: entry.text })
|
|
5326
6596
|
);
|
|
5327
6597
|
}
|
|
5328
6598
|
|
|
6599
|
+
/** Tab chip for a captured snapshot: the file glyph the host's document tabs carry. */
|
|
6600
|
+
function ContextTabTitle(props) {
|
|
6601
|
+
const t = typeof props.t === "function" ? props.t : translate;
|
|
6602
|
+
const info = typeof props.useTabInfo === "function" ? props.useTabInfo() : null;
|
|
6603
|
+
const key = info && isRecord(info.tab) ? parseContextAddress(info.tab.contentId) : undefined;
|
|
6604
|
+
const entry = key === undefined ? null : readContextPreview(key);
|
|
6605
|
+
const title = entry !== null ? entry.title : t("context.tab");
|
|
6606
|
+
/* only documents carry the glyph; reference snapshots keep a plain caption */
|
|
6607
|
+
if (entry === null || (entry.kind !== "conclusion" && entry.kind !== "plan")) return title;
|
|
6608
|
+
return h(React.Fragment, null, h(FileTypeIcon, { kind: "other", size: 16, className: "dsov-docTitleIcon" }), title);
|
|
6609
|
+
}
|
|
6610
|
+
|
|
5329
6611
|
/* ------------------------------------------------------------------ */
|
|
5330
6612
|
/* Settings form (settings page + Plugins row config slot) */
|
|
5331
6613
|
/* ------------------------------------------------------------------ */
|
|
@@ -5380,7 +6662,7 @@ function FileTypeIcon(props) {
|
|
|
5380
6662
|
}
|
|
5381
6663
|
|
|
5382
6664
|
/* dsh-context style settings rows: label left, pill-select control right. */
|
|
5383
|
-
function SettingSelect({ label, value, options, onPick }) {
|
|
6665
|
+
function SettingSelect({ label, value, options, onPick, hint }) {
|
|
5384
6666
|
const [open, setOpen] = React.useState(false);
|
|
5385
6667
|
const rootRef = React.useRef(null);
|
|
5386
6668
|
React.useEffect(() => {
|
|
@@ -5396,7 +6678,7 @@ function FileTypeIcon(props) {
|
|
|
5396
6678
|
return h(
|
|
5397
6679
|
"div",
|
|
5398
6680
|
{ className: "dsov-set-row" },
|
|
5399
|
-
h("span", { className: "dsov-set-label" }, label),
|
|
6681
|
+
h("span", { className: "dsov-set-label" }, h("span", { className: "dsov-set-labelText" }, label), hint ?? null),
|
|
5400
6682
|
h(
|
|
5401
6683
|
"div",
|
|
5402
6684
|
{ className: "dsov-set-control", ref: rootRef },
|
|
@@ -5489,7 +6771,7 @@ function FileTypeIcon(props) {
|
|
|
5489
6771
|
return h(
|
|
5490
6772
|
"div",
|
|
5491
6773
|
{ className: "dsov-set-row dsov-set-arearow" },
|
|
5492
|
-
h("span", { className: "dsov-set-label" }, label),
|
|
6774
|
+
label === "" ? null : h("span", { className: "dsov-set-label" }, label),
|
|
5493
6775
|
h("textarea", {
|
|
5494
6776
|
className: "dsov-set-textarea",
|
|
5495
6777
|
rows: 7,
|
|
@@ -5511,6 +6793,7 @@ function FileTypeIcon(props) {
|
|
|
5511
6793
|
function SettingsForm(props) {
|
|
5512
6794
|
const t = typeof props.t === "function" ? props.t : translate;
|
|
5513
6795
|
const hookForm = useSettingsForm();
|
|
6796
|
+
const [showDefaultPrompt, setShowDefaultPrompt] = React.useState(false);
|
|
5514
6797
|
if (props.view === "summary") {
|
|
5515
6798
|
return h("span", null, t("settings.summary"));
|
|
5516
6799
|
}
|
|
@@ -5540,6 +6823,14 @@ function FileTypeIcon(props) {
|
|
|
5540
6823
|
{ id: "clock", label: t("settings.timeMode.clock") },
|
|
5541
6824
|
{ id: "elapsed", label: t("settings.timeMode.elapsed") },
|
|
5542
6825
|
];
|
|
6826
|
+
const todoPromptOptions = [
|
|
6827
|
+
{ id: "prompt", label: t("settings.todoPromptOn") },
|
|
6828
|
+
{ id: "off", label: t("settings.todoPromptOff") },
|
|
6829
|
+
];
|
|
6830
|
+
const conclusionTabOptions = [
|
|
6831
|
+
{ id: "newtab", label: t("settings.conclusionTabOn") },
|
|
6832
|
+
{ id: "off", label: t("settings.conclusionTabOff") },
|
|
6833
|
+
];
|
|
5543
6834
|
const boolRow = (label, field) =>
|
|
5544
6835
|
h(SettingSelect, {
|
|
5545
6836
|
label,
|
|
@@ -5568,23 +6859,53 @@ function FileTypeIcon(props) {
|
|
|
5568
6859
|
note(t("settings.timeModeNote")),
|
|
5569
6860
|
boolRow(t("settings.stepAnchors"), "stepAnchors"),
|
|
5570
6861
|
note(t("settings.stepAnchorsNote")),
|
|
5571
|
-
|
|
6862
|
+
h(SettingSelect, {
|
|
6863
|
+
label: t("settings.todoPromptRow"),
|
|
6864
|
+
value: cfg.todoPrompt ? "prompt" : "off",
|
|
6865
|
+
options: todoPromptOptions,
|
|
6866
|
+
onPick: (id) => set("todoPrompt", id === "prompt"),
|
|
6867
|
+
hint: h(
|
|
6868
|
+
"button",
|
|
6869
|
+
{
|
|
6870
|
+
type: "button",
|
|
6871
|
+
className: "dsov-set-hint",
|
|
6872
|
+
title: t("settings.todoPromptHint"),
|
|
6873
|
+
"aria-label": t("settings.todoPromptHint"),
|
|
6874
|
+
"data-active": showDefaultPrompt ? "true" : "false",
|
|
6875
|
+
onClick: () => setShowDefaultPrompt(!showDefaultPrompt),
|
|
6876
|
+
},
|
|
6877
|
+
h(IconInfo, { size: 13 })
|
|
6878
|
+
),
|
|
6879
|
+
}),
|
|
5572
6880
|
cfg.todoPrompt
|
|
5573
6881
|
? h(SettingTextarea, {
|
|
5574
|
-
label:
|
|
6882
|
+
label: "",
|
|
5575
6883
|
value: cfg.todoPromptTemplate,
|
|
5576
|
-
placeholder:
|
|
6884
|
+
placeholder: t("settings.todoPromptPlaceholder"),
|
|
5577
6885
|
onCommit: (value) => set("todoPromptTemplate", value),
|
|
5578
6886
|
})
|
|
6887
|
+
: h("div", { className: "dsov-set-note dsov-set-noteWrap" }, t("settings.todoPromptPlaceholder")),
|
|
6888
|
+
showDefaultPrompt
|
|
6889
|
+
? h(
|
|
6890
|
+
"div",
|
|
6891
|
+
{ className: "dsov-set-defaultPrompt" },
|
|
6892
|
+
h("div", { className: "dsov-set-defaultPromptHead" }, t("settings.todoPromptDefault")),
|
|
6893
|
+
h("pre", { className: "dsov-set-defaultPromptBody" }, DEFAULT_TODO_TEMPLATE.trim())
|
|
6894
|
+
)
|
|
5579
6895
|
: null,
|
|
5580
|
-
|
|
5581
|
-
|
|
6896
|
+
h(SettingSelect, {
|
|
6897
|
+
label: t("settings.conclusionTabRow"),
|
|
6898
|
+
value: cfg.conclusionTab ? "newtab" : "off",
|
|
6899
|
+
options: conclusionTabOptions,
|
|
6900
|
+
onPick: (id) => set("conclusionTab", id === "newtab"),
|
|
6901
|
+
}),
|
|
6902
|
+
note(t(cfg.conclusionTab ? "settings.conclusionTabNote" : "settings.conclusionTabOffNote"))
|
|
5582
6903
|
),
|
|
5583
6904
|
group(
|
|
5584
6905
|
t("settings.quickNav"),
|
|
5585
6906
|
boolRow(t("settings.quickNavRow"), "quickNav"),
|
|
5586
|
-
boolRow(t("settings.quickNavLinkRow"), "quickNavLink"),
|
|
5587
6907
|
note(t("settings.quickNavNote")),
|
|
6908
|
+
boolRow(t("settings.quickNavLinkRow"), "quickNavLink"),
|
|
5588
6909
|
note(t("settings.quickNavLinkNote"))
|
|
5589
6910
|
),
|
|
5590
6911
|
group(
|
|
@@ -5595,6 +6916,7 @@ function FileTypeIcon(props) {
|
|
|
5595
6916
|
options: placeOptions,
|
|
5596
6917
|
onPick: (id) => set("placement", id),
|
|
5597
6918
|
}),
|
|
6919
|
+
note(t("settings.placementNote")),
|
|
5598
6920
|
boolRow(t("settings.fullHeightRow"), "sectionFullHeight"),
|
|
5599
6921
|
note(t("settings.fullHeightNote"))
|
|
5600
6922
|
),
|
|
@@ -5662,9 +6984,9 @@ function FileTypeIcon(props) {
|
|
|
5662
6984
|
.dsov-headAction{flex:none;display:inline-flex;align-items:center;gap:5px;height:26px;padding:0 9px;border-radius:8px;border:.5px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer;white-space:nowrap}
|
|
5663
6985
|
.dsov-headAction:hover{color:var(--dsw-alias-brand-primary);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 45%,transparent);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent)}
|
|
5664
6986
|
.dsov-empty{font-size:12.5px;line-height:1.6;color:var(--dsw-alias-label-secondary);padding:4px 2px}
|
|
5665
|
-
.dsov-secBody{max-height:320px;overflow-y:auto;overflow-x:hidden;
|
|
6987
|
+
.dsov-secBody{max-height:320px;overflow-y:auto;overflow-x:hidden;margin:0 -12px 0 -8px;padding:0 12px 0 8px}
|
|
5666
6988
|
.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)}
|
|
6989
|
+
.dsov-loading{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
5668
6990
|
.dsov-goal{display:flex;align-items:flex-start;gap:8px;font-size:13px;color:var(--dsw-alias-label-primary);padding:2px}
|
|
5669
6991
|
.dsov-goalIcon{flex:none;display:inline-flex;margin-top:2px;color:var(--dsw-alias-brand-primary)}
|
|
5670
6992
|
.dsov-goalObjective{flex:1;min-width:0;line-height:1.55;overflow-wrap:anywhere}
|
|
@@ -5716,7 +7038,39 @@ function FileTypeIcon(props) {
|
|
|
5716
7038
|
.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
7039
|
.dsov-member[data-expandable=false]{cursor:default}
|
|
5718
7040
|
.dsov-member[data-expandable=false]:hover{background:transparent}
|
|
5719
|
-
.dsov-stepChevron{flex:none;margin-right:-2px}
|
|
7041
|
+
.dsov-stepChevron{flex:none;margin-right:-2px;position:relative}
|
|
7042
|
+
.dsov-stepChevron::after{content:"";position:absolute;inset:-5px -4px}
|
|
7043
|
+
.dsov-stepIconSwap{position:relative;flex:none;display:inline-flex;align-items:center;justify-content:center;width:15px;height:15px;cursor:pointer;border-radius:5px}
|
|
7044
|
+
.dsov-stepIconSwap::after{content:"";position:absolute;inset:-4px}
|
|
7045
|
+
.dsov-stepIconSwap>.dsov-todoIcon{transition:opacity .12s ease}
|
|
7046
|
+
.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}
|
|
7047
|
+
.dsov-step:hover .dsov-stepIconSwap>.dsov-todoIcon,.dsov-stepIconSwap:focus-visible>.dsov-todoIcon{opacity:0}
|
|
7048
|
+
.dsov-step:hover .dsov-stepSwapChevron{opacity:1}
|
|
7049
|
+
.dsov-stepIconSwap[data-open=true]>.dsov-todoIcon{opacity:0}
|
|
7050
|
+
.dsov-stepIconSwap[data-open=true] .dsov-stepSwapChevron{opacity:1}
|
|
7051
|
+
.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)}
|
|
7052
|
+
.dsov-stepBodySec{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
7053
|
+
.dsov-stepBodyLabel{font-size:10px;line-height:14px;letter-spacing:.02em;color:var(--dsw-alias-label-tertiary)}
|
|
7054
|
+
.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}
|
|
7055
|
+
.dsov-stepBodyErr{color:var(--dsw-alias-state-error-primary,#ff453a)}
|
|
7056
|
+
.dsov-stepBodyMeta{font-size:10px;line-height:14px;color:var(--dsw-alias-label-tertiary)}
|
|
7057
|
+
.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}
|
|
7058
|
+
.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)}
|
|
7059
|
+
.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)}
|
|
7060
|
+
.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)}
|
|
7061
|
+
.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)}
|
|
7062
|
+
.dsov-stepApprovalItem[data-tone=ok]{background:transparent}
|
|
7063
|
+
.dsov-stepApprovalHead{display:flex;align-items:center;gap:5px;min-width:0}
|
|
7064
|
+
.dsov-stepApprovalIcon{flex:none;display:inline-flex;color:var(--dsw-alias-label-secondary)}
|
|
7065
|
+
.dsov-stepApprovalName{flex:none;font-size:11px;font-weight:500;color:var(--dsw-alias-label-primary)}
|
|
7066
|
+
.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)}
|
|
7067
|
+
.dsov-stepApprovalBadge{flex:none;padding:0 5px;height:15px;display:inline-flex;align-items:center;border-radius:4px;font-size:10px;white-space:nowrap}
|
|
7068
|
+
.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)}
|
|
7069
|
+
.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)}
|
|
7070
|
+
.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)}
|
|
7071
|
+
.dsov-stepApprovalTime{flex:none;margin-left:auto;font-size:10px;color:var(--dsw-alias-label-tertiary)}
|
|
7072
|
+
.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}
|
|
7073
|
+
.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
7074
|
.dsov-compactIcon{color:var(--dsw-alias-label-secondary)}
|
|
5721
7075
|
.dsov-todoIconSlot{position:relative;flex:none;display:inline-flex;margin-top:2px}
|
|
5722
7076
|
.dsov-todoIconSlot .dsov-todoIcon{margin-top:0}
|
|
@@ -5728,7 +7082,8 @@ function FileTypeIcon(props) {
|
|
|
5728
7082
|
.dsov-rowStatic{cursor:default}
|
|
5729
7083
|
.dsov-rowStatic:hover{background:transparent}
|
|
5730
7084
|
.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}
|
|
7085
|
+
.dsov-fileIcon{flex:none;width:22px;display:inline-flex;justify-content:center;align-items:center}
|
|
7086
|
+
.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
7087
|
.dsov-fileIcon svg{display:block}
|
|
5733
7088
|
.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
7089
|
.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 +7106,8 @@ function FileTypeIcon(props) {
|
|
|
5751
7106
|
.dsov-set-link:hover{text-decoration:underline}
|
|
5752
7107
|
.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
7108
|
.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}
|
|
7109
|
+
.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}
|
|
7110
|
+
.dsov-set-labelText{min-width:0;overflow-wrap:anywhere}
|
|
5755
7111
|
.dsov-set-control{position:relative;flex:none;display:inline-flex}
|
|
5756
7112
|
.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
7113
|
.dsov-set-select:hover{background:var(--dsw-alias-interactive-bg-hover,color-mix(in srgb,var(--dsw-alias-label-primary) 8%,transparent))}
|
|
@@ -5764,6 +7120,13 @@ function FileTypeIcon(props) {
|
|
|
5764
7120
|
.dsov-set-menuItem[data-selected=true]{color:var(--dsw-alias-brand-primary);font-weight:500}
|
|
5765
7121
|
.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
7122
|
.dsov-set-note+.dsov-set-note{margin-top:2px}
|
|
7123
|
+
.dsov-set-labelText{min-width:0}
|
|
7124
|
+
.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}
|
|
7125
|
+
.dsov-set-noteWrap{white-space:pre-line}
|
|
7126
|
+
.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)}
|
|
7127
|
+
.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}
|
|
7128
|
+
.dsov-set-defaultPromptHead{font-size:11.5px;font-weight:600;color:var(--dsw-alias-label-secondary);margin-bottom:4px}
|
|
7129
|
+
.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
7130
|
.dsov-set-textrow{flex-direction:column;align-items:stretch;gap:6px}
|
|
5768
7131
|
.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
7132
|
.dsov-set-input:focus{outline:none;border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 55%,transparent)}
|
|
@@ -5775,6 +7138,10 @@ function FileTypeIcon(props) {
|
|
|
5775
7138
|
html[data-dsov-quicknav="on"] nav:has(button[data-index][aria-label]),html[data-dsov-quicknav="on"] nav[class*="_frame"]{display:block!important}
|
|
5776
7139
|
html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data-dsov-quicknav="off"] nav[class*="_frame"]{display:none!important}
|
|
5777
7140
|
.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}
|
|
7141
|
+
.dsov-docPage{box-sizing:border-box;height:100%;padding:20px 24px 40px;position:relative;overflow:auto}
|
|
7142
|
+
.dsov-docDocument{min-width:0;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere;font-size:14px;line-height:1.75}
|
|
7143
|
+
.dsov-docTitleIcon{flex:none}
|
|
7144
|
+
@media (width<=767px){.dsov-docPage{padding:16px 18px 32px}}
|
|
5778
7145
|
.dsov-vikingHead{display:flex;align-items:flex-start;gap:8px}
|
|
5779
7146
|
.dsov-vikingIcon{flex:none;display:inline-flex;margin-top:2px;color:var(--dsw-alias-brand-primary)}
|
|
5780
7147
|
.dsov-vikingHeadText{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
|
|
@@ -5854,6 +7221,51 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
5854
7221
|
.dsov-errPopTime{color:var(--dsw-alias-label-tertiary);font-size:10px;white-space:nowrap}
|
|
5855
7222
|
.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
7223
|
.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}
|
|
7224
|
+
.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)}
|
|
7225
|
+
.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)}
|
|
7226
|
+
.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)}
|
|
7227
|
+
.dsov-roleMode{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px;color:var(--dsw-alias-label-tertiary)}
|
|
7228
|
+
.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}
|
|
7229
|
+
.dsov-roleBtn:hover{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary)}
|
|
7230
|
+
.dsov-roleBtn[data-link=true]{color:var(--dsw-alias-brand-primary,#247bbf)}
|
|
7231
|
+
.dsov-roleBtn:disabled{cursor:default;opacity:.65}
|
|
7232
|
+
.dsov-roleChevron{flex:none;transition:transform .16s ease}
|
|
7233
|
+
.dsov-roleBtn[data-open=true] .dsov-roleChevron{transform:rotate(90deg)}
|
|
7234
|
+
.dsov-roleParent{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary)}
|
|
7235
|
+
.dsov-roleHint{display:inline-flex;align-items:center;gap:4px;min-width:0;color:var(--dsw-alias-label-tertiary)}
|
|
7236
|
+
.dsov-roleList{display:flex;flex-direction:column;gap:1px;width:100%;margin:1px 0 0}
|
|
7237
|
+
.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}
|
|
7238
|
+
.dsov-roleItem:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-label-primary) 6%,transparent)}
|
|
7239
|
+
.dsov-roleItem:disabled{cursor:default}
|
|
7240
|
+
.dsov-roleDot{flex:none;width:5px;height:5px;border-radius:50%;background:color-mix(in srgb,var(--dsw-alias-label-secondary) 45%,transparent)}
|
|
7241
|
+
.dsov-roleDot[data-running=true]{background:var(--dsw-alias-state-success-primary,#00b42a)}
|
|
7242
|
+
.dsov-roleItemTitle{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
7243
|
+
.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}
|
|
7244
|
+
.dsov-loadingReload:hover{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary)}
|
|
7245
|
+
@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}}
|
|
7246
|
+
@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}}
|
|
7247
|
+
.dsov-section[data-intro=even]{animation:dsov-secIntroA .44s cubic-bezier(.22,.61,.36,1) both}
|
|
7248
|
+
.dsov-section[data-intro=odd]{animation:dsov-secIntroB .44s cubic-bezier(.22,.61,.36,1) both}
|
|
7249
|
+
@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)}}
|
|
7250
|
+
.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}
|
|
7251
|
+
.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)}
|
|
7252
|
+
.dsov-stepChild{margin-left:21px;padding:3px 6px;border-radius:8px;align-items:flex-start;cursor:default;font-size:11.5px}
|
|
7253
|
+
.dsov-stepChild:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 3.5%,transparent)}
|
|
7254
|
+
.dsov-stepChild[data-waiting=true]{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#ff9f0a) 8%,transparent)}
|
|
7255
|
+
.dsov-stepChild .dsov-stepName{font-weight:400;color:var(--dsw-alias-label-secondary)}
|
|
7256
|
+
.dsov-stepChild .dsov-time{font-size:10.5px;color:var(--dsw-alias-label-tertiary)}
|
|
7257
|
+
.dsov-stepChild>.dsov-todoIcon{margin-top:1px;flex:none}
|
|
7258
|
+
.dsov-stepChildBody{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
|
|
7259
|
+
.dsov-stepChildTop{display:flex;align-items:center;gap:6px;min-width:0}
|
|
7260
|
+
.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}
|
|
7261
|
+
.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)}
|
|
7262
|
+
.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)}
|
|
7263
|
+
.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}
|
|
7264
|
+
.dsov-mermaid{margin:6px 0;text-align:center;overflow-x:auto}
|
|
7265
|
+
.dsov-mermaid svg{max-width:100%;height:auto}
|
|
7266
|
+
.dsov-mermaid .dsov-md-pre{text-align:left}
|
|
7267
|
+
.dsov-mermaidFailed>.dsov-md-pre{border-left:2px solid var(--dsw-alias-state-warning-primary,#e6a23c)}
|
|
7268
|
+
.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
7269
|
.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
7270
|
.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
7271
|
.dsov-turnSelect:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3,var(--dsw-alias-border-l2))}
|
|
@@ -5861,11 +7273,13 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
5861
7273
|
@keyframes dsov-pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
|
5862
7274
|
@keyframes dsov-stepIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
|
|
5863
7275
|
@keyframes dsov-stepOut{from{opacity:1;transform:none}to{opacity:0;transform:translateY(-6px)}}
|
|
7276
|
+
@keyframes dsov-refIn{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}
|
|
7277
|
+
.dsov-row[data-enter=true]{animation:dsov-refIn .32s ease both}
|
|
5864
7278
|
.dsov-step[data-enter=true]{animation:dsov-stepIn .34s ease both}
|
|
5865
7279
|
.dsov-step[data-exit=true]{position:absolute;top:0;left:0;right:0;animation:dsov-stepOut .4s ease both;pointer-events:none}
|
|
5866
7280
|
.dsov-todoSpin{transform-origin:7.5px 7.5px;animation:dsov-spin 1.1s linear infinite}
|
|
5867
7281
|
.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}}
|
|
7282
|
+
@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
7283
|
.dsov-vikingMode{align-self:flex-start;margin-left:auto}
|
|
5870
7284
|
.dsov-seg{flex:none;display:inline-flex;gap:2px;padding:2px;border-radius:8px;background:var(--dsw-alias-bg-layer-2)}
|
|
5871
7285
|
.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 +7521,7 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
6107
7521
|
kind: CONTEXT_TAB_KIND,
|
|
6108
7522
|
patterns: [`${CONTEXT_ADDRESS_PREFIX}**`],
|
|
6109
7523
|
priority: "builtin",
|
|
7524
|
+
multiple: true,
|
|
6110
7525
|
canOpen: (address) => parseContextAddress(address) !== undefined,
|
|
6111
7526
|
title: (address) => {
|
|
6112
7527
|
const key = parseContextAddress(address);
|
|
@@ -6135,6 +7550,22 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
6135
7550
|
"conversation-overview: context tab body"
|
|
6136
7551
|
);
|
|
6137
7552
|
|
|
7553
|
+
ctx.effect(
|
|
7554
|
+
() =>
|
|
7555
|
+
ctx.slots.inject(
|
|
7556
|
+
"sidebar.right.pane.tab.title",
|
|
7557
|
+
() =>
|
|
7558
|
+
ctx.slots.register(
|
|
7559
|
+
{
|
|
7560
|
+
name: "sidebar.right.pane.tab.title",
|
|
7561
|
+
key: CONTEXT_TAB_ID,
|
|
7562
|
+
},
|
|
7563
|
+
ContextTabTitle
|
|
7564
|
+
)
|
|
7565
|
+
),
|
|
7566
|
+
"conversation-overview: context tab title"
|
|
7567
|
+
);
|
|
7568
|
+
|
|
6138
7569
|
ctx.effect(() => {
|
|
6139
7570
|
if (typeof document === "undefined" || !document.documentElement) return undefined;
|
|
6140
7571
|
const applyAttr = () => {
|
|
@@ -6297,10 +7728,31 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
6297
7728
|
ProgressSection,
|
|
6298
7729
|
ArtifactsSection,
|
|
6299
7730
|
ReferencesSection,
|
|
7731
|
+
ContextPreviewBody,
|
|
7732
|
+
SettingsForm,
|
|
7733
|
+
chatMarkdown: ChatMarkdown,
|
|
7734
|
+
chatMarkdownLabels,
|
|
6300
7735
|
StatsBar,
|
|
6301
7736
|
ArtifactFileMenu,
|
|
6302
7737
|
OverviewGlyph,
|
|
6303
7738
|
OverviewContent,
|
|
7739
|
+
MarkdownBody,
|
|
7740
|
+
mdToHtml,
|
|
7741
|
+
Section,
|
|
7742
|
+
sectionIntroProps,
|
|
7743
|
+
SessionRoleBar,
|
|
7744
|
+
sessionRoleOf,
|
|
7745
|
+
canNavigateSession,
|
|
7746
|
+
openSessionById,
|
|
7747
|
+
planTitleOf,
|
|
7748
|
+
openNativePlan,
|
|
7749
|
+
documentAddress,
|
|
7750
|
+
openNativeDocument,
|
|
7751
|
+
openArtifact,
|
|
7752
|
+
ContextTabTitle,
|
|
7753
|
+
__setRuntime: (value) => {
|
|
7754
|
+
runtime = value;
|
|
7755
|
+
},
|
|
6304
7756
|
OverviewBoundary,
|
|
6305
7757
|
SettingsForm,
|
|
6306
7758
|
SettingsSection,
|