dsh-conversation-overview 1.4.0 → 1.4.2

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 CHANGED
@@ -38,7 +38,7 @@ CLI 安装后重启 `dsh` 生效(方式一/二安装后刷新浏览器页面
38
38
 
39
39
  安装后在「设置 → 会话概览」或插件行内配置中调整选项;对话页刷新即可看到「概览」标签页与右侧边栏入口。
40
40
 
41
- > 注意:若此前安装过开发版 `@local/dsh-conversation-overview`,请先移除它再安装本包——两者使用相同的插件行 id(`conversation-overview`)与插槽 key,不应同时启用。
41
+ > 注意:若此前用 `link:` 装过本地开发副本(同名包指向你的工作目录),请先移除再安装本包——两者使用相同的插件行 id(`conversation-overview`)与插槽 key,不应同时启用。判断方法:`grep dsh-conversation-overview ~/.dsh/profiles/web/package.json`,出现 `link:/…` 即为开发版。
42
42
 
43
43
  **插件更新**:
44
44
 
@@ -78,14 +78,69 @@ CLI 安装后重启 `dsh` 生效(方式一/二安装后刷新浏览器页面
78
78
 
79
79
  ## 开发与测试
80
80
 
81
- 包内无构建步骤:`index.js`(Host 侧,schemastery Config + 资源定位)与 `client.js`(浏览器侧,`window.__ModuleLoader__` 工厂格式)均为源文件直发。
81
+ ### 代码结构
82
82
 
83
- 离线测试用真实会话日志(`session.v4.jsonl.zstd`)驱动折叠逻辑与设置表单断言:
83
+ 包内**没有构建步骤**,两个源文件直接就是发布物:
84
+
85
+ | 文件 | 运行位置 | 职责 |
86
+ | --- | --- | --- |
87
+ | `index.js` | Host(Node) | schemastery 配置字段定义、`dsh-resource://` 资源定位与 Host 侧数据接口 |
88
+ | `client.js` | 浏览器(Web UI) | 概览页/侧边栏 UI、会话事件折叠、标签页注册,以 `window.__ModuleLoader__` 工厂函数形式被加载 |
89
+
90
+ 改完文件后:Host 侧(`index.js`、`cordis.patch.yml` 的配置项)需要**重启 DSH** 才生效;纯客户端(`client.js`)**刷新页面**即可。
91
+
92
+ ### 本地调试安装
93
+
94
+ ```bash
95
+ # 让 profile 直接引用本地目录(改完刷新即生效,不必反复装包)
96
+ dsh plugin --profile web add link:/absolute/path/to/dsh-conversation-overview
97
+
98
+ # 确认装的是开发版而不是 npm 版
99
+ grep dsh-conversation-overview ~/.dsh/profiles/web/package.json # 应看到 link:/…
100
+ ```
101
+
102
+ ### 运行测试
103
+
104
+ 测试脚本随包发布,无需构建、无需联网。它会在 Node 里用桩(stub)替代浏览器环境——`window`、`localStorage`、`React`——然后加载 `client.js`,对**真实会话日志**做一遍完整折叠并断言结果。
84
105
 
85
106
  ```bash
86
- node .scratch/test.mjs <path/to/session-a.jsonl.zstd> <path/to/session-b.jsonl.zstd>
107
+ # 默认自动取本机最近的 2 个会话日志
108
+ npm test
109
+
110
+ # 或指定任意日志(可传多个,跨会话、跨工作区都行)
111
+ node .scratch/test.mjs ~/.dsh/sessions/<workspace>/<session-id>/session.v4.jsonl.zstd \
112
+ ~/.dsh/sessions/<workspace>/<other-session-id>/session.v4.jsonl.zstd
87
113
  ```
88
114
 
115
+ - 依赖 **Node ≥ 20** 与 **`zstdcat`**(日志是 zstd 压缩的;macOS:`brew install zstd`)。
116
+ - 会话日志位置:`~/.dsh/sessions/<工作区>/<session-id>/session.v4.jsonl.zstd`,每一轮对话都在里面。
117
+ - 全部通过时输出 `ALL TESTS PASSED`,并列出实际检查了哪几个日志;任一断言失败会直接给出断言信息与所在行号。
118
+
119
+ ### 测试覆盖范围
120
+
121
+ 共 16 组、约 460 条断言,全部离线运行:
122
+
123
+ | # | 覆盖内容 |
124
+ | --- | --- |
125
+ | 1 | `zh`/`en` 词典键完全对齐(少一个键就失败,避免界面出现原始 key) |
126
+ | 2 | `apply()` 在 mock `ctx` 上注册了哪些标签页/插槽,以及每个配置开关的显隐效果 |
127
+ | 3 | 用真实日志跑完整折叠:待办、关键步骤、产物、引用、统计、里程碑 |
128
+ | 4 | 地址解析、助手提及、变更事件等纯函数 |
129
+ | 5 | 文件类型分类与工作区变更聚合 |
130
+ | 6 | OpenViking 预览与 Markdown 渲染 |
131
+ | 7 | frontmatter、面包屑、引用排序 |
132
+ | 8 | 按轮次折叠(`latest` / `all` 两种范围)与上下文快照 |
133
+ | 9 | 全量引用识别(正文里出现过的资源是否都被收集) |
134
+ | 10 | 关键步骤流与轮次导航轨的联动刻度 |
135
+ | 11 | 「新标签打开」确实落到宿主自己的 Markdown 文档页 |
136
+ | 12 | 「本轮结论」只在该轮结束后出现 |
137
+ | 13 | 结论自动打开:只在观察到该轮实际运行后打开一次(刷新不会重开) |
138
+ | 14 | 子代理相关界面只在真有子代理工作时出现(压缩里程碑不算) |
139
+ | 15 | 文档标签刷新后仍能恢复正文(结论/计划快照镜像到 `localStorage`) |
140
+ | 16 | 代理身份判定:`origin: "subagent"` 才是子代理,分叉/续聊会话算主会话 |
141
+
142
+ > 这些断言同时充当回归护栏:改动 `client.js` 的折叠或显隐逻辑后先跑 `npm test`,比在浏览器里逐个点要快得多。
143
+
89
144
  ## License
90
145
 
91
146
  MIT © 2026
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
- if (openNativeDocument(sessionId, docKey, artifact.title, entry === null ? undefined : entry.text)) return;
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
- if (openNativeDocument(sessionId, docKey, entry !== null ? entry.title : artifact.planTitle, entry === null ? undefined : entry.text)) return;
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
- contextPreviews.set(globalKey, { 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 } : {}) });
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
- return entry === undefined ? null : entry;
1900
+ if (entry !== undefined) return entry;
1901
+ /* a tab restored after a reload finds its body here */
1902
+ const stored = readDocPreviewStore().find((item) => item[0] === globalKey);
1903
+ if (stored === undefined) return null;
1904
+ const revived = isRecord(stored[1]) && typeof stored[1].text === "string" ? stored[1] : null;
1905
+ if (revived === null || revived.text === "") return null;
1906
+ const restored = { title: typeof revived.title === "string" && revived.title !== "" ? revived.title : globalKey, text: revived.text, ...(typeof revived.kind === "string" && revived.kind !== "" ? { kind: revived.kind } : {}) };
1907
+ contextPreviews.set(globalKey, restored);
1908
+ return restored;
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
- sidebar.openResource(contextPreviewAddress(globalKey));
1899
- return true;
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 parentId =
5792
- isRecord(address) && typeof address.parentSessionId === "string" && address.parentSessionId !== ""
5793
- ? address.parentSessionId
5794
- : self !== null && typeof self.parentId === "string" && self.parentId !== ""
5795
- ? self.parentId
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.parentId !== sessionId) continue;
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 && typeof parent.displayTitle === "string" ? parent.displayTitle : "",
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 that finishes while this overview is open pops
5963
- its closing reply into a tab once. A turn that had already finished before the
5964
- overview mounted is never re-opened (no tab storm on refresh or scope switch). */
5965
- const conclusionGuard = React.useRef({ ready: false, opened: new Set() });
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
- if (guard.ready !== true) {
5971
- /* first paint: a turn that had already finished is never re-opened */
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) || openContextPreview(conclusion.previewKey);
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 — otherwise the overview is a single-agent conversation. */
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.0",
3
+ "version": "1.4.2",
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": [
@@ -40,8 +40,12 @@
40
40
  "icon.svg",
41
41
  "README.md",
42
42
  "LICENSE",
43
- "assets/overview.png"
43
+ "assets/overview.png",
44
+ ".scratch/test.mjs"
44
45
  ],
46
+ "scripts": {
47
+ "test": "node .scratch/test.mjs"
48
+ },
45
49
  "engines": {
46
50
  "dsh": ">=0.2.1-rc.2",
47
51
  "node": ">=20"