dsh-ntr 0.3.1 → 0.3.3

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.
Files changed (57) hide show
  1. package/README.en.md +3 -1
  2. package/README.md +3 -1
  3. package/cordis.patch.yml +17 -1
  4. package/lib/bridge.js +2436 -82
  5. package/lib/client/acp-ui.js +103 -3
  6. package/lib/client/acp-view.js +24 -4
  7. package/lib/client/activity-pill.js +156 -0
  8. package/lib/client/branch-action.js +149 -0
  9. package/lib/client/composer-mode.js +48 -13
  10. package/lib/client/context-meter.js +86 -0
  11. package/lib/client/index.js +279 -16
  12. package/lib/client/model-seat.js +8 -2
  13. package/lib/client/panel.js +96 -29
  14. package/lib/client/released-banner.js +66 -0
  15. package/lib/client/session-controls.js +38 -13
  16. package/lib/client/slash-isolation.js +2 -2
  17. package/lib/client/store.js +380 -13
  18. package/lib/client.js +1364 -70
  19. package/lib/client.js.map +1 -1
  20. package/lib/impl.js +1229 -8
  21. package/lib/index.js +34 -0
  22. package/lib/protocol.js +19 -0
  23. package/lib/tps.js +32 -0
  24. package/lib/types/bridge.d.ts +850 -18
  25. package/lib/types/client/acp-ui.d.ts +34 -2
  26. package/lib/types/client/acp-view.d.ts +13 -2
  27. package/lib/types/client/activity-pill.d.ts +81 -0
  28. package/lib/types/client/branch-action.d.ts +47 -0
  29. package/lib/types/client/composer-mode.d.ts +28 -0
  30. package/lib/types/client/context-meter.d.ts +51 -0
  31. package/lib/types/client/model-seat.d.ts +21 -0
  32. package/lib/types/client/released-banner.d.ts +34 -0
  33. package/lib/types/client/slash-isolation.d.ts +1 -1
  34. package/lib/types/client/store.d.ts +108 -1
  35. package/lib/types/impl.d.ts +94 -7
  36. package/lib/types/protocol.d.ts +145 -0
  37. package/lib/types/tps.d.ts +17 -0
  38. package/package.json +2 -1
  39. package/src/bridge.ts +2754 -86
  40. package/src/client/acp-ui.tsx +123 -2
  41. package/src/client/acp-view.tsx +30 -5
  42. package/src/client/activity-pill.tsx +201 -0
  43. package/src/client/branch-action.tsx +178 -0
  44. package/src/client/composer-mode.tsx +59 -13
  45. package/src/client/context-meter.tsx +128 -0
  46. package/src/client/index.tsx +298 -15
  47. package/src/client/model-seat.tsx +28 -3
  48. package/src/client/panel.tsx +157 -40
  49. package/src/client/primitives.d.ts +27 -0
  50. package/src/client/released-banner.tsx +97 -0
  51. package/src/client/session-controls.tsx +34 -11
  52. package/src/client/slash-isolation.ts +2 -1
  53. package/src/client/store.ts +412 -14
  54. package/src/impl.ts +1251 -13
  55. package/src/index.ts +33 -0
  56. package/src/protocol.ts +145 -0
  57. package/src/tps.ts +40 -0
@@ -18,6 +18,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
18
18
  import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
19
19
  import { formatCommandLine, isAbsolutePathLike, splitCommandLine } from "./acp-ui.js";
20
20
  import { DEFAULT_KNOWN_PRESETS, LOG_MAX_ENTRIES } from "./store.js";
21
+ import { IDLE_RELEASE_MS } from "../protocol.js";
21
22
  /**
22
23
  * Mirrors `AGENT_ID_PATTERN` in impl.ts — the client cannot import host-side
23
24
  * modules, so this copy must stay in sync (same deal as the preset copies,
@@ -442,21 +443,15 @@ function partitionAgents(detectedAgents, agents) {
442
443
  * def (env / npmPackage / exposedModels included); the synthesized fallback
443
444
  * covers only the fields an `AgentView` carries, so anything else compares as
444
445
  * "changed" — the honest answer when the host never reported it.
446
+ *
447
+ * The idle-release field is compared AS STORED: an absent per-agent field
448
+ * means "follow the global window", and the host's resolved value must not be
449
+ * written back into it — that would freeze the global window into this agent's
450
+ * definition and the global slider would then never reach it.
445
451
  */
446
- /**
447
- * The idle-release window in effect for an agent: an unset per-agent field is
448
- * resolved by the HOST and reported back on `AgentView`, so filling it from
449
- * there keeps "unset" and "explicitly the host default" the same state —
450
- * merely opening the form must never read as an unsaved change.
451
- */
452
- function resolvedIdleTimeoutMs(def, activeAgent) {
453
- if (def.idleTimeoutMs !== undefined || activeAgent?.idleTimeoutMs === undefined)
454
- return def;
455
- return { ...def, idleTimeoutMs: activeAgent.idleTimeoutMs };
456
- }
457
452
  function persistedDef(detected, activeAgent) {
458
453
  if (detected.currentConfig !== undefined)
459
- return resolvedIdleTimeoutMs(detected.currentConfig, activeAgent);
454
+ return detected.currentConfig;
460
455
  if (activeAgent === undefined)
461
456
  return undefined;
462
457
  return {
@@ -467,7 +462,6 @@ function persistedDef(detected, activeAgent) {
467
462
  ...(activeAgent.cwd === undefined ? {} : { cwd: activeAgent.cwd }),
468
463
  allowedCwdRoots: [...activeAgent.allowedCwdRoots],
469
464
  env: {},
470
- ...(activeAgent.idleTimeoutMs === undefined ? {} : { idleTimeoutMs: activeAgent.idleTimeoutMs }),
471
465
  };
472
466
  }
473
467
  /**
@@ -498,9 +492,10 @@ export function payloadEquals(payload, current) {
498
492
  return false;
499
493
  if ((payload.autoDiscoverModels !== false) !== (current.autoDiscoverModels !== false))
500
494
  return false;
501
- // An absent field means "use the host default" — which is what `current`
502
- // already resolved to (see `resolvedIdleTimeoutMs`), so it is not a change.
503
- if (payload.idleTimeoutMs !== undefined && payload.idleTimeoutMs !== current.idleTimeoutMs)
495
+ // The per-agent OVERRIDE, compared as stored: an absent field on both sides
496
+ // means "follow the global window" and is not a change; a field on one
497
+ // side only means the user is adding or dropping an override.
498
+ if (payload.idleTimeoutMs !== current.idleTimeoutMs)
504
499
  return false;
505
500
  return true;
506
501
  }
@@ -737,6 +732,82 @@ function ApprovalDeadlinesCard(props) {
737
732
  const waitMode = deadlines.mode === 'wait';
738
733
  return (_jsxs("section", { className: "dwa-section", children: [_jsx("div", { className: "dwa-section-heading", children: _jsxs("div", { children: [_jsx("h3", { className: "dwa-section-title", children: acp.label('审批时限', 'Approval deadlines') }), _jsx("p", { className: "dwa-section-copy", children: acp.label('权限请求的等待时长与超时处理方式。', 'How long to wait for a permission request, and what happens when nobody answers.') })] }) }), _jsxs("div", { className: "dwa-preferences", children: [_jsx(DeadlineModeBlock, { acp: acp, mode: deadlines.mode, override: modeNote }), _jsx(DeadlineSliderBlock, { acp: acp, value: waitMode ? undefined : deadlines.permissionTimeoutMs, mode: deadlines.mode, override: baselineNote }), facts.overridden && (_jsx("div", { className: "dwa-deadline-reset", children: _jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", onClick: () => void acp.resetApprovalDeadlines(), children: acp.label('恢复配置文件的值', 'Use config file value') }) }))] })] }));
739
734
  }
735
+ /**
736
+ * The idle-release window, as the settings page edits it.
737
+ *
738
+ * ONE control, because there is ONE window: how long a chat process may sit
739
+ * idle before it is parked. The slider snaps to the shared ladder
740
+ * ({@link IDLE_RELEASE_MS}), so every rung it offers is a window the host can
741
+ * run.
742
+ *
743
+ * The card reports the deployment baseline as the fact being overridden, and
744
+ * offers a reset ONLY when something is really overridden — the same rule the
745
+ * approval deadline card follows.
746
+ */
747
+ function IdleReleaseCard(props) {
748
+ const { acp, ui } = props;
749
+ const release = ui.idleRelease;
750
+ // Older host half: no card rather than a number this browser invented.
751
+ if (release === undefined)
752
+ return null;
753
+ // The value a RESET returns to: the window `cordis.patch.yml` names, else
754
+ // the built-in default. The same split the approval card keeps — the
755
+ // reset must restore what the deployment asked for, not a constant.
756
+ const baseline = release.defaultIdleTimeoutMs;
757
+ // A reset is only honest when something is actually being overridden.
758
+ const overridden = release.idleTimeoutMs !== baseline;
759
+ // A window the deployment named that is not on the ladder still runs as
760
+ // configured: the thumb parks on the nearest rung and the readout prints the
761
+ // exact number, so the card never lies about what is in force.
762
+ const index = IDLE_RELEASE_MS.indexOf(release.idleTimeoutMs);
763
+ const exact = index >= 0;
764
+ const shown = exact ? release.idleTimeoutMs : nearestIdleRung(release.idleTimeoutMs);
765
+ // The config file's window is worth saying only when it is being
766
+ // outranked — otherwise it IS what runs, and saying "the value set here
767
+ // wins" would name a win nobody claimed.
768
+ const note = release.configuredIdleTimeoutMs === undefined
769
+ || release.configuredIdleTimeoutMs === release.idleTimeoutMs
770
+ ? undefined
771
+ : acp.label(`配置文件(cordis.patch.yml)设为 ${idleWindowText(release.configuredIdleTimeoutMs, acp)},此处设置优先。`, `The config file (cordis.patch.yml) asks for ${idleWindowText(release.configuredIdleTimeoutMs, acp)}; the value set here wins.`);
772
+ const resetLabel = release.configuredIdleTimeoutMs === undefined
773
+ ? acp.label('恢复默认值', 'Restore default')
774
+ : acp.label('恢复配置文件的值', 'Use config file value');
775
+ return (_jsxs("section", { className: "dwa-section", children: [_jsx("div", { className: "dwa-section-heading", children: _jsxs("div", { children: [_jsx("h3", { className: "dwa-section-title", children: acp.label('空闲释放', 'Idle release') }), _jsx("p", { className: "dwa-section-copy", children: acp.label('空闲多久后挂起聊天进程。', 'How long an idle chat process is kept before it is parked.') })] }) }), _jsxs("div", { className: "dwa-preferences", children: [_jsxs("div", { className: "dwa-preference-block", children: [_jsx("div", { className: "dwa-preference-label", children: acp.label('释放窗口', 'Release window') }), _jsx("div", { className: "dwa-deadline-value", children: idleWindowText(release.idleTimeoutMs, acp) }), _jsx("input", { className: "dwa-deadline-range", type: "range", min: 0, max: IDLE_RELEASE_MS.length - 1, step: 1, value: exact ? index : IDLE_RELEASE_MS.indexOf(shown), "aria-label": acp.label('释放窗口', 'Release window'), onChange: event => {
776
+ const next = IDLE_RELEASE_MS[Number(event.target.value)];
777
+ if (next !== undefined && next !== release.idleTimeoutMs)
778
+ void acp.setIdleRelease(next);
779
+ } }), _jsx("div", { className: "dwa-deadline-ticks", children: IDLE_RELEASE_MS.filter((_, i) => i % 2 === 0).map(ms => (_jsx("span", { children: idleWindowText(ms, acp) }, ms))) }), _jsx("div", { className: "dwa-preference-copy", children: acp.label('挂起后会话仍保留,下一条消息自动恢复。', 'A parked session is kept; the next message resumes it.') }), note !== undefined && _jsxs("div", { className: "dwa-deadline-flag", children: ["\u26A0 ", note] })] }), overridden && (_jsx("div", { className: "dwa-deadline-reset", children: _jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", onClick: () => void acp.resetIdleRelease(), children: resetLabel }) }))] })] }));
780
+ }
781
+ /**
782
+ * The rung nearest a window that is not on the ladder — used only to park the
783
+ * thumb; the readout always prints the exact value.
784
+ */
785
+ function nearestIdleRung(ms) {
786
+ let best = IDLE_RELEASE_MS[0];
787
+ let bestDistance = Number.POSITIVE_INFINITY;
788
+ for (const rung of IDLE_RELEASE_MS) {
789
+ const distance = Math.abs(rung - ms);
790
+ if (distance < bestDistance) {
791
+ bestDistance = distance;
792
+ best = rung;
793
+ }
794
+ }
795
+ return best;
796
+ }
797
+ /**
798
+ * `30 分钟` — the readout and the tick label, one formatter.
799
+ *
800
+ * ALWAYS minutes, even at the top of the ladder: a scale that runs "1 分钟"
801
+ * to "50 分钟" and then "1 小时" reads as two different scales.
802
+ */
803
+ function idleWindowText(ms, acp) {
804
+ // 0 is the deployment's way of saying "never release", and reading it as
805
+ // "0 分钟" would say the opposite.
806
+ if (ms <= 0)
807
+ return acp.label('永不释放', 'Never');
808
+ const minutes = Math.round(ms / 60_000);
809
+ return acp.label(`${String(minutes)} 分钟`, `${String(minutes)}m`);
810
+ }
740
811
  /**
741
812
  * Local wall-clock `HH:MM:SS` for one runtime-log line. Hand-built on purpose:
742
813
  * `toLocaleTimeString` may drop seconds or pick a 12-hour clock depending on
@@ -883,7 +954,7 @@ export function AcpSettingsSection(props) {
883
954
  && update.status !== 'not-installed';
884
955
  return (_jsxs("div", { className: "dwa-root", children: [_jsxs("header", { className: "dwa-page-header", children: [_jsxs("div", { className: "dwa-page-heading", children: [_jsxs("h2", { className: "dwa-page-title", "aria-label": acp.label('NTR 设置', 'NTR settings'), children: [acp.label('NTR 设置', 'NTR settings'), ui.pluginVersion !== undefined && (versionOpensUpdate
885
956
  ? (_jsx("button", { type: "button", className: "dwa-badge dwa-version dwa-version-btn", title: versionTitle, "aria-expanded": updateOpen, "aria-controls": "ntr-plugin-update-card", onClick: () => setUpdateOpen(open => !open), children: `v${ui.pluginVersion}` }))
886
- : _jsx("span", { className: "dwa-badge dwa-version", title: versionTitle, children: `v${ui.pluginVersion}` })), update !== undefined && (_jsx(PluginUpdateBadge, { acp: acp, update: update, open: updateOpen, onToggle: () => setUpdateOpen(open => !open) }))] }), _jsx("p", { className: "dwa-page-description", children: acp.label('连接智能体,设置你的对话偏好。', 'Connect agents and set your conversation preferences.') })] }), page === 'agents' && configured.length > 0 && (_jsx("button", { type: "button", className: "dwa-btn dwa-btn-primary", onClick: openAdd, children: acp.label('添加智能体', 'Add agent') }))] }), update !== undefined && updateCardVisible(update, updateOpen) && (_jsx(PluginUpdateCard, { acp: acp, update: update, onClose: () => setUpdateOpen(false) })), ledger.visible && (_jsxs("div", { className: "dwa-card dwa-card-warn", style: { marginBottom: 16 }, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 4 }, children: acp.label('待处理权限请求', 'Pending Permission Requests') }), _jsx("div", { className: "dwa-hint", style: { marginBottom: 8 }, children: acp.label('对话打开时在那边的卡片上回答;这里兜底显示其他会话的询问。', 'Answer in the conversation card while that conversation is open; this list covers the asks whose conversation is not.') }), ui.permissions.map(permission => (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6, flexWrap: 'wrap' }, children: [_jsxs("span", { style: { flex: 1, minWidth: 160 }, children: [_jsx("span", { style: { fontWeight: 600 }, children: permission.toolName }), permission.reason !== undefined && (_jsxs("span", { style: { color: 'var(--ntr-muted)' }, children: [" \u00B7 ", permission.reason] }))] }), permission.options.map(option => (_jsx("button", { type: "button", className: "dwa-btn", onClick: () => { void acp.respondPermission(permission.requestId, option.optionId).catch(() => undefined); }, children: option.name }, option.optionId))), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-quiet", onClick: () => { void acp.respondPermission(permission.requestId).catch(() => undefined); }, children: acp.label('取消', 'Cancel') })] }, permission.requestId)))] })), ui.notice !== undefined && (_jsxs("div", { className: "dwa-card dwa-card-warn", style: { marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }, children: [_jsx("span", { style: { flex: 1 }, children: ui.notice }), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-quiet", "aria-label": acp.label('关闭通知', 'Dismiss notice'), onClick: () => acp.clearNotice(), children: "\u2715" })] })), presetProblem && (_jsxs("div", { className: "dwa-card dwa-card-warn dwa-notice", role: "status", children: [_jsx("span", { children: acp.label('NTR 模式需要检查,当前可能无法开始对话。', 'NTR mode needs attention; new conversations may be unavailable.') }), _jsx("button", { type: "button", className: "dwa-btn", onClick: () => setPage('diagnostics'), children: acp.label('查看诊断', 'View diagnostics') })] })), _jsxs("nav", { className: "dwa-tabs", "aria-label": acp.label('NTR 设置分类', 'NTR settings categories'), children: [_jsx("button", { type: "button", "aria-pressed": page === 'agents', onClick: () => setPage('agents'), children: acp.label('智能体', 'Agents') }), ui.approvalDeadlines !== undefined && _jsx("button", { type: "button", "aria-pressed": page === 'approvals', onClick: () => setPage('approvals'), children: acp.label('审批时限', 'Approval deadlines') }), _jsx("button", { type: "button", "aria-pressed": page === 'appearance', onClick: () => setPage('appearance'), children: acp.label('对话外观', 'Appearance') }), _jsxs("button", { type: "button", "aria-pressed": page === 'logs', onClick: () => setPage('logs'), children: [acp.label('日志', 'Logs'), ui.logUnread > 0 && _jsx("span", { className: "dwa-log-dot", "aria-hidden": "true" })] })] }), page === 'diagnostics' && (_jsxs("section", { className: "dwa-section", children: [_jsx("h3", { className: "dwa-section-title", children: acp.label('连接诊断', 'Connection diagnostics') }), _jsxs("div", { className: "dwa-preference-row", children: [_jsx("span", { children: acp.label('宿主事件流', 'Host event stream') }), _jsx("span", { className: "dwa-hint", children: streamText(ui.stream, acp) })] }), _jsxs("div", { className: "dwa-row-actions", children: [_jsx("button", { type: "button", className: "dwa-btn", onClick: () => { void acp.resync(); void acp.detectAgents(); }, disabled: ui.detecting, children: ui.detecting ? acp.label('检测中...', 'Detecting...') : acp.label('重新检测与刷新', 'Re-detect & Refresh') }), ui.detectSummary !== undefined && _jsx("span", { className: "dwa-hint", role: "status", children: ui.detectSummary })] })] })), page === 'diagnostics' && ui.preset !== undefined && (_jsxs("div", { className: "dwa-card dwa-mode-banner", children: [_jsxs("div", { className: "dwa-mode-title", children: [acp.label('NTR 模式', 'NTR mode'), _jsx("span", { className: "dwa-badge", children: ui.preset.id })] }), _jsx("span", { className: `dwa-badge ${ui.preset.installed ? 'dwa-badge-success' : 'dwa-badge-warn'}`, children: ui.preset.installed ? acp.label('已就绪', 'Ready') : acp.label('需检查', 'Needs attention') }), _jsxs("div", { className: "dwa-mode-copy", children: [ui.preset.installed
957
+ : _jsx("span", { className: "dwa-badge dwa-version", title: versionTitle, children: `v${ui.pluginVersion}` })), update !== undefined && (_jsx(PluginUpdateBadge, { acp: acp, update: update, open: updateOpen, onToggle: () => setUpdateOpen(open => !open) }))] }), _jsx("p", { className: "dwa-page-description", children: acp.label('连接智能体,设置你的对话偏好。', 'Connect agents and set your conversation preferences.') })] }), page === 'agents' && configured.length > 0 && (_jsx("button", { type: "button", className: "dwa-btn dwa-btn-primary", onClick: openAdd, children: acp.label('添加智能体', 'Add agent') }))] }), update !== undefined && updateCardVisible(update, updateOpen) && (_jsx(PluginUpdateCard, { acp: acp, update: update, onClose: () => setUpdateOpen(false) })), ledger.visible && (_jsxs("div", { className: "dwa-card dwa-card-warn", style: { marginBottom: 16 }, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 4 }, children: acp.label('待处理权限请求', 'Pending Permission Requests') }), _jsx("div", { className: "dwa-hint", style: { marginBottom: 8 }, children: acp.label('对话打开时在那边的卡片上回答;这里兜底显示其他会话的询问。', 'Answer in the conversation card while that conversation is open; this list covers the asks whose conversation is not.') }), ui.permissions.map(permission => (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6, flexWrap: 'wrap' }, children: [_jsxs("span", { style: { flex: 1, minWidth: 160 }, children: [_jsx("span", { style: { fontWeight: 600 }, children: permission.toolName }), permission.reason !== undefined && (_jsxs("span", { style: { color: 'var(--ntr-muted)' }, children: [" \u00B7 ", permission.reason] }))] }), permission.options.map(option => (_jsx("button", { type: "button", className: "dwa-btn", onClick: () => { void acp.respondPermission(permission.requestId, option.optionId).catch(() => undefined); }, children: option.name }, option.optionId))), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-quiet", onClick: () => { void acp.respondPermission(permission.requestId).catch(() => undefined); }, children: acp.label('取消', 'Cancel') })] }, permission.requestId)))] })), ui.notice !== undefined && (_jsxs("div", { className: "dwa-card dwa-card-warn", style: { marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }, children: [_jsx("span", { style: { flex: 1 }, children: ui.notice }), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-quiet", "aria-label": acp.label('关闭通知', 'Dismiss notice'), onClick: () => acp.clearNotice(), children: "\u2715" })] })), presetProblem && (_jsxs("div", { className: "dwa-card dwa-card-warn dwa-notice", role: "status", children: [_jsx("span", { children: acp.label('NTR 模式需要检查,当前可能无法开始对话。', 'NTR mode needs attention; new conversations may be unavailable.') }), _jsx("button", { type: "button", className: "dwa-btn", onClick: () => setPage('diagnostics'), children: acp.label('查看诊断', 'View diagnostics') })] })), _jsxs("nav", { className: "dwa-tabs", "aria-label": acp.label('NTR 设置分类', 'NTR settings categories'), children: [_jsx("button", { type: "button", "aria-pressed": page === 'agents', onClick: () => setPage('agents'), children: acp.label('智能体', 'Agents') }), ui.approvalDeadlines !== undefined && _jsx("button", { type: "button", "aria-pressed": page === 'approvals', onClick: () => setPage('approvals'), children: acp.label('审批时限', 'Approval deadlines') }), ui.idleRelease !== undefined && _jsx("button", { type: "button", "aria-pressed": page === 'processes', onClick: () => setPage('processes'), children: acp.label('进程管理', 'Processes') }), _jsx("button", { type: "button", "aria-pressed": page === 'appearance', onClick: () => setPage('appearance'), children: acp.label('对话外观', 'Appearance') }), _jsxs("button", { type: "button", "aria-pressed": page === 'logs', onClick: () => setPage('logs'), children: [acp.label('日志', 'Logs'), ui.logUnread > 0 && _jsx("span", { className: "dwa-log-dot", "aria-hidden": "true" })] })] }), page === 'diagnostics' && (_jsxs("section", { className: "dwa-section", children: [_jsx("h3", { className: "dwa-section-title", children: acp.label('连接诊断', 'Connection diagnostics') }), _jsxs("div", { className: "dwa-preference-row", children: [_jsx("span", { children: acp.label('宿主事件流', 'Host event stream') }), _jsx("span", { className: "dwa-hint", children: streamText(ui.stream, acp) })] }), _jsxs("div", { className: "dwa-row-actions", children: [_jsx("button", { type: "button", className: "dwa-btn", onClick: () => { void acp.resync(); void acp.detectAgents(); }, disabled: ui.detecting, children: ui.detecting ? acp.label('检测中...', 'Detecting...') : acp.label('重新检测与刷新', 'Re-detect & Refresh') }), ui.detectSummary !== undefined && _jsx("span", { className: "dwa-hint", role: "status", children: ui.detectSummary })] })] })), page === 'diagnostics' && ui.preset !== undefined && (_jsxs("div", { className: "dwa-card dwa-mode-banner", children: [_jsxs("div", { className: "dwa-mode-title", children: [acp.label('NTR 模式', 'NTR mode'), _jsx("span", { className: "dwa-badge", children: ui.preset.id })] }), _jsx("span", { className: `dwa-badge ${ui.preset.installed ? 'dwa-badge-success' : 'dwa-badge-warn'}`, children: ui.preset.installed ? acp.label('已就绪', 'Ready') : acp.label('需检查', 'Needs attention') }), _jsxs("div", { className: "dwa-mode-copy", children: [ui.preset.installed
887
958
  ? acp.label('已加入宿主模式列表,新建会话时选择「NTR 模式」即可使用。', 'Added to the harness roster. Pick “NTR mode” when creating a session.')
888
959
  : acp.label('宿主模式列表中没有可用的「NTR 模式」,请检查预设声明。', 'The harness roster has no usable “NTR mode”. Check the preset declaration.'), _jsx("code", { className: "dwa-mode-source dwa-mono", children: ui.preset.source })] }), ui.preset.problem !== undefined && (_jsx("div", { className: "dwa-hint", style: { gridColumn: '1 / -1', color: 'var(--ntr-warn)' }, children: acp.label(`宿主报告:${ui.preset.problem}`, `Harness reported: ${ui.preset.problem}`) }))] })), _jsxs("section", { className: "dwa-section", hidden: page !== 'agents', "aria-label": acp.label('已接入的智能体', 'Connected agents'), children: [_jsxs("div", { className: "dwa-section-heading", children: [_jsx("div", { children: _jsxs("h3", { className: "dwa-section-title", children: [acp.label('已接入的智能体', 'Connected agents'), ui.agentsLoaded && _jsx("span", { className: "dwa-badge", children: configured.length })] }) }), configured.length > 0 && _jsx("span", { className: "dwa-hint", children: acp.label('默认项用于新的 NTR 会话', 'The default serves new NTR conversations') })] }), !ui.agentsLoaded && _jsx("div", { className: "dwa-empty", role: "status", children: acp.label('正在读取智能体…', 'Loading agents…') }), ui.agentsLoaded && configured.length === 0 && (_jsxs("div", { className: "dwa-empty", children: [_jsx("h3", { children: acp.label('接入第一个智能体', 'Connect your first agent') }), _jsx("p", { className: "dwa-section-copy", children: acp.label('选择已有预设,或填写自己的 ACP 启动命令。', 'Choose a preset or enter your own ACP launch command.') }), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-primary", onClick: openAdd, children: acp.label('添加智能体', 'Add agent') })] })), _jsx("div", { className: "dwa-agent-list", children: configured.map(({ info, synthesized }) => {
889
960
  const activeAgent = ui.agents.find(a => a.id === info.id);
@@ -892,7 +963,7 @@ export function AcpSettingsSection(props) {
892
963
  setAddTarget(undefined);
893
964
  setAddOpen(false);
894
965
  setPage('agents');
895
- } }, selected.id)] }))] })] }), _jsx("div", { hidden: page !== 'approvals', children: _jsx(ApprovalDeadlinesCard, { acp: acp, ui: ui }) }), _jsxs("section", { className: "dwa-section", hidden: page !== 'appearance', "aria-label": acp.label('对话外观', 'Appearance'), children: [_jsx("div", { className: "dwa-section-heading", children: _jsxs("div", { children: [_jsx("h3", { className: "dwa-section-title", children: acp.label('对话外观', 'Conversation appearance') }), _jsx("p", { className: "dwa-section-copy", children: acp.label('设置对话中 NTR 挂牌的进程存活提示。', 'Choose how the NTR tag indicates a running agent process.') })] }) }), _jsxs("div", { className: "dwa-preferences", children: [_jsxs("div", { className: "dwa-preference-row", children: [_jsxs("div", { children: [_jsx("div", { className: "dwa-preference-label", children: acp.label('挂牌光晕', 'Tag halo') }), _jsx("div", { className: "dwa-preference-copy", children: acp.label('呼吸为脉动光,常亮为恒定光。', 'Breathe pulses gently; steady keeps a constant glow.') })] }), _jsx("div", { className: "dwa-seg", children: ['breath', 'steady'].map(mode => (_jsx("button", { type: "button", className: ui.tagHalo.mode === mode ? 'is-on' : '', onClick: () => void acp.setTagHalo({ ...ui.tagHalo, mode }), children: mode === 'breath' ? acp.label('呼吸', 'Breathe') : acp.label('常亮', 'Steady') }, mode))) })] }), ui.tagHalo.mode === 'breath' && (_jsxs("div", { className: "dwa-preference-row", children: [_jsxs("div", { children: [_jsx("div", { className: "dwa-preference-label", children: acp.label('呼吸频率', 'Breathing speed') }), _jsx("div", { className: "dwa-preference-copy", children: acp.label('控制光晕的脉动速度。', 'Controls how quickly the halo pulses.') })] }), _jsx("div", { className: "dwa-seg", children: ['slow', 'normal', 'fast'].map(speed => (_jsx("button", { type: "button", className: ui.tagHalo.speed === speed ? 'is-on' : '', onClick: () => void acp.setTagHalo({ ...ui.tagHalo, speed }), children: speed === 'slow' ? acp.label('慢速', 'Slow') : speed === 'normal' ? acp.label('正常', 'Normal') : acp.label('快速', 'Fast') }, speed))) })] }))] })] }), page === 'logs' && (_jsxs("section", { className: "dwa-section", "aria-label": acp.label('运行日志', 'Runtime log'), children: [_jsxs("div", { className: "dwa-section-heading", children: [_jsxs("div", { children: [_jsx("h3", { className: "dwa-section-title", children: acp.label('运行日志', 'Runtime log') }), _jsx("p", { className: "dwa-section-copy", children: acp.label(`agent 运行消息,最多 ${String(LOG_MAX_ENTRIES)} 条。`, `Agent runtime messages, up to ${String(LOG_MAX_ENTRIES)}.`) })] }), ui.logs.length > 0 && (_jsx("button", { type: "button", className: "dwa-btn dwa-btn-quiet", onClick: () => acp.clearLogs(), children: acp.label('清空', 'Clear') }))] }), ui.logs.length === 0
966
+ } }, selected.id)] }))] })] }), _jsx("div", { hidden: page !== 'approvals', children: _jsx(ApprovalDeadlinesCard, { acp: acp, ui: ui }) }), _jsx("div", { hidden: page !== 'processes', children: _jsx(IdleReleaseCard, { acp: acp, ui: ui }) }), _jsxs("section", { className: "dwa-section", hidden: page !== 'appearance', "aria-label": acp.label('对话外观', 'Appearance'), children: [_jsx("div", { className: "dwa-section-heading", children: _jsxs("div", { children: [_jsx("h3", { className: "dwa-section-title", children: acp.label('对话外观', 'Conversation appearance') }), _jsx("p", { className: "dwa-section-copy", children: acp.label('设置对话中 NTR 挂牌的进程存活提示。', 'Choose how the NTR tag indicates a running agent process.') })] }) }), _jsxs("div", { className: "dwa-preferences", children: [_jsxs("div", { className: "dwa-preference-row", children: [_jsxs("div", { children: [_jsx("div", { className: "dwa-preference-label", children: acp.label('挂牌光晕', 'Tag halo') }), _jsx("div", { className: "dwa-preference-copy", children: acp.label('呼吸为脉动光,常亮为恒定光。', 'Breathe pulses gently; steady keeps a constant glow.') })] }), _jsx("div", { className: "dwa-seg", children: ['breath', 'steady'].map(mode => (_jsx("button", { type: "button", className: ui.tagHalo.mode === mode ? 'is-on' : '', onClick: () => void acp.setTagHalo({ ...ui.tagHalo, mode }), children: mode === 'breath' ? acp.label('呼吸', 'Breathe') : acp.label('常亮', 'Steady') }, mode))) })] }), ui.tagHalo.mode === 'breath' && (_jsxs("div", { className: "dwa-preference-row", children: [_jsxs("div", { children: [_jsx("div", { className: "dwa-preference-label", children: acp.label('呼吸频率', 'Breathing speed') }), _jsx("div", { className: "dwa-preference-copy", children: acp.label('控制光晕的脉动速度。', 'Controls how quickly the halo pulses.') })] }), _jsx("div", { className: "dwa-seg", children: ['slow', 'normal', 'fast'].map(speed => (_jsx("button", { type: "button", className: ui.tagHalo.speed === speed ? 'is-on' : '', onClick: () => void acp.setTagHalo({ ...ui.tagHalo, speed }), children: speed === 'slow' ? acp.label('慢速', 'Slow') : speed === 'normal' ? acp.label('正常', 'Normal') : acp.label('快速', 'Fast') }, speed))) })] }))] })] }), page === 'logs' && (_jsxs("section", { className: "dwa-section", "aria-label": acp.label('运行日志', 'Runtime log'), children: [_jsxs("div", { className: "dwa-section-heading", children: [_jsxs("div", { children: [_jsx("h3", { className: "dwa-section-title", children: acp.label('运行日志', 'Runtime log') }), _jsx("p", { className: "dwa-section-copy", children: acp.label(`agent 运行消息,最多 ${String(LOG_MAX_ENTRIES)} 条。`, `Agent runtime messages, up to ${String(LOG_MAX_ENTRIES)}.`) })] }), ui.logs.length > 0 && (_jsx("button", { type: "button", className: "dwa-btn dwa-btn-quiet", onClick: () => acp.clearLogs(), children: acp.label('清空', 'Clear') }))] }), ui.logs.length === 0
896
967
  ? _jsx("div", { className: "dwa-empty", role: "status", children: acp.label('暂无运行日志。', 'No runtime messages yet.') })
897
968
  : (_jsx("ul", { className: "dwa-log-list", children: [...ui.logs].reverse().map(entry => (_jsxs("li", { className: entry.level === 'error' ? 'dwa-log-row is-error' : 'dwa-log-row', children: [_jsx("span", { className: "dwa-log-time dwa-mono", children: logClock(entry.at) }), _jsx("span", { className: `dwa-badge dwa-log-level ${entry.level === 'error' ? 'is-error' : 'is-info'}`, children: entry.level === 'error' ? acp.label('错误', 'Error') : acp.label('信息', 'Info') }), entry.agentId !== undefined && _jsx("span", { className: "dwa-badge", children: entry.agentId }), _jsx("span", { className: "dwa-log-text", children: entry.text })] }, entry.id))) }))] })), _jsxs("footer", { className: "dwa-health", children: [_jsxs("div", { className: "dwa-health-status", children: [_jsx("span", { className: "dwa-dot", style: { background: ui.stream === 'live' ? 'var(--ntr-success)' : ui.stream === 'down' ? 'var(--ntr-danger)' : 'var(--ntr-muted)' } }), _jsx("span", { children: streamText(ui.stream, acp) }), ui.preset !== undefined && _jsx("span", { children: ui.preset.installed && !presetProblem ? acp.label('NTR 模式已就绪', 'NTR mode ready') : acp.label('NTR 模式需检查', 'NTR mode needs attention') })] }), page !== 'diagnostics' && _jsx("button", { type: "button", className: "dwa-btn dwa-btn-quiet", onClick: () => setPage('diagnostics'), children: acp.label('查看诊断', 'View diagnostics') })] })] }));
898
969
  }
@@ -997,13 +1068,6 @@ function AgentRow({ detected, activeAgent, acp, isDefault, synthesized, active,
997
1068
  // here only prevents an unhandled rejection.
998
1069
  onRemove: async () => { await acp.deleteAgent(detected.id).catch(() => { }); } }))] })] }), _jsxs("div", { className: "dwa-agent-body", style: open ? undefined : { display: 'none' }, children: [stderr.length > 0 && _jsx("pre", { className: "dwa-stderr", children: stderr.join('\n') }), _jsxs("nav", { className: "dwa-tabs dwa-detail-tabs", "aria-label": acp.label(`${displayName} 配置`, `${displayName} settings`), children: [configured && _jsx("button", { type: "button", "aria-pressed": detailPage === 'models', onClick: () => setDetailPage('models'), children: acp.label('模型', 'Models') }), _jsx("button", { type: "button", "aria-pressed": detailPage === 'connection', onClick: () => setDetailPage('connection'), children: acp.label('连接', 'Connection') }), _jsx("button", { type: "button", "aria-pressed": detailPage === 'advanced', onClick: () => setDetailPage('advanced'), children: acp.label('高级', 'Advanced') })] }), configured && (_jsx("section", { className: "dwa-subsection", hidden: detailPage !== 'models', children: _jsx(ModelCatalogBlock, { agentId: detected.id, detected: detected, acp: acp, active: active && open && detailPage === 'models', exposed: form.exposed, onExposedChange: exposed => patch({ exposed }), autoDiscover: form.autoDiscoverModels, onAutoDiscoverChange: autoDiscoverModels => patch({ autoDiscoverModels }), isDefault: isDefault }) })), _jsx("section", { className: "dwa-subsection", hidden: detailPage === 'models', children: _jsx(AgentDetails, { acp: acp, advancedOpen: detailPage === 'advanced', dirty: dirty, detected: detected, form: form, patch: patch, isConfigured: configured, newCustom: newCustom, formIncomplete: formIncomplete, idValid: idValid, idTaken: idTaken, cwdInvalid: cwdInvalid, rootsInvalid: rootsInvalid, idleInvalid: idleInvalid, mcpInvalid: mcpInvalid, saving: saving, savedSuccess: savedSuccess, testing: testing, testResult: testResult, showSaveWarning: showSaveWarning, onUseDefault: handleUseDefault, onSave: () => void handleSave(), onTest: () => void handleTest() }) })] })] }));
999
1070
  }
1000
- /**
1001
- * Hand copy of `DEFAULT_TUNING.idleTimeoutMs` (src/bridge.ts): the browser
1002
- * cannot import the host plane, and this number is only ever placeholder text
1003
- * for a NOT-yet-configured agent (a configured one renders the host's resolved
1004
- * value from `AgentView.idleTimeoutMs`). Keep the two in sync.
1005
- */
1006
- const DEFAULT_IDLE_RELEASE_MS = 1_800_000;
1007
1071
  /**
1008
1072
  * Hand copy of `DEFAULT_TUNING.probeIdleTimeoutMs` (src/bridge.ts). A process
1009
1073
  * that has never hosted a session — the model probe / post-save probe — lives
@@ -1028,8 +1092,11 @@ export function initialForm(detected, activeAgent) {
1028
1092
  envRows: Object.entries(config?.env ?? {}).map(([key, value]) => ({ key, value })),
1029
1093
  exposed: config?.exposedModels === undefined ? undefined : [...config.exposedModels],
1030
1094
  autoDiscoverModels: config?.autoDiscoverModels !== false,
1031
- // Prefer the host's resolved value: it is the one actually in effect.
1032
- idleStr: idleText(activeAgent?.idleTimeoutMs ?? config?.idleTimeoutMs),
1095
+ // The agent's OWN override only. The host's resolved value is NOT
1096
+ // written back here: it would freeze the global window into this
1097
+ // agent's definition, and the global slider would then never reach it.
1098
+ // An empty field means "follow the global window".
1099
+ idleStr: idleText(config?.idleTimeoutMs),
1033
1100
  // The MCP list comes back from the host in its NORMALIZED shape (defaults
1034
1101
  // filled in), so re-serializing it reproduces exactly what is stored —
1035
1102
  // that is what keeps the card from being permanently dirty.
@@ -1127,9 +1194,9 @@ function AgentDetails({ acp, advancedOpen, dirty, detected, form, patch, isConfi
1127
1194
  ? acp.label('小写字母或数字开头,可用 a-z 0-9 . _ -,最长 64 字符。', 'Starts with a lowercase letter or digit; a-z 0-9 . _ - allowed; max 64 chars.')
1128
1195
  : acp.label('ID 格式不合法:小写字母或数字开头,可用 a-z 0-9 . _ -。', 'Invalid id: start with a lowercase letter or digit; only a-z 0-9 . _ - allowed.') })] }) })), _jsx("div", { className: "dwa-form-row", children: _jsxs("div", { className: "dwa-form-col", children: [_jsx("label", { className: "dwa-form-label", children: acp.label('显示名', 'Display Name') }), _jsx("input", { className: "dwa-input", "aria-label": acp.label('显示名', 'Display Name'), value: form.title, onChange: e => patch({ title: e.target.value }), placeholder: detected.name === '' ? acp.label('我的 Agent', 'My Agent') : detected.name })] }) }), _jsx("div", { className: "dwa-form-row", children: _jsxs("div", { className: "dwa-form-col", children: [_jsx("label", { className: "dwa-form-label", children: acp.label('启动命令(程序 + 参数)', 'Launch Command (program + arguments)') }), _jsx("input", { className: "dwa-input dwa-input-mono", "aria-label": acp.label('启动命令(程序 + 参数)', 'Launch Command (program + arguments)'), value: form.commandLine, onChange: e => patch({ commandLine: e.target.value }), placeholder: detected.defaultCommand === ''
1129
1196
  ? 'e.g. codebuddy --acp'
1130
- : formatCommandLine(detected.defaultCommand, detected.defaultArgs) }), _jsx("span", { className: "dwa-hint", children: commandPreview }), _jsx("span", { className: "dwa-hint", children: acp.label('含空格的程序路径请加引号。', 'Quote the program path if it contains spaces.') })] }) }), _jsx(WorkspaceBlock, { acp: acp, form: form, patch: patch, cwdInvalid: cwdInvalid, rootsInvalid: rootsInvalid })] }), _jsxs("div", { hidden: !advancedOpen, children: [!newCustom && (_jsxs("div", { className: "dwa-default-hint", children: [_jsxs("div", { children: [_jsx("span", { style: { fontWeight: 600, marginRight: 6 }, children: acp.label('官方测试默认命令:', 'Tested Default Command:') }), _jsxs("code", { className: "dwa-input-mono", style: { background: 'var(--dsw-alias-markdown-inline-code, #0000000a)', padding: '2px 6px', borderRadius: 4 }, children: [detected.defaultCommand, " ", detected.defaultArgs.join(' ')] })] }), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", onClick: onUseDefault, title: acp.label('把命令与参数重置为官方验证值', 'Reset command and arguments to the tested default'), children: acp.label('使用预设值', 'Use Preset') })] })), _jsx("div", { className: "dwa-form-row", children: _jsxs("div", { className: "dwa-form-col", children: [_jsx("label", { className: "dwa-form-label", children: acp.label('npm 包名(PATH 中找不到时自动安装)', 'npm Package (auto-installed when missing from PATH)') }), _jsx("input", { className: "dwa-input dwa-input-mono", value: form.npmPackage, onChange: e => patch({ npmPackage: e.target.value }), placeholder: acp.label('原生支持 ACP 的 agent 无需填写', 'Not needed for agents with native ACP support') })] }) }), _jsx("div", { className: "dwa-form-row", children: _jsxs("div", { className: "dwa-form-col", children: [_jsx("label", { className: "dwa-form-label", children: acp.label('空闲释放(毫秒)', 'Idle Release (ms)') }), _jsx("input", { className: "dwa-input dwa-input-mono", "aria-label": acp.label('空闲释放(毫秒)', 'Idle Release (ms)'), value: form.idleStr, onChange: e => patch({ idleStr: e.target.value }), placeholder: acp.label(`留空 = 宿主默认(承载会话 ${String(DEFAULT_IDLE_RELEASE_MS / 60_000)} 分钟 / 探测进程 ${String(DEFAULT_PROBE_RELEASE_MS / 60_000)} 分钟)`, `Blank = host default (${String(DEFAULT_IDLE_RELEASE_MS / 60_000)} min for a chat process / ${String(DEFAULT_PROBE_RELEASE_MS / 60_000)} min for a probe)`) }), _jsx("span", { className: idleInvalid ? 'dwa-error' : 'dwa-hint', children: idleInvalid
1197
+ : formatCommandLine(detected.defaultCommand, detected.defaultArgs) }), _jsx("span", { className: "dwa-hint", children: commandPreview }), _jsx("span", { className: "dwa-hint", children: acp.label('含空格的程序路径请加引号。', 'Quote the program path if it contains spaces.') })] }) }), _jsx(WorkspaceBlock, { acp: acp, form: form, patch: patch, cwdInvalid: cwdInvalid, rootsInvalid: rootsInvalid })] }), _jsxs("div", { hidden: !advancedOpen, children: [!newCustom && (_jsxs("div", { className: "dwa-default-hint", children: [_jsxs("div", { children: [_jsx("span", { style: { fontWeight: 600, marginRight: 6 }, children: acp.label('官方测试默认命令:', 'Tested Default Command:') }), _jsxs("code", { className: "dwa-input-mono", style: { background: 'var(--dsw-alias-markdown-inline-code, #0000000a)', padding: '2px 6px', borderRadius: 4 }, children: [detected.defaultCommand, " ", detected.defaultArgs.join(' ')] })] }), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", onClick: onUseDefault, title: acp.label('把命令与参数重置为官方验证值', 'Reset command and arguments to the tested default'), children: acp.label('使用预设值', 'Use Preset') })] })), _jsx("div", { className: "dwa-form-row", children: _jsxs("div", { className: "dwa-form-col", children: [_jsx("label", { className: "dwa-form-label", children: acp.label('npm 包名(PATH 中找不到时自动安装)', 'npm Package (auto-installed when missing from PATH)') }), _jsx("input", { className: "dwa-input dwa-input-mono", value: form.npmPackage, onChange: e => patch({ npmPackage: e.target.value }), placeholder: acp.label('原生支持 ACP 的 agent 无需填写', 'Not needed for agents with native ACP support') })] }) }), _jsx("div", { className: "dwa-form-row", children: _jsxs("div", { className: "dwa-form-col", children: [_jsx("label", { className: "dwa-form-label", children: acp.label('空闲释放覆盖(毫秒)', 'Idle Release Override (ms)') }), _jsx("input", { className: "dwa-input dwa-input-mono", "aria-label": acp.label('空闲释放覆盖(毫秒)', 'Idle Release Override (ms)'), value: form.idleStr, onChange: e => patch({ idleStr: e.target.value }), placeholder: acp.label(`留空 = 跟随全局设置(探测进程固定 ${String(DEFAULT_PROBE_RELEASE_MS / 60_000)} 分钟)`, `Blank = follow the global setting (probes always release after ${String(DEFAULT_PROBE_RELEASE_MS / 60_000)} min)`) }), _jsx("span", { className: idleInvalid ? 'dwa-error' : 'dwa-hint', children: idleInvalid
1131
1198
  ? acp.label('必须是 0(永不释放)或 [1000, 86400000] 之间的整数毫秒。', 'Must be 0 (never release) or an integer in [1000, 86400000] ms.')
1132
- : (_jsx(_Fragment, { children: acp.label('承载过会话的进程空闲后挂起;0 不释放,探测进程固定 5 分钟。', 'Park idle chat processes; 0 keeps them alive. Probe processes always release after 5 minutes.') })) })] }) }), _jsxs("div", { className: "dwa-form-row", style: { flexDirection: 'column', gap: 6 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsx("span", { className: "dwa-form-label", children: acp.label('环境变量', 'Environment Variables') }), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", onClick: () => patch({ envRows: [...form.envRows, { key: '', value: '' }] }), children: acp.label('添加', 'Add') })] }), form.envRows.map((row, index) => (_jsxs("div", { style: { display: 'flex', gap: 6, alignItems: 'center' }, children: [_jsx("input", { className: "dwa-input dwa-input-mono", style: { flex: 2 }, value: row.key, onChange: e => patch({ envRows: form.envRows.map((r, i) => (i === index ? { ...r, key: e.target.value } : r)) }), placeholder: "KEY" }), _jsx("input", { className: "dwa-input dwa-input-mono", style: { flex: 3 }, value: row.value, onChange: e => patch({ envRows: form.envRows.map((r, i) => (i === index ? { ...r, value: e.target.value } : r)) }), placeholder: "VALUE" }), _jsx("button", { type: "button", className: "dwa-btn", title: acp.label('删除该变量', 'Remove this variable'), onClick: () => patch({ envRows: form.envRows.filter((_, i) => i !== index) }), children: "\u2715" })] }, index))), form.envRows.length === 0 && (_jsx("span", { className: "dwa-hint", children: acp.label('无自定义变量;子进程继承宿主环境。', 'No custom variables; the child inherits the host environment.') }))] }), _jsxs("div", { className: "dwa-form-row", style: { flexDirection: 'column', gap: 6 }, children: [_jsx("span", { className: "dwa-form-label", children: acp.label('MCP 服务器(JSON)', 'MCP Servers (JSON)') }), _jsx("textarea", { className: "dwa-input dwa-input-mono", style: { minHeight: 96, resize: 'vertical' }, "aria-label": acp.label('MCP 服务器(JSON)', 'MCP Servers (JSON)'), value: form.mcpText, onChange: e => patch({ mcpText: e.target.value }), spellCheck: false, placeholder: '[\n { "name": "example", "transport": "stdio", "command": "/abs/path/to/server", "args": ["--mcp"] }\n]' }), mcpInvalid && (_jsx("span", { className: "dwa-error", children: acp.label('必须是一个 JSON 数组;每个条目写明 name 与 transport。', 'Must be a JSON array; every entry needs a name and a transport.') })), _jsx("span", { className: "dwa-hint", children: acp.label('额外追加给智能体的 MCP 服务器,不影响其已有配置。', 'Additional MCP servers for the agent; its own configuration is unchanged.') })] })] }), !advancedOpen && (mcpInvalid || idleInvalid) && _jsx("p", { className: "dwa-error", role: "alert", children: acp.label('请检查「高级」中的无效配置。', 'Check the invalid settings in Advanced.') }), advancedOpen && (!idValid || idTaken || cwdInvalid || rootsInvalid || parsedCommand.command === '') && _jsx("p", { className: "dwa-error", role: "alert", children: acp.label('请检查「连接」中的未完成或无效配置。', 'Check the incomplete or invalid settings in Connection.') }), _jsxs("div", { className: "dwa-form-actions", children: [_jsx("span", { className: "dwa-hint", children: isConfigured ? dirty ? acp.label('有未保存的更改', 'Unsaved changes') : acp.label('配置已保存', 'Settings saved') : '' }), _jsx("button", { className: "dwa-btn", onClick: onTest, disabled: testing || saving || formIncomplete, title: acp.label('用当前表单值拉起一次性进程并完成 ACP 握手,不保存配置', 'Spawns a throwaway process from the form values and completes the ACP handshake; nothing is saved'), children: testing ? acp.label('测试中...', 'Testing...') : acp.label('测试连接', 'Test Connection') }), _jsx("button", { className: "dwa-btn dwa-btn-primary", onClick: onSave, disabled: saving || testing || formIncomplete || (isConfigured && !dirty), children: saving
1199
+ : (_jsx(_Fragment, { children: acp.label('仅覆盖此智能体;留空即跟随「进程管理」的全局设置。', 'Overrides this agent only; leave blank to follow the global setting in “Processes”.') })) })] }) }), _jsxs("div", { className: "dwa-form-row", style: { flexDirection: 'column', gap: 6 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsx("span", { className: "dwa-form-label", children: acp.label('环境变量', 'Environment Variables') }), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", onClick: () => patch({ envRows: [...form.envRows, { key: '', value: '' }] }), children: acp.label('添加', 'Add') })] }), form.envRows.map((row, index) => (_jsxs("div", { style: { display: 'flex', gap: 6, alignItems: 'center' }, children: [_jsx("input", { className: "dwa-input dwa-input-mono", style: { flex: 2 }, value: row.key, onChange: e => patch({ envRows: form.envRows.map((r, i) => (i === index ? { ...r, key: e.target.value } : r)) }), placeholder: "KEY" }), _jsx("input", { className: "dwa-input dwa-input-mono", style: { flex: 3 }, value: row.value, onChange: e => patch({ envRows: form.envRows.map((r, i) => (i === index ? { ...r, value: e.target.value } : r)) }), placeholder: "VALUE" }), _jsx("button", { type: "button", className: "dwa-btn", title: acp.label('删除该变量', 'Remove this variable'), onClick: () => patch({ envRows: form.envRows.filter((_, i) => i !== index) }), children: "\u2715" })] }, index))), form.envRows.length === 0 && (_jsx("span", { className: "dwa-hint", children: acp.label('无自定义变量;子进程继承宿主环境。', 'No custom variables; the child inherits the host environment.') }))] }), _jsxs("div", { className: "dwa-form-row", style: { flexDirection: 'column', gap: 6 }, children: [_jsx("span", { className: "dwa-form-label", children: acp.label('MCP 服务器(JSON)', 'MCP Servers (JSON)') }), _jsx("textarea", { className: "dwa-input dwa-input-mono", style: { minHeight: 96, resize: 'vertical' }, "aria-label": acp.label('MCP 服务器(JSON)', 'MCP Servers (JSON)'), value: form.mcpText, onChange: e => patch({ mcpText: e.target.value }), spellCheck: false, placeholder: '[\n { "name": "example", "transport": "stdio", "command": "/abs/path/to/server", "args": ["--mcp"] }\n]' }), mcpInvalid && (_jsx("span", { className: "dwa-error", children: acp.label('必须是一个 JSON 数组;每个条目写明 name 与 transport。', 'Must be a JSON array; every entry needs a name and a transport.') })), _jsx("span", { className: "dwa-hint", children: acp.label('额外追加给智能体的 MCP 服务器,不影响其已有配置。', 'Additional MCP servers for the agent; its own configuration is unchanged.') })] })] }), !advancedOpen && (mcpInvalid || idleInvalid) && _jsx("p", { className: "dwa-error", role: "alert", children: acp.label('请检查「高级」中的无效配置。', 'Check the invalid settings in Advanced.') }), advancedOpen && (!idValid || idTaken || cwdInvalid || rootsInvalid || parsedCommand.command === '') && _jsx("p", { className: "dwa-error", role: "alert", children: acp.label('请检查「连接」中的未完成或无效配置。', 'Check the incomplete or invalid settings in Connection.') }), _jsxs("div", { className: "dwa-form-actions", children: [_jsx("span", { className: "dwa-hint", children: isConfigured ? dirty ? acp.label('有未保存的更改', 'Unsaved changes') : acp.label('配置已保存', 'Settings saved') : '' }), _jsx("button", { className: "dwa-btn", onClick: onTest, disabled: testing || saving || formIncomplete, title: acp.label('用当前表单值拉起一次性进程并完成 ACP 握手,不保存配置', 'Spawns a throwaway process from the form values and completes the ACP handshake; nothing is saved'), children: testing ? acp.label('测试中...', 'Testing...') : acp.label('测试连接', 'Test Connection') }), _jsx("button", { className: "dwa-btn dwa-btn-primary", onClick: onSave, disabled: saving || testing || formIncomplete || (isConfigured && !dirty), children: saving
1133
1200
  ? acp.label('保存中...', 'Saving...')
1134
1201
  : savedSuccess
1135
1202
  ? acp.label('✓ 已保存', '✓ Saved')
@@ -0,0 +1,66 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The conversation's released-session banner: one row above the composer
4
+ * (`conversation.input.dock`) saying the agent process behind this
5
+ * conversation has been released, with the explicit resume button.
6
+ *
7
+ * WHY A BANNER: releasing a process is deliberately silent (constraint 19 —
8
+ * an idle teardown is the expected lifecycle, no `agent/notice`), and the
9
+ * only other trace was the NTR view tab's status badge — a surface most
10
+ * users never open. The "your next message resumes it" promise is real but
11
+ * unverifiable from the chat itself, so the banner states it here and the
12
+ * button turns it into a gesture (`session/chat-resume`), the explicit form
13
+ * of the same recovery the next prompt performs lazily.
14
+ *
15
+ * The gate is a pure function ({@link releasedBannerVisible}) — the seam the
16
+ * composer's other visibility decisions use, so the rules are testable with
17
+ * no DOM. Like the agent tag and the context ring, an explicit
18
+ * `acpMode === false` outranks a lingering `chatSessions` row: a conversation
19
+ * that left NTR mode must not keep claiming a parked NTR process.
20
+ */
21
+ import { useEffect, useState } from 'react';
22
+ import { ensureAcpStyle, useUi } from "./acp-ui.js";
23
+ /**
24
+ * Whether the banner should render: this conversation's ACP session records
25
+ * are all released — the process is parked (idle release or crash) and
26
+ * nothing is live to serve the next message yet. An empty list (no agent has
27
+ * run here) and a live row anywhere both mean "nothing to recover", so the
28
+ * banner stays out of the way.
29
+ */
30
+ export function releasedBannerVisible(ui, dshSessionId) {
31
+ if (ui.acpMode[dshSessionId] === false)
32
+ return false;
33
+ if (ui.chatSessionsFor !== dshSessionId)
34
+ return false;
35
+ return ui.chatSessions.length > 0 && ui.chatSessions.every(row => row.released === true);
36
+ }
37
+ export function ReleasedBanner({ acp, sessionId }) {
38
+ ensureAcpStyle();
39
+ const ui = useUi(acp);
40
+ const dshSessionId = String(sessionId);
41
+ const [busy, setBusy] = useState(false);
42
+ const [error, setError] = useState();
43
+ // The binding has no announcing frame on load (a conversation opened on an
44
+ // already-parked session shows nothing else that would read the list), so
45
+ // this entry reads it once itself — deduped in flight by the store.
46
+ useEffect(() => {
47
+ void acp.loadChatSessions(dshSessionId);
48
+ }, [acp, dshSessionId]);
49
+ if (!releasedBannerVisible(ui, dshSessionId))
50
+ return null;
51
+ const resume = () => {
52
+ if (busy)
53
+ return;
54
+ setBusy(true);
55
+ setError(undefined);
56
+ void acp.resumeChatSession(dshSessionId)
57
+ // `resumeChatSession` reports to the store's notice too, but that notice
58
+ // lives on the settings page; this pins the failure where the user is
59
+ // looking (same contract as the permission card).
60
+ .catch((failure) => {
61
+ setError(failure instanceof Error ? failure.message : String(failure));
62
+ })
63
+ .finally(() => { setBusy(false); });
64
+ };
65
+ return (_jsxs("div", { className: "ntr-released-banner", role: "region", "aria-label": acp.label('进程已释放', 'Process released'), children: [_jsxs("div", { className: "ntr-released-banner-row", children: [_jsxs("div", { className: "ntr-released-banner-text", children: [_jsx("span", { className: "ntr-released-banner-title", children: acp.label('进程已释放', 'Process released') }), _jsx("span", { className: "ntr-released-banner-body", children: acp.label('上下文已保留;下一条消息将自动恢复,或点击【恢复进程】按钮。', 'Context is kept — your next message resumes it, or click [Resume process].') })] }), _jsx("button", { type: "button", className: "ntr-acp-more is-primary", disabled: busy, title: acp.label('立即拉起 agent 进程并恢复这个会话', 'Bring the agent process back and restore this session now'), onClick: resume, children: busy ? acp.label('正在恢复…', 'Resuming…') : acp.label('恢复进程', 'Resume process') })] }), error !== undefined && (_jsx("div", { className: "ntr-permission-error", role: "alert", children: error }))] }));
66
+ }
@@ -223,9 +223,20 @@ export function describeRoute(agents, bound, defaultAgent, catalog, boundAgentId
223
223
  // pick a conversation set up ahead of its first message could not show the
224
224
  // model and thought level it is about to use, which is the whole point of
225
225
  // choosing them early.
226
+ //
227
+ // A RELEASED row flips the precedence: it only describes what ran when the
228
+ // process was parked, while the pick is what the next resume applies
229
+ // (`applyChatChoice`) — a newer pick must outrank it, or a pre-switch on a
230
+ // parked session looks like a dead control (the chip snaps back to the
231
+ // parked value on every re-read). The parked row still beats nothing, so a
232
+ // conversation that never picked keeps naming what it ran.
226
233
  const pick = choice !== undefined && choice.agentId === agentId ? choice : undefined;
227
- const modelId = bound?.modelId ?? pick?.modelId;
228
- const effort = bound?.reasoningEffort ?? pick?.effort;
234
+ const modelId = bound?.released === true
235
+ ? (pick?.modelId ?? bound?.modelId)
236
+ : (bound?.modelId ?? pick?.modelId);
237
+ const effort = bound?.released === true
238
+ ? (pick?.effort ?? bound?.reasoningEffort)
239
+ : (bound?.reasoningEffort ?? pick?.effort);
229
240
  const reasoning = catalog?.reasoning;
230
241
  // The model the AGENT says it is on, for a conversation nobody has picked a
231
242
  // model for yet. Without it the chip has no label and does not render at all,
@@ -443,6 +454,18 @@ export function AcpSessionControls(props) {
443
454
  if (served && agentListed && agentId !== undefined)
444
455
  void acp.loadAgentCatalog(agentId);
445
456
  }, [acp, served, agentListed, agentId]);
457
+ // The picker could already be open when the first turn starts (clicking the
458
+ // send button is inside the composer card, which AcpMenu treats as its own
459
+ // furniture, so it does not close the menu). Once locked, that menu can only
460
+ // offer a switch the host will refuse — close it instead of leaving a live
461
+ // control over a locked conversation. Kept above every early return so the
462
+ // component never changes its hook order between renders.
463
+ useEffect(() => {
464
+ if (lockedTo === undefined || menu === null)
465
+ return;
466
+ if (menu.sessionId === sessionId && menu.kind === 'agent')
467
+ closeAcpMenu();
468
+ }, [lockedTo, menu?.sessionId, menu?.kind, sessionId]);
446
469
  const route = describeRoute(agents, bound,
447
470
  // A locked conversation has no default fallback: the lock is the answer.
448
471
  lockedTo === undefined ? ui.defaultAgent : undefined, catalog, lockedTo ?? ui.chatBoundAgent[sessionId], choice);
@@ -463,17 +486,15 @@ export function AcpSessionControls(props) {
463
486
  // base never comes back, and the tag is the only place the agent's name
464
487
  // lives.
465
488
  //
466
- // The picker could already be open when the first turn starts (clicking the
467
- // send button is inside the composer card, which AcpMenu treats as its own
468
- // furniture, so it does not close the menu). Once locked, that menu can only
469
- // offer a switch the host will refuse — close it instead of leaving a live
470
- // control over a locked conversation.
471
- useEffect(() => {
472
- if (lockedTo === undefined || menu === null)
473
- return;
474
- if (menu.sessionId === sessionId && menu.kind === 'agent')
475
- closeAcpMenu();
476
- }, [lockedTo, menu?.sessionId, menu?.kind, sessionId]);
489
+ // The gate releases this cell when the conversation leaves ACP mode, and it
490
+ // only ever installs the claim for a session whose verdict it has already
491
+ // folded as TRUE — so the occupant requires that same affirmative fact. A
492
+ // claim held for one more evaluation (or a layout with more than one visible
493
+ // session) must never put NTR chips on an ordinary conversation's bar.
494
+ // Placed AFTER every hook: an early return above a hook would change the
495
+ // hook order between renders.
496
+ if (ui.acpMode[sessionId] !== true)
497
+ return null;
477
498
  // Keep the base mounted for the shatter half even though the lock already
478
499
  // retired it: unmounting on the same commit the lock arrives would skip the
479
500
  // transition entirely (the tag waits for the same phase to leave 'shatter').
@@ -802,6 +823,10 @@ export function AcpSessionMenu(props) {
802
823
  }, [acp, menu, served, agentId]);
803
824
  if (menu === null || menu.sessionId !== sessionId)
804
825
  return null;
826
+ // A stale picker must not outlive the conversation's ACP mode (the gate
827
+ // releases the view/tab, but the overlay is unconditionally registered).
828
+ if (ui.acpMode[sessionId] !== true)
829
+ return null;
805
830
  const route = describeRoute(agents, bound, lockedTo === undefined ? ui.defaultAgent : undefined, catalog, lockedTo ?? ui.chatBoundAgent[sessionId], choice);
806
831
  // A menu-model bug must never retire the whole picker: a render throw inside
807
832
  // this slot entry makes the error boundary ABDICATE it for the rest of the
@@ -43,8 +43,8 @@ const WRAP_MARKER = Symbol.for('dsh-ntr.slash-isolation');
43
43
  * caller reads as "not ACP" (fail-open: an ordinary conversation must never
44
44
  * lose its native commands to a guess).
45
45
  */
46
- export function slashAcpVerdict(snapshot, sessionId, verdict) {
47
- const staged = stagedSessionOf(snapshot);
46
+ export function slashAcpVerdict(snapshot, sessionId, verdict, currentSessionId) {
47
+ const staged = stagedSessionOf(snapshot, currentSessionId);
48
48
  if (staged !== undefined && staged.sessionId === sessionId) {
49
49
  const listed = rowAgentPreset(staged.row);
50
50
  if (listed !== undefined)