dsh-conversation-overview 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js CHANGED
@@ -18,8 +18,10 @@ window.__ModuleLoader__.load({
18
18
  const OVERLAY_KEY = "dsh.conversationOverview.config";
19
19
  const ROW_ID = "conversation-overview";
20
20
  const ROW_KEY = "dsh-conversation-overview#conversation-overview";
21
+ const FEEDBACK_URL = "https://github.com/LingF/dsh-conversation-overview/issues";
21
22
  const PLACEMENTS = ["conversation", "sidebar", "both"];
22
- const CONFIG_FIELDS = ["showProgress", "showArtifacts", "showReferences", "placement", "sectionFullHeight", "quickNav", "quickNavLink", "vikingPreview", "vikingUrl", "vikingAccount", "vikingUser"];
23
+ const TIME_MODES = ["clock", "elapsed"];
24
+ const CONFIG_FIELDS = ["showProgress", "showArtifacts", "showReferences", "showStats", "timeMode", "placement", "sectionFullHeight", "todoPrompt", "todoPromptTemplate", "quickNav", "quickNavLink", "vikingPreview", "vikingUrl", "vikingAccount", "vikingUser"];
23
25
  const VIKING_TAB_ID = "dsh-conversation-overview/viking";
24
26
  const VIKING_TAB_KIND = "viking-memory";
25
27
  const VIKING_ADDRESS_PREFIX = "dsh-resource://viking-memory/";
@@ -46,7 +48,18 @@ window.__ModuleLoader__.load({
46
48
  "progress.empty": "生成任务/待办后将在此展示进展",
47
49
  "progress.emptyTurn": "本轮未创建任务计划",
48
50
  "progress.recentSteps": "本轮关键步骤(最近 6 条)",
49
- "progress.todoHint": "点击将该步骤的重执行提示词填入对话框",
51
+ "progress.allSteps": "本轮关键步骤(全部)",
52
+ "progress.showEarlierSteps": "显示更早的步骤",
53
+ "progress.moreStepsCount": "还有 {n} 条",
54
+ "progress.hideEarlierSteps": "收起早期步骤",
55
+ "team.phase.provisioning": "创建中",
56
+ "team.phase.active": "运行中",
57
+ "team.phase.inactive": "空闲",
58
+ "team.phase.failed": "失败",
59
+ "team.blocked": "阻塞 {n}",
60
+ "progress.expandTeam": "展开团队详情",
61
+ "progress.collapseTeam": "收起团队详情",
62
+ "progress.todoHint": "双击将该步骤的重执行提示词填入对话框",
50
63
  "progress.todoInserted": "已填入对话框",
51
64
  "progress.todoCopied": "未找到对话框,提示词已复制到剪贴板",
52
65
  "progress.todoFailed": "填入失败,请重试",
@@ -61,6 +74,12 @@ window.__ModuleLoader__.load({
61
74
  "settings.showProgress": "进展(任务与待办)",
62
75
  "settings.showArtifacts": "产物(本次对话产出)",
63
76
  "settings.showReferences": "引用(上下文、记忆等)",
77
+ "settings.showStats": "统计(用时、执行次数等)",
78
+ "settings.timeMode": "时间显示",
79
+ "settings.timeMode.clock": "时间点",
80
+ "settings.timeMode.elapsed": "本轮计时(从 0s 起)",
81
+ "settings.feedbackGroup": "反馈",
82
+ "settings.feedback": "问题反馈(GitHub Issues)",
64
83
  "settings.fullHeightRow": "分区完整展示(不限高)",
65
84
  "settings.fullHeightNote": "关闭时进展/产物/引用各自限制最大高度,超出部分在分区内滚动。",
66
85
  "settings.layout": "布局",
@@ -78,6 +97,11 @@ window.__ModuleLoader__.load({
78
97
  "settings.quickNavLinkRow": "导航联动概览",
79
98
  "settings.quickNavNote": "对话区右侧的轮次刻度条:悬停预览该轮问答,点击跳转;开启后窄窗口下也保持显示,刻度悬停可见轮次号。",
80
99
  "settings.quickNavLinkNote": "点击导航刻度跳转对话时,已打开的概览同步切换到对应轮次。",
100
+ "settings.interaction": "交互",
101
+ "settings.todoPromptRow": "双击待办生成提示词",
102
+ "settings.todoPromptTemplate": "提示词模板",
103
+ "settings.todoPromptNote": "双击进展中的待办行,把重执行提示词填入输入框。",
104
+ "settings.todoPromptTemplateNote": "模板留空则使用内置提示词;支持占位符 {index}(步骤序号)与 {content}(步骤内容)。",
81
105
  "settings.placementRow": "概览显示位置",
82
106
  "settings.viking": "OpenViking 记忆预览",
83
107
  "settings.vikingEnable": "启用记忆预览",
@@ -95,12 +119,12 @@ window.__ModuleLoader__.load({
95
119
  "viking.dirEmpty": "此目录为空",
96
120
  "viking.field.scenarios": "使用场景",
97
121
  "viking.field.keywords": "关键词",
122
+ "viking.field.source": "来源",
98
123
  "reference.sort.time": "时间",
99
- "reference.sort.kind": "类型",
100
124
  "reference.sort.count": "次数",
101
125
  "reference.sort.time.title": "按最近引用排序",
102
- "reference.sort.kind.title": "按类型分组排序(记忆、文件、技能、会话、上下文)",
103
126
  "reference.sort.count.title": "按引用次数排序",
127
+ "reference.sort.cancel": "点击取消排序",
104
128
  "reference.previewHint": "点击预览注入的内容快照",
105
129
  "scope.label": "概览范围",
106
130
  "scope.latest": "最新一轮",
@@ -178,11 +202,22 @@ window.__ModuleLoader__.load({
178
202
  "progress.empty": "Progress appears here once tasks or todos are generated",
179
203
  "progress.emptyTurn": "No task plan was created in this turn",
180
204
  "progress.recentSteps": "Key steps in this turn (latest 6)",
181
- "progress.todoHint": "Click to fill the composer with a re-run prompt for this step",
205
+ "progress.allSteps": "Key steps in this turn (all)",
206
+ "progress.showEarlierSteps": "Show earlier steps",
207
+ "progress.moreStepsCount": "{n} more steps",
208
+ "progress.hideEarlierSteps": "Hide earlier steps",
209
+ "progress.expandTeam": "Expand team details",
210
+ "progress.collapseTeam": "Collapse team details",
211
+ "progress.todoHint": "Double-click to fill the composer with a re-run prompt for this step",
182
212
  "progress.todoInserted": "Added to the composer",
183
213
  "progress.todoCopied": "Composer not found — prompt copied to the clipboard",
184
214
  "progress.todoFailed": "Could not fill the composer, try again",
185
215
  "progress.waiting": "Waiting for confirmation",
216
+ "team.phase.provisioning": "Provisioning",
217
+ "team.phase.active": "Active",
218
+ "team.phase.inactive": "Inactive",
219
+ "team.phase.failed": "Failed",
220
+ "team.blocked": "Blocked {n}",
186
221
  "progress.counts": "{completed} of {total} completed",
187
222
  "artifacts.empty": "Files produced in this conversation appear here",
188
223
  "artifacts.viewAll": "Review all changes",
@@ -193,6 +228,12 @@ window.__ModuleLoader__.load({
193
228
  "settings.showProgress": "Progress (tasks and todos)",
194
229
  "settings.showArtifacts": "Artifacts (files produced)",
195
230
  "settings.showReferences": "References (context, memories)",
231
+ "settings.showStats": "Stats (duration, tool calls, …)",
232
+ "settings.timeMode": "Time display",
233
+ "settings.timeMode.clock": "Clock time",
234
+ "settings.timeMode.elapsed": "Turn elapsed (from 0s)",
235
+ "settings.feedbackGroup": "Feedback",
236
+ "settings.feedback": "Report an issue (GitHub Issues)",
196
237
  "settings.fullHeightRow": "Show sections in full (no max height)",
197
238
  "settings.fullHeightNote": "When off, Progress/Artifacts/References cap their height and scroll inside each section.",
198
239
  "settings.layout": "Layout",
@@ -210,6 +251,11 @@ window.__ModuleLoader__.load({
210
251
  "settings.quickNavLinkRow": "Link rail with overview",
211
252
  "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.",
212
253
  "settings.quickNavLinkNote": "When you click a rail tick to jump, an open overview switches to that turn as well.",
254
+ "settings.interaction": "Interaction",
255
+ "settings.todoPromptRow": "Double-click todo prompt",
256
+ "settings.todoPromptTemplate": "Prompt template",
257
+ "settings.todoPromptNote": "Double-click a todo row in Progress to fill the rerun prompt into the composer.",
258
+ "settings.todoPromptTemplateNote": "Leave empty for the built-in prompt; placeholders: {index} (step number), {content} (step text).",
213
259
  "settings.placementRow": "Show the overview in",
214
260
  "settings.viking": "OpenViking memory preview",
215
261
  "settings.vikingEnable": "Enable memory preview",
@@ -227,12 +273,12 @@ window.__ModuleLoader__.load({
227
273
  "viking.dirEmpty": "This directory is empty",
228
274
  "viking.field.scenarios": "Usage scenarios",
229
275
  "viking.field.keywords": "Keywords",
276
+ "viking.field.source": "Source",
230
277
  "reference.sort.time": "Time",
231
- "reference.sort.kind": "Kind",
232
278
  "reference.sort.count": "Count",
233
279
  "reference.sort.time.title": "Sort by most recently referenced",
234
- "reference.sort.kind.title": "Group by kind (memories, files, skills, sessions, context)",
235
280
  "reference.sort.count.title": "Sort by reference count",
281
+ "reference.sort.cancel": "Click again to clear",
236
282
  "reference.previewHint": "Click to preview the injected content snapshot",
237
283
  "scope.label": "Scope",
238
284
  "scope.latest": "Latest turn",
@@ -322,8 +368,12 @@ window.__ModuleLoader__.load({
322
368
  showProgress: flag(source.showProgress),
323
369
  showArtifacts: flag(source.showArtifacts),
324
370
  showReferences: flag(source.showReferences),
371
+ showStats: flag(source.showStats),
372
+ timeMode: TIME_MODES.indexOf(source.timeMode) !== -1 ? source.timeMode : "clock",
325
373
  placement: PLACEMENTS.indexOf(source.placement) !== -1 ? source.placement : "both",
326
374
  sectionFullHeight: source.sectionFullHeight === true,
375
+ todoPrompt: typeof source.todoPrompt === "boolean" ? source.todoPrompt : true,
376
+ todoPromptTemplate: typeof source.todoPromptTemplate === "string" ? source.todoPromptTemplate : "",
327
377
  quickNav: typeof source.quickNav === "boolean" ? source.quickNav : true,
328
378
  quickNavLink: typeof source.quickNavLink === "boolean" ? source.quickNavLink : true,
329
379
  vikingPreview: typeof source.vikingPreview === "boolean" ? source.vikingPreview : true,
@@ -452,8 +502,18 @@ window.__ModuleLoader__.load({
452
502
  return String(value).padStart(2, "0");
453
503
  }
454
504
 
455
- function formatTime(time) {
505
+ /** Compact "since turn start" reading: 0s → 59s → 3m05s → 1h02m. */
506
+ function formatElapsed(ms) {
507
+ const sec = Math.max(0, Math.floor(ms / 1000));
508
+ if (sec < 60) return `${sec}s`;
509
+ const min = Math.floor(sec / 60);
510
+ if (min < 60) return `${min}m${pad2(sec % 60)}s`;
511
+ return `${Math.floor(min / 60)}h${pad2(min % 60)}m`;
512
+ }
513
+
514
+ function formatTime(time, mode, base) {
456
515
  if (typeof time !== "number" || !Number.isFinite(time) || time <= 0) return "";
516
+ if (mode === "elapsed" && typeof base === "number" && base > 0 && time >= base) return formatElapsed(time - base);
457
517
  const date = new Date(time);
458
518
  const now = new Date();
459
519
  const hm = `${pad2(date.getHours())}:${pad2(date.getMinutes())}`;
@@ -1106,18 +1166,28 @@ window.__ModuleLoader__.load({
1106
1166
  pending: new Map(),
1107
1167
  changes: [],
1108
1168
  steps: [],
1169
+ anchors: [],
1109
1170
  stepByCall: new Map(),
1110
1171
  byTurn: new Map(),
1172
+ members: new Map(),
1173
+ teamTasks: new Map(),
1174
+ subagents: new Map(),
1111
1175
  stats: createStats(),
1112
1176
  changed: false,
1113
1177
  };
1114
1178
  }
1115
1179
 
1180
+ /** Upsert one team/member, team/task or subagent/catalog record; first observation time is kept. */
1181
+ function upsertTeamRecord(map, key, record) {
1182
+ const existing = map.get(key);
1183
+ map.set(key, existing === undefined ? { ...record, firstTime: record.time } : { ...existing, ...record, firstTime: existing.firstTime });
1184
+ }
1185
+
1116
1186
  function createTurnBucket() {
1117
- return { artifacts: new Map(), refs: new Map(), refOrder: [], changes: [], steps: [], stats: createStats() };
1187
+ return { artifacts: new Map(), refs: new Map(), refOrder: [], changes: [], steps: [], anchors: [], members: new Map(), teamTasks: new Map(), subagents: new Map(), stats: createStats() };
1118
1188
  }
1119
1189
 
1120
- const EMPTY_BUCKET = Object.freeze({ artifacts: new Map(), refs: new Map(), refOrder: [], changes: [], steps: [], stats: Object.freeze(createStats()) });
1190
+ 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()) });
1121
1191
 
1122
1192
  function turnBucket(folder, turn) {
1123
1193
  let bucket = folder.byTurn.get(turn);
@@ -1182,23 +1252,27 @@ window.__ModuleLoader__.load({
1182
1252
  for (const listener of [...navScopeStore.listeners]) listener();
1183
1253
  }
1184
1254
 
1185
- /* Re-run prompt for one todo step (template provided by the user). */
1186
- function rerunPrompt(number, content) {
1187
- return [
1188
- "请查看当前任务的完整 TODO 计划,重新执行以下指定步骤:",
1189
- "",
1190
- `**目标步骤:${number}. ${content}**`,
1191
- "",
1192
- "要求:",
1193
- "1. 查看完整计划及相关上下文,明确目标步骤的执行要求和前置依赖。",
1194
- "2. 无论该步骤当前状态如何,都必须重新检查并实际执行,不得直接跳过。",
1195
- "3. 仅处理目标步骤所需内容,避免重复执行其他步骤或修改无关内容。",
1196
- "4. 执行必要的测试或验证,如实反馈结果。",
1197
- "5. 更新目标步骤的 TODO 状态,保持其他步骤状态不变。",
1198
- "6. 汇报执行结果、修改内容及未解决的问题。",
1199
- "",
1200
- "**请直接执行,不要只提供方案。**",
1201
- ].join("\n");
1255
+ /* Re-run prompt for one todo step. Placeholder {index}/{content} are substituted;
1256
+ an empty template falls back to the built-in default. */
1257
+ const DEFAULT_TODO_TEMPLATE = [
1258
+ "请查看当前任务的完整 TODO 计划,重新执行以下指定步骤:",
1259
+ "",
1260
+ "**目标步骤:{index}. {content}**",
1261
+ "",
1262
+ "要求:",
1263
+ "1. 查看完整计划及相关上下文,明确目标步骤的执行要求和前置依赖。",
1264
+ "2. 无论该步骤当前状态如何,都必须重新检查并实际执行,不得直接跳过。",
1265
+ "3. 仅处理目标步骤所需内容,避免重复执行其他步骤或修改无关内容。",
1266
+ "4. 执行必要的测试或验证,如实反馈结果。",
1267
+ "5. 更新目标步骤的 TODO 状态,保持其他步骤状态不变。",
1268
+ "6. 汇报执行结果、修改内容及未解决的问题。",
1269
+ "",
1270
+ "**请直接执行,不要只提供方案。**",
1271
+ ].join("\n");
1272
+
1273
+ function rerunPrompt(number, content, template) {
1274
+ const source = typeof template === "string" && template.trim() !== "" ? template : DEFAULT_TODO_TEMPLATE;
1275
+ return source.split("{index}").join(String(number)).split("{content}").join(String(content));
1202
1276
  }
1203
1277
 
1204
1278
  /** The visible Lexical chat composer root (contenteditable textbox), or null. */
@@ -1370,9 +1444,18 @@ window.__ModuleLoader__.load({
1370
1444
 
1371
1445
  /* Key-step stream: every tool call is one step; the result marks it done. Shown by
1372
1446
  the Progress section when a turn has no todo plan of its own. */
1373
- const STEP_CAP = 40;
1447
+ const STEP_CAP = 200;
1374
1448
 
1375
1449
  /** One-line human target for a tool call: file, command, query, url, name… */
1450
+ /** Team-record key a tool call creates: teammate name / task subject / subagent label. */
1451
+ function teamStepKey(name, args) {
1452
+ if (!isRecord(args)) return undefined;
1453
+ if (name === "spawn_teammate" && typeof args.name === "string" && args.name !== "") return args.name;
1454
+ if (name === "team_task_create" && typeof args.subject === "string" && args.subject !== "") return args.subject;
1455
+ if ((name === "subagent" || name === "subagent_fork") && typeof args.description === "string" && args.description !== "") return args.description;
1456
+ return undefined;
1457
+ }
1458
+
1376
1459
  function stepDetail(name, args) {
1377
1460
  if (!isRecord(args)) return "";
1378
1461
  const keys = ["file_path", "path", "command", "pattern", "url", "query", "prompt", "subject", "description", "name", "uri", "target", "id"];
@@ -1405,7 +1488,16 @@ window.__ModuleLoader__.load({
1405
1488
  return text.length > 4000 ? text.slice(0, 4000) : text;
1406
1489
  }
1407
1490
 
1491
+ const ANCHOR_CAP = 8;
1408
1492
  function addStep(folder, bucket, step) {
1493
+ if (typeof step.teamKey === "string") {
1494
+ folder.anchors.push(step);
1495
+ if (folder.anchors.length > ANCHOR_CAP) folder.anchors.shift();
1496
+ if (isRecord(bucket) && Array.isArray(bucket.anchors)) {
1497
+ bucket.anchors.push(step);
1498
+ if (bucket.anchors.length > ANCHOR_CAP) bucket.anchors.shift();
1499
+ }
1500
+ }
1409
1501
  folder.steps.push(step);
1410
1502
  if (folder.steps.length > STEP_CAP) {
1411
1503
  const dropped = folder.steps.shift();
@@ -1465,6 +1557,57 @@ window.__ModuleLoader__.load({
1465
1557
  touch(folder);
1466
1558
  return;
1467
1559
  }
1560
+ case "team/member": {
1561
+ if (!isRecord(data) || !isRecord(data.member)) return;
1562
+ const member = data.member;
1563
+ if (typeof member.id !== "string" || member.id === "" || typeof member.name !== "string") return;
1564
+ const memberRecord = {
1565
+ id: member.id,
1566
+ name: member.name,
1567
+ description: typeof member.description === "string" ? member.description : "",
1568
+ phase: typeof member.phase === "string" ? member.phase : "",
1569
+ seq: event.seq,
1570
+ time: typeof event.time === "number" ? event.time : 0,
1571
+ };
1572
+ upsertTeamRecord(folder.members, member.id, memberRecord);
1573
+ upsertTeamRecord(turnBucket(folder, folder.currentTurn).members, member.id, memberRecord);
1574
+ touch(folder);
1575
+ return;
1576
+ }
1577
+ case "team/task": {
1578
+ if (!isRecord(data) || !isRecord(data.task)) return;
1579
+ const task = data.task;
1580
+ if (typeof task.id !== "string" || task.id === "" || typeof task.subject !== "string") return;
1581
+ const taskRecord = {
1582
+ id: task.id,
1583
+ subject: task.subject,
1584
+ description: typeof task.description === "string" ? task.description : "",
1585
+ status: typeof task.status === "string" ? task.status : "",
1586
+ ownerId: typeof task.ownerId === "string" ? task.ownerId : undefined,
1587
+ blockedBy: Array.isArray(task.blockedBy) ? task.blockedBy.filter((item) => typeof item === "string") : [],
1588
+ revision: typeof task.revision === "number" ? task.revision : 0,
1589
+ seq: event.seq,
1590
+ time: typeof event.time === "number" ? event.time : 0,
1591
+ };
1592
+ upsertTeamRecord(folder.teamTasks, task.id, taskRecord);
1593
+ upsertTeamRecord(turnBucket(folder, folder.currentTurn).teamTasks, task.id, taskRecord);
1594
+ touch(folder);
1595
+ return;
1596
+ }
1597
+ case "subagent/catalog": {
1598
+ if (!isRecord(data) || typeof data.childId !== "string" || data.childId === "") return;
1599
+ const subRecord = {
1600
+ childId: data.childId,
1601
+ label: typeof data.label === "string" ? data.label : "",
1602
+ mode: typeof data.mode === "string" ? data.mode : "",
1603
+ seq: event.seq,
1604
+ time: typeof event.time === "number" ? event.time : 0,
1605
+ };
1606
+ upsertTeamRecord(folder.subagents, data.childId, subRecord);
1607
+ upsertTeamRecord(turnBucket(folder, folder.currentTurn).subagents, data.childId, subRecord);
1608
+ touch(folder);
1609
+ return;
1610
+ }
1468
1611
  case "workspace/changes": {
1469
1612
  if (!isRecord(data) || typeof data.turn !== "number" || !Number.isSafeInteger(data.turn) || data.turn < 1) return;
1470
1613
  if (typeof event.seq !== "number") return;
@@ -1495,7 +1638,7 @@ window.__ModuleLoader__.load({
1495
1638
  const name = data.name;
1496
1639
  const args = parseArgs(data.arguments);
1497
1640
  const callBucket = turnBucket(folder, folder.currentTurn);
1498
- addStep(folder, callBucket, { callId, name, detail: stepDetail(name, args), full: stepFull(name, args), done: false, seq: event.seq, time: event.time });
1641
+ addStep(folder, callBucket, { callId, name, detail: stepDetail(name, args), full: stepFull(name, args), teamKey: teamStepKey(name, args), done: false, seq: event.seq, time: event.time });
1499
1642
  folder.stats.toolCalls += 1;
1500
1643
  callBucket.stats.toolCalls += 1;
1501
1644
  if (name === "bash") {
@@ -1715,13 +1858,32 @@ window.__ModuleLoader__.load({
1715
1858
  errors: st.errors,
1716
1859
  files: st.files.size,
1717
1860
  };
1861
+ /* Agent-team projections: members, shared board tasks, and non-team subagents. */
1862
+ const byFirstTime = (left, right) => (left.firstTime || left.time || 0) - (right.firstTime || right.time || 0) || (left.seq || 0) - (right.seq || 0);
1863
+ const mapValues = (map) => (map instanceof Map ? [...map.values()] : []);
1864
+ const members = mapValues(source.members).sort(byFirstTime);
1865
+ const subagents = mapValues(source.subagents)
1866
+ .filter((sub) => !(source.members instanceof Map && source.members.has(sub.childId)))
1867
+ .sort(byFirstTime);
1868
+ const team = { members, tasks: mapValues(source.teamTasks).sort(byFirstTime), subagents };
1869
+ /* team anchor steps (spawn/task-create/subagent) stay pinned above the recent window
1870
+ so member/task rows can nest under their creating step */
1871
+ const allSteps = Array.isArray(source.steps) ? source.steps : [];
1872
+ const recentSteps = allSteps.slice(-6);
1873
+ const anchorCandidates = [...(Array.isArray(source.anchors) ? source.anchors : []), ...allSteps].filter(
1874
+ (step, index, list) => isRecord(step) && typeof step.teamKey === "string" && list.indexOf(step) === index && recentSteps.indexOf(step) === -1
1875
+ );
1876
+ const anchorSteps = anchorCandidates.slice(-6);
1718
1877
  return {
1719
1878
  todos,
1720
1879
  todosTime,
1721
1880
  artifacts,
1722
1881
  references,
1723
- steps: Array.isArray(source.steps) ? source.steps.slice(-6) : [],
1882
+ steps: [...anchorSteps, ...recentSteps],
1883
+ stepsAll: allSteps.slice(),
1884
+ team,
1724
1885
  stats,
1886
+ turnStart: st.start > 0 ? st.start : null,
1725
1887
  changeTurns: changeTurns.slice(),
1726
1888
  hasMore: windowSnapshot.hasMore === true,
1727
1889
  localeRev,
@@ -1731,7 +1893,7 @@ window.__ModuleLoader__.load({
1731
1893
  };
1732
1894
  }
1733
1895
 
1734
- const EMPTY_STATE = { todos: null, todosTime: 0, artifacts: [], references: [], steps: [], stats: { durationMs: 0, toolCalls: 0, commands: 0, errors: 0, files: 0 }, changeTurns: [], hasMore: false, localeRev: 0, turns: [], latestTurn: null, scope: "latest" };
1896
+ 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, changeTurns: [], hasMore: false, localeRev: 0, turns: [], latestTurn: null, scope: "latest" };
1735
1897
 
1736
1898
  function createSummaryStore(binding) {
1737
1899
  const source = binding.eventSource;
@@ -1903,7 +2065,7 @@ window.__ModuleLoader__.load({
1903
2065
  /* Prefer the jump loader: it pages back until the window covers seq and
1904
2066
  survives concurrent loaders. loadOlder() is a no-op while another load
1905
2067
  is in flight, so retry until the window actually stops reporting more. */
1906
- for (let attempt = 0; attempt < 40 && !cancelled; attempt += 1) {
2068
+ for (let attempt = 0; attempt < 200 && !cancelled; attempt += 1) {
1907
2069
  const snapshot = binding.eventSource.getSnapshot();
1908
2070
  if (!isRecord(snapshot) || snapshot.hasMore !== true) return;
1909
2071
  const session = binding.session;
@@ -2677,6 +2839,14 @@ function FileTypeIcon(props) {
2677
2839
  return svg(size, className, [h("path", { key: "a", d: "m5.8 3.6 4.4 4.4-4.4 4.4", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round" })]);
2678
2840
  }
2679
2841
 
2842
+ function IconHistory({ size = 16, className }) {
2843
+ return svg(size, className, [
2844
+ h("path", { key: "a", d: "M2.6 8a5.4 5.4 0 1 0 5.4-5.4A5.9 5.9 0 0 0 3.9 4.3L2.6 5.6", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round" }),
2845
+ h("path", { key: "b", d: "M2.6 2.4v3.2h3.2", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round" }),
2846
+ h("path", { key: "c", d: "M8 4.9v3.2l2.7 1.5", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round" }),
2847
+ ]);
2848
+ }
2849
+
2680
2850
  function IconCompare({ size = 16, className }) {
2681
2851
  return svg(size, className, [
2682
2852
  h("path", { key: "a", d: "M3 5.4h8.2M9.2 3.4l2 2-2 2", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round" }),
@@ -2698,6 +2868,13 @@ function FileTypeIcon(props) {
2698
2868
  ]);
2699
2869
  }
2700
2870
 
2871
+ function IconAgent({ size = 16, className }) {
2872
+ return svg(size, className, [
2873
+ h("circle", { key: "a", cx: 8, cy: 5.4, r: 2.5, stroke: "currentColor" }),
2874
+ h("path", { key: "b", d: "M3.4 13c.7-2.2 2.4-3.4 4.6-3.4S12 10.8 12.6 13", stroke: "currentColor", strokeLinecap: "round" }),
2875
+ ]);
2876
+ }
2877
+
2701
2878
  function TodoStatusIcon({ status }) {
2702
2879
  if (status === "completed") {
2703
2880
  return svg(15, undefined, [
@@ -2732,12 +2909,13 @@ function FileTypeIcon(props) {
2732
2909
 
2733
2910
  /* References ordering, shared by both surfaces and persisted per browser. */
2734
2911
  const REF_SORT_KEY = "dsh.conversationOverview.refSort";
2735
- const REF_SORT_MODES = ["time", "kind", "count"];
2912
+ const REF_SORT_MODES = ["time", "count"];
2736
2913
  const KIND_ORDER = ["memory-file", "memory", "file", "skill", "session", "context"];
2737
2914
 
2738
2915
  function readStoredRefSort() {
2739
2916
  try {
2740
2917
  const value = globalThis.localStorage ? globalThis.localStorage.getItem(REF_SORT_KEY) : null;
2918
+ if (value === "none") return "";
2741
2919
  return REF_SORT_MODES.indexOf(value) !== -1 ? value : "time";
2742
2920
  } catch (_unreadable) {
2743
2921
  return "time";
@@ -2747,10 +2925,11 @@ function FileTypeIcon(props) {
2747
2925
  const refSortStore = { mode: readStoredRefSort(), listeners: new Set() };
2748
2926
 
2749
2927
  function setRefSort(mode) {
2750
- if (REF_SORT_MODES.indexOf(mode) === -1 || refSortStore.mode === mode) return;
2751
- refSortStore.mode = mode;
2928
+ const next = mode === "" ? "" : REF_SORT_MODES.indexOf(mode) !== -1 ? mode : undefined;
2929
+ if (next === undefined || refSortStore.mode === next) return;
2930
+ refSortStore.mode = next;
2752
2931
  try {
2753
- if (globalThis.localStorage) globalThis.localStorage.setItem(REF_SORT_KEY, mode);
2932
+ if (globalThis.localStorage) globalThis.localStorage.setItem(REF_SORT_KEY, next === "" ? "none" : next);
2754
2933
  } catch (_unwritable) {
2755
2934
  /* the choice still applies for this page load */
2756
2935
  }
@@ -2867,7 +3046,6 @@ function FileTypeIcon(props) {
2867
3046
  /** Incoming order is newest-first; kind/count modes re-rank, breaking ties by recency. */
2868
3047
  function sortReferences(refs, mode) {
2869
3048
  const byRecency = (left, right) => (right.time || right.seq || 0) - (left.time || left.seq || 0) || (right.seq || 0) - (left.seq || 0);
2870
- if (mode === "kind") return refs.slice().sort((left, right) => kindRank(left.kind) - kindRank(right.kind) || byRecency(left, right));
2871
3049
  if (mode === "count") return refs.slice().sort((left, right) => (right.count || 1) - (left.count || 1) || byRecency(left, right));
2872
3050
  return refs;
2873
3051
  }
@@ -2907,6 +3085,14 @@ function FileTypeIcon(props) {
2907
3085
  completed: "todo.completed",
2908
3086
  };
2909
3087
 
3088
+ /* Agent-team member phases (tooltip text; status is conveyed by icons). */
3089
+ const MEMBER_PHASE_KEYS = {
3090
+ provisioning: "team.phase.provisioning",
3091
+ active: "team.phase.active",
3092
+ inactive: "team.phase.inactive",
3093
+ failed: "team.phase.failed",
3094
+ };
3095
+
2910
3096
  const GOAL_PHASE_KEYS = {
2911
3097
  active: "goal.active",
2912
3098
  paused: "goal.paused",
@@ -2921,13 +3107,17 @@ function FileTypeIcon(props) {
2921
3107
  complete: "success",
2922
3108
  };
2923
3109
 
2924
- function ProgressSection({ summary, goal, t, waiting }) {
3110
+ function ProgressSection({ summary, goal, t, waiting, cfg }) {
3111
+ const fmtTime = (ts) => formatTime(ts, cfg.timeMode, summary.turnStart);
3112
+ const timeTitle = (ts) => (cfg.timeMode === "elapsed" ? formatTime(ts) : undefined);
2925
3113
  /* todo → composer prompt flash */
2926
3114
  const [promptFlash, setPromptFlash] = React.useState(null);
2927
3115
  const flashTimer = React.useRef(undefined);
2928
3116
  /* step → copy summary flash */
2929
3117
  const [stepFlash, setStepFlash] = React.useState(null);
2930
3118
  const copyTimer = React.useRef(undefined);
3119
+ /* per-member expansion overrides for the merged team rows */
3120
+ const [teamExpanded, setTeamExpanded] = React.useState(null);
2931
3121
  React.useEffect(
2932
3122
  () => () => {
2933
3123
  if (flashTimer.current !== undefined) clearTimeout(flashTimer.current);
@@ -2935,8 +3125,11 @@ function FileTypeIcon(props) {
2935
3125
  },
2936
3126
  []
2937
3127
  );
3128
+ const todoPromptOn = !isRecord(cfg) || cfg.todoPrompt !== false;
3129
+ const todoPromptTemplate = isRecord(cfg) && typeof cfg.todoPromptTemplate === "string" ? cfg.todoPromptTemplate : "";
2938
3130
  const onTodoClick = (index, content) => {
2939
- const result = insertIntoComposer(rerunPrompt(index + 1, content));
3131
+ if (!todoPromptOn) return;
3132
+ const result = insertIntoComposer(rerunPrompt(index + 1, content, todoPromptTemplate));
2940
3133
  if (flashTimer.current !== undefined) clearTimeout(flashTimer.current);
2941
3134
  setPromptFlash({ index, state: result });
2942
3135
  flashTimer.current = setTimeout(() => {
@@ -2972,15 +3165,24 @@ function FileTypeIcon(props) {
2972
3165
  };
2973
3166
  /* steps: newest-first window; keep the evicted bottom row as a fading ghost */
2974
3167
  const steps = Array.isArray(summary.steps) ? summary.steps.filter((step) => isRecord(step)) : [];
3168
+ const stepsAll = Array.isArray(summary.stepsAll) ? summary.stepsAll.filter((step) => isRecord(step)) : [];
2975
3169
  const [ghost, setGhost] = React.useState(null);
3170
+ const [stepsExpanded, setStepsExpanded] = React.useState(false);
2976
3171
  const ghostTimer = React.useRef(undefined);
2977
3172
  const prevSteps = React.useRef([]);
2978
3173
  const prevScope = React.useRef(summary.scope);
2979
3174
  /* enter animation only for rows that arrive while mounted; first paint per scope is silent */
2980
3175
  const stepKeyOf = (step, index) => (typeof step.callId === "string" && step.callId !== "" ? `step-${step.callId}` : `step-idx-${index}`);
2981
3176
  const seenSteps = React.useRef(null);
3177
+ /* full retained history for the expander: anchors outside the window + the whole bucket */
3178
+ const allStepIds = new Set(stepsAll.map((step) => step.callId).filter((id) => typeof id === "string"));
3179
+ const anchorExtras = steps.filter((step) => typeof step.callId === "string" && !allStepIds.has(step.callId));
3180
+ const expandedSteps = [...anchorExtras, ...stepsAll];
3181
+ const canExpand = expandedSteps.length > steps.length;
3182
+ const stepsOpen = stepsExpanded && canExpand;
3183
+ const displaySteps = stepsOpen ? expandedSteps : steps;
2982
3184
  if (seenSteps.current === null || seenSteps.current.scope !== summary.scope) {
2983
- seenSteps.current = { scope: summary.scope, keys: new Set(steps.map(stepKeyOf)) };
3185
+ seenSteps.current = { scope: summary.scope, keys: new Set(displaySteps.map(stepKeyOf)) };
2984
3186
  }
2985
3187
  React.useEffect(
2986
3188
  () => () => {
@@ -3006,6 +3208,173 @@ function FileTypeIcon(props) {
3006
3208
  setGhost(null);
3007
3209
  }, 400);
3008
3210
  }, [steps]);
3211
+ /* team data + todos drive both the merged list and the step nesting */
3212
+ const team = isRecord(summary.team) ? summary.team : null;
3213
+ const members = team && Array.isArray(team.members) ? team.members.filter((member) => isRecord(member)) : [];
3214
+ const teamTasks = team && Array.isArray(team.tasks) ? team.tasks.filter((task) => isRecord(task)) : [];
3215
+ const subagents = team && Array.isArray(team.subagents) ? team.subagents.filter((sub) => isRecord(sub)) : [];
3216
+ const todos = Array.isArray(summary.todos) ? summary.todos : null;
3217
+ const hasTodos = todos !== null && todos.length > 0;
3218
+ const hasTeam = members.length + teamTasks.length + subagents.length > 0;
3219
+ const showSteps = !hasTodos && steps.length > 0;
3220
+ const flashOf = (key) => (stepFlash !== null && isRecord(stepFlash) && stepFlash.key === key ? stepFlash.state : null);
3221
+ const overrideOf = (key, fallback) => {
3222
+ const stored = teamExpanded !== null && isRecord(teamExpanded) ? teamExpanded[key] : undefined;
3223
+ return typeof stored === "boolean" ? stored : fallback;
3224
+ };
3225
+ const toggleKey = (key, next) => setTeamExpanded({ ...(teamExpanded !== null && isRecord(teamExpanded) ? teamExpanded : {}), [key]: next });
3226
+ const ownedBy = (id) => teamTasks.filter((task) => task.ownerId === id);
3227
+ const taskRow = (task, depth) => {
3228
+ const status = TODO_KEYS[task.status] ? task.status : "pending";
3229
+ const blocked = Array.isArray(task.blockedBy) ? task.blockedBy.length : 0;
3230
+ const flash = flashOf(`teamtask-${task.id}`);
3231
+ const titleParts = [task.subject];
3232
+ if (task.description !== "") titleParts.push(task.description);
3233
+ titleParts.push(blocked > 0 ? `${t(TODO_KEYS[status])} · ${t("team.blocked", { n: blocked })}` : t(TODO_KEYS[status]));
3234
+ return h(
3235
+ "div",
3236
+ {
3237
+ className: "dsov-todo dsov-task",
3238
+ key: `task-${task.id}`,
3239
+ "data-status": status,
3240
+ "data-depth": depth > 0 ? String(depth) : undefined,
3241
+ "data-flash": flash === "ok" ? "ok" : flash === "err" ? "err" : undefined,
3242
+ title: titleParts.join("\n\n"),
3243
+ onClick: () => onStepClick({ callId: `teamtask-${task.id}`, name: task.subject, full: task.subject, seq: task.seq }),
3244
+ },
3245
+ h("span", { className: "dsov-todoIcon", "data-status": status }, h(TodoStatusIcon, { status })),
3246
+ h("span", { className: "dsov-taskSubject" }, task.subject),
3247
+ h("span", { className: "dsov-time", title: timeTitle(task.time) }, fmtTime(task.time))
3248
+ );
3249
+ };
3250
+ const subRow = (sub, depth) => {
3251
+ const flash = flashOf(`subagent-${sub.childId}`);
3252
+ return h(
3253
+ "div",
3254
+ {
3255
+ className: "dsov-todo dsov-subagent",
3256
+ key: `subagent-${sub.childId}`,
3257
+ "data-depth": depth > 0 ? String(depth) : undefined,
3258
+ "data-flash": flash === "ok" ? "ok" : flash === "err" ? "err" : undefined,
3259
+ title: sub.label !== "" ? sub.label : sub.childId,
3260
+ onClick: () => onStepClick({ callId: `subagent-${sub.childId}`, name: sub.label, full: sub.label !== "" ? sub.label : sub.childId, seq: sub.seq }),
3261
+ },
3262
+ h("span", { className: "dsov-todoIcon dsov-agentIcon" }, h(IconAgent, { size: 14 })),
3263
+ h("span", { className: "dsov-memberName" }, sub.label !== "" ? sub.label : sub.childId),
3264
+ sub.mode !== "" ? h("span", { className: "dsov-badge", "data-tone": "quiet" }, sub.mode) : null,
3265
+ h("span", { className: "dsov-time", title: timeTitle(sub.time) }, fmtTime(sub.time))
3266
+ );
3267
+ };
3268
+ const memberIcon = (member, owned) => {
3269
+ const phase = typeof member.phase === "string" ? member.phase : "";
3270
+ if (phase === "failed") {
3271
+ return h(
3272
+ "span",
3273
+ { className: "dsov-todoIcon", "data-status": "failed" },
3274
+ h(
3275
+ "svg",
3276
+ { width: 14, height: 14, viewBox: "0 0 14 14", fill: "none", "aria-hidden": "true" },
3277
+ h("circle", { cx: 7, cy: 7, r: 5.6, stroke: "currentColor", strokeWidth: 1.3 }),
3278
+ h("path", { d: "M7 4v3.6", stroke: "currentColor", strokeWidth: 1.3, strokeLinecap: "round" }),
3279
+ h("circle", { cx: 7, cy: 9.9, r: 0.8, fill: "currentColor" })
3280
+ )
3281
+ );
3282
+ }
3283
+ if (owned.length > 0 && owned.every((task) => task.status === "completed")) {
3284
+ return h("span", { className: "dsov-todoIcon", "data-status": "completed" }, h(TodoStatusIcon, { status: "completed" }));
3285
+ }
3286
+ return h("span", { className: "dsov-todoIcon dsov-agentIcon" }, h(IconAgent, { size: 14 }));
3287
+ };
3288
+ const memberRow = (member, depth, expandable, open) => {
3289
+ const owned = ownedBy(member.id);
3290
+ const phase = typeof member.phase === "string" ? member.phase : "";
3291
+ const titleParts = [];
3292
+ if (member.description !== "") titleParts.push(member.description);
3293
+ if (MEMBER_PHASE_KEYS[phase]) titleParts.push(t(MEMBER_PHASE_KEYS[phase]));
3294
+ return h(
3295
+ "div",
3296
+ {
3297
+ className: "dsov-todo dsov-member",
3298
+ key: `member-${member.id}`,
3299
+ "data-depth": depth > 0 ? String(depth) : undefined,
3300
+ "data-expandable": expandable ? "true" : "false",
3301
+ "data-open": expandable ? (open ? "true" : "false") : undefined,
3302
+ title: titleParts.length > 0 ? titleParts.join(" · ") : member.name,
3303
+ onClick: expandable ? () => toggleKey(`member:${member.id}`, !open) : undefined,
3304
+ },
3305
+ expandable ? h("span", { className: "dsov-chevron", "data-open": open ? "true" : "false" }, h(IconChevron, { size: 11 })) : null,
3306
+ memberIcon(member, owned),
3307
+ h("span", { className: "dsov-memberName" }, member.name),
3308
+ h("span", { className: "dsov-memberDesc" }, member.description),
3309
+ h("span", { className: "dsov-time", title: timeTitle(member.time) }, fmtTime(member.time))
3310
+ );
3311
+ };
3312
+ /* attach team records under their creating step (steps view only) */
3313
+ const consumedMembers = new Set();
3314
+ const consumedTasks = new Set();
3315
+ const consumedSubs = new Set();
3316
+ const stepAttachments = new Map();
3317
+ if (showSteps && hasTeam) {
3318
+ displaySteps.forEach((step, index) => {
3319
+ const teamKey = typeof step.teamKey === "string" ? step.teamKey : "";
3320
+ if (teamKey === "" || typeof step.name !== "string") return;
3321
+ const attached = [];
3322
+ if (step.name === "spawn_teammate") {
3323
+ const member = members.find((candidate) => candidate.name === teamKey && !consumedMembers.has(candidate.id));
3324
+ if (member !== undefined) {
3325
+ consumedMembers.add(member.id);
3326
+ const phase = typeof member.phase === "string" ? member.phase : "";
3327
+ attached.push({ row: memberRow(member, 1, false, false), unfinished: phase === "provisioning" || phase === "active" });
3328
+ ownedBy(member.id).forEach((task) => {
3329
+ if (consumedTasks.has(task.id)) return;
3330
+ consumedTasks.add(task.id);
3331
+ attached.push({ row: taskRow(task, 2), unfinished: task.status !== "completed" });
3332
+ });
3333
+ }
3334
+ } else if (step.name === "subagent" || step.name === "subagent_fork") {
3335
+ const sub = subagents.find((candidate) => (candidate.label === teamKey || candidate.childId === teamKey) && !consumedSubs.has(candidate.childId));
3336
+ if (sub !== undefined) {
3337
+ consumedSubs.add(sub.childId);
3338
+ attached.push({ row: subRow(sub, 1), unfinished: false });
3339
+ }
3340
+ } else if (step.name === "team_task_create") {
3341
+ /* owned tasks nest under their member instead; only unowned tasks attach here */
3342
+ const task = teamTasks.find(
3343
+ (candidate) =>
3344
+ candidate.subject === teamKey &&
3345
+ !consumedTasks.has(candidate.id) &&
3346
+ !(typeof candidate.ownerId === "string" && members.some((member) => member.id === candidate.ownerId))
3347
+ );
3348
+ if (task !== undefined) {
3349
+ consumedTasks.add(task.id);
3350
+ attached.push({ row: taskRow(task, 1), unfinished: task.status !== "completed" });
3351
+ }
3352
+ }
3353
+ if (attached.length > 0) stepAttachments.set(stepKeyOf(step, index), attached);
3354
+ });
3355
+ }
3356
+ /* records not attached to a visible step render flat in the list */
3357
+ const pushFlatTeam = (target) => {
3358
+ teamTasks.forEach((task) => {
3359
+ if (consumedTasks.has(task.id)) return;
3360
+ const ownerKnown = typeof task.ownerId === "string" && members.some((member) => member.id === task.ownerId);
3361
+ if (ownerKnown) return;
3362
+ target.push(taskRow(task, 0));
3363
+ });
3364
+ members.forEach((member) => {
3365
+ if (consumedMembers.has(member.id)) return;
3366
+ const owned = ownedBy(member.id).filter((task) => !consumedTasks.has(task.id));
3367
+ const expandable = owned.length > 0;
3368
+ const phase = typeof member.phase === "string" ? member.phase : "";
3369
+ const open = expandable ? overrideOf(`member:${member.id}`, owned.some((task) => task.status !== "completed") || phase === "provisioning") : false;
3370
+ target.push(memberRow(member, 0, expandable, open));
3371
+ if (expandable && open) owned.forEach((task) => target.push(taskRow(task, 1)));
3372
+ });
3373
+ subagents.forEach((sub) => {
3374
+ if (consumedSubs.has(sub.childId)) return;
3375
+ target.push(subRow(sub, 0));
3376
+ });
3377
+ };
3009
3378
  const stepRow = (step, key, extraProps, isWaiting) => {
3010
3379
  const stepRunning = step.done !== true;
3011
3380
  const failed = step.error === true;
@@ -3013,7 +3382,10 @@ function FileTypeIcon(props) {
3013
3382
  const detail = typeof step.detail === "string" && step.detail !== "" ? step.detail : "";
3014
3383
  const full = typeof step.full === "string" && step.full !== "" ? step.full : detail !== "" ? detail : typeof step.name === "string" ? step.name : "";
3015
3384
  const flash = stepFlash !== null && isRecord(stepFlash) && stepFlash.key === (typeof step.callId === "string" && step.callId !== "" ? step.callId : `${step.name}|${step.seq}`) ? stepFlash.state : null;
3016
- return h(
3385
+ const attached = stepAttachments.get(key);
3386
+ const expandable = Array.isArray(attached) && attached.length > 0;
3387
+ const open = expandable ? overrideOf(`step:${key}`, attached.some((item) => item.unfinished)) : false;
3388
+ const row = h(
3017
3389
  "div",
3018
3390
  {
3019
3391
  className: "dsov-step",
@@ -3025,6 +3397,21 @@ function FileTypeIcon(props) {
3025
3397
  ...(extraProps === undefined ? {} : extraProps),
3026
3398
  onClick: () => onStepClick(step),
3027
3399
  },
3400
+ expandable
3401
+ ? h(
3402
+ "span",
3403
+ {
3404
+ className: "dsov-chevron dsov-stepChevron",
3405
+ "data-open": open ? "true" : "false",
3406
+ title: t(open ? "progress.collapseTeam" : "progress.expandTeam"),
3407
+ onClick: (event) => {
3408
+ event.stopPropagation();
3409
+ toggleKey(`step:${key}`, !open);
3410
+ },
3411
+ },
3412
+ h(IconChevron, { size: 11 })
3413
+ )
3414
+ : null,
3028
3415
  waitingRow
3029
3416
  ? h(
3030
3417
  "span",
@@ -3039,8 +3426,14 @@ function FileTypeIcon(props) {
3039
3426
  : h("span", { className: "dsov-todoIcon", "data-status": stepRunning ? "in_progress" : "completed" }, h(TodoStatusIcon, { status: stepRunning ? "in_progress" : "completed" })),
3040
3427
  h("span", { className: "dsov-stepName" }, typeof step.name === "string" ? step.name : ""),
3041
3428
  h("span", { className: "dsov-stepDetail" }, detail),
3042
- h("span", { className: "dsov-time" }, formatTime(typeof step.endTime === "number" ? step.endTime : step.time))
3429
+ h("span", { className: "dsov-time", title: timeTitle(typeof step.endTime === "number" ? step.endTime : step.time) }, fmtTime(typeof step.endTime === "number" ? step.endTime : step.time))
3043
3430
  );
3431
+ return expandable && open ? [row, ...attached.map((item) => item.row)] : row;
3432
+ };
3433
+ const pushStepRow = (target, step, key, extraProps, isWaiting) => {
3434
+ const rendered = stepRow(step, key, extraProps, isWaiting);
3435
+ if (Array.isArray(rendered)) target.push(...rendered);
3436
+ else target.push(rendered);
3044
3437
  };
3045
3438
  const kids = [];
3046
3439
  if (goal) {
@@ -3055,8 +3448,7 @@ function FileTypeIcon(props) {
3055
3448
  )
3056
3449
  );
3057
3450
  }
3058
- const todos = Array.isArray(summary.todos) ? summary.todos : null;
3059
- if (todos && todos.length > 0) {
3451
+ if (hasTodos) {
3060
3452
  const total = todos.length;
3061
3453
  const done = todos.filter((item) => item.status === "completed").length;
3062
3454
  kids.push(
@@ -3070,8 +3462,9 @@ function FileTypeIcon(props) {
3070
3462
  todos.forEach((todo, index) => {
3071
3463
  const status = TODO_KEYS[todo.status] ? todo.status : "pending";
3072
3464
  const flash = promptFlash !== null && isRecord(promptFlash) && promptFlash.index === index ? promptFlash.state : null;
3073
- const title =
3074
- flash === "inserted"
3465
+ const title = !todoPromptOn
3466
+ ? todo.content
3467
+ : flash === "inserted"
3075
3468
  ? t("progress.todoInserted")
3076
3469
  : flash === "copied"
3077
3470
  ? t("progress.todoCopied")
@@ -3087,9 +3480,9 @@ function FileTypeIcon(props) {
3087
3480
  "data-status": status,
3088
3481
  "data-flash": flash === "inserted" || flash === "copied" ? "ok" : flash === "failed" ? "err" : undefined,
3089
3482
  role: "button",
3090
- tabIndex: 0,
3483
+ tabIndex: todoPromptOn ? 0 : undefined,
3091
3484
  title,
3092
- onClick: () => onTodoClick(index, todo.content),
3485
+ onDoubleClick: () => onTodoClick(index, todo.content),
3093
3486
  onKeyDown: (event) => {
3094
3487
  if (event.key === "Enter" || event.key === " ") {
3095
3488
  event.preventDefault();
@@ -3103,12 +3496,14 @@ function FileTypeIcon(props) {
3103
3496
  )
3104
3497
  );
3105
3498
  });
3106
- } else if (steps.length > 0) {
3499
+ }
3500
+ if (hasTeam && !showSteps) pushFlatTeam(kids);
3501
+ if (showSteps) {
3107
3502
  const markWaiting = waiting === true && summary.scope === "latest";
3108
3503
  let waitingIndex = -1;
3109
3504
  if (markWaiting) {
3110
- for (let index = steps.length - 1; index >= 0; index -= 1) {
3111
- if (steps[index].done !== true && steps[index].error !== true) {
3505
+ for (let index = displaySteps.length - 1; index >= 0; index -= 1) {
3506
+ if (displaySteps[index].done !== true && displaySteps[index].error !== true) {
3112
3507
  waitingIndex = index;
3113
3508
  break;
3114
3509
  }
@@ -3116,25 +3511,48 @@ function FileTypeIcon(props) {
3116
3511
  }
3117
3512
  const rows = [];
3118
3513
  let freshCount = 0;
3119
- steps.forEach((step, index) => {
3514
+ displaySteps.forEach((step, index) => {
3120
3515
  const key = stepKeyOf(step, index);
3121
3516
  const fresh = !seenSteps.current.keys.has(key);
3122
3517
  if (fresh) {
3123
3518
  seenSteps.current.keys.add(key);
3124
3519
  freshCount += 1;
3125
3520
  }
3126
- rows.push(stepRow(step, key, fresh ? { "data-enter": "true", style: { animationDelay: `${(freshCount - 1) * 45}ms` } } : undefined, index === waitingIndex));
3521
+ pushStepRow(rows, step, key, fresh ? { "data-enter": "true", style: { animationDelay: `${(freshCount - 1) * 45}ms` } } : undefined, index === waitingIndex);
3127
3522
  });
3128
- if (ghost !== null && isRecord(ghost)) {
3129
- rows.push(stepRow(ghost, `ghost-${ghost.callId}`, { "data-exit": "true" }, false));
3523
+ if (ghost !== null && isRecord(ghost) && !stepsOpen) {
3524
+ pushStepRow(rows, ghost, `ghost-${ghost.callId}`, { "data-exit": "true" }, false);
3130
3525
  }
3131
- kids.push(h("div", { className: "dsov-stepsHead", key: "stepsHead" }, t("progress.recentSteps")));
3526
+ if (hasTeam) pushFlatTeam(rows);
3527
+ kids.push(
3528
+ h(
3529
+ "div",
3530
+ { className: "dsov-stepsHead", key: "stepsHead" },
3531
+ h("span", null, t(stepsOpen ? "progress.allSteps" : "progress.recentSteps")),
3532
+ canExpand
3533
+ ? h(
3534
+ "button",
3535
+ {
3536
+ type: "button",
3537
+ className: "dsov-moreSteps",
3538
+ title: stepsOpen ? t("progress.hideEarlierSteps") : t("progress.moreStepsCount", { n: expandedSteps.length - steps.length }),
3539
+ onClick: () => setStepsExpanded(!stepsOpen),
3540
+ },
3541
+ t(stepsOpen ? "progress.hideEarlierSteps" : "progress.showEarlierSteps"),
3542
+ h(IconHistory, { size: 12, className: "dsov-moreIcon" })
3543
+ )
3544
+ : null
3545
+ )
3546
+ );
3132
3547
  kids.push(h("div", { className: "dsov-steps", key: "steps" }, rows));
3133
3548
  }
3134
3549
  return h(Section, { id: "progress", icon: h(IconProgress, { size: 15 }), title: t("section.progress"), empty: t(summary.scope === "all" ? "progress.empty" : "progress.emptyTurn") }, kids);
3135
3550
  }
3136
3551
 
3137
3552
  function ArtifactsSection({ summary, t, binding, stats }) {
3553
+ const cfg = useConfig();
3554
+ const fmtTime = (ts) => formatTime(ts, cfg.timeMode, summary.turnStart);
3555
+ const timeTitle = (ts) => (cfg.timeMode === "elapsed" ? formatTime(ts) : undefined);
3138
3556
  const sessionId = binding ? binding.sessionId : undefined;
3139
3557
  const cwd = sessionCwd(sessionId);
3140
3558
  const turns = summary.changeTurns;
@@ -3173,7 +3591,7 @@ function FileTypeIcon(props) {
3173
3591
  ? h("span", { className: "dsov-badge", "data-tone": "quiet" }, t("artifact.binary"))
3174
3592
  : null,
3175
3593
  artifact.presented ? h("span", { className: "dsov-badge", "data-tone": "brand" }, t("artifact.presented")) : null,
3176
- h("span", { className: "dsov-rowTail" }, h("span", { className: "dsov-time" }, formatTime(artifact.time)))
3594
+ h("span", { className: "dsov-rowTail" }, h("span", { className: "dsov-time", title: timeTitle(artifact.time) }, fmtTime(artifact.time)))
3177
3595
  );
3178
3596
  });
3179
3597
  return h(
@@ -3290,6 +3708,8 @@ function FileTypeIcon(props) {
3290
3708
 
3291
3709
  function ReferencesSection({ summary, t, sessionId }) {
3292
3710
  const cfg = useConfig();
3711
+ const fmtTime = (ts) => formatTime(ts, cfg.timeMode, summary.turnStart);
3712
+ const timeTitle = (ts) => (cfg.timeMode === "elapsed" ? formatTime(ts) : undefined);
3293
3713
  const sortMode = useRefSort();
3294
3714
  const hideCommon = useHideCommonRefs();
3295
3715
  const typeFilter = useRefTypes();
@@ -3386,7 +3806,7 @@ function FileTypeIcon(props) {
3386
3806
  )
3387
3807
  : null
3388
3808
  ),
3389
- h("span", { className: "dsov-time", key: "time" }, formatTime(ref.time)),
3809
+ h("span", { className: "dsov-time", key: "time", title: timeTitle(ref.time) }, fmtTime(ref.time)),
3390
3810
  ];
3391
3811
  const isFile = ref.kind === "file";
3392
3812
  if (isFile) {
@@ -3504,8 +3924,8 @@ function FileTypeIcon(props) {
3504
3924
  key: mode,
3505
3925
  className: "dsov-segBtn",
3506
3926
  "data-active": sortMode === mode ? "true" : "false",
3507
- title: t(`reference.sort.${mode}.title`),
3508
- onClick: () => setRefSort(mode),
3927
+ title: sortMode === mode ? `${t(`reference.sort.${mode}.title`)} · ${t("reference.sort.cancel")}` : t(`reference.sort.${mode}.title`),
3928
+ onClick: () => setRefSort(sortMode === mode ? "" : mode),
3509
3929
  },
3510
3930
  t(`reference.sort.${mode}`)
3511
3931
  )
@@ -3571,6 +3991,7 @@ function FileTypeIcon(props) {
3571
3991
  const cfg = useConfig();
3572
3992
  const sessionId = binding ? binding.sessionId : undefined;
3573
3993
  const rootClass = variant === "panel" ? "dsov-panel" : "dsov-view";
3994
+ const rootRef = React.useRef(null);
3574
3995
  const scope = summary.scope === selection || selection === "latest" ? summary.scope : selection;
3575
3996
  const hasTurns = Array.isArray(summary.turns) && summary.turns.length >= 2;
3576
3997
  const turnOptions = Array.isArray(summary.turns) ? summary.turns.slice().reverse() : [];
@@ -3591,33 +4012,92 @@ function FileTypeIcon(props) {
3591
4012
  h("option", { value: "all", key: "all" }, t("scope.all"))
3592
4013
  )
3593
4014
  : null;
3594
- const statsBar = h(StatsBar, { stats: summary.stats, t });
3595
- const hasStats = isRecord(summary.stats) && (summary.stats.durationMs > 0 || summary.stats.toolCalls > 0);
4015
+ const statsBar = cfg.showStats !== false ? h(StatsBar, { stats: summary.stats, t }) : null;
4016
+ const hasStats = cfg.showStats !== false && isRecord(summary.stats) && (summary.stats.durationMs > 0 || summary.stats.toolCalls > 0);
4017
+ const barPresent = variant === "panel" ? scopeSelect !== null || hasStats : !(scopeSelect === null && cfg.placement === "conversation" && !hasStats);
4018
+ /* toggle data-stuck on the sticky wrapper: background only shows while pinned */
4019
+ React.useEffect(() => {
4020
+ const root = rootRef.current;
4021
+ if (root === null || typeof window === "undefined" || typeof getComputedStyle !== "function") return undefined;
4022
+ const wrapper = root.querySelector('[data-sticky-bar="true"]');
4023
+ if (wrapper === null) return undefined;
4024
+ let scroller = root;
4025
+ while (scroller !== null && scroller !== document.body && scroller !== document.documentElement) {
4026
+ const oy = getComputedStyle(scroller).overflowY;
4027
+ if (oy === "auto" || oy === "scroll" || oy === "overlay") break;
4028
+ /* an overflow:hidden ancestor only counts when it actually scrolls; otherwise the
4029
+ search continues to the real scroller (the conversation tab has hidden wrappers) */
4030
+ if (oy === "hidden" && scroller.scrollHeight > scroller.clientHeight + 1) break;
4031
+ scroller = scroller.parentElement;
4032
+ }
4033
+ const target = scroller !== null && scroller !== document.body && scroller !== document.documentElement ? scroller : null;
4034
+ /* Transform-based pinning: CSS sticky binds to the nearest scrollport, which an
4035
+ overflow:hidden wrapper can hijack, leaving the bar to slide under the tab bar. */
4036
+ let lastDelta = 0;
4037
+ const update = () => {
4038
+ const frameTop = target === null ? 0 : target.getBoundingClientRect().top;
4039
+ const scrolled = target === null ? window.scrollY : target.scrollTop;
4040
+ const natural = wrapper.getBoundingClientRect().top - lastDelta - frameTop + scrolled;
4041
+ const delta = Math.max(0, scrolled - natural);
4042
+ if (delta !== lastDelta) {
4043
+ wrapper.style.transform = delta > 0 ? `translateY(${delta}px)` : "";
4044
+ lastDelta = delta;
4045
+ }
4046
+ const flag = delta > 0.5 ? "true" : "false";
4047
+ if (wrapper.getAttribute("data-stuck") !== flag) wrapper.setAttribute("data-stuck", flag);
4048
+ };
4049
+ update();
4050
+ const host = target === null ? window : target;
4051
+ host.addEventListener("scroll", update, { passive: true });
4052
+ window.addEventListener("resize", update);
4053
+ return () => {
4054
+ host.removeEventListener("scroll", update);
4055
+ window.removeEventListener("resize", update);
4056
+ wrapper.style.transform = "";
4057
+ };
4058
+ }, [variant, barPresent]);
3596
4059
  return h(
3597
4060
  "div",
3598
- { className: rootClass, "data-conversation-overview": variant === "panel" ? "panel" : "view" },
4061
+ { className: rootClass, ref: rootRef, "data-conversation-overview": variant === "panel" ? "panel" : "view" },
3599
4062
  variant === "panel"
3600
4063
  ? scopeSelect !== null || hasStats
3601
- ? h("div", { className: "dsov-turnbar" }, statsBar, scopeSelect)
4064
+ ? h(
4065
+ "div",
4066
+ { className: "dsov-turnbarWrap", "data-sticky-bar": "true", "data-stuck": "false" },
4067
+ h(
4068
+ "div",
4069
+ { className: "dsov-turnbar" },
4070
+ statsBar !== null ? h("div", { className: "dsov-turnbarScroll" }, statsBar) : null,
4071
+ scopeSelect !== null ? h("div", { className: "dsov-turnbarFixed" }, scopeSelect) : null
4072
+ )
4073
+ )
3602
4074
  : null
3603
4075
  : scopeSelect === null && cfg.placement === "conversation" && !hasStats
3604
4076
  ? null
3605
4077
  : h(
3606
4078
  "div",
3607
- { className: "dsov-toolbar" },
3608
- statsBar,
3609
- scopeSelect,
3610
- cfg.placement === "conversation"
3611
- ? null
3612
- : h(
3613
- "button",
3614
- { type: "button", className: "dsov-iconBtn", onClick: openOverviewTab, title: t("action.openInSidebar") },
3615
- h(IconOpenSidebar, { size: 14 }),
3616
- t("action.openInSidebar")
3617
- )
4079
+ { className: "dsov-toolbarWrap", "data-sticky-bar": "true", "data-stuck": "false" },
4080
+ h(
4081
+ "div",
4082
+ { className: "dsov-toolbar" },
4083
+ statsBar !== null ? h("div", { className: "dsov-toolbarScroll" }, statsBar) : null,
4084
+ h(
4085
+ "div",
4086
+ { className: "dsov-toolbarFixed" },
4087
+ scopeSelect,
4088
+ cfg.placement === "conversation"
4089
+ ? null
4090
+ : h(
4091
+ "button",
4092
+ { type: "button", className: "dsov-iconBtn", onClick: openOverviewTab, title: t("action.openInSidebar") },
4093
+ h(IconOpenSidebar, { size: 14 }),
4094
+ t("action.openInSidebar")
4095
+ )
4096
+ )
4097
+ )
3618
4098
  ),
3619
4099
  summary.hasMore ? h("div", { className: "dsov-loading" }, t("state.loadingHistory")) : null,
3620
- cfg.showProgress ? h(ProgressSection, { summary, goal, t, waiting: sessionWaiting }) : null,
4100
+ cfg.showProgress ? h(ProgressSection, { summary, goal, t, waiting: sessionWaiting, cfg }) : null,
3621
4101
  cfg.showArtifacts ? h(ArtifactsSection, { summary, t, binding, stats }) : null,
3622
4102
  cfg.showReferences ? h(ReferencesSection, { summary, t, sessionId }) : null
3623
4103
  );
@@ -3803,7 +4283,8 @@ function FileTypeIcon(props) {
3803
4283
  const FRONTMATTER_TITLE_KEYS = ["title", "name"];
3804
4284
  const FRONTMATTER_TAG_KEYS = ["keywords", "tags"];
3805
4285
  const FRONTMATTER_SCENARIO_KEYS = ["usage_scenario", "when_to_use"];
3806
- const FRONTMATTER_SKIP_KEYS = new Set([...FRONTMATTER_TITLE_KEYS, ...FRONTMATTER_TAG_KEYS, ...FRONTMATTER_SCENARIO_KEYS, "description"]);
4286
+ const FRONTMATTER_SOURCE_KEYS = ["source", "sources"];
4287
+ const FRONTMATTER_SKIP_KEYS = new Set([...FRONTMATTER_TITLE_KEYS, ...FRONTMATTER_TAG_KEYS, ...FRONTMATTER_SCENARIO_KEYS, ...FRONTMATTER_SOURCE_KEYS, "description"]);
3807
4288
 
3808
4289
  function frontmatterTitle(fields) {
3809
4290
  for (const key of FRONTMATTER_TITLE_KEYS) {
@@ -3822,6 +4303,26 @@ function FileTypeIcon(props) {
3822
4303
  if (description && description.value !== "") {
3823
4304
  blocks.push(h("p", { className: "dsov-vikingDesc", key: "description" }, description.value));
3824
4305
  }
4306
+ const sourceField = fields.find((field) => FRONTMATTER_SOURCE_KEYS.indexOf(field.key) !== -1 && (field.value !== "" || field.items.length > 0));
4307
+ if (sourceField) {
4308
+ const values = sourceField.items.length > 0 ? sourceField.items : [sourceField.value];
4309
+ blocks.push(
4310
+ h(
4311
+ "div",
4312
+ { className: "dsov-vikingField", key: "source" },
4313
+ h("div", { className: "dsov-vikingFieldLabel" }, t("viking.field.source")),
4314
+ h(
4315
+ "div",
4316
+ { className: "dsov-vikingSource" },
4317
+ values.map((value, index) =>
4318
+ /^https?:\/\//i.test(value)
4319
+ ? h("a", { key: `${value}#${index}`, className: "dsov-vikingSourceLink", href: value, target: "_blank", rel: "noreferrer" }, value)
4320
+ : h("span", { key: `${value}#${index}`, className: "dsov-vikingFieldValue" }, value)
4321
+ )
4322
+ )
4323
+ )
4324
+ );
4325
+ }
3825
4326
  const scenarios = fields.filter((field) => FRONTMATTER_SCENARIO_KEYS.indexOf(field.key) !== -1 && field.items.length > 0);
3826
4327
  if (scenarios.length > 0) {
3827
4328
  blocks.push(
@@ -4042,6 +4543,41 @@ function FileTypeIcon(props) {
4042
4543
  );
4043
4544
  }
4044
4545
 
4546
+ /* Multi-line text row committing on blur and on unmount (prompt template). */
4547
+ function SettingTextarea({ label, value, placeholder, onCommit }) {
4548
+ const [draft, setDraft] = React.useState(value);
4549
+ const [focused, setFocused] = React.useState(false);
4550
+ const pending = React.useRef({ draft: value, value, commit: onCommit });
4551
+ pending.current = { draft, value, commit: onCommit };
4552
+ React.useEffect(
4553
+ () => () => {
4554
+ const state = pending.current;
4555
+ if (typeof state.draft === "string" && state.draft !== state.value) state.commit(state.draft);
4556
+ },
4557
+ []
4558
+ );
4559
+ return h(
4560
+ "div",
4561
+ { className: "dsov-set-row dsov-set-arearow" },
4562
+ h("span", { className: "dsov-set-label" }, label),
4563
+ h("textarea", {
4564
+ className: "dsov-set-textarea",
4565
+ rows: 7,
4566
+ value: focused === true ? draft : value,
4567
+ placeholder,
4568
+ onFocus: () => {
4569
+ setDraft(value);
4570
+ setFocused(true);
4571
+ },
4572
+ onBlur: () => {
4573
+ setFocused(false);
4574
+ if (typeof draft === "string" && draft !== value) onCommit(draft);
4575
+ },
4576
+ onChange: (event) => setDraft(event.target.value),
4577
+ })
4578
+ );
4579
+ }
4580
+
4045
4581
  function SettingsForm(props) {
4046
4582
  const t = typeof props.t === "function" ? props.t : translate;
4047
4583
  const hookForm = useSettingsForm();
@@ -4070,6 +4606,10 @@ function FileTypeIcon(props) {
4070
4606
  { id: "sidebar", label: t("settings.placement.sidebar") },
4071
4607
  { id: "both", label: t("settings.placement.both") },
4072
4608
  ];
4609
+ const timeOptions = [
4610
+ { id: "clock", label: t("settings.timeMode.clock") },
4611
+ { id: "elapsed", label: t("settings.timeMode.elapsed") },
4612
+ ];
4073
4613
  const boolRow = (label, field) =>
4074
4614
  h(SettingSelect, {
4075
4615
  label,
@@ -4089,7 +4629,23 @@ function FileTypeIcon(props) {
4089
4629
  t("settings.sections"),
4090
4630
  boolRow(t("settings.showProgress"), "showProgress"),
4091
4631
  boolRow(t("settings.showArtifacts"), "showArtifacts"),
4092
- boolRow(t("settings.showReferences"), "showReferences")
4632
+ boolRow(t("settings.showReferences"), "showReferences"),
4633
+ boolRow(t("settings.showStats"), "showStats")
4634
+ ),
4635
+ group(
4636
+ t("settings.interaction"),
4637
+ h(SettingSelect, { label: t("settings.timeMode"), value: cfg.timeMode, options: timeOptions, onPick: (id) => set("timeMode", id) }),
4638
+ boolRow(t("settings.todoPromptRow"), "todoPrompt"),
4639
+ cfg.todoPrompt
4640
+ ? h(SettingTextarea, {
4641
+ label: t("settings.todoPromptTemplate"),
4642
+ value: cfg.todoPromptTemplate,
4643
+ placeholder: DEFAULT_TODO_TEMPLATE,
4644
+ onCommit: (value) => set("todoPromptTemplate", value),
4645
+ })
4646
+ : null,
4647
+ note(t("settings.todoPromptNote")),
4648
+ cfg.todoPrompt ? note(t("settings.todoPromptTemplateNote")) : null
4093
4649
  ),
4094
4650
  group(
4095
4651
  t("settings.quickNav"),
@@ -4123,6 +4679,19 @@ function FileTypeIcon(props) {
4123
4679
  : null,
4124
4680
  note(cfg.vikingPreview ? t("settings.vikingNote") : t("settings.vikingOffNote"))
4125
4681
  ),
4682
+ group(
4683
+ t("settings.feedbackGroup"),
4684
+ h(
4685
+ "a",
4686
+ { className: "dsov-set-link", href: FEEDBACK_URL, target: "_blank", rel: "noreferrer", title: FEEDBACK_URL },
4687
+ h("span", null, t("settings.feedback")),
4688
+ h(
4689
+ "svg",
4690
+ { width: 11, height: 11, viewBox: "0 0 12 12", fill: "none", "aria-hidden": "true" },
4691
+ h("path", { d: "M4.6 2.4H2.8c-.7 0-1.3.6-1.3 1.3v5.5c0 .7.6 1.3 1.3 1.3h5.5c.7 0 1.3-.6 1.3-1.3V7.4M7.2 1.5h3.3v3.3M6.3 5.7 10.2 1.8", stroke: "currentColor", strokeWidth: 1.1, strokeLinecap: "round", strokeLinejoin: "round" })
4692
+ )
4693
+ )
4694
+ ),
4126
4695
  formUsable ? note(t("settings.hint")) : null
4127
4696
  );
4128
4697
  }
@@ -4134,8 +4703,15 @@ function FileTypeIcon(props) {
4134
4703
  const CSS = `
4135
4704
  .dsov-view{box-sizing:border-box;width:100%;max-width:880px;margin:0 auto;padding:20px 28px 64px;display:flex;flex-direction:column;gap:14px}
4136
4705
  .dsov-panel{box-sizing:border-box;height:100%;min-height:0;overflow-y:auto;padding:14px 16px 28px;display:flex;flex-direction:column;gap:12px}
4137
- .dsov-toolbar{display:flex;align-items:center;justify-content:flex-end;gap:8px}
4138
- .dsov-iconBtn{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;border-radius:8px;border:.5px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer}
4706
+ .dsov-panel,.dsov-viking,.dsov-secBody,.dsov-typeMenu,.dsov-md-pre,.dsov-turnbarScroll,.dsov-toolbarScroll{scrollbar-width:thin;scrollbar-color:color-mix(in srgb,var(--dsw-alias-label-secondary) 35%,transparent) transparent}
4707
+ .dsov-panel::-webkit-scrollbar,.dsov-viking::-webkit-scrollbar,.dsov-secBody::-webkit-scrollbar,.dsov-typeMenu::-webkit-scrollbar,.dsov-md-pre::-webkit-scrollbar,.dsov-turnbarScroll::-webkit-scrollbar,.dsov-toolbarScroll::-webkit-scrollbar{width:6px;height:5px}
4708
+ .dsov-panel::-webkit-scrollbar-thumb,.dsov-viking::-webkit-scrollbar-thumb,.dsov-secBody::-webkit-scrollbar-thumb,.dsov-typeMenu::-webkit-scrollbar-thumb,.dsov-md-pre::-webkit-scrollbar-thumb,.dsov-turnbarScroll::-webkit-scrollbar-thumb,.dsov-toolbarScroll::-webkit-scrollbar-thumb{background:color-mix(in srgb,var(--dsw-alias-label-secondary) 35%,transparent);border-radius:3px}
4709
+ .dsov-panel::-webkit-scrollbar-track,.dsov-viking::-webkit-scrollbar-track,.dsov-secBody::-webkit-scrollbar-track,.dsov-typeMenu::-webkit-scrollbar-track,.dsov-md-pre::-webkit-scrollbar-track,.dsov-turnbarScroll::-webkit-scrollbar-track,.dsov-toolbarScroll::-webkit-scrollbar-track{background:transparent}
4710
+ .dsov-toolbar{display:flex;align-items:center;gap:8px}
4711
+ .dsov-toolbarScroll{flex:1 1 auto;min-width:0;display:flex;align-items:center;overflow-x:auto;overflow-y:hidden;padding-bottom:5px}
4712
+ .dsov-toolbarFixed{flex:none;display:flex;align-items:center;gap:8px;margin-left:auto;padding-bottom:5px}
4713
+ .dsov-iconBtn{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;border-radius:8px;border:.5px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer;white-space:nowrap;flex:none}
4714
+ .dsov-turnSelect{flex:none}
4139
4715
  .dsov-iconBtn:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 7%,transparent);color:var(--dsw-alias-label-primary)}
4140
4716
  .dsov-section{background:var(--dsw-alias-bg-layer-1);border:.5px solid var(--dsw-alias-border-l1);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:8px}
4141
4717
  .dsov-sectionHead{display:flex;align-items:center;gap:6px}
@@ -4143,6 +4719,9 @@ function FileTypeIcon(props) {
4143
4719
  .dsov-sectionToggle:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 6%,transparent)}
4144
4720
  .dsov-chevron{flex:none;display:inline-flex;color:var(--dsw-alias-label-secondary);transition:transform .15s ease}
4145
4721
  .dsov-chevron[data-open=true]{transform:rotate(90deg)}
4722
+ .dsov-moreSteps{appearance:none;background:transparent;border:none;cursor:pointer;display:inline-flex;align-items:center;gap:4px;flex:none;margin-left:auto;padding:1px 6px;border-radius:8px;font-size:10.5px;line-height:16px;color:var(--dsw-alias-label-secondary);white-space:nowrap}
4723
+ .dsov-moreSteps:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
4724
+ .dsov-moreIcon{flex:none;display:inline-flex;opacity:.75}
4146
4725
  .dsov-sectionIcon{flex:none;display:inline-flex;color:var(--dsw-alias-label-secondary)}
4147
4726
  .dsov-sectionTitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);white-space:nowrap}
4148
4727
  .dsov-count{flex:none;font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);border-radius:999px;padding:0 8px;font-variant-numeric:tabular-nums}
@@ -4178,7 +4757,7 @@ function FileTypeIcon(props) {
4178
4757
  .dsov-todoText{flex:1;min-width:0;overflow-wrap:anywhere}
4179
4758
  .dsov-todo[data-status=completed] .dsov-todoText{color:var(--dsw-alias-label-secondary)}
4180
4759
  .dsov-todoStatus{flex:none;font-size:11px;line-height:20px;color:var(--dsw-alias-label-secondary)}
4181
- .dsov-stepsHead{font-size:10.5px;line-height:16px;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));letter-spacing:.02em;padding:1px 2px 4px}
4760
+ .dsov-stepsHead{display:flex;align-items:center;gap:8px;font-size:10.5px;line-height:16px;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));letter-spacing:.02em;padding:1px 2px 4px}
4182
4761
  .dsov-steps{position:relative;display:flex;flex-direction:column;gap:3px}
4183
4762
  .dsov-step{display:flex;align-items:center;gap:6px;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-primary);padding:3px 6px;min-width:0;border-radius:8px;cursor:pointer;transition:background-color .18s ease}
4184
4763
  .dsov-step:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 5%,transparent)}
@@ -4192,6 +4771,18 @@ function FileTypeIcon(props) {
4192
4771
  .dsov-step[data-waiting=true]{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#ff9f0a) 9%,transparent)}
4193
4772
  .dsov-step[data-waiting=true] .dsov-stepName{color:var(--dsw-alias-state-warn-primary,#ff9f0a)}
4194
4773
  .dsov-step[data-waiting=true] .dsov-todoIcon[data-status=waiting]{color:var(--dsw-alias-state-warn-primary,#ff9f0a)}
4774
+ .dsov-agentIcon{color:var(--dsw-alias-brand-primary)}
4775
+ .dsov-memberName{flex:none;font-weight:500}
4776
+ .dsov-memberDesc{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;color:var(--dsw-alias-label-secondary)}
4777
+ .dsov-todo .dsov-chevron{margin-top:4px}
4778
+ .dsov-todoIcon[data-status=failed]{color:var(--dsw-alias-state-error-primary)}
4779
+ .dsov-taskSubject{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
4780
+ .dsov-todo[data-depth="1"]{position:relative;margin-left:26px}
4781
+ .dsov-todo[data-depth="2"]{position:relative;margin-left:46px}
4782
+ .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}
4783
+ .dsov-member[data-expandable=false]{cursor:default}
4784
+ .dsov-member[data-expandable=false]:hover{background:transparent}
4785
+ .dsov-stepChevron{flex:none;margin-right:-2px}
4195
4786
  .dsov-row{display:flex;align-items:center;gap:10px;width:100%;text-align:left;padding:6px 8px;margin:0 -8px;border-radius:10px;border:none;background:transparent;cursor:pointer;font:inherit;color:inherit;box-sizing:content-box}
4196
4787
  .dsov-row:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 6%,transparent)}
4197
4788
  .dsov-rowStatic{cursor:default}
@@ -4214,13 +4805,15 @@ function FileTypeIcon(props) {
4214
4805
  .dsov-settingsPage{display:flex;flex-direction:column;gap:14px;padding:6px 2px;max-width:560px}
4215
4806
  .dsov-settingsHeading{font-size:16px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0}
4216
4807
  .dsov-set-prefs{padding:4px 0}
4217
- .dsov-set-group{border-top:.5px solid var(--dsw-alias-border-l2);margin-top:18px;padding-top:16px}
4808
+ .dsov-set-group{border-top:.5px solid var(--dsw-alias-border-l2);margin-top:20px;padding-top:16px}
4218
4809
  .dsov-set-group:first-child{border-top:none;margin-top:0;padding-top:0}
4219
- .dsov-set-group-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:20px;margin-bottom:8px}
4220
- .dsov-set-row{align-items:center;gap:8px;padding:7px 0;display:flex}
4221
- .dsov-set-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:14px;font-weight:400}
4810
+ .dsov-set-link{display:inline-flex;align-items:center;gap:6px;padding:6px 0;font-size:13px;line-height:20px;color:var(--dsw-alias-brand-primary);text-decoration:none;width:fit-content}
4811
+ .dsov-set-link:hover{text-decoration:underline}
4812
+ .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}
4813
+ .dsov-set-row{align-items:center;gap:10px;padding:8px 0;display:flex}
4814
+ .dsov-set-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:400;line-height:20px}
4222
4815
  .dsov-set-control{position:relative;flex:none;display:inline-flex}
4223
- .dsov-set-select{background:var(--dsw-alias-bg-module-platform,var(--dsw-alias-bg-layer-2));height:36px;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:18px;align-items:center;gap:12px;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}
4816
+ .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}
4224
4817
  .dsov-set-select:hover{background:var(--dsw-alias-interactive-bg-hover,color-mix(in srgb,var(--dsw-alias-label-primary) 8%,transparent))}
4225
4818
  .dsov-set-selectLabel{max-width:180px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
4226
4819
  .dsov-set-caret{flex:none;display:inline-flex;color:var(--dsw-alias-label-secondary);transform:rotate(90deg);transition:transform .15s ease}
@@ -4229,11 +4822,16 @@ function FileTypeIcon(props) {
4229
4822
  .dsov-set-menuItem{display:flex;align-items:center;gap:8px;width:100%;padding:7px 10px;border:none;border-radius:7px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;text-align:left;cursor:pointer}
4230
4823
  .dsov-set-menuItem:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 7%,transparent)}
4231
4824
  .dsov-set-menuItem[data-selected=true]{color:var(--dsw-alias-brand-primary);font-weight:500}
4232
- .dsov-set-note{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));margin:2px 0 0;font-size:12px;line-height:1.6}
4233
- .dsov-set-note+.dsov-set-note{margin-top:6px}
4825
+ .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}
4826
+ .dsov-set-note+.dsov-set-note{margin-top:2px}
4234
4827
  .dsov-set-textrow{flex-direction:column;align-items:stretch;gap:6px}
4235
4828
  .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}
4236
4829
  .dsov-set-input:focus{outline:none;border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 55%,transparent)}
4830
+ .dsov-set-arearow{display:block}
4831
+ .dsov-set-arearow .dsov-set-label{display:block;margin-bottom:6px}
4832
+ .dsov-set-textarea{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;font-size:12.5px;line-height:1.55;padding:8px 12px;resize:vertical}
4833
+ .dsov-set-textarea:focus{outline:none;border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 55%,transparent)}
4834
+ .dsov-set-textarea::placeholder{color:var(--dsw-alias-label-quaternary,var(--dsw-alias-label-secondary));opacity:.75}
4237
4835
  html[data-dsov-quicknav="on"] nav:has(button[data-index][aria-label]),html[data-dsov-quicknav="on"] nav[class*="_frame"]{display:block!important}
4238
4836
  html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data-dsov-quicknav="off"] nav[class*="_frame"]{display:none!important}
4239
4837
  .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}
@@ -4267,8 +4865,14 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
4267
4865
  .dsov-filterBtn{display:inline-flex;align-items:center;gap:4px;height:20px;padding:0 8px;border:.5px solid var(--dsw-alias-border-l2);border-radius:10px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:10.5px;line-height:1;cursor:pointer;white-space:nowrap}
4268
4866
  .dsov-filterBtn:hover{color:var(--dsw-alias-label-primary);background:color-mix(in srgb,var(--dsw-alias-label-primary) 6%,transparent)}
4269
4867
  .dsov-filterBtn[data-active=true]{color:var(--dsw-alias-brand-primary);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 45%,transparent);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent)}
4270
- .dsov-turnbar{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:0 0 2px}
4271
- .dsov-stats{display:flex;align-items:center;gap:5px;margin-right:auto;min-width:0;overflow:hidden}
4868
+ .dsov-turnbar{display:flex;justify-content:flex-start;align-items:center;gap:8px;padding:0}
4869
+ .dsov-turnbarScroll{flex:1 1 auto;min-width:0;display:flex;align-items:center;overflow-x:auto;overflow-y:hidden;padding-bottom:5px}
4870
+ .dsov-turnbarFixed{flex:none;display:flex;align-items:center;margin-left:auto;padding-bottom:5px}
4871
+ .dsov-panel .dsov-turnbarWrap{position:relative;z-index:5;margin:-14px -16px 0;padding:8px 16px 3px;background:transparent;transition:background-color .18s ease,box-shadow .18s ease}
4872
+ .dsov-panel .dsov-turnbarWrap[data-stuck=true]{background:var(--dsw-specific-sidebar-fill,var(--dsw-alias-bg-base));box-shadow:0 6px 10px -8px color-mix(in srgb,var(--dsw-alias-label-primary) 25%,transparent)}
4873
+ .dsov-view .dsov-toolbarWrap{position:relative;z-index:4;margin:-20px -28px 0;padding:10px 28px 5px;background:transparent;transition:background-color .18s ease,box-shadow .18s ease}
4874
+ .dsov-view .dsov-toolbarWrap[data-stuck=true]{background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-1));backdrop-filter:var(--dsw-menu-backdrop-filter,none);box-shadow:0 6px 10px -8px color-mix(in srgb,var(--dsw-alias-label-primary) 22%,transparent)}
4875
+ .dsov-stats{display:flex;align-items:center;gap:5px;margin-right:auto;flex:none}
4272
4876
  .dsov-stat{flex:none;font-size:10.5px;line-height:18px;padding:0 8px;border-radius:999px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);white-space:nowrap;font-variant-numeric:tabular-nums}
4273
4877
  .dsov-stat[data-tone=err]{color:var(--dsw-alias-state-error-primary,#ff453a);background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#ff453a) 10%,transparent)}
4274
4878
  .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}
@@ -4287,6 +4891,12 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
4287
4891
  .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}
4288
4892
  .dsov-segBtn:hover{color:var(--dsw-alias-label-primary)}
4289
4893
  .dsov-segBtn[data-active=true]{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-weight:500;box-shadow:0 0 0 .5px var(--dsw-alias-border-l1)}
4894
+ .dsov-refControls .dsov-seg{display:inline-flex;align-items:stretch;gap:0;padding:0;border:.5px solid var(--dsw-alias-border-l2);border-radius:10px;background:transparent;overflow:hidden}
4895
+ .dsov-refControls .dsov-segBtn{height:19px;align-self:center;line-height:1;padding:0 9px;border:none;border-radius:0;background:transparent;color:var(--dsw-alias-label-secondary);font-size:10.5px}
4896
+ .dsov-refControls .dsov-segBtn+.dsov-segBtn{border-left:.5px solid var(--dsw-alias-border-l2)}
4897
+ .dsov-refControls .dsov-segBtn:hover{color:var(--dsw-alias-label-primary);background:color-mix(in srgb,var(--dsw-alias-label-primary) 6%,transparent)}
4898
+ .dsov-refControls .dsov-segBtn[data-active=true]{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent);font-weight:500;box-shadow:none}
4899
+ .dsov-refControls .dsov-segBtn[data-active=true]:hover{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}
4290
4900
  .dsov-vikingList{display:flex;flex-direction:column}
4291
4901
  .dsov-vikingSize{font-size:11px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}
4292
4902
  .dsov-vikingSource{white-space:pre-wrap;overflow-wrap:anywhere;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l1);border-radius:8px;padding:10px 12px;margin:0}
@@ -4298,6 +4908,9 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
4298
4908
  .dsov-vikingTags{display:flex;flex-wrap:wrap;gap:5px}
4299
4909
  .dsov-tag{font-size:11px;line-height:19px;padding:0 8px;border-radius:999px;color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 10%,transparent);border:.5px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 30%,transparent)}
4300
4910
  .dsov-vikingFieldValue{font-size:12.5px;line-height:1.6;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}
4911
+ .dsov-vikingSource{display:flex;flex-direction:column;gap:2px}
4912
+ .dsov-vikingSourceLink{font-size:12.5px;line-height:1.6;color:var(--dsw-alias-brand-primary);text-decoration:none;overflow-wrap:anywhere}
4913
+ .dsov-vikingSourceLink:hover{text-decoration:underline}
4301
4914
  .dsov-vikingError{display:flex;flex-direction:column;align-items:flex-start;gap:8px;font-size:13px;color:var(--dsw-alias-state-error-primary)}
4302
4915
  .dsov-vikingErrorDetail{font-size:12px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}
4303
4916
  .dsov-md{font-size:13.5px;line-height:1.65;color:var(--dsw-alias-label-primary);display:flex;flex-direction:column;gap:2px}
@@ -4698,6 +5311,7 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
4698
5311
  refDisplayType,
4699
5312
  stepDetail,
4700
5313
  stepFull,
5314
+ teamStepKey,
4701
5315
  formatDuration,
4702
5316
  rerunPrompt,
4703
5317
  insertIntoComposer,