dsh-theone 0.3.14 → 0.3.16

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
@@ -45,7 +45,7 @@ Each project gets its own background session that reasons, runs tools and compac
45
45
 
46
46
  - **Feels native.** Thinking streams in its usual place from the first token, and tool cards, approvals, questions, todo lists, retries and mid-reply steering all work as usual.
47
47
  - **Related work connects; unrelated work stays out.** Related topics share progress automatically and TheOne learns which ones belong together from how you use them. A topic's constraints (say, "budget figures stay out of the paper") are attached verbatim every time, so compaction never drops them.
48
- - **Gets better with use.** Say "wrong topic" or click **Move to…** in the directory, and similar messages go to the right place from then on.
48
+ - **Gets better with use.** Say "wrong topic" or click **Move to…** in the directory, and TheOne learns which terms tie that kind of message to the right topic (one small model call), so similar messages go there from then on. The directory shows how often routing was kept as is.
49
49
  - **Your old sessions become a topic directory.** After install it reads your existing sessions in the background, turns them into topics grouped into workspaces, and you pick up where you left off.
50
50
  - **Nothing extra to configure.** No separate API key: routing and the background sessions use the model you chose in DSH. When TheOne is the selected model, a **Background** button beside the model menu shows which model does the routing and the work, and switches it.
51
51
 
@@ -57,7 +57,7 @@ Each project gets its own background session that reasons, runs tools and compac
57
57
  2. Open **Plugins → Add plugin**, paste `https://github.com/YunongDai2005/dsh-theone`, and install.
58
58
  3. Click **TheOne · Main chat** in the sidebar and start talking.
59
59
 
60
- From the command line: `dsh plugin --profile web add github:YunongDai2005/dsh-theone --ignore-scripts`. Works with DSH `0.2.0-rc.2` and Node.js 24; the interface follows DSH's language (English / Simplified Chinese).
60
+ From the command line: `dsh plugin --profile web add github:YunongDai2005/dsh-theone --ignore-scripts`. Works with DSH `0.2.0-rc.2` and Node.js 24; the interface follows DSH's language (English / Simplified Chinese). Installing `dsh-theone` from npm also works, but DSH's package manager only accepts a version once it has been published for 24 hours, so the GitHub address gets new versions first. When an update is newer than that, the Update button explains it and can exempt TheOne alone so it installs right away.
61
61
 
62
62
  > Unofficial community project, maintained independently. It is not affiliated with or endorsed by DeepSeek.
63
63
 
package/README.zh.md CHANGED
@@ -45,7 +45,7 @@
45
45
 
46
46
  - **和原生会话一模一样**:思考从第一个字起就在原位置,工具卡片、授权、提问、待办、重试、回答中途插话,全都照常。
47
47
  - **相关的事会互通,无关的事互不打扰**:相关话题自动共享进展,并从你的使用中学习哪些话题有关联;话题的约束(比如「预算数字不能写进论文」)每次都原样带上,压缩不会把它丢掉。
48
- - **越用越准**:说「分错了」或在目录里点「改到…」,TheOne 会记住,类似的消息以后直接分对。
48
+ - **越用越准**:说「分错了」或在目录里点「改到…」,TheOne 会学到哪些词把这类消息和正确话题联系起来(只需一次很小的模型调用),类似的消息以后直接分对;话题目录里能看到分配的准确率。
49
49
  - **旧会话自动变成话题目录**:装上后它会在后台读取你已有的会话,整理成话题并按工作区分组,直接接着聊。
50
50
  - **零额外配置**:不用另填 API Key,路由和后台都用你在 DSH 里选好的模型。选中 TheOne 时,模型按钮旁边会出现 **后台** 按钮,显示并切换分配话题和干活用的模型。
51
51
 
@@ -57,7 +57,7 @@
57
57
  2. 打开 **插件 → 添加插件**,粘贴 `https://github.com/YunongDai2005/dsh-theone`,点击安装。
58
58
  3. 点击左侧 **TheOne · 主聊天**,开始说话。
59
59
 
60
- 命令行安装:`dsh plugin --profile web add github:YunongDai2005/dsh-theone --ignore-scripts`。兼容 DSH `0.2.0-rc.2` 与 Node.js 24;界面跟随 DSH 的语言(简体中文 / English)。
60
+ 命令行安装:`dsh plugin --profile web add github:YunongDai2005/dsh-theone --ignore-scripts`。兼容 DSH `0.2.0-rc.2` 与 Node.js 24;界面跟随 DSH 的语言(简体中文 / English)。也可以从 npm 安装 `dsh-theone`,但 DSH 的包管理器只接受发布满 24 小时的版本,所以用 GitHub 地址安装能最早拿到新版本。遇到这种情况,「更新」按钮会说明原因,并可以只为 TheOne 放行、立即安装。
61
61
 
62
62
  > 非官方社区项目,由社区成员独立维护,与 DeepSeek 不存在隶属或背书关系。
63
63
 
@@ -17,9 +17,21 @@ export declare const zh: {
17
17
  readonly 'update.available': "更新";
18
18
  readonly 'update.installing': "更新中…";
19
19
  readonly 'update.restart': "重启生效";
20
+ readonly 'update.waiting': "新版本";
21
+ readonly 'update.waitingHint': "有新版本 {latest},发布还不满 24 小时。点击查看如何现在安装。";
22
+ readonly 'age.title': "新版本发布还不满 24 小时";
23
+ readonly 'age.why': "DSH 用 pnpm 安装插件,它默认只安装发布满 24 小时的版本,用来防范被篡改的新包。TheOne {version} 刚发布不久,所以暂时装不上。";
24
+ readonly 'age.allowHow': "你可以只为 TheOne 放行:在这个 DSH 配置的 pnpm 设置(pnpm-workspace.yaml)里把 dsh-theone 加入例外,然后立即安装。其他插件仍然受 24 小时规则保护。";
25
+ readonly 'age.cannot': "当前无法自动修改这个 DSH 配置的 pnpm 设置。可以等它满 24 小时,或改用 GitHub 地址重新安装。";
26
+ readonly 'age.readyAt': "不放行的话,{time} 之后就能直接更新。";
27
+ readonly 'age.wait': "等满 24 小时";
28
+ readonly 'age.allow': "放行并更新";
29
+ readonly 'age.ok': "知道了";
20
30
  readonly 'update.reloading': "正在重新加载…";
21
31
  readonly 'update.reloadingHint': "TheOne 正在用新版本重新加载,DSH 不需要重启;完成后页面会自动刷新。";
22
32
  readonly 'update.busy': "有回复正在进行,等它结束后再点更新。";
33
+ readonly 'update.tooNew': "DSH 的安全策略只允许安装发布满 24 小时的版本,这一版还太新。过一阵再点一次即可。";
34
+ readonly 'update.network': "连不上下载源,请检查网络后点击重试。";
23
35
  readonly 'update.failed': "更新失败";
24
36
  readonly 'update.hint': "当前 {current},可更新到 {latest}。点击一键更新,不用重启 DSH。";
25
37
  readonly 'update.manualHint': "当前 {current},最新 {latest}。这份插件不是从 GitHub 或 npm 安装的,请在「插件」页面重新安装。";
@@ -17,9 +17,21 @@ export const zh = {
17
17
  'update.available': '更新',
18
18
  'update.installing': '更新中…',
19
19
  'update.restart': '重启生效',
20
+ 'update.waiting': "新版本",
21
+ 'update.waitingHint': "有新版本 {latest},发布还不满 24 小时。点击查看如何现在安装。",
22
+ 'age.title': "新版本发布还不满 24 小时",
23
+ 'age.why': "DSH 用 pnpm 安装插件,它默认只安装发布满 24 小时的版本,用来防范被篡改的新包。TheOne {version} 刚发布不久,所以暂时装不上。",
24
+ 'age.allowHow': "你可以只为 TheOne 放行:在这个 DSH 配置的 pnpm 设置(pnpm-workspace.yaml)里把 dsh-theone 加入例外,然后立即安装。其他插件仍然受 24 小时规则保护。",
25
+ 'age.cannot': "当前无法自动修改这个 DSH 配置的 pnpm 设置。可以等它满 24 小时,或改用 GitHub 地址重新安装。",
26
+ 'age.readyAt': "不放行的话,{time} 之后就能直接更新。",
27
+ 'age.wait': "等满 24 小时",
28
+ 'age.allow': "放行并更新",
29
+ 'age.ok': "知道了",
20
30
  'update.reloading': '正在重新加载…',
21
31
  'update.reloadingHint': 'TheOne 正在用新版本重新加载,DSH 不需要重启;完成后页面会自动刷新。',
22
32
  'update.busy': '有回复正在进行,等它结束后再点更新。',
33
+ 'update.tooNew': 'DSH 的安全策略只允许安装发布满 24 小时的版本,这一版还太新。过一阵再点一次即可。',
34
+ 'update.network': '连不上下载源,请检查网络后点击重试。',
23
35
  'update.failed': '更新失败',
24
36
  'update.hint': '当前 {current},可更新到 {latest}。点击一键更新,不用重启 DSH。',
25
37
  'update.manualHint': '当前 {current},最新 {latest}。这份插件不是从 GitHub 或 npm 安装的,请在「插件」页面重新安装。',
@@ -207,9 +219,21 @@ export const en = {
207
219
  'update.available': 'Update',
208
220
  'update.installing': 'Updating…',
209
221
  'update.restart': 'Restart to apply',
222
+ 'update.waiting': "New version",
223
+ 'update.waitingHint': "{latest} is out but less than 24 hours old. Click to see how to install it now.",
224
+ 'age.title': "This version is less than 24 hours old",
225
+ 'age.why': "DSH installs plugins with pnpm, which by default only installs versions published at least 24 hours ago, as a guard against tampered packages. TheOne {version} was published recently, so it cannot be installed yet.",
226
+ 'age.allowHow': "You can exempt TheOne only: dsh-theone is added to the exceptions in this DSH profile's pnpm settings (pnpm-workspace.yaml) and installed now. Other plugins keep the 24-hour rule.",
227
+ 'age.cannot': "This DSH profile's pnpm settings cannot be changed from here. Wait 24 hours, or reinstall from the GitHub address.",
228
+ 'age.readyAt': "Without the exemption, you can update after {time}.",
229
+ 'age.wait': "Wait 24 hours",
230
+ 'age.allow': "Exempt and update",
231
+ 'age.ok': "OK",
210
232
  'update.reloading': 'Reloading…',
211
233
  'update.reloadingHint': 'TheOne is reloading with the new version; DSH keeps running, and the page refreshes when it is done.',
212
234
  'update.busy': 'A reply is in progress. Update once it finishes.',
235
+ 'update.tooNew': 'DSH only installs versions published at least 24 hours ago, as a safety policy, and this one is newer. Try again later.',
236
+ 'update.network': 'Could not reach the download source. Check the network and click to retry.',
213
237
  'update.failed': 'Update failed',
214
238
  'update.hint': 'You have {current}; {latest} is available. Click to update without restarting DSH.',
215
239
  'update.manualHint': 'You have {current}; {latest} is available. This copy was not installed from GitHub or npm, so reinstall it from the Plugins page.',
package/dist/client.js CHANGED
@@ -100,9 +100,21 @@ var zh = {
100
100
  "update.available": "\u66F4\u65B0",
101
101
  "update.installing": "\u66F4\u65B0\u4E2D\u2026",
102
102
  "update.restart": "\u91CD\u542F\u751F\u6548",
103
+ "update.waiting": "\u65B0\u7248\u672C",
104
+ "update.waitingHint": "\u6709\u65B0\u7248\u672C {latest}\uFF0C\u53D1\u5E03\u8FD8\u4E0D\u6EE1 24 \u5C0F\u65F6\u3002\u70B9\u51FB\u67E5\u770B\u5982\u4F55\u73B0\u5728\u5B89\u88C5\u3002",
105
+ "age.title": "\u65B0\u7248\u672C\u53D1\u5E03\u8FD8\u4E0D\u6EE1 24 \u5C0F\u65F6",
106
+ "age.why": "DSH \u7528 pnpm \u5B89\u88C5\u63D2\u4EF6\uFF0C\u5B83\u9ED8\u8BA4\u53EA\u5B89\u88C5\u53D1\u5E03\u6EE1 24 \u5C0F\u65F6\u7684\u7248\u672C\uFF0C\u7528\u6765\u9632\u8303\u88AB\u7BE1\u6539\u7684\u65B0\u5305\u3002TheOne {version} \u521A\u53D1\u5E03\u4E0D\u4E45\uFF0C\u6240\u4EE5\u6682\u65F6\u88C5\u4E0D\u4E0A\u3002",
107
+ "age.allowHow": "\u4F60\u53EF\u4EE5\u53EA\u4E3A TheOne \u653E\u884C\uFF1A\u5728\u8FD9\u4E2A DSH \u914D\u7F6E\u7684 pnpm \u8BBE\u7F6E\uFF08pnpm-workspace.yaml\uFF09\u91CC\u628A dsh-theone \u52A0\u5165\u4F8B\u5916\uFF0C\u7136\u540E\u7ACB\u5373\u5B89\u88C5\u3002\u5176\u4ED6\u63D2\u4EF6\u4ECD\u7136\u53D7 24 \u5C0F\u65F6\u89C4\u5219\u4FDD\u62A4\u3002",
108
+ "age.cannot": "\u5F53\u524D\u65E0\u6CD5\u81EA\u52A8\u4FEE\u6539\u8FD9\u4E2A DSH \u914D\u7F6E\u7684 pnpm \u8BBE\u7F6E\u3002\u53EF\u4EE5\u7B49\u5B83\u6EE1 24 \u5C0F\u65F6\uFF0C\u6216\u6539\u7528 GitHub \u5730\u5740\u91CD\u65B0\u5B89\u88C5\u3002",
109
+ "age.readyAt": "\u4E0D\u653E\u884C\u7684\u8BDD\uFF0C{time} \u4E4B\u540E\u5C31\u80FD\u76F4\u63A5\u66F4\u65B0\u3002",
110
+ "age.wait": "\u7B49\u6EE1 24 \u5C0F\u65F6",
111
+ "age.allow": "\u653E\u884C\u5E76\u66F4\u65B0",
112
+ "age.ok": "\u77E5\u9053\u4E86",
103
113
  "update.reloading": "\u6B63\u5728\u91CD\u65B0\u52A0\u8F7D\u2026",
104
114
  "update.reloadingHint": "TheOne \u6B63\u5728\u7528\u65B0\u7248\u672C\u91CD\u65B0\u52A0\u8F7D\uFF0CDSH \u4E0D\u9700\u8981\u91CD\u542F\uFF1B\u5B8C\u6210\u540E\u9875\u9762\u4F1A\u81EA\u52A8\u5237\u65B0\u3002",
105
115
  "update.busy": "\u6709\u56DE\u590D\u6B63\u5728\u8FDB\u884C\uFF0C\u7B49\u5B83\u7ED3\u675F\u540E\u518D\u70B9\u66F4\u65B0\u3002",
116
+ "update.tooNew": "DSH \u7684\u5B89\u5168\u7B56\u7565\u53EA\u5141\u8BB8\u5B89\u88C5\u53D1\u5E03\u6EE1 24 \u5C0F\u65F6\u7684\u7248\u672C\uFF0C\u8FD9\u4E00\u7248\u8FD8\u592A\u65B0\u3002\u8FC7\u4E00\u9635\u518D\u70B9\u4E00\u6B21\u5373\u53EF\u3002",
117
+ "update.network": "\u8FDE\u4E0D\u4E0A\u4E0B\u8F7D\u6E90\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u70B9\u51FB\u91CD\u8BD5\u3002",
106
118
  "update.failed": "\u66F4\u65B0\u5931\u8D25",
107
119
  "update.hint": "\u5F53\u524D {current}\uFF0C\u53EF\u66F4\u65B0\u5230 {latest}\u3002\u70B9\u51FB\u4E00\u952E\u66F4\u65B0\uFF0C\u4E0D\u7528\u91CD\u542F DSH\u3002",
108
120
  "update.manualHint": "\u5F53\u524D {current}\uFF0C\u6700\u65B0 {latest}\u3002\u8FD9\u4EFD\u63D2\u4EF6\u4E0D\u662F\u4ECE GitHub \u6216 npm \u5B89\u88C5\u7684\uFF0C\u8BF7\u5728\u300C\u63D2\u4EF6\u300D\u9875\u9762\u91CD\u65B0\u5B89\u88C5\u3002",
@@ -290,9 +302,21 @@ var en = {
290
302
  "update.available": "Update",
291
303
  "update.installing": "Updating\u2026",
292
304
  "update.restart": "Restart to apply",
305
+ "update.waiting": "New version",
306
+ "update.waitingHint": "{latest} is out but less than 24 hours old. Click to see how to install it now.",
307
+ "age.title": "This version is less than 24 hours old",
308
+ "age.why": "DSH installs plugins with pnpm, which by default only installs versions published at least 24 hours ago, as a guard against tampered packages. TheOne {version} was published recently, so it cannot be installed yet.",
309
+ "age.allowHow": "You can exempt TheOne only: dsh-theone is added to the exceptions in this DSH profile's pnpm settings (pnpm-workspace.yaml) and installed now. Other plugins keep the 24-hour rule.",
310
+ "age.cannot": "This DSH profile's pnpm settings cannot be changed from here. Wait 24 hours, or reinstall from the GitHub address.",
311
+ "age.readyAt": "Without the exemption, you can update after {time}.",
312
+ "age.wait": "Wait 24 hours",
313
+ "age.allow": "Exempt and update",
314
+ "age.ok": "OK",
293
315
  "update.reloading": "Reloading\u2026",
294
316
  "update.reloadingHint": "TheOne is reloading with the new version; DSH keeps running, and the page refreshes when it is done.",
295
317
  "update.busy": "A reply is in progress. Update once it finishes.",
318
+ "update.tooNew": "DSH only installs versions published at least 24 hours ago, as a safety policy, and this one is newer. Try again later.",
319
+ "update.network": "Could not reach the download source. Check the network and click to retry.",
296
320
  "update.failed": "Update failed",
297
321
  "update.hint": "You have {current}; {latest} is available. Click to update without restarting DSH.",
298
322
  "update.manualHint": "You have {current}; {latest} is available. This copy was not installed from GitHub or npm, so reinstall it from the Plugins page.",
@@ -551,9 +575,14 @@ function apply(ctx) {
551
575
  updateListeners.delete(listener);
552
576
  };
553
577
  };
554
- const readUpdate = async (method = "GET") => {
578
+ const readUpdate = async (method = "GET", body) => {
555
579
  try {
556
- const response = await fetch("/api/theone/update", { method, signal: lifetime.signal, cache: "no-store" });
580
+ const response = await fetch("/api/theone/update", {
581
+ method,
582
+ signal: lifetime.signal,
583
+ cache: "no-store",
584
+ ...body ? { headers: { "content-type": "application/json" }, body: JSON.stringify(body) } : {}
585
+ });
557
586
  if (response.ok || response.status === 409) setUpdate(await response.json());
558
587
  if (update?.state === "reloading") void awaitReload(update.current);
559
588
  } catch {
@@ -580,16 +609,78 @@ function apply(ctx) {
580
609
  }, 6 * 36e5);
581
610
  return () => clearInterval(timer);
582
611
  });
612
+ function openReleaseAgeDialog(status) {
613
+ const backdrop = document.createElement("div");
614
+ backdrop.className = "theone-dialog-backdrop";
615
+ backdrop.setAttribute("translate", "no");
616
+ const card = document.createElement("div");
617
+ card.className = "theone-dialog";
618
+ card.setAttribute("role", "dialog");
619
+ card.setAttribute("aria-modal", "true");
620
+ const title = document.createElement("h2");
621
+ title.id = "theone-dialog-title";
622
+ title.textContent = t("age.title");
623
+ card.setAttribute("aria-labelledby", title.id);
624
+ const paragraph = (text, className) => {
625
+ const p = document.createElement("p");
626
+ p.textContent = text;
627
+ if (className) p.className = className;
628
+ return p;
629
+ };
630
+ const version = status.waiting?.version ?? status.latest ?? "";
631
+ card.append(title, paragraph(t("age.why", { version })), paragraph(t(status.canExempt ? "age.allowHow" : "age.cannot")));
632
+ if (status.waiting) card.append(paragraph(t("age.readyAt", { time: new Date(status.waiting.readyAt).toLocaleString(localeSnapshot().active, { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" }) }), "theone-dialog-note"));
633
+ const footer = document.createElement("div");
634
+ footer.className = "theone-dialog-actions";
635
+ const button = (text, primary, act) => {
636
+ const element = document.createElement("button");
637
+ element.type = "button";
638
+ element.textContent = text;
639
+ if (primary) element.className = "theone-dialog-primary";
640
+ element.addEventListener("click", act);
641
+ footer.append(element);
642
+ return element;
643
+ };
644
+ const close = () => {
645
+ backdrop.remove();
646
+ document.removeEventListener("keydown", keydown, true);
647
+ };
648
+ const keydown = (event) => {
649
+ if (event.key === "Escape") {
650
+ event.preventDefault();
651
+ close();
652
+ }
653
+ };
654
+ button(t(status.canExempt ? "age.wait" : "age.ok"), !status.canExempt, close);
655
+ const allow = status.canExempt ? button(t("age.allow"), true, () => {
656
+ close();
657
+ setUpdate({ ...status, state: "installing", error: void 0 });
658
+ void readUpdate("POST", { allowFresh: true });
659
+ }) : void 0;
660
+ card.append(footer);
661
+ backdrop.append(card);
662
+ backdrop.addEventListener("pointerdown", (event) => {
663
+ if (event.target === backdrop) close();
664
+ });
665
+ document.addEventListener("keydown", keydown, true);
666
+ document.body.append(backdrop);
667
+ (allow ?? footer.querySelector("button"))?.focus();
668
+ }
583
669
  function UpdateButton() {
584
670
  const t2 = useText();
585
671
  const status = (0, import_react.useSyncExternalStore)(subscribeUpdate, () => update);
586
- if (!status || !status.available && !status.state) return null;
587
- const label = status.state === "installing" ? t2("update.installing") : status.state === "reloading" ? t2("update.reloading") : status.state === "restart" ? t2("update.restart") : status.state === "failed" ? t2("update.failed") : t2("update.available");
588
- const title = status.error === "GATEWAY_BUSY" ? t2("update.busy") : status.state === "reloading" ? t2("update.reloadingHint") : status.state === "restart" ? t2("update.restartHint") : status.state === "failed" ? t2("update.failedHint", { error: status.error ?? "" }) : status.installable ? t2("update.hint", { current: status.current, latest: status.latest ?? "" }) : t2("update.manualHint", { current: status.current, latest: status.latest ?? "" });
672
+ const waiting = !!status?.waiting && !status.available && !status.state;
673
+ if (!status || !status.available && !status.state && !waiting) return null;
674
+ const label = waiting ? t2("update.waiting") : status.state === "installing" ? t2("update.installing") : status.state === "reloading" ? t2("update.reloading") : status.state === "restart" ? t2("update.restart") : status.state === "failed" ? t2("update.failed") : t2("update.available");
675
+ const title = waiting ? t2("update.waitingHint", { latest: status.waiting.version }) : status.error === "GATEWAY_BUSY" ? t2("update.busy") : status.state === "reloading" ? t2("update.reloadingHint") : status.state === "restart" ? t2("update.restartHint") : status.state === "failed" ? status.error === "MINIMUM_RELEASE_AGE" ? t2("update.tooNew") : status.error === "NETWORK" ? t2("update.network") : t2("update.failedHint", { error: status.error ?? "" }) : status.installable ? t2("update.hint", { current: status.current, latest: status.latest ?? "" }) : t2("update.manualHint", { current: status.current, latest: status.latest ?? "" });
589
676
  const act = (event) => {
590
677
  event.preventDefault();
591
678
  event.stopPropagation();
592
679
  if (status.state === "installing" || status.state === "reloading" || status.state === "restart") return;
680
+ if (waiting || status.state === "failed" && status.error === "MINIMUM_RELEASE_AGE") {
681
+ openReleaseAgeDialog(status);
682
+ return;
683
+ }
593
684
  if (!status.installable) {
594
685
  window.open("https://github.com/YunongDai2005/dsh-theone#readme", "_blank", "noopener");
595
686
  return;
@@ -605,7 +696,7 @@ function apply(ctx) {
605
696
  tabIndex: 0,
606
697
  title,
607
698
  "aria-label": title,
608
- "data-state": status.state ?? "available",
699
+ "data-state": waiting ? "waiting" : status.state ?? "available",
609
700
  onClick: act,
610
701
  onPointerDown: (event) => event.stopPropagation(),
611
702
  onKeyDown: (event) => {
@@ -614,7 +705,7 @@ function apply(ctx) {
614
705
  },
615
706
  status.state === "installing" || status.state === "reloading" ? (0, import_react.createElement)("span", { className: "theone-update-spin", "aria-hidden": true }) : null,
616
707
  label,
617
- !status.state && status.latest ? (0, import_react.createElement)("small", null, `v${status.latest}`) : null
708
+ !status.state && (waiting ? status.waiting.version : status.latest) ? (0, import_react.createElement)("small", null, `v${waiting ? status.waiting.version : status.latest}`) : null
618
709
  );
619
710
  }
620
711
  const symbol = () => (0, import_react.createElement)(
@@ -1714,6 +1805,18 @@ button:has(.theone-nav[data-wide=true])>span:has(.theone-nav){flex:1;min-width:0
1714
1805
  .theone-update small{font-size:11px;opacity:.75}
1715
1806
  .theone-update[data-state=installing],.theone-update[data-state=reloading],.theone-update[data-state=restart]{cursor:default}
1716
1807
  .theone-update[data-state=failed]{color:#d9480f;border-color:#d9480f66;background:#d9480f14}
1808
+ .theone-update[data-state=waiting]{opacity:.8;border-style:dashed}
1809
+ .theone-dialog-backdrop{position:fixed;inset:0;z-index:10001;display:flex;align-items:center;justify-content:center;padding:16px;background:#0008}
1810
+ .theone-dialog{width:min(440px,100%);box-sizing:border-box;padding:22px 22px 18px;border:1px solid var(--dsw-alias-border-l2,#ffffff1f);border-radius:16px;background:var(--dsw-specific-sidebar-fill,#232326);color:var(--dsw-alias-label-primary,#e8e8ea);box-shadow:0 16px 48px #0005;font:inherit;font-size:14px;line-height:1.65}
1811
+ .theone-dialog h2{margin:0 0 10px;font-size:17px}
1812
+ .theone-dialog p{margin:0 0 10px;color:var(--dsw-alias-label-secondary,#a0a0a6)}
1813
+ .theone-dialog .theone-dialog-note{font-size:13px}
1814
+ .theone-dialog-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:16px}
1815
+ .theone-dialog-actions button{padding:8px 14px;border:1px solid var(--dsw-alias-border-l2,#ffffff1f);border-radius:9px;background:transparent;color:inherit;font:inherit;cursor:pointer}
1816
+ .theone-dialog-actions button:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff12)}
1817
+ .theone-dialog-actions .theone-dialog-primary{border-color:transparent;background:#3b6fb0;color:#fff}
1818
+ .theone-dialog-actions .theone-dialog-primary:hover{background:#4a7fc0}
1819
+ .theone-dialog-actions button:focus-visible{outline:2px solid #4a7fc0;outline-offset:2px}
1717
1820
  .theone-update-spin{width:10px;height:10px;border-radius:50%;border:1.5px solid currentColor;border-right-color:transparent;animation:theone-spin 800ms linear infinite}
1718
1821
  @keyframes theone-spin{to{transform:rotate(360deg)}}
1719
1822
  @media(prefers-reduced-motion:reduce){.theone-update-spin{animation:none}}
package/dist/index.js CHANGED
@@ -205,9 +205,10 @@ export default class TheOne extends Service {
205
205
  }
206
206
  descriptors ??= config.contextsPath ? readDescriptors(config.contextsPath) : [];
207
207
  this.gatewayDirectory = resolve(dirname(databasePath), 'gateway');
208
+ const profileDir = () => ctx.profileContext?.dir;
208
209
  this.updater = new Updater(readOwnVersion(), () => {
209
210
  // The profile's package.json records how this plugin was installed (GitHub, npm or a local path).
210
- const dir = ctx.profileContext?.dir;
211
+ const dir = profileDir();
211
212
  if (!dir)
212
213
  return undefined;
213
214
  try {
@@ -216,7 +217,7 @@ export default class TheOne extends Service {
216
217
  catch {
217
218
  return undefined;
218
219
  }
219
- });
220
+ }, fetch, Date.now, () => { const dir = profileDir(); return dir ? join(dir, 'pnpm-workspace.yaml') : undefined; });
220
221
  ctx.effect(() => async () => {
221
222
  try {
222
223
  await Promise.all([...this.workers.values()].map(handle => handle.dispose()));
@@ -628,6 +629,18 @@ export default class TheOne extends Service {
628
629
  const manager = this.ctx.get('pluginManager');
629
630
  // With DSH's hot reload, switching the bundle off and on loads the new version without a DSH restart.
630
631
  const live = !!this.ctx.get('hmr') && typeof manager?.setBundleEnabled === 'function';
632
+ if (request.method === 'POST') {
633
+ // The user chose to let TheOne skip pnpm's one-day wait; only this package is exempted.
634
+ const body = await request.json().catch(() => ({}));
635
+ if (body?.allowFresh === true) {
636
+ try {
637
+ this.updater.allowFresh();
638
+ }
639
+ catch (error) {
640
+ return Response.json({ ...await this.updater.status(), error: error instanceof Error && /^[A-Z_]+$/.test(error.message) ? error.message : 'EXEMPT_FAILED' }, { status: 400 });
641
+ }
642
+ }
643
+ }
631
644
  const status = request.method === 'POST'
632
645
  ? await this.updater.install(manager, live ? bundle => reloadBundle(manager, bundle) : undefined)
633
646
  : await this.updater.status(new URL(request.url).searchParams.has('force'));
package/dist/update.d.ts CHANGED
@@ -12,8 +12,30 @@ export interface UpdateStatus {
12
12
  source: UpdateSource;
13
13
  /** installing → reloading (TheOne restarts itself; DSH keeps running), restart (applies after DSH restarts) or failed. */
14
14
  state?: 'installing' | 'reloading' | 'restart' | 'failed';
15
+ /** Why it failed: MINIMUM_RELEASE_AGE, NETWORK, or the plugin manager's own code. */
15
16
  error?: string;
17
+ /** A newer npm version that pnpm will accept only once it is a day old, and when that is. */
18
+ waiting?: {
19
+ version: string;
20
+ readyAt: number;
21
+ };
22
+ /** TheOne is exempt from that rule in this DSH profile, so fresh versions install right away. */
23
+ exempt?: boolean;
24
+ /** Whether this profile's pnpm settings can be edited to grant that exemption. */
25
+ canExempt?: boolean;
16
26
  }
27
+ /** The packages a pnpm-workspace.yaml exempts from the release-age rule (block or flow list). */
28
+ export declare function releaseAgeExemptions(text: string): string[];
29
+ /**
30
+ * The same pnpm-workspace.yaml with `name` added to the release-age exemptions, every other line
31
+ * kept as it was. Throws for a layout it does not recognise rather than guess.
32
+ */
33
+ export declare function withReleaseAgeExemption(text: string, name?: string): string;
34
+ /**
35
+ * pnpm, which DSH installs plugins with, refuses npm versions published less than a day ago
36
+ * (minimumReleaseAge, a supply-chain safeguard). Updates from npm wait until then.
37
+ */
38
+ export declare const RELEASE_AGE_MS: number;
17
39
  /** The DSH plugin manager's install call, as TheOne uses it. */
18
40
  export interface PluginInstaller {
19
41
  installBundle(spec: string, options?: {
@@ -25,6 +47,9 @@ export interface PluginInstaller {
25
47
  code?: string;
26
48
  message?: string;
27
49
  } | unknown;
50
+ packageResult?: {
51
+ output?: string;
52
+ };
28
53
  }>;
29
54
  }
30
55
  /** Compare dotted versions numerically; a pre-release sorts before its release. */
@@ -37,11 +62,20 @@ export declare class Updater {
37
62
  private readonly spec;
38
63
  private readonly fetcher;
39
64
  private readonly now;
65
+ private readonly workspace;
40
66
  private checked?;
41
67
  private state?;
42
68
  private error?;
43
69
  private pending?;
44
- constructor(current: string, spec: () => string | undefined, fetcher?: typeof fetch, now?: () => number);
70
+ /**
71
+ * @param workspace - this DSH profile's pnpm-workspace.yaml, where pnpm reads the release-age
72
+ * exemptions; undefined when the profile is unknown.
73
+ */
74
+ constructor(current: string, spec: () => string | undefined, fetcher?: typeof fetch, now?: () => number, workspace?: () => string | undefined);
75
+ /** Whether pnpm in this profile already lets TheOne install versions under a day old. */
76
+ get exempt(): boolean;
77
+ /** Exempt TheOne, and only TheOne, from pnpm's release-age rule in this profile. */
78
+ allowFresh(): void;
45
79
  get source(): UpdateSource;
46
80
  /** The latest known status; checks again at most every six hours (or now, when forced). */
47
81
  status(force?: boolean): Promise<UpdateStatus>;
package/dist/update.js CHANGED
@@ -1,5 +1,53 @@
1
+ import { readFileSync, renameSync, writeFileSync } from 'node:fs';
1
2
  export const REPOSITORY = 'YunongDai2005/dsh-theone';
2
3
  export const PACKAGE_NAME = 'dsh-theone';
4
+ const EXCLUDE_KEY = 'minimumReleaseAgeExclude';
5
+ const unquote = (value) => value.trim().replace(/^['"]|['"]$/g, '');
6
+ /** The packages a pnpm-workspace.yaml exempts from the release-age rule (block or flow list). */
7
+ export function releaseAgeExemptions(text) {
8
+ const lines = text.split(/\r?\n/);
9
+ const index = lines.findIndex(line => line.startsWith(`${EXCLUDE_KEY}:`));
10
+ if (index < 0)
11
+ return [];
12
+ const inline = lines[index].slice(EXCLUDE_KEY.length + 1).trim();
13
+ if (inline.startsWith('['))
14
+ return inline.replace(/^\[|\]$/g, '').split(',').map(unquote).filter(Boolean);
15
+ const items = [];
16
+ for (let line = index + 1; line < lines.length && /^\s+-/.test(lines[line]); line++)
17
+ items.push(unquote(lines[line].replace(/^\s+-\s*/, '')));
18
+ return items;
19
+ }
20
+ /**
21
+ * The same pnpm-workspace.yaml with `name` added to the release-age exemptions, every other line
22
+ * kept as it was. Throws for a layout it does not recognise rather than guess.
23
+ */
24
+ export function withReleaseAgeExemption(text, name = PACKAGE_NAME) {
25
+ if (releaseAgeExemptions(text).includes(name))
26
+ return text;
27
+ const newline = text.includes('\r\n') ? '\r\n' : '\n';
28
+ const lines = text.split(/\r?\n/);
29
+ const index = lines.findIndex(line => line.startsWith(`${EXCLUDE_KEY}:`));
30
+ if (index < 0)
31
+ return `${text}${text && !/\r?\n$/.test(text) ? newline : ''}${EXCLUDE_KEY}:${newline} - ${name}${newline}`;
32
+ const inline = lines[index].slice(EXCLUDE_KEY.length + 1).trim();
33
+ if (inline.startsWith('[') && inline.endsWith(']')) {
34
+ lines[index] = `${EXCLUDE_KEY}: [${[...releaseAgeExemptions(text), name].map(item => JSON.stringify(item)).join(', ')}]`;
35
+ return lines.join(newline);
36
+ }
37
+ if (inline)
38
+ throw new Error('UNRECOGNISED_WORKSPACE');
39
+ let end = index + 1;
40
+ while (end < lines.length && /^\s+-/.test(lines[end]))
41
+ end++;
42
+ const indent = end > index + 1 ? lines[index + 1].match(/^\s+/)[0] : ' ';
43
+ lines.splice(end, 0, `${indent}- ${name}`);
44
+ return lines.join(newline);
45
+ }
46
+ /**
47
+ * pnpm, which DSH installs plugins with, refuses npm versions published less than a day ago
48
+ * (minimumReleaseAge, a supply-chain safeguard). Updates from npm wait until then.
49
+ */
50
+ export const RELEASE_AGE_MS = 24 * 3600000;
3
51
  /** Compare dotted versions numerically; a pre-release sorts before its release. */
4
52
  export function compareVersions(a, b) {
5
53
  const parse = (value) => {
@@ -33,21 +81,64 @@ export class Updater {
33
81
  spec;
34
82
  fetcher;
35
83
  now;
84
+ workspace;
36
85
  checked;
37
86
  state;
38
87
  error;
39
88
  pending;
40
- constructor(current, spec, fetcher = fetch, now = Date.now) {
89
+ /**
90
+ * @param workspace - this DSH profile's pnpm-workspace.yaml, where pnpm reads the release-age
91
+ * exemptions; undefined when the profile is unknown.
92
+ */
93
+ constructor(current, spec, fetcher = fetch, now = Date.now, workspace = () => undefined) {
41
94
  this.current = current;
42
95
  this.spec = spec;
43
96
  this.fetcher = fetcher;
44
97
  this.now = now;
98
+ this.workspace = workspace;
99
+ }
100
+ /** Whether pnpm in this profile already lets TheOne install versions under a day old. */
101
+ get exempt() {
102
+ const file = this.workspace();
103
+ if (!file)
104
+ return false;
105
+ try {
106
+ return releaseAgeExemptions(readFileSync(file, 'utf8')).includes(PACKAGE_NAME);
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ }
112
+ /** Exempt TheOne, and only TheOne, from pnpm's release-age rule in this profile. */
113
+ allowFresh() {
114
+ const file = this.workspace();
115
+ if (!file)
116
+ throw new Error('NO_PROFILE');
117
+ let text = '';
118
+ try {
119
+ text = readFileSync(file, 'utf8');
120
+ }
121
+ catch (error) {
122
+ if (error.code !== 'ENOENT')
123
+ throw error;
124
+ }
125
+ const next = withReleaseAgeExemption(text);
126
+ if (next === text)
127
+ return;
128
+ writeFileSync(`${file}.theone-tmp`, next, { mode: 0o600 });
129
+ renameSync(`${file}.theone-tmp`, file);
130
+ if (this.state === 'failed') {
131
+ this.state = undefined;
132
+ this.error = undefined;
133
+ }
45
134
  }
46
135
  get source() { return installSource(this.spec()); }
47
136
  /** The latest known status; checks again at most every six hours (or now, when forced). */
48
137
  async status(force = false) {
49
138
  const age = this.checked ? this.now() - this.checked.at : Infinity;
50
- if (this.state !== 'installing' && (force || age > (this.checked?.error ? HOUR / 2 : 6 * HOUR))) {
139
+ // A version waiting out pnpm's release age is looked at again as soon as it qualifies.
140
+ const due = !!this.checked?.waiting && this.now() >= this.checked.waiting.readyAt;
141
+ if (this.state !== 'installing' && (force || due || age > (this.checked?.error ? HOUR / 2 : 6 * HOUR))) {
51
142
  this.pending ??= this.check().finally(() => { this.pending = undefined; });
52
143
  await this.pending;
53
144
  }
@@ -61,7 +152,7 @@ export class Updater {
61
152
  const status = await this.status();
62
153
  if (!status.available || !status.installable || !installer || this.state === 'installing' || this.state === 'reloading' || this.state === 'restart')
63
154
  return this.snapshot();
64
- const spec = this.source === 'github' ? `github:${REPOSITORY}#${this.checked.sha}` : `${PACKAGE_NAME}@${this.checked.latest}`;
155
+ const spec = this.source === 'github' ? `github:${REPOSITORY}#${this.checked.sha}` : `${PACKAGE_NAME}@${status.latest}`;
65
156
  this.state = 'installing';
66
157
  this.error = undefined;
67
158
  try {
@@ -73,7 +164,11 @@ export class Updater {
73
164
  else {
74
165
  this.state = 'failed';
75
166
  const error = result.error;
76
- this.error = String(error?.code ?? error?.message ?? result.application).slice(0, 120);
167
+ // pnpm's own reasons are clearer than the plugin manager's generic failure.
168
+ const output = `${result.packageResult?.output ?? ''}\n${error?.message ?? ''}`;
169
+ this.error = /MINIMUM_RELEASE_AGE/.test(output) ? 'MINIMUM_RELEASE_AGE'
170
+ : /UND_ERR|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|ERR_PNPM_META_FETCH_FAIL|network/i.test(output) ? 'NETWORK'
171
+ : String(error?.code ?? error?.message ?? result.application).slice(0, 120);
77
172
  }
78
173
  }
79
174
  catch (error) {
@@ -83,10 +178,15 @@ export class Updater {
83
178
  return this.snapshot();
84
179
  }
85
180
  snapshot() {
86
- const latest = this.checked?.latest;
181
+ const exempt = this.source === 'npm' && this.exempt;
182
+ // Exempt from the release-age rule, the newest version is ready as soon as it is published.
183
+ const fresh = this.checked?.waiting && compareVersions(this.checked.waiting.version, this.current) > 0 ? this.checked.waiting : undefined;
184
+ const latest = exempt && fresh ? fresh.version : this.checked?.latest;
87
185
  const source = this.source;
88
186
  const available = !!latest && compareVersions(latest, this.current) > 0;
89
- return { current: this.current, ...(latest ? { latest } : {}), available, source,
187
+ const waiting = fresh && !exempt ? { waiting: fresh } : {};
188
+ return { current: this.current, ...(latest ? { latest } : {}), available, source, ...waiting,
189
+ ...(source === 'npm' ? { exempt, canExempt: !!this.workspace() } : {}),
90
190
  installable: available && (source === 'npm' || (source === 'github' && !!this.checked?.sha)),
91
191
  ...(this.state ? { state: this.state } : {}), ...(this.error ?? this.checked?.error ? { error: this.error ?? this.checked?.error } : {}) };
92
192
  }
@@ -94,10 +194,19 @@ export class Updater {
94
194
  const signal = AbortSignal.timeout(10000);
95
195
  try {
96
196
  if (this.source === 'npm') {
97
- const response = await this.fetcher(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { signal });
197
+ const response = await this.fetcher(`https://registry.npmjs.org/${PACKAGE_NAME}`, { signal });
98
198
  if (!response.ok)
99
199
  throw new Error(`npm ${response.status}`);
100
- this.checked = { at: this.now(), latest: String((await response.json()).version ?? '') || undefined };
200
+ const packument = await response.json();
201
+ // Offer the newest release pnpm will accept now; a newer one waits until it is a day old.
202
+ const now = this.now();
203
+ const releases = Object.keys(packument.versions ?? {}).filter(version => /^\d+\.\d+\.\d+$/.test(version))
204
+ .map(version => ({ version, at: Date.parse(packument.time?.[version] ?? '') }))
205
+ .filter(release => Number.isFinite(release.at)).sort((a, b) => compareVersions(b.version, a.version));
206
+ const ready = releases.find(release => now - release.at >= RELEASE_AGE_MS);
207
+ const newest = releases[0];
208
+ this.checked = { at: now, ...(ready ? { latest: ready.version } : {}),
209
+ ...(newest && newest !== ready ? { waiting: { version: newest.version, readyAt: newest.at + RELEASE_AGE_MS } } : {}) };
101
210
  return;
102
211
  }
103
212
  // A GitHub install updates to the exact commit whose version was read.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-theone",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Automatic context routing for DeepSeek Harness",