dsh-ntr 0.2.1 → 0.3.1

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",
@@ -308,6 +322,8 @@ window.__ModuleLoader__.load({
308
322
  blocks: [],
309
323
  permissions: [],
310
324
  busy: false,
325
+ logs: [],
326
+ logUnread: 0,
311
327
  detectedAgents: [...DEFAULT_KNOWN_PRESETS],
312
328
  detecting: false,
313
329
  modelCatalogs: {},
@@ -329,6 +345,8 @@ window.__ModuleLoader__.load({
329
345
  };
330
346
  listeners = /* @__PURE__ */ new Set();
331
347
  source;
348
+ /** Monotonic id source for {@link AcpLogEntry}s (never reset by `clearLogs`). */
349
+ logSeq = 0;
332
350
  lastSeq = /* @__PURE__ */ new Map();
333
351
  /** Sessions with a finalized turn (dedups the cancel-fallback + late real turn/end pair). */
334
352
  openTurns = /* @__PURE__ */ new Set();
@@ -377,6 +395,22 @@ window.__ModuleLoader__.load({
377
395
  cachedModelReads = /* @__PURE__ */ new Set();
378
396
  /** Counts behind the last successful explicit re-detect, so a locale switch can re-render its summary. */
379
397
  detectCounts;
398
+ /** When {@link UiState.pluginUpdate} was last read from the host (epoch ms). */
399
+ pluginUpdateFetchedAt = 0;
400
+ /**
401
+ * The shared in-flight `plugin/update-check`. The settings section mounts
402
+ * twice (both entries) and the update popover asks too; without this, two
403
+ * mounts in the same tick each fire their own registry read.
404
+ */
405
+ pluginUpdateInflight;
406
+ /** Re-entry guard for `plugin/update-apply` (a double click is one install). */
407
+ pluginUpdateApplying = false;
408
+ /**
409
+ * Update read generation. An apply bumps it so a check that was already in
410
+ * flight (started before the install) cannot land afterwards and overwrite
411
+ * the install's verdict with the pre-install `available`.
412
+ */
413
+ pluginUpdateGeneration = 0;
380
414
  constructor(rpc, locale) {
381
415
  this.rpc = rpc;
382
416
  this.locale = locale;
@@ -399,6 +433,23 @@ window.__ModuleLoader__.load({
399
433
  for (const listener of [...this.listeners]) listener();
400
434
  }
401
435
  /**
436
+ * Build the log patch for one line. Does NOT mutate: the caller merges it
437
+ * into the single `mutate` call it was already making for the same event.
438
+ */
439
+ logPatch(level, text, agentId) {
440
+ const entry = {
441
+ id: ++this.logSeq,
442
+ at: Date.now(),
443
+ level,
444
+ text,
445
+ ...agentId === void 0 ? {} : { agentId }
446
+ };
447
+ return {
448
+ logs: [...this.state.logs, entry].slice(-200),
449
+ logUnread: this.state.logUnread + 1
450
+ };
451
+ }
452
+ /**
402
453
  * Live `session/update` frames waiting to be folded. Agents stream at token
403
454
  * granularity — hundreds of frames a second with usage ticks interleaved —
404
455
  * and folding plus notifying listeners per frame re-rendered every store
@@ -499,14 +550,15 @@ window.__ModuleLoader__.load({
499
550
  /** Snapshot refresh used on panel open and after a stream outage. */
500
551
  async resync() {
501
552
  try {
502
- const [{ agents }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult] = await Promise.all([
553
+ const [{ agents }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult, versionResult] = await Promise.all([
503
554
  this.call("agents/list"),
504
555
  this.call("sessions/list"),
505
556
  this.call("default/agent").catch(() => ({})),
506
557
  this.call("agents/detect").catch(() => null),
507
558
  this.call("preset/status").catch(() => null),
508
559
  this.call("ui/tag-halo").catch(() => ({ halo: { ...DEFAULT_TAG_HALO } })),
509
- this.call("ui/approval-deadlines").catch(() => ({ deadlines: void 0 }))
560
+ this.call("ui/approval-deadlines").catch(() => ({ deadlines: void 0 })),
561
+ this.call("plugin/version").catch(() => null)
510
562
  ]);
511
563
  const detectedAgents = detectResult && detectResult.agents.length > 0 ? detectResult.agents : this.state.detectedAgents.length > 0 ? this.state.detectedAgents : [...DEFAULT_KNOWN_PRESETS];
512
564
  this.mutate({
@@ -517,7 +569,8 @@ window.__ModuleLoader__.load({
517
569
  detectedAgents,
518
570
  preset: presetResult?.preset ?? void 0,
519
571
  tagHalo: tagHaloResult?.halo ?? { ...DEFAULT_TAG_HALO },
520
- approvalDeadlines: deadlinesResult.deadlines
572
+ approvalDeadlines: deadlinesResult.deadlines,
573
+ pluginVersion: versionResult?.version !== void 0 && versionResult.version !== "" ? versionResult.version : void 0
521
574
  });
522
575
  const active = this.state.activeSessionId ?? sessions[0]?.sessionId;
523
576
  if (active !== void 0) await this.loadTranscript(active);
@@ -525,6 +578,89 @@ window.__ModuleLoader__.load({
525
578
  this.mutate({ notice: String(error instanceof Error ? error.message : error) });
526
579
  }
527
580
  }
581
+ /**
582
+ * Read the plugin's update state (`plugin/update-check`). Throttled to ten
583
+ * minutes unless `force`: the settings panel asks on mount, and every
584
+ * remount must not hit the registry.
585
+ *
586
+ * An older host serves no such endpoint (`unknown endpoint`). That is a
587
+ * normal degradation, not an error worth a notice: the answer is `undefined`
588
+ * and the panel simply keeps its update entry hidden.
589
+ */
590
+ async checkPluginUpdate(force = false) {
591
+ if (this.pluginUpdateInflight !== void 0) return this.pluginUpdateInflight;
592
+ const current = this.state.pluginUpdate;
593
+ if (!force && current !== void 0 && Date.now() - this.pluginUpdateFetchedAt < PLUGIN_UPDATE_TTL_MS) return current;
594
+ const generation = this.pluginUpdateGeneration;
595
+ const inflight = this.readPluginUpdate(generation);
596
+ this.pluginUpdateInflight = inflight;
597
+ try {
598
+ return await inflight;
599
+ } finally {
600
+ this.pluginUpdateInflight = void 0;
601
+ }
602
+ }
603
+ async readPluginUpdate(generation) {
604
+ try {
605
+ const pluginUpdate = await this.call("plugin/update-check");
606
+ if (generation !== this.pluginUpdateGeneration) return void 0;
607
+ this.pluginUpdateFetchedAt = Date.now();
608
+ this.mutate({ pluginUpdate });
609
+ return pluginUpdate;
610
+ } catch {
611
+ return;
612
+ }
613
+ }
614
+ /**
615
+ * Install one exact version (`plugin/update-apply`). The status flips to
616
+ * `applying` first so the badge answers the click; the host's answer then
617
+ * becomes the state.
618
+ *
619
+ * A failed call does NOT prove the install did not happen: on an HMR host
620
+ * the update uninstalls the plugin before reinstalling it, which can cut the
621
+ * response. Whatever the error, the disk may already carry the new version,
622
+ * so the state becomes `restart-required` (the one neutral verdict for "the
623
+ * running code is not the disk's") with `installed` dropped (unknown, not a
624
+ * fact to display) and the notice asks for a reload rather than restoring
625
+ * `available` and pretending nothing changed.
626
+ */
627
+ async applyPluginUpdate(version) {
628
+ if (this.pluginUpdateApplying) return void 0;
629
+ this.pluginUpdateApplying = true;
630
+ this.pluginUpdateGeneration += 1;
631
+ const previous = this.state.pluginUpdate;
632
+ if (previous !== void 0) this.mutate({ pluginUpdate: {
633
+ ...previous,
634
+ status: "applying"
635
+ } });
636
+ try {
637
+ const pluginUpdate = await this.call("plugin/update-apply", { version });
638
+ this.pluginUpdateFetchedAt = Date.now();
639
+ this.mutate({ pluginUpdate });
640
+ return pluginUpdate;
641
+ } catch (error) {
642
+ const message = error instanceof Error ? error.message : String(error);
643
+ const interrupted = previous !== void 0 ? {
644
+ ...previous,
645
+ status: "restart-required",
646
+ installed: void 0,
647
+ message
648
+ } : {
649
+ status: "restart-required",
650
+ current: this.state.pluginVersion ?? "?",
651
+ message
652
+ };
653
+ this.pluginUpdateFetchedAt = 0;
654
+ this.mutate({
655
+ pluginUpdate: interrupted,
656
+ notice: this.locale("更新请求已中断;请刷新页面确认当前版本。", "The update request was interrupted. Reload the page to confirm the installed version.")
657
+ });
658
+ return;
659
+ } finally {
660
+ this.pluginUpdateGeneration += 1;
661
+ this.pluginUpdateApplying = false;
662
+ }
663
+ }
528
664
  async detectAgents() {
529
665
  this.mutate({ detecting: true });
530
666
  try {
@@ -1364,13 +1500,25 @@ window.__ModuleLoader__.load({
1364
1500
  clearNotice() {
1365
1501
  this.mutate({ notice: void 0 });
1366
1502
  }
1503
+ /** Drop every retained runtime-log line (the unread counter restarts too). */
1504
+ clearLogs() {
1505
+ this.mutate({
1506
+ logs: [],
1507
+ logUnread: 0
1508
+ });
1509
+ }
1510
+ /** The user looked at the log: clear the unread badge. */
1511
+ markLogsRead() {
1512
+ if (this.state.logUnread === 0) return;
1513
+ this.mutate({ logUnread: 0 });
1514
+ }
1367
1515
  flashError(error) {
1368
1516
  this.mutate({ notice: error instanceof Error ? error.message : String(error) });
1369
1517
  }
1370
1518
  applyEvent(event) {
1371
1519
  switch (event.type) {
1372
1520
  case "agent/notice":
1373
- this.mutate({ notice: event.text });
1521
+ this.mutate(this.logPatch(event.level, event.text, event.agentId));
1374
1522
  return Promise.resolve();
1375
1523
  case "agent/status":
1376
1524
  if (event.agent !== void 0) {
@@ -1395,6 +1543,7 @@ window.__ModuleLoader__.load({
1395
1543
  return Promise.resolve();
1396
1544
  }
1397
1545
  case "session/closed": {
1546
+ const closingSession = this.state.sessions.find((s) => s.sessionId === event.sessionId);
1398
1547
  const sessions = this.state.sessions.filter((s) => s.sessionId !== event.sessionId);
1399
1548
  const active = this.state.activeSessionId === event.sessionId ? sessions[0]?.sessionId : this.state.activeSessionId;
1400
1549
  this.openTurns.delete(event.sessionId);
@@ -1407,7 +1556,7 @@ window.__ModuleLoader__.load({
1407
1556
  chatTranscripts: closedTranscripts,
1408
1557
  ...chatSessions.length !== this.state.chatSessions.length ? { chatSessions } : {},
1409
1558
  permissions: this.state.permissions.filter((p) => p.sessionId !== event.sessionId),
1410
- ...event.reason === "agent-died" ? { notice: this.locale("会话因 agent 进程退出而终止", "session ended: agent process died") } : {}
1559
+ ...event.reason === "agent-died" ? this.logPatch("error", this.locale("会话因 agent 进程退出而终止", "session ended: agent process died"), closingSession?.agentId) : {}
1411
1560
  });
1412
1561
  return Promise.resolve();
1413
1562
  }
@@ -1418,12 +1567,18 @@ window.__ModuleLoader__.load({
1418
1567
  ...s,
1419
1568
  released: true
1420
1569
  } : s) : this.state.chatSessions;
1570
+ const cause = event.cause ?? "died";
1571
+ let log = {};
1572
+ if (cause === "workspace") log = this.logPatch("info", this.locale("agent 为切换工作区重启,会话已挂起;再次发送消息将自动恢复。", "agent restarted for its workspace; the session is paused and will resume on your next message."), event.session.agentId);
1573
+ else if (cause === "resume-failed") log = this.logPatch("error", this.locale("会话恢复失败;再次发送消息将重试恢复。", "session resume failed; your next message will retry it."), event.session.agentId);
1574
+ else if (cause === "stalled") log = this.logPatch("error", this.locale("agent 未响应卡死的回合,会话已挂起;再次发送消息将自动恢复。", "agent did not acknowledge the stalled turn; the session is paused and will resume on your next message."), event.session.agentId);
1575
+ else if (cause === "died") log = this.logPatch("error", this.locale("agent 进程已退出,会话已挂起;再次发送消息将自动恢复。", "agent process died; the session is paused and will resume on your next message."), event.session.agentId);
1421
1576
  this.mutate({
1422
1577
  sessions,
1423
1578
  ...chatSessions !== this.state.chatSessions ? { chatSessions } : {},
1424
1579
  permissions: this.state.permissions.filter((p) => p.sessionId !== event.sessionId),
1425
1580
  ...this.watchedSessionPatch(event.sessionId, event.session),
1426
- notice: this.locale("agent 进程已退出,会话已挂起;再次发送消息将自动恢复。", "agent process died; the session is paused and will resume on your next message.")
1581
+ ...log
1427
1582
  });
1428
1583
  return Promise.resolve();
1429
1584
  }
@@ -2638,7 +2793,7 @@ window.__ModuleLoader__.load({
2638
2793
  }
2639
2794
  .dwa-page-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 28px; }
2640
2795
  .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; }
2796
+ .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
2797
  .dwa-page-description { margin: 5px 0 0; color: var(--ntr-muted); font-size: var(--ntr-font-sm); }
2643
2798
  .dwa-btn {
2644
2799
  min-height: 32px; box-sizing: border-box; font: inherit; border-radius: var(--ntr-radius-sm);
@@ -2670,6 +2825,20 @@ window.__ModuleLoader__.load({
2670
2825
  .dwa-badge-success { background: var(--dsw-alias-state-success-tertiary, #dcfce7); color: var(--ntr-success); }
2671
2826
  .dwa-badge-warn { background: var(--dsw-alias-state-warn-tertiary, #fef3c7); color: var(--ntr-warn); }
2672
2827
  .dwa-badge-info { background: var(--dsw-alias-state-business-tertiary, #dbeafe); color: var(--dsw-alias-state-business-primary, #2563eb); }
2828
+ /* The update badge is a button (it expands the update card): strip the
2829
+ browser's default button chrome, keep the badge look. */
2830
+ .dwa-update-badge { border: 0; font-family: inherit; cursor: pointer; }
2831
+ .dwa-update-badge:hover { opacity: 0.85; }
2832
+ /* The version badge doubles as the update entry: same badge look, no chrome. */
2833
+ .dwa-version-btn { border: 0; font-family: inherit; cursor: pointer; }
2834
+ .dwa-version-btn:hover { opacity: 0.85; }
2835
+ .dwa-version { font-family: var(--ds-font-family-code, monospace); }
2836
+ /* The update card sits between the page header and the permission mirror. */
2837
+ .dwa-update-card { margin-bottom: 16px; }
2838
+ .dwa-update-summary { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; font-weight: 550; }
2839
+ .dwa-update-copy { margin: 6px 0 0; color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
2840
+ .dwa-update-message { margin: 6px 0 0; color: var(--ntr-muted); font-size: var(--ntr-font-xs); white-space: pre-wrap; overflow-wrap: anywhere; }
2841
+ .dwa-update-actions { margin-top: 10px; }
2673
2842
  .dwa-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
2674
2843
  .dwa-agent-list { display: flex; flex-direction: column; border-top: var(--ntr-line); }
2675
2844
  .dwa-agent-card { border-bottom: var(--ntr-line); min-width: 0; }
@@ -2792,6 +2961,17 @@ window.__ModuleLoader__.load({
2792
2961
  .dwa-deadline-flag { margin-top: 4px; font-size: var(--ntr-font-xs); font-weight: 550; color: var(--ntr-warn); }
2793
2962
  .dwa-deadline-warn { margin-top: 10px; padding: 9px 12px; border-left: 3px solid var(--dsw-alias-state-warn-primary, #d97706); background: var(--dsw-alias-state-warn-secondary, #fffbeb); border-radius: var(--ntr-radius-sm); font-size: var(--ntr-font-xs); line-height: 1.55; }
2794
2963
  .dwa-deadline-reset { display: flex; justify-content: flex-end; margin-top: 8px; }
2964
+ /* Runtime log: one bounded list, newest first. The time column never wraps;
2965
+ the message is the only cell that may break anywhere. */
2966
+ .dwa-log-list { list-style: none; margin: 0; padding: 0; border: var(--ntr-line); border-radius: var(--ntr-radius); overflow: hidden; }
2967
+ .dwa-log-row { display: flex; align-items: baseline; gap: 8px; padding: 6px 10px; font-size: var(--ntr-font-sm); border-top: var(--ntr-line); min-width: 0; }
2968
+ .dwa-log-row:first-child { border-top: 0; }
2969
+ .dwa-log-row.is-error { background: var(--dsw-alias-state-error-tertiary, #fef2f2); }
2970
+ .dwa-log-time { color: var(--ntr-muted); white-space: nowrap; }
2971
+ .dwa-log-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
2972
+ .dwa-log-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ntr-danger); display: inline-block; margin-left: 6px; }
2973
+ .dwa-log-level.is-info { background: var(--dsw-alias-state-business-tertiary, #dbeafe); color: var(--dsw-alias-state-business-primary, #2563eb); }
2974
+ .dwa-log-level.is-error { background: var(--dsw-alias-state-error-tertiary, #fef2f2); color: var(--ntr-danger); }
2795
2975
  @media (max-width: 640px) {
2796
2976
  .dwa-root { padding: 20px 16px; }
2797
2977
  }
@@ -3320,6 +3500,152 @@ window.__ModuleLoader__.load({
3320
3500
  })]
3321
3501
  });
3322
3502
  }
3503
+ /**
3504
+ * Local wall-clock `HH:MM:SS` for one runtime-log line. Hand-built on purpose:
3505
+ * `toLocaleTimeString` may drop seconds or pick a 12-hour clock depending on
3506
+ * the browser locale, which makes the column ragged and ambiguous.
3507
+ */
3508
+ function logClock(at) {
3509
+ const date = new Date(at);
3510
+ return [
3511
+ date.getHours(),
3512
+ date.getMinutes(),
3513
+ date.getSeconds()
3514
+ ].map((value) => String(value).padStart(2, "0")).join(":");
3515
+ }
3516
+ /** The states that carry an update badge next to the version badge. */
3517
+ function updateBadgeText(update, acp) {
3518
+ if (update.status === "available") return acp.label(`可更新 v${update.latest ?? update.current}`, `Update v${update.latest ?? update.current}`);
3519
+ if (update.status === "applying") return acp.label("更新中…", "Updating…");
3520
+ if (update.status === "applied") {
3521
+ const installed = update.installed ?? update.latest ?? update.current;
3522
+ return acp.label(`已更新 v${installed}`, `Updated v${installed}`);
3523
+ }
3524
+ if (update.status === "restart-required") {
3525
+ if (update.installed === void 0) return void 0;
3526
+ return acp.label(`待重启 v${update.installed}`, `Restart for v${update.installed}`);
3527
+ }
3528
+ }
3529
+ /**
3530
+ * The version badge's update companion. Every badge is a button, `applying`
3531
+ * included: the card is where the install's outcome is read, so the running
3532
+ * install must be able to open it too. States without anything to act on get
3533
+ * no badge at all — the version badge itself still opens the card.
3534
+ */
3535
+ function PluginUpdateBadge(props) {
3536
+ const { acp, update, open, onToggle } = props;
3537
+ const text = updateBadgeText(update, acp);
3538
+ if (text === void 0) return null;
3539
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3540
+ type: "button",
3541
+ className: "dwa-badge dwa-badge-info dwa-update-badge",
3542
+ "aria-expanded": open,
3543
+ "aria-controls": "ntr-plugin-update-card",
3544
+ onClick: onToggle,
3545
+ children: text
3546
+ });
3547
+ }
3548
+ /**
3549
+ * The update card. In-page, no dialog: the settings page already owns the one
3550
+ * action (install the exact version the host reported), and a modal on top of
3551
+ * a settings section would hide the version it is updating.
3552
+ *
3553
+ * The copy stays to one line per state (constraint 33); the host's raw error
3554
+ * (`message`) is the only long text, and it wraps on its own line so a stack
3555
+ * trace or a registry error is readable without stretching the card.
3556
+ */
3557
+ function PluginUpdateCard(props) {
3558
+ const { acp, update, onClose } = props;
3559
+ const applying = update.status === "applying";
3560
+ const latest = update.latest ?? update.current;
3561
+ const onDisk = update.installed !== void 0 && update.installed !== update.current ? update.installed : void 0;
3562
+ const published = update.publishedAt !== void 0 && !Number.isNaN(new Date(update.publishedAt).getTime()) ? new Date(update.publishedAt).toLocaleDateString() : void 0;
3563
+ 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;
3564
+ const showMessage = (update.status === "failed" || update.status === "busy" || update.status === "managed") && update.message !== void 0;
3565
+ const apply = async () => {
3566
+ if (update.latest === void 0) return;
3567
+ try {
3568
+ await acp.applyPluginUpdate(update.latest);
3569
+ } catch {}
3570
+ };
3571
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3572
+ id: "ntr-plugin-update-card",
3573
+ className: "dwa-card dwa-update-card",
3574
+ role: "group",
3575
+ "aria-label": acp.label("插件更新", "Plugin update"),
3576
+ children: [
3577
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3578
+ className: "dwa-update-summary",
3579
+ children: [
3580
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: acp.label(`当前 v${update.current}`, `Current v${update.current}`) }),
3581
+ onDisk !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3582
+ className: "dwa-hint",
3583
+ children: acp.label(`磁盘 v${onDisk}`, `On disk v${onDisk}`)
3584
+ }),
3585
+ update.latest !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3586
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3587
+ "aria-hidden": "true",
3588
+ children: "→"
3589
+ }),
3590
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: acp.label(`最新 v${latest}`, `Latest v${latest}`) }),
3591
+ published !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3592
+ className: "dwa-hint",
3593
+ children: published
3594
+ })
3595
+ ] })
3596
+ ]
3597
+ }),
3598
+ copy !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3599
+ className: "dwa-update-copy",
3600
+ children: copy
3601
+ }),
3602
+ showMessage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3603
+ className: "dwa-update-message",
3604
+ children: update.message
3605
+ }),
3606
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3607
+ className: "dwa-row-actions dwa-update-actions",
3608
+ children: [
3609
+ update.status === "available" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3610
+ type: "button",
3611
+ className: "dwa-btn dwa-btn-primary",
3612
+ onClick: () => {
3613
+ apply();
3614
+ },
3615
+ children: acp.label("更新", "Update")
3616
+ }),
3617
+ applying && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3618
+ type: "button",
3619
+ className: "dwa-btn dwa-btn-primary",
3620
+ disabled: true,
3621
+ children: acp.label("正在更新…", "Updating…")
3622
+ }),
3623
+ update.status !== "applying" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3624
+ type: "button",
3625
+ className: "dwa-btn",
3626
+ onClick: onClose,
3627
+ children: acp.label("关闭", "Close")
3628
+ }),
3629
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3630
+ type: "button",
3631
+ className: "dwa-btn dwa-btn-quiet",
3632
+ disabled: applying,
3633
+ onClick: () => void acp.checkPluginUpdate(true),
3634
+ children: acp.label("重新检查", "Check again")
3635
+ })
3636
+ ]
3637
+ })
3638
+ ]
3639
+ });
3640
+ }
3641
+ /**
3642
+ * The card is drawn for every state a host can report about this install,
3643
+ * except the two where there is no install to talk about at all.
3644
+ */
3645
+ function updateCardVisible(update, open) {
3646
+ if (update === void 0 || !open) return false;
3647
+ return update.status !== "unsupported" && update.status !== "not-installed";
3648
+ }
3323
3649
  function AcpSettingsSection(props) {
3324
3650
  const acp = props.acp;
3325
3651
  ensureStyle();
@@ -3328,6 +3654,17 @@ window.__ModuleLoader__.load({
3328
3654
  const [page, setPage] = (0, react.useState)("agents");
3329
3655
  const [addOpen, setAddOpen] = (0, react.useState)(false);
3330
3656
  const [addTarget, setAddTarget] = (0, react.useState)(void 0);
3657
+ const [updateOpen, setUpdateOpen] = (0, react.useState)(false);
3658
+ (0, react.useEffect)(() => {
3659
+ acp.checkPluginUpdate();
3660
+ }, [acp]);
3661
+ (0, react.useEffect)(() => {
3662
+ if (page === "logs" && ui.logUnread > 0) acp.markLogsRead();
3663
+ }, [
3664
+ acp,
3665
+ page,
3666
+ ui.logUnread
3667
+ ]);
3331
3668
  const { configured, available } = partitionAgents(ui.detectedAgents, ui.agents);
3332
3669
  const selected = addTarget ?? available[0] ?? NEW_AGENT_SEED;
3333
3670
  const openAdd = () => {
@@ -3335,6 +3672,9 @@ window.__ModuleLoader__.load({
3335
3672
  setPage("add");
3336
3673
  };
3337
3674
  const presetProblem = ui.preset !== void 0 && (!ui.preset.installed || ui.preset.problem !== void 0);
3675
+ const update = ui.pluginUpdate;
3676
+ const versionTitle = update?.status === "source-install" ? acp.label("源码或本地安装,无法通过 registry 更新。", "Source or local install; update it through its own checkout.") : void 0;
3677
+ const versionOpensUpdate = update !== void 0 && update.status !== "unsupported" && update.status !== "not-installed";
3338
3678
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3339
3679
  className: "dwa-root",
3340
3680
  children: [
@@ -3342,9 +3682,31 @@ window.__ModuleLoader__.load({
3342
3682
  className: "dwa-page-header",
3343
3683
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3344
3684
  className: "dwa-page-heading",
3345
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
3685
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h2", {
3346
3686
  className: "dwa-page-title",
3347
- children: acp.label("NTR 设置", "NTR settings")
3687
+ "aria-label": acp.label("NTR 设置", "NTR settings"),
3688
+ children: [
3689
+ acp.label("NTR 设置", "NTR settings"),
3690
+ ui.pluginVersion !== void 0 && (versionOpensUpdate ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3691
+ type: "button",
3692
+ className: "dwa-badge dwa-version dwa-version-btn",
3693
+ title: versionTitle,
3694
+ "aria-expanded": updateOpen,
3695
+ "aria-controls": "ntr-plugin-update-card",
3696
+ onClick: () => setUpdateOpen((open) => !open),
3697
+ children: `v${ui.pluginVersion}`
3698
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3699
+ className: "dwa-badge dwa-version",
3700
+ title: versionTitle,
3701
+ children: `v${ui.pluginVersion}`
3702
+ })),
3703
+ update !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PluginUpdateBadge, {
3704
+ acp,
3705
+ update,
3706
+ open: updateOpen,
3707
+ onToggle: () => setUpdateOpen((open) => !open)
3708
+ })
3709
+ ]
3348
3710
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3349
3711
  className: "dwa-page-description",
3350
3712
  children: acp.label("连接智能体,设置你的对话偏好。", "Connect agents and set your conversation preferences.")
@@ -3356,6 +3718,11 @@ window.__ModuleLoader__.load({
3356
3718
  children: acp.label("添加智能体", "Add agent")
3357
3719
  })]
3358
3720
  }),
3721
+ update !== void 0 && updateCardVisible(update, updateOpen) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PluginUpdateCard, {
3722
+ acp,
3723
+ update,
3724
+ onClose: () => setUpdateOpen(false)
3725
+ }),
3359
3726
  ledger.visible && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3360
3727
  className: "dwa-card dwa-card-warn",
3361
3728
  style: { marginBottom: 16 },
@@ -3464,6 +3831,15 @@ window.__ModuleLoader__.load({
3464
3831
  "aria-pressed": page === "appearance",
3465
3832
  onClick: () => setPage("appearance"),
3466
3833
  children: acp.label("对话外观", "Appearance")
3834
+ }),
3835
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3836
+ type: "button",
3837
+ "aria-pressed": page === "logs",
3838
+ onClick: () => setPage("logs"),
3839
+ children: [acp.label("日志", "Logs"), ui.logUnread > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3840
+ className: "dwa-log-dot",
3841
+ "aria-hidden": "true"
3842
+ })]
3467
3843
  })
3468
3844
  ]
3469
3845
  }),
@@ -3715,6 +4091,52 @@ window.__ModuleLoader__.load({
3715
4091
  })]
3716
4092
  })]
3717
4093
  }),
4094
+ page === "logs" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4095
+ className: "dwa-section",
4096
+ "aria-label": acp.label("运行日志", "Runtime log"),
4097
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4098
+ className: "dwa-section-heading",
4099
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
4100
+ className: "dwa-section-title",
4101
+ children: acp.label("运行日志", "Runtime log")
4102
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4103
+ className: "dwa-section-copy",
4104
+ children: acp.label(`agent 运行消息,最多 ${String(200)} 条。`, `Agent runtime messages, up to ${String(200)}.`)
4105
+ })] }), ui.logs.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4106
+ type: "button",
4107
+ className: "dwa-btn dwa-btn-quiet",
4108
+ onClick: () => acp.clearLogs(),
4109
+ children: acp.label("清空", "Clear")
4110
+ })]
4111
+ }), ui.logs.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4112
+ className: "dwa-empty",
4113
+ role: "status",
4114
+ children: acp.label("暂无运行日志。", "No runtime messages yet.")
4115
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
4116
+ className: "dwa-log-list",
4117
+ children: [...ui.logs].reverse().map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
4118
+ className: entry.level === "error" ? "dwa-log-row is-error" : "dwa-log-row",
4119
+ children: [
4120
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4121
+ className: "dwa-log-time dwa-mono",
4122
+ children: logClock(entry.at)
4123
+ }),
4124
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4125
+ className: `dwa-badge dwa-log-level ${entry.level === "error" ? "is-error" : "is-info"}`,
4126
+ children: entry.level === "error" ? acp.label("错误", "Error") : acp.label("信息", "Info")
4127
+ }),
4128
+ entry.agentId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4129
+ className: "dwa-badge",
4130
+ children: entry.agentId
4131
+ }),
4132
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4133
+ className: "dwa-log-text",
4134
+ children: entry.text
4135
+ })
4136
+ ]
4137
+ }, entry.id))
4138
+ })]
4139
+ }),
3718
4140
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
3719
4141
  className: "dwa-health",
3720
4142
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {