@michengai/dsh-archive-manager 0.1.30 → 0.1.31

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,13 @@
4
4
 
5
5
  Published release notes are retained below; new versions are added without removing earlier entries.
6
6
 
7
+ ## Unreleased
8
+
9
+ ## 0.1.31 - 2026-09-07
10
+
11
+ - Added independent in-product update checks with automatic updates when a verified DSH update service is available and a profile-specific manual fallback otherwise.
12
+ - Removed the update-button dependency on `react-dom/client` so the client can load on Hosts that do not register that module id.
13
+
7
14
  ## 0.1.30 — 2026-09-04
8
15
 
9
16
  - Kept failed archived-session deletions retryable until their transcript artifact is removed.
@@ -4,6 +4,13 @@
4
4
 
5
5
  以下发布说明会持续保留;新增版本时不再删除较早记录。
6
6
 
7
+ ## 未发布
8
+
9
+ ## 0.1.31 - 2026-09-07
10
+
11
+ - 新增独立的插件内检查更新能力:仅在已验证的 DSH 更新服务可用时自动更新,其余环境提供与 Profile 对应的手工更新命令。
12
+ - 更新按钮图标不再依赖 `react-dom/client`,避免宿主未注册该模块 id 时客户端无法加载。
13
+
7
14
  ## 0.1.30 — 2026-09-04
8
15
 
9
16
  - 归档会话物理删除失败时保留可重试状态,直至转录工件删除成功。
package/README.md CHANGED
@@ -23,7 +23,7 @@
23
23
 
24
24
  - Archive a session from the sidebar session menu.
25
25
  - Archive every active chat in a workspace from its sidebar action menu.
26
- - Search archived chats, sort them by update time, creation time, or title, and filter them by workspace in **Settings → Archived**.
26
+ - Search archived chats, sort them by update time, creation time, or title, and filter them by workspace in **Settings → Archived sessions**.
27
27
  - Restore a session to its original workspace with **Unarchive**.
28
28
  - Restore or permanently delete every archived chat in a project group.
29
29
  - Restore all archived chats from the page header.
@@ -38,9 +38,9 @@ Open the sidebar session menu and choose **Archive session**:
38
38
 
39
39
  ![Archive a session from the session menu](assets/screenshots/archive-session-menu.png)
40
40
 
41
- Search, sort, filter by project, unarchive, or permanently delete chats in **Settings → Archived**:
41
+ Search, sort, filter by project, unarchive, or permanently delete chats in **Settings → Archived sessions**:
42
42
 
43
- ![Archived chats settings page](assets/screenshots/archived-sessions.png)
43
+ ![Archived sessions settings page](assets/screenshots/archived-sessions.png)
44
44
 
45
45
  ## DSH product ecosystem
46
46
 
@@ -85,7 +85,7 @@ Install the DSH plugin from source at https://github.com/MichengAI/dsh-archive-m
85
85
  | Codex | Send one of the sentences above to Codex and let it install locally. |
86
86
  | WorkBuddy | Send one of the sentences above to WorkBuddy; for a source install you can also paste `https://github.com/MichengAI/dsh-archive-manager`. |
87
87
 
88
- Codex and WorkBuddy only install the plugin. After that, open DSH Web and use **Settings → Archived**.
88
+ Codex and WorkBuddy only install the plugin. After that, open DSH Web and use **Settings → Archived sessions**.
89
89
 
90
90
  You can also run the same npm command yourself:
91
91
 
@@ -128,10 +128,14 @@ dsh --profile web --dump-config
128
128
 
129
129
  Restart DSH Web and hard-refresh the browser. `dsh plugin ... add .` reads the package metadata and `cordis.patch.yml`; do not install by copying `lib` directly.
130
130
 
131
+ ## Updates
132
+
133
+ The settings title shows the installed version and a **Check for updates** button. When a newer release is available, **Update automatically** runs only when the DSH CLI or Desktop update service is available; otherwise, the dialog provides a profile-specific manual command to copy and run.
134
+
131
135
  ## Usage
132
136
 
133
137
  1. Open the sidebar session menu and choose **Archive session**.
134
- 2. Open **Settings → Archived** to inspect sessions by workspace.
138
+ 2. Open **Settings → Archived sessions** to inspect sessions by workspace.
135
139
  3. Search by title, sort by update time, creation time, or title, or filter the list by project.
136
140
  4. Select **Unarchive** to restore one session, or select **Restore all** in the page header.
137
141
  5. Open a project heading's action menu to restore or delete all of that project's archived chats.
package/README.zh-CN.md CHANGED
@@ -23,7 +23,7 @@
23
23
 
24
24
  - 在侧栏会话菜单中选择「归档会话」。
25
25
  - 在工作区侧栏操作菜单中一键归档该工作区全部未归档聊天。
26
- - 在「设置 → 已归档」按工作区查看归档会话,并支持搜索、按更新时间/创建时间/标题排序和按项目筛选。
26
+ - 在「设置 → 归档会话」按工作区查看归档会话,并支持搜索、按更新时间/创建时间/标题排序和按项目筛选。
27
27
  - 安全取消归档,将会话恢复到原工作区位置。
28
28
  - 在项目分组中批量恢复或永久删除该项目的全部已归档聊天。
29
29
  - 在页面顶部一键恢复全部已归档聊天。
@@ -38,7 +38,7 @@
38
38
 
39
39
  ![从会话菜单归档会话](assets/screenshots/archive-session-menu.png)
40
40
 
41
- 在「设置 → 已归档」中搜索、排序、按项目筛选、取消归档或永久删除:
41
+ 在「设置 → 归档会话」中搜索、排序、按项目筛选、取消归档或永久删除:
42
42
 
43
43
  ![已归档聊天设置页面](assets/screenshots/archived-sessions.png)
44
44
 
@@ -85,7 +85,7 @@
85
85
  | Codex | 把上面其中一句发给 Codex,让它在本机执行安装。 |
86
86
  | WorkBuddy | 把上面其中一句发给 WorkBuddy;源码安装也可同时粘贴仓库地址 `https://github.com/MichengAI/dsh-archive-manager`。 |
87
87
 
88
- Codex 和 WorkBuddy 只负责代装;装好后仍要打开 DSH Web 使用「设置 → 已归档」。
88
+ Codex 和 WorkBuddy 只负责代装;装好后仍要打开 DSH Web 使用「设置 → 归档会话」。
89
89
 
90
90
  也可以自己执行同一条 npm 命令:
91
91
 
@@ -128,10 +128,14 @@ dsh --profile web --dump-config
128
128
 
129
129
  完成后重启 DSH Web 并硬刷新浏览器。`dsh plugin ... add .` 会读取当前目录的包信息和 `cordis.patch.yml`;不要改为直接复制 `lib` 目录。
130
130
 
131
+ ## 在线更新
132
+
133
+ 设置标题会显示当前版本和“检查更新”按钮。发现新版后,只有检测到 DSH CLI 或 Desktop 更新服务时才可使用“自动更新”;其他环境会在弹窗中提供可复制、与当前 Profile 对应的手工更新命令。
134
+
131
135
  ## 使用
132
136
 
133
137
  1. 在侧栏右键或打开会话菜单,选择「归档会话」。
134
- 2. 打开「设置 → 已归档」,按工作区查看归档会话。
138
+ 2. 打开「设置 → 归档会话」,按工作区查看归档会话。
135
139
  3. 按标题搜索,按更新时间、创建时间或标题排序,或按项目筛选列表。
136
140
  4. 点击「取消归档」恢复单个会话,或在顶部点击「全部恢复」。
137
141
  5. 打开项目标题右侧菜单,可恢复或删除该项目的全部已归档聊天。
package/lib/client.js CHANGED
@@ -1,4 +1,295 @@
1
1
  (() => {
2
+ // src/plugin-update-ui.js
3
+ var UPDATE_HEADER = "x-michengai-plugin-update";
4
+ var STYLE_ID = "michengai-plugin-update-ui";
5
+ var CSS = `
6
+ .mpi-version{margin-left:8px;color:var(--dsw-alias-label-tertiary,#9da1aa);font-family:inherit;font-size:12px;font-weight:500;line-height:18px;letter-spacing:0;white-space:nowrap;vertical-align:baseline}.mpi-check{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:28px;padding:0 8px;border:1px solid var(--dsw-alias-border-l2,#4b4d52);border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary,#b8bbc2);font:inherit;font-size:12px;font-weight:500;line-height:18px;white-space:nowrap;cursor:pointer}.mpi-check:hover{background:var(--dsw-alias-interactive-bg-hover,#3a3b3f);color:var(--dsw-alias-label-primary,#fff)}.mpi-check:focus-visible,.mpi-action:focus-visible,.mpi-dialog-close:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4f8cff);outline-offset:2px}.mpi-icon{display:inline-flex;flex:0 0 auto;width:16px;height:16px;align-items:center;justify-content:center;pointer-events:none}.mpi-icon svg{display:block;width:16px;height:16px}
7
+ .mpi-overlay{position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(0,0,0,.62)}.mpi-dialog{position:relative;box-sizing:border-box;width:min(680px,100%);max-height:calc(100vh - 48px);overflow:auto;border:1px solid var(--dsw-alias-border-l2,#4b4d52);border-radius:14px;padding:22px;background:var(--dsw-alias-bg-layer-2,var(--dsw-specific-menu,#202124));color:var(--dsw-alias-label-primary,#fff);box-shadow:var(--dsw-shadow-lv3,0 16px 48px rgba(0,0,0,.24));font-family:inherit}.mpi-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.mpi-dialog h2{margin:0;font-size:18px;line-height:26px}.mpi-dialog-close{display:inline-flex;flex:0 0 28px;width:28px;height:28px;align-items:center;justify-content:center;padding:0;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary,#b8bbc2);cursor:pointer}.mpi-dialog-close:hover{background:var(--dsw-alias-interactive-bg-hover,#3a3b3f);color:var(--dsw-alias-label-primary,#fff)}.mpi-intro{margin:8px 0 18px;color:var(--dsw-alias-label-secondary,#c2c4ca);font-size:13px;line-height:20px}.mpi-meta{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:8px 18px;margin:0 0 16px;font-size:12px;line-height:18px}.mpi-meta dt{color:var(--dsw-alias-label-secondary,#c2c4ca)}.mpi-meta dd{margin:0;font-family:ui-monospace,SFMono-Regular,Consolas,monospace}.mpi-status{margin:0 0 18px;border-radius:7px;padding:12px 14px;background:var(--dsw-alias-bg-layer-3,var(--dsw-specific-menu-item-hover,#252527));font-size:13px;font-weight:600;line-height:20px}.mpi-status[data-kind=error]{color:var(--dsw-alias-state-error-primary,#ff6464)}.mpi-status[data-kind=success]{color:var(--dsw-alias-state-success-primary,#36d67a)}.mpi-manual{border-top:1px solid var(--dsw-alias-border-l2,#4b4d52);padding-top:16px}.mpi-manual h3{margin:0 0 6px;font-size:14px;line-height:20px}.mpi-manual p{margin:0 0 10px;color:var(--dsw-alias-label-secondary,#c2c4ca);font-size:12px;line-height:18px}.mpi-command{display:flex;align-items:center;gap:8px;border:1px solid var(--dsw-alias-border-l2,#4b4d52);border-radius:7px;padding:10px;background:var(--dsw-alias-bg-layer-3,var(--dsw-specific-menu-item-hover,#252527))}.mpi-command code{min-width:0;flex:1;overflow:auto;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;line-height:18px;white-space:nowrap}.mpi-actions{display:flex;align-items:center;justify-content:flex-end;gap:12px;margin-top:18px}.mpi-actions-group{display:flex;gap:8px}.mpi-action{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:32px;border:1px solid var(--dsw-alias-border-l2,#4b4d52);border-radius:7px;padding:6px 10px;background:transparent;color:inherit;font:inherit;font-size:12px;cursor:pointer}.mpi-action:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#414247)}.mpi-action:disabled{cursor:not-allowed;opacity:.55}.mpi-primary{border-color:var(--dsw-alias-state-business-primary,#4f8cff);background:var(--dsw-alias-state-business-primary,#4f8cff);color:#fff}.mpi-progress{height:4px;margin-top:10px;overflow:hidden;border-radius:99px;background:var(--dsw-alias-border-l2,#4b4d52)}.mpi-progress::after{display:block;width:32%;height:100%;background:var(--dsw-alias-state-business-primary,#4f8cff);content:'';animation:mpi-wave 1.15s ease-in-out infinite}@keyframes mpi-wave{from{transform:translateX(-110%)}to{transform:translateX(330%)}}@media(max-width:560px){.mpi-overlay{padding:10px}.mpi-dialog{max-height:calc(100vh - 20px);padding:16px}.mpi-actions{align-items:stretch}.mpi-actions-group{justify-content:flex-end;flex-wrap:wrap}.mpi-meta{grid-template-columns:1fr;gap:2px}.mpi-meta dd{margin-bottom:6px}}
8
+ `;
9
+ var ZH = {
10
+ check: "\u68C0\u67E5\u66F4\u65B0",
11
+ update: "\u66F4\u65B0",
12
+ close: "\u5173\u95ED",
13
+ recheck: "\u91CD\u65B0\u68C0\u67E5",
14
+ auto: "\u81EA\u52A8\u66F4\u65B0",
15
+ updating: "\u6B63\u5728\u66F4\u65B0\u2026",
16
+ copy: "\u590D\u5236\u547D\u4EE4",
17
+ copied: "\u5DF2\u590D\u5236",
18
+ copyFailed: "\u590D\u5236\u5931\u8D25",
19
+ checking: "\u6B63\u5728\u68C0\u67E5\u66F4\u65B0\u2026",
20
+ latest: "\u5DF2\u662F\u6700\u65B0\u7248\u672C",
21
+ found: "\u53D1\u73B0\u65B0\u7248\u672C",
22
+ failed: "\u68C0\u67E5\u66F4\u65B0\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",
23
+ current: "\u8FD0\u884C\u7248\u672C",
24
+ latestLabel: "\u6700\u65B0\u7248\u672C",
25
+ profile: "\u76EE\u6807 profile",
26
+ unknown: "\u672A\u77E5",
27
+ manual: "\u624B\u5DE5\u66F4\u65B0",
28
+ manualHint: "\u81EA\u52A8\u66F4\u65B0\u5931\u8D25\u65F6\uFF0C\u53EF\u5728\u5F53\u524D DSH \u7EC8\u7AEF\u6267\u884C\u4EE5\u4E0B\u547D\u4EE4\uFF0C\u5B8C\u6210\u540E\u91CD\u542F DSH Web\u3002",
29
+ intro: "\u4EC5\u68C0\u67E5\u5E76\u66F4\u65B0\u5F53\u524D\u63D2\u4EF6\uFF0C\u4E0D\u4F1A\u8054\u52A8\u5B89\u88C5\u5176\u4ED6\u63D2\u4EF6\u3002",
30
+ restart: "\u66F4\u65B0\u5B8C\u6210\uFF0C\u8BF7\u91CD\u542F DSH Web\u3002",
31
+ restarting: "\u66F4\u65B0\u5B8C\u6210\uFF0C\u6B63\u5728\u91CD\u542F DSH Desktop\u2026",
32
+ unavailable: "\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u81EA\u52A8\u66F4\u65B0\uFF0C\u8BF7\u4F7F\u7528\u624B\u5DE5\u66F4\u65B0\u547D\u4EE4\u3002"
33
+ };
34
+ var EN = {
35
+ check: "Check for updates",
36
+ update: "Update",
37
+ close: "Close",
38
+ recheck: "Check again",
39
+ auto: "Update automatically",
40
+ updating: "Updating\u2026",
41
+ copy: "Copy command",
42
+ copied: "Copied",
43
+ copyFailed: "Copy failed",
44
+ checking: "Checking for updates\u2026",
45
+ latest: "You are up to date",
46
+ found: "New version available",
47
+ failed: "Could not check for updates. Try again later.",
48
+ current: "Running version",
49
+ latestLabel: "Latest version",
50
+ profile: "Target profile",
51
+ unknown: "Unknown",
52
+ manual: "Manual update",
53
+ manualHint: "If automatic update fails, run this command in the current DSH terminal, then restart DSH Web.",
54
+ intro: "Only this plugin is checked and updated. Other plugins are not changed.",
55
+ restart: "Update complete. Restart DSH Web.",
56
+ restarting: "Update complete. Restarting DSH Desktop\u2026",
57
+ unavailable: "Automatic update is unavailable. Use the manual command."
58
+ };
59
+ function strings() {
60
+ const lang = document.documentElement.lang.toLowerCase();
61
+ const settings = document.querySelector('[role="dialog"]')?.textContent ?? "";
62
+ return lang.startsWith("en") || settings.includes("Settings") && !settings.includes("\u8BBE\u7F6E") ? EN : ZH;
63
+ }
64
+ function ensureStyle() {
65
+ if (document.getElementById(STYLE_ID) !== null) return;
66
+ const style = document.createElement("style");
67
+ style.id = STYLE_ID;
68
+ style.textContent = CSS;
69
+ (document.head ?? document.documentElement).append(style);
70
+ }
71
+ function validPayload(value) {
72
+ if (value === null || typeof value !== "object") return false;
73
+ const item = value;
74
+ return typeof item.packageName === "string" && typeof item.currentVersion === "string" && typeof item.updateAvailable === "boolean" && typeof item.profileName === "string" && typeof item.canAutoUpdate === "boolean" && typeof item.latestCheckFailed === "boolean" && (item.latestVersion === void 0 || typeof item.latestVersion === "string");
75
+ }
76
+ async function requestStatus(endpoint, method, signal) {
77
+ const signalOption = signal === void 0 ? {} : { signal };
78
+ const response = await fetch(endpoint, method === "GET" ? { cache: "no-store", ...signalOption } : {
79
+ method: "POST",
80
+ headers: { "content-type": "application/json", [UPDATE_HEADER]: "1" },
81
+ body: "{}",
82
+ ...signalOption
83
+ });
84
+ const value = await response.json();
85
+ if (!response.ok || !validPayload(value)) throw new Error(typeof value.error === "string" ? value.error : strings().failed);
86
+ return value;
87
+ }
88
+ function manualPluginUpdateCommand(profileName, packageName, version) {
89
+ const profile = profileName.trim() === "" ? "" : ` --profile ${profileName.trim()}`;
90
+ return `dsh plugin${profile} add ${packageName}@${version} --registry=https://registry.npmjs.org/`;
91
+ }
92
+ function handlePluginUpdateEscape(event, close) {
93
+ if (event.key !== "Escape") return false;
94
+ event.preventDefault();
95
+ event.stopPropagation();
96
+ event.stopImmediatePropagation();
97
+ close();
98
+ return true;
99
+ }
100
+ function observePluginUpdate(options) {
101
+ if (typeof document === "undefined" || document.body === null) return () => {
102
+ };
103
+ ensureStyle();
104
+ const controller = new AbortController();
105
+ let payload;
106
+ let overlay;
107
+ let frame;
108
+ const setButtonContent = (button, label, iconName) => {
109
+ const icon = options.createIcon(iconName);
110
+ icon.classList.add("mpi-icon");
111
+ icon.setAttribute("aria-hidden", "true");
112
+ const text = document.createElement("span");
113
+ text.dataset.mpiLabel = "";
114
+ text.textContent = label;
115
+ button.replaceChildren(icon, text);
116
+ };
117
+ const setButtonLabel = (button, label) => {
118
+ const text = button.querySelector("[data-mpi-label]");
119
+ if (text === null) button.textContent = label;
120
+ else text.textContent = label;
121
+ };
122
+ const applyControls = () => {
123
+ const row = document.querySelector(options.titleRowSelector);
124
+ if (row === null) return;
125
+ const heading = row.querySelector("h1,h2");
126
+ if (heading !== null && payload !== void 0) {
127
+ let version = heading.querySelector(`.mpi-version[data-package="${options.packageName}"]`);
128
+ if (version === null) {
129
+ version = document.createElement("span");
130
+ version.className = "mpi-version";
131
+ version.dataset.package = options.packageName;
132
+ heading.append(version);
133
+ }
134
+ const versionLabel = `v${payload.currentVersion}`;
135
+ if (version.textContent !== versionLabel) version.textContent = versionLabel;
136
+ }
137
+ const links = row.querySelector(options.linksSelector);
138
+ if (links === null || links.querySelector(`[data-mpi-check="${options.packageName}"]`) !== null) return;
139
+ const button = document.createElement("button");
140
+ button.type = "button";
141
+ button.className = "mpi-check";
142
+ button.dataset.mpiCheck = options.packageName;
143
+ setButtonContent(button, strings().check, "refresh");
144
+ button.addEventListener("click", openDialog);
145
+ links.append(button);
146
+ };
147
+ const load = async () => {
148
+ payload = await requestStatus(options.endpoint, "GET", controller.signal);
149
+ applyControls();
150
+ return payload;
151
+ };
152
+ const closeDialog = () => {
153
+ overlay?.remove();
154
+ overlay = void 0;
155
+ };
156
+ function openDialog() {
157
+ closeDialog();
158
+ const text = strings();
159
+ overlay = document.createElement("div");
160
+ overlay.className = "mpi-overlay";
161
+ const dialog = document.createElement("section");
162
+ dialog.className = "mpi-dialog";
163
+ dialog.setAttribute("role", "dialog");
164
+ dialog.setAttribute("aria-modal", "true");
165
+ dialog.innerHTML = `<header class="mpi-head"><h2></h2><button type="button" class="mpi-dialog-close" data-action="close"></button></header><p class="mpi-intro"></p><dl class="mpi-meta"><dt></dt><dd data-role="current"></dd><dt></dt><dd data-role="latest"></dd><dt></dt><dd data-role="profile"></dd></dl><div class="mpi-status" role="status"></div><div class="mpi-progress" hidden></div><section class="mpi-manual"><h3></h3><p></p><div class="mpi-command"><code></code><button type="button" class="mpi-action" data-action="copy"></button></div></section><footer class="mpi-actions"><div class="mpi-actions-group"><button type="button" class="mpi-action" data-action="check"></button><button type="button" class="mpi-action mpi-primary" data-action="update"></button></div></footer>`;
166
+ const name = document.documentElement.lang.toLowerCase().startsWith("en") ? options.enName : options.zhName;
167
+ dialog.querySelector("h2").textContent = `${name} ${text.update}`;
168
+ dialog.querySelector(".mpi-intro").textContent = text.intro;
169
+ const terms = dialog.querySelectorAll("dt");
170
+ terms[0].textContent = text.current;
171
+ terms[1].textContent = text.latestLabel;
172
+ terms[2].textContent = text.profile;
173
+ dialog.querySelector(".mpi-manual h3").textContent = text.manual;
174
+ dialog.querySelector(".mpi-manual p").textContent = text.manualHint;
175
+ const status = dialog.querySelector(".mpi-status");
176
+ const progress = dialog.querySelector(".mpi-progress");
177
+ const command = dialog.querySelector(".mpi-command code");
178
+ const close = dialog.querySelector("[data-action=close]");
179
+ const check = dialog.querySelector("[data-action=check]");
180
+ const update = dialog.querySelector("[data-action=update]");
181
+ const copy = dialog.querySelector("[data-action=copy]");
182
+ setButtonContent(close, text.close, "close");
183
+ close.querySelector("[data-mpi-label]")?.remove();
184
+ close.setAttribute("aria-label", text.close);
185
+ close.title = text.close;
186
+ setButtonContent(check, text.recheck, "refresh");
187
+ setButtonContent(update, text.auto, "download");
188
+ setButtonContent(copy, text.copy, "copy");
189
+ let busy = false;
190
+ const setMessage = (message, kind = "") => {
191
+ status.textContent = message;
192
+ status.dataset.kind = kind;
193
+ };
194
+ const setBusy = (value) => {
195
+ busy = value;
196
+ check.disabled = value;
197
+ copy.disabled = value;
198
+ update.disabled = value || payload?.canAutoUpdate !== true || payload.updateAvailable !== true;
199
+ progress.hidden = !value;
200
+ };
201
+ const render = () => {
202
+ dialog.querySelector("[data-role=current]").textContent = payload === void 0 ? text.unknown : `v${payload.currentVersion}`;
203
+ dialog.querySelector("[data-role=latest]").textContent = payload?.latestVersion === void 0 ? text.unknown : `v${payload.latestVersion}`;
204
+ dialog.querySelector("[data-role=profile]").textContent = payload?.profileName ?? text.unknown;
205
+ command.textContent = manualPluginUpdateCommand(payload?.profileName ?? "", options.packageName, payload?.latestVersion ?? "latest");
206
+ update.disabled = busy || payload?.canAutoUpdate !== true || payload.updateAvailable !== true;
207
+ if (payload === void 0) setMessage(text.checking);
208
+ else if (payload.latestCheckFailed) setMessage(text.failed, "error");
209
+ else if (payload.updateAvailable) setMessage(`${text.found}: v${payload.latestVersion ?? text.unknown}`);
210
+ else setMessage(text.latest, "success");
211
+ if (payload !== void 0 && !payload.canAutoUpdate && payload.updateAvailable) setMessage(text.unavailable);
212
+ };
213
+ const checkNow = async () => {
214
+ if (busy) return;
215
+ setBusy(true);
216
+ setMessage(text.checking);
217
+ try {
218
+ await load();
219
+ setBusy(false);
220
+ render();
221
+ } catch (error) {
222
+ setBusy(false);
223
+ setMessage(error instanceof Error ? error.message : text.failed, "error");
224
+ }
225
+ };
226
+ const updateNow = async () => {
227
+ if (busy) return;
228
+ setBusy(true);
229
+ setButtonLabel(update, text.updating);
230
+ setMessage(text.updating);
231
+ try {
232
+ payload = await requestStatus(options.endpoint, "POST", controller.signal);
233
+ applyControls();
234
+ render();
235
+ setMessage(payload.autoReload === true ? text.restarting : text.restart, "success");
236
+ } catch (error) {
237
+ setMessage(error instanceof Error ? error.message : text.failed, "error");
238
+ } finally {
239
+ setButtonLabel(update, text.auto);
240
+ setBusy(false);
241
+ }
242
+ };
243
+ close.addEventListener("click", closeDialog);
244
+ check.addEventListener("click", () => {
245
+ void checkNow();
246
+ });
247
+ update.addEventListener("click", () => {
248
+ void updateNow();
249
+ });
250
+ copy.addEventListener("click", () => {
251
+ void navigator.clipboard?.writeText(command.textContent ?? "").then(() => {
252
+ setButtonLabel(copy, text.copied);
253
+ setTimeout(() => {
254
+ setButtonLabel(copy, text.copy);
255
+ }, 1400);
256
+ }).catch(() => {
257
+ setButtonLabel(copy, text.copyFailed);
258
+ });
259
+ });
260
+ overlay.addEventListener("click", (event) => {
261
+ if (event.target === overlay) closeDialog();
262
+ });
263
+ overlay.addEventListener("keydown", (event) => {
264
+ handlePluginUpdateEscape(event, closeDialog);
265
+ }, true);
266
+ overlay.append(dialog);
267
+ document.body.append(overlay);
268
+ render();
269
+ close.focus();
270
+ void checkNow();
271
+ }
272
+ const observer = new MutationObserver(() => {
273
+ if (frame !== void 0) return;
274
+ frame = window.requestAnimationFrame(() => {
275
+ frame = void 0;
276
+ applyControls();
277
+ });
278
+ });
279
+ observer.observe(document.body, { childList: true, subtree: true });
280
+ applyControls();
281
+ void load().catch(() => {
282
+ });
283
+ return () => {
284
+ controller.abort();
285
+ observer.disconnect();
286
+ closeDialog();
287
+ if (frame !== void 0) window.cancelAnimationFrame(frame);
288
+ document.querySelectorAll(`[data-mpi-check="${options.packageName}"],.mpi-version[data-package="${options.packageName}"]`).forEach((node) => node.remove());
289
+ };
290
+ }
291
+
292
+ // src/client.js
2
293
  window.__ModuleLoader__.load({
3
294
  id: "@michengai/dsh-archive-manager",
4
295
  factory: (require2) => {
@@ -16,6 +307,32 @@
16
307
  let react_jsx_runtime = require2("react/jsx-runtime");
17
308
  let react = require2("react");
18
309
  let _deepseek_ai_dsh_client_ui_primitives = require2("@deepseek-ai/dsh-client-ui-primitives");
310
+ const UPDATE_ICON_PATHS = {
311
+ refresh: ["M13.5 5.5V2.5m0 0h-3m3 0-2.1 2.1A5.5 5.5 0 1 0 13.2 12"],
312
+ download: ["M8 2v8m0 0 3-3m-3 3-3-3M3 13v2h10v-2"],
313
+ copy: ["M5 5h8v8H5z", "M3 3h8"],
314
+ close: ["m4 4 8 8M12 4 4 12"]
315
+ };
316
+ function createPluginUpdateIcon(name) {
317
+ const element = document.createElement("span");
318
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
319
+ svg.setAttribute("viewBox", "0 0 16 16");
320
+ svg.setAttribute("width", "16");
321
+ svg.setAttribute("height", "16");
322
+ svg.setAttribute("fill", "none");
323
+ svg.setAttribute("stroke", "currentColor");
324
+ svg.setAttribute("stroke-width", "1.5");
325
+ svg.setAttribute("stroke-linecap", "round");
326
+ svg.setAttribute("stroke-linejoin", "round");
327
+ svg.setAttribute("aria-hidden", "true");
328
+ for (const d of UPDATE_ICON_PATHS[name]) {
329
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
330
+ path.setAttribute("d", d);
331
+ svg.append(path);
332
+ }
333
+ element.append(svg);
334
+ return element;
335
+ }
19
336
  const sessionIdSchema = {
20
337
  parse(value) {
21
338
  if (typeof value !== "string" || value.length === 0) throw new TypeError(`sessionId must be a non-empty string, got ${String(value)}`);
@@ -3180,9 +3497,9 @@
3180
3497
  "menu.addWorkspace": "\u6DFB\u52A0\u5DE5\u4F5C\u533A\u2026",
3181
3498
  "menu.unarchive": "\u53D6\u6D88\u5F52\u6863",
3182
3499
  "menu.deleteSession": "\u5220\u9664\u4F1A\u8BDD",
3183
- "archived.badge": "\u5DF2\u5F52\u6863",
3500
+ "archived.badge": "\u5F52\u6863\u4F1A\u8BDD",
3184
3501
  "archived.notOpenable": "\u5DF2\u5F52\u6863\uFF0C\u53D6\u6D88\u5F52\u6863\u540E\u53EF\u7EE7\u7EED\u5BF9\u8BDD",
3185
- "archives.title": "\u5DF2\u5F52\u6863\u7684\u804A\u5929",
3502
+ "archives.title": "\u5F52\u6863\u4F1A\u8BDD",
3186
3503
  "archives.description": "\u7BA1\u7406\u5DF2\u5F52\u6863\u7684\u4F1A\u8BDD\u3002",
3187
3504
  "archives.viewProject": "GitHub",
3188
3505
  "archives.feedback": "\u95EE\u9898\u53CD\u9988",
@@ -3310,9 +3627,9 @@
3310
3627
  "menu.addWorkspace": "Add workspace\u2026",
3311
3628
  "menu.unarchive": "Unarchive",
3312
3629
  "menu.deleteSession": "Delete session",
3313
- "archived.badge": "Archived",
3630
+ "archived.badge": "Archived sessions",
3314
3631
  "archived.notOpenable": "This session is archived. Unarchive it to continue the conversation.",
3315
- "archives.title": "Archived chats",
3632
+ "archives.title": "Archived sessions",
3316
3633
  "archives.description": "Manage archived sessions.",
3317
3634
  "archives.viewProject": "GitHub",
3318
3635
  "archives.feedback": "Issues",
@@ -3589,6 +3906,15 @@
3589
3906
  zh,
3590
3907
  en
3591
3908
  }), "dsh-archive-manager: dictionaries");
3909
+ ctx.effect(() => observePluginUpdate({
3910
+ endpoint: "/api/michengai/dsh-archive-manager/update",
3911
+ packageName: "@michengai/dsh-archive-manager",
3912
+ titleRowSelector: ".dsham_settingsTitleRow",
3913
+ linksSelector: ".dsham_settingsLinks",
3914
+ zhName: "\u5F52\u6863\u4F1A\u8BDD",
3915
+ enName: "Archived sessions",
3916
+ createIcon: createPluginUpdateIcon
3917
+ }), "dsh-archive-manager: plugin update ui");
3592
3918
  const uiWorkspaceAt = () => ctx.get("uiWorkspace");
3593
3919
  const searchSessions = async (query, signal) => {
3594
3920
  const result = await ctx.sessions.search(query, signal);
package/lib/index.js CHANGED
@@ -1,5 +1,13 @@
1
- function apply() {
1
+ import { registerPluginUpdater } from "./plugin-updater.js";
2
+ const inject = ["webServer"];
3
+ function apply(ctx) {
4
+ return registerPluginUpdater(ctx, {
5
+ endpoint: "/api/michengai/dsh-archive-manager/update",
6
+ packageName: "@michengai/dsh-archive-manager",
7
+ manifestUrl: new URL("../package.json", import.meta.url)
8
+ });
2
9
  }
3
10
  export {
4
- apply
11
+ apply,
12
+ inject
5
13
  };
@@ -0,0 +1,257 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { basename, dirname, isAbsolute, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ const PLUGIN_UPDATE_HEADER = "x-michengai-plugin-update";
8
+ const PLUGIN_UPDATE_IPC = "apply-plugin-updates";
9
+ function header(request, name) {
10
+ const value = request.headers?.[name];
11
+ return Array.isArray(value) ? value[0] : value;
12
+ }
13
+ function isLoopbackAddress(value) {
14
+ const address = value?.toLowerCase().replace(/^\[|\]$/g, "");
15
+ return address === "localhost" || address === "localhost." || address === "::1" || address?.startsWith("127.") === true || address?.startsWith("::ffff:127.") === true;
16
+ }
17
+ function isTrustedUpdateRequest(request) {
18
+ if (header(request, PLUGIN_UPDATE_HEADER) !== "1") return false;
19
+ if (!isLoopbackAddress(request.socket?.remoteAddress)) return false;
20
+ const site = header(request, "sec-fetch-site");
21
+ if (site !== void 0 && site !== "same-origin") return false;
22
+ const origin = header(request, "origin");
23
+ const host = header(request, "host");
24
+ if (origin === void 0 || host === void 0) return false;
25
+ try {
26
+ const url = new URL(origin);
27
+ return (url.protocol === "http:" || url.protocol === "https:") && isLoopbackAddress(url.hostname) && url.host === host;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+ function validProfileName(value) {
33
+ return typeof value === "string" && value !== "" && value !== "." && value !== ".." && !value.includes("/") && !value.includes("\\") && !/[\0-\x1f\x7f]/.test(value);
34
+ }
35
+ function profileNameFromArgv(argv) {
36
+ for (let index = 2; index < argv.length; index += 1) {
37
+ if (argv[index] === "--profile") return argv[index + 1];
38
+ if (argv[index]?.startsWith("--profile=")) return argv[index].slice("--profile=".length);
39
+ }
40
+ return argv[2] === "web" ? "web" : void 0;
41
+ }
42
+ function isDshCliEntry(entry, manifest, packageRoot) {
43
+ if (typeof manifest !== "object" || manifest === null) return false;
44
+ if (manifest.name !== "@deepseek-ai/dsh") return false;
45
+ const bin = typeof manifest.bin === "string" ? manifest.bin : typeof manifest.bin === "object" && manifest.bin !== null ? manifest.bin.dsh : void 0;
46
+ return typeof bin === "string" && bin !== "" && !isAbsolute(bin) && resolve(packageRoot, bin) === resolve(entry);
47
+ }
48
+ function cliEntry() {
49
+ const value = process.argv[1];
50
+ if (value === void 0 || value === "") return void 0;
51
+ const entry = value.startsWith("file:") ? fileURLToPath(value) : resolve(process.cwd(), value);
52
+ if (!existsSync(entry)) return void 0;
53
+ for (let directory = dirname(entry); ; ) {
54
+ const manifestPath = resolve(directory, "package.json");
55
+ if (existsSync(manifestPath)) {
56
+ try {
57
+ if (isDshCliEntry(entry, JSON.parse(readFileSync(manifestPath, "utf8")), directory)) return entry;
58
+ } catch {
59
+ }
60
+ }
61
+ const parent = dirname(directory);
62
+ if (parent === directory) return void 0;
63
+ directory = parent;
64
+ }
65
+ }
66
+ function runtime(ctx) {
67
+ const profiles = ctx.get?.("desktopProfiles");
68
+ const desktopPnpm = ctx.get?.("desktopPnpm");
69
+ if (profiles?.current !== void 0) {
70
+ const current = profiles.current;
71
+ if (!validProfileName(current.name) || typeof current.dir !== "string" || !isAbsolute(current.dir)) {
72
+ throw new Error("\u5F53\u524D Desktop Profile \u4FE1\u606F\u65E0\u6548\uFF0C\u8BF7\u91CD\u542F\u540E\u91CD\u8BD5\u3002");
73
+ }
74
+ return { profileName: current.name, profileDir: resolve(current.dir), ...typeof desktopPnpm?.runPlugin === "function" ? { desktopPnpm } : {} };
75
+ }
76
+ const profileDir = resolve(process.env.DSH_PROFILE_DIR ?? resolve(homedir(), ".dsh", "profiles", "web"));
77
+ const selected = profileNameFromArgv(process.argv);
78
+ const profileName = validProfileName(selected) ? selected : validProfileName(basename(profileDir)) ? basename(profileDir) : "web";
79
+ const entry = cliEntry();
80
+ return { profileName, profileDir, ...entry === void 0 ? {} : { cliEntry: entry } };
81
+ }
82
+ function parseSemver(value) {
83
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
84
+ if (match === null) return void 0;
85
+ return { core: [Number(match[1]), Number(match[2]), Number(match[3])], prerelease: match[4]?.split(".") ?? [] };
86
+ }
87
+ function isNewerVersion(currentValue, candidateValue) {
88
+ const current = parseSemver(currentValue);
89
+ const candidate = parseSemver(candidateValue);
90
+ if (current === void 0 || candidate === void 0) return false;
91
+ for (let index = 0; index < 3; index += 1) {
92
+ if (candidate.core[index] !== current.core[index]) return candidate.core[index] > current.core[index];
93
+ }
94
+ return comparePrerelease(candidate.prerelease, current.prerelease) > 0;
95
+ }
96
+ function comparePrerelease(left, right) {
97
+ if (left.length === 0 || right.length === 0) return left.length === right.length ? 0 : left.length === 0 ? 1 : -1;
98
+ const length = Math.max(left.length, right.length);
99
+ for (let index = 0; index < length; index += 1) {
100
+ const a = left[index];
101
+ const b = right[index];
102
+ if (a === void 0 || b === void 0) return a === b ? 0 : a === void 0 ? -1 : 1;
103
+ if (a === b) continue;
104
+ const aNumeric = /^\d+$/.test(a);
105
+ const bNumeric = /^\d+$/.test(b);
106
+ if (aNumeric && bNumeric) {
107
+ const aNumber = BigInt(a);
108
+ const bNumber = BigInt(b);
109
+ if (aNumber !== bNumber) return aNumber > bNumber ? 1 : -1;
110
+ continue;
111
+ }
112
+ if (aNumeric !== bNumeric) return aNumeric ? -1 : 1;
113
+ return a > b ? 1 : -1;
114
+ }
115
+ return 0;
116
+ }
117
+ let latestCache;
118
+ async function latestVersion(packageName) {
119
+ if (latestCache?.packageName === packageName && Date.now() < latestCache.expiresAt) return latestCache.version;
120
+ try {
121
+ const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, { signal: AbortSignal.timeout(8e3) });
122
+ if (!response.ok) return void 0;
123
+ const value = await response.json();
124
+ if (typeof value.version !== "string" || value.version === "") return void 0;
125
+ latestCache = { packageName, version: value.version, expiresAt: Date.now() + 5 * 6e4 };
126
+ return value.version;
127
+ } catch {
128
+ return void 0;
129
+ }
130
+ }
131
+ async function currentVersion(manifestUrl) {
132
+ const value = JSON.parse(await readFile(manifestUrl, "utf8"));
133
+ if (typeof value.version !== "string" || value.version === "") throw new Error("\u65E0\u6CD5\u8BFB\u53D6\u5F53\u524D\u63D2\u4EF6\u7248\u672C\u3002");
134
+ return value.version;
135
+ }
136
+ async function status(options, target) {
137
+ const current = await currentVersion(options.manifestUrl);
138
+ const latest = await latestVersion(options.packageName);
139
+ return {
140
+ packageName: options.packageName,
141
+ currentVersion: current,
142
+ ...latest === void 0 ? {} : { latestVersion: latest },
143
+ latestCheckFailed: latest === void 0,
144
+ updateAvailable: latest !== void 0 && isNewerVersion(current, latest),
145
+ profileName: target.profileName,
146
+ canAutoUpdate: target.desktopPnpm !== void 0 || target.cliEntry !== void 0
147
+ };
148
+ }
149
+ async function runCliInstall(target, packageSpec) {
150
+ if (target.cliEntry === void 0) throw new Error("\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u81EA\u52A8\u66F4\u65B0\uFF0C\u8BF7\u4F7F\u7528\u624B\u5DE5\u66F4\u65B0\u547D\u4EE4\u3002");
151
+ await new Promise((resolvePromise, reject) => {
152
+ const child = spawn(process.execPath, [target.cliEntry, "plugin", "--profile", target.profileName, "add", "--config.minimumReleaseAge=0", packageSpec, "--registry=https://registry.npmjs.org/"], {
153
+ cwd: target.profileDir,
154
+ windowsHide: true,
155
+ stdio: ["ignore", "pipe", "pipe"],
156
+ env: { ...process.env, NO_COLOR: "1" }
157
+ });
158
+ let detail = "";
159
+ child.stdout?.on("data", (chunk) => {
160
+ detail = (detail + String(chunk)).slice(-4e3);
161
+ });
162
+ child.stderr?.on("data", (chunk) => {
163
+ detail = (detail + String(chunk)).slice(-4e3);
164
+ });
165
+ const timer = setTimeout(() => {
166
+ child.kill();
167
+ reject(new Error("\u66F4\u65B0\u8D85\u65F6\uFF0C\u8BF7\u6539\u7528\u624B\u5DE5\u66F4\u65B0\u3002"));
168
+ }, 10 * 6e4);
169
+ child.once("error", (error) => {
170
+ clearTimeout(timer);
171
+ reject(error);
172
+ });
173
+ child.once("exit", (code) => {
174
+ clearTimeout(timer);
175
+ if (code === 0) resolvePromise();
176
+ else reject(new Error(detail.trim() || `\u66F4\u65B0\u8FDB\u7A0B\u9000\u51FA\u7801 ${String(code)}`));
177
+ });
178
+ });
179
+ }
180
+ async function install(target, packageSpec) {
181
+ if (target.desktopPnpm === void 0) return runCliInstall(target, packageSpec);
182
+ const handle = target.desktopPnpm.runPlugin(["add", "--config.minimumReleaseAge=0", packageSpec, "--registry=https://registry.npmjs.org/"], target.profileDir);
183
+ const result = await handle.done;
184
+ if (result.exitCode !== 0) throw new Error(`\u66F4\u65B0\u8FDB\u7A0B\u9000\u51FA\u7801 ${String(result.exitCode)}\u3002`);
185
+ }
186
+ function json(response, statusCode, value) {
187
+ response.writeHead(statusCode, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
188
+ response.end(JSON.stringify(value));
189
+ }
190
+ function publicError(error) {
191
+ const message = error instanceof Error ? error.message : "\u66F4\u65B0\u6682\u4E0D\u53EF\u7528\u3002";
192
+ return /[A-Za-z]:[\\/]|\/(?:home|root|Users|var|tmp)\//.test(message) ? "\u66F4\u65B0\u5931\u8D25\uFF0C\u8BF7\u67E5\u770B\u670D\u52A1\u7AEF\u65E5\u5FD7\u3002" : message;
193
+ }
194
+ function registerPluginUpdater(ctx, options) {
195
+ const host = ctx;
196
+ let installing = false;
197
+ return host.webServer.register({
198
+ kind: "exact",
199
+ path: options.endpoint,
200
+ handler: async (request, response) => {
201
+ try {
202
+ const target = runtime(host);
203
+ if (request.method === "GET" || request.method === "HEAD") {
204
+ const payload = await status(options, target);
205
+ response.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
206
+ response.end(request.method === "HEAD" ? void 0 : JSON.stringify(payload));
207
+ return;
208
+ }
209
+ if (request.method !== "POST") {
210
+ response.writeHead(405, { allow: "GET, HEAD, POST" });
211
+ response.end();
212
+ return;
213
+ }
214
+ if (!isTrustedUpdateRequest(request)) {
215
+ json(response, 403, { error: "\u5DF2\u62D2\u7EDD\u975E\u672C\u673A\u540C\u6E90\u66F4\u65B0\u8BF7\u6C42\u3002" });
216
+ return;
217
+ }
218
+ if (installing) {
219
+ json(response, 409, { error: "\u5F53\u524D\u63D2\u4EF6\u6B63\u5728\u66F4\u65B0\uFF0C\u8BF7\u7A0D\u5019\u3002" });
220
+ return;
221
+ }
222
+ const before = await status(options, target);
223
+ if (before.latestVersion === void 0) {
224
+ json(response, 503, { error: "\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\u6700\u65B0\u7248\u672C\u3002" });
225
+ return;
226
+ }
227
+ if (!before.updateAvailable) {
228
+ json(response, 200, before);
229
+ return;
230
+ }
231
+ installing = true;
232
+ try {
233
+ await install(target, `${options.packageName}@${before.latestVersion}`);
234
+ } finally {
235
+ installing = false;
236
+ }
237
+ const notifyParent = target.desktopPnpm === void 0 && typeof process.send === "function";
238
+ const autoReload = target.desktopPnpm !== void 0 || notifyParent;
239
+ json(response, 200, { ...before, updatedVersion: before.latestVersion, restartRequired: true, autoReload });
240
+ if (notifyParent) setTimeout(() => {
241
+ process.send?.(PLUGIN_UPDATE_IPC);
242
+ }, 150).unref?.();
243
+ } catch (error) {
244
+ ctx.logger.warn(`plugin updater failed: ${String(error)}`);
245
+ json(response, 503, { error: publicError(error) });
246
+ }
247
+ }
248
+ });
249
+ }
250
+ export {
251
+ PLUGIN_UPDATE_HEADER,
252
+ PLUGIN_UPDATE_IPC,
253
+ isDshCliEntry,
254
+ isNewerVersion,
255
+ isTrustedUpdateRequest,
256
+ registerPluginUpdater
257
+ };
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@michengai/dsh-archive-manager",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "description": "NPM-installable DSH Web plugin for managing archived sessions.",
5
5
  "type": "module",
6
- "packageManager": "pnpm@11.21.0",
6
+ "packageManager": "pnpm@11.22.0",
7
7
  "engines": {
8
8
  "node": "^22.19.0 || >=24.0.0"
9
9
  },
@@ -43,6 +43,7 @@
43
43
  "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.0-rc.5 <0.2.0",
44
44
  "@deepseek-ai/dsh-client-ui-sidebar": ">=0.1.0-rc.5 <0.2.0",
45
45
  "@deepseek-ai/dsh-client-ui-workspace": ">=0.1.0-rc.5 <0.2.0",
46
+ "@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.5 <0.2.0",
46
47
  "@deepseek-ai/dsh-session": ">=0.1.0-rc.5 <0.2.0",
47
48
  "@deepseek-ai/dsh-session-projection-cache": ">=0.1.0-rc.5 <0.2.0",
48
49
  "@deepseek-ai/dsh-spill-local": ">=0.1.0-rc.5 <0.2.0",
@@ -70,6 +71,7 @@
70
71
  "@deepseek-ai/dsh-client-ui-sidebar": "0.1.2-rc.1",
71
72
  "@deepseek-ai/dsh-client-ui-slots": "0.1.2-rc.1",
72
73
  "@deepseek-ai/dsh-client-ui-workspace": "0.1.2-rc.1",
74
+ "@deepseek-ai/dsh-host-webserver": "0.1.2-rc.1",
73
75
  "@deepseek-ai/dsh-invariants": "0.1.2-rc.1",
74
76
  "@deepseek-ai/dsh-scope": "0.1.2-rc.1",
75
77
  "@deepseek-ai/dsh-session": "0.1.2-rc.1",