dsh-conversation-overview 1.3.1 → 1.4.1

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