dsh-sessions-manager 3.6.1 → 3.6.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.en.md +3 -3
- package/README.md +3 -3
- package/lib/client.js +100 -28
- package/lib/client.js.map +2 -2
- package/lib/index.js +217 -85
- package/lib/index.js.map +2 -2
- package/package.json +1 -1
- package/src/client/index.jsx +104 -28
- package/src/client/logic.js +29 -0
- package/src/index.js +264 -86
- package/src/title-persist-index.js +13 -3
package/README.en.md
CHANGED
|
@@ -20,7 +20,7 @@ A persistent DSH plugin (host + browser halves) covering both the **settings pan
|
|
|
20
20
|
|
|
21
21
|
- **Unified panel**: three always-visible top-level views — **All / Active / Archived** — while the low-frequency ones (**Starred / Empty / Recycle bin**) are tucked behind an arrow button at the end of the filter row and slide out horizontally on click (they stay expanded automatically while a low-frequency view is selected). Search across title, session ID, and workspace, plus workspace filters and creation-time/title sorting. The result line is a single sentence such as "Showing 5 sessions, 4 more subagents folded under their parents". A maintenance row below the filters holds the **Storage usage** and **Auto-archive** tools, which expand on demand instead of taking up a view.
|
|
22
22
|
- **Starred sessions**: an always-visible star button on each session row toggles starring optimistically with rollback on failure; the Starred view is orthogonal to DSH's active/archived states and they stack. The star index is plugin-owned (schema v3) and never touches DSH logs; stars are cleaned up automatically when a session is permanently purged.
|
|
23
|
-
- **Cold-title synchronization**: the sidebar is corrected from the latest log-backed `session/title`, so renamed cold sessions no longer need to be opened before showing their current name.
|
|
23
|
+
- **Cold-title synchronization**: the sidebar is corrected from the latest log-backed `session/title`, so renamed cold sessions no longer need to be opened before showing their current name. On cold starts of large stores, titles are filled in by a background warm queue, and once resolved they **retroactively update already-rendered sidebar rows and the “⑂ branch” chip tooltip** — no need to wait for a row to rebuild.
|
|
24
24
|
- **Sidebar workspace drag-and-drop**: drag a session onto a workspace heading to move it, with a highlighted drop target, same-workspace protection, failure feedback, and the More → Move session menu retained for keyboard access.
|
|
25
25
|
- **Archive / Restore**: archive hides a session from the sidebar; restore unarchives it and puts it back into its original workspace group.
|
|
26
26
|
- **Move to a workspace**: pick an **existing workspace**, or enter a **new directory path** (auto-created); the new-path mode also lets you open the **native OS directory picker** with a **「浏览…」** button. The session's working directory and log are migrated together. On the legacy runtime (`0.1.2-rc.1`) even an open session can be moved; on the new runtime (`0.1.3-alpha.1`) session logs are **single-writer** — DSH claims write ownership when it opens a session and holds it, so **any session you have opened in DSH** is temporarily unmovable. The error bar states this plainly; in practice switching sessions does not release the ownership, so **restart DSH and move it again**.
|
|
@@ -180,7 +180,7 @@ lib/client.js pre-built client (ModuleLoader CJS handshake)
|
|
|
180
180
|
| POST | `/archived-sessions/pending-moves` | Inspect the pending-move queue (sessions queued while held open; completed automatically once released) |
|
|
181
181
|
| POST | `/archived-sessions/pending-moves/cancel` | Cancel queued moves `{ sessionIds }` |
|
|
182
182
|
| POST | `/archived-sessions/details` | Session details (disk / stats / tools / fetch / files / lineage) `{ sessionId }` |
|
|
183
|
-
| POST | `/archived-sessions/sidebar-state` | Authoritative sidebar titles, recycle-bin IDs, permanent-deletion tombstones,
|
|
183
|
+
| POST | `/archived-sessions/sidebar-state` | Authoritative sidebar titles, recycle-bin IDs, permanent-deletion tombstones, **lineage disclosure** (subagent / branch / empty badges), and `warmPending` (whether background title warming is still in flight) |
|
|
184
184
|
| POST | `/archived-sessions/lineage-tree` | Recursive subagent tree (feeds the panel's Lineage grouping and the sidebar folding), filtered for recycle-bin and tombstoned sessions |
|
|
185
185
|
| POST | `/archived-sessions/star/set` | Star / unstar sessions `{ sessionId or sessionIds, starred }` |
|
|
186
186
|
| GET | `/archived-sessions/export-md?sessionId=` | Single-session Markdown export (human-readable transcript) |
|
|
@@ -207,7 +207,7 @@ lib/client.js pre-built client (ModuleLoader CJS handshake)
|
|
|
207
207
|
- **Permanent purge / empty recycle bin / automatic physical cleanup**: the official public contract offers no delete API, so the plugin keeps the legacy-era semi-official approach — a **three-layer guarded path derivation** starting from the backend instance's storage-root field (root → session directory structure → session-id ownership check), followed by a whole-directory removal verified with the official `stat`. Sessions with an active writer are refused (409). When the derivation fails (e.g. the storage root is unavailable), the actions degrade to disabled with an explicit reason instead of deleting blindly.
|
|
208
208
|
- **Cross-workspace move**: the primary path replays events through the official `create` + `append` (writer-ownership probe refuses sessions whose write ownership is held by DSH, before/after `revision` checks guard against concurrent writes, backup + rollback guarantees no half-moved state on failure); when the backend holds a ghost record for the same id it falls back to relocating the log with a frame0 cwd rewrite (frame count and content verified byte-for-byte). Paths come from the same guarded derivation; the workspace grouping index is rebuilt automatically after the move — no restart needed. Since runtime `0.1.5` a session may keep **several log generations side by side** (the runtime publishes a newer generation but keeps the older ones: `session.jsonl.zstd` + `session.v2.jsonl.zstd` + `session.v3.jsonl.zstd`), while the runtime insists that one id lives in exactly one project directory — so a move is a **whole-directory move**: the source session directory is renamed out of sight first, the target is materialized and verified, then the source directory is deleted. Superseded older-generation copies left in other project directories by earlier versions are **reclaimed automatically**, so a session can never get stuck as a cross-directory duplicate.
|
|
209
209
|
- **Purge tombstones never suppress new sessions**: if a session with the same id is recreated later, the tombstone yields automatically and the new session appears normally in lists and the sidebar.
|
|
210
|
-
- **Metadata-cache change tokens**: on the new runtime the plugin prefers the official `stat`/`list` snapshot `revision` (comparable only within one service instance and one session id) to detect changes; legacy runtimes keep the `(mtime, size)` file fingerprint. Revisions are never persisted into the cross-process title index. List metadata comes straight from `snapshot.header` and titles from the batch projection — a missing title does **not** trigger a full-log decode, avoiding amplification of the known historical-session loading regression in alpha.
|
|
210
|
+
- **Metadata-cache change tokens**: on the new runtime the plugin prefers the official `stat`/`list` snapshot `revision` (comparable only within one service instance and one session id) to detect changes; legacy runtimes keep the `(mtime, size)` file fingerprint. Revisions are never persisted into the cross-process title index. List metadata comes straight from `snapshot.header` and titles from the batch projection — a missing title does **not** trigger a full-log decode, avoiding amplification of the known historical-session loading regression in alpha. A projection **failure** is kept strictly apart from a session that genuinely **has no title**: failures are never written to the memory or persisted cache and retry with per-session exponential backoff (re-armed the moment the log changes), so a single failure can never be baked into a permanent placeholder.
|
|
211
211
|
- **Auto-archive**: the new runtime exposes no reliable "last activity" timestamp; the sweep skips (`no-activity-data`) when idleness cannot be proven, and never archives on a guess.
|
|
212
212
|
- **Sidebar injection**: DOM / React fiber recognition is centralized in a versioned adapter; when the upstream sidebar shape is no longer recognized, all injections degrade safely and the official sidebar is left untouched. While recognition works, updates are driven incrementally by a MutationObserver with only a low-frequency fallback check.
|
|
213
213
|
- **Lineage disclosure**: subagent sessions are recognized via the official header fields (`origin: 'subagent'`, `parentSession`, `delegationDepth`); since the upstream sidebar renders neither subagent nor empty sessions, the plugin injects subagent rows under their parent and hides empty rows. Empty detection uses the **event-type method** — `0.1.3` always writes one lifecycle-metadata frame at creation (`permission/preset` / `sandbox/mode` / `approval/policy`), which breaks the old size threshold; candidates up to 8KB (compressed) are decoded once through the official API and marked empty only when no content event exists beyond the header, decode failures fall back to non-empty, and results are cached per (session, size) so polling stays nearly free.
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
- **统一面板**:顶部提供「全部 / 活动 / 已归档」三个常驻视图,低频的「已收藏 / 空白 / 回收站」收进行尾的箭头按钮,点按横向滑出展开(低频视图处于选中态时自动保持展开);支持按标题、会话 ID、工作区搜索,按工作区筛选,并按创建时间或标题排序。结果行合并为一句说明,如「显示 5 个会话,另有 4 个子代理折叠在父会话下」。筛选栏下方的维护栏收纳「存储占用」与「自动归档」两个按需展开的工具,不占用视图位置。
|
|
22
22
|
- **收藏(星标)**:会话行左侧常驻星标按钮,单击即收藏 / 取消(乐观更新、失败回滚);「已收藏」视图与 DSH 的活动 / 归档状态正交、可叠加;收藏索引为插件自有 schema v3,不触碰 DSH 日志,会话被彻底删除时自动清理。
|
|
23
|
-
- **冷态标题同步**:侧栏使用日志中最新的 `session/title`
|
|
23
|
+
- **冷态标题同步**:侧栏使用日志中最新的 `session/title` 修正冷启动缓存,改名后的会话无需先打开即可显示新名称;冷启动大库里标题在后台分批预热补齐,补齐后**已渲染的侧栏行与「⑂ 分支」标签提示自动回填**,不必等行重建。
|
|
24
24
|
- **侧栏跨工作区拖拽**:直接把会话拖到目标工作区标题即可切换工作区;目标高亮、同工作区拦截、失败反馈,并保留“更多 → 移动会话”作为键盘操作入口。
|
|
25
25
|
- **归档 / 恢复**:归档把会话从侧栏隐藏;恢复取消归档并放回原工作区分组。
|
|
26
26
|
- **移动到工作区**:任选**已有工作区**或**新建目录路径**(自动创建),新建目录支持点击 **「浏览…」** 调用系统目录选择窗口。会话的工作目录与日志一起迁移。旧版 Runtime(`0.1.2-rc.1`)下已打开的会话也可移动;新版 Runtime(`0.1.3-alpha.1`)为会话日志引入了**单写者所有权**——DSH 打开会话时即取得写权限并持有,因此**在 DSH 里打开过的会话**暂时无法移动,错误条会如实说明;实测切换会话不会释放写权限,**重启 DSH 后再移动**即可。
|
|
@@ -180,7 +180,7 @@ lib/client.js 预构建 client(ModuleLoader CJS handshake)
|
|
|
180
180
|
| POST | `/archived-sessions/pending-moves` | 查看待移动队列(会话被占用时排队,释放后自动完成) |
|
|
181
181
|
| POST | `/archived-sessions/pending-moves/cancel` | 取消排队 `{ sessionIds }` |
|
|
182
182
|
| POST | `/archived-sessions/details` | 会话详情(磁盘/统计/工具/fetch/文件/血统)`{ sessionId }` |
|
|
183
|
-
| POST | `/archived-sessions/sidebar-state` | 返回侧栏权威标题、回收站 ID
|
|
183
|
+
| POST | `/archived-sessions/sidebar-state` | 返回侧栏权威标题、回收站 ID、永久删除墓碑、**血缘分层**(子代理 / 分支 / 空白标记)与 `warmPending`(后台标题预热是否仍在途) |
|
|
184
184
|
| POST | `/archived-sessions/lineage-tree` | 递归子代理树(面板血缘分组与侧栏折叠的数据源),已过滤回收站与墓碑 |
|
|
185
185
|
| POST | `/archived-sessions/star/set` | 收藏 / 取消收藏 `{ sessionId 或 sessionIds, starred }` |
|
|
186
186
|
| GET | `/archived-sessions/export-md?sessionId=` | 单会话 Markdown 导出(人类可读对话记录) |
|
|
@@ -208,7 +208,7 @@ lib/client.js 预构建 client(ModuleLoader CJS handshake)
|
|
|
208
208
|
- **彻底删除 / 清空回收站 / 自动物理清理**:官方公共契约未提供删除 API,插件沿用 legacy 时代的半官方路线——从后端实例的存储根目录字段出发做**三层守卫式路径推导**(根目录 → 会话目录结构 → 会话 ID 归属校验),通过后整目录删除并以官方 `stat` 复核;存在活跃写入的会话会被拒绝(409)。推导失败(如无法确认存储根目录)时自动禁用并说明原因,绝不盲删。
|
|
209
209
|
- **跨工作区移动**:主路径为官方 `create` + `append` 事件重放(写所有权探测会拒绝**被 DSH 占用写权限**的会话、revision 前后校验防并发写入、备份回滚保证失败不留半移动状态);后端存在同 ID 幽灵记录时回退到 frame0 cwd 改写搬运(帧数与内容逐位校验)。路径同样来自守卫式推导;移动后自动重建工作区分组索引,无需重启。Runtime `0.1.5` 起会话日志会**多代共存**(官方发布新代时保留旧代,如 `session.jsonl.zstd` + `session.v2.jsonl.zstd` + `session.v3.jsonl.zstd`),而官方按「同一 ID 只能落在一个项目目录」校验——因此移动是**整目录搬运**:先把源会话目录整体移开(对官方扫描彻底不可见),目标侧重放校验成功后再删除源目录;顺带**自动回收**其它项目目录里被取代的旧代副本(历史移动残留),避免同一会话永久卡在「跨目录重复」而无法移动或删除。
|
|
210
210
|
- **彻底删除墓碑不压制新会话**:若同 ID 会话被重新创建,墓碑自动让位,新会话正常出现在列表与侧栏。
|
|
211
|
-
- **元数据缓存的变更令牌**:新 Runtime 优先使用官方 `stat`/`list` 快照的 `revision`(仅同一 service 实例、同一会话内可比较)判断会话是否变化,旧 Runtime 继续使用 `(mtime, size)` 文件指纹;`revision` 绝不写入跨进程持久索引。列表元数据直接取自 `snapshot.header`,标题走批量投影——没有标题时**不会**自动解码整本日志,避免放大 alpha
|
|
211
|
+
- **元数据缓存的变更令牌**:新 Runtime 优先使用官方 `stat`/`list` 快照的 `revision`(仅同一 service 实例、同一会话内可比较)判断会话是否变化,旧 Runtime 继续使用 `(mtime, size)` 文件指纹;`revision` 绝不写入跨进程持久索引。列表元数据直接取自 `snapshot.header`,标题走批量投影——没有标题时**不会**自动解码整本日志,避免放大 alpha 已知的历史会话加载性能回退。投影**失败**与会话**真无标题**严格区分:失败绝不写入内存或持久缓存,按会话指数退避自动重试(日志一变即重新武装),杜绝「一次失败被固化为永久占位」。
|
|
212
212
|
- **自动归档**:新 Runtime 不提供可靠的「最后活跃时间」,无法证明会话闲置时检查会跳过(`no-activity-data`),绝不基于猜测归档。
|
|
213
213
|
- **侧栏注入**:DOM / React fiber 识别收敛为可版本化的 adapter;上游侧栏结构不被识别时整体安全停用,不影响官方侧栏本身。识别正常时以 MutationObserver 增量驱动为主,仅保留低频兜底检查。
|
|
214
214
|
- **血缘分层**:子代理会话以官方头部字段识别(`origin: 'subagent'`、`parentSession`、`delegationDepth`);上游侧栏不渲染子代理行与空白会话,插件据此把子代理行就地注入父会话下、把空白行隐藏。空白判定用**事件类型法**——`0.1.3` 创建会话时恒写一帧生命周期元数据(`permission/preset` / `sandbox/mode` / `approval/policy`),体积阈值法失效;插件对压缩体积 ≤8KB 的候选走官方解码精判,头部之外无内容事件才判空白,解码失败按非空兜底,结果按 (会话, 体积) 缓存使轮询近零成本。
|
package/lib/client.js
CHANGED
|
@@ -55,9 +55,24 @@ function dotStateFor({ manualUnread = false, dataState = "", isActive = false }
|
|
|
55
55
|
return null;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
function titleBackfillDecision({ rendered = "", baseline = null, authoritative = "" } = {}) {
|
|
59
|
+
const auth = authoritative || "";
|
|
60
|
+
if (!baseline) {
|
|
61
|
+
const next = auth || rendered;
|
|
62
|
+
return { text: next, changed: !!auth && auth !== rendered, nextBaseline: { text: next, authoritative: auth || null } };
|
|
63
|
+
}
|
|
64
|
+
if (rendered !== baseline.text) {
|
|
65
|
+
return { changed: false, nextBaseline: { text: rendered, authoritative: baseline.authoritative || null } };
|
|
66
|
+
}
|
|
67
|
+
if (!auth || auth === baseline.authoritative) return null;
|
|
68
|
+
return { text: auth, changed: true, nextBaseline: { text: auth, authoritative: auth } };
|
|
69
|
+
}
|
|
70
|
+
function effectiveTitleOf(item, authoritativeTitles) {
|
|
71
|
+
if (!item) return "";
|
|
72
|
+
if (item.title) return String(item.title);
|
|
73
|
+
const id = String(item.sessionId);
|
|
74
|
+
const auth = authoritativeTitles && authoritativeTitles.get(id);
|
|
75
|
+
return auth ? String(auth) : "";
|
|
61
76
|
}
|
|
62
77
|
function canDropOnWorkspace(item, target) {
|
|
63
78
|
if (!item || !target) return false;
|
|
@@ -453,6 +468,7 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
453
468
|
const [trashCheck, setTrashCheck] = (0, import_react.useState)(null);
|
|
454
469
|
const [purgeTarget, setPurgeTarget] = (0, import_react.useState)(null);
|
|
455
470
|
const [details, setDetails] = (0, import_react.useState)({});
|
|
471
|
+
const detailsAt = (0, import_react.useRef)({});
|
|
456
472
|
const [openDetails, setOpenDetails] = (0, import_react.useState)(null);
|
|
457
473
|
const [detailsLoading, setDetailsLoading] = (0, import_react.useState)(null);
|
|
458
474
|
const [mdBusy, setMdBusy] = (0, import_react.useState)(null);
|
|
@@ -509,6 +525,11 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
509
525
|
if (timer.current) clearTimeout(timer.current);
|
|
510
526
|
};
|
|
511
527
|
}, []);
|
|
528
|
+
const refreshRef = (0, import_react.useRef)(null);
|
|
529
|
+
refreshRef.current = refresh;
|
|
530
|
+
(0, import_react.useEffect)(() => dsmOnWarmDone(() => {
|
|
531
|
+
if (refreshRef.current) refreshRef.current();
|
|
532
|
+
}), []);
|
|
512
533
|
(0, import_react.useEffect)(() => {
|
|
513
534
|
try {
|
|
514
535
|
localStorage.setItem(PANEL_PREFS_KEY, JSON.stringify({ filter, workspaceFilter, sortBy, groupByLineage }));
|
|
@@ -614,11 +635,11 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
614
635
|
const filtered = base.filter((item) => {
|
|
615
636
|
if (workspaceFilter !== "all" && (item.workspacePath || "") !== workspaceFilter) return false;
|
|
616
637
|
if (!needle) return true;
|
|
617
|
-
return [item
|
|
638
|
+
return [effectiveTitleOf(item, dsmAuthoritativeTitles), item.sessionId, item.workspaceTitle, item.workspacePath].some((value) => String(value || "").toLocaleLowerCase().includes(needle));
|
|
618
639
|
});
|
|
619
640
|
return [...filtered].sort((a, b) => {
|
|
620
641
|
if (sortBy === "oldest") return Number(a.createdAt || 0) - Number(b.createdAt || 0);
|
|
621
|
-
if (sortBy === "title") return
|
|
642
|
+
if (sortBy === "title") return effectiveTitleOf(a, dsmAuthoritativeTitles).localeCompare(effectiveTitleOf(b, dsmAuthoritativeTitles), "zh-CN");
|
|
622
643
|
return Number(b.createdAt || 0) - Number(a.createdAt || 0);
|
|
623
644
|
});
|
|
624
645
|
}, [sessions, filter, query, workspaceFilter, sortBy, emptyList, starredList]);
|
|
@@ -987,10 +1008,19 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
987
1008
|
}
|
|
988
1009
|
if (details[it.sessionId]) {
|
|
989
1010
|
setOpenDetails(it.sessionId);
|
|
1011
|
+
const at = detailsAt.current[it.sessionId] || 0;
|
|
1012
|
+
if (Date.now() - at > 3e4) {
|
|
1013
|
+
postJSON("/archived-sessions/details", { sessionId: it.sessionId }).then((d) => {
|
|
1014
|
+
detailsAt.current[it.sessionId] = Date.now();
|
|
1015
|
+
setDetails((m) => ({ ...m, [it.sessionId]: d }));
|
|
1016
|
+
}).catch(() => {
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
990
1019
|
return;
|
|
991
1020
|
}
|
|
992
1021
|
setDetailsLoading(it.sessionId);
|
|
993
1022
|
postJSON("/archived-sessions/details", { sessionId: it.sessionId }).then((d) => {
|
|
1023
|
+
detailsAt.current[it.sessionId] = Date.now();
|
|
994
1024
|
setDetails((m) => ({ ...m, [it.sessionId]: d }));
|
|
995
1025
|
setDetailsLoading(null);
|
|
996
1026
|
setOpenDetails(it.sessionId);
|
|
@@ -1792,6 +1822,13 @@ var dsmAuthoritativeTitles = /* @__PURE__ */ new Map();
|
|
|
1792
1822
|
var dsmTrashTick = 0;
|
|
1793
1823
|
var dsmCapabilities = null;
|
|
1794
1824
|
var dsmLineage = /* @__PURE__ */ new Map();
|
|
1825
|
+
var dsmWarmPending = false;
|
|
1826
|
+
var dsmTrashRetryTimer = null;
|
|
1827
|
+
var dsmWarmDoneHooks = /* @__PURE__ */ new Set();
|
|
1828
|
+
function dsmOnWarmDone(fn) {
|
|
1829
|
+
dsmWarmDoneHooks.add(fn);
|
|
1830
|
+
return () => dsmWarmDoneHooks.delete(fn);
|
|
1831
|
+
}
|
|
1795
1832
|
async function dsmLoadCapabilities() {
|
|
1796
1833
|
try {
|
|
1797
1834
|
dsmCapabilities = await postJSON("/archived-sessions/capabilities", {});
|
|
@@ -1816,8 +1853,23 @@ async function dsmLoadTrashIds() {
|
|
|
1816
1853
|
for (const [id, info] of Object.entries(r && r.lineage || {})) {
|
|
1817
1854
|
if (info && typeof info === "object") dsmLineage.set(String(id), info);
|
|
1818
1855
|
}
|
|
1856
|
+
const wasPending = dsmWarmPending;
|
|
1857
|
+
dsmWarmPending = !!(r && r.warmPending);
|
|
1858
|
+
if (wasPending && !dsmWarmPending) {
|
|
1859
|
+
try {
|
|
1860
|
+
for (const fn of dsmWarmDoneHooks) fn();
|
|
1861
|
+
} catch (e) {
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1819
1864
|
if (dsmRepaintDots) dsmRepaintDots();
|
|
1820
1865
|
} catch (e) {
|
|
1866
|
+
if (dsmTrashRetryTimer == null) {
|
|
1867
|
+
dsmTrashRetryTimer = setTimeout(() => {
|
|
1868
|
+
dsmTrashRetryTimer = null;
|
|
1869
|
+
dsmLoadTrashIds();
|
|
1870
|
+
}, 2e3);
|
|
1871
|
+
if (typeof dsmTrashRetryTimer.unref === "function") dsmTrashRetryTimer.unref();
|
|
1872
|
+
}
|
|
1821
1873
|
}
|
|
1822
1874
|
return dsmTrashIds;
|
|
1823
1875
|
}
|
|
@@ -2193,7 +2245,7 @@ function installSidebarStatusDots() {
|
|
|
2193
2245
|
// 红 出错/需关注 (DSH error)
|
|
2194
2246
|
};
|
|
2195
2247
|
const manualUnread = dsmLoadManual();
|
|
2196
|
-
const
|
|
2248
|
+
const titleBaselines = /* @__PURE__ */ new WeakMap();
|
|
2197
2249
|
function dsmSyncEmptyGroups() {
|
|
2198
2250
|
let groups = null;
|
|
2199
2251
|
try {
|
|
@@ -2283,21 +2335,22 @@ function installSidebarStatusDots() {
|
|
|
2283
2335
|
}
|
|
2284
2336
|
if (row.style.display === "none") row.style.display = "";
|
|
2285
2337
|
if (!isEmpty && li && li.parentSession && li.origin !== "subagent") {
|
|
2286
|
-
|
|
2287
|
-
|
|
2338
|
+
const parentTitle = dsmAuthoritativeTitles.get(li.parentSession);
|
|
2339
|
+
const tip = parentTitle ? "\u5206\u652F\u4E8E\uFF1A" + parentTitle : "\u5206\u652F\u4F1A\u8BDD\uFF08\u70B9\u51FB\u67E5\u770B\u6765\u6E90\u4F1A\u8BDD\uFF09";
|
|
2340
|
+
let fork = row.querySelector('[data-dsm-tag="fork"]');
|
|
2341
|
+
if (!fork) {
|
|
2342
|
+
fork = document.createElement("button");
|
|
2288
2343
|
fork.type = "button";
|
|
2289
2344
|
fork.dataset.dsmTag = "fork";
|
|
2290
2345
|
fork.className = "dsm-tag dsm-tag-fork";
|
|
2291
|
-
const parentTitle = dsmAuthoritativeTitles.get(li.parentSession);
|
|
2292
2346
|
fork.textContent = "\u2442 \u5206\u652F";
|
|
2293
|
-
fork.title = parentTitle ? "\u5206\u652F\u4E8E\uFF1A" + parentTitle : "\u5206\u652F\u4F1A\u8BDD\uFF08\u70B9\u51FB\u67E5\u770B\u6765\u6E90\u4F1A\u8BDD\uFF09";
|
|
2294
|
-
fork.setAttribute("aria-label", fork.title);
|
|
2295
2347
|
fork.addEventListener("click", (e) => {
|
|
2296
2348
|
e.stopPropagation();
|
|
2297
2349
|
e.preventDefault();
|
|
2350
|
+
const target = e.currentTarget.dataset.dsmForkParent || "";
|
|
2298
2351
|
const rows2 = document.querySelectorAll('[role="treeitem"]');
|
|
2299
2352
|
for (const other of rows2) {
|
|
2300
|
-
if (rowId(other) ===
|
|
2353
|
+
if (rowId(other) === target) {
|
|
2301
2354
|
other.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
2302
2355
|
other.classList.add("dsm-row-flash");
|
|
2303
2356
|
setTimeout(() => other.classList.remove("dsm-row-flash"), 1200);
|
|
@@ -2307,6 +2360,11 @@ function installSidebarStatusDots() {
|
|
|
2307
2360
|
});
|
|
2308
2361
|
row.appendChild(fork);
|
|
2309
2362
|
}
|
|
2363
|
+
if (fork.dataset.dsmForkParent !== li.parentSession) fork.dataset.dsmForkParent = String(li.parentSession);
|
|
2364
|
+
if (fork.title !== tip) {
|
|
2365
|
+
fork.title = tip;
|
|
2366
|
+
fork.setAttribute("aria-label", tip);
|
|
2367
|
+
}
|
|
2310
2368
|
} else {
|
|
2311
2369
|
const staleFork = row.querySelector('[data-dsm-tag="fork"]');
|
|
2312
2370
|
if (staleFork) staleFork.remove();
|
|
@@ -2351,19 +2409,29 @@ function installSidebarStatusDots() {
|
|
|
2351
2409
|
if (staleKids) staleKids.remove();
|
|
2352
2410
|
}
|
|
2353
2411
|
const authoritativeTitle = dsmAuthoritativeTitles.get(id);
|
|
2354
|
-
|
|
2355
|
-
const
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2412
|
+
{
|
|
2413
|
+
const base = titleBaselines.get(row);
|
|
2414
|
+
if (!base || base.id !== id || authoritativeTitle && authoritativeTitle !== base.authoritative) {
|
|
2415
|
+
const node = rowNode(row) || {};
|
|
2416
|
+
const expected = new Set([node.title, node.displayTitle, node.name].filter((value) => typeof value === "string"));
|
|
2417
|
+
const spans = [...row.children].filter((el) => el.tagName === "SPAN" && !el.querySelector("[data-state]") && (el.textContent || "").trim());
|
|
2418
|
+
let titleEl = null;
|
|
2419
|
+
if (base && base.id === id) {
|
|
2420
|
+
titleEl = spans.find((el) => (el.textContent || "") === base.text) || spans.find((el) => expected.has((el.textContent || "").trim())) || null;
|
|
2421
|
+
} else {
|
|
2422
|
+
titleEl = spans.find((el) => expected.has((el.textContent || "").trim())) || spans[0] || null;
|
|
2423
|
+
}
|
|
2424
|
+
if (titleEl) {
|
|
2425
|
+
const d = titleBackfillDecision({
|
|
2426
|
+
rendered: titleEl.textContent || "",
|
|
2427
|
+
baseline: base && base.id === id ? { text: base.text, authoritative: base.authoritative } : null,
|
|
2428
|
+
authoritative: authoritativeTitle || ""
|
|
2429
|
+
});
|
|
2430
|
+
if (d) {
|
|
2431
|
+
if (d.changed && titleEl.textContent !== d.text) titleEl.textContent = d.text;
|
|
2432
|
+
titleBaselines.set(row, { id, text: d.nextBaseline.text, authoritative: d.nextBaseline.authoritative });
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2367
2435
|
}
|
|
2368
2436
|
}
|
|
2369
2437
|
const sd = row.querySelector("[data-state]");
|
|
@@ -2518,7 +2586,8 @@ function installSidebarStatusDots() {
|
|
|
2518
2586
|
}
|
|
2519
2587
|
};
|
|
2520
2588
|
walk(rec.nodes, 0);
|
|
2521
|
-
const
|
|
2589
|
+
const effTitle = (n) => n.title || dsmAuthoritativeTitles.get(String(n.sessionId)) || "";
|
|
2590
|
+
const sig = rec.status + "|" + flat.map(([n, d]) => [n.sessionId, effTitle(n), n.live ? 1 : 0, n.sizeBytes, d].join(":")).join(";");
|
|
2522
2591
|
if (box.dataset.dsmSig === sig) return;
|
|
2523
2592
|
box.dataset.dsmSig = sig;
|
|
2524
2593
|
box.textContent = "";
|
|
@@ -2557,7 +2626,7 @@ function installSidebarStatusDots() {
|
|
|
2557
2626
|
});
|
|
2558
2627
|
const name = document.createElement("span");
|
|
2559
2628
|
name.className = "dsm-sub-name";
|
|
2560
|
-
name.textContent = n
|
|
2629
|
+
name.textContent = effTitle(n) || String(n.sessionId).slice(0, 8) + "\u2026";
|
|
2561
2630
|
name.title = n.sessionId;
|
|
2562
2631
|
r.appendChild(name);
|
|
2563
2632
|
const meta = document.createElement("span");
|
|
@@ -2592,11 +2661,13 @@ function installSidebarStatusDots() {
|
|
|
2592
2661
|
const FALLBACK_TICK_MS = 4e3;
|
|
2593
2662
|
const tick = () => {
|
|
2594
2663
|
if (typeof document !== "undefined" && document.hidden) return;
|
|
2595
|
-
|
|
2664
|
+
const cadence = dsmWarmPending ? 1 : 8;
|
|
2665
|
+
if (++dsmTrashTick % cadence === 0) dsmLoadTrashIds();
|
|
2596
2666
|
paint();
|
|
2597
2667
|
};
|
|
2598
2668
|
tick();
|
|
2599
2669
|
dsmLoadTrashIds();
|
|
2670
|
+
const coldSecondBeat = setTimeout(() => dsmLoadTrashIds(), 1500);
|
|
2600
2671
|
paint();
|
|
2601
2672
|
const obs = new MutationObserver(schedulePaint);
|
|
2602
2673
|
obs.observe(document.body, {
|
|
@@ -2609,6 +2680,7 @@ function installSidebarStatusDots() {
|
|
|
2609
2680
|
const tickTimer = setInterval(tick, FALLBACK_TICK_MS);
|
|
2610
2681
|
return () => {
|
|
2611
2682
|
clearInterval(tickTimer);
|
|
2683
|
+
clearTimeout(coldSecondBeat);
|
|
2612
2684
|
obs.disconnect();
|
|
2613
2685
|
dsmTeardownSidebarAug();
|
|
2614
2686
|
};
|