dsh-conversation-overview 1.4.2 → 1.5.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/.scratch/test.mjs +198 -0
- package/README.md +9 -2
- package/client.js +662 -99
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -7,15 +7,22 @@ window.__ModuleLoader__.load({
|
|
|
7
7
|
|
|
8
8
|
/* The chat surface renders Markdown with the harness' own MarkdownText, so a
|
|
9
9
|
document opened by this plugin borrows that exact component when it can: same
|
|
10
|
-
fonts, spacing, code banners and
|
|
11
|
-
|
|
10
|
+
fonts, spacing, code banners, tables and file links as a message in the
|
|
11
|
+
conversation. React.memo/forwardRef components are OBJECTS, not functions, so
|
|
12
|
+
the component check must accept both — a typeof-function test silently fell
|
|
13
|
+
back to the plugin renderer (raw markdown links, wrong type scale). */
|
|
14
|
+
const primitives = (() => {
|
|
12
15
|
try {
|
|
13
|
-
const
|
|
14
|
-
return
|
|
16
|
+
const module = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
17
|
+
return module && typeof module === "object" ? module : null;
|
|
15
18
|
} catch (error) {
|
|
16
19
|
return null;
|
|
17
20
|
}
|
|
18
21
|
})();
|
|
22
|
+
const ChatMarkdown = primitives !== null && primitives.MarkdownText !== undefined && primitives.MarkdownText !== null ? primitives.MarkdownText : null;
|
|
23
|
+
/* The chat wraps its flow in MarkdownDelegateProvider so inline file links render
|
|
24
|
+
as clickable chips and external links open in a new tab; borrow the provider too. */
|
|
25
|
+
const ChatMarkdownProvider = primitives !== null && typeof primitives.MarkdownDelegateProvider === "function" ? primitives.MarkdownDelegateProvider : null;
|
|
19
26
|
|
|
20
27
|
function cx(...parts) {
|
|
21
28
|
return parts.filter((part) => typeof part === "string" && part.length > 0).join(" ");
|
|
@@ -372,6 +379,14 @@ window.__ModuleLoader__.load({
|
|
|
372
379
|
"goal.paused": "已暂停",
|
|
373
380
|
"goal.blocked": "受阻",
|
|
374
381
|
"goal.complete": "已完成",
|
|
382
|
+
"goal.mode": "目标模式",
|
|
383
|
+
"goal.modeTip": "会话级目标:每轮结束后自动续跑,直到目标达成、受阻、暂停或到达轮次上限",
|
|
384
|
+
"goal.modeLine": "每轮结束后自动续跑,直到达成、受阻、暂停或到达轮次上限",
|
|
385
|
+
"goal.rounds": "自动续跑 {n} / 上限 {max} 轮",
|
|
386
|
+
"goal.roundsNoLimit": "已自动续跑 {n} 轮",
|
|
387
|
+
"goal.done": "目标已达成",
|
|
388
|
+
"goal.pausedLine": "目标已暂停,恢复后继续自动续跑",
|
|
389
|
+
"goal.blockedReason": "受阻原因:{reason}",
|
|
375
390
|
"artifact.presented": "交付",
|
|
376
391
|
"artifact.plan": "计划",
|
|
377
392
|
"artifact.memory": "记忆",
|
|
@@ -424,7 +439,9 @@ window.__ModuleLoader__.load({
|
|
|
424
439
|
"role.children": "子代理 {n}",
|
|
425
440
|
"role.childrenToggle": "展开/收起本会话的子代理",
|
|
426
441
|
"role.switchTitle": "切换到该子代理的概览",
|
|
427
|
-
"role.openParent": "
|
|
442
|
+
"role.openParent": "返回主代理",
|
|
443
|
+
"role.openParentHint": "查看主会话",
|
|
444
|
+
"doc.openFile": "在侧边栏打开 {name}",
|
|
428
445
|
"role.openParentTitle": "切换到父会话的概览",
|
|
429
446
|
"role.parentPrefix": "·",
|
|
430
447
|
"state.noSession": "当前没有打开的会话",
|
|
@@ -609,6 +626,14 @@ window.__ModuleLoader__.load({
|
|
|
609
626
|
"goal.paused": "Paused",
|
|
610
627
|
"goal.blocked": "Blocked",
|
|
611
628
|
"goal.complete": "Completed",
|
|
629
|
+
"goal.mode": "Goal mode",
|
|
630
|
+
"goal.modeTip": "Session goal: rounds run automatically until the goal completes, blocks, pauses, or spends its round budget",
|
|
631
|
+
"goal.modeLine": "Rounds run automatically until the goal completes, blocks, pauses, or hits the round cap",
|
|
632
|
+
"goal.rounds": "Auto rounds {n} / max {max}",
|
|
633
|
+
"goal.roundsNoLimit": "{n} auto rounds",
|
|
634
|
+
"goal.done": "Goal reached",
|
|
635
|
+
"goal.pausedLine": "Goal paused — resume to continue the auto rounds",
|
|
636
|
+
"goal.blockedReason": "Blocked: {reason}",
|
|
612
637
|
"artifact.presented": "Delivered",
|
|
613
638
|
"artifact.plan": "Plan",
|
|
614
639
|
"artifact.memory": "Memory",
|
|
@@ -661,7 +686,9 @@ window.__ModuleLoader__.load({
|
|
|
661
686
|
"role.children": "Subagents {n}",
|
|
662
687
|
"role.childrenToggle": "Show/hide this session's subagents",
|
|
663
688
|
"role.switchTitle": "Switch to this subagent's overview",
|
|
664
|
-
"role.openParent": "
|
|
689
|
+
"role.openParent": "Back to main agent",
|
|
690
|
+
"role.openParentHint": "Open main session",
|
|
691
|
+
"doc.openFile": "Open {name} in sidebar",
|
|
665
692
|
"role.openParentTitle": "Switch to the parent session's overview",
|
|
666
693
|
"role.parentPrefix": "·",
|
|
667
694
|
"state.noSession": "No session is open",
|
|
@@ -674,6 +701,29 @@ window.__ModuleLoader__.load({
|
|
|
674
701
|
|
|
675
702
|
let runtime = null; // { ctx, t }
|
|
676
703
|
|
|
704
|
+
/* Cordis' context proxy throws for services this entry never injected, and the
|
|
705
|
+
optional ones here are enhancements (desktop actions, session navigation,
|
|
706
|
+
conversation view switching) that must degrade instead of break. `ctx.get()`
|
|
707
|
+
reads a provided service without the inject requirement. */
|
|
708
|
+
function hostService(name) {
|
|
709
|
+
const ctx = runtime === null || runtime === undefined ? null : runtime.ctx;
|
|
710
|
+
if (ctx === null || ctx === undefined || typeof name !== "string" || name === "") return null;
|
|
711
|
+
try {
|
|
712
|
+
if (typeof ctx.get === "function") {
|
|
713
|
+
const service = ctx.get(name);
|
|
714
|
+
if (service !== undefined && service !== null) return service;
|
|
715
|
+
}
|
|
716
|
+
} catch (_notProvided) {
|
|
717
|
+
/* fall through to the proxy read best effort */
|
|
718
|
+
}
|
|
719
|
+
try {
|
|
720
|
+
const service = ctx[name];
|
|
721
|
+
return service === undefined || service === null ? null : service;
|
|
722
|
+
} catch (_notInjected) {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
677
727
|
function translate(key, params) {
|
|
678
728
|
const t = runtime && runtime.t;
|
|
679
729
|
return typeof t === "function" ? t(key, params) : key;
|
|
@@ -855,6 +905,19 @@ window.__ModuleLoader__.load({
|
|
|
855
905
|
return parts.join("\n");
|
|
856
906
|
}
|
|
857
907
|
|
|
908
|
+
/** Closing reply text: the chat renders each text block as its own paragraph with a
|
|
909
|
+
16px gap, so a document reusing the same body must separate them by a blank line. */
|
|
910
|
+
function conclusionTextOf(content) {
|
|
911
|
+
if (!Array.isArray(content)) return "";
|
|
912
|
+
const parts = [];
|
|
913
|
+
for (const block of content) {
|
|
914
|
+
if (!isRecord(block) || block.type !== "text" || typeof block.text !== "string") continue;
|
|
915
|
+
const text = block.text.trim();
|
|
916
|
+
if (text !== "") parts.push(text);
|
|
917
|
+
}
|
|
918
|
+
return parts.join("\n\n");
|
|
919
|
+
}
|
|
920
|
+
|
|
858
921
|
function collect(source, member, field) {
|
|
859
922
|
const list = isRecord(source) ? source[member] : null;
|
|
860
923
|
if (!Array.isArray(list)) return [];
|
|
@@ -974,20 +1037,60 @@ window.__ModuleLoader__.load({
|
|
|
974
1037
|
/* ------------------------------------------------------------------ */
|
|
975
1038
|
|
|
976
1039
|
const desktopOpenState = { value: null, inflight: null };
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1040
|
+
/* `remote.session` is a cordis ACCESSOR: reading it without a declared inject
|
|
1041
|
+
throws ("cannot get property \"remote.session\" without inject"), and the artifact
|
|
1042
|
+
file menu reads it during render. Mount it as an optional injected dependency
|
|
1043
|
+
once, keep the resolved namespace here, and degrade to no desktop actions. */
|
|
1044
|
+
const desktopRemote = { session: null };
|
|
1045
|
+
|
|
1046
|
+
function mountDesktopRemote(ctx) {
|
|
1047
|
+
/* a fresh mount owns the cache: never inherit a previous host's namespace */
|
|
1048
|
+
desktopRemote.session = null;
|
|
1049
|
+
desktopOpenState.value = null;
|
|
1050
|
+
desktopOpenState.inflight = null;
|
|
1051
|
+
if (ctx === null || ctx === undefined || typeof ctx.inject !== "function") return;
|
|
1052
|
+
const mount = (scoped) => {
|
|
1053
|
+
let session = null;
|
|
1054
|
+
/* the accessor is reachable as a flat property name, and as the nested member
|
|
1055
|
+
of the `remote` service; read whichever this cordis build exposes */
|
|
1056
|
+
try {
|
|
1057
|
+
const direct = scoped["remote.session"];
|
|
1058
|
+
if (isRecord(direct)) session = direct;
|
|
1059
|
+
} catch (_flatNotInjected) {
|
|
1060
|
+
session = null;
|
|
1061
|
+
}
|
|
1062
|
+
if (!isRecord(session)) {
|
|
1063
|
+
try {
|
|
1064
|
+
const remote = scoped.remote;
|
|
1065
|
+
if (isRecord(remote) && isRecord(remote.session)) session = remote.session;
|
|
1066
|
+
} catch (_nestedNotInjected) {
|
|
1067
|
+
session = null;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
desktopRemote.session = isRecord(session) ? session : null;
|
|
1071
|
+
/* reset the cached capability probe so a re-mount re-asks the new host */
|
|
1072
|
+
desktopOpenState.value = null;
|
|
1073
|
+
desktopOpenState.inflight = null;
|
|
1074
|
+
return () => {
|
|
1075
|
+
desktopRemote.session = null;
|
|
1076
|
+
desktopOpenState.value = null;
|
|
1077
|
+
desktopOpenState.inflight = null;
|
|
1078
|
+
};
|
|
1079
|
+
};
|
|
981
1080
|
try {
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
} catch {
|
|
987
|
-
return null;
|
|
1081
|
+
const fiber = ctx.inject(["remote", "remote.session"], mount);
|
|
1082
|
+
if (fiber !== null && fiber !== undefined && typeof fiber.catch === "function") fiber.catch(() => {});
|
|
1083
|
+
} catch (_unavailable) {
|
|
1084
|
+
desktopRemote.session = null;
|
|
988
1085
|
}
|
|
989
1086
|
}
|
|
990
1087
|
|
|
1088
|
+
/** The desktop RPC namespace, or null when the host/profile has no such route. */
|
|
1089
|
+
function hostSessionRemote() {
|
|
1090
|
+
const session = desktopRemote.session;
|
|
1091
|
+
return isRecord(session) ? session : null;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
991
1094
|
function desktopCanOpen() {
|
|
992
1095
|
const remote = hostSessionRemote();
|
|
993
1096
|
if (remote === null || typeof remote.canOpenWorkspacePath !== "function") return Promise.resolve(false);
|
|
@@ -1401,7 +1504,14 @@ window.__ModuleLoader__.load({
|
|
|
1401
1504
|
.replace(
|
|
1402
1505
|
/\[([^\]]+)\]\((https?:[^)\s]+)\)/g,
|
|
1403
1506
|
'<a href="$2" target="_blank" rel="noreferrer noopener">$1</a>'
|
|
1404
|
-
)
|
|
1507
|
+
)
|
|
1508
|
+
/* file destinations (relative or absolute paths): never print the raw
|
|
1509
|
+
`[label](path)` syntax — render the label and let the body open it */
|
|
1510
|
+
.replace(/\[([^\]]+)\]\(([^)#][^)\s]*)\)/g, (match, label, destination) => {
|
|
1511
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(destination) && !/^[a-z]:[\\/]/i.test(destination)) return match;
|
|
1512
|
+
if (!looksLikePath(destination)) return match;
|
|
1513
|
+
return `<span class="dsov-md-fileLink" data-file-path="${destination.replace(/"/g, """)}">${label}</span>`;
|
|
1514
|
+
});
|
|
1405
1515
|
}
|
|
1406
1516
|
|
|
1407
1517
|
/**
|
|
@@ -1760,6 +1870,8 @@ window.__ModuleLoader__.load({
|
|
|
1760
1870
|
subagents: new Map(),
|
|
1761
1871
|
stats: createStats(),
|
|
1762
1872
|
conclusion: null,
|
|
1873
|
+
goal: null,
|
|
1874
|
+
goalRoundSeen: 0,
|
|
1763
1875
|
changed: false,
|
|
1764
1876
|
};
|
|
1765
1877
|
}
|
|
@@ -1840,7 +1952,7 @@ window.__ModuleLoader__.load({
|
|
|
1840
1952
|
}
|
|
1841
1953
|
|
|
1842
1954
|
/** Markdown block that upgrades ```mermaid fences to rendered diagrams after mount. */
|
|
1843
|
-
function MarkdownBody({ text }) {
|
|
1955
|
+
function MarkdownBody({ text, onOpenFile }) {
|
|
1844
1956
|
const ref = React.useRef(null);
|
|
1845
1957
|
const html = React.useMemo(() => mdToHtml(typeof text === "string" ? text : ""), [text]);
|
|
1846
1958
|
React.useEffect(() => {
|
|
@@ -1849,13 +1961,39 @@ window.__ModuleLoader__.load({
|
|
|
1849
1961
|
const timer = setTimeout(() => renderMermaidNodes(node), 0);
|
|
1850
1962
|
return () => clearTimeout(timer);
|
|
1851
1963
|
}, [html]);
|
|
1852
|
-
|
|
1964
|
+
const onClick = (event) => {
|
|
1965
|
+
if (typeof onOpenFile !== "function") return;
|
|
1966
|
+
const target = event.target;
|
|
1967
|
+
const link = target !== null && target !== undefined && typeof target.closest === "function" ? target.closest("[data-file-path]") : null;
|
|
1968
|
+
if (link === null) return;
|
|
1969
|
+
event.preventDefault();
|
|
1970
|
+
onOpenFile(link.getAttribute("data-file-path") || "");
|
|
1971
|
+
};
|
|
1972
|
+
return h("div", { className: "dsov-md", ref, onClick, dangerouslySetInnerHTML: { __html: html } });
|
|
1853
1973
|
}
|
|
1854
1974
|
|
|
1855
|
-
|
|
1975
|
+
/** File paths a turn produced, capped for the document tab's file chips. */
|
|
1976
|
+
function docFilesOf(artifacts) {
|
|
1977
|
+
const files = [];
|
|
1978
|
+
for (const artifact of Array.isArray(artifacts) ? artifacts : []) {
|
|
1979
|
+
if (!isRecord(artifact) || typeof artifact.path !== "string" || artifact.path === "") continue;
|
|
1980
|
+
if (artifact.path.startsWith("viking://") || artifact.path.startsWith("plan:") || artifact.path.startsWith("conclusion:")) continue;
|
|
1981
|
+
if (files.includes(artifact.path) || files.length >= 40) continue;
|
|
1982
|
+
files.push(artifact.path);
|
|
1983
|
+
}
|
|
1984
|
+
return files;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
function rememberContextPreview(sessionId, refKey, title, text, kind, files) {
|
|
1856
1988
|
if (typeof sessionId !== "string" || sessionId === "" || typeof text !== "string" || text === "") return undefined;
|
|
1857
1989
|
const globalKey = `${sessionId}|${refKey}`;
|
|
1858
|
-
const
|
|
1990
|
+
const docFiles = Array.isArray(files) ? files.filter((file) => typeof file === "string" && file !== "").slice(0, 40) : [];
|
|
1991
|
+
const entry = {
|
|
1992
|
+
title: typeof title === "string" && title !== "" ? title : refKey,
|
|
1993
|
+
text: text.length > CONTEXT_TEXT_LIMIT ? `${text.slice(0, CONTEXT_TEXT_LIMIT)}\n…` : text,
|
|
1994
|
+
...(typeof kind === "string" && kind !== "" ? { kind } : {}),
|
|
1995
|
+
...(docFiles.length === 0 ? {} : { files: docFiles }),
|
|
1996
|
+
};
|
|
1859
1997
|
contextPreviews.set(globalKey, entry);
|
|
1860
1998
|
/* Documents (turn conclusion, plan snapshot) open as tabs the host restores
|
|
1861
1999
|
after a reload, and their body lives only here — mirror them into
|
|
@@ -1888,7 +2026,7 @@ window.__ModuleLoader__.load({
|
|
|
1888
2026
|
try {
|
|
1889
2027
|
const kept = readDocPreviewStore().filter((item) => item[0] !== globalKey);
|
|
1890
2028
|
const text = entry.text.length > DOC_PREVIEW_TEXT_LIMIT ? `${entry.text.slice(0, DOC_PREVIEW_TEXT_LIMIT)}\n…` : entry.text;
|
|
1891
|
-
kept.push([globalKey, { title: entry.title, text, kind: entry.kind }]);
|
|
2029
|
+
kept.push([globalKey, { title: entry.title, text, kind: entry.kind, ...(Array.isArray(entry.files) && entry.files.length > 0 ? { files: entry.files.slice(0, 40) } : {}) }]);
|
|
1892
2030
|
window.localStorage.setItem(DOC_PREVIEW_STORE_KEY, JSON.stringify(kept.slice(-DOC_PREVIEW_STORE_LIMIT)));
|
|
1893
2031
|
} catch {
|
|
1894
2032
|
/* storage full or unavailable: the in-memory copy still serves this page */
|
|
@@ -1903,7 +2041,12 @@ window.__ModuleLoader__.load({
|
|
|
1903
2041
|
if (stored === undefined) return null;
|
|
1904
2042
|
const revived = isRecord(stored[1]) && typeof stored[1].text === "string" ? stored[1] : null;
|
|
1905
2043
|
if (revived === null || revived.text === "") return null;
|
|
1906
|
-
const restored = {
|
|
2044
|
+
const restored = {
|
|
2045
|
+
title: typeof revived.title === "string" && revived.title !== "" ? revived.title : globalKey,
|
|
2046
|
+
text: revived.text,
|
|
2047
|
+
...(typeof revived.kind === "string" && revived.kind !== "" ? { kind: revived.kind } : {}),
|
|
2048
|
+
...(Array.isArray(revived.files) ? { files: revived.files.filter((file) => typeof file === "string" && file !== "").slice(0, 40) } : {}),
|
|
2049
|
+
};
|
|
1907
2050
|
contextPreviews.set(globalKey, restored);
|
|
1908
2051
|
return restored;
|
|
1909
2052
|
}
|
|
@@ -2250,47 +2393,115 @@ window.__ModuleLoader__.load({
|
|
|
2250
2393
|
/* Key-step stream: every tool call is one step; the result marks it done. Shown by
|
|
2251
2394
|
the Progress section when a turn has no todo plan of its own. */
|
|
2252
2395
|
const STEP_CAP = 200;
|
|
2396
|
+
/** Steps shown when the progress list is collapsed: the last six *gated* steps
|
|
2397
|
+
(approval rows are gates of a tool call, not steps of their own) plus the
|
|
2398
|
+
approvals that belong to them. Slicing the raw stream put an orphan approval
|
|
2399
|
+
at the top of the window whenever the cut fell between a call and its gate. */
|
|
2400
|
+
const STEP_WINDOW = 6;
|
|
2401
|
+
function recentStepWindow(allSteps) {
|
|
2402
|
+
const source = Array.isArray(allSteps) ? allSteps : [];
|
|
2403
|
+
const picked = new Set();
|
|
2404
|
+
let budget = STEP_WINDOW;
|
|
2405
|
+
for (let index = source.length - 1; index >= 0 && budget > 0; index -= 1) {
|
|
2406
|
+
const step = source[index];
|
|
2407
|
+
if (!isRecord(step) || step.name === "approval") continue;
|
|
2408
|
+
picked.add(step);
|
|
2409
|
+
budget -= 1;
|
|
2410
|
+
}
|
|
2411
|
+
const pickedCalls = new Set();
|
|
2412
|
+
for (const step of picked) if (typeof step.callId === "string" && step.callId !== "") pickedCalls.add(step.callId);
|
|
2413
|
+
return source.filter((step) => {
|
|
2414
|
+
if (picked.has(step)) return true;
|
|
2415
|
+
return isRecord(step) && step.name === "approval" && typeof step.parentCallId === "string" && pickedCalls.has(step.parentCallId);
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2253
2418
|
|
|
2254
2419
|
/** One-line human target for a tool call: file, command, query, url, name… */
|
|
2255
2420
|
/** Team-record key a tool call creates: teammate name / task subject / subagent label. */
|
|
2256
|
-
/**
|
|
2257
|
-
|
|
2258
|
-
function
|
|
2421
|
+
/** Word/bigram tokens of one todo or delegate text: latin words plus CJK bigrams, so
|
|
2422
|
+
「直播域底稿」/「直播域技术底稿分析」 share evidence without full containment. */
|
|
2423
|
+
function planTokens(text) {
|
|
2424
|
+
const tokens = new Set();
|
|
2425
|
+
const source = String(typeof text === "string" ? text : "").toLowerCase();
|
|
2426
|
+
for (const word of source.match(/[a-z][a-z0-9]{2,}/g) || []) tokens.add(word);
|
|
2427
|
+
for (const run of source.match(/[\u4e00-\u9fff]+/g) || []) {
|
|
2428
|
+
if (run.length === 1) tokens.add(run);
|
|
2429
|
+
for (let index = 0; index + 1 < run.length; index += 1) tokens.add(run.slice(index, index + 2));
|
|
2430
|
+
}
|
|
2431
|
+
return tokens;
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
/** Index of the todo that was open (not yet completed) at `time`, from the todo log. */
|
|
2435
|
+
function planActiveAt(todoHistory, time) {
|
|
2436
|
+
if (!Array.isArray(todoHistory) || typeof time !== "number" || time <= 0) return null;
|
|
2437
|
+
let active = null;
|
|
2438
|
+
for (const record of todoHistory) {
|
|
2439
|
+
if (!isRecord(record) || !Array.isArray(record.todos)) continue;
|
|
2440
|
+
if (typeof record.time === "number" && record.time > time) break;
|
|
2441
|
+
active = new Set();
|
|
2442
|
+
record.todos.forEach((todo, index) => {
|
|
2443
|
+
if (isRecord(todo) && todo.status !== "completed") active.add(index);
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
return active;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
/** Match team delegates (members, subagents, tasks) to todos: IDF-weighted token
|
|
2450
|
+
coverage, preferring the todo that was in progress when the delegate appeared, so
|
|
2451
|
+
each delegate renders under the todo it belongs to instead of a flat tail list. */
|
|
2452
|
+
function planTodoAttachments(todos, members, teamTasks, subagents, todoHistory) {
|
|
2259
2453
|
const attachments = new Map();
|
|
2260
2454
|
if (!Array.isArray(todos) || todos.length === 0) return attachments;
|
|
2261
|
-
const
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
if (x.length >= 6 && y.includes(x)) return x.length;
|
|
2276
|
-
if (y.length >= 6 && x.includes(y)) return y.length;
|
|
2277
|
-
return 0;
|
|
2455
|
+
const todoTokens = todos.map((todo) => planTokens(isRecord(todo) ? todo.content : ""));
|
|
2456
|
+
/* rare words are the evidence: 「直播域」 in one todo says more than 「多端」 in four */
|
|
2457
|
+
const docFrequency = new Map();
|
|
2458
|
+
for (const set of todoTokens) for (const token of set) docFrequency.set(token, (docFrequency.get(token) || 0) + 1);
|
|
2459
|
+
const weightOf = (token) => Math.log((todos.length + 1) / ((docFrequency.get(token) || 0) + 0.5)) + 0.2;
|
|
2460
|
+
const coverage = (tokens, index) => {
|
|
2461
|
+
let covered = 0;
|
|
2462
|
+
let total = 0;
|
|
2463
|
+
for (const token of tokens) {
|
|
2464
|
+
const weight = weightOf(token);
|
|
2465
|
+
total += weight;
|
|
2466
|
+
if (todoTokens[index].has(token)) covered += weight;
|
|
2467
|
+
}
|
|
2468
|
+
return total === 0 ? 0 : covered / total;
|
|
2278
2469
|
};
|
|
2279
|
-
const
|
|
2280
|
-
const bestFor = (texts) => {
|
|
2470
|
+
const MATCH_MIN = 0.08;
|
|
2471
|
+
const bestFor = (texts, time) => {
|
|
2472
|
+
const scores = new Array(todos.length).fill(0);
|
|
2281
2473
|
let best = -1;
|
|
2282
|
-
let bestScore =
|
|
2283
|
-
|
|
2284
|
-
const
|
|
2285
|
-
let
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
bestScore
|
|
2289
|
-
|
|
2474
|
+
let bestScore = MATCH_MIN;
|
|
2475
|
+
for (const text of texts) {
|
|
2476
|
+
const tokens = planTokens(text);
|
|
2477
|
+
for (let index = 0; index < todos.length; index += 1) {
|
|
2478
|
+
if (todos[index] === undefined) continue;
|
|
2479
|
+
scores[index] = Math.max(scores[index], coverage(tokens, index));
|
|
2480
|
+
if (scores[index] > bestScore) {
|
|
2481
|
+
bestScore = scores[index];
|
|
2482
|
+
best = index;
|
|
2483
|
+
}
|
|
2290
2484
|
}
|
|
2291
|
-
}
|
|
2485
|
+
}
|
|
2486
|
+
if (best < 0) return -1;
|
|
2487
|
+
/* the delegate appeared while one plan row was being worked on: prefer it */
|
|
2488
|
+
const active = planActiveAt(todoHistory, time);
|
|
2489
|
+
if (active !== null && active.size > 0 && !active.has(best)) {
|
|
2490
|
+
let activeBest = -1;
|
|
2491
|
+
let activeScore = bestScore * 0.6;
|
|
2492
|
+
for (const index of active) {
|
|
2493
|
+
if (todos[index] === undefined) continue;
|
|
2494
|
+
if (scores[index] > activeScore) {
|
|
2495
|
+
activeScore = scores[index];
|
|
2496
|
+
activeBest = index;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
if (activeBest >= 0) return activeBest;
|
|
2500
|
+
}
|
|
2292
2501
|
return best;
|
|
2293
2502
|
};
|
|
2503
|
+
const ownedBy = (id) => teamTasks.filter((task) => isRecord(task) && task.ownerId === id);
|
|
2504
|
+
const timeOf = (record) => (isRecord(record) && typeof record.time === "number" ? record.time : 0);
|
|
2294
2505
|
const attach = (index, item) => {
|
|
2295
2506
|
const list = attachments.get(index);
|
|
2296
2507
|
if (list === undefined) attachments.set(index, [item]);
|
|
@@ -2298,11 +2509,11 @@ window.__ModuleLoader__.load({
|
|
|
2298
2509
|
};
|
|
2299
2510
|
for (const member of members.filter(isRecord)) {
|
|
2300
2511
|
const owned = ownedBy(member.id);
|
|
2301
|
-
const index = bestFor([member.name, member.description, ...owned.map((task) => task.subject)]);
|
|
2512
|
+
const index = bestFor([member.name, member.description, ...owned.map((task) => task.subject)], timeOf(member));
|
|
2302
2513
|
if (index >= 0) attach(index, { kind: "member", record: member, owned });
|
|
2303
2514
|
}
|
|
2304
2515
|
for (const sub of subagents.filter(isRecord)) {
|
|
2305
|
-
const index = bestFor([sub.label]);
|
|
2516
|
+
const index = bestFor([sub.label], timeOf(sub));
|
|
2306
2517
|
if (index >= 0) attach(index, { kind: "sub", record: sub, owned: [] });
|
|
2307
2518
|
}
|
|
2308
2519
|
const memberOwned = new Set();
|
|
@@ -2314,11 +2525,10 @@ window.__ModuleLoader__.load({
|
|
|
2314
2525
|
for (const task of teamTasks.filter(isRecord)) {
|
|
2315
2526
|
if (memberOwned.has(task.id)) continue;
|
|
2316
2527
|
if (typeof task.ownerId === "string" && members.some((member) => isRecord(member) && member.id === task.ownerId)) continue;
|
|
2317
|
-
const index = bestFor([task.subject]);
|
|
2528
|
+
const index = bestFor([task.subject], timeOf(task));
|
|
2318
2529
|
if (index >= 0) attach(index, { kind: "task", record: task, owned: [] });
|
|
2319
2530
|
}
|
|
2320
|
-
const
|
|
2321
|
-
for (const list of attachments.values()) list.sort((left, right) => timeOf(left) - timeOf(right));
|
|
2531
|
+
for (const list of attachments.values()) list.sort((left, right) => timeOf(left.record) - timeOf(right.record));
|
|
2322
2532
|
return attachments;
|
|
2323
2533
|
}
|
|
2324
2534
|
|
|
@@ -2453,7 +2663,7 @@ window.__ModuleLoader__.load({
|
|
|
2453
2663
|
{message:{content}}. Reading only the first left every real turn without a
|
|
2454
2664
|
conclusion document, so the auto-open never fired. */
|
|
2455
2665
|
const body = isRecord(data.message) ? data.message : data;
|
|
2456
|
-
const text = (typeof body.text === "string" && body.text !== "" ? body.text :
|
|
2666
|
+
const text = (typeof body.text === "string" && body.text !== "" ? body.text : conclusionTextOf(body.content)).trim();
|
|
2457
2667
|
if (text === "") return;
|
|
2458
2668
|
const turn = folder.currentTurn >= 1 ? folder.currentTurn : Number.isSafeInteger(data.turn) && data.turn >= 1 ? data.turn : 0;
|
|
2459
2669
|
const bucket = folder.byTurn.get(turn) ?? turnBucket(folder, folder.currentTurn);
|
|
@@ -2499,6 +2709,29 @@ window.__ModuleLoader__.load({
|
|
|
2499
2709
|
touch(folder);
|
|
2500
2710
|
return;
|
|
2501
2711
|
}
|
|
2712
|
+
case "goal/change": {
|
|
2713
|
+
/* A session-level goal: every round is driven automatically until the goal
|
|
2714
|
+
completes, blocks, pauses, or spends its round budget. Folding the durable
|
|
2715
|
+
change lets a historical session show the same banner the live projection does. */
|
|
2716
|
+
if (!isRecord(data)) return;
|
|
2717
|
+
const goal = isRecord(data.goal) ? data.goal : null;
|
|
2718
|
+
if (goal === null || typeof goal.objective !== "string" || goal.objective.trim() === "" || typeof goal.phase !== "string") return;
|
|
2719
|
+
folder.goal = {
|
|
2720
|
+
id: typeof goal.id === "string" ? goal.id : "",
|
|
2721
|
+
revision: typeof goal.revision === "number" ? goal.revision : 0,
|
|
2722
|
+
objective: goal.objective,
|
|
2723
|
+
phase: goal.phase,
|
|
2724
|
+
maxGoalRounds: typeof goal.maxGoalRounds === "number" ? goal.maxGoalRounds : 0,
|
|
2725
|
+
blockedReason:
|
|
2726
|
+
isRecord(goal.blockedReason) && typeof goal.blockedReason.message === "string" && goal.blockedReason.message !== "" ? goal.blockedReason.message : "",
|
|
2727
|
+
roundsStarted: typeof data.roundsStarted === "number" ? data.roundsStarted : 0,
|
|
2728
|
+
createdAt: typeof data.createdAt === "number" ? data.createdAt : 0,
|
|
2729
|
+
updatedAt: typeof data.updatedAt === "number" ? data.updatedAt : 0,
|
|
2730
|
+
turn: folder.currentTurn,
|
|
2731
|
+
};
|
|
2732
|
+
touch(folder);
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2502
2735
|
case "team/member": {
|
|
2503
2736
|
if (!isRecord(data) || !isRecord(data.member)) return;
|
|
2504
2737
|
const member = data.member;
|
|
@@ -2766,6 +2999,16 @@ window.__ModuleLoader__.load({
|
|
|
2766
2999
|
/* a compact checkpoint replaces the event span it summarizes; every other message must append */
|
|
2767
3000
|
if (kind !== "compact-checkpoint" && !isAppendSurfaceEvent(event)) return;
|
|
2768
3001
|
const bucket = turnBucket(folder, folder.currentTurn);
|
|
3002
|
+
if (kind === "goal" && source !== null) {
|
|
3003
|
+
/* an admitted goal round: keep the banner's round counter live even when
|
|
3004
|
+
the projection (or a paused goal) lags the event log */
|
|
3005
|
+
const round = typeof source.round === "number" && Number.isSafeInteger(source.round) && source.round > 0 ? source.round : 0;
|
|
3006
|
+
if (round > folder.goalRoundSeen) folder.goalRoundSeen = round;
|
|
3007
|
+
if (folder.goal !== null && typeof folder.goal === "object" && round > folder.goal.roundsStarted) {
|
|
3008
|
+
folder.goal = { ...folder.goal, roundsStarted: round };
|
|
3009
|
+
touch(folder);
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
2769
3012
|
if (kind === "user") {
|
|
2770
3013
|
const mentions = mentionsOfUserText(textOf(data.content));
|
|
2771
3014
|
for (const label of mentions.sessions) {
|
|
@@ -2884,7 +3127,7 @@ window.__ModuleLoader__.load({
|
|
|
2884
3127
|
let conclusion = null;
|
|
2885
3128
|
if (rawConclusion !== null && typeof rawConclusion.text === "string" && rawConclusion.text !== "") {
|
|
2886
3129
|
const title = translate("artifact.conclusionTurn", { n: rawConclusion.turn });
|
|
2887
|
-
const previewKey = rememberContextPreview(folder.sessionId, `conclusion|${rawConclusion.turn}`, title, rawConclusion.text, "conclusion");
|
|
3130
|
+
const previewKey = rememberContextPreview(folder.sessionId, `conclusion|${rawConclusion.turn}`, title, rawConclusion.text, "conclusion", docFilesOf(artifacts));
|
|
2888
3131
|
conclusion = {
|
|
2889
3132
|
path: `conclusion:${rawConclusion.turn}`,
|
|
2890
3133
|
kind: "conclusion",
|
|
@@ -2934,7 +3177,7 @@ window.__ModuleLoader__.load({
|
|
|
2934
3177
|
/* team anchor steps (spawn/task-create/subagent) stay pinned above the recent window
|
|
2935
3178
|
so member/task rows can nest under their creating step */
|
|
2936
3179
|
const allSteps = Array.isArray(source.steps) ? source.steps : [];
|
|
2937
|
-
const recentSteps = allSteps
|
|
3180
|
+
const recentSteps = recentStepWindow(allSteps);
|
|
2938
3181
|
const anchorCandidates = [...(Array.isArray(source.anchors) ? source.anchors : []), ...allSteps].filter(
|
|
2939
3182
|
(step, index, list) =>
|
|
2940
3183
|
isRecord(step) && typeof step.teamKey === "string" && step.milestone !== true && list.indexOf(step) === index && recentSteps.indexOf(step) === -1
|
|
@@ -2943,9 +3186,14 @@ window.__ModuleLoader__.load({
|
|
|
2943
3186
|
/* milestone steps (compaction, approvals) ride the anchor lane for retention only;
|
|
2944
3187
|
whether they pin above the window is a display choice (cfg.stepAnchors) */
|
|
2945
3188
|
const milestoneSteps = (Array.isArray(source.anchors) ? source.anchors : []).filter((step) => isRecord(step) && step.milestone === true);
|
|
3189
|
+
const goalState =
|
|
3190
|
+
isRecord(folder.goal) && typeof folder.goal.objective === "string"
|
|
3191
|
+
? { ...folder.goal, roundsStarted: Math.max(typeof folder.goal.roundsStarted === "number" ? folder.goal.roundsStarted : 0, folder.goalRoundSeen || 0) }
|
|
3192
|
+
: null;
|
|
2946
3193
|
return {
|
|
2947
3194
|
todos,
|
|
2948
3195
|
todosTime,
|
|
3196
|
+
goal: goalState,
|
|
2949
3197
|
artifacts,
|
|
2950
3198
|
references,
|
|
2951
3199
|
steps: [...anchorSteps, ...recentSteps],
|
|
@@ -2968,7 +3216,7 @@ window.__ModuleLoader__.load({
|
|
|
2968
3216
|
};
|
|
2969
3217
|
}
|
|
2970
3218
|
|
|
2971
|
-
const EMPTY_STATE = { todos: null, todosTime: 0, artifacts: [], references: [], steps: [], stepsAll: [], team: { members: [], tasks: [], subagents: [] }, stats: { durationMs: 0, toolCalls: 0, commands: 0, errors: 0, files: 0 }, turnStart: null, turnEnd: null, milestones: [], todoHistory: [], changeTurns: [], hasMore: false, localeRev: 0, turns: [], latestTurn: null, scope: "latest", scopeTurns: 1, conclusion: null };
|
|
3219
|
+
const EMPTY_STATE = { todos: null, todosTime: 0, goal: null, artifacts: [], references: [], steps: [], stepsAll: [], team: { members: [], tasks: [], subagents: [] }, stats: { durationMs: 0, toolCalls: 0, commands: 0, errors: 0, files: 0 }, turnStart: null, turnEnd: null, milestones: [], todoHistory: [], changeTurns: [], hasMore: false, localeRev: 0, turns: [], latestTurn: null, scope: "latest", scopeTurns: 1, conclusion: null };
|
|
2972
3220
|
|
|
2973
3221
|
function createSummaryStore(binding) {
|
|
2974
3222
|
const source = binding.eventSource;
|
|
@@ -3128,7 +3376,49 @@ window.__ModuleLoader__.load({
|
|
|
3128
3376
|
);
|
|
3129
3377
|
if (!isRecord(value)) return null;
|
|
3130
3378
|
const goal = value.goal;
|
|
3131
|
-
|
|
3379
|
+
if (!isRecord(goal) || typeof goal.objective !== "string" || goal.objective.trim() === "") return null;
|
|
3380
|
+
return {
|
|
3381
|
+
...goal,
|
|
3382
|
+
roundsStarted: typeof value.roundsStarted === "number" ? value.roundsStarted : 0,
|
|
3383
|
+
createdAt: typeof value.createdAt === "number" ? value.createdAt : 0,
|
|
3384
|
+
updatedAt: typeof value.updatedAt === "number" ? value.updatedAt : 0,
|
|
3385
|
+
};
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
/** Merge the folded goal (history) with the live projection (current truth): the
|
|
3389
|
+
projection owns phase/rounds while the event log keeps a historical session's
|
|
3390
|
+
banner readable. */
|
|
3391
|
+
function mergeGoalState(folded, live) {
|
|
3392
|
+
const recorded = isRecord(folded) && typeof folded.objective === "string" && folded.objective.trim() !== "" ? folded : null;
|
|
3393
|
+
const projected = isRecord(live) && typeof live.objective === "string" && live.objective.trim() !== "" ? live : null;
|
|
3394
|
+
if (recorded === null && projected === null) return null;
|
|
3395
|
+
const number = (value) => (typeof value === "number" && Number.isFinite(value) ? value : 0);
|
|
3396
|
+
const source = projected !== null ? projected : recorded;
|
|
3397
|
+
const roundsStarted = Math.max(
|
|
3398
|
+
number(projected === null ? undefined : projected.roundsStarted),
|
|
3399
|
+
number(recorded === null ? undefined : recorded.roundsStarted)
|
|
3400
|
+
);
|
|
3401
|
+
const maxGoalRounds = Math.max(
|
|
3402
|
+
number(projected === null ? undefined : projected.maxGoalRounds),
|
|
3403
|
+
number(recorded === null ? undefined : recorded.maxGoalRounds)
|
|
3404
|
+
);
|
|
3405
|
+
const blockedReason =
|
|
3406
|
+
projected !== null && typeof projected.blockedReason === "string" && projected.blockedReason !== ""
|
|
3407
|
+
? projected.blockedReason
|
|
3408
|
+
: recorded !== null && typeof recorded.blockedReason === "string"
|
|
3409
|
+
? recorded.blockedReason
|
|
3410
|
+
: "";
|
|
3411
|
+
return {
|
|
3412
|
+
id: typeof source.id === "string" ? source.id : "",
|
|
3413
|
+
objective: source.objective,
|
|
3414
|
+
phase: typeof source.phase === "string" ? source.phase : "",
|
|
3415
|
+
maxGoalRounds,
|
|
3416
|
+
roundsStarted,
|
|
3417
|
+
blockedReason,
|
|
3418
|
+
createdAt: Math.max(number(projected === null ? undefined : projected.createdAt), number(recorded === null ? undefined : recorded.createdAt)),
|
|
3419
|
+
updatedAt: Math.max(number(projected === null ? undefined : projected.updatedAt), number(recorded === null ? undefined : recorded.updatedAt)),
|
|
3420
|
+
turn: recorded !== null && typeof recorded.turn === "number" ? recorded.turn : 0,
|
|
3421
|
+
};
|
|
3132
3422
|
}
|
|
3133
3423
|
|
|
3134
3424
|
/** Backfills older history. Returns true once several consecutive loads made no
|
|
@@ -4392,7 +4682,7 @@ function FileTypeIcon(props) {
|
|
|
4392
4682
|
? stepsRaw.filter(stepAt).map(stepClamped)
|
|
4393
4683
|
: (() => {
|
|
4394
4684
|
const visible = stepsAllRaw.filter(stepAt);
|
|
4395
|
-
const recent = visible
|
|
4685
|
+
const recent = recentStepWindow(visible);
|
|
4396
4686
|
const anchors = stepsRaw.filter(
|
|
4397
4687
|
(step) => stepAt(step) && typeof step.teamKey === "string" && step.milestone !== true && recent.indexOf(step) === -1
|
|
4398
4688
|
);
|
|
@@ -4616,7 +4906,7 @@ function FileTypeIcon(props) {
|
|
|
4616
4906
|
const consumedTasks = new Set();
|
|
4617
4907
|
const consumedSubs = new Set();
|
|
4618
4908
|
/* delegates matched to a todo render nested under it and skip the flat tail */
|
|
4619
|
-
const todoAttachments = hasTodos && hasTeam ? planTodoAttachments(todos, members, teamTasks, subagents) : new Map();
|
|
4909
|
+
const todoAttachments = hasTodos && hasTeam ? planTodoAttachments(todos, members, teamTasks, subagents, todoHistory) : new Map();
|
|
4620
4910
|
for (const list of todoAttachments.values()) {
|
|
4621
4911
|
for (const item of list) {
|
|
4622
4912
|
if (item.kind === "member") {
|
|
@@ -4985,14 +5275,37 @@ function FileTypeIcon(props) {
|
|
|
4985
5275
|
};
|
|
4986
5276
|
const kids = [];
|
|
4987
5277
|
if (goal) {
|
|
5278
|
+
/* A session goal is a mode, not a todo: rounds are re-driven automatically.
|
|
5279
|
+
The banner says so, carries the phase and the round budget, and names the
|
|
5280
|
+
blocker/pause instead of leaving a bare objective line. */
|
|
4988
5281
|
const phase = typeof goal.phase === "string" ? goal.phase : "";
|
|
5282
|
+
const roundsStarted = typeof goal.roundsStarted === "number" && goal.roundsStarted > 0 ? goal.roundsStarted : 0;
|
|
5283
|
+
const maxRounds = typeof goal.maxGoalRounds === "number" && goal.maxGoalRounds > 0 ? goal.maxGoalRounds : 0;
|
|
5284
|
+
const blockedReason = phase === "blocked" && typeof goal.blockedReason === "string" ? goal.blockedReason.trim() : "";
|
|
5285
|
+
const phaseLabel = GOAL_PHASE_KEYS[phase] ? t(GOAL_PHASE_KEYS[phase]) : phase || "—";
|
|
5286
|
+
const fact = maxRounds > 0 ? t("goal.rounds", { n: roundsStarted, max: maxRounds }) : roundsStarted > 0 ? t("goal.roundsNoLimit", { n: roundsStarted }) : "";
|
|
5287
|
+
const metaLine =
|
|
5288
|
+
phase === "complete"
|
|
5289
|
+
? t("goal.done")
|
|
5290
|
+
: phase === "blocked"
|
|
5291
|
+
? (blockedReason !== "" ? t("goal.blockedReason", { reason: blockedReason }) : t("goal.modeTip"))
|
|
5292
|
+
: phase === "paused"
|
|
5293
|
+
? t("goal.pausedLine")
|
|
5294
|
+
: t("goal.modeLine");
|
|
4989
5295
|
kids.push(
|
|
4990
5296
|
h(
|
|
4991
5297
|
"div",
|
|
4992
|
-
{ className: "dsov-goal", key: "goal" },
|
|
4993
|
-
h(
|
|
4994
|
-
|
|
4995
|
-
|
|
5298
|
+
{ className: "dsov-goal", key: "goal", "data-phase": phase === "" ? "unknown" : phase, title: t("goal.modeTip") },
|
|
5299
|
+
h(
|
|
5300
|
+
"div",
|
|
5301
|
+
{ className: "dsov-goalHead" },
|
|
5302
|
+
h("span", { className: "dsov-goalIcon" }, h(IconTarget, { size: 13 })),
|
|
5303
|
+
h("span", { className: "dsov-goalMode" }, t("goal.mode")),
|
|
5304
|
+
h("span", { className: "dsov-goalPhase", "data-tone": GOAL_PHASE_TONES[phase] ?? "idle" }, phaseLabel),
|
|
5305
|
+
fact === "" ? null : h("span", { className: "dsov-goalFacts" }, fact)
|
|
5306
|
+
),
|
|
5307
|
+
h("div", { className: "dsov-goalObjective" }, typeof goal.objective === "string" ? goal.objective : ""),
|
|
5308
|
+
h("div", { className: "dsov-goalMeta", "data-phase": phase === "" ? "unknown" : phase }, metaLine)
|
|
4996
5309
|
)
|
|
4997
5310
|
);
|
|
4998
5311
|
}
|
|
@@ -5939,28 +6252,73 @@ function FileTypeIcon(props) {
|
|
|
5939
6252
|
|
|
5940
6253
|
/** Whether the workspace service can move the main conversation to another session. */
|
|
5941
6254
|
function canNavigateSession() {
|
|
6255
|
+
const workspace = hostService("uiWorkspace");
|
|
6256
|
+
return isRecord(workspace) && typeof workspace.openSession === "function";
|
|
6257
|
+
}
|
|
6258
|
+
|
|
6259
|
+
function openSessionById(sessionId) {
|
|
6260
|
+
if (typeof sessionId !== "string" || sessionId === "") return false;
|
|
6261
|
+
const workspace = hostService("uiWorkspace");
|
|
6262
|
+
if (!isRecord(workspace) || typeof workspace.openSession !== "function") return false;
|
|
5942
6263
|
try {
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
} catch {
|
|
6264
|
+
workspace.openSession(sessionId);
|
|
6265
|
+
return true;
|
|
6266
|
+
} catch (_navigationRejected) {
|
|
5947
6267
|
return false;
|
|
5948
6268
|
}
|
|
5949
6269
|
}
|
|
5950
6270
|
|
|
5951
|
-
|
|
6271
|
+
/* The host remembers the active conversation view per session under
|
|
6272
|
+
`<conversation store key>.<sessionId>`, and restores it when the session is
|
|
6273
|
+
revealed. Writing the overview there is what makes a subagent link land on
|
|
6274
|
+
its 概览 tab instead of whatever view that session last showed. */
|
|
6275
|
+
function preferSessionOverview(sessionId) {
|
|
5952
6276
|
if (typeof sessionId !== "string" || sessionId === "") return false;
|
|
5953
6277
|
try {
|
|
5954
|
-
|
|
5955
|
-
const
|
|
5956
|
-
|
|
5957
|
-
|
|
6278
|
+
if (typeof window === "undefined" || window.localStorage === undefined) return false;
|
|
6279
|
+
const key = `dsh.conversation.${sessionId}`;
|
|
6280
|
+
const raw = window.localStorage.getItem(key);
|
|
6281
|
+
const stored = raw === null ? {} : JSON.parse(raw);
|
|
6282
|
+
const next = isRecord(stored) ? { ...stored, view: VIEW_ID } : { view: VIEW_ID };
|
|
6283
|
+
window.localStorage.setItem(key, JSON.stringify(next));
|
|
5958
6284
|
return true;
|
|
5959
|
-
} catch {
|
|
6285
|
+
} catch (_unavailable) {
|
|
5960
6286
|
return false;
|
|
5961
6287
|
}
|
|
5962
6288
|
}
|
|
5963
6289
|
|
|
6290
|
+
/** Ask the running conversation to activate a view once that session is bound. */
|
|
6291
|
+
function activateSessionView(sessionId, view, attempts) {
|
|
6292
|
+
const sessions = hostService("sessions");
|
|
6293
|
+
const conversationService = hostService("uiConversation");
|
|
6294
|
+
try {
|
|
6295
|
+
const binding = sessions !== null && typeof sessions.binding === "function" ? sessions.binding(sessionId) : undefined;
|
|
6296
|
+
if (binding !== undefined && conversationService !== null && typeof conversationService.binding === "function") {
|
|
6297
|
+
const conversation = conversationService.binding(sessionId);
|
|
6298
|
+
if (conversation !== null && conversation !== undefined && typeof conversation.activate === "function") {
|
|
6299
|
+
conversation.activate(view);
|
|
6300
|
+
return true;
|
|
6301
|
+
}
|
|
6302
|
+
}
|
|
6303
|
+
} catch (_notBoundYet) {
|
|
6304
|
+
/* the session is still materializing: retry below */
|
|
6305
|
+
}
|
|
6306
|
+
if (attempts <= 0 || typeof setTimeout !== "function") return false;
|
|
6307
|
+
setTimeout(() => {
|
|
6308
|
+
activateSessionView(sessionId, view, attempts - 1);
|
|
6309
|
+
}, 80);
|
|
6310
|
+
return false;
|
|
6311
|
+
}
|
|
6312
|
+
|
|
6313
|
+
/** Open one session and land on its 概览 (main agent ⇄ subagent navigation). */
|
|
6314
|
+
function openSessionOverview(sessionId) {
|
|
6315
|
+
if (typeof sessionId !== "string" || sessionId === "") return false;
|
|
6316
|
+
preferSessionOverview(sessionId);
|
|
6317
|
+
const opened = openSessionById(sessionId);
|
|
6318
|
+
activateSessionView(sessionId, VIEW_ID, 25);
|
|
6319
|
+
return opened;
|
|
6320
|
+
}
|
|
6321
|
+
|
|
5964
6322
|
/* Whether this session's visible scope actually involved subagents: agent-team
|
|
5965
6323
|
members, subagent-catalog records, or real subagent/spawn tool calls. A
|
|
5966
6324
|
context-compaction milestone also carries a teamKey, so teamKey alone is not
|
|
@@ -6013,7 +6371,7 @@ function FileTypeIcon(props) {
|
|
|
6013
6371
|
key: child.id,
|
|
6014
6372
|
title: canSwitch ? t("role.switchTitle") : child.title,
|
|
6015
6373
|
disabled: !canSwitch,
|
|
6016
|
-
onClick: canSwitch ? () =>
|
|
6374
|
+
onClick: canSwitch ? () => openSessionOverview(child.id) : undefined,
|
|
6017
6375
|
},
|
|
6018
6376
|
h("span", { className: "dsov-roleDot", "data-running": child.running ? "true" : "false" }),
|
|
6019
6377
|
h("span", { className: "dsov-roleItemTitle" }, child.title)
|
|
@@ -6030,11 +6388,11 @@ function FileTypeIcon(props) {
|
|
|
6030
6388
|
canSwitch
|
|
6031
6389
|
? h(
|
|
6032
6390
|
"button",
|
|
6033
|
-
{ type: "button", className: "dsov-roleBtn", "data-link": "true", title: t("role.openParentTitle"), onClick: () =>
|
|
6391
|
+
{ type: "button", className: "dsov-roleBtn", "data-link": "true", title: t("role.openParentTitle"), onClick: () => openSessionOverview(role.parentId) },
|
|
6034
6392
|
t("role.openParent"),
|
|
6035
6393
|
h("span", { className: "dsov-roleParent" }, parentName)
|
|
6036
6394
|
)
|
|
6037
|
-
: h("span", { className: "dsov-roleHint" }, t("role.
|
|
6395
|
+
: h("span", { className: "dsov-roleHint" }, t("role.openParentHint"), h("span", { className: "dsov-roleParent" }, parentName)),
|
|
6038
6396
|
children,
|
|
6039
6397
|
list
|
|
6040
6398
|
);
|
|
@@ -6070,7 +6428,44 @@ function FileTypeIcon(props) {
|
|
|
6070
6428
|
host loader silently no-ops (another loader holds the lock) or a page fails. */
|
|
6071
6429
|
const [historyRetry, setHistoryRetry] = React.useState(0);
|
|
6072
6430
|
const historyStalled = useBackfill(binding, summary.hasMore, historyRetry);
|
|
6073
|
-
|
|
6431
|
+
/* A freshly opened conversation can mount this surface before the host installs
|
|
6432
|
+
its event window: the window then reads empty WITH hasMore=false, so the
|
|
6433
|
+
backfill loop has nothing to do and the panel stays blank even though the
|
|
6434
|
+
session already has content. Ask for the first page once in that situation. */
|
|
6435
|
+
const hydratedFor = React.useRef(null);
|
|
6436
|
+
React.useEffect(() => {
|
|
6437
|
+
const id = binding === null || binding === undefined ? undefined : binding.sessionId;
|
|
6438
|
+
if (typeof id !== "string" || id === "" || hydratedFor.current === id) return;
|
|
6439
|
+
const source = binding.eventSource;
|
|
6440
|
+
let window = null;
|
|
6441
|
+
try {
|
|
6442
|
+
window = source !== null && source !== undefined && typeof source.getSnapshot === "function" ? source.getSnapshot() : null;
|
|
6443
|
+
} catch (_unavailable) {
|
|
6444
|
+
window = null;
|
|
6445
|
+
}
|
|
6446
|
+
const size = isRecord(window) && Array.isArray(window.entries) ? window.entries.length : 0;
|
|
6447
|
+
if (size > 0) {
|
|
6448
|
+
hydratedFor.current = id;
|
|
6449
|
+
return;
|
|
6450
|
+
}
|
|
6451
|
+
const row = sessionListRow(id);
|
|
6452
|
+
if (row === null || row.blank === true) return;
|
|
6453
|
+
hydratedFor.current = id;
|
|
6454
|
+
const session = binding.session;
|
|
6455
|
+
if (session === null || session === undefined) return;
|
|
6456
|
+
const load = async () => {
|
|
6457
|
+
try {
|
|
6458
|
+
if (typeof session.loadThrough === "function") await session.loadThrough(0);
|
|
6459
|
+
else if (typeof session.loadOlder === "function") await session.loadOlder();
|
|
6460
|
+
} catch (_pageFailed) {
|
|
6461
|
+
/* the 「重新加载」 escape hatch covers a permanent failure */
|
|
6462
|
+
}
|
|
6463
|
+
};
|
|
6464
|
+
load();
|
|
6465
|
+
}, [binding, summary]);
|
|
6466
|
+
const goalProjection = useGoal(binding);
|
|
6467
|
+
/* the live projection wins; a historical session falls back to the folded goal */
|
|
6468
|
+
const goal = React.useMemo(() => mergeGoalState(summary.goal, goalProjection), [summary.goal, goalProjection]);
|
|
6074
6469
|
const stats = useArtifactStats(binding, summary.changeTurns);
|
|
6075
6470
|
const cfg = useConfig();
|
|
6076
6471
|
const sessionId = binding ? binding.sessionId : undefined;
|
|
@@ -6402,9 +6797,58 @@ function FileTypeIcon(props) {
|
|
|
6402
6797
|
}
|
|
6403
6798
|
}
|
|
6404
6799
|
|
|
6800
|
+
/** The host's reactive session roster (`sessions.list`), or null when unavailable. */
|
|
6801
|
+
function sessionListStore() {
|
|
6802
|
+
try {
|
|
6803
|
+
const ctx = runtime === null || runtime === undefined ? null : runtime.ctx;
|
|
6804
|
+
const sessions = ctx === null || ctx === undefined ? undefined : ctx.sessions;
|
|
6805
|
+
const list = sessions === undefined || sessions === null ? undefined : sessions.list;
|
|
6806
|
+
return list !== undefined && list !== null && typeof list.subscribe === "function" && typeof list.getSnapshot === "function" ? list : null;
|
|
6807
|
+
} catch (_unavailable) {
|
|
6808
|
+
return null;
|
|
6809
|
+
}
|
|
6810
|
+
}
|
|
6811
|
+
|
|
6812
|
+
/** One roster row for a session id (blank/running/origin/title), or null. */
|
|
6813
|
+
function sessionListRow(sessionId) {
|
|
6814
|
+
const list = sessionListStore();
|
|
6815
|
+
if (list === null || typeof sessionId !== "string" || sessionId === "") return null;
|
|
6816
|
+
try {
|
|
6817
|
+
const snapshot = list.getSnapshot();
|
|
6818
|
+
const byId = isRecord(snapshot) && isRecord(snapshot.byId) ? snapshot.byId : {};
|
|
6819
|
+
return isRecord(byId[sessionId]) ? byId[sessionId] : null;
|
|
6820
|
+
} catch (_unavailable) {
|
|
6821
|
+
return null;
|
|
6822
|
+
}
|
|
6823
|
+
}
|
|
6824
|
+
|
|
6825
|
+
/** Bind the surface to its session, re-resolving while the host materializes a
|
|
6826
|
+
just-opened conversation: a brand-new chat mounts the overview before the
|
|
6827
|
+
session scope exists, and without this the panel stuck on 「未选择会话」. */
|
|
6828
|
+
function useSessionBinding(sessionId) {
|
|
6829
|
+
const list = sessionListStore();
|
|
6830
|
+
const subscribe = React.useCallback((onChange) => (list === null ? () => {} : list.subscribe(onChange)), [list]);
|
|
6831
|
+
const readRoster = React.useCallback(() => (list === null ? null : list.getSnapshot()), [list]);
|
|
6832
|
+
React.useSyncExternalStore(subscribe, readRoster, readRoster);
|
|
6833
|
+
const binding = resolveBinding(sessionId);
|
|
6834
|
+
const [attempt, setAttempt] = React.useState(0);
|
|
6835
|
+
React.useEffect(() => {
|
|
6836
|
+
if (binding !== null || typeof sessionId !== "string" || sessionId === "") return undefined;
|
|
6837
|
+
if (typeof setInterval !== "function") return undefined;
|
|
6838
|
+
let tries = 0;
|
|
6839
|
+
const timer = setInterval(() => {
|
|
6840
|
+
tries += 1;
|
|
6841
|
+
if (resolveBinding(sessionId) !== null) setAttempt((value) => value + 1);
|
|
6842
|
+
if (tries >= 30) clearInterval(timer);
|
|
6843
|
+
}, 100);
|
|
6844
|
+
return () => clearInterval(timer);
|
|
6845
|
+
}, [binding, sessionId, attempt]);
|
|
6846
|
+
return binding;
|
|
6847
|
+
}
|
|
6848
|
+
|
|
6405
6849
|
function OverviewSurface(props, variant) {
|
|
6406
6850
|
const t = typeof props.t === "function" ? props.t : translate;
|
|
6407
|
-
const binding =
|
|
6851
|
+
const binding = useSessionBinding(props.sessionId);
|
|
6408
6852
|
if (!binding) {
|
|
6409
6853
|
return h("div", { className: variant === "panel" ? "dsov-panel" : "dsov-view" }, h("div", { className: "dsov-empty" }, t("state.noSession")));
|
|
6410
6854
|
}
|
|
@@ -6652,6 +7096,68 @@ function FileTypeIcon(props) {
|
|
|
6652
7096
|
return blocks.length > 0 ? h("div", { className: "dsov-vikingFields" }, ...blocks) : null;
|
|
6653
7097
|
}
|
|
6654
7098
|
|
|
7099
|
+
/** Chat parity: a document's inline file link opens the same sidebar file preview. */
|
|
7100
|
+
function openDocLinkedFile(sessionId, path) {
|
|
7101
|
+
if (typeof path !== "string" || path.trim() === "") return;
|
|
7102
|
+
openSessionFile(typeof sessionId === "string" ? sessionId : undefined, path.trim());
|
|
7103
|
+
}
|
|
7104
|
+
|
|
7105
|
+
/** Chat parity: external links in a document open in a new tab, like the chat's. */
|
|
7106
|
+
function openDocExternalLink(url) {
|
|
7107
|
+
if (typeof url !== "string" || !/^https?:/iu.test(url)) return;
|
|
7108
|
+
try {
|
|
7109
|
+
if (typeof window !== "undefined" && typeof window.open === "function") window.open(url, "_blank", "noopener,noreferrer");
|
|
7110
|
+
} catch (_popupBlocked) {
|
|
7111
|
+
/* a blocked popup must not break rendering */
|
|
7112
|
+
}
|
|
7113
|
+
}
|
|
7114
|
+
|
|
7115
|
+
/** Chat parity: inline `path` code resolves to a file chip when the turn produced it.
|
|
7116
|
+
Mirrors the host resolver: an exact path or a unique basename match, never a guess
|
|
7117
|
+
between two same-named files. */
|
|
7118
|
+
function docFileMentionResolver(entry, sessionId, translator) {
|
|
7119
|
+
const files = isRecord(entry) && Array.isArray(entry.files) ? entry.files.filter((file) => typeof file === "string" && file !== "") : [];
|
|
7120
|
+
if (files.length === 0) return undefined;
|
|
7121
|
+
const t = typeof translator === "function" ? translator : translate;
|
|
7122
|
+
const baseName = (path) => path.replace(/\\/g, "/").split("/").pop();
|
|
7123
|
+
return {
|
|
7124
|
+
resolve: (value) => {
|
|
7125
|
+
const text = String(typeof value === "string" ? value : "").trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
7126
|
+
if (text === "") return undefined;
|
|
7127
|
+
const matches = new Set();
|
|
7128
|
+
for (const file of files) if (file === text || baseName(file) === text) matches.add(file);
|
|
7129
|
+
if (matches.size !== 1) return undefined;
|
|
7130
|
+
const path = matches.values().next().value;
|
|
7131
|
+
return {
|
|
7132
|
+
title: path,
|
|
7133
|
+
label: t("doc.openFile", { name: baseName(path) }),
|
|
7134
|
+
open: () => {
|
|
7135
|
+
if (typeof sessionId === "string" && sessionId !== "") openSessionFile(sessionId, path);
|
|
7136
|
+
},
|
|
7137
|
+
};
|
|
7138
|
+
},
|
|
7139
|
+
};
|
|
7140
|
+
}
|
|
7141
|
+
|
|
7142
|
+
/** Chat parity: local image destinations resolve through the host file API. */
|
|
7143
|
+
function docPathImages() {
|
|
7144
|
+
if (typeof document === "undefined" || typeof document.baseURI !== "string") return undefined;
|
|
7145
|
+
const base = document.baseURI;
|
|
7146
|
+
if (!/^https?:/u.test(base) && !base.startsWith("dsh-app://app/")) return undefined;
|
|
7147
|
+
return {
|
|
7148
|
+
resolve: (value) => {
|
|
7149
|
+
try {
|
|
7150
|
+
const path = decodeURIComponent(String(typeof value === "string" ? value : "").split(/[?#]/u)[0] ?? "");
|
|
7151
|
+
if (path === "" || !isAbsoluteWorkspacePath(path) || /^[/\\]{2}/u.test(path)) return undefined;
|
|
7152
|
+
if (/[\u0000-\u001f\u007f]/u.test(path)) return undefined;
|
|
7153
|
+
return new URL(`api/file?path=${encodeURIComponent(path)}`, base).href;
|
|
7154
|
+
} catch (_bad) {
|
|
7155
|
+
return undefined;
|
|
7156
|
+
}
|
|
7157
|
+
},
|
|
7158
|
+
};
|
|
7159
|
+
}
|
|
7160
|
+
|
|
6655
7161
|
/** Sidebar tab body previewing one captured context injection snapshot as markdown. */
|
|
6656
7162
|
function ContextPreviewBody(props) {
|
|
6657
7163
|
const t = typeof props.t === "function" ? props.t : translate;
|
|
@@ -6662,19 +7168,37 @@ function FileTypeIcon(props) {
|
|
|
6662
7168
|
return h("div", { className: "dsov-viking" }, h("div", { className: "dsov-vikingError" }, t("context.unavailable")));
|
|
6663
7169
|
}
|
|
6664
7170
|
if (entry.kind === "conclusion") {
|
|
6665
|
-
/* Just the turn's closing answer, rendered
|
|
6666
|
-
|
|
6667
|
-
|
|
7171
|
+
/* Just the turn's closing answer, rendered through the chat's own Markdown
|
|
7172
|
+
component inside the chat's own typographic wrapper and delegate provider,
|
|
7173
|
+
so the document page reads exactly like the message on the left: same font
|
|
7174
|
+
ladder, same line height, same image resolution, same file chips and same
|
|
7175
|
+
external links. */
|
|
7176
|
+
const sessionSeparator = typeof key === "string" ? key.indexOf("|") : -1;
|
|
7177
|
+
const docSessionId = sessionSeparator > 0 ? key.slice(0, sessionSeparator) : undefined;
|
|
7178
|
+
const docFileMentions = docFileMentionResolver(entry, docSessionId, t);
|
|
7179
|
+
const docImages = docPathImages();
|
|
7180
|
+
const markdown = ChatMarkdown
|
|
7181
|
+
? h(ChatMarkdown, {
|
|
7182
|
+
text: entry.text,
|
|
7183
|
+
labels: chatMarkdownLabels(t),
|
|
7184
|
+
...(docFileMentions === undefined ? {} : { fileMentions: docFileMentions }),
|
|
7185
|
+
...(docImages === undefined ? {} : { pathImages: docImages }),
|
|
7186
|
+
})
|
|
7187
|
+
: h(MarkdownBody, { text: entry.text, onOpenFile: (path) => openDocLinkedFile(docSessionId, path) });
|
|
7188
|
+
/* the chat's own link delegates: inline file links become sidebar chips,
|
|
7189
|
+
external links open in a new tab */
|
|
7190
|
+
const content =
|
|
7191
|
+
ChatMarkdown !== null && ChatMarkdownProvider !== null
|
|
7192
|
+
? h(ChatMarkdownProvider, {
|
|
7193
|
+
openFile: (path) => openDocLinkedFile(docSessionId, path),
|
|
7194
|
+
openExternalLink: openDocExternalLink,
|
|
7195
|
+
children: markdown,
|
|
7196
|
+
})
|
|
7197
|
+
: markdown;
|
|
6668
7198
|
return h(
|
|
6669
7199
|
"section",
|
|
6670
7200
|
{ className: "dsov-docPage", "data-doc-preview": "conclusion", "aria-label": entry.title },
|
|
6671
|
-
h(
|
|
6672
|
-
"div",
|
|
6673
|
-
{ className: "dsov-docDocument" },
|
|
6674
|
-
ChatMarkdown
|
|
6675
|
-
? h(ChatMarkdown, { text: entry.text, labels: chatMarkdownLabels(t) })
|
|
6676
|
-
: h(MarkdownBody, { text: entry.text })
|
|
6677
|
-
)
|
|
7201
|
+
h("div", { className: "dsov-docDocument" }, h("div", { className: "dsov-docChat" }, content))
|
|
6678
7202
|
);
|
|
6679
7203
|
}
|
|
6680
7204
|
return h(
|
|
@@ -7090,9 +7614,29 @@ function FileTypeIcon(props) {
|
|
|
7090
7614
|
.dsov-secBody{max-height:320px;overflow-y:auto;overflow-x:hidden;margin:0 -12px 0 -8px;padding:0 12px 0 8px}
|
|
7091
7615
|
.dsov-section[data-full=true] .dsov-secBody{max-height:none;overflow:visible;margin:0;padding:0}
|
|
7092
7616
|
.dsov-loading{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
7093
|
-
.dsov-goal{display:flex;
|
|
7094
|
-
.dsov-
|
|
7095
|
-
.dsov-
|
|
7617
|
+
.dsov-goal{display:flex;flex-direction:column;gap:3px;box-sizing:border-box;border:.5px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 32%,transparent);border-radius:10px;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 6%,transparent);padding:7px 9px 8px;color:var(--dsw-alias-label-primary)}
|
|
7618
|
+
.dsov-goal[data-phase=paused]{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#ff9f0a) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#ff9f0a) 7%,transparent)}
|
|
7619
|
+
.dsov-goal[data-phase=blocked]{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary,#ff453a) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#ff453a) 7%,transparent)}
|
|
7620
|
+
.dsov-goal[data-phase=complete]{border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary,#2ea36a) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#2ea36a) 7%,transparent)}
|
|
7621
|
+
.dsov-goalHead{display:flex;align-items:center;gap:5px;min-width:0}
|
|
7622
|
+
.dsov-goalIcon{flex:none;display:inline-flex;color:var(--dsw-alias-brand-primary)}
|
|
7623
|
+
.dsov-goal[data-phase=paused] .dsov-goalIcon{color:var(--dsw-alias-state-warn-primary,#ff9f0a)}
|
|
7624
|
+
.dsov-goal[data-phase=blocked] .dsov-goalIcon{color:var(--dsw-alias-state-error-primary,#ff453a)}
|
|
7625
|
+
.dsov-goal[data-phase=complete] .dsov-goalIcon{color:var(--dsw-alias-state-success-primary,#2ea36a)}
|
|
7626
|
+
.dsov-goalMode{flex:none;font-size:10.5px;line-height:16px;font-weight:600;color:var(--dsw-alias-brand-primary)}
|
|
7627
|
+
.dsov-goal[data-phase=paused] .dsov-goalMode{color:var(--dsw-alias-state-warn-primary,#ff9f0a)}
|
|
7628
|
+
.dsov-goal[data-phase=blocked] .dsov-goalMode{color:var(--dsw-alias-state-error-primary,#ff453a)}
|
|
7629
|
+
.dsov-goal[data-phase=complete] .dsov-goalMode{color:var(--dsw-alias-state-success-primary,#2ea36a)}
|
|
7630
|
+
.dsov-goalPhase{flex:none;font-size:9.5px;line-height:14px;padding:0 5px;border-radius:4px;border:.5px solid transparent}
|
|
7631
|
+
.dsov-goalPhase[data-tone=brand]{color:var(--dsw-alias-brand-primary);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 45%,transparent)}
|
|
7632
|
+
.dsov-goalPhase[data-tone=warn]{color:var(--dsw-alias-state-warn-primary,#ff9f0a);border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#ff9f0a) 45%,transparent)}
|
|
7633
|
+
.dsov-goalPhase[data-tone=error]{color:var(--dsw-alias-state-error-primary,#ff453a);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary,#ff453a) 45%,transparent)}
|
|
7634
|
+
.dsov-goalPhase[data-tone=success]{color:var(--dsw-alias-state-success-primary,#2ea36a);border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary,#2ea36a) 45%,transparent)}
|
|
7635
|
+
.dsov-goalFacts{margin-left:auto;flex:none;font-size:9.5px;line-height:14px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
7636
|
+
.dsov-goalObjective{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}
|
|
7637
|
+
.dsov-goalMeta{font-size:10px;line-height:1.45;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}
|
|
7638
|
+
.dsov-goalMeta[data-phase=blocked]{color:var(--dsw-alias-state-error-primary,#ff453a)}
|
|
7639
|
+
.dsov-goalMeta[data-phase=complete]{color:var(--dsw-alias-state-success-primary,#2ea36a)}
|
|
7096
7640
|
.dsov-badge{flex:none;font-size:10.5px;line-height:17px;padding:0 7px;border-radius:999px;border:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap}
|
|
7097
7641
|
.dsov-badge[data-tone=brand]{color:var(--dsw-alias-brand-primary);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 45%,transparent)}
|
|
7098
7642
|
.dsov-badge[data-tone=success]{color:var(--dsw-alias-state-success-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 45%,transparent)}
|
|
@@ -7243,6 +7787,7 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
7243
7787
|
.dsov-viking{box-sizing:border-box;height:100%;min-height:0;overflow-y:auto;padding:14px 16px 28px;display:flex;flex-direction:column;gap:10px}
|
|
7244
7788
|
.dsov-docPage{box-sizing:border-box;height:100%;padding:20px 24px 40px;position:relative;overflow:auto}
|
|
7245
7789
|
.dsov-docDocument{min-width:0;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere;font-size:14px;line-height:1.75}
|
|
7790
|
+
.dsov-docChat{display:flex;flex-direction:column;font-size:var(--dsh-content-font-size,14px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-primary)}
|
|
7246
7791
|
.dsov-docTitleIcon{flex:none}
|
|
7247
7792
|
@media (width<=767px){.dsov-docPage{padding:16px 18px 32px}}
|
|
7248
7793
|
.dsov-vikingHead{display:flex;align-items:flex-start;gap:8px}
|
|
@@ -7414,6 +7959,7 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
7414
7959
|
.dsov-md-h{font-size:14px;font-weight:600;margin:14px 0 4px;color:var(--dsw-alias-label-primary)}
|
|
7415
7960
|
.dsov-md p{margin:4px 0}
|
|
7416
7961
|
.dsov-md-list{margin:4px 0;padding-left:20px}
|
|
7962
|
+
.dsov-md-fileLink{color:var(--dsw-alias-link,var(--dsw-alias-brand-primary));cursor:pointer;text-decoration:none;border-bottom:.5px dotted currentColor}
|
|
7417
7963
|
.dsov-md-code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;background:var(--dsw-alias-bg-layer-2);border-radius:4px;padding:1px 4px}
|
|
7418
7964
|
.dsov-md-pre{background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l1);border-radius:8px;padding:10px 12px;overflow-x:auto;margin:6px 0}
|
|
7419
7965
|
.dsov-md-pre code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:1.5;background:none;padding:0}
|
|
@@ -7443,6 +7989,7 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
7443
7989
|
|
|
7444
7990
|
function apply(ctx) {
|
|
7445
7991
|
runtime = { ctx, t: ctx.locale.bind(NS) };
|
|
7992
|
+
mountDesktopRemote(ctx);
|
|
7446
7993
|
|
|
7447
7994
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "conversation-overview: dictionaries");
|
|
7448
7995
|
|
|
@@ -7804,6 +8351,10 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
7804
8351
|
foldEntry,
|
|
7805
8352
|
buildState,
|
|
7806
8353
|
planTodoAttachments,
|
|
8354
|
+
mergeGoalState,
|
|
8355
|
+
planTokens,
|
|
8356
|
+
planActiveAt,
|
|
8357
|
+
recentStepWindow,
|
|
7807
8358
|
vikingLabel,
|
|
7808
8359
|
vikingRefKind,
|
|
7809
8360
|
vikingIsDir,
|
|
@@ -7839,19 +8390,31 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
7839
8390
|
ContextPreviewBody,
|
|
7840
8391
|
SettingsForm,
|
|
7841
8392
|
chatMarkdown: ChatMarkdown,
|
|
8393
|
+
chatMarkdownProvider: ChatMarkdownProvider,
|
|
8394
|
+
openDocLinkedFile,
|
|
8395
|
+
openDocExternalLink,
|
|
7842
8396
|
chatMarkdownLabels,
|
|
7843
8397
|
StatsBar,
|
|
7844
8398
|
ArtifactFileMenu,
|
|
7845
8399
|
OverviewGlyph,
|
|
7846
8400
|
OverviewContent,
|
|
8401
|
+
OverviewSurface,
|
|
8402
|
+
SessionRoleBar,
|
|
7847
8403
|
MarkdownBody,
|
|
7848
8404
|
mdToHtml,
|
|
7849
8405
|
Section,
|
|
7850
8406
|
sectionIntroProps,
|
|
7851
|
-
SessionRoleBar,
|
|
7852
8407
|
sessionRoleOf,
|
|
7853
8408
|
canNavigateSession,
|
|
7854
8409
|
openSessionById,
|
|
8410
|
+
openSessionOverview,
|
|
8411
|
+
hostService,
|
|
8412
|
+
hostSessionRemote,
|
|
8413
|
+
mountDesktopRemote,
|
|
8414
|
+
desktopCanOpen,
|
|
8415
|
+
sessionListRow,
|
|
8416
|
+
sessionListStore,
|
|
8417
|
+
preferSessionOverview,
|
|
7855
8418
|
planTitleOf,
|
|
7856
8419
|
openNativePlan,
|
|
7857
8420
|
documentAddress,
|