dsh-ntr 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/cordis.patch.yml +17 -1
- package/lib/bridge.js +614 -25
- package/lib/client/acp-ui.js +103 -3
- package/lib/client/acp-view.js +24 -4
- package/lib/client/branch-action.js +133 -0
- package/lib/client/composer-mode.js +48 -13
- package/lib/client/context-meter.js +86 -0
- package/lib/client/index.js +238 -15
- package/lib/client/panel.js +127 -30
- package/lib/client/released-banner.js +66 -0
- package/lib/client/session-controls.js +38 -13
- package/lib/client/slash-isolation.js +2 -2
- package/lib/client/store.js +403 -16
- package/lib/client.js +1267 -70
- package/lib/client.js.map +1 -1
- package/lib/impl.js +54 -1
- package/lib/index.js +7 -0
- package/lib/protocol.js +19 -0
- package/lib/types/bridge.d.ts +230 -7
- package/lib/types/client/acp-ui.d.ts +34 -2
- package/lib/types/client/acp-view.d.ts +13 -2
- package/lib/types/client/branch-action.d.ts +40 -0
- package/lib/types/client/composer-mode.d.ts +28 -0
- package/lib/types/client/context-meter.d.ts +51 -0
- package/lib/types/client/released-banner.d.ts +34 -0
- package/lib/types/client/slash-isolation.d.ts +1 -1
- package/lib/types/client/store.d.ts +131 -1
- package/lib/types/impl.d.ts +41 -1
- package/lib/types/protocol.d.ts +130 -0
- package/lib/types/update.d.ts +7 -4
- package/lib/update.js +25 -10
- package/package.json +2 -1
- package/src/bridge.ts +645 -27
- package/src/client/acp-ui.tsx +123 -2
- package/src/client/acp-view.tsx +30 -5
- package/src/client/branch-action.tsx +160 -0
- package/src/client/composer-mode.tsx +59 -13
- package/src/client/context-meter.tsx +128 -0
- package/src/client/index.tsx +265 -14
- package/src/client/panel.tsx +229 -41
- package/src/client/primitives.d.ts +27 -0
- package/src/client/released-banner.tsx +97 -0
- package/src/client/session-controls.tsx +34 -11
- package/src/client/slash-isolation.ts +2 -1
- package/src/client/store.ts +463 -20
- package/src/impl.ts +84 -2
- package/src/index.ts +8 -0
- package/src/protocol.ts +129 -0
- package/src/update.ts +21 -8
package/lib/client/panel.js
CHANGED
|
@@ -17,7 +17,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
17
17
|
*/
|
|
18
18
|
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
19
19
|
import { formatCommandLine, isAbsolutePathLike, splitCommandLine } from "./acp-ui.js";
|
|
20
|
-
import { DEFAULT_KNOWN_PRESETS } from "./store.js";
|
|
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,
|
|
@@ -242,6 +243,17 @@ const STYLE = `
|
|
|
242
243
|
.dwa-deadline-flag { margin-top: 4px; font-size: var(--ntr-font-xs); font-weight: 550; color: var(--ntr-warn); }
|
|
243
244
|
.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; }
|
|
244
245
|
.dwa-deadline-reset { display: flex; justify-content: flex-end; margin-top: 8px; }
|
|
246
|
+
/* Runtime log: one bounded list, newest first. The time column never wraps;
|
|
247
|
+
the message is the only cell that may break anywhere. */
|
|
248
|
+
.dwa-log-list { list-style: none; margin: 0; padding: 0; border: var(--ntr-line); border-radius: var(--ntr-radius); overflow: hidden; }
|
|
249
|
+
.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; }
|
|
250
|
+
.dwa-log-row:first-child { border-top: 0; }
|
|
251
|
+
.dwa-log-row.is-error { background: var(--dsw-alias-state-error-tertiary, #fef2f2); }
|
|
252
|
+
.dwa-log-time { color: var(--ntr-muted); white-space: nowrap; }
|
|
253
|
+
.dwa-log-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
254
|
+
.dwa-log-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ntr-danger); display: inline-block; margin-left: 6px; }
|
|
255
|
+
.dwa-log-level.is-info { background: var(--dsw-alias-state-business-tertiary, #dbeafe); color: var(--dsw-alias-state-business-primary, #2563eb); }
|
|
256
|
+
.dwa-log-level.is-error { background: var(--dsw-alias-state-error-tertiary, #fef2f2); color: var(--ntr-danger); }
|
|
245
257
|
@media (max-width: 640px) {
|
|
246
258
|
.dwa-root { padding: 20px 16px; }
|
|
247
259
|
}
|
|
@@ -431,21 +443,15 @@ function partitionAgents(detectedAgents, agents) {
|
|
|
431
443
|
* def (env / npmPackage / exposedModels included); the synthesized fallback
|
|
432
444
|
* covers only the fields an `AgentView` carries, so anything else compares as
|
|
433
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.
|
|
434
451
|
*/
|
|
435
|
-
/**
|
|
436
|
-
* The idle-release window in effect for an agent: an unset per-agent field is
|
|
437
|
-
* resolved by the HOST and reported back on `AgentView`, so filling it from
|
|
438
|
-
* there keeps "unset" and "explicitly the host default" the same state —
|
|
439
|
-
* merely opening the form must never read as an unsaved change.
|
|
440
|
-
*/
|
|
441
|
-
function resolvedIdleTimeoutMs(def, activeAgent) {
|
|
442
|
-
if (def.idleTimeoutMs !== undefined || activeAgent?.idleTimeoutMs === undefined)
|
|
443
|
-
return def;
|
|
444
|
-
return { ...def, idleTimeoutMs: activeAgent.idleTimeoutMs };
|
|
445
|
-
}
|
|
446
452
|
function persistedDef(detected, activeAgent) {
|
|
447
453
|
if (detected.currentConfig !== undefined)
|
|
448
|
-
return
|
|
454
|
+
return detected.currentConfig;
|
|
449
455
|
if (activeAgent === undefined)
|
|
450
456
|
return undefined;
|
|
451
457
|
return {
|
|
@@ -456,7 +462,6 @@ function persistedDef(detected, activeAgent) {
|
|
|
456
462
|
...(activeAgent.cwd === undefined ? {} : { cwd: activeAgent.cwd }),
|
|
457
463
|
allowedCwdRoots: [...activeAgent.allowedCwdRoots],
|
|
458
464
|
env: {},
|
|
459
|
-
...(activeAgent.idleTimeoutMs === undefined ? {} : { idleTimeoutMs: activeAgent.idleTimeoutMs }),
|
|
460
465
|
};
|
|
461
466
|
}
|
|
462
467
|
/**
|
|
@@ -487,9 +492,10 @@ export function payloadEquals(payload, current) {
|
|
|
487
492
|
return false;
|
|
488
493
|
if ((payload.autoDiscoverModels !== false) !== (current.autoDiscoverModels !== false))
|
|
489
494
|
return false;
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
|
|
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)
|
|
493
499
|
return false;
|
|
494
500
|
return true;
|
|
495
501
|
}
|
|
@@ -726,6 +732,93 @@ function ApprovalDeadlinesCard(props) {
|
|
|
726
732
|
const waitMode = deadlines.mode === 'wait';
|
|
727
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') }) }))] })] }));
|
|
728
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
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Local wall-clock `HH:MM:SS` for one runtime-log line. Hand-built on purpose:
|
|
813
|
+
* `toLocaleTimeString` may drop seconds or pick a 12-hour clock depending on
|
|
814
|
+
* the browser locale, which makes the column ragged and ambiguous.
|
|
815
|
+
*/
|
|
816
|
+
function logClock(at) {
|
|
817
|
+
const date = new Date(at);
|
|
818
|
+
return [date.getHours(), date.getMinutes(), date.getSeconds()]
|
|
819
|
+
.map(value => String(value).padStart(2, '0'))
|
|
820
|
+
.join(':');
|
|
821
|
+
}
|
|
729
822
|
/** The states that carry an update badge next to the version badge. */
|
|
730
823
|
function updateBadgeText(update, acp) {
|
|
731
824
|
if (update.status === 'available') {
|
|
@@ -835,6 +928,12 @@ export function AcpSettingsSection(props) {
|
|
|
835
928
|
useEffect(() => {
|
|
836
929
|
void acp.checkPluginUpdate();
|
|
837
930
|
}, [acp]);
|
|
931
|
+
// Opening the log is the read receipt. The store does not mutate at zero,
|
|
932
|
+
// so this cannot loop.
|
|
933
|
+
useEffect(() => {
|
|
934
|
+
if (page === 'logs' && ui.logUnread > 0)
|
|
935
|
+
acp.markLogsRead();
|
|
936
|
+
}, [acp, page, ui.logUnread]);
|
|
838
937
|
const { configured, available } = partitionAgents(ui.detectedAgents, ui.agents);
|
|
839
938
|
const selected = addTarget ?? available[0] ?? NEW_AGENT_SEED;
|
|
840
939
|
const openAdd = () => {
|
|
@@ -855,7 +954,7 @@ export function AcpSettingsSection(props) {
|
|
|
855
954
|
&& update.status !== 'not-installed';
|
|
856
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
|
|
857
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}` }))
|
|
858
|
-
: _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') })] }), 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
|
|
859
958
|
? acp.label('已加入宿主模式列表,新建会话时选择「NTR 模式」即可使用。', 'Added to the harness roster. Pick “NTR mode” when creating a session.')
|
|
860
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 }) => {
|
|
861
960
|
const activeAgent = ui.agents.find(a => a.id === info.id);
|
|
@@ -864,7 +963,9 @@ export function AcpSettingsSection(props) {
|
|
|
864
963
|
setAddTarget(undefined);
|
|
865
964
|
setAddOpen(false);
|
|
866
965
|
setPage('agents');
|
|
867
|
-
} }, 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))) })] }))] })] }), _jsxs("
|
|
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
|
|
967
|
+
? _jsx("div", { className: "dwa-empty", role: "status", children: acp.label('暂无运行日志。', 'No runtime messages yet.') })
|
|
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') })] })] }));
|
|
868
969
|
}
|
|
869
970
|
/** One row per agent: compact when collapsed, the full form when expanded. */
|
|
870
971
|
function AgentRow({ detected, activeAgent, acp, isDefault, synthesized, active, newCustom = false, onCreated, }) {
|
|
@@ -967,13 +1068,6 @@ function AgentRow({ detected, activeAgent, acp, isDefault, synthesized, active,
|
|
|
967
1068
|
// here only prevents an unhandled rejection.
|
|
968
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() }) })] })] }));
|
|
969
1070
|
}
|
|
970
|
-
/**
|
|
971
|
-
* Hand copy of `DEFAULT_TUNING.idleTimeoutMs` (src/bridge.ts): the browser
|
|
972
|
-
* cannot import the host plane, and this number is only ever placeholder text
|
|
973
|
-
* for a NOT-yet-configured agent (a configured one renders the host's resolved
|
|
974
|
-
* value from `AgentView.idleTimeoutMs`). Keep the two in sync.
|
|
975
|
-
*/
|
|
976
|
-
const DEFAULT_IDLE_RELEASE_MS = 1_800_000;
|
|
977
1071
|
/**
|
|
978
1072
|
* Hand copy of `DEFAULT_TUNING.probeIdleTimeoutMs` (src/bridge.ts). A process
|
|
979
1073
|
* that has never hosted a session — the model probe / post-save probe — lives
|
|
@@ -998,8 +1092,11 @@ export function initialForm(detected, activeAgent) {
|
|
|
998
1092
|
envRows: Object.entries(config?.env ?? {}).map(([key, value]) => ({ key, value })),
|
|
999
1093
|
exposed: config?.exposedModels === undefined ? undefined : [...config.exposedModels],
|
|
1000
1094
|
autoDiscoverModels: config?.autoDiscoverModels !== false,
|
|
1001
|
-
//
|
|
1002
|
-
|
|
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),
|
|
1003
1100
|
// The MCP list comes back from the host in its NORMALIZED shape (defaults
|
|
1004
1101
|
// filled in), so re-serializing it reproduces exactly what is stored —
|
|
1005
1102
|
// that is what keeps the card from being permanently dirty.
|
|
@@ -1097,9 +1194,9 @@ function AgentDetails({ acp, advancedOpen, dirty, detected, form, patch, isConfi
|
|
|
1097
1194
|
? acp.label('小写字母或数字开头,可用 a-z 0-9 . _ -,最长 64 字符。', 'Starts with a lowercase letter or digit; a-z 0-9 . _ - allowed; max 64 chars.')
|
|
1098
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 === ''
|
|
1099
1196
|
? 'e.g. codebuddy --acp'
|
|
1100
|
-
: 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('
|
|
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
|
|
1101
1198
|
? acp.label('必须是 0(永不释放)或 [1000, 86400000] 之间的整数毫秒。', 'Must be 0 (never release) or an integer in [1000, 86400000] ms.')
|
|
1102
|
-
: (_jsx(_Fragment, { children: acp.label('
|
|
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
|
|
1103
1200
|
? acp.label('保存中...', 'Saving...')
|
|
1104
1201
|
: savedSuccess
|
|
1105
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?.
|
|
228
|
-
|
|
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
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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)
|