dsh-session-mgr 0.6.8 → 0.6.9

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/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@
4
4
 
5
5
  All notable changes to `dsh-session-mgr` are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.6.9] - 2026-11-14
8
+
9
+ ### 中文
10
+
11
+ #### 修复
12
+ - **移动「已打开但闲置」的会话后,它不再变成僵尸。** 以前只把会话从内存的 session store 逐出,它的 Agent 仍留在 agent registry 里,会拿着搬移前的 `cwd` 与旧事件继续「接收」后续提示——提示看似被接受,实际上既不回覆也不写入磁盘。现在会先注销 Agent 再逐出会话,下次打开会从改写后的磁盘 header 重新开始。感谢 [@deathbypain](https://github.com/deathbypain) 回报并修复([#8](https://github.com/mienfong/dsh-session-mgr/issues/8) / [#7](https://github.com/mienfong/dsh-session-mgr/pull/7))。
13
+ - **DSH 0.2.2+ 上,会话标题栏的「移动」对话框不再被裁掉。** 0.2.2 起标题栏是自带 clipping 的容器,插件原本的 `position: fixed` 覆盖层被裁掉而看不见;现在改用应用提供的 portaled Modal 渲染。若较旧的 DSH 没有该模组,仍回退到原本的覆盖层(不会因此载入失败)。感谢 [@deathbypain](https://github.com/deathbypain) 回报并修复([#10](https://github.com/mienfong/dsh-session-mgr/issues/10) / [#9](https://github.com/mienfong/dsh-session-mgr/pull/9))。
14
+
15
+ #### 变更
16
+ - 安全说明补充「移动时会一并注销 Agent」;`detachLive` 现在导出并附回归测试(`scripts/test-move.mjs`)。
17
+
18
+ ### English
19
+
20
+ #### Fixes
21
+ - **A session moved while open-but-idle no longer turns into a zombie.** Previously only the session was evicted from the in-memory session store, while its Agent stayed in the agent registry and kept "accepting" later prompts against its pre-move `cwd` and stale event history — the prompts looked accepted but produced no reply and nothing was persisted. The Agent is now unregistered before the session is evicted, so the next open resumes from the rewritten on-disk header. Reported and fixed by [@deathbypain](https://github.com/deathbypain) ([#8](https://github.com/mienfong/dsh-session-mgr/issues/8) / [#7](https://github.com/mienfong/dsh-session-mgr/pull/7)).
22
+ - **On DSH 0.2.2+ the move dialog in the conversation header is no longer clipped away.** Since 0.2.2 the header is its own clipped container, so the plugin's `position: fixed` overlay was invisible; it now renders through the app's portaled Modal primitive. Harness builds without that module fall back to the plugin's own overlay instead of failing to load. Reported and fixed by [@deathbypain](https://github.com/deathbypain) ([#10](https://github.com/mienfong/dsh-session-mgr/issues/10) / [#9](https://github.com/mienfong/dsh-session-mgr/pull/9)).
23
+
24
+ #### Changed
25
+ - Safety notes now say the Agent is unregistered on move; `detachLive` is exported and covered by a regression test (`scripts/test-move.mjs`).
26
+
7
27
  ## [0.6.8] - 2026-10-04
8
28
 
9
29
  ### 中文
package/README.EN.md CHANGED
@@ -143,7 +143,7 @@ Archive/unarchive uses the registry-global archive set; an archived session keep
143
143
  ## Safety
144
144
 
145
145
  - **Running** sessions cannot be moved / archived / backed up / deleted (checked via the `agents` service).
146
- - Idle live sessions are evicted from memory before a move/delete so the next open reads the new header.
146
+ - Idle live sessions are evicted from memory before a move/delete so the next open reads the new header. The session's Agent is also unregistered from the agent registry, so a move never leaves a stale in-memory Agent that would shadow later prompts for that session.
147
147
  - Existing files/folders with the same name at the destination are **refused** (no silent overwrite).
148
148
  - Cross-volume (EXDEV) moves fall back to copy + delete source.
149
149
  - Delete requires a **double confirmation** and is irreversible — a backup is recommended.
package/README.md CHANGED
@@ -145,7 +145,7 @@ DSH 的「工作区」本质上是文件夹:会话通过 session header 的 `c
145
145
  ## 安全机制
146
146
 
147
147
  - **运行中**的会话不可移动/归档/备份/删除(通过 `agents` 服务判断)。
148
- - 已打开(live)但闲置的会话在移动/删除前会先从内存卸载,下次打开读取新 header。
148
+ - 已打开(live)但闲置的会话在移动/删除前会先从内存卸载,下次打开读取新 header;其 Agent 同时会从 agent registry 注销,避免残留的内存 Agent 接管该会话后续的提示。
149
149
  - 目标位置已存在同名文件/文件夹时**拒绝**并报错(不会静默覆盖)。
150
150
  - 跨磁盘(EXDEV)移动时自动改为复制+删除来源。
151
151
  - 删除需要**二次确认**且不可恢复——建议先备份。
package/lib/client.js CHANGED
@@ -18,6 +18,17 @@ window.__ModuleLoader__.load({
18
18
  var exports = module.exports;
19
19
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
20
20
  let react = require("react");
21
+ // The conversation header (DSH >= 0.2.2) owns a clipped region with its own
22
+ // compositor layer, so slot overlays have to render through the app's
23
+ // portaled Modal primitive to stay outside that clip. Older harness builds
24
+ // do not expose the primitives package at all: keep loading there and fall
25
+ // back to the plugin's own fixed overlay instead of failing the whole client.
26
+ let primitives;
27
+ try {
28
+ primitives = require("@deepseek-ai/dsh-client-ui-primitives");
29
+ } catch {
30
+ primitives = undefined;
31
+ }
21
32
 
22
33
  // ---------- styles ----------
23
34
  var STYLE_ID = "dsh-session-mgr-style";
@@ -116,8 +127,8 @@ window.__ModuleLoader__.load({
116
127
  "page.deleteSel": "Delete selected ({n})…",
117
128
  "page.tipNoArchiveSel": "No selected session can be archived",
118
129
  "page.tipNoRestoreSel": "No selected session can be restored",
119
- "page.ungrouped": "Ungrouped",
120
- "group.unreadable": "{n} unreadable session folder(s)",
130
+ "page.ungrouped": "Ungrouped",
131
+ "group.unreadable": "{n} unreadable session folder(s)",
121
132
  "group.unreadableHint": "The harness cannot read these session logs, so they appear in no listing. Deleting one removes its folder and cannot be undone.",
122
133
  "page.noTitle": "(untitled)",
123
134
  "page.created": "created",
@@ -249,8 +260,8 @@ window.__ModuleLoader__.load({
249
260
  "page.deleteSel": "删除选中 ({n})…",
250
261
  "page.tipNoArchiveSel": "选中的会话没有可归档的",
251
262
  "page.tipNoRestoreSel": "选中的会话没有可恢复的",
252
- "page.ungrouped": "未分组",
253
- "group.unreadable": "{n} 个无法读取的会话文件夹",
263
+ "page.ungrouped": "未分组",
264
+ "group.unreadable": "{n} 个无法读取的会话文件夹",
254
265
  "group.unreadableHint": "harness 读不出这些会话日志,因此它们不会出现在任何列表中。删除会一并移除其文件夹,且不可复原。",
255
266
  "page.noTitle": "(无标题)",
256
267
  "page.created": "创建于",
@@ -382,8 +393,8 @@ window.__ModuleLoader__.load({
382
393
  "page.deleteSel": "刪除選中 ({n})…",
383
394
  "page.tipNoArchiveSel": "沒有可歸檔的已選會話",
384
395
  "page.tipNoRestoreSel": "沒有可恢復的已選會話",
385
- "page.ungrouped": "未分組",
386
- "group.unreadable": "{n} 個無法讀取的會話資料夾",
396
+ "page.ungrouped": "未分組",
397
+ "group.unreadable": "{n} 個無法讀取的會話資料夾",
387
398
  "group.unreadableHint": "harness 讀不出這些會話日誌,因此它們不會出現在任何列表中。刪除會一併移除其資料夾,且不可復原。",
388
399
  "page.noTitle": "(無標題)",
389
400
  "page.created": "建立於",
@@ -733,31 +744,45 @@ window.__ModuleLoader__.load({
733
744
  )
734
745
  );
735
746
 
736
- return react.createElement("div", { className: "dsm-overlay", onClick: function (e) { if (e.target === e.currentTarget && !props.busy) props.onClose(); } },
737
- react.createElement("div", { className: "dsm-modal" },
738
- react.createElement("div", { className: "dsm-modal-title" }, props.title),
739
- props.subtitle ? react.createElement("div", { className: "dsm-modal-sub" }, props.subtitle) : null,
740
- react.createElement("div", null, t("moveModal.destLabel")),
741
- (!props.workspaces || props.workspaces.length === 0)
742
- ? react.createElement("div", { className: "dsm-hint" }, t("moveModal.noOther"))
743
- : null,
744
- react.createElement("div", { style: { display: "flex", flexDirection: "column", gap: "6px" } }, wsOptions, customOpt),
745
- useCustom ? react.createElement("input", {
746
- className: "dsm-input",
747
- placeholder: "D:\\projects\\my-project",
748
- value: customPath,
749
- onChange: function (e) { setCustomPath(e.target.value); }
750
- }) : null,
751
- props.error ? react.createElement("div", { className: "dsm-error", style: { padding: 0, textAlign: "left" } }, String(props.error)) : null,
752
- react.createElement("div", { className: "dsm-modal-actions" },
753
- react.createElement("button", { className: "dsm-btn", disabled: props.busy, onClick: props.onClose }, t("moveModal.cancel")),
754
- react.createElement("button", {
755
- className: "dsm-btn dsm-btn-primary",
756
- disabled: props.busy || !canConfirm,
757
- onClick: function () { props.onConfirm(target); }
758
- }, props.busy ? t("moveModal.busy") : t("moveModal.confirm"))
759
- )
747
+ var modalBody = [
748
+ react.createElement("div", { className: "dsm-modal-title" }, props.title),
749
+ props.subtitle ? react.createElement("div", { className: "dsm-modal-sub" }, props.subtitle) : null,
750
+ react.createElement("div", null, t("moveModal.destLabel")),
751
+ (!props.workspaces || props.workspaces.length === 0)
752
+ ? react.createElement("div", { className: "dsm-hint" }, t("moveModal.noOther"))
753
+ : null,
754
+ react.createElement("div", { style: { display: "flex", flexDirection: "column", gap: "6px" } }, wsOptions, customOpt),
755
+ useCustom ? react.createElement("input", {
756
+ className: "dsm-input",
757
+ placeholder: "D:\\projects\\my-project",
758
+ value: customPath,
759
+ onChange: function (e) { setCustomPath(e.target.value); }
760
+ }) : null,
761
+ props.error ? react.createElement("div", { className: "dsm-error", style: { padding: 0, textAlign: "left" } }, String(props.error)) : null,
762
+ react.createElement("div", { className: "dsm-modal-actions" },
763
+ react.createElement("button", { className: "dsm-btn", disabled: props.busy, onClick: props.onClose }, t("moveModal.cancel")),
764
+ react.createElement("button", {
765
+ className: "dsm-btn dsm-btn-primary",
766
+ disabled: props.busy || !canConfirm,
767
+ onClick: function () { props.onConfirm(target); }
768
+ }, props.busy ? t("moveModal.busy") : t("moveModal.confirm"))
760
769
  )
770
+ ];
771
+
772
+ // Portal through the app's Modal where available (required inside the
773
+ // conversation header on DSH >= 0.2.2); otherwise keep the local overlay.
774
+ if (primitives && typeof primitives.Modal === "function") {
775
+ return react.createElement(primitives.Modal, {
776
+ open: true,
777
+ onClose: function () { if (!props.busy) props.onClose(); },
778
+ title: props.title,
779
+ className: "dsm-modal",
780
+ headless: true,
781
+ children: modalBody
782
+ });
783
+ }
784
+ return react.createElement("div", { className: "dsm-overlay", onClick: function (e) { if (e.target === e.currentTarget && !props.busy) props.onClose(); } },
785
+ react.createElement("div", { className: "dsm-modal" }, modalBody)
761
786
  );
762
787
  }
763
788
 
package/lib/host.js CHANGED
@@ -282,8 +282,41 @@ function sessionRunning(ctx, sessionId) {
282
282
  return !!(agent && agent.status === "running");
283
283
  }
284
284
 
285
- /** Evict an idle live session from the in-memory store so the next open reads the rewritten header. */
286
- function detachLive(ctx, sessionId) {
285
+ /**
286
+ * Evict an idle live session from the in-memory store so the next open reads
287
+ * the rewritten header.
288
+ *
289
+ * This must ALSO unregister the session's Agent from the agent registry. The
290
+ * session store and the agent registry are independent in-memory structures
291
+ * with no cross-coupling: detaching the session alone leaves a live Agent
292
+ * registered under the same id, whose `.session` still points at the stale
293
+ * in-memory header. That zombie Agent then shadows every subsequent prompt for
294
+ * the id (it is resolved ahead of a disk resume and appends to its orphaned
295
+ * in-memory session, never the moved artifact), so the moved session silently
296
+ * stops working. Unregistering the Agent first forces the next resolve to
297
+ * resume fresh from the rewritten on-disk header.
298
+ *
299
+ * The agent-store entry carries no `.detach()`; the service's
300
+ * `detachEntered()` is the removal primitive (it deletes the entry and emits
301
+ * `agent/disposed`). Note it unregisters only: the Agent's full lifecycle
302
+ * teardown (scope disposal) is owned by its creator fiber and runs at process
303
+ * exit, so each moved idle session retains one Agent scope until then. That is
304
+ * bounded and minor; a deeper `disposeAgent(id)` hook would belong upstream in
305
+ * DSH.
306
+ *
307
+ * Best effort: a missing or refusing agents service must not fail the move.
308
+ * @returns true when the session was evicted (the caller treats false as fatal).
309
+ */
310
+ export function detachLive(ctx, sessionId) {
311
+ const agents = ctx.get("agents");
312
+ if (agents && typeof agents.detachEntered === "function") {
313
+ try {
314
+ const agentEntry = agents.store?.get?.(sessionId);
315
+ if (agentEntry !== undefined) agents.detachEntered(agentEntry);
316
+ } catch {
317
+ // ignore — session eviction below is what matters
318
+ }
319
+ }
287
320
  const sessions = ctx.get("sessions");
288
321
  if (!sessions) return false;
289
322
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-mgr",
3
- "version": "0.6.8",
3
+ "version": "0.6.9",
4
4
  "description": "Session manager for the DeepSeek Harness web UI: move, archive, restore, backup(portable archive) and import conversations across workspaces. Trilingual (English / 简体中文 / 繁體中文). · DSH Web 外掛「會話管理」:移動、歸檔、恢復、可攜式備份(壓縮檔)與匯入、刪除會話,支援中(簡/繁)英文介面。",
5
5
  "type": "module",
6
6
  "main": "lib/host.js",