dsh-conversation-overview 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client.js +155 -47
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -1565,20 +1565,23 @@ window.__ModuleLoader__.load({
|
|
|
1565
1565
|
function openArtifact(binding, artifact, stats) {
|
|
1566
1566
|
const sessionId = binding ? binding.sessionId : undefined;
|
|
1567
1567
|
if (artifact.kind === "conclusion") {
|
|
1568
|
+
/* The plugin's own tab is restorable: its body is mirrored into localStorage,
|
|
1569
|
+
while the host's plan-review page keeps its payload in tab params only and
|
|
1570
|
+
turns into the expired "计划" page after a reload. */
|
|
1571
|
+
if (typeof artifact.previewKey === "string" && artifact.previewKey !== "" && openContextPreview(artifact.previewKey)) return;
|
|
1568
1572
|
const entry = typeof artifact.previewKey === "string" ? readContextPreview(artifact.previewKey) : null;
|
|
1569
1573
|
const docKey = `conclusion:${typeof artifact.turn === "number" ? artifact.turn : artifact.previewKey}`;
|
|
1570
|
-
|
|
1571
|
-
if (typeof artifact.previewKey === "string" && artifact.previewKey !== "") openContextPreview(artifact.previewKey);
|
|
1574
|
+
openNativeDocument(sessionId, docKey, artifact.title, entry === null ? undefined : entry.text);
|
|
1572
1575
|
return;
|
|
1573
1576
|
}
|
|
1574
1577
|
if (artifact.plan === true) {
|
|
1575
1578
|
if (openNativePlan(sessionId, artifact.planCallId)) return;
|
|
1576
1579
|
/* host plan viewer unavailable (plugin disabled, no sidebar): the document
|
|
1577
1580
|
page over the same snapshot, then the plugin's own tab */
|
|
1581
|
+
if (typeof artifact.previewKey === "string" && artifact.previewKey !== "" && openContextPreview(artifact.previewKey)) return;
|
|
1578
1582
|
const entry = typeof artifact.previewKey === "string" ? readContextPreview(artifact.previewKey) : null;
|
|
1579
1583
|
const docKey = `plan:${typeof artifact.planCallId === "string" && artifact.planCallId !== "" ? artifact.planCallId : artifact.previewKey}`;
|
|
1580
|
-
|
|
1581
|
-
if (typeof artifact.previewKey === "string" && artifact.previewKey !== "") openContextPreview(artifact.previewKey);
|
|
1584
|
+
openNativeDocument(sessionId, docKey, entry !== null ? entry.title : artifact.planTitle, entry === null ? undefined : entry.text);
|
|
1582
1585
|
return;
|
|
1583
1586
|
}
|
|
1584
1587
|
if (typeof artifact.path === "string" && artifact.path.startsWith("viking://")) {
|
|
@@ -1852,7 +1855,12 @@ window.__ModuleLoader__.load({
|
|
|
1852
1855
|
function rememberContextPreview(sessionId, refKey, title, text, kind) {
|
|
1853
1856
|
if (typeof sessionId !== "string" || sessionId === "" || typeof text !== "string" || text === "") return undefined;
|
|
1854
1857
|
const globalKey = `${sessionId}|${refKey}`;
|
|
1855
|
-
|
|
1858
|
+
const entry = { title: typeof title === "string" && title !== "" ? title : refKey, text: text.length > CONTEXT_TEXT_LIMIT ? `${text.slice(0, CONTEXT_TEXT_LIMIT)}\n…` : text, ...(typeof kind === "string" && kind !== "" ? { kind } : {}) };
|
|
1859
|
+
contextPreviews.set(globalKey, entry);
|
|
1860
|
+
/* Documents (turn conclusion, plan snapshot) open as tabs the host restores
|
|
1861
|
+
after a reload, and their body lives only here — mirror them into
|
|
1862
|
+
localStorage so a refreshed tab does not render the "unavailable" state. */
|
|
1863
|
+
if (entry.kind === "conclusion" || entry.kind === "plan") persistDocPreview(globalKey, entry);
|
|
1856
1864
|
while (contextPreviews.size > CONTEXT_PREVIEW_LIMIT) {
|
|
1857
1865
|
const oldest = contextPreviews.keys().next();
|
|
1858
1866
|
if (oldest.done === true) break;
|
|
@@ -1861,9 +1869,43 @@ window.__ModuleLoader__.load({
|
|
|
1861
1869
|
return globalKey;
|
|
1862
1870
|
}
|
|
1863
1871
|
|
|
1872
|
+
const DOC_PREVIEW_STORE_KEY = "dsh.conversationOverview.docPreviews";
|
|
1873
|
+
const DOC_PREVIEW_STORE_LIMIT = 8;
|
|
1874
|
+
const DOC_PREVIEW_TEXT_LIMIT = 20000;
|
|
1875
|
+
|
|
1876
|
+
function readDocPreviewStore() {
|
|
1877
|
+
try {
|
|
1878
|
+
const raw = window.localStorage.getItem(DOC_PREVIEW_STORE_KEY);
|
|
1879
|
+
const parsed = typeof raw === "string" ? JSON.parse(raw) : null;
|
|
1880
|
+
if (!Array.isArray(parsed)) return [];
|
|
1881
|
+
return parsed.filter((item) => Array.isArray(item) && typeof item[0] === "string" && isRecord(item[1]));
|
|
1882
|
+
} catch {
|
|
1883
|
+
return [];
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
function persistDocPreview(globalKey, entry) {
|
|
1888
|
+
try {
|
|
1889
|
+
const kept = readDocPreviewStore().filter((item) => item[0] !== globalKey);
|
|
1890
|
+
const text = entry.text.length > DOC_PREVIEW_TEXT_LIMIT ? `${entry.text.slice(0, DOC_PREVIEW_TEXT_LIMIT)}\n…` : entry.text;
|
|
1891
|
+
kept.push([globalKey, { title: entry.title, text, kind: entry.kind }]);
|
|
1892
|
+
window.localStorage.setItem(DOC_PREVIEW_STORE_KEY, JSON.stringify(kept.slice(-DOC_PREVIEW_STORE_LIMIT)));
|
|
1893
|
+
} catch {
|
|
1894
|
+
/* storage full or unavailable: the in-memory copy still serves this page */
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1864
1898
|
function readContextPreview(globalKey) {
|
|
1865
1899
|
const entry = contextPreviews.get(globalKey);
|
|
1866
|
-
|
|
1900
|
+
if (entry !== undefined) return entry;
|
|
1901
|
+
/* a tab restored after a reload finds its body here */
|
|
1902
|
+
const stored = readDocPreviewStore().find((item) => item[0] === globalKey);
|
|
1903
|
+
if (stored === undefined) return null;
|
|
1904
|
+
const revived = isRecord(stored[1]) && typeof stored[1].text === "string" ? stored[1] : null;
|
|
1905
|
+
if (revived === null || revived.text === "") return null;
|
|
1906
|
+
const restored = { title: typeof revived.title === "string" && revived.title !== "" ? revived.title : globalKey, text: revived.text, ...(typeof revived.kind === "string" && revived.kind !== "" ? { kind: revived.kind } : {}) };
|
|
1907
|
+
contextPreviews.set(globalKey, restored);
|
|
1908
|
+
return restored;
|
|
1867
1909
|
}
|
|
1868
1910
|
|
|
1869
1911
|
const CONTEXT_ADDRESS_PREFIX = "dsh-resource://overview-context/";
|
|
@@ -1895,8 +1937,13 @@ window.__ModuleLoader__.load({
|
|
|
1895
1937
|
function openContextPreview(globalKey) {
|
|
1896
1938
|
const sidebar = contextSidebar();
|
|
1897
1939
|
if (sidebar === null) return false;
|
|
1898
|
-
|
|
1899
|
-
|
|
1940
|
+
try {
|
|
1941
|
+
sidebar.openResource(contextPreviewAddress(globalKey));
|
|
1942
|
+
return true;
|
|
1943
|
+
} catch (_unavailable) {
|
|
1944
|
+
/* no tab type claims our address (plugin half not mounted): caller falls back */
|
|
1945
|
+
return false;
|
|
1946
|
+
}
|
|
1900
1947
|
}
|
|
1901
1948
|
|
|
1902
1949
|
/* An overview can be mounted twice (inline card + sidebar tab); only the first
|
|
@@ -1910,6 +1957,27 @@ window.__ModuleLoader__.load({
|
|
|
1910
1957
|
return true;
|
|
1911
1958
|
}
|
|
1912
1959
|
|
|
1960
|
+
/* Auto-open decision for the turn-conclusion tab. Guard shape:
|
|
1961
|
+
{ ready, opened:Set, sawRunning }. A conclusion only earns a tab when this
|
|
1962
|
+
overview watched the session run at some point after mounting — history that
|
|
1963
|
+
is replayed on mount/refresh (even when it arrives in several flushes, after
|
|
1964
|
+
the first paint) must never pop a tab. Returns the key to open, else null. */
|
|
1965
|
+
function conclusionAutoOpenKey(guard, conclusion, sessionId, sawRunning) {
|
|
1966
|
+
if (!isRecord(guard) || !(guard.opened instanceof Set)) return null;
|
|
1967
|
+
const record = isRecord(conclusion) ? conclusion : null;
|
|
1968
|
+
const key = `${sessionId}|${record === null ? "" : record.turn}`;
|
|
1969
|
+
if (guard.ready !== true) {
|
|
1970
|
+
/* first paint: whatever already exists is history, never re-opened */
|
|
1971
|
+
guard.ready = true;
|
|
1972
|
+
if (record !== null) guard.opened.add(key);
|
|
1973
|
+
return null;
|
|
1974
|
+
}
|
|
1975
|
+
if (record === null || record.ended !== true || guard.opened.has(key)) return null;
|
|
1976
|
+
guard.opened.add(key);
|
|
1977
|
+
if (sawRunning !== true) return null;
|
|
1978
|
+
return key;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1913
1981
|
/* Quick-nav linkage: a click on the built-in turn rail broadcasts a scope request
|
|
1914
1982
|
that every mounted overview follows (module-level, cross-instance). */
|
|
1915
1983
|
const navScopeStore = { rev: 0, selection: null, listeners: new Set() };
|
|
@@ -2038,6 +2106,26 @@ window.__ModuleLoader__.load({
|
|
|
2038
2106
|
return React.useSyncExternalStore(subscribe, read);
|
|
2039
2107
|
}
|
|
2040
2108
|
|
|
2109
|
+
/** True while the session is actively producing a turn (running flag of sessionStatus). */
|
|
2110
|
+
function useSessionRunning(sessionId) {
|
|
2111
|
+
const source = readSessionStatusSource();
|
|
2112
|
+
const subscribe = React.useCallback(
|
|
2113
|
+
(onChange) => (source !== undefined && typeof source.subscribe === "function" ? source.subscribe(onChange) : () => {}),
|
|
2114
|
+
[source]
|
|
2115
|
+
);
|
|
2116
|
+
const read = React.useCallback(() => {
|
|
2117
|
+
if (source === undefined || typeof source.getSnapshot !== "function" || typeof sessionId !== "string") return false;
|
|
2118
|
+
try {
|
|
2119
|
+
const snapshot = source.getSnapshot();
|
|
2120
|
+
const status = snapshot !== null && snapshot !== undefined && typeof snapshot.get === "function" ? snapshot.get(sessionId) : undefined;
|
|
2121
|
+
return status !== null && status !== undefined && status.running === true;
|
|
2122
|
+
} catch {
|
|
2123
|
+
return false;
|
|
2124
|
+
}
|
|
2125
|
+
}, [source, sessionId]);
|
|
2126
|
+
return React.useSyncExternalStore(subscribe, read);
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2041
2129
|
function touch(folder) {
|
|
2042
2130
|
folder.changed = true;
|
|
2043
2131
|
}
|
|
@@ -5788,25 +5876,34 @@ function FileTypeIcon(props) {
|
|
|
5788
5876
|
}
|
|
5789
5877
|
const byId = isRecord(snapshot) && isRecord(snapshot.byId) ? snapshot.byId : {};
|
|
5790
5878
|
const self = isRecord(byId[sessionId]) ? byId[sessionId] : null;
|
|
5791
|
-
const
|
|
5792
|
-
|
|
5793
|
-
?
|
|
5794
|
-
:
|
|
5795
|
-
?
|
|
5879
|
+
const entryTitle = (entry, fallback) =>
|
|
5880
|
+
entry !== null && typeof entry.title === "string" && entry.title !== ""
|
|
5881
|
+
? entry.title
|
|
5882
|
+
: entry !== null && typeof entry.displayTitle === "string" && entry.displayTitle !== ""
|
|
5883
|
+
? entry.displayTitle
|
|
5884
|
+
: fallback;
|
|
5885
|
+
const entryParent = (entry) =>
|
|
5886
|
+
typeof entry.parentSessionId === "string" && entry.parentSessionId !== ""
|
|
5887
|
+
? entry.parentSessionId
|
|
5888
|
+
: typeof entry.parentId === "string" && entry.parentId !== ""
|
|
5889
|
+
? entry.parentId
|
|
5796
5890
|
: null;
|
|
5891
|
+
/* `origin: "subagent"` is the durable marker. A branched/forked conversation
|
|
5892
|
+
also carries a parentSessionId (and the host even hands out a subagent
|
|
5893
|
+
address for it), but it is a main session with lineage — not a subagent. */
|
|
5894
|
+
const isSubagent = self !== null && self.origin === "subagent";
|
|
5895
|
+
const parentId = isSubagent
|
|
5896
|
+
? (isRecord(address) && typeof address.parentSessionId === "string" && address.parentSessionId !== "" ? address.parentSessionId : entryParent(self))
|
|
5897
|
+
: null;
|
|
5797
5898
|
const parent = parentId !== null && isRecord(byId[parentId]) ? byId[parentId] : null;
|
|
5798
5899
|
const ids = isRecord(snapshot) && Array.isArray(snapshot.ids) ? snapshot.ids : [];
|
|
5799
5900
|
const children = [];
|
|
5800
5901
|
const seen = new Set();
|
|
5801
5902
|
for (const id of ids) {
|
|
5802
5903
|
const entry = isRecord(byId[id]) ? byId[id] : null;
|
|
5803
|
-
if (entry === null || entry.
|
|
5904
|
+
if (entry === null || entry.origin !== "subagent" || entryParent(entry) !== sessionId) continue;
|
|
5804
5905
|
seen.add(id);
|
|
5805
|
-
children.push({
|
|
5806
|
-
id,
|
|
5807
|
-
title: typeof entry.displayTitle === "string" && entry.displayTitle !== "" ? entry.displayTitle : id,
|
|
5808
|
-
running: entry.running === true,
|
|
5809
|
-
});
|
|
5906
|
+
children.push({ id, title: entryTitle(entry, id), running: entry.running === true });
|
|
5810
5907
|
}
|
|
5811
5908
|
/* A child can exist in the parent's catalog projection before the list is
|
|
5812
5909
|
projected; the catalog is the same metadata the host itself reads. */
|
|
@@ -5834,7 +5931,7 @@ function FileTypeIcon(props) {
|
|
|
5834
5931
|
return {
|
|
5835
5932
|
kind: "sub",
|
|
5836
5933
|
parentId,
|
|
5837
|
-
parentTitle: parent !== null
|
|
5934
|
+
parentTitle: parent !== null ? entryTitle(parent, "") : "",
|
|
5838
5935
|
mode: isRecord(address) && typeof address.mode === "string" ? address.mode : "",
|
|
5839
5936
|
children,
|
|
5840
5937
|
};
|
|
@@ -5864,6 +5961,22 @@ function FileTypeIcon(props) {
|
|
|
5864
5961
|
}
|
|
5865
5962
|
}
|
|
5866
5963
|
|
|
5964
|
+
/* Whether this session's visible scope actually involved subagents: agent-team
|
|
5965
|
+
members, subagent-catalog records, or real subagent/spawn tool calls. A
|
|
5966
|
+
context-compaction milestone also carries a teamKey, so teamKey alone is not
|
|
5967
|
+
evidence (that used to announce a subagent in single-agent conversations). */
|
|
5968
|
+
function subagentWorkInScope(summary) {
|
|
5969
|
+
const team = isRecord(summary) && isRecord(summary.team) ? summary.team : null;
|
|
5970
|
+
if (team !== null && Array.isArray(team.members) && team.members.length > 0) return true;
|
|
5971
|
+
if (team !== null && Array.isArray(team.subagents) && team.subagents.length > 0) return true;
|
|
5972
|
+
const steps = isRecord(summary) && Array.isArray(summary.stepsAll) ? summary.stepsAll : [];
|
|
5973
|
+
return steps.some(
|
|
5974
|
+
(step) =>
|
|
5975
|
+
isRecord(step) &&
|
|
5976
|
+
(step.name === "subagent" || step.name === "subagent_fork" || step.name === "spawn_teammate" || step.name === "team_task_create")
|
|
5977
|
+
);
|
|
5978
|
+
}
|
|
5979
|
+
|
|
5867
5980
|
/** One compact line that says whether this overview belongs to the main agent or to
|
|
5868
5981
|
a subagent, and offers to switch to the other side of that lineage. */
|
|
5869
5982
|
function SessionRoleBar({ t, sessionId, hasWork }) {
|
|
@@ -5928,6 +6041,8 @@ function FileTypeIcon(props) {
|
|
|
5928
6041
|
}
|
|
5929
6042
|
/* A plain main session with no subagents in this scope has no identity to
|
|
5930
6043
|
announce: the badge, the child list, and the parent link are all noise. */
|
|
6044
|
+
/* A finished child session from an older turn is lineage, not status: only an
|
|
6045
|
+
actually running subagent (or subagent work in the visible scope) is announced. */
|
|
5931
6046
|
if (role.children.length === 0 || hasWork === false) return null;
|
|
5932
6047
|
return h("div", { className: "dsov-role", "data-kind": "main" }, h("span", { className: "dsov-roleBadge" }, t("role.main")), children, list);
|
|
5933
6048
|
}
|
|
@@ -5959,22 +6074,20 @@ function FileTypeIcon(props) {
|
|
|
5959
6074
|
const stats = useArtifactStats(binding, summary.changeTurns);
|
|
5960
6075
|
const cfg = useConfig();
|
|
5961
6076
|
const sessionId = binding ? binding.sessionId : undefined;
|
|
5962
|
-
/* 本轮结论 as a document: a turn
|
|
5963
|
-
its closing reply into a tab once. A
|
|
5964
|
-
|
|
5965
|
-
|
|
6077
|
+
/* 本轮结论 as a document: only a turn this overview actually watched finish
|
|
6078
|
+
pops its closing reply into a tab, once. A conclusion replayed from history
|
|
6079
|
+
on mount/refresh — even when it arrives in a late flush after the first
|
|
6080
|
+
paint — is never opened. */
|
|
6081
|
+
const sessionRunning = useSessionRunning(binding ? binding.sessionId : undefined);
|
|
6082
|
+
const conclusionGuard = React.useRef({ ready: false, opened: new Set(), sawRunning: false });
|
|
6083
|
+
React.useEffect(() => {
|
|
6084
|
+
if (sessionRunning === true) conclusionGuard.current.sawRunning = true;
|
|
6085
|
+
}, [sessionRunning]);
|
|
5966
6086
|
React.useEffect(() => {
|
|
5967
6087
|
const conclusion = isRecord(summary.conclusion) ? summary.conclusion : null;
|
|
5968
|
-
const key = `${sessionId}|${conclusion === null ? "" : conclusion.turn}`;
|
|
5969
6088
|
const guard = conclusionGuard.current;
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
guard.ready = true;
|
|
5973
|
-
if (conclusion !== null && conclusion.ended === true) guard.opened.add(key);
|
|
5974
|
-
return;
|
|
5975
|
-
}
|
|
5976
|
-
if (conclusion === null || conclusion.ended !== true || guard.opened.has(key)) return;
|
|
5977
|
-
guard.opened.add(key);
|
|
6089
|
+
const key = conclusionAutoOpenKey(guard, conclusion, sessionId, guard.sawRunning);
|
|
6090
|
+
if (key === null) return;
|
|
5978
6091
|
if (cfg.conclusionTab !== true) return;
|
|
5979
6092
|
if (typeof conclusion.previewKey !== "string" || conclusion.previewKey === "") return;
|
|
5980
6093
|
if (contextSidebar() === null) return;
|
|
@@ -5982,22 +6095,12 @@ function FileTypeIcon(props) {
|
|
|
5982
6095
|
/* a failed open must not eat the claim: another instance may still show the tab */
|
|
5983
6096
|
const entry = readContextPreview(conclusion.previewKey);
|
|
5984
6097
|
const docKey = `conclusion:${typeof conclusion.turn === "number" ? conclusion.turn : conclusion.previewKey}`;
|
|
5985
|
-
const opened = openNativeDocument(sessionId, docKey, conclusion.title, entry === null ? undefined : entry.text)
|
|
6098
|
+
const opened = openContextPreview(conclusion.previewKey) || openNativeDocument(sessionId, docKey, conclusion.title, entry === null ? undefined : entry.text);
|
|
5986
6099
|
if (!opened) conclusionAutoOpened.delete(key);
|
|
5987
|
-
}, [summary, cfg.conclusionTab, sessionId]);
|
|
6100
|
+
}, [summary, cfg.conclusionTab, sessionId, sessionRunning]);
|
|
5988
6101
|
/* Agent identity only earns its row when a subagent actually took part in the
|
|
5989
|
-
visible scope
|
|
5990
|
-
const roleWork = React.useMemo(() =>
|
|
5991
|
-
const team = isRecord(summary.team) ? summary.team : null;
|
|
5992
|
-
if (team !== null && (Array.isArray(team.subagents) && team.subagents.length > 0)) return true;
|
|
5993
|
-
if (team !== null && (Array.isArray(team.members) && team.members.length > 0)) return true;
|
|
5994
|
-
const steps = Array.isArray(summary.stepsAll) ? summary.stepsAll : [];
|
|
5995
|
-
return steps.some(
|
|
5996
|
-
(step) =>
|
|
5997
|
-
isRecord(step) &&
|
|
5998
|
-
(step.name === "subagent" || step.name === "subagent_fork" || step.name === "spawn_teammate" || step.name === "team_task_create" || typeof step.teamKey === "string")
|
|
5999
|
-
);
|
|
6000
|
-
}, [summary]);
|
|
6102
|
+
visible scope (see subagentWorkInScope). */
|
|
6103
|
+
const roleWork = React.useMemo(() => subagentWorkInScope(summary), [summary]);
|
|
6001
6104
|
const rootClass = variant === "panel" ? "dsov-panel" : "dsov-view";
|
|
6002
6105
|
const rootRef = React.useRef(null);
|
|
6003
6106
|
const scope = summary.scope === selection || selection === "latest" ? summary.scope : selection;
|
|
@@ -7721,6 +7824,11 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
7721
7824
|
changesReviewAddress,
|
|
7722
7825
|
absPath,
|
|
7723
7826
|
normalizeConfig,
|
|
7827
|
+
conclusionAutoOpenKey,
|
|
7828
|
+
subagentWorkInScope,
|
|
7829
|
+
persistDocPreview,
|
|
7830
|
+
readDocPreviewStore,
|
|
7831
|
+
DOC_PREVIEW_STORE_KEY,
|
|
7724
7832
|
setFormConfig,
|
|
7725
7833
|
setOverlayConfig,
|
|
7726
7834
|
clearOverlayConfig,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-conversation-overview",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DeepSeek Harness (DSH) plugin: an Overview tab beside Chat/Trajectory plus a right-sidebar page summarizing session progress (goal, todos, key steps), artifacts, and references, with per-turn scoping, OpenViking memory preview, and quick-nav linkage.",
|
|
6
6
|
"keywords": [
|