dsh-ntr 0.2.1 → 0.3.0

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/lib/client.js CHANGED
@@ -49,6 +49,12 @@ window.__ModuleLoader__.load({
49
49
  const MAX_WATCHED = 6;
50
50
  /** Raw JSON budget for one event block; the cut is marked. */
51
51
  const RAW_MAX = 256e3;
52
+ /**
53
+ * How long a fetched update state stays fresh. The settings panel asks on every
54
+ * mount; without this, every open would hit the registry. A forced check (the
55
+ * card's own button) always goes out.
56
+ */
57
+ const PLUGIN_UPDATE_TTL_MS = 6e5;
52
58
  /** Pretty-printed JSON of an agent message (never throws, cuts only at {@link RAW_MAX} with a marker). */
53
59
  function jsonText(value) {
54
60
  let text;
@@ -262,6 +268,14 @@ window.__ModuleLoader__.load({
262
268
  defaultCommand: "opencode",
263
269
  defaultArgs: ["acp"]
264
270
  },
271
+ {
272
+ id: "cursor",
273
+ name: "Cursor CLI",
274
+ description: "Cursor CLI — run as `agent acp` over stdio (legacy alias `cursor-agent acp`)",
275
+ detected: false,
276
+ defaultCommand: "agent",
277
+ defaultArgs: ["acp"]
278
+ },
265
279
  {
266
280
  id: "qwen",
267
281
  name: "Qwen Code",
@@ -377,6 +391,22 @@ window.__ModuleLoader__.load({
377
391
  cachedModelReads = /* @__PURE__ */ new Set();
378
392
  /** Counts behind the last successful explicit re-detect, so a locale switch can re-render its summary. */
379
393
  detectCounts;
394
+ /** When {@link UiState.pluginUpdate} was last read from the host (epoch ms). */
395
+ pluginUpdateFetchedAt = 0;
396
+ /**
397
+ * The shared in-flight `plugin/update-check`. The settings section mounts
398
+ * twice (both entries) and the update popover asks too; without this, two
399
+ * mounts in the same tick each fire their own registry read.
400
+ */
401
+ pluginUpdateInflight;
402
+ /** Re-entry guard for `plugin/update-apply` (a double click is one install). */
403
+ pluginUpdateApplying = false;
404
+ /**
405
+ * Update read generation. An apply bumps it so a check that was already in
406
+ * flight (started before the install) cannot land afterwards and overwrite
407
+ * the install's verdict with the pre-install `available`.
408
+ */
409
+ pluginUpdateGeneration = 0;
380
410
  constructor(rpc, locale) {
381
411
  this.rpc = rpc;
382
412
  this.locale = locale;
@@ -499,14 +529,15 @@ window.__ModuleLoader__.load({
499
529
  /** Snapshot refresh used on panel open and after a stream outage. */
500
530
  async resync() {
501
531
  try {
502
- const [{ agents }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult] = await Promise.all([
532
+ const [{ agents }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult, versionResult] = await Promise.all([
503
533
  this.call("agents/list"),
504
534
  this.call("sessions/list"),
505
535
  this.call("default/agent").catch(() => ({})),
506
536
  this.call("agents/detect").catch(() => null),
507
537
  this.call("preset/status").catch(() => null),
508
538
  this.call("ui/tag-halo").catch(() => ({ halo: { ...DEFAULT_TAG_HALO } })),
509
- this.call("ui/approval-deadlines").catch(() => ({ deadlines: void 0 }))
539
+ this.call("ui/approval-deadlines").catch(() => ({ deadlines: void 0 })),
540
+ this.call("plugin/version").catch(() => null)
510
541
  ]);
511
542
  const detectedAgents = detectResult && detectResult.agents.length > 0 ? detectResult.agents : this.state.detectedAgents.length > 0 ? this.state.detectedAgents : [...DEFAULT_KNOWN_PRESETS];
512
543
  this.mutate({
@@ -517,7 +548,8 @@ window.__ModuleLoader__.load({
517
548
  detectedAgents,
518
549
  preset: presetResult?.preset ?? void 0,
519
550
  tagHalo: tagHaloResult?.halo ?? { ...DEFAULT_TAG_HALO },
520
- approvalDeadlines: deadlinesResult.deadlines
551
+ approvalDeadlines: deadlinesResult.deadlines,
552
+ pluginVersion: versionResult?.version !== void 0 && versionResult.version !== "" ? versionResult.version : void 0
521
553
  });
522
554
  const active = this.state.activeSessionId ?? sessions[0]?.sessionId;
523
555
  if (active !== void 0) await this.loadTranscript(active);
@@ -525,6 +557,89 @@ window.__ModuleLoader__.load({
525
557
  this.mutate({ notice: String(error instanceof Error ? error.message : error) });
526
558
  }
527
559
  }
560
+ /**
561
+ * Read the plugin's update state (`plugin/update-check`). Throttled to ten
562
+ * minutes unless `force`: the settings panel asks on mount, and every
563
+ * remount must not hit the registry.
564
+ *
565
+ * An older host serves no such endpoint (`unknown endpoint`). That is a
566
+ * normal degradation, not an error worth a notice: the answer is `undefined`
567
+ * and the panel simply keeps its update entry hidden.
568
+ */
569
+ async checkPluginUpdate(force = false) {
570
+ if (this.pluginUpdateInflight !== void 0) return this.pluginUpdateInflight;
571
+ const current = this.state.pluginUpdate;
572
+ if (!force && current !== void 0 && Date.now() - this.pluginUpdateFetchedAt < PLUGIN_UPDATE_TTL_MS) return current;
573
+ const generation = this.pluginUpdateGeneration;
574
+ const inflight = this.readPluginUpdate(generation);
575
+ this.pluginUpdateInflight = inflight;
576
+ try {
577
+ return await inflight;
578
+ } finally {
579
+ this.pluginUpdateInflight = void 0;
580
+ }
581
+ }
582
+ async readPluginUpdate(generation) {
583
+ try {
584
+ const pluginUpdate = await this.call("plugin/update-check");
585
+ if (generation !== this.pluginUpdateGeneration) return void 0;
586
+ this.pluginUpdateFetchedAt = Date.now();
587
+ this.mutate({ pluginUpdate });
588
+ return pluginUpdate;
589
+ } catch {
590
+ return;
591
+ }
592
+ }
593
+ /**
594
+ * Install one exact version (`plugin/update-apply`). The status flips to
595
+ * `applying` first so the badge answers the click; the host's answer then
596
+ * becomes the state.
597
+ *
598
+ * A failed call does NOT prove the install did not happen: on an HMR host
599
+ * the update uninstalls the plugin before reinstalling it, which can cut the
600
+ * response. Whatever the error, the disk may already carry the new version,
601
+ * so the state becomes `restart-required` (the one neutral verdict for "the
602
+ * running code is not the disk's") with `installed` dropped (unknown, not a
603
+ * fact to display) and the notice asks for a reload rather than restoring
604
+ * `available` and pretending nothing changed.
605
+ */
606
+ async applyPluginUpdate(version) {
607
+ if (this.pluginUpdateApplying) return void 0;
608
+ this.pluginUpdateApplying = true;
609
+ this.pluginUpdateGeneration += 1;
610
+ const previous = this.state.pluginUpdate;
611
+ if (previous !== void 0) this.mutate({ pluginUpdate: {
612
+ ...previous,
613
+ status: "applying"
614
+ } });
615
+ try {
616
+ const pluginUpdate = await this.call("plugin/update-apply", { version });
617
+ this.pluginUpdateFetchedAt = Date.now();
618
+ this.mutate({ pluginUpdate });
619
+ return pluginUpdate;
620
+ } catch (error) {
621
+ const message = error instanceof Error ? error.message : String(error);
622
+ const interrupted = previous !== void 0 ? {
623
+ ...previous,
624
+ status: "restart-required",
625
+ installed: void 0,
626
+ message
627
+ } : {
628
+ status: "restart-required",
629
+ current: this.state.pluginVersion ?? "?",
630
+ message
631
+ };
632
+ this.pluginUpdateFetchedAt = 0;
633
+ this.mutate({
634
+ pluginUpdate: interrupted,
635
+ notice: this.locale("更新请求已中断;请刷新页面确认当前版本。", "The update request was interrupted. Reload the page to confirm the installed version.")
636
+ });
637
+ return;
638
+ } finally {
639
+ this.pluginUpdateGeneration += 1;
640
+ this.pluginUpdateApplying = false;
641
+ }
642
+ }
528
643
  async detectAgents() {
529
644
  this.mutate({ detecting: true });
530
645
  try {
@@ -2638,7 +2753,7 @@ window.__ModuleLoader__.load({
2638
2753
  }
2639
2754
  .dwa-page-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 28px; }
2640
2755
  .dwa-page-heading { min-width: 0; }
2641
- .dwa-page-title { margin: 0; font-size: 20px; line-height: 1.35; font-weight: 550; letter-spacing: -0.02em; }
2756
+ .dwa-page-title { margin: 0; font-size: 20px; line-height: 1.35; font-weight: 550; letter-spacing: -0.02em; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
2642
2757
  .dwa-page-description { margin: 5px 0 0; color: var(--ntr-muted); font-size: var(--ntr-font-sm); }
2643
2758
  .dwa-btn {
2644
2759
  min-height: 32px; box-sizing: border-box; font: inherit; border-radius: var(--ntr-radius-sm);
@@ -2670,6 +2785,20 @@ window.__ModuleLoader__.load({
2670
2785
  .dwa-badge-success { background: var(--dsw-alias-state-success-tertiary, #dcfce7); color: var(--ntr-success); }
2671
2786
  .dwa-badge-warn { background: var(--dsw-alias-state-warn-tertiary, #fef3c7); color: var(--ntr-warn); }
2672
2787
  .dwa-badge-info { background: var(--dsw-alias-state-business-tertiary, #dbeafe); color: var(--dsw-alias-state-business-primary, #2563eb); }
2788
+ /* The update badge is a button (it expands the update card): strip the
2789
+ browser's default button chrome, keep the badge look. */
2790
+ .dwa-update-badge { border: 0; font-family: inherit; cursor: pointer; }
2791
+ .dwa-update-badge:hover { opacity: 0.85; }
2792
+ /* The version badge doubles as the update entry: same badge look, no chrome. */
2793
+ .dwa-version-btn { border: 0; font-family: inherit; cursor: pointer; }
2794
+ .dwa-version-btn:hover { opacity: 0.85; }
2795
+ .dwa-version { font-family: var(--ds-font-family-code, monospace); }
2796
+ /* The update card sits between the page header and the permission mirror. */
2797
+ .dwa-update-card { margin-bottom: 16px; }
2798
+ .dwa-update-summary { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; font-weight: 550; }
2799
+ .dwa-update-copy { margin: 6px 0 0; color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
2800
+ .dwa-update-message { margin: 6px 0 0; color: var(--ntr-muted); font-size: var(--ntr-font-xs); white-space: pre-wrap; overflow-wrap: anywhere; }
2801
+ .dwa-update-actions { margin-top: 10px; }
2673
2802
  .dwa-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
2674
2803
  .dwa-agent-list { display: flex; flex-direction: column; border-top: var(--ntr-line); }
2675
2804
  .dwa-agent-card { border-bottom: var(--ntr-line); min-width: 0; }
@@ -3320,6 +3449,139 @@ window.__ModuleLoader__.load({
3320
3449
  })]
3321
3450
  });
3322
3451
  }
3452
+ /** The states that carry an update badge next to the version badge. */
3453
+ function updateBadgeText(update, acp) {
3454
+ if (update.status === "available") return acp.label(`可更新 v${update.latest ?? update.current}`, `Update v${update.latest ?? update.current}`);
3455
+ if (update.status === "applying") return acp.label("更新中…", "Updating…");
3456
+ if (update.status === "applied") {
3457
+ const installed = update.installed ?? update.latest ?? update.current;
3458
+ return acp.label(`已更新 v${installed}`, `Updated v${installed}`);
3459
+ }
3460
+ if (update.status === "restart-required") {
3461
+ if (update.installed === void 0) return void 0;
3462
+ return acp.label(`待重启 v${update.installed}`, `Restart for v${update.installed}`);
3463
+ }
3464
+ }
3465
+ /**
3466
+ * The version badge's update companion. Every badge is a button, `applying`
3467
+ * included: the card is where the install's outcome is read, so the running
3468
+ * install must be able to open it too. States without anything to act on get
3469
+ * no badge at all — the version badge itself still opens the card.
3470
+ */
3471
+ function PluginUpdateBadge(props) {
3472
+ const { acp, update, open, onToggle } = props;
3473
+ const text = updateBadgeText(update, acp);
3474
+ if (text === void 0) return null;
3475
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3476
+ type: "button",
3477
+ className: "dwa-badge dwa-badge-info dwa-update-badge",
3478
+ "aria-expanded": open,
3479
+ "aria-controls": "ntr-plugin-update-card",
3480
+ onClick: onToggle,
3481
+ children: text
3482
+ });
3483
+ }
3484
+ /**
3485
+ * The update card. In-page, no dialog: the settings page already owns the one
3486
+ * action (install the exact version the host reported), and a modal on top of
3487
+ * a settings section would hide the version it is updating.
3488
+ *
3489
+ * The copy stays to one line per state (constraint 33); the host's raw error
3490
+ * (`message`) is the only long text, and it wraps on its own line so a stack
3491
+ * trace or a registry error is readable without stretching the card.
3492
+ */
3493
+ function PluginUpdateCard(props) {
3494
+ const { acp, update, onClose } = props;
3495
+ const applying = update.status === "applying";
3496
+ const latest = update.latest ?? update.current;
3497
+ const onDisk = update.installed !== void 0 && update.installed !== update.current ? update.installed : void 0;
3498
+ const published = update.publishedAt !== void 0 && !Number.isNaN(new Date(update.publishedAt).getTime()) ? new Date(update.publishedAt).toLocaleDateString() : void 0;
3499
+ const copy = update.status === "available" ? acp.label("更新会卸载并重装插件,之后可能需要重启 DSH。", "Updating uninstalls and reinstalls the plugin, and may require restarting DSH.") : update.status === "applied" ? acp.label("已应用,刷新页面即可使用新版本。", "Applied. Reload the page to use the new version.") : update.status === "restart-required" ? update.installed !== void 0 ? acp.label(`磁盘已是 v${update.installed},重启 DSH 后生效(桌面 App 退出重开;命令行重启 dsh web)。`, `On disk v${update.installed}. Restart DSH to apply (quit and reopen the app, or restart dsh web).`) : acp.label("更新可能已完成,请刷新页面确认当前版本。", "The update may already be complete. Refresh the page to confirm the current version.") : update.status === "busy" ? acp.label("有进行中的对话,结束后才能更新。", "Finish the running conversation before updating.") : update.status === "failed" ? acp.label("更新失败。", "Update failed.") : update.status === "current" ? acp.label("已是最新版本。", "Up to date.") : update.status === "source-install" ? acp.label("源码或本地安装,无法通过 registry 更新。", "Source or local install; update it through its own checkout.") : update.status === "managed" ? acp.label("此安装由宿主管理,不能在这里更新。", "This install is managed by the host and cannot be updated here.") : void 0;
3500
+ const showMessage = (update.status === "failed" || update.status === "busy" || update.status === "managed") && update.message !== void 0;
3501
+ const apply = async () => {
3502
+ if (update.latest === void 0) return;
3503
+ try {
3504
+ await acp.applyPluginUpdate(update.latest);
3505
+ } catch {}
3506
+ };
3507
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3508
+ id: "ntr-plugin-update-card",
3509
+ className: "dwa-card dwa-update-card",
3510
+ role: "group",
3511
+ "aria-label": acp.label("插件更新", "Plugin update"),
3512
+ children: [
3513
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3514
+ className: "dwa-update-summary",
3515
+ children: [
3516
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: acp.label(`当前 v${update.current}`, `Current v${update.current}`) }),
3517
+ onDisk !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3518
+ className: "dwa-hint",
3519
+ children: acp.label(`磁盘 v${onDisk}`, `On disk v${onDisk}`)
3520
+ }),
3521
+ update.latest !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3522
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3523
+ "aria-hidden": "true",
3524
+ children: "→"
3525
+ }),
3526
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: acp.label(`最新 v${latest}`, `Latest v${latest}`) }),
3527
+ published !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3528
+ className: "dwa-hint",
3529
+ children: published
3530
+ })
3531
+ ] })
3532
+ ]
3533
+ }),
3534
+ copy !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3535
+ className: "dwa-update-copy",
3536
+ children: copy
3537
+ }),
3538
+ showMessage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3539
+ className: "dwa-update-message",
3540
+ children: update.message
3541
+ }),
3542
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3543
+ className: "dwa-row-actions dwa-update-actions",
3544
+ children: [
3545
+ update.status === "available" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3546
+ type: "button",
3547
+ className: "dwa-btn dwa-btn-primary",
3548
+ onClick: () => {
3549
+ apply();
3550
+ },
3551
+ children: acp.label("更新", "Update")
3552
+ }),
3553
+ applying && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3554
+ type: "button",
3555
+ className: "dwa-btn dwa-btn-primary",
3556
+ disabled: true,
3557
+ children: acp.label("正在更新…", "Updating…")
3558
+ }),
3559
+ update.status !== "applying" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3560
+ type: "button",
3561
+ className: "dwa-btn",
3562
+ onClick: onClose,
3563
+ children: acp.label("关闭", "Close")
3564
+ }),
3565
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3566
+ type: "button",
3567
+ className: "dwa-btn dwa-btn-quiet",
3568
+ disabled: applying,
3569
+ onClick: () => void acp.checkPluginUpdate(true),
3570
+ children: acp.label("重新检查", "Check again")
3571
+ })
3572
+ ]
3573
+ })
3574
+ ]
3575
+ });
3576
+ }
3577
+ /**
3578
+ * The card is drawn for every state a host can report about this install,
3579
+ * except the two where there is no install to talk about at all.
3580
+ */
3581
+ function updateCardVisible(update, open) {
3582
+ if (update === void 0 || !open) return false;
3583
+ return update.status !== "unsupported" && update.status !== "not-installed";
3584
+ }
3323
3585
  function AcpSettingsSection(props) {
3324
3586
  const acp = props.acp;
3325
3587
  ensureStyle();
@@ -3328,6 +3590,10 @@ window.__ModuleLoader__.load({
3328
3590
  const [page, setPage] = (0, react.useState)("agents");
3329
3591
  const [addOpen, setAddOpen] = (0, react.useState)(false);
3330
3592
  const [addTarget, setAddTarget] = (0, react.useState)(void 0);
3593
+ const [updateOpen, setUpdateOpen] = (0, react.useState)(false);
3594
+ (0, react.useEffect)(() => {
3595
+ acp.checkPluginUpdate();
3596
+ }, [acp]);
3331
3597
  const { configured, available } = partitionAgents(ui.detectedAgents, ui.agents);
3332
3598
  const selected = addTarget ?? available[0] ?? NEW_AGENT_SEED;
3333
3599
  const openAdd = () => {
@@ -3335,6 +3601,9 @@ window.__ModuleLoader__.load({
3335
3601
  setPage("add");
3336
3602
  };
3337
3603
  const presetProblem = ui.preset !== void 0 && (!ui.preset.installed || ui.preset.problem !== void 0);
3604
+ const update = ui.pluginUpdate;
3605
+ const versionTitle = update?.status === "source-install" ? acp.label("源码或本地安装,无法通过 registry 更新。", "Source or local install; update it through its own checkout.") : void 0;
3606
+ const versionOpensUpdate = update !== void 0 && update.status !== "unsupported" && update.status !== "not-installed";
3338
3607
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3339
3608
  className: "dwa-root",
3340
3609
  children: [
@@ -3342,9 +3611,31 @@ window.__ModuleLoader__.load({
3342
3611
  className: "dwa-page-header",
3343
3612
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3344
3613
  className: "dwa-page-heading",
3345
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
3614
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h2", {
3346
3615
  className: "dwa-page-title",
3347
- children: acp.label("NTR 设置", "NTR settings")
3616
+ "aria-label": acp.label("NTR 设置", "NTR settings"),
3617
+ children: [
3618
+ acp.label("NTR 设置", "NTR settings"),
3619
+ ui.pluginVersion !== void 0 && (versionOpensUpdate ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3620
+ type: "button",
3621
+ className: "dwa-badge dwa-version dwa-version-btn",
3622
+ title: versionTitle,
3623
+ "aria-expanded": updateOpen,
3624
+ "aria-controls": "ntr-plugin-update-card",
3625
+ onClick: () => setUpdateOpen((open) => !open),
3626
+ children: `v${ui.pluginVersion}`
3627
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3628
+ className: "dwa-badge dwa-version",
3629
+ title: versionTitle,
3630
+ children: `v${ui.pluginVersion}`
3631
+ })),
3632
+ update !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PluginUpdateBadge, {
3633
+ acp,
3634
+ update,
3635
+ open: updateOpen,
3636
+ onToggle: () => setUpdateOpen((open) => !open)
3637
+ })
3638
+ ]
3348
3639
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3349
3640
  className: "dwa-page-description",
3350
3641
  children: acp.label("连接智能体,设置你的对话偏好。", "Connect agents and set your conversation preferences.")
@@ -3356,6 +3647,11 @@ window.__ModuleLoader__.load({
3356
3647
  children: acp.label("添加智能体", "Add agent")
3357
3648
  })]
3358
3649
  }),
3650
+ update !== void 0 && updateCardVisible(update, updateOpen) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PluginUpdateCard, {
3651
+ acp,
3652
+ update,
3653
+ onClose: () => setUpdateOpen(false)
3654
+ }),
3359
3655
  ledger.visible && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3360
3656
  className: "dwa-card dwa-card-warn",
3361
3657
  style: { marginBottom: 16 },