dsh-conversation-overview 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/client.js +961 -76
- package/cordis.patch.yml +1 -0
- package/index.js +1 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -19,9 +19,154 @@ window.__ModuleLoader__.load({
|
|
|
19
19
|
const ROW_ID = "conversation-overview";
|
|
20
20
|
const ROW_KEY = "dsh-conversation-overview#conversation-overview";
|
|
21
21
|
const FEEDBACK_URL = "https://github.com/LingF/dsh-conversation-overview/issues";
|
|
22
|
+
const RELEASES_URL = "https://github.com/LingF/dsh-conversation-overview/releases";
|
|
23
|
+
const RELEASE_API_URL = "https://api.github.com/repos/LingF/dsh-conversation-overview/releases/latest";
|
|
24
|
+
const RELEASE_TAGS_API_URL = "https://api.github.com/repos/LingF/dsh-conversation-overview/tags?per_page=100";
|
|
25
|
+
const TAGS_URL = "https://github.com/LingF/dsh-conversation-overview/tags";
|
|
26
|
+
/* keep in sync with package.json */
|
|
27
|
+
const PLUGIN_VERSION = "1.1.0";
|
|
28
|
+
const LATEST_CACHE_KEY = "dsh.conversationOverview.latestRelease";
|
|
29
|
+
const LATEST_CACHE_TTL = 6 * 60 * 60 * 1000;
|
|
30
|
+
const latestStore = { tag: null, via: "release", loaded: false, inflight: null };
|
|
31
|
+
|
|
32
|
+
function compareVersions(left, right) {
|
|
33
|
+
const parse = (value) =>
|
|
34
|
+
String(value)
|
|
35
|
+
.replace(/^v/i, "")
|
|
36
|
+
.split("-")[0]
|
|
37
|
+
.split(".")
|
|
38
|
+
.map((part) => Number.parseInt(part, 10) || 0);
|
|
39
|
+
const a = parse(left);
|
|
40
|
+
const b = parse(right);
|
|
41
|
+
for (let index = 0; index < 3; index += 1) {
|
|
42
|
+
const delta = (a[index] || 0) - (b[index] || 0);
|
|
43
|
+
if (delta !== 0) return delta;
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fetchLatestRelease() {
|
|
49
|
+
if (latestStore.loaded) return Promise.resolve(latestStore.tag);
|
|
50
|
+
if (latestStore.inflight !== null) return latestStore.inflight;
|
|
51
|
+
try {
|
|
52
|
+
const raw = localStorage.getItem(LATEST_CACHE_KEY);
|
|
53
|
+
if (typeof raw === "string") {
|
|
54
|
+
const parsed = JSON.parse(raw);
|
|
55
|
+
if (isRecord(parsed) && typeof parsed.tag === "string" && typeof parsed.time === "number" && Date.now() - parsed.time < LATEST_CACHE_TTL) {
|
|
56
|
+
latestStore.tag = parsed.tag;
|
|
57
|
+
latestStore.via = parsed.via === "tags" ? "tags" : "release";
|
|
58
|
+
latestStore.loaded = true;
|
|
59
|
+
return Promise.resolve(parsed.tag);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
/* cache is best-effort */
|
|
64
|
+
}
|
|
65
|
+
if (typeof fetch !== "function") return Promise.resolve(null);
|
|
66
|
+
latestStore.inflight = fetch(RELEASE_API_URL, { headers: { Accept: "application/vnd.github+json" } })
|
|
67
|
+
.then((response) => (response.ok ? response.json() : null))
|
|
68
|
+
.then((payload) => {
|
|
69
|
+
/* repos without formal releases fall back to the highest version tag */
|
|
70
|
+
if (isRecord(payload) && typeof payload.tag_name === "string") return { tag: payload.tag_name, via: "release" };
|
|
71
|
+
return fetch(RELEASE_TAGS_API_URL, { headers: { Accept: "application/vnd.github+json" } })
|
|
72
|
+
.then((response) => (response.ok ? response.json() : []))
|
|
73
|
+
.then((list) => {
|
|
74
|
+
if (!Array.isArray(list)) return { tag: null, via: "release" };
|
|
75
|
+
let best = null;
|
|
76
|
+
for (const entry of list) {
|
|
77
|
+
const name = isRecord(entry) && typeof entry.name === "string" ? entry.name : null;
|
|
78
|
+
if (name !== null && (best === null || compareVersions(name, best) > 0)) best = name;
|
|
79
|
+
}
|
|
80
|
+
return { tag: best, via: "tags" };
|
|
81
|
+
});
|
|
82
|
+
})
|
|
83
|
+
.then((found) => {
|
|
84
|
+
latestStore.inflight = null;
|
|
85
|
+
latestStore.loaded = true;
|
|
86
|
+
latestStore.tag = found.tag;
|
|
87
|
+
latestStore.via = found.via;
|
|
88
|
+
if (found.tag !== null) {
|
|
89
|
+
try {
|
|
90
|
+
localStorage.setItem(LATEST_CACHE_KEY, JSON.stringify({ tag: found.tag, via: found.via, time: Date.now() }));
|
|
91
|
+
} catch {
|
|
92
|
+
/* storage is best-effort */
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return found.tag;
|
|
96
|
+
})
|
|
97
|
+
.catch(() => {
|
|
98
|
+
latestStore.inflight = null;
|
|
99
|
+
return null;
|
|
100
|
+
});
|
|
101
|
+
return latestStore.inflight;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function useLatestVersion() {
|
|
105
|
+
const [tag, setTag] = React.useState(latestStore.tag);
|
|
106
|
+
React.useEffect(() => {
|
|
107
|
+
let cancelled = false;
|
|
108
|
+
Promise.resolve(fetchLatestRelease()).then((value) => {
|
|
109
|
+
if (!cancelled && typeof value === "string") setTag(value);
|
|
110
|
+
});
|
|
111
|
+
return () => {
|
|
112
|
+
cancelled = true;
|
|
113
|
+
};
|
|
114
|
+
}, []);
|
|
115
|
+
return typeof tag === "string" && compareVersions(tag, PLUGIN_VERSION) > 0 ? tag : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* The plugin-manager renders every detail.badge entry on every page; only answer for ours. */
|
|
119
|
+
function isOverviewSubject(subject) {
|
|
120
|
+
if (!isRecord(subject)) return false;
|
|
121
|
+
if (subject.kind === "item") return typeof subject.id === "string" && subject.id.indexOf("conversation-overview") !== -1;
|
|
122
|
+
if (subject.kind === "row") {
|
|
123
|
+
const row = isRecord(subject.row) ? subject.row : null;
|
|
124
|
+
const pkg = isRecord(subject.pkg) ? subject.pkg : null;
|
|
125
|
+
return (row !== null && String(row.rowId ?? "").indexOf("conversation-overview") !== -1) || (pkg !== null && String(pkg.name ?? "").indexOf("conversation-overview") !== -1);
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function NewVersionBadge({ subject }) {
|
|
131
|
+
const tag = useLatestVersion();
|
|
132
|
+
if (tag === null || !isOverviewSubject(subject)) return null;
|
|
133
|
+
return h(
|
|
134
|
+
"a",
|
|
135
|
+
{
|
|
136
|
+
className: "dsov-newVer",
|
|
137
|
+
href: latestStore.via === "tags" ? TAGS_URL : `${RELEASES_URL}/tag/${encodeURIComponent(tag)}`,
|
|
138
|
+
target: "_blank",
|
|
139
|
+
rel: "noreferrer",
|
|
140
|
+
title: translate("settings.latestTitle"),
|
|
141
|
+
},
|
|
142
|
+
`${translate("settings.latestBadge")} ${tag}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function VersionRow({ t }) {
|
|
147
|
+
const tag = useLatestVersion();
|
|
148
|
+
return h(
|
|
149
|
+
"div",
|
|
150
|
+
{ className: "dsov-set-ver" },
|
|
151
|
+
tag !== null
|
|
152
|
+
? h(
|
|
153
|
+
"a",
|
|
154
|
+
{
|
|
155
|
+
className: "dsov-newVer",
|
|
156
|
+
href: latestStore.via === "tags" ? TAGS_URL : `${RELEASES_URL}/tag/${encodeURIComponent(tag)}`,
|
|
157
|
+
target: "_blank",
|
|
158
|
+
rel: "noreferrer",
|
|
159
|
+
title: t("settings.latestTitle"),
|
|
160
|
+
},
|
|
161
|
+
`${t("settings.latestBadge")} ${tag}`
|
|
162
|
+
)
|
|
163
|
+
: null,
|
|
164
|
+
h("span", null, `${t("settings.currentVersion")} v${PLUGIN_VERSION}`)
|
|
165
|
+
);
|
|
166
|
+
}
|
|
22
167
|
const PLACEMENTS = ["conversation", "sidebar", "both"];
|
|
23
168
|
const TIME_MODES = ["clock", "elapsed"];
|
|
24
|
-
const CONFIG_FIELDS = ["showProgress", "showArtifacts", "showReferences", "showStats", "timeMode", "placement", "sectionFullHeight", "todoPrompt", "todoPromptTemplate", "quickNav", "quickNavLink", "vikingPreview", "vikingUrl", "vikingAccount", "vikingUser"];
|
|
169
|
+
const CONFIG_FIELDS = ["showProgress", "showArtifacts", "showReferences", "showStats", "timeMode", "stepAnchors", "placement", "sectionFullHeight", "todoPrompt", "todoPromptTemplate", "quickNav", "quickNavLink", "vikingPreview", "vikingUrl", "vikingAccount", "vikingUser"];
|
|
25
170
|
const VIKING_TAB_ID = "dsh-conversation-overview/viking";
|
|
26
171
|
const VIKING_TAB_KIND = "viking-memory";
|
|
27
172
|
const VIKING_ADDRESS_PREFIX = "dsh-resource://viking-memory/";
|
|
@@ -40,7 +185,7 @@ window.__ModuleLoader__.load({
|
|
|
40
185
|
const zh = {
|
|
41
186
|
"view.tab": "概览",
|
|
42
187
|
"panel.title": "概览",
|
|
43
|
-
"guide.title": "
|
|
188
|
+
"guide.title": "会话概览",
|
|
44
189
|
"guide.description": "进展、产物与引用",
|
|
45
190
|
"section.progress": "进展",
|
|
46
191
|
"section.artifacts": "产物",
|
|
@@ -48,6 +193,8 @@ window.__ModuleLoader__.load({
|
|
|
48
193
|
"progress.empty": "生成任务/待办后将在此展示进展",
|
|
49
194
|
"progress.emptyTurn": "本轮未创建任务计划",
|
|
50
195
|
"progress.recentSteps": "本轮关键步骤(最近 6 条)",
|
|
196
|
+
"step.compact": "上下文压缩",
|
|
197
|
+
"step.approval": "用户审批",
|
|
51
198
|
"progress.allSteps": "本轮关键步骤(全部)",
|
|
52
199
|
"progress.showEarlierSteps": "显示更早的步骤",
|
|
53
200
|
"progress.moreStepsCount": "还有 {n} 条",
|
|
@@ -69,15 +216,21 @@ window.__ModuleLoader__.load({
|
|
|
69
216
|
"artifacts.viewAll": "查看所有改动",
|
|
70
217
|
"settings.summary": "控制概览的板块与显示位置",
|
|
71
218
|
"settings.nav": "概览",
|
|
72
|
-
"settings.pageTitle": "
|
|
219
|
+
"settings.pageTitle": "会话概览(dsh-conversation-overview)",
|
|
73
220
|
"settings.sections": "板块显示",
|
|
74
221
|
"settings.showProgress": "进展(任务与待办)",
|
|
75
222
|
"settings.showArtifacts": "产物(本次对话产出)",
|
|
76
223
|
"settings.showReferences": "引用(上下文、记忆等)",
|
|
77
224
|
"settings.showStats": "统计(用时、执行次数等)",
|
|
78
225
|
"settings.timeMode": "时间显示",
|
|
226
|
+
"settings.stepAnchors": "里程碑步骤置顶",
|
|
227
|
+
"settings.stepAnchorsNote": "开启后,上下文压缩、用户审批与失败的工具调用固定在关键步骤顶部,不被最近窗口挤出;默认关闭,随窗口滚动。",
|
|
79
228
|
"settings.timeMode.clock": "时间点",
|
|
80
|
-
"settings.timeMode.elapsed": "
|
|
229
|
+
"settings.timeMode.elapsed": "本轮计时",
|
|
230
|
+
"settings.timeModeNote": "「本轮计时」以本轮任务开始为 0s 起点,显示相对用时;悬停时间可查看对应的具体时间点。",
|
|
231
|
+
"settings.currentVersion": "当前版本",
|
|
232
|
+
"settings.latestBadge": "新版本",
|
|
233
|
+
"settings.latestTitle": "在 GitHub 查看最新版本",
|
|
81
234
|
"settings.feedbackGroup": "反馈",
|
|
82
235
|
"settings.feedback": "问题反馈(GitHub Issues)",
|
|
83
236
|
"settings.fullHeightRow": "分区完整展示(不限高)",
|
|
@@ -146,6 +299,12 @@ window.__ModuleLoader__.load({
|
|
|
146
299
|
"reference.hideCommon": "过滤常见",
|
|
147
300
|
"stats.duration": "用时 {v}",
|
|
148
301
|
"stats.duration.title": "本轮用时(首末事件间隔)",
|
|
302
|
+
"stats.duration.timeline": "点击打开时间轴,按时间点回看进展 / 产物 / 引用",
|
|
303
|
+
"timeline.live": "最新",
|
|
304
|
+
"timeline.reset": "回到最新",
|
|
305
|
+
"timeline.prevTurn": "上一轮",
|
|
306
|
+
"timeline.prevTurnTitle": "切换到第 {n} 轮,时间轴保持打开",
|
|
307
|
+
"timeline.close": "关闭时间轴",
|
|
149
308
|
"stats.tools": "工具 {n}",
|
|
150
309
|
"stats.tools.title": "工具调用次数",
|
|
151
310
|
"stats.commands": "命令 {n}",
|
|
@@ -167,6 +326,15 @@ window.__ModuleLoader__.load({
|
|
|
167
326
|
"goal.blocked": "受阻",
|
|
168
327
|
"goal.complete": "已完成",
|
|
169
328
|
"artifact.presented": "交付",
|
|
329
|
+
"artifact.memory": "记忆",
|
|
330
|
+
"artifact.menu.reveal": "在上层文件夹中显示",
|
|
331
|
+
"artifact.menu.openWith": "打开方式",
|
|
332
|
+
"artifact.menu.loading": "正在读取可用应用…",
|
|
333
|
+
"artifact.menu.unavailable": "当前部署不支持系统打开",
|
|
334
|
+
"artifact.menu.noApps": "未找到关联应用",
|
|
335
|
+
"artifact.menu.default": "默认",
|
|
336
|
+
"artifact.menu.revealError": "无法在上层文件夹中显示,请重试",
|
|
337
|
+
"artifact.menu.openError": "无法用该应用打开,请重试",
|
|
170
338
|
"artifact.produced": "生成",
|
|
171
339
|
"artifact.binary": "二进制",
|
|
172
340
|
"reference.kind.context": "上下文",
|
|
@@ -194,7 +362,7 @@ window.__ModuleLoader__.load({
|
|
|
194
362
|
const en = {
|
|
195
363
|
"view.tab": "Overview",
|
|
196
364
|
"panel.title": "Overview",
|
|
197
|
-
"guide.title": "Overview",
|
|
365
|
+
"guide.title": "Conversation Overview",
|
|
198
366
|
"guide.description": "Progress, artifacts & references",
|
|
199
367
|
"section.progress": "Progress",
|
|
200
368
|
"section.artifacts": "Artifacts",
|
|
@@ -202,6 +370,8 @@ window.__ModuleLoader__.load({
|
|
|
202
370
|
"progress.empty": "Progress appears here once tasks or todos are generated",
|
|
203
371
|
"progress.emptyTurn": "No task plan was created in this turn",
|
|
204
372
|
"progress.recentSteps": "Key steps in this turn (latest 6)",
|
|
373
|
+
"step.compact": "Context compaction",
|
|
374
|
+
"step.approval": "User approval",
|
|
205
375
|
"progress.allSteps": "Key steps in this turn (all)",
|
|
206
376
|
"progress.showEarlierSteps": "Show earlier steps",
|
|
207
377
|
"progress.moreStepsCount": "{n} more steps",
|
|
@@ -223,15 +393,21 @@ window.__ModuleLoader__.load({
|
|
|
223
393
|
"artifacts.viewAll": "Review all changes",
|
|
224
394
|
"settings.summary": "Control the overview sections and where they appear",
|
|
225
395
|
"settings.nav": "Overview",
|
|
226
|
-
"settings.pageTitle": "Conversation Overview",
|
|
396
|
+
"settings.pageTitle": "Conversation Overview (dsh-conversation-overview)",
|
|
227
397
|
"settings.sections": "Sections",
|
|
228
398
|
"settings.showProgress": "Progress (tasks and todos)",
|
|
229
399
|
"settings.showArtifacts": "Artifacts (files produced)",
|
|
230
400
|
"settings.showReferences": "References (context, memories)",
|
|
231
401
|
"settings.showStats": "Stats (duration, tool calls, …)",
|
|
232
402
|
"settings.timeMode": "Time display",
|
|
403
|
+
"settings.stepAnchors": "Pin milestone steps",
|
|
404
|
+
"settings.stepAnchorsNote": "When on, context compaction, user approvals, and failed tool calls stay pinned above the latest-steps window instead of scrolling out. Off by default.",
|
|
233
405
|
"settings.timeMode.clock": "Clock time",
|
|
234
|
-
"settings.timeMode.elapsed": "Turn elapsed
|
|
406
|
+
"settings.timeMode.elapsed": "Turn elapsed",
|
|
407
|
+
"settings.timeModeNote": "'Turn elapsed' counts from 0s at the start of the turn; hover a time to see its clock value.",
|
|
408
|
+
"settings.currentVersion": "Current version",
|
|
409
|
+
"settings.latestBadge": "New version",
|
|
410
|
+
"settings.latestTitle": "View the latest release on GitHub",
|
|
235
411
|
"settings.feedbackGroup": "Feedback",
|
|
236
412
|
"settings.feedback": "Report an issue (GitHub Issues)",
|
|
237
413
|
"settings.fullHeightRow": "Show sections in full (no max height)",
|
|
@@ -300,6 +476,12 @@ window.__ModuleLoader__.load({
|
|
|
300
476
|
"reference.hideCommon": "Filter common",
|
|
301
477
|
"stats.duration": "{v} elapsed",
|
|
302
478
|
"stats.duration.title": "Turn duration (first to last event)",
|
|
479
|
+
"stats.duration.timeline": "Open the timeline to review progress, artifacts, and references at any moment",
|
|
480
|
+
"timeline.live": "Live",
|
|
481
|
+
"timeline.reset": "Back to live",
|
|
482
|
+
"timeline.prevTurn": "Previous turn",
|
|
483
|
+
"timeline.prevTurnTitle": "Switch to turn {n} and keep the timeline open",
|
|
484
|
+
"timeline.close": "Close timeline",
|
|
303
485
|
"stats.tools": "{n} tools",
|
|
304
486
|
"stats.tools.title": "Tool calls",
|
|
305
487
|
"stats.commands": "{n} cmds",
|
|
@@ -321,6 +503,15 @@ window.__ModuleLoader__.load({
|
|
|
321
503
|
"goal.blocked": "Blocked",
|
|
322
504
|
"goal.complete": "Completed",
|
|
323
505
|
"artifact.presented": "Delivered",
|
|
506
|
+
"artifact.memory": "Memory",
|
|
507
|
+
"artifact.menu.reveal": "Show in Enclosing Folder",
|
|
508
|
+
"artifact.menu.openWith": "Open With",
|
|
509
|
+
"artifact.menu.loading": "Loading applications…",
|
|
510
|
+
"artifact.menu.unavailable": "System open is unavailable in this deployment",
|
|
511
|
+
"artifact.menu.noApps": "No associated applications found",
|
|
512
|
+
"artifact.menu.default": "Default",
|
|
513
|
+
"artifact.menu.revealError": "Could not reveal the file; try again",
|
|
514
|
+
"artifact.menu.openError": "Could not open with this application; try again",
|
|
324
515
|
"artifact.produced": "Produced",
|
|
325
516
|
"artifact.binary": "binary",
|
|
326
517
|
"reference.kind.context": "Context",
|
|
@@ -370,6 +561,7 @@ window.__ModuleLoader__.load({
|
|
|
370
561
|
showReferences: flag(source.showReferences),
|
|
371
562
|
showStats: flag(source.showStats),
|
|
372
563
|
timeMode: TIME_MODES.indexOf(source.timeMode) !== -1 ? source.timeMode : "clock",
|
|
564
|
+
stepAnchors: source.stepAnchors === true,
|
|
373
565
|
placement: PLACEMENTS.indexOf(source.placement) !== -1 ? source.placement : "both",
|
|
374
566
|
sectionFullHeight: source.sectionFullHeight === true,
|
|
375
567
|
todoPrompt: typeof source.todoPrompt === "boolean" ? source.todoPrompt : true,
|
|
@@ -644,6 +836,125 @@ window.__ModuleLoader__.load({
|
|
|
644
836
|
}
|
|
645
837
|
}
|
|
646
838
|
|
|
839
|
+
/* ------------------------------------------------------------------ */
|
|
840
|
+
/* Host desktop file actions (reveal / open with) via the session remote */
|
|
841
|
+
/* ------------------------------------------------------------------ */
|
|
842
|
+
|
|
843
|
+
const desktopOpenState = { value: null, inflight: null };
|
|
844
|
+
|
|
845
|
+
function hostSessionRemote() {
|
|
846
|
+
const ctx = runtime && runtime.ctx;
|
|
847
|
+
if (!ctx) return null;
|
|
848
|
+
try {
|
|
849
|
+
/* the cordis context proxy THROWS for services this entry has not
|
|
850
|
+
injected; degrade to "no desktop actions" instead of crashing render */
|
|
851
|
+
const remote = ctx.remote;
|
|
852
|
+
return isRecord(remote) && isRecord(remote.session) ? remote.session : null;
|
|
853
|
+
} catch {
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function desktopCanOpen() {
|
|
859
|
+
const remote = hostSessionRemote();
|
|
860
|
+
if (remote === null || typeof remote.canOpenWorkspacePath !== "function") return Promise.resolve(false);
|
|
861
|
+
if (desktopOpenState.value !== null) return Promise.resolve(desktopOpenState.value);
|
|
862
|
+
if (desktopOpenState.inflight !== null) return desktopOpenState.inflight;
|
|
863
|
+
desktopOpenState.inflight = Promise.resolve()
|
|
864
|
+
.then(() => remote.canOpenWorkspacePath())
|
|
865
|
+
.then((result) => {
|
|
866
|
+
desktopOpenState.value = isRecord(result) && result.ok === true && result.value === true;
|
|
867
|
+
desktopOpenState.inflight = null;
|
|
868
|
+
return desktopOpenState.value;
|
|
869
|
+
})
|
|
870
|
+
.catch(() => {
|
|
871
|
+
desktopOpenState.inflight = null;
|
|
872
|
+
return false;
|
|
873
|
+
});
|
|
874
|
+
return desktopOpenState.inflight;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function desktopOpenPath(path, action, application) {
|
|
878
|
+
const remote = hostSessionRemote();
|
|
879
|
+
if (remote === null || typeof remote.openWorkspacePath !== "function" || typeof path !== "string" || path === "") return Promise.resolve(false);
|
|
880
|
+
const request = action === "reveal" ? { path, action } : application === undefined ? { path } : { path, application };
|
|
881
|
+
return Promise.resolve()
|
|
882
|
+
.then(() => remote.openWorkspacePath(request))
|
|
883
|
+
.then((result) => isRecord(result) && result.ok === true)
|
|
884
|
+
.catch(() => false);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function desktopApplications(path, signal) {
|
|
888
|
+
const remote = hostSessionRemote();
|
|
889
|
+
if (remote === null || typeof remote.workspacePathApplications !== "function") return Promise.resolve(null);
|
|
890
|
+
return Promise.resolve()
|
|
891
|
+
.then(() => remote.workspacePathApplications({ path }, signal))
|
|
892
|
+
.then((result) => (isRecord(result) && result.ok === true && Array.isArray(result.value) ? result.value : null))
|
|
893
|
+
.catch(() => null);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/* Right-click menu on an artifact row: reveal in the enclosing folder, or pick
|
|
897
|
+
one of the OS-registered applications to open the file with. */
|
|
898
|
+
function ArtifactFileMenu({ menu, t, onClose }) {
|
|
899
|
+
const [phase, setPhase] = React.useState("loading"); /* loading | ready | unavailable */
|
|
900
|
+
const [apps, setApps] = React.useState([]);
|
|
901
|
+
const [failed, setFailed] = React.useState(null);
|
|
902
|
+
const pathRef = React.useRef(menu.path);
|
|
903
|
+
React.useEffect(() => {
|
|
904
|
+
let cancelled = false;
|
|
905
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
906
|
+
desktopCanOpen().then((can) => {
|
|
907
|
+
if (cancelled) return undefined;
|
|
908
|
+
if (!can) {
|
|
909
|
+
setPhase("unavailable");
|
|
910
|
+
return undefined;
|
|
911
|
+
}
|
|
912
|
+
return desktopApplications(pathRef.current, controller !== null ? controller.signal : undefined).then((list) => {
|
|
913
|
+
if (cancelled || (controller !== null && controller.signal.aborted)) return;
|
|
914
|
+
setApps(list === null ? [] : list.filter((app) => isRecord(app) && typeof app.id === "string" && app.id !== "" && typeof app.name === "string"));
|
|
915
|
+
setPhase("ready");
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
return () => {
|
|
919
|
+
cancelled = true;
|
|
920
|
+
if (controller !== null) controller.abort();
|
|
921
|
+
};
|
|
922
|
+
}, []);
|
|
923
|
+
const runAction = (action, application) => {
|
|
924
|
+
desktopOpenPath(menu.path, action, application).then((ok) => {
|
|
925
|
+
if (ok) onClose();
|
|
926
|
+
else setFailed(action === "reveal" ? t("artifact.menu.revealError") : t("artifact.menu.openError"));
|
|
927
|
+
});
|
|
928
|
+
};
|
|
929
|
+
const clamp = (value, max) => Math.max(8, Math.min(value, max));
|
|
930
|
+
const style = {
|
|
931
|
+
left: `${clamp(menu.x, (typeof window !== "undefined" ? window.innerWidth : 1200) - 250)}px`,
|
|
932
|
+
top: `${clamp(menu.y, (typeof window !== "undefined" ? window.innerHeight : 800) - 320)}px`,
|
|
933
|
+
};
|
|
934
|
+
return h(
|
|
935
|
+
"div",
|
|
936
|
+
{ className: "dsov-fileMenu", style, onMouseDown: (event) => event.stopPropagation(), onContextMenu: (event) => event.preventDefault() },
|
|
937
|
+
h("button", { type: "button", className: "dsov-fileMenuItem", onClick: () => runAction("reveal") }, t("artifact.menu.reveal")),
|
|
938
|
+
h("div", { className: "dsov-fileMenuSep" }),
|
|
939
|
+
h("div", { className: "dsov-fileMenuHead" }, t("artifact.menu.openWith")),
|
|
940
|
+
phase === "loading" ? h("div", { className: "dsov-fileMenuHint" }, t("artifact.menu.loading")) : null,
|
|
941
|
+
phase === "unavailable" ? h("div", { className: "dsov-fileMenuHint" }, t("artifact.menu.unavailable")) : null,
|
|
942
|
+
phase === "ready" && apps.length === 0 ? h("div", { className: "dsov-fileMenuHint" }, t("artifact.menu.noApps")) : null,
|
|
943
|
+
phase === "ready"
|
|
944
|
+
? apps.map((app) =>
|
|
945
|
+
h(
|
|
946
|
+
"button",
|
|
947
|
+
{ type: "button", className: "dsov-fileMenuItem", key: app.id, onClick: () => runAction("open", app.id) },
|
|
948
|
+
typeof app.icon === "string" && app.icon !== "" ? h("img", { className: "dsov-fileMenuIcon", src: app.icon, alt: "" }) : h("span", { className: "dsov-fileMenuIcon" }),
|
|
949
|
+
h("span", { className: "dsov-fileMenuName" }, app.name),
|
|
950
|
+
app.default === true ? h("span", { className: "dsov-fileMenuTag" }, t("artifact.menu.default")) : null
|
|
951
|
+
)
|
|
952
|
+
)
|
|
953
|
+
: null,
|
|
954
|
+
failed !== null ? h("div", { className: "dsov-fileMenuErr" }, failed) : null
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
|
|
647
958
|
function openOverviewTab() {
|
|
648
959
|
const ctx = runtime && runtime.ctx;
|
|
649
960
|
const sidebar = ctx && ctx.sidebarRight;
|
|
@@ -743,6 +1054,32 @@ window.__ModuleLoader__.load({
|
|
|
743
1054
|
return name === "" ? String(uri) : name;
|
|
744
1055
|
}
|
|
745
1056
|
|
|
1057
|
+
/** SKILL.md refs read as their skill folder name so several skills stay distinguishable. */
|
|
1058
|
+
function vikingSkillName(uri) {
|
|
1059
|
+
const match = /\/skills\/([^/]+)\/SKILL\.md$/i.exec(String(uri));
|
|
1060
|
+
return match !== null ? match[1] : null;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function vikingLabel(uri) {
|
|
1064
|
+
const skill = vikingSkillName(uri);
|
|
1065
|
+
return skill !== null ? skill : vikingName(uri);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/** Ref kind: anything under a skills/ root reads as SKILL, the rest stays a memory file. */
|
|
1069
|
+
function vikingRefKind(uri) {
|
|
1070
|
+
const value = String(uri);
|
|
1071
|
+
return /\/skills\/[^/]+(\/SKILL\.md)?\/?$/i.test(value) ? "skill" : "memory-file";
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/** Folder URIs end with "/" or their last segment carries no file extension. */
|
|
1075
|
+
function vikingIsDir(uri) {
|
|
1076
|
+
const value = String(uri);
|
|
1077
|
+
if (value.endsWith("/")) return true;
|
|
1078
|
+
const parts = value.split("/").filter((part) => part !== "");
|
|
1079
|
+
const last = parts.length > 0 ? parts[parts.length - 1] : "";
|
|
1080
|
+
return last !== "" && last.indexOf(".") === -1;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
746
1083
|
function vikingAddress(uri) {
|
|
747
1084
|
return `${VIKING_ADDRESS_PREFIX}${encodeURIComponent(uri)}`;
|
|
748
1085
|
}
|
|
@@ -1004,6 +1341,10 @@ window.__ModuleLoader__.load({
|
|
|
1004
1341
|
}
|
|
1005
1342
|
|
|
1006
1343
|
function openArtifact(binding, artifact, stats) {
|
|
1344
|
+
if (typeof artifact.path === "string" && artifact.path.startsWith("viking://")) {
|
|
1345
|
+
openVikingPreview(artifact.path);
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1007
1348
|
if (!binding) return;
|
|
1008
1349
|
const stat = stats ? stats.get(absPath(sessionCwd(binding.sessionId), artifact.path)) : undefined;
|
|
1009
1350
|
if (stat && stat.coords && openChangesReview(binding.sessionId, stat.coords, stat.index)) return;
|
|
@@ -1093,6 +1434,8 @@ window.__ModuleLoader__.load({
|
|
|
1093
1434
|
}
|
|
1094
1435
|
|
|
1095
1436
|
const MEMORY_REFERENCE_TOOLS = new Set(["search", "find", "read", "glob", "grep", "list", "tree"]);
|
|
1437
|
+
/* viking write tools produce durable memory files: they fold into the artifacts section */
|
|
1438
|
+
const MEMORY_MUTATION_TOOLS = new Set(["write", "edit", "add_resource", "add_skill"]);
|
|
1096
1439
|
|
|
1097
1440
|
function refKindForSource(source, kind) {
|
|
1098
1441
|
if (kind.startsWith("plugin:openviking") || kind.startsWith("plugin:ov")) return "memory";
|
|
@@ -1341,9 +1684,18 @@ window.__ModuleLoader__.load({
|
|
|
1341
1684
|
}
|
|
1342
1685
|
|
|
1343
1686
|
/** True while the session has a pending interaction (approval / question / plan exit). */
|
|
1687
|
+
function readSessionStatusSource() {
|
|
1688
|
+
const ctx = runtime && runtime.ctx;
|
|
1689
|
+
if (!ctx) return undefined;
|
|
1690
|
+
try {
|
|
1691
|
+
return ctx.uiSession && ctx.uiSession.sessionStatus ? ctx.uiSession.sessionStatus : undefined;
|
|
1692
|
+
} catch {
|
|
1693
|
+
return undefined;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1344
1697
|
function useSessionWaiting(sessionId) {
|
|
1345
|
-
const
|
|
1346
|
-
const source = ctx && ctx.uiSession && ctx.uiSession.sessionStatus ? ctx.uiSession.sessionStatus : undefined;
|
|
1698
|
+
const source = readSessionStatusSource();
|
|
1347
1699
|
const subscribe = React.useCallback(
|
|
1348
1700
|
(onChange) => (source !== undefined && typeof source.subscribe === "function" ? source.subscribe(onChange) : () => {}),
|
|
1349
1701
|
[source]
|
|
@@ -1392,6 +1744,7 @@ window.__ModuleLoader__.load({
|
|
|
1392
1744
|
presented: update.presented === true,
|
|
1393
1745
|
produced: update.produced === true,
|
|
1394
1746
|
time: typeof update.time === "number" ? update.time : 0,
|
|
1747
|
+
firstTime: typeof update.time === "number" ? update.time : 0,
|
|
1395
1748
|
seq: typeof update.seq === "number" ? update.seq : 0,
|
|
1396
1749
|
};
|
|
1397
1750
|
folder.artifacts.set(path, artifact);
|
|
@@ -1433,7 +1786,7 @@ window.__ModuleLoader__.load({
|
|
|
1433
1786
|
touch(folder);
|
|
1434
1787
|
return;
|
|
1435
1788
|
}
|
|
1436
|
-
const stored = { ...ref, count: 1 };
|
|
1789
|
+
const stored = { ...ref, count: 1, firstTime: typeof ref.time === "number" ? ref.time : 0 };
|
|
1437
1790
|
noteRefTurn(stored, folder);
|
|
1438
1791
|
folder.refs.set(ref.key, stored);
|
|
1439
1792
|
folder.refOrder.push(ref.key);
|
|
@@ -1448,6 +1801,75 @@ window.__ModuleLoader__.load({
|
|
|
1448
1801
|
|
|
1449
1802
|
/** One-line human target for a tool call: file, command, query, url, name… */
|
|
1450
1803
|
/** Team-record key a tool call creates: teammate name / task subject / subagent label. */
|
|
1804
|
+
/** Match team delegates (members, subagents, tasks) to todo items by normalized text
|
|
1805
|
+
containment, so each delegate renders under the todo it belongs to. */
|
|
1806
|
+
function planTodoAttachments(todos, members, teamTasks, subagents) {
|
|
1807
|
+
const attachments = new Map();
|
|
1808
|
+
if (!Array.isArray(todos) || todos.length === 0) return attachments;
|
|
1809
|
+
const normText = (value) =>
|
|
1810
|
+
typeof value === "string"
|
|
1811
|
+
? value
|
|
1812
|
+
.toLowerCase()
|
|
1813
|
+
.replace(/[-_/]+/g, " ")
|
|
1814
|
+
.replace(/[^a-z0-9\u4e00-\u9fff ]+/g, "")
|
|
1815
|
+
.replace(/\s+/g, " ")
|
|
1816
|
+
.trim()
|
|
1817
|
+
: "";
|
|
1818
|
+
const matchScore = (a, b) => {
|
|
1819
|
+
const x = normText(a);
|
|
1820
|
+
const y = normText(b);
|
|
1821
|
+
if (x === "" || y === "") return 0;
|
|
1822
|
+
if (x === y) return x.length;
|
|
1823
|
+
if (x.length >= 6 && y.includes(x)) return x.length;
|
|
1824
|
+
if (y.length >= 6 && x.includes(y)) return y.length;
|
|
1825
|
+
return 0;
|
|
1826
|
+
};
|
|
1827
|
+
const ownedBy = (id) => teamTasks.filter((task) => isRecord(task) && task.ownerId === id);
|
|
1828
|
+
const bestFor = (texts) => {
|
|
1829
|
+
let best = -1;
|
|
1830
|
+
let bestScore = 5;
|
|
1831
|
+
todos.forEach((todo, index) => {
|
|
1832
|
+
const content = isRecord(todo) ? todo.content : "";
|
|
1833
|
+
let score = 0;
|
|
1834
|
+
for (const text of texts) score = Math.max(score, matchScore(text, content));
|
|
1835
|
+
if (score > bestScore) {
|
|
1836
|
+
bestScore = score;
|
|
1837
|
+
best = index;
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1840
|
+
return best;
|
|
1841
|
+
};
|
|
1842
|
+
const attach = (index, item) => {
|
|
1843
|
+
const list = attachments.get(index);
|
|
1844
|
+
if (list === undefined) attachments.set(index, [item]);
|
|
1845
|
+
else list.push(item);
|
|
1846
|
+
};
|
|
1847
|
+
for (const member of members.filter(isRecord)) {
|
|
1848
|
+
const owned = ownedBy(member.id);
|
|
1849
|
+
const index = bestFor([member.name, member.description, ...owned.map((task) => task.subject)]);
|
|
1850
|
+
if (index >= 0) attach(index, { kind: "member", record: member, owned });
|
|
1851
|
+
}
|
|
1852
|
+
for (const sub of subagents.filter(isRecord)) {
|
|
1853
|
+
const index = bestFor([sub.label]);
|
|
1854
|
+
if (index >= 0) attach(index, { kind: "sub", record: sub, owned: [] });
|
|
1855
|
+
}
|
|
1856
|
+
const memberOwned = new Set();
|
|
1857
|
+
for (const list of attachments.values()) {
|
|
1858
|
+
for (const item of list) {
|
|
1859
|
+
if (item.kind === "member") for (const task of item.owned) memberOwned.add(task.id);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
for (const task of teamTasks.filter(isRecord)) {
|
|
1863
|
+
if (memberOwned.has(task.id)) continue;
|
|
1864
|
+
if (typeof task.ownerId === "string" && members.some((member) => isRecord(member) && member.id === task.ownerId)) continue;
|
|
1865
|
+
const index = bestFor([task.subject]);
|
|
1866
|
+
if (index >= 0) attach(index, { kind: "task", record: task, owned: [] });
|
|
1867
|
+
}
|
|
1868
|
+
const timeOf = (item) => (isRecord(item.record) && typeof item.record.time === "number" ? item.record.time : 0);
|
|
1869
|
+
for (const list of attachments.values()) list.sort((left, right) => timeOf(left) - timeOf(right));
|
|
1870
|
+
return attachments;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1451
1873
|
function teamStepKey(name, args) {
|
|
1452
1874
|
if (!isRecord(args)) return undefined;
|
|
1453
1875
|
if (name === "spawn_teammate" && typeof args.name === "string" && args.name !== "") return args.name;
|
|
@@ -1608,6 +2030,32 @@ window.__ModuleLoader__.load({
|
|
|
1608
2030
|
touch(folder);
|
|
1609
2031
|
return;
|
|
1610
2032
|
}
|
|
2033
|
+
case "approval/asked": {
|
|
2034
|
+
if (!isRecord(data) || typeof data.id !== "string" || data.id === "") return;
|
|
2035
|
+
const approvalBucket = turnBucket(folder, folder.currentTurn);
|
|
2036
|
+
const toolName = typeof data.toolName === "string" ? data.toolName : "";
|
|
2037
|
+
addStep(folder, approvalBucket, {
|
|
2038
|
+
callId: `approval-${data.id}`,
|
|
2039
|
+
name: "approval",
|
|
2040
|
+
detail: toolName,
|
|
2041
|
+
full: typeof data.reason === "string" && data.reason !== "" ? `${toolName} · ${data.reason}` : toolName,
|
|
2042
|
+
milestone: true,
|
|
2043
|
+
done: false,
|
|
2044
|
+
seq: event.seq,
|
|
2045
|
+
time: event.time,
|
|
2046
|
+
});
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
case "approval/decided": {
|
|
2050
|
+
if (!isRecord(data) || typeof data.id !== "string" || data.id === "") return;
|
|
2051
|
+
const outcome = typeof data.outcome === "string" ? data.outcome : "";
|
|
2052
|
+
const approvalStep = folder.stepByCall.get(`approval-${data.id}`);
|
|
2053
|
+
if (approvalStep !== undefined && outcome !== "" && typeof approvalStep.detail === "string" && !approvalStep.detail.includes(outcome)) {
|
|
2054
|
+
approvalStep.detail = approvalStep.detail === "" ? outcome : `${approvalStep.detail} · ${outcome}`;
|
|
2055
|
+
}
|
|
2056
|
+
markStepDone(folder, `approval-${data.id}`, event.time, /denied|reject/i.test(outcome));
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
1611
2059
|
case "workspace/changes": {
|
|
1612
2060
|
if (!isRecord(data) || typeof data.turn !== "number" || !Number.isSafeInteger(data.turn) || data.turn < 1) return;
|
|
1613
2061
|
if (typeof event.seq !== "number") return;
|
|
@@ -1661,6 +2109,23 @@ window.__ModuleLoader__.load({
|
|
|
1661
2109
|
return;
|
|
1662
2110
|
}
|
|
1663
2111
|
const memoryTool = memoryToolsOf(name);
|
|
2112
|
+
if (memoryTool !== null && MEMORY_MUTATION_TOOLS.has(memoryTool)) {
|
|
2113
|
+
const uri =
|
|
2114
|
+
args !== null && isRecord(args)
|
|
2115
|
+
? isVikingUri(args.uri)
|
|
2116
|
+
? args.uri
|
|
2117
|
+
: isVikingUri(args.to)
|
|
2118
|
+
? args.to
|
|
2119
|
+
: isVikingUri(args.parent)
|
|
2120
|
+
? args.parent
|
|
2121
|
+
: null
|
|
2122
|
+
: null;
|
|
2123
|
+
if (uri !== null) {
|
|
2124
|
+
const description = typeof args.description === "string" ? args.description : "";
|
|
2125
|
+
folder.pending.set(callId, { type: "artifact", path: uri, description });
|
|
2126
|
+
}
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
1664
2129
|
if (memoryTool !== null && MEMORY_REFERENCE_TOOLS.has(memoryTool)) {
|
|
1665
2130
|
const detail = [];
|
|
1666
2131
|
const uris = [];
|
|
@@ -1680,10 +2145,10 @@ window.__ModuleLoader__.load({
|
|
|
1680
2145
|
const refs = [];
|
|
1681
2146
|
for (const uri of uris.slice(0, 8)) {
|
|
1682
2147
|
refs.push({
|
|
1683
|
-
kind:
|
|
2148
|
+
kind: vikingRefKind(uri),
|
|
1684
2149
|
uri,
|
|
1685
2150
|
producer: `openviking · ${memoryTool}`,
|
|
1686
|
-
label:
|
|
2151
|
+
label: vikingLabel(uri),
|
|
1687
2152
|
detail: [],
|
|
1688
2153
|
key: `memory-file|${uri}`,
|
|
1689
2154
|
seq: event.seq,
|
|
@@ -1723,7 +2188,12 @@ window.__ModuleLoader__.load({
|
|
|
1723
2188
|
if (message.isError === true) return;
|
|
1724
2189
|
const bucket = turnBucket(folder, folder.currentTurn);
|
|
1725
2190
|
if (pending.type === "artifact") {
|
|
1726
|
-
upsertArtifact(folder, bucket, pending.path, {
|
|
2191
|
+
upsertArtifact(folder, bucket, pending.path, {
|
|
2192
|
+
produced: true,
|
|
2193
|
+
...(typeof pending.description === "string" && pending.description !== "" ? { description: pending.description } : {}),
|
|
2194
|
+
seq: event.seq,
|
|
2195
|
+
time: event.time,
|
|
2196
|
+
});
|
|
1727
2197
|
} else {
|
|
1728
2198
|
const resultText = textOf(message.content);
|
|
1729
2199
|
for (const ref of pending.refs) {
|
|
@@ -1738,9 +2208,11 @@ window.__ModuleLoader__.load({
|
|
|
1738
2208
|
return;
|
|
1739
2209
|
}
|
|
1740
2210
|
case "user/message": {
|
|
1741
|
-
if (!
|
|
2211
|
+
if (!isRecord(data)) return;
|
|
1742
2212
|
const source = isRecord(data.source) ? data.source : null;
|
|
1743
2213
|
const kind = source !== null && typeof source.kind === "string" ? source.kind : "user";
|
|
2214
|
+
/* a compact checkpoint replaces the event span it summarizes; every other message must append */
|
|
2215
|
+
if (kind !== "compact-checkpoint" && !isAppendSurfaceEvent(event)) return;
|
|
1744
2216
|
const bucket = turnBucket(folder, folder.currentTurn);
|
|
1745
2217
|
if (kind === "user") {
|
|
1746
2218
|
const mentions = mentionsOfUserText(textOf(data.content));
|
|
@@ -1752,6 +2224,22 @@ window.__ModuleLoader__.load({
|
|
|
1752
2224
|
}
|
|
1753
2225
|
return;
|
|
1754
2226
|
}
|
|
2227
|
+
if (kind === "compact-checkpoint") {
|
|
2228
|
+
/* context compaction is a milestone: anchor it as a durable key step */
|
|
2229
|
+
const compactionId = source !== null && typeof source.compactionId === "string" ? source.compactionId : `seq-${event.seq}`;
|
|
2230
|
+
const summaryText = textOf(data.content);
|
|
2231
|
+
addStep(folder, bucket, {
|
|
2232
|
+
name: "compact-checkpoint",
|
|
2233
|
+
detail: summaryText.length > 1024 ? `≈${Math.round(summaryText.length / 1024)} KB` : "",
|
|
2234
|
+
full: compactionId,
|
|
2235
|
+
teamKey: `compact:${compactionId}`,
|
|
2236
|
+
milestone: true,
|
|
2237
|
+
done: true,
|
|
2238
|
+
seq: event.seq,
|
|
2239
|
+
time: event.time,
|
|
2240
|
+
});
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
1755
2243
|
const label = contextLabel(source, kind);
|
|
1756
2244
|
const detail = contextDetail(source, data.content);
|
|
1757
2245
|
const refKind = refKindForSource(source, kind);
|
|
@@ -1760,11 +2248,11 @@ window.__ModuleLoader__.load({
|
|
|
1760
2248
|
if (refKind === "memory" && detail.length > 0 && detail.every(isVikingUri)) {
|
|
1761
2249
|
for (const uri of detail) {
|
|
1762
2250
|
addReference(folder, bucket, {
|
|
1763
|
-
kind:
|
|
2251
|
+
kind: vikingRefKind(uri),
|
|
1764
2252
|
uri,
|
|
1765
2253
|
producer,
|
|
1766
2254
|
form,
|
|
1767
|
-
label:
|
|
2255
|
+
label: vikingLabel(uri),
|
|
1768
2256
|
detail: [],
|
|
1769
2257
|
key: `memory-file|${uri}`,
|
|
1770
2258
|
seq: event.seq,
|
|
@@ -1871,9 +2359,13 @@ window.__ModuleLoader__.load({
|
|
|
1871
2359
|
const allSteps = Array.isArray(source.steps) ? source.steps : [];
|
|
1872
2360
|
const recentSteps = allSteps.slice(-6);
|
|
1873
2361
|
const anchorCandidates = [...(Array.isArray(source.anchors) ? source.anchors : []), ...allSteps].filter(
|
|
1874
|
-
(step, index, list) =>
|
|
2362
|
+
(step, index, list) =>
|
|
2363
|
+
isRecord(step) && typeof step.teamKey === "string" && step.milestone !== true && list.indexOf(step) === index && recentSteps.indexOf(step) === -1
|
|
1875
2364
|
);
|
|
1876
2365
|
const anchorSteps = anchorCandidates.slice(-6);
|
|
2366
|
+
/* milestone steps (compaction, approvals) ride the anchor lane for retention only;
|
|
2367
|
+
whether they pin above the window is a display choice (cfg.stepAnchors) */
|
|
2368
|
+
const milestoneSteps = (Array.isArray(source.anchors) ? source.anchors : []).filter((step) => isRecord(step) && step.milestone === true);
|
|
1877
2369
|
return {
|
|
1878
2370
|
todos,
|
|
1879
2371
|
todosTime,
|
|
@@ -1884,6 +2376,9 @@ window.__ModuleLoader__.load({
|
|
|
1884
2376
|
team,
|
|
1885
2377
|
stats,
|
|
1886
2378
|
turnStart: st.start > 0 ? st.start : null,
|
|
2379
|
+
turnEnd: st.end > 0 ? st.end : null,
|
|
2380
|
+
milestones: milestoneSteps,
|
|
2381
|
+
todoHistory: folder.todoRecords.slice(-300),
|
|
1887
2382
|
changeTurns: changeTurns.slice(),
|
|
1888
2383
|
hasMore: windowSnapshot.hasMore === true,
|
|
1889
2384
|
localeRev,
|
|
@@ -1893,7 +2388,7 @@ window.__ModuleLoader__.load({
|
|
|
1893
2388
|
};
|
|
1894
2389
|
}
|
|
1895
2390
|
|
|
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" };
|
|
2391
|
+
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" };
|
|
1897
2392
|
|
|
1898
2393
|
function createSummaryStore(binding) {
|
|
1899
2394
|
const source = binding.eventSource;
|
|
@@ -2764,13 +3259,18 @@ function FileTypeIcon(props) {
|
|
|
2764
3259
|
);
|
|
2765
3260
|
}
|
|
2766
3261
|
|
|
3262
|
+
/* Same artwork as icon.svg, the icon the plugin list and settings show. */
|
|
2767
3263
|
function OverviewGlyph({ size = 16, className }) {
|
|
2768
|
-
return svg(
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
3264
|
+
return svg(
|
|
3265
|
+
size,
|
|
3266
|
+
className,
|
|
3267
|
+
[
|
|
3268
|
+
h("rect", { key: "r", x: 8, y: 8, width: 48, height: 48, rx: 12, stroke: "#565e6d", strokeWidth: 3 }),
|
|
3269
|
+
h("path", { key: "l", d: "M20 24h24M20 33h24M20 42h14", stroke: "#247bbf", strokeWidth: 3, strokeLinecap: "round" }),
|
|
3270
|
+
h("circle", { key: "c", cx: 43, cy: 42, r: 4, fill: "#2ea36a" }),
|
|
3271
|
+
],
|
|
3272
|
+
"0 0 64 64"
|
|
3273
|
+
);
|
|
2774
3274
|
}
|
|
2775
3275
|
|
|
2776
3276
|
function IconProgress({ size = 16, className }) {
|
|
@@ -2839,6 +3339,14 @@ function FileTypeIcon(props) {
|
|
|
2839
3339
|
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" })]);
|
|
2840
3340
|
}
|
|
2841
3341
|
|
|
3342
|
+
function IconCompact({ size = 16, className }) {
|
|
3343
|
+
return svg(size, className, [
|
|
3344
|
+
h("path", { key: "a", d: "M3.2 8h9.6", stroke: "currentColor", strokeLinecap: "round" }),
|
|
3345
|
+
h("path", { key: "b", d: "M8 2.4v3.4M6.3 4.1 8 5.8l1.7-1.7", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round" }),
|
|
3346
|
+
h("path", { key: "c", d: "M8 13.6v-3.4M6.3 11.9 8 10.2l1.7 1.7", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round" }),
|
|
3347
|
+
]);
|
|
3348
|
+
}
|
|
3349
|
+
|
|
2842
3350
|
function IconHistory({ size = 16, className }) {
|
|
2843
3351
|
return svg(size, className, [
|
|
2844
3352
|
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" }),
|
|
@@ -3107,9 +3615,13 @@ function FileTypeIcon(props) {
|
|
|
3107
3615
|
complete: "success",
|
|
3108
3616
|
};
|
|
3109
3617
|
|
|
3110
|
-
function ProgressSection({ summary, goal, t, waiting, cfg }) {
|
|
3618
|
+
function ProgressSection({ summary, goal, t, waiting, cfg, asOf }) {
|
|
3111
3619
|
const fmtTime = (ts) => formatTime(ts, cfg.timeMode, summary.turnStart);
|
|
3112
3620
|
const timeTitle = (ts) => (cfg.timeMode === "elapsed" ? formatTime(ts) : undefined);
|
|
3621
|
+
/* timeline scrubbing: only what already happened at `asOf` is shown */
|
|
3622
|
+
const asOfTime = typeof asOf === "number" && asOf > 0 ? asOf : null;
|
|
3623
|
+
const pinMilestones = isRecord(cfg) && cfg.stepAnchors === true;
|
|
3624
|
+
const inTime = (record) => asOfTime === null || ((record.firstTime || record.time || 0) <= asOfTime);
|
|
3113
3625
|
/* todo → composer prompt flash */
|
|
3114
3626
|
const [promptFlash, setPromptFlash] = React.useState(null);
|
|
3115
3627
|
const flashTimer = React.useRef(undefined);
|
|
@@ -3164,8 +3676,30 @@ function FileTypeIcon(props) {
|
|
|
3164
3676
|
if (!handled) setFlash("err");
|
|
3165
3677
|
};
|
|
3166
3678
|
/* steps: newest-first window; keep the evicted bottom row as a fading ghost */
|
|
3167
|
-
const
|
|
3168
|
-
const
|
|
3679
|
+
const stepsRaw = Array.isArray(summary.steps) ? summary.steps.filter((step) => isRecord(step)) : [];
|
|
3680
|
+
const stepsAllRaw = Array.isArray(summary.stepsAll) ? summary.stepsAll.filter((step) => isRecord(step)) : [];
|
|
3681
|
+
const stepAt = (step) => asOfTime === null || (typeof step.time === "number" ? step.time <= asOfTime : true);
|
|
3682
|
+
/* a step whose result landed after the scrub point reads as still running */
|
|
3683
|
+
const stepClamped = (step) => {
|
|
3684
|
+
if (asOfTime === null || step.done !== true) return step;
|
|
3685
|
+
const end = typeof step.endTime === "number" ? step.endTime : typeof step.time === "number" ? step.time : 0;
|
|
3686
|
+
return end <= asOfTime ? step : { ...step, done: false, error: false, endTime: undefined };
|
|
3687
|
+
};
|
|
3688
|
+
const stepsAll = stepsAllRaw.filter(stepAt).map(stepClamped);
|
|
3689
|
+
/* while scrubbing, rebuild the "latest 6" window as it looked at time T:
|
|
3690
|
+
the live window sits near the turn end, so filtering it by T would empty
|
|
3691
|
+
the section mid-drag even though earlier steps already happened */
|
|
3692
|
+
const steps =
|
|
3693
|
+
asOfTime === null
|
|
3694
|
+
? stepsRaw.filter(stepAt).map(stepClamped)
|
|
3695
|
+
: (() => {
|
|
3696
|
+
const visible = stepsAllRaw.filter(stepAt);
|
|
3697
|
+
const recent = visible.slice(-6);
|
|
3698
|
+
const anchors = stepsRaw.filter(
|
|
3699
|
+
(step) => stepAt(step) && typeof step.teamKey === "string" && step.milestone !== true && recent.indexOf(step) === -1
|
|
3700
|
+
);
|
|
3701
|
+
return [...anchors, ...recent].map(stepClamped);
|
|
3702
|
+
})();
|
|
3169
3703
|
const [ghost, setGhost] = React.useState(null);
|
|
3170
3704
|
const [stepsExpanded, setStepsExpanded] = React.useState(false);
|
|
3171
3705
|
const ghostTimer = React.useRef(undefined);
|
|
@@ -3173,6 +3707,7 @@ function FileTypeIcon(props) {
|
|
|
3173
3707
|
const prevScope = React.useRef(summary.scope);
|
|
3174
3708
|
/* enter animation only for rows that arrive while mounted; first paint per scope is silent */
|
|
3175
3709
|
const stepKeyOf = (step, index) => (typeof step.callId === "string" && step.callId !== "" ? `step-${step.callId}` : `step-idx-${index}`);
|
|
3710
|
+
const milestoneKeyOf = (step) => (typeof step.callId === "string" && step.callId !== "" ? step.callId : `${step.name}|${step.seq}`);
|
|
3176
3711
|
const seenSteps = React.useRef(null);
|
|
3177
3712
|
/* full retained history for the expander: anchors outside the window + the whole bucket */
|
|
3178
3713
|
const allStepIds = new Set(stepsAll.map((step) => step.callId).filter((id) => typeof id === "string"));
|
|
@@ -3180,7 +3715,25 @@ function FileTypeIcon(props) {
|
|
|
3180
3715
|
const expandedSteps = [...anchorExtras, ...stepsAll];
|
|
3181
3716
|
const canExpand = expandedSteps.length > steps.length;
|
|
3182
3717
|
const stepsOpen = stepsExpanded && canExpand;
|
|
3183
|
-
|
|
3718
|
+
/* cfg.stepAnchors pins compaction/approval/failure steps above the window */
|
|
3719
|
+
const milestoneExtras = [];
|
|
3720
|
+
if (pinMilestones) {
|
|
3721
|
+
const baseSteps = stepsOpen ? expandedSteps : steps;
|
|
3722
|
+
const baseKeys = new Set(baseSteps.map(milestoneKeyOf));
|
|
3723
|
+
const candidates = [
|
|
3724
|
+
...(Array.isArray(summary.milestones) ? summary.milestones.filter((step) => isRecord(step) && step.milestone === true) : []),
|
|
3725
|
+
...stepsAll.filter((step) => step.error === true),
|
|
3726
|
+
];
|
|
3727
|
+
for (const candidate of candidates) {
|
|
3728
|
+
if (!stepAt(candidate)) continue;
|
|
3729
|
+
const key = milestoneKeyOf(candidate);
|
|
3730
|
+
if (baseKeys.has(key)) continue;
|
|
3731
|
+
baseKeys.add(key);
|
|
3732
|
+
milestoneExtras.push(stepClamped(candidate));
|
|
3733
|
+
}
|
|
3734
|
+
milestoneExtras.sort((left, right) => (left.time || 0) - (right.time || 0));
|
|
3735
|
+
}
|
|
3736
|
+
const displaySteps = milestoneExtras.length > 0 ? [...milestoneExtras, ...(stepsOpen ? expandedSteps : steps)] : stepsOpen ? expandedSteps : steps;
|
|
3184
3737
|
if (seenSteps.current === null || seenSteps.current.scope !== summary.scope) {
|
|
3185
3738
|
seenSteps.current = { scope: summary.scope, keys: new Set(displaySteps.map(stepKeyOf)) };
|
|
3186
3739
|
}
|
|
@@ -3197,7 +3750,7 @@ function FileTypeIcon(props) {
|
|
|
3197
3750
|
prevScope.current = summary.scope;
|
|
3198
3751
|
return;
|
|
3199
3752
|
}
|
|
3200
|
-
if (previous.length === 0) return;
|
|
3753
|
+
if (asOfTime !== null || previous.length === 0) return;
|
|
3201
3754
|
const currentIds = new Set(steps.map((step) => step.callId));
|
|
3202
3755
|
const dropped = previous.filter((step) => typeof step.callId === "string" && !currentIds.has(step.callId));
|
|
3203
3756
|
if (dropped.length === 0) return;
|
|
@@ -3210,10 +3763,28 @@ function FileTypeIcon(props) {
|
|
|
3210
3763
|
}, [steps]);
|
|
3211
3764
|
/* team data + todos drive both the merged list and the step nesting */
|
|
3212
3765
|
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
|
|
3766
|
+
const members = team && Array.isArray(team.members) ? team.members.filter((member) => isRecord(member) && inTime(member)) : [];
|
|
3767
|
+
const teamTasks = team && Array.isArray(team.tasks) ? team.tasks.filter((task) => isRecord(task) && inTime(task)) : [];
|
|
3768
|
+
const subagents = team && Array.isArray(team.subagents) ? team.subagents.filter((sub) => isRecord(sub) && inTime(sub)) : [];
|
|
3769
|
+
const todoHistory = Array.isArray(summary.todoHistory) ? summary.todoHistory : [];
|
|
3770
|
+
let todos = Array.isArray(summary.todos) ? summary.todos : null;
|
|
3771
|
+
if (asOfTime !== null) {
|
|
3772
|
+
/* mirror buildState's turn scoping: a turn without its own todos must
|
|
3773
|
+
not inherit an earlier turn's snapshot while scrubbing */
|
|
3774
|
+
const scopeText = typeof summary.scope === "string" ? summary.scope : "";
|
|
3775
|
+
const scopedTurn = scopeText.startsWith("turn:")
|
|
3776
|
+
? Number.parseInt(scopeText.slice(5), 10)
|
|
3777
|
+
: scopeText === "latest" && Number.isFinite(summary.latestTurn)
|
|
3778
|
+
? summary.latestTurn
|
|
3779
|
+
: null;
|
|
3780
|
+
let snapshot = null;
|
|
3781
|
+
for (const record of todoHistory) {
|
|
3782
|
+
if (!isRecord(record) || !Array.isArray(record.todos)) continue;
|
|
3783
|
+
if (scopedTurn !== null && record.turn !== scopedTurn) continue;
|
|
3784
|
+
if ((record.time || 0) <= asOfTime) snapshot = record;
|
|
3785
|
+
}
|
|
3786
|
+
todos = snapshot !== null ? snapshot.todos : null;
|
|
3787
|
+
}
|
|
3217
3788
|
const hasTodos = todos !== null && todos.length > 0;
|
|
3218
3789
|
const hasTeam = members.length + teamTasks.length + subagents.length > 0;
|
|
3219
3790
|
const showSteps = !hasTodos && steps.length > 0;
|
|
@@ -3265,23 +3836,11 @@ function FileTypeIcon(props) {
|
|
|
3265
3836
|
h("span", { className: "dsov-time", title: timeTitle(sub.time) }, fmtTime(sub.time))
|
|
3266
3837
|
);
|
|
3267
3838
|
};
|
|
3839
|
+
/* members and subagents always read as agents: person icon, tinted red on failure */
|
|
3268
3840
|
const memberIcon = (member, owned) => {
|
|
3269
3841
|
const phase = typeof member.phase === "string" ? member.phase : "";
|
|
3270
3842
|
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" }));
|
|
3843
|
+
return h("span", { className: "dsov-todoIcon dsov-agentIcon", "data-status": "failed" }, h(IconAgent, { size: 14 }));
|
|
3285
3844
|
}
|
|
3286
3845
|
return h("span", { className: "dsov-todoIcon dsov-agentIcon" }, h(IconAgent, { size: 14 }));
|
|
3287
3846
|
};
|
|
@@ -3313,6 +3872,20 @@ function FileTypeIcon(props) {
|
|
|
3313
3872
|
const consumedMembers = new Set();
|
|
3314
3873
|
const consumedTasks = new Set();
|
|
3315
3874
|
const consumedSubs = new Set();
|
|
3875
|
+
/* delegates matched to a todo render nested under it and skip the flat tail */
|
|
3876
|
+
const todoAttachments = hasTodos && hasTeam ? planTodoAttachments(todos, members, teamTasks, subagents) : new Map();
|
|
3877
|
+
for (const list of todoAttachments.values()) {
|
|
3878
|
+
for (const item of list) {
|
|
3879
|
+
if (item.kind === "member") {
|
|
3880
|
+
consumedMembers.add(item.record.id);
|
|
3881
|
+
item.owned.forEach((task) => consumedTasks.add(task.id));
|
|
3882
|
+
} else if (item.kind === "sub") {
|
|
3883
|
+
consumedSubs.add(item.record.childId);
|
|
3884
|
+
} else {
|
|
3885
|
+
consumedTasks.add(item.record.id);
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3316
3889
|
const stepAttachments = new Map();
|
|
3317
3890
|
if (showSteps && hasTeam) {
|
|
3318
3891
|
displaySteps.forEach((step, index) => {
|
|
@@ -3412,7 +3985,9 @@ function FileTypeIcon(props) {
|
|
|
3412
3985
|
h(IconChevron, { size: 11 })
|
|
3413
3986
|
)
|
|
3414
3987
|
: null,
|
|
3415
|
-
|
|
3988
|
+
step.name === "compact-checkpoint"
|
|
3989
|
+
? h("span", { className: "dsov-todoIcon dsov-compactIcon" }, h(IconCompact, { size: 15 }))
|
|
3990
|
+
: waitingRow
|
|
3416
3991
|
? h(
|
|
3417
3992
|
"span",
|
|
3418
3993
|
{ className: "dsov-todoIcon", "data-status": "waiting" },
|
|
@@ -3424,7 +3999,11 @@ function FileTypeIcon(props) {
|
|
|
3424
3999
|
)
|
|
3425
4000
|
)
|
|
3426
4001
|
: h("span", { className: "dsov-todoIcon", "data-status": stepRunning ? "in_progress" : "completed" }, h(TodoStatusIcon, { status: stepRunning ? "in_progress" : "completed" })),
|
|
3427
|
-
h(
|
|
4002
|
+
h(
|
|
4003
|
+
"span",
|
|
4004
|
+
{ className: "dsov-stepName" },
|
|
4005
|
+
step.name === "compact-checkpoint" ? t("step.compact") : step.name === "approval" ? t("step.approval") : typeof step.name === "string" ? step.name : ""
|
|
4006
|
+
),
|
|
3428
4007
|
h("span", { className: "dsov-stepDetail" }, detail),
|
|
3429
4008
|
h("span", { className: "dsov-time", title: timeTitle(typeof step.endTime === "number" ? step.endTime : step.time) }, fmtTime(typeof step.endTime === "number" ? step.endTime : step.time))
|
|
3430
4009
|
);
|
|
@@ -3459,6 +4038,7 @@ function FileTypeIcon(props) {
|
|
|
3459
4038
|
h("div", { className: "dsov-progressMeta" }, t("progress.counts", { completed: done, total }))
|
|
3460
4039
|
)
|
|
3461
4040
|
);
|
|
4041
|
+
const todoRows = [];
|
|
3462
4042
|
todos.forEach((todo, index) => {
|
|
3463
4043
|
const status = TODO_KEYS[todo.status] ? todo.status : "pending";
|
|
3464
4044
|
const flash = promptFlash !== null && isRecord(promptFlash) && promptFlash.index === index ? promptFlash.state : null;
|
|
@@ -3471,7 +4051,21 @@ function FileTypeIcon(props) {
|
|
|
3471
4051
|
: flash === "failed"
|
|
3472
4052
|
? t("progress.todoFailed")
|
|
3473
4053
|
: t("progress.todoHint");
|
|
3474
|
-
|
|
4054
|
+
const attached = todoAttachments.get(index);
|
|
4055
|
+
const expandable = Array.isArray(attached) && attached.length > 0;
|
|
4056
|
+
const open = expandable
|
|
4057
|
+
? overrideOf(
|
|
4058
|
+
`todo:${index}`,
|
|
4059
|
+
attached.some((item) =>
|
|
4060
|
+
item.kind === "task"
|
|
4061
|
+
? item.record.status !== "completed"
|
|
4062
|
+
: item.kind === "member"
|
|
4063
|
+
? item.record.phase === "provisioning" || item.record.phase === "active" || item.owned.some((task) => task.status !== "completed")
|
|
4064
|
+
: false
|
|
4065
|
+
)
|
|
4066
|
+
)
|
|
4067
|
+
: false;
|
|
4068
|
+
todoRows.push(
|
|
3475
4069
|
h(
|
|
3476
4070
|
"div",
|
|
3477
4071
|
{
|
|
@@ -3479,9 +4073,12 @@ function FileTypeIcon(props) {
|
|
|
3479
4073
|
key: `todo-${index}`,
|
|
3480
4074
|
"data-status": status,
|
|
3481
4075
|
"data-flash": flash === "inserted" || flash === "copied" ? "ok" : flash === "failed" ? "err" : undefined,
|
|
4076
|
+
"data-expandable": expandable ? "true" : undefined,
|
|
4077
|
+
"data-open": expandable ? (open ? "true" : "false") : undefined,
|
|
3482
4078
|
role: "button",
|
|
3483
4079
|
tabIndex: todoPromptOn ? 0 : undefined,
|
|
3484
4080
|
title,
|
|
4081
|
+
onClick: expandable ? () => toggleKey(`todo:${index}`, !open) : undefined,
|
|
3485
4082
|
onDoubleClick: () => onTodoClick(index, todo.content),
|
|
3486
4083
|
onKeyDown: (event) => {
|
|
3487
4084
|
if (event.key === "Enter" || event.key === " ") {
|
|
@@ -3490,12 +4087,46 @@ function FileTypeIcon(props) {
|
|
|
3490
4087
|
}
|
|
3491
4088
|
},
|
|
3492
4089
|
},
|
|
3493
|
-
h(
|
|
4090
|
+
h(
|
|
4091
|
+
"span",
|
|
4092
|
+
{ className: "dsov-todoIconSlot" },
|
|
4093
|
+
h("span", { className: "dsov-todoIcon", "data-status": status }, h(TodoStatusIcon, { status })),
|
|
4094
|
+
expandable
|
|
4095
|
+
? h(
|
|
4096
|
+
"span",
|
|
4097
|
+
{
|
|
4098
|
+
className: "dsov-chevron dsov-todoChevron",
|
|
4099
|
+
"data-open": open ? "true" : "false",
|
|
4100
|
+
title: t(open ? "progress.collapseTeam" : "progress.expandTeam"),
|
|
4101
|
+
onClick: (event) => {
|
|
4102
|
+
event.stopPropagation();
|
|
4103
|
+
toggleKey(`todo:${index}`, !open);
|
|
4104
|
+
},
|
|
4105
|
+
},
|
|
4106
|
+
h(IconChevron, { size: 11 })
|
|
4107
|
+
)
|
|
4108
|
+
: null
|
|
4109
|
+
),
|
|
3494
4110
|
h("span", { className: "dsov-todoText" }, todo.content),
|
|
3495
4111
|
h("span", { className: "dsov-todoStatus" }, t(TODO_KEYS[status]))
|
|
3496
4112
|
)
|
|
3497
4113
|
);
|
|
4114
|
+
if (expandable && open) {
|
|
4115
|
+
attached.forEach((item) => {
|
|
4116
|
+
if (item.kind === "member") {
|
|
4117
|
+
const memberExpandable = item.owned.length > 0;
|
|
4118
|
+
const memberOpen = memberExpandable ? overrideOf(`member:${item.record.id}`, item.owned.some((task) => task.status !== "completed")) : false;
|
|
4119
|
+
todoRows.push(memberRow(item.record, 1, memberExpandable, memberOpen));
|
|
4120
|
+
if (memberExpandable && memberOpen) item.owned.forEach((task) => todoRows.push(taskRow(task, 2)));
|
|
4121
|
+
} else if (item.kind === "sub") {
|
|
4122
|
+
todoRows.push(subRow(item.record, 1));
|
|
4123
|
+
} else {
|
|
4124
|
+
todoRows.push(taskRow(item.record, 1));
|
|
4125
|
+
}
|
|
4126
|
+
});
|
|
4127
|
+
}
|
|
3498
4128
|
});
|
|
4129
|
+
kids.push(h("div", { className: "dsov-todos", key: "todos" }, todoRows));
|
|
3499
4130
|
}
|
|
3500
4131
|
if (hasTeam && !showSteps) pushFlatTeam(kids);
|
|
3501
4132
|
if (showSteps) {
|
|
@@ -3513,7 +4144,7 @@ function FileTypeIcon(props) {
|
|
|
3513
4144
|
let freshCount = 0;
|
|
3514
4145
|
displaySteps.forEach((step, index) => {
|
|
3515
4146
|
const key = stepKeyOf(step, index);
|
|
3516
|
-
const fresh = !seenSteps.current.keys.has(key);
|
|
4147
|
+
const fresh = asOfTime === null && !seenSteps.current.keys.has(key);
|
|
3517
4148
|
if (fresh) {
|
|
3518
4149
|
seenSteps.current.keys.add(key);
|
|
3519
4150
|
freshCount += 1;
|
|
@@ -3549,12 +4180,33 @@ function FileTypeIcon(props) {
|
|
|
3549
4180
|
return h(Section, { id: "progress", icon: h(IconProgress, { size: 15 }), title: t("section.progress"), empty: t(summary.scope === "all" ? "progress.empty" : "progress.emptyTurn") }, kids);
|
|
3550
4181
|
}
|
|
3551
4182
|
|
|
3552
|
-
function ArtifactsSection({ summary, t, binding, stats }) {
|
|
4183
|
+
function ArtifactsSection({ summary, t, binding, stats, asOf }) {
|
|
3553
4184
|
const cfg = useConfig();
|
|
4185
|
+
const asOfTime = typeof asOf === "number" && asOf > 0 ? asOf : null;
|
|
4186
|
+
const visibleArtifacts = asOfTime === null ? summary.artifacts : summary.artifacts.filter((artifact) => (artifact.firstTime || artifact.time || 0) <= asOfTime);
|
|
3554
4187
|
const fmtTime = (ts) => formatTime(ts, cfg.timeMode, summary.turnStart);
|
|
3555
4188
|
const timeTitle = (ts) => (cfg.timeMode === "elapsed" ? formatTime(ts) : undefined);
|
|
3556
4189
|
const sessionId = binding ? binding.sessionId : undefined;
|
|
3557
4190
|
const cwd = sessionCwd(sessionId);
|
|
4191
|
+
const canFileMenu = hostSessionRemote() !== null;
|
|
4192
|
+
const [fileMenu, setFileMenu] = React.useState(null);
|
|
4193
|
+
React.useEffect(() => {
|
|
4194
|
+
if (fileMenu === null) return undefined;
|
|
4195
|
+
const close = () => setFileMenu(null);
|
|
4196
|
+
const onKey = (event) => {
|
|
4197
|
+
if (event.key === "Escape") close();
|
|
4198
|
+
};
|
|
4199
|
+
document.addEventListener("mousedown", close);
|
|
4200
|
+
document.addEventListener("keydown", onKey);
|
|
4201
|
+
window.addEventListener("scroll", close, true);
|
|
4202
|
+
window.addEventListener("resize", close);
|
|
4203
|
+
return () => {
|
|
4204
|
+
document.removeEventListener("mousedown", close);
|
|
4205
|
+
document.removeEventListener("keydown", onKey);
|
|
4206
|
+
window.removeEventListener("scroll", close, true);
|
|
4207
|
+
window.removeEventListener("resize", close);
|
|
4208
|
+
};
|
|
4209
|
+
}, [fileMenu]);
|
|
3558
4210
|
const turns = summary.changeTurns;
|
|
3559
4211
|
const latest = turns.length > 0 ? turns[turns.length - 1] : null;
|
|
3560
4212
|
const action =
|
|
@@ -3566,9 +4218,10 @@ function FileTypeIcon(props) {
|
|
|
3566
4218
|
t("artifacts.viewAll")
|
|
3567
4219
|
)
|
|
3568
4220
|
: null;
|
|
3569
|
-
const rows =
|
|
4221
|
+
const rows = visibleArtifacts.map((artifact, index) => {
|
|
3570
4222
|
const stat = stats ? stats.get(absPath(cwd, artifact.path)) : undefined;
|
|
3571
4223
|
const showCounts = stat !== undefined && !stat.binary && !stat.oversized && (stat.added > 0 || stat.deleted > 0);
|
|
4224
|
+
const isMemory = typeof artifact.path === "string" && artifact.path.startsWith("viking://");
|
|
3572
4225
|
return h(
|
|
3573
4226
|
"button",
|
|
3574
4227
|
{
|
|
@@ -3577,6 +4230,13 @@ function FileTypeIcon(props) {
|
|
|
3577
4230
|
key: `${artifact.path}#${index}`,
|
|
3578
4231
|
title: artifact.description ? `${artifact.path} — ${artifact.description}` : artifact.path,
|
|
3579
4232
|
onClick: () => openArtifact(binding, artifact, stats),
|
|
4233
|
+
onContextMenu:
|
|
4234
|
+
canFileMenu && !isMemory
|
|
4235
|
+
? (event) => {
|
|
4236
|
+
event.preventDefault();
|
|
4237
|
+
setFileMenu({ x: event.clientX, y: event.clientY, path: absPath(cwd, artifact.path) });
|
|
4238
|
+
}
|
|
4239
|
+
: undefined,
|
|
3580
4240
|
},
|
|
3581
4241
|
h("span", { className: "dsov-fileIcon" }, h(FileTypeIcon, { path: artifact.path, size: 20 })),
|
|
3582
4242
|
h("span", { className: "dsov-artifactName" }, basename(artifact.path)),
|
|
@@ -3590,17 +4250,19 @@ function FileTypeIcon(props) {
|
|
|
3590
4250
|
: stat !== undefined && (stat.binary || stat.oversized)
|
|
3591
4251
|
? h("span", { className: "dsov-badge", "data-tone": "quiet" }, t("artifact.binary"))
|
|
3592
4252
|
: null,
|
|
4253
|
+
isMemory ? h("span", { className: "dsov-badge", "data-tone": "quiet" }, t("artifact.memory")) : null,
|
|
3593
4254
|
artifact.presented ? h("span", { className: "dsov-badge", "data-tone": "brand" }, t("artifact.presented")) : null,
|
|
3594
4255
|
h("span", { className: "dsov-rowTail" }, h("span", { className: "dsov-time", title: timeTitle(artifact.time) }, fmtTime(artifact.time)))
|
|
3595
4256
|
);
|
|
3596
4257
|
});
|
|
4258
|
+
if (fileMenu !== null) rows.push(h(ArtifactFileMenu, { key: "dsov-file-menu", menu: fileMenu, t, onClose: () => setFileMenu(null) }));
|
|
3597
4259
|
return h(
|
|
3598
4260
|
Section,
|
|
3599
4261
|
{
|
|
3600
4262
|
id: "artifacts",
|
|
3601
4263
|
icon: h(IconArtifact, { size: 15 }),
|
|
3602
4264
|
title: t("section.artifacts"),
|
|
3603
|
-
badge:
|
|
4265
|
+
badge: visibleArtifacts.length > 0 ? String(visibleArtifacts.length) : undefined,
|
|
3604
4266
|
empty: t("artifacts.empty"),
|
|
3605
4267
|
action,
|
|
3606
4268
|
},
|
|
@@ -3706,8 +4368,19 @@ function FileTypeIcon(props) {
|
|
|
3706
4368
|
);
|
|
3707
4369
|
}
|
|
3708
4370
|
|
|
3709
|
-
|
|
4371
|
+
/** Ref tile: viking rows get file/folder icons, other kinds keep their type icon. */
|
|
4372
|
+
function refTile(ref) {
|
|
4373
|
+
if (ref.kind === "file") return h(FileTypeIcon, { path: ref.label, size: 18 });
|
|
4374
|
+
if ((ref.kind === "memory-file" || ref.kind === "skill") && typeof ref.uri === "string" && isVikingUri(ref.uri)) {
|
|
4375
|
+
return vikingIsDir(ref.uri) ? h(FileTypeIcon, { kind: "folder", size: 18 }) : h(FileTypeIcon, { path: ref.uri, size: 18 });
|
|
4376
|
+
}
|
|
4377
|
+
return h(RefIcon, { kind: ref.kind, size: 14 });
|
|
4378
|
+
}
|
|
4379
|
+
|
|
4380
|
+
function ReferencesSection({ summary, t, sessionId, asOf }) {
|
|
3710
4381
|
const cfg = useConfig();
|
|
4382
|
+
const asOfTime = typeof asOf === "number" && asOf > 0 ? asOf : null;
|
|
4383
|
+
const allRefs = asOfTime === null ? summary.references : summary.references.filter((ref) => (ref.firstTime || ref.time || 0) <= asOfTime);
|
|
3711
4384
|
const fmtTime = (ts) => formatTime(ts, cfg.timeMode, summary.turnStart);
|
|
3712
4385
|
const timeTitle = (ts) => (cfg.timeMode === "elapsed" ? formatTime(ts) : undefined);
|
|
3713
4386
|
const sortMode = useRefSort();
|
|
@@ -3745,7 +4418,7 @@ function FileTypeIcon(props) {
|
|
|
3745
4418
|
}, 1200);
|
|
3746
4419
|
}
|
|
3747
4420
|
};
|
|
3748
|
-
const scoped = hideCommon ?
|
|
4421
|
+
const scoped = hideCommon ? allRefs.filter((ref) => ref.ubiquitous !== true) : allRefs;
|
|
3749
4422
|
const selectedTypes = Array.isArray(typeFilter) ? typeFilter : [];
|
|
3750
4423
|
const filtered = selectedTypes.length === 0 ? scoped : scoped.filter((ref) => selectedTypes.indexOf(refDisplayType(ref)) !== -1);
|
|
3751
4424
|
const sorted = sortReferences(filtered, sortMode);
|
|
@@ -3755,7 +4428,7 @@ function FileTypeIcon(props) {
|
|
|
3755
4428
|
const extra = detail.length - shown.length;
|
|
3756
4429
|
const skillCatalog = ref.producer === "skill-catalog" && shown.length > 0;
|
|
3757
4430
|
const content = [
|
|
3758
|
-
h("span", { className: "dsov-tile", key: "tile" },
|
|
4431
|
+
h("span", { className: "dsov-tile", key: "tile" }, refTile(ref)),
|
|
3759
4432
|
h(
|
|
3760
4433
|
"span",
|
|
3761
4434
|
{ className: "dsov-rowText", key: "text" },
|
|
@@ -3822,7 +4495,7 @@ function FileTypeIcon(props) {
|
|
|
3822
4495
|
...content
|
|
3823
4496
|
);
|
|
3824
4497
|
}
|
|
3825
|
-
if (ref.kind === "memory-file" && isVikingUri(ref.uri) && cfg.vikingPreview && cfg.vikingUrl !== "") {
|
|
4498
|
+
if ((ref.kind === "memory-file" || ref.kind === "skill") && isVikingUri(ref.uri) && cfg.vikingPreview && cfg.vikingUrl !== "") {
|
|
3826
4499
|
return h(
|
|
3827
4500
|
"button",
|
|
3828
4501
|
{
|
|
@@ -3850,13 +4523,13 @@ function FileTypeIcon(props) {
|
|
|
3850
4523
|
}
|
|
3851
4524
|
return h(
|
|
3852
4525
|
"div",
|
|
3853
|
-
{ className: "dsov-row dsov-rowStatic", key: `ref-${index}`, title:
|
|
4526
|
+
{ className: "dsov-row dsov-rowStatic", key: `ref-${index}`, title: isVikingUri(ref.uri) ? ref.uri : ref.label },
|
|
3854
4527
|
...content
|
|
3855
4528
|
);
|
|
3856
4529
|
});
|
|
3857
|
-
const ubiquitousCount =
|
|
4530
|
+
const ubiquitousCount = allRefs.filter((ref) => ref.ubiquitous === true).length;
|
|
3858
4531
|
const typeSet = [];
|
|
3859
|
-
for (const ref of
|
|
4532
|
+
for (const ref of allRefs) {
|
|
3860
4533
|
const kind = refDisplayType(ref);
|
|
3861
4534
|
if (kind !== "" && typeSet.indexOf(kind) === -1) typeSet.push(kind);
|
|
3862
4535
|
}
|
|
@@ -3892,7 +4565,7 @@ function FileTypeIcon(props) {
|
|
|
3892
4565
|
badge: filtered.length > 0 ? String(filtered.length) : undefined,
|
|
3893
4566
|
empty: hideCommon && ubiquitousCount > 0 && scoped.length === 0 ? t("references.onlyCommon") : t("references.empty"),
|
|
3894
4567
|
action:
|
|
3895
|
-
|
|
4568
|
+
allRefs.length > 1
|
|
3896
4569
|
? h(
|
|
3897
4570
|
"div",
|
|
3898
4571
|
{ className: "dsov-refControls" },
|
|
@@ -3954,19 +4627,47 @@ function FileTypeIcon(props) {
|
|
|
3954
4627
|
}
|
|
3955
4628
|
|
|
3956
4629
|
/** Left-hand stat pills on the scope row: duration, tool calls, commands, files, failures. */
|
|
3957
|
-
function StatsBar({ stats, t }) {
|
|
4630
|
+
function StatsBar({ stats, t, canTimeline, timelineOpen, scrubbing, onToggleTimeline, prevJump, onPrevTurn }) {
|
|
3958
4631
|
if (!isRecord(stats)) return null;
|
|
3959
4632
|
const duration = typeof stats.durationMs === "number" ? stats.durationMs : 0;
|
|
3960
4633
|
const toolCalls = typeof stats.toolCalls === "number" ? stats.toolCalls : 0;
|
|
3961
4634
|
const commands = typeof stats.commands === "number" ? stats.commands : 0;
|
|
3962
4635
|
const files = typeof stats.files === "number" ? stats.files : 0;
|
|
3963
4636
|
const errors = typeof stats.errors === "number" ? stats.errors : 0;
|
|
3964
|
-
|
|
4637
|
+
/* scrubbed to 0s: every pill is zero and the bar would vanish — keep it
|
|
4638
|
+
alive with the "previous turn" jump instead */
|
|
4639
|
+
const jumpBtn =
|
|
4640
|
+
Number.isFinite(prevJump) && typeof onPrevTurn === "function"
|
|
4641
|
+
? h(
|
|
4642
|
+
"button",
|
|
4643
|
+
{ type: "button", className: "dsov-stat dsov-statBtn dsov-prevTurn", key: "prevTurn", title: t("timeline.prevTurnTitle", { n: prevJump }), onClick: onPrevTurn },
|
|
4644
|
+
`\u2039 ${t("timeline.prevTurn")}`
|
|
4645
|
+
)
|
|
4646
|
+
: null;
|
|
4647
|
+
if (duration === 0 && toolCalls === 0) return jumpBtn === null ? null : h("div", { className: "dsov-stats" }, jumpBtn);
|
|
3965
4648
|
const pill = (key, text, title, tone) => h("span", { className: "dsov-stat", key, title, ...(tone === undefined ? {} : { "data-tone": tone }) }, text);
|
|
4649
|
+
const scrubable = canTimeline === true && typeof onToggleTimeline === "function";
|
|
3966
4650
|
return h(
|
|
3967
4651
|
"div",
|
|
3968
4652
|
{ className: "dsov-stats" },
|
|
3969
|
-
|
|
4653
|
+
jumpBtn,
|
|
4654
|
+
duration > 0
|
|
4655
|
+
? scrubable
|
|
4656
|
+
? h(
|
|
4657
|
+
"button",
|
|
4658
|
+
{
|
|
4659
|
+
type: "button",
|
|
4660
|
+
className: "dsov-stat dsov-statBtn",
|
|
4661
|
+
key: "duration",
|
|
4662
|
+
title: t("stats.duration.timeline"),
|
|
4663
|
+
"data-active": timelineOpen === true ? "true" : undefined,
|
|
4664
|
+
"data-scrub": scrubbing === true ? "true" : undefined,
|
|
4665
|
+
onClick: onToggleTimeline,
|
|
4666
|
+
},
|
|
4667
|
+
t("stats.duration", { v: formatDuration(duration) })
|
|
4668
|
+
)
|
|
4669
|
+
: pill("duration", t("stats.duration", { v: formatDuration(duration) }), t("stats.duration.title"))
|
|
4670
|
+
: null,
|
|
3970
4671
|
pill("tools", t("stats.tools", { n: toolCalls }), t("stats.tools.title")),
|
|
3971
4672
|
commands > 0 ? pill("commands", t("stats.commands", { n: commands }), t("stats.commands.title")) : null,
|
|
3972
4673
|
files > 0 ? pill("files", t("stats.files", { n: files }), t("stats.files.title")) : null,
|
|
@@ -3976,6 +4677,9 @@ function FileTypeIcon(props) {
|
|
|
3976
4677
|
|
|
3977
4678
|
function OverviewContent({ binding, t, variant }) {
|
|
3978
4679
|
const [selection, setSelection] = React.useState("latest");
|
|
4680
|
+
/* timeline scrubbing: a null asOf always means "live/latest state" */
|
|
4681
|
+
const [asOf, setAsOf] = React.useState(null);
|
|
4682
|
+
const [timelineOpen, setTimelineOpen] = React.useState(false);
|
|
3979
4683
|
const sessionWaiting = useSessionWaiting(binding ? binding.sessionId : undefined);
|
|
3980
4684
|
/* Follow quick-nav linkage clicks issued after this instance mounted. */
|
|
3981
4685
|
const navRev = useNavScopeRev();
|
|
@@ -3984,6 +4688,11 @@ function FileTypeIcon(props) {
|
|
|
3984
4688
|
if (navScopeStore.rev === mountNavRev.current) return;
|
|
3985
4689
|
if (typeof navScopeStore.selection === "string") setSelection(navScopeStore.selection);
|
|
3986
4690
|
}, [navRev]);
|
|
4691
|
+
React.useEffect(() => {
|
|
4692
|
+
/* new scope: back to live, but keep the timeline row open so a
|
|
4693
|
+
"上一轮" jump lands ready to drag in the previous turn */
|
|
4694
|
+
setAsOf(null);
|
|
4695
|
+
}, [selection]);
|
|
3987
4696
|
const summary = useSummary(binding, selection);
|
|
3988
4697
|
useBackfill(binding, summary.hasMore);
|
|
3989
4698
|
const goal = useGoal(binding);
|
|
@@ -4012,7 +4721,101 @@ function FileTypeIcon(props) {
|
|
|
4012
4721
|
h("option", { value: "all", key: "all" }, t("scope.all"))
|
|
4013
4722
|
)
|
|
4014
4723
|
: null;
|
|
4015
|
-
|
|
4724
|
+
/* while scrubbing, the stats pills recompute from the steps that already happened */
|
|
4725
|
+
const viewStats = React.useMemo(() => {
|
|
4726
|
+
if (asOf === null || typeof summary.turnStart !== "number") return summary.stats;
|
|
4727
|
+
const list = (Array.isArray(summary.stepsAll) ? summary.stepsAll : []).filter((step) => isRecord(step) && (step.time || 0) <= asOf);
|
|
4728
|
+
return {
|
|
4729
|
+
...summary.stats,
|
|
4730
|
+
durationMs: Math.max(0, asOf - summary.turnStart),
|
|
4731
|
+
toolCalls: list.length,
|
|
4732
|
+
commands: list.filter((step) => step.name === "bash").length,
|
|
4733
|
+
errors: list.filter((step) => step.error === true && (step.endTime || step.time || 0) <= asOf).length,
|
|
4734
|
+
};
|
|
4735
|
+
}, [asOf, summary]);
|
|
4736
|
+
const canTimeline = typeof summary.turnStart === "number" && typeof summary.turnEnd === "number" && summary.turnEnd > summary.turnStart;
|
|
4737
|
+
/* at the 0s point nothing has happened yet in this turn: offer a jump to
|
|
4738
|
+
the previous turn, rendered inside the (otherwise empty) stats bar */
|
|
4739
|
+
const turnNums = Array.isArray(summary.turns) ? summary.turns.filter((value) => Number.isFinite(value)) : [];
|
|
4740
|
+
let prevTurn = null;
|
|
4741
|
+
if (scopeTurn !== null) {
|
|
4742
|
+
const oldest = turnNums.length > 0 ? turnNums[0] : 1;
|
|
4743
|
+
if (scopeTurn - 1 >= oldest) prevTurn = scopeTurn - 1;
|
|
4744
|
+
} else if (scope === "latest" && turnNums.length >= 2) {
|
|
4745
|
+
prevTurn = turnNums[turnNums.length - 2];
|
|
4746
|
+
}
|
|
4747
|
+
const statsBar =
|
|
4748
|
+
cfg.showStats !== false
|
|
4749
|
+
? h(StatsBar, {
|
|
4750
|
+
stats: viewStats,
|
|
4751
|
+
t,
|
|
4752
|
+
canTimeline,
|
|
4753
|
+
timelineOpen,
|
|
4754
|
+
scrubbing: asOf !== null,
|
|
4755
|
+
prevJump: asOf !== null && asOf <= summary.turnStart ? prevTurn : null,
|
|
4756
|
+
onPrevTurn: () => {
|
|
4757
|
+
setSelection(`turn:${prevTurn}`);
|
|
4758
|
+
setAsOf(null);
|
|
4759
|
+
setTimelineOpen(true);
|
|
4760
|
+
},
|
|
4761
|
+
onToggleTimeline: () => {
|
|
4762
|
+
setTimelineOpen((open) => {
|
|
4763
|
+
if (open) setAsOf(null);
|
|
4764
|
+
return !open;
|
|
4765
|
+
});
|
|
4766
|
+
},
|
|
4767
|
+
})
|
|
4768
|
+
: null;
|
|
4769
|
+
const timelineTotal = canTimeline ? summary.turnEnd - summary.turnStart : 0;
|
|
4770
|
+
const timelineValue = asOf === null ? summary.turnEnd : Math.min(Math.max(asOf, summary.turnStart), summary.turnEnd);
|
|
4771
|
+
const timelinePct = timelineTotal > 0 ? ((timelineValue - summary.turnStart) / timelineTotal) * 100 : 100;
|
|
4772
|
+
const timelineBubble = asOf === null ? t("timeline.live") : `${formatElapsed(asOf - summary.turnStart)} · ${formatTime(asOf)}`;
|
|
4773
|
+
const timelineRow =
|
|
4774
|
+
timelineOpen && canTimeline
|
|
4775
|
+
? h(
|
|
4776
|
+
"div",
|
|
4777
|
+
{ className: "dsov-timeline" },
|
|
4778
|
+
/* fixed-width label: the scope's total duration never changes while scrubbing */
|
|
4779
|
+
h("span", { className: "dsov-timelineLabel" }, t("stats.duration", { v: formatDuration(timelineTotal) })),
|
|
4780
|
+
h(
|
|
4781
|
+
"div",
|
|
4782
|
+
{ className: "dsov-timelineTrack" },
|
|
4783
|
+
h("input", {
|
|
4784
|
+
type: "range",
|
|
4785
|
+
className: "dsov-timelineSlider",
|
|
4786
|
+
min: summary.turnStart,
|
|
4787
|
+
max: summary.turnEnd,
|
|
4788
|
+
step: Math.max(1, Math.floor(timelineTotal / 1000)),
|
|
4789
|
+
value: timelineValue,
|
|
4790
|
+
title: timelineBubble,
|
|
4791
|
+
onChange: (event) => {
|
|
4792
|
+
const next = Number(event.target.value);
|
|
4793
|
+
setAsOf(next >= summary.turnEnd ? null : next);
|
|
4794
|
+
},
|
|
4795
|
+
}),
|
|
4796
|
+
h("span", { className: "dsov-timelineBubble", style: { left: `${timelinePct}%` } }, timelineBubble)
|
|
4797
|
+
),
|
|
4798
|
+
h(
|
|
4799
|
+
"button",
|
|
4800
|
+
{ type: "button", className: "dsov-moreSteps", title: t("timeline.reset"), disabled: asOf === null, onClick: () => setAsOf(null) },
|
|
4801
|
+
t("timeline.reset")
|
|
4802
|
+
),
|
|
4803
|
+
h(
|
|
4804
|
+
"button",
|
|
4805
|
+
{
|
|
4806
|
+
type: "button",
|
|
4807
|
+
className: "dsov-moreSteps",
|
|
4808
|
+
title: t("timeline.close"),
|
|
4809
|
+
"aria-label": t("timeline.close"),
|
|
4810
|
+
onClick: () => {
|
|
4811
|
+
setTimelineOpen(false);
|
|
4812
|
+
setAsOf(null);
|
|
4813
|
+
},
|
|
4814
|
+
},
|
|
4815
|
+
"\u00d7"
|
|
4816
|
+
)
|
|
4817
|
+
)
|
|
4818
|
+
: null;
|
|
4016
4819
|
const hasStats = cfg.showStats !== false && isRecord(summary.stats) && (summary.stats.durationMs > 0 || summary.stats.toolCalls > 0);
|
|
4017
4820
|
const barPresent = variant === "panel" ? scopeSelect !== null || hasStats : !(scopeSelect === null && cfg.placement === "conversation" && !hasStats);
|
|
4018
4821
|
/* toggle data-stuck on the sticky wrapper: background only shows while pinned */
|
|
@@ -4069,7 +4872,8 @@ function FileTypeIcon(props) {
|
|
|
4069
4872
|
{ className: "dsov-turnbar" },
|
|
4070
4873
|
statsBar !== null ? h("div", { className: "dsov-turnbarScroll" }, statsBar) : null,
|
|
4071
4874
|
scopeSelect !== null ? h("div", { className: "dsov-turnbarFixed" }, scopeSelect) : null
|
|
4072
|
-
)
|
|
4875
|
+
),
|
|
4876
|
+
timelineRow
|
|
4073
4877
|
)
|
|
4074
4878
|
: null
|
|
4075
4879
|
: scopeSelect === null && cfg.placement === "conversation" && !hasStats
|
|
@@ -4094,26 +4898,40 @@ function FileTypeIcon(props) {
|
|
|
4094
4898
|
t("action.openInSidebar")
|
|
4095
4899
|
)
|
|
4096
4900
|
)
|
|
4097
|
-
)
|
|
4901
|
+
),
|
|
4902
|
+
timelineRow
|
|
4098
4903
|
),
|
|
4099
4904
|
summary.hasMore ? h("div", { className: "dsov-loading" }, t("state.loadingHistory")) : null,
|
|
4100
|
-
cfg.showProgress ? h(ProgressSection, { summary, goal, t, waiting: sessionWaiting, cfg }) : null,
|
|
4101
|
-
cfg.showArtifacts ? h(ArtifactsSection, { summary, t, binding, stats }) : null,
|
|
4102
|
-
cfg.showReferences ? h(ReferencesSection, { summary, t, sessionId }) : null
|
|
4905
|
+
cfg.showProgress ? h(ProgressSection, { summary, goal, t, waiting: sessionWaiting && asOf === null, cfg, asOf }) : null,
|
|
4906
|
+
cfg.showArtifacts ? h(ArtifactsSection, { summary, t, binding, stats, asOf }) : null,
|
|
4907
|
+
cfg.showReferences ? h(ReferencesSection, { summary, t, sessionId, asOf }) : null
|
|
4103
4908
|
);
|
|
4104
4909
|
}
|
|
4105
4910
|
|
|
4106
4911
|
class OverviewBoundary extends React.Component {
|
|
4107
4912
|
constructor(props) {
|
|
4108
4913
|
super(props);
|
|
4109
|
-
this.state = { failed: false };
|
|
4914
|
+
this.state = { failed: false, message: "" };
|
|
4915
|
+
}
|
|
4916
|
+
static getDerivedStateFromError(error) {
|
|
4917
|
+
return { failed: true, message: error && typeof error.message === "string" ? error.message : String(error) };
|
|
4110
4918
|
}
|
|
4111
|
-
|
|
4112
|
-
|
|
4919
|
+
componentDidCatch(error, info) {
|
|
4920
|
+
/* surface the real cause: in the console and inside the fallback panel */
|
|
4921
|
+
try {
|
|
4922
|
+
console.error("[dsh-conversation-overview] overview render failed:", error, info && info.componentStack);
|
|
4923
|
+
} catch {
|
|
4924
|
+
/* console is best-effort */
|
|
4925
|
+
}
|
|
4113
4926
|
}
|
|
4114
4927
|
render() {
|
|
4115
4928
|
if (this.state.failed) {
|
|
4116
|
-
return h(
|
|
4929
|
+
return h(
|
|
4930
|
+
"div",
|
|
4931
|
+
{ className: this.props.variant === "panel" ? "dsov-panel" : "dsov-view" },
|
|
4932
|
+
h("div", { className: "dsov-empty" }, translate("state.error")),
|
|
4933
|
+
this.state.message !== "" ? h("pre", { className: "dsov-renderErr" }, this.state.message) : null
|
|
4934
|
+
);
|
|
4117
4935
|
}
|
|
4118
4936
|
return this.props.children;
|
|
4119
4937
|
}
|
|
@@ -4635,6 +5453,9 @@ function FileTypeIcon(props) {
|
|
|
4635
5453
|
group(
|
|
4636
5454
|
t("settings.interaction"),
|
|
4637
5455
|
h(SettingSelect, { label: t("settings.timeMode"), value: cfg.timeMode, options: timeOptions, onPick: (id) => set("timeMode", id) }),
|
|
5456
|
+
note(t("settings.timeModeNote")),
|
|
5457
|
+
boolRow(t("settings.stepAnchors"), "stepAnchors"),
|
|
5458
|
+
note(t("settings.stepAnchorsNote")),
|
|
4638
5459
|
boolRow(t("settings.todoPromptRow"), "todoPrompt"),
|
|
4639
5460
|
cfg.todoPrompt
|
|
4640
5461
|
? h(SettingTextarea, {
|
|
@@ -4681,6 +5502,7 @@ function FileTypeIcon(props) {
|
|
|
4681
5502
|
),
|
|
4682
5503
|
group(
|
|
4683
5504
|
t("settings.feedbackGroup"),
|
|
5505
|
+
h(VersionRow, { t }),
|
|
4684
5506
|
h(
|
|
4685
5507
|
"a",
|
|
4686
5508
|
{ className: "dsov-set-link", href: FEEDBACK_URL, target: "_blank", rel: "noreferrer", title: FEEDBACK_URL },
|
|
@@ -4783,6 +5605,12 @@ function FileTypeIcon(props) {
|
|
|
4783
5605
|
.dsov-member[data-expandable=false]{cursor:default}
|
|
4784
5606
|
.dsov-member[data-expandable=false]:hover{background:transparent}
|
|
4785
5607
|
.dsov-stepChevron{flex:none;margin-right:-2px}
|
|
5608
|
+
.dsov-compactIcon{color:var(--dsw-alias-label-secondary)}
|
|
5609
|
+
.dsov-todoIconSlot{position:relative;flex:none;display:inline-flex;margin-top:2px}
|
|
5610
|
+
.dsov-todoIconSlot .dsov-todoIcon{margin-top:0}
|
|
5611
|
+
.dsov-todoChevron{position:absolute;inset:0;display:inline-flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .12s ease}
|
|
5612
|
+
.dsov-todo[data-expandable=true]:hover .dsov-todoChevron,.dsov-todo[data-expandable=true][data-open=true] .dsov-todoChevron{opacity:1;pointer-events:auto}
|
|
5613
|
+
.dsov-todo[data-expandable=true]:hover .dsov-todoIconSlot .dsov-todoIcon,.dsov-todo[data-expandable=true][data-open=true] .dsov-todoIconSlot .dsov-todoIcon{visibility:hidden}
|
|
4786
5614
|
.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}
|
|
4787
5615
|
.dsov-row:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 6%,transparent)}
|
|
4788
5616
|
.dsov-rowStatic{cursor:default}
|
|
@@ -4875,6 +5703,32 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
4875
5703
|
.dsov-stats{display:flex;align-items:center;gap:5px;margin-right:auto;flex:none}
|
|
4876
5704
|
.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}
|
|
4877
5705
|
.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)}
|
|
5706
|
+
.dsov-statBtn{cursor:pointer;border:none;margin:0;font-family:inherit;transition:color .15s ease,background-color .15s ease}
|
|
5707
|
+
.dsov-statBtn:hover{color:var(--dsw-alias-label-primary)}
|
|
5708
|
+
.dsov-statBtn[data-active=true]{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent)}
|
|
5709
|
+
.dsov-statBtn[data-scrub=true]{color:var(--dsw-alias-brand-primary)}
|
|
5710
|
+
.dsov-timeline{display:flex;align-items:center;gap:8px;margin-top:6px}
|
|
5711
|
+
.dsov-timelineLabel{flex:none;font-size:10.5px;line-height:16px;color:var(--dsw-alias-label-secondary);white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
5712
|
+
.dsov-prevTurn{color:var(--dsw-alias-brand-primary,#247bbf)}
|
|
5713
|
+
.dsov-timelineTrack{position:relative;flex:1 1 auto;min-width:0;display:flex;align-items:center}
|
|
5714
|
+
.dsov-timelineSlider{flex:1 1 auto;min-width:0;height:18px;margin:0;cursor:pointer;accent-color:var(--dsw-alias-brand-primary)}
|
|
5715
|
+
.dsov-timelineBubble{position:absolute;bottom:19px;left:0;transform:translateX(-50%);z-index:6;padding:1px 7px;border-radius:6px;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);font-size:10.5px;line-height:16px;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .12s ease;font-variant-numeric:tabular-nums}
|
|
5716
|
+
.dsov-timelineTrack:hover .dsov-timelineBubble,.dsov-timelineSlider:focus-visible+.dsov-timelineBubble{opacity:1}
|
|
5717
|
+
.dsov-moreSteps[disabled]{opacity:.45;cursor:default;pointer-events:none}
|
|
5718
|
+
.dsov-newVer{display:inline-flex;align-items:center;flex:none;padding:1px 8px;border-radius:999px;font-size:11px;line-height:18px;text-decoration:none;white-space:nowrap;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) 40%,transparent)}
|
|
5719
|
+
.dsov-newVer:hover{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,transparent)}
|
|
5720
|
+
.dsov-set-ver{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary);padding:3px 0}
|
|
5721
|
+
.dsov-fileMenu{position:fixed;z-index:80;min-width:190px;max-width:260px;max-height:320px;overflow-y:auto;padding:4px;border-radius:9px;background:var(--dsw-alias-bg-layer-1);border:.5px solid var(--dsw-alias-border-l1);box-shadow:0 10px 30px rgba(0,0,0,.30)}
|
|
5722
|
+
.dsov-fileMenuItem{display:flex;align-items:center;gap:7px;width:100%;padding:5px 8px;border:none;margin:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);font-family:inherit;font-size:12px;line-height:18px;text-align:left;cursor:pointer}
|
|
5723
|
+
.dsov-fileMenuItem:hover{background:var(--dsw-alias-bg-layer-2)}
|
|
5724
|
+
.dsov-fileMenuIcon{flex:none;width:16px;height:16px;border-radius:4px;object-fit:contain;background:var(--dsw-alias-bg-layer-2)}
|
|
5725
|
+
.dsov-fileMenuName{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
5726
|
+
.dsov-fileMenuTag{flex:none;font-size:10px;line-height:14px;padding:0 5px;border-radius:999px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2)}
|
|
5727
|
+
.dsov-fileMenuHead{padding:5px 8px 3px;font-size:10.5px;color:var(--dsw-alias-label-secondary)}
|
|
5728
|
+
.dsov-fileMenuSep{height:1px;margin:4px 6px;background:var(--dsw-alias-border-l1)}
|
|
5729
|
+
.dsov-fileMenuHint{padding:5px 8px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}
|
|
5730
|
+
.dsov-fileMenuErr{padding:5px 8px;font-size:11px;color:var(--dsw-alias-state-error-primary,#ff453a)}
|
|
5731
|
+
.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}
|
|
4878
5732
|
.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}
|
|
4879
5733
|
.dsov-turnSelect:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3,var(--dsw-alias-border-l2))}
|
|
4880
5734
|
@keyframes dsov-spin{to{transform:rotate(360deg)}}
|
|
@@ -5226,6 +6080,24 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
5226
6080
|
};
|
|
5227
6081
|
}, "conversation-overview: quick nav link");
|
|
5228
6082
|
|
|
6083
|
+
ctx.effect(
|
|
6084
|
+
() =>
|
|
6085
|
+
ctx.slots.inject(
|
|
6086
|
+
"plugins.detail.badge",
|
|
6087
|
+
() =>
|
|
6088
|
+
ctx.slots.register(
|
|
6089
|
+
{
|
|
6090
|
+
name: "plugins.detail.badge",
|
|
6091
|
+
id: "dsov-new-version",
|
|
6092
|
+
order: 100,
|
|
6093
|
+
locale: NS,
|
|
6094
|
+
},
|
|
6095
|
+
NewVersionBadge
|
|
6096
|
+
)
|
|
6097
|
+
),
|
|
6098
|
+
"conversation-overview: new version badge"
|
|
6099
|
+
);
|
|
6100
|
+
|
|
5229
6101
|
ctx.effect(
|
|
5230
6102
|
() =>
|
|
5231
6103
|
ctx.slots.inject(
|
|
@@ -5271,6 +6143,10 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
5271
6143
|
foldEvent,
|
|
5272
6144
|
foldEntry,
|
|
5273
6145
|
buildState,
|
|
6146
|
+
planTodoAttachments,
|
|
6147
|
+
vikingLabel,
|
|
6148
|
+
vikingRefKind,
|
|
6149
|
+
vikingIsDir,
|
|
5274
6150
|
normalizeSelection,
|
|
5275
6151
|
vikingParentUri,
|
|
5276
6152
|
rememberContextPreview,
|
|
@@ -5291,6 +6167,15 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
5291
6167
|
setFormConfig,
|
|
5292
6168
|
setOverlayConfig,
|
|
5293
6169
|
clearOverlayConfig,
|
|
6170
|
+
compareVersions,
|
|
6171
|
+
ProgressSection,
|
|
6172
|
+
ArtifactsSection,
|
|
6173
|
+
ReferencesSection,
|
|
6174
|
+
StatsBar,
|
|
6175
|
+
ArtifactFileMenu,
|
|
6176
|
+
OverviewGlyph,
|
|
6177
|
+
OverviewContent,
|
|
6178
|
+
OverviewBoundary,
|
|
5294
6179
|
SettingsForm,
|
|
5295
6180
|
SettingsSection,
|
|
5296
6181
|
vikingName,
|