dsh-ntr 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +4 -4
- package/README.md +4 -4
- package/cordis.patch.yml +12 -0
- package/lib/bridge.js +646 -68
- package/lib/client/panel.js +162 -4
- package/lib/client/store.js +188 -4
- package/lib/client.js +431 -9
- package/lib/client.js.map +1 -1
- package/lib/impl.js +47 -0
- package/lib/index.js +15 -1
- package/lib/types/bridge.d.ts +206 -4
- package/lib/types/client/store.d.ts +87 -1
- package/lib/types/impl.d.ts +26 -1
- package/lib/types/protocol.d.ts +80 -0
- package/lib/types/update.d.ts +186 -0
- package/lib/update.js +696 -0
- package/package.json +1 -1
- package/src/bridge.ts +691 -68
- package/src/client/panel.tsx +288 -5
- package/src/client/store.ts +234 -8
- package/src/impl.ts +66 -1
- package/src/index.ts +16 -1
- package/src/protocol.ts +84 -0
- package/src/update.ts +712 -0
package/lib/client/panel.js
CHANGED
|
@@ -17,7 +17,7 @@ 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
21
|
/**
|
|
22
22
|
* Mirrors `AGENT_ID_PATTERN` in impl.ts — the client cannot import host-side
|
|
23
23
|
* modules, so this copy must stay in sync (same deal as the preset copies,
|
|
@@ -74,7 +74,7 @@ const STYLE = `
|
|
|
74
74
|
}
|
|
75
75
|
.dwa-page-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 28px; }
|
|
76
76
|
.dwa-page-heading { min-width: 0; }
|
|
77
|
-
.dwa-page-title { margin: 0; font-size: 20px; line-height: 1.35; font-weight: 550; letter-spacing: -0.02em; }
|
|
77
|
+
.dwa-page-title { margin: 0; font-size: 20px; line-height: 1.35; font-weight: 550; letter-spacing: -0.02em; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
78
78
|
.dwa-page-description { margin: 5px 0 0; color: var(--ntr-muted); font-size: var(--ntr-font-sm); }
|
|
79
79
|
.dwa-btn {
|
|
80
80
|
min-height: 32px; box-sizing: border-box; font: inherit; border-radius: var(--ntr-radius-sm);
|
|
@@ -106,6 +106,20 @@ const STYLE = `
|
|
|
106
106
|
.dwa-badge-success { background: var(--dsw-alias-state-success-tertiary, #dcfce7); color: var(--ntr-success); }
|
|
107
107
|
.dwa-badge-warn { background: var(--dsw-alias-state-warn-tertiary, #fef3c7); color: var(--ntr-warn); }
|
|
108
108
|
.dwa-badge-info { background: var(--dsw-alias-state-business-tertiary, #dbeafe); color: var(--dsw-alias-state-business-primary, #2563eb); }
|
|
109
|
+
/* The update badge is a button (it expands the update card): strip the
|
|
110
|
+
browser's default button chrome, keep the badge look. */
|
|
111
|
+
.dwa-update-badge { border: 0; font-family: inherit; cursor: pointer; }
|
|
112
|
+
.dwa-update-badge:hover { opacity: 0.85; }
|
|
113
|
+
/* The version badge doubles as the update entry: same badge look, no chrome. */
|
|
114
|
+
.dwa-version-btn { border: 0; font-family: inherit; cursor: pointer; }
|
|
115
|
+
.dwa-version-btn:hover { opacity: 0.85; }
|
|
116
|
+
.dwa-version { font-family: var(--ds-font-family-code, monospace); }
|
|
117
|
+
/* The update card sits between the page header and the permission mirror. */
|
|
118
|
+
.dwa-update-card { margin-bottom: 16px; }
|
|
119
|
+
.dwa-update-summary { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; font-weight: 550; }
|
|
120
|
+
.dwa-update-copy { margin: 6px 0 0; color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
|
|
121
|
+
.dwa-update-message { margin: 6px 0 0; color: var(--ntr-muted); font-size: var(--ntr-font-xs); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
122
|
+
.dwa-update-actions { margin-top: 10px; }
|
|
109
123
|
.dwa-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
|
|
110
124
|
.dwa-agent-list { display: flex; flex-direction: column; border-top: var(--ntr-line); }
|
|
111
125
|
.dwa-agent-card { border-bottom: var(--ntr-line); min-width: 0; }
|
|
@@ -228,6 +242,17 @@ const STYLE = `
|
|
|
228
242
|
.dwa-deadline-flag { margin-top: 4px; font-size: var(--ntr-font-xs); font-weight: 550; color: var(--ntr-warn); }
|
|
229
243
|
.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; }
|
|
230
244
|
.dwa-deadline-reset { display: flex; justify-content: flex-end; margin-top: 8px; }
|
|
245
|
+
/* Runtime log: one bounded list, newest first. The time column never wraps;
|
|
246
|
+
the message is the only cell that may break anywhere. */
|
|
247
|
+
.dwa-log-list { list-style: none; margin: 0; padding: 0; border: var(--ntr-line); border-radius: var(--ntr-radius); overflow: hidden; }
|
|
248
|
+
.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; }
|
|
249
|
+
.dwa-log-row:first-child { border-top: 0; }
|
|
250
|
+
.dwa-log-row.is-error { background: var(--dsw-alias-state-error-tertiary, #fef2f2); }
|
|
251
|
+
.dwa-log-time { color: var(--ntr-muted); white-space: nowrap; }
|
|
252
|
+
.dwa-log-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
253
|
+
.dwa-log-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ntr-danger); display: inline-block; margin-left: 6px; }
|
|
254
|
+
.dwa-log-level.is-info { background: var(--dsw-alias-state-business-tertiary, #dbeafe); color: var(--dsw-alias-state-business-primary, #2563eb); }
|
|
255
|
+
.dwa-log-level.is-error { background: var(--dsw-alias-state-error-tertiary, #fef2f2); color: var(--ntr-danger); }
|
|
231
256
|
@media (max-width: 640px) {
|
|
232
257
|
.dwa-root { padding: 20px 16px; }
|
|
233
258
|
}
|
|
@@ -712,6 +737,112 @@ function ApprovalDeadlinesCard(props) {
|
|
|
712
737
|
const waitMode = deadlines.mode === 'wait';
|
|
713
738
|
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') }) }))] })] }));
|
|
714
739
|
}
|
|
740
|
+
/**
|
|
741
|
+
* Local wall-clock `HH:MM:SS` for one runtime-log line. Hand-built on purpose:
|
|
742
|
+
* `toLocaleTimeString` may drop seconds or pick a 12-hour clock depending on
|
|
743
|
+
* the browser locale, which makes the column ragged and ambiguous.
|
|
744
|
+
*/
|
|
745
|
+
function logClock(at) {
|
|
746
|
+
const date = new Date(at);
|
|
747
|
+
return [date.getHours(), date.getMinutes(), date.getSeconds()]
|
|
748
|
+
.map(value => String(value).padStart(2, '0'))
|
|
749
|
+
.join(':');
|
|
750
|
+
}
|
|
751
|
+
/** The states that carry an update badge next to the version badge. */
|
|
752
|
+
function updateBadgeText(update, acp) {
|
|
753
|
+
if (update.status === 'available') {
|
|
754
|
+
return acp.label(`可更新 v${update.latest ?? update.current}`, `Update v${update.latest ?? update.current}`);
|
|
755
|
+
}
|
|
756
|
+
if (update.status === 'applying')
|
|
757
|
+
return acp.label('更新中…', 'Updating…');
|
|
758
|
+
if (update.status === 'applied') {
|
|
759
|
+
const installed = update.installed ?? update.latest ?? update.current;
|
|
760
|
+
return acp.label(`已更新 v${installed}`, `Updated v${installed}`);
|
|
761
|
+
}
|
|
762
|
+
if (update.status === 'restart-required') {
|
|
763
|
+
// Only a KNOWN disk version can be named. An interrupted install leaves
|
|
764
|
+
// `installed` unset, and a badge would have to invent a version.
|
|
765
|
+
if (update.installed === undefined)
|
|
766
|
+
return undefined;
|
|
767
|
+
// Neutral: the disk version can be newer OR older than the running one.
|
|
768
|
+
return acp.label(`待重启 v${update.installed}`, `Restart for v${update.installed}`);
|
|
769
|
+
}
|
|
770
|
+
return undefined;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* The version badge's update companion. Every badge is a button, `applying`
|
|
774
|
+
* included: the card is where the install's outcome is read, so the running
|
|
775
|
+
* install must be able to open it too. States without anything to act on get
|
|
776
|
+
* no badge at all — the version badge itself still opens the card.
|
|
777
|
+
*/
|
|
778
|
+
function PluginUpdateBadge(props) {
|
|
779
|
+
const { acp, update, open, onToggle } = props;
|
|
780
|
+
const text = updateBadgeText(update, acp);
|
|
781
|
+
if (text === undefined)
|
|
782
|
+
return null;
|
|
783
|
+
return (_jsx("button", { type: "button", className: "dwa-badge dwa-badge-info dwa-update-badge", "aria-expanded": open, "aria-controls": "ntr-plugin-update-card", onClick: onToggle, children: text }));
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* The update card. In-page, no dialog: the settings page already owns the one
|
|
787
|
+
* action (install the exact version the host reported), and a modal on top of
|
|
788
|
+
* a settings section would hide the version it is updating.
|
|
789
|
+
*
|
|
790
|
+
* The copy stays to one line per state (constraint 33); the host's raw error
|
|
791
|
+
* (`message`) is the only long text, and it wraps on its own line so a stack
|
|
792
|
+
* trace or a registry error is readable without stretching the card.
|
|
793
|
+
*/
|
|
794
|
+
function PluginUpdateCard(props) {
|
|
795
|
+
const { acp, update, onClose } = props;
|
|
796
|
+
const applying = update.status === 'applying';
|
|
797
|
+
const latest = update.latest ?? update.current;
|
|
798
|
+
const onDisk = update.installed !== undefined && update.installed !== update.current
|
|
799
|
+
? update.installed
|
|
800
|
+
: undefined;
|
|
801
|
+
const published = update.publishedAt !== undefined && !Number.isNaN(new Date(update.publishedAt).getTime())
|
|
802
|
+
? new Date(update.publishedAt).toLocaleDateString()
|
|
803
|
+
: undefined;
|
|
804
|
+
const copy = update.status === 'available'
|
|
805
|
+
? acp.label('更新会卸载并重装插件,之后可能需要重启 DSH。', 'Updating uninstalls and reinstalls the plugin, and may require restarting DSH.')
|
|
806
|
+
: update.status === 'applied'
|
|
807
|
+
? acp.label('已应用,刷新页面即可使用新版本。', 'Applied. Reload the page to use the new version.')
|
|
808
|
+
: update.status === 'restart-required'
|
|
809
|
+
? update.installed !== undefined
|
|
810
|
+
? acp.label(`磁盘已是 v${update.installed},重启 DSH 后生效(桌面 App 退出重开;命令行重启 dsh web)。`, `On disk v${update.installed}. Restart DSH to apply (quit and reopen the app, or restart dsh web).`)
|
|
811
|
+
: acp.label('更新可能已完成,请刷新页面确认当前版本。', 'The update may already be complete. Refresh the page to confirm the current version.')
|
|
812
|
+
: update.status === 'busy'
|
|
813
|
+
? acp.label('有进行中的对话,结束后才能更新。', 'Finish the running conversation before updating.')
|
|
814
|
+
: update.status === 'failed'
|
|
815
|
+
? acp.label('更新失败。', 'Update failed.')
|
|
816
|
+
: update.status === 'current'
|
|
817
|
+
? acp.label('已是最新版本。', 'Up to date.')
|
|
818
|
+
: update.status === 'source-install'
|
|
819
|
+
? acp.label('源码或本地安装,无法通过 registry 更新。', 'Source or local install; update it through its own checkout.')
|
|
820
|
+
: update.status === 'managed'
|
|
821
|
+
? acp.label('此安装由宿主管理,不能在这里更新。', 'This install is managed by the host and cannot be updated here.')
|
|
822
|
+
: undefined;
|
|
823
|
+
const showMessage = (update.status === 'failed' || update.status === 'busy' || update.status === 'managed')
|
|
824
|
+
&& update.message !== undefined;
|
|
825
|
+
const apply = async () => {
|
|
826
|
+
if (update.latest === undefined)
|
|
827
|
+
return;
|
|
828
|
+
try {
|
|
829
|
+
await acp.applyPluginUpdate(update.latest);
|
|
830
|
+
}
|
|
831
|
+
catch {
|
|
832
|
+
// The store keeps the "disk may have changed" state and writes its own notice.
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
return (_jsxs("div", { id: "ntr-plugin-update-card", className: "dwa-card dwa-update-card", role: "group", "aria-label": acp.label('插件更新', 'Plugin update'), children: [_jsxs("div", { className: "dwa-update-summary", children: [_jsx("span", { children: acp.label(`当前 v${update.current}`, `Current v${update.current}`) }), onDisk !== undefined && _jsx("span", { className: "dwa-hint", children: acp.label(`磁盘 v${onDisk}`, `On disk v${onDisk}`) }), update.latest !== undefined && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", children: "\u2192" }), _jsx("span", { children: acp.label(`最新 v${latest}`, `Latest v${latest}`) }), published !== undefined && _jsx("span", { className: "dwa-hint", children: published })] }))] }), copy !== undefined && _jsx("p", { className: "dwa-update-copy", children: copy }), showMessage && _jsx("div", { className: "dwa-update-message", children: update.message }), _jsxs("div", { className: "dwa-row-actions dwa-update-actions", children: [update.status === 'available' && (_jsx("button", { type: "button", className: "dwa-btn dwa-btn-primary", onClick: () => { void apply(); }, children: acp.label('更新', 'Update') })), applying && (_jsx("button", { type: "button", className: "dwa-btn dwa-btn-primary", disabled: true, children: acp.label('正在更新…', 'Updating…') })), update.status !== 'applying' && (_jsx("button", { type: "button", className: "dwa-btn", onClick: onClose, children: acp.label('关闭', 'Close') })), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-quiet", disabled: applying, onClick: () => void acp.checkPluginUpdate(true), children: acp.label('重新检查', 'Check again') })] })] }));
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* The card is drawn for every state a host can report about this install,
|
|
839
|
+
* except the two where there is no install to talk about at all.
|
|
840
|
+
*/
|
|
841
|
+
function updateCardVisible(update, open) {
|
|
842
|
+
if (update === undefined || !open)
|
|
843
|
+
return false;
|
|
844
|
+
return update.status !== 'unsupported' && update.status !== 'not-installed';
|
|
845
|
+
}
|
|
715
846
|
export function AcpSettingsSection(props) {
|
|
716
847
|
const acp = props.acp;
|
|
717
848
|
ensureStyle();
|
|
@@ -720,6 +851,18 @@ export function AcpSettingsSection(props) {
|
|
|
720
851
|
const [page, setPage] = useState('agents');
|
|
721
852
|
const [addOpen, setAddOpen] = useState(false);
|
|
722
853
|
const [addTarget, setAddTarget] = useState(undefined);
|
|
854
|
+
const [updateOpen, setUpdateOpen] = useState(false);
|
|
855
|
+
// The store throttles this to one registry read per ten minutes, so every
|
|
856
|
+
// remount (both settings entries mount this section) is cheap.
|
|
857
|
+
useEffect(() => {
|
|
858
|
+
void acp.checkPluginUpdate();
|
|
859
|
+
}, [acp]);
|
|
860
|
+
// Opening the log is the read receipt. The store does not mutate at zero,
|
|
861
|
+
// so this cannot loop.
|
|
862
|
+
useEffect(() => {
|
|
863
|
+
if (page === 'logs' && ui.logUnread > 0)
|
|
864
|
+
acp.markLogsRead();
|
|
865
|
+
}, [acp, page, ui.logUnread]);
|
|
723
866
|
const { configured, available } = partitionAgents(ui.detectedAgents, ui.agents);
|
|
724
867
|
const selected = addTarget ?? available[0] ?? NEW_AGENT_SEED;
|
|
725
868
|
const openAdd = () => {
|
|
@@ -727,7 +870,20 @@ export function AcpSettingsSection(props) {
|
|
|
727
870
|
setPage('add');
|
|
728
871
|
};
|
|
729
872
|
const presetProblem = ui.preset !== undefined && (!ui.preset.installed || ui.preset.problem !== undefined);
|
|
730
|
-
|
|
873
|
+
const update = ui.pluginUpdate;
|
|
874
|
+
// A source install cannot be replaced by this browser; the version badge
|
|
875
|
+
// says so on hover. Neutral wording: a checkout may be git or not.
|
|
876
|
+
const versionTitle = update?.status === 'source-install'
|
|
877
|
+
? acp.label('源码或本地安装,无法通过 registry 更新。', 'Source or local install; update it through its own checkout.')
|
|
878
|
+
: undefined;
|
|
879
|
+
// The running version is the update entry: every state with a real install
|
|
880
|
+
// report can open the card, including "up to date" (which offers a re-check).
|
|
881
|
+
const versionOpensUpdate = update !== undefined
|
|
882
|
+
&& update.status !== 'unsupported'
|
|
883
|
+
&& update.status !== 'not-installed';
|
|
884
|
+
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
|
+
? (_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
|
|
731
887
|
? acp.label('已加入宿主模式列表,新建会话时选择「NTR 模式」即可使用。', 'Added to the harness roster. Pick “NTR mode” when creating a session.')
|
|
732
888
|
: 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 }) => {
|
|
733
889
|
const activeAgent = ui.agents.find(a => a.id === info.id);
|
|
@@ -736,7 +892,9 @@ export function AcpSettingsSection(props) {
|
|
|
736
892
|
setAddTarget(undefined);
|
|
737
893
|
setAddOpen(false);
|
|
738
894
|
setPage('agents');
|
|
739
|
-
} }, 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("
|
|
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
|
|
896
|
+
? _jsx("div", { className: "dwa-empty", role: "status", children: acp.label('暂无运行日志。', 'No runtime messages yet.') })
|
|
897
|
+
: (_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') })] })] }));
|
|
740
898
|
}
|
|
741
899
|
/** One row per agent: compact when collapsed, the full form when expanded. */
|
|
742
900
|
function AgentRow({ detected, activeAgent, acp, isDefault, synthesized, active, newCustom = false, onCreated, }) {
|
package/lib/client/store.js
CHANGED
|
@@ -39,11 +39,19 @@ export function deadlineRemainingLabel(deadlineAt, now, mode) {
|
|
|
39
39
|
const rest = seconds % 60;
|
|
40
40
|
return rest === 0 ? `${String(minutes)}m` : `${String(minutes)}m ${String(rest)}s`;
|
|
41
41
|
}
|
|
42
|
+
/** Cap on retained runtime-log lines; older entries fall off the front. */
|
|
43
|
+
export const LOG_MAX_ENTRIES = 200;
|
|
42
44
|
const MAX_BLOCKS = 400;
|
|
43
45
|
/** How many observed ACP transcripts stay live at once (insertion-ordered eviction). */
|
|
44
46
|
const MAX_WATCHED = 6;
|
|
45
47
|
/** Raw JSON budget for one event block; the cut is marked. */
|
|
46
48
|
const RAW_MAX = 256_000;
|
|
49
|
+
/**
|
|
50
|
+
* How long a fetched update state stays fresh. The settings panel asks on every
|
|
51
|
+
* mount; without this, every open would hit the registry. A forced check (the
|
|
52
|
+
* card's own button) always goes out.
|
|
53
|
+
*/
|
|
54
|
+
const PLUGIN_UPDATE_TTL_MS = 10 * 60 * 1000;
|
|
47
55
|
/** Pretty-printed JSON of an agent message (never throws, cuts only at {@link RAW_MAX} with a marker). */
|
|
48
56
|
export function jsonText(value) {
|
|
49
57
|
let text;
|
|
@@ -279,6 +287,14 @@ export const DEFAULT_KNOWN_PRESETS = [
|
|
|
279
287
|
defaultCommand: 'opencode',
|
|
280
288
|
defaultArgs: ['acp'],
|
|
281
289
|
},
|
|
290
|
+
{
|
|
291
|
+
id: 'cursor',
|
|
292
|
+
name: 'Cursor CLI',
|
|
293
|
+
description: 'Cursor CLI — run as `agent acp` over stdio (legacy alias `cursor-agent acp`)',
|
|
294
|
+
detected: false,
|
|
295
|
+
defaultCommand: 'agent',
|
|
296
|
+
defaultArgs: ['acp'],
|
|
297
|
+
},
|
|
282
298
|
{
|
|
283
299
|
id: 'qwen',
|
|
284
300
|
name: 'Qwen Code',
|
|
@@ -325,6 +341,8 @@ export class AcpStore {
|
|
|
325
341
|
blocks: [],
|
|
326
342
|
permissions: [],
|
|
327
343
|
busy: false,
|
|
344
|
+
logs: [],
|
|
345
|
+
logUnread: 0,
|
|
328
346
|
detectedAgents: [...DEFAULT_KNOWN_PRESETS],
|
|
329
347
|
detecting: false,
|
|
330
348
|
modelCatalogs: {},
|
|
@@ -346,6 +364,8 @@ export class AcpStore {
|
|
|
346
364
|
};
|
|
347
365
|
listeners = new Set();
|
|
348
366
|
source;
|
|
367
|
+
/** Monotonic id source for {@link AcpLogEntry}s (never reset by `clearLogs`). */
|
|
368
|
+
logSeq = 0;
|
|
349
369
|
lastSeq = new Map();
|
|
350
370
|
/** Sessions with a finalized turn (dedups the cancel-fallback + late real turn/end pair). */
|
|
351
371
|
openTurns = new Set();
|
|
@@ -394,6 +414,22 @@ export class AcpStore {
|
|
|
394
414
|
cachedModelReads = new Set();
|
|
395
415
|
/** Counts behind the last successful explicit re-detect, so a locale switch can re-render its summary. */
|
|
396
416
|
detectCounts;
|
|
417
|
+
/** When {@link UiState.pluginUpdate} was last read from the host (epoch ms). */
|
|
418
|
+
pluginUpdateFetchedAt = 0;
|
|
419
|
+
/**
|
|
420
|
+
* The shared in-flight `plugin/update-check`. The settings section mounts
|
|
421
|
+
* twice (both entries) and the update popover asks too; without this, two
|
|
422
|
+
* mounts in the same tick each fire their own registry read.
|
|
423
|
+
*/
|
|
424
|
+
pluginUpdateInflight;
|
|
425
|
+
/** Re-entry guard for `plugin/update-apply` (a double click is one install). */
|
|
426
|
+
pluginUpdateApplying = false;
|
|
427
|
+
/**
|
|
428
|
+
* Update read generation. An apply bumps it so a check that was already in
|
|
429
|
+
* flight (started before the install) cannot land afterwards and overwrite
|
|
430
|
+
* the install's verdict with the pre-install `available`.
|
|
431
|
+
*/
|
|
432
|
+
pluginUpdateGeneration = 0;
|
|
397
433
|
constructor(rpc, locale) {
|
|
398
434
|
this.rpc = rpc;
|
|
399
435
|
this.locale = locale;
|
|
@@ -415,6 +451,23 @@ export class AcpStore {
|
|
|
415
451
|
for (const listener of [...this.listeners])
|
|
416
452
|
listener();
|
|
417
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* Build the log patch for one line. Does NOT mutate: the caller merges it
|
|
456
|
+
* into the single `mutate` call it was already making for the same event.
|
|
457
|
+
*/
|
|
458
|
+
logPatch(level, text, agentId) {
|
|
459
|
+
const entry = {
|
|
460
|
+
id: ++this.logSeq,
|
|
461
|
+
at: Date.now(),
|
|
462
|
+
level,
|
|
463
|
+
text,
|
|
464
|
+
...(agentId === undefined ? {} : { agentId }),
|
|
465
|
+
};
|
|
466
|
+
return {
|
|
467
|
+
logs: [...this.state.logs, entry].slice(-LOG_MAX_ENTRIES),
|
|
468
|
+
logUnread: this.state.logUnread + 1,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
418
471
|
// ---- Streaming update coalescing ----
|
|
419
472
|
/**
|
|
420
473
|
* Live `session/update` frames waiting to be folded. Agents stream at token
|
|
@@ -533,7 +586,7 @@ export class AcpStore {
|
|
|
533
586
|
/** Snapshot refresh used on panel open and after a stream outage. */
|
|
534
587
|
async resync() {
|
|
535
588
|
try {
|
|
536
|
-
const [{ agents }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult] = await Promise.all([
|
|
589
|
+
const [{ agents }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult, versionResult] = await Promise.all([
|
|
537
590
|
this.call('agents/list'),
|
|
538
591
|
this.call('sessions/list'),
|
|
539
592
|
// The plugin's OWN default ACP agent — a plugin-private id, not a DSH
|
|
@@ -547,6 +600,9 @@ export class AcpStore {
|
|
|
547
600
|
// `undefined` (older half) hides the deadline card instead of showing a
|
|
548
601
|
// number this browser made up.
|
|
549
602
|
this.call('ui/approval-deadlines').catch(() => ({ deadlines: undefined })),
|
|
603
|
+
// The settings badge's data source; older halves answer `unknown
|
|
604
|
+
// endpoint`, which hides the badge.
|
|
605
|
+
this.call('plugin/version').catch(() => null),
|
|
550
606
|
]);
|
|
551
607
|
const detectedAgents = (detectResult && detectResult.agents.length > 0)
|
|
552
608
|
? detectResult.agents
|
|
@@ -561,6 +617,7 @@ export class AcpStore {
|
|
|
561
617
|
preset: presetResult?.preset ?? undefined,
|
|
562
618
|
tagHalo: tagHaloResult?.halo ?? { ...DEFAULT_TAG_HALO },
|
|
563
619
|
approvalDeadlines: deadlinesResult.deadlines,
|
|
620
|
+
pluginVersion: versionResult?.version !== undefined && versionResult.version !== '' ? versionResult.version : undefined,
|
|
564
621
|
});
|
|
565
622
|
const active = this.state.activeSessionId ?? sessions[0]?.sessionId;
|
|
566
623
|
if (active !== undefined)
|
|
@@ -570,6 +627,104 @@ export class AcpStore {
|
|
|
570
627
|
this.mutate({ notice: String(error instanceof Error ? error.message : error) });
|
|
571
628
|
}
|
|
572
629
|
}
|
|
630
|
+
/**
|
|
631
|
+
* Read the plugin's update state (`plugin/update-check`). Throttled to ten
|
|
632
|
+
* minutes unless `force`: the settings panel asks on mount, and every
|
|
633
|
+
* remount must not hit the registry.
|
|
634
|
+
*
|
|
635
|
+
* An older host serves no such endpoint (`unknown endpoint`). That is a
|
|
636
|
+
* normal degradation, not an error worth a notice: the answer is `undefined`
|
|
637
|
+
* and the panel simply keeps its update entry hidden.
|
|
638
|
+
*/
|
|
639
|
+
async checkPluginUpdate(force = false) {
|
|
640
|
+
// One read for every caller while it is in flight, force included: the
|
|
641
|
+
// settings page and the popover mounting together must not hit the
|
|
642
|
+
// registry twice.
|
|
643
|
+
if (this.pluginUpdateInflight !== undefined)
|
|
644
|
+
return this.pluginUpdateInflight;
|
|
645
|
+
const current = this.state.pluginUpdate;
|
|
646
|
+
if (!force && current !== undefined && Date.now() - this.pluginUpdateFetchedAt < PLUGIN_UPDATE_TTL_MS) {
|
|
647
|
+
return current;
|
|
648
|
+
}
|
|
649
|
+
const generation = this.pluginUpdateGeneration;
|
|
650
|
+
const inflight = this.readPluginUpdate(generation);
|
|
651
|
+
this.pluginUpdateInflight = inflight;
|
|
652
|
+
try {
|
|
653
|
+
return await inflight;
|
|
654
|
+
}
|
|
655
|
+
finally {
|
|
656
|
+
this.pluginUpdateInflight = undefined;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
async readPluginUpdate(generation) {
|
|
660
|
+
try {
|
|
661
|
+
const pluginUpdate = await this.call('plugin/update-check');
|
|
662
|
+
// An install started while this read was in flight: its answer is
|
|
663
|
+
// already stale, and the install's verdict owns the state now.
|
|
664
|
+
if (generation !== this.pluginUpdateGeneration)
|
|
665
|
+
return undefined;
|
|
666
|
+
this.pluginUpdateFetchedAt = Date.now();
|
|
667
|
+
this.mutate({ pluginUpdate });
|
|
668
|
+
return pluginUpdate;
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
return undefined;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Install one exact version (`plugin/update-apply`). The status flips to
|
|
676
|
+
* `applying` first so the badge answers the click; the host's answer then
|
|
677
|
+
* becomes the state.
|
|
678
|
+
*
|
|
679
|
+
* A failed call does NOT prove the install did not happen: on an HMR host
|
|
680
|
+
* the update uninstalls the plugin before reinstalling it, which can cut the
|
|
681
|
+
* response. Whatever the error, the disk may already carry the new version,
|
|
682
|
+
* so the state becomes `restart-required` (the one neutral verdict for "the
|
|
683
|
+
* running code is not the disk's") with `installed` dropped (unknown, not a
|
|
684
|
+
* fact to display) and the notice asks for a reload rather than restoring
|
|
685
|
+
* `available` and pretending nothing changed.
|
|
686
|
+
*/
|
|
687
|
+
async applyPluginUpdate(version) {
|
|
688
|
+
if (this.pluginUpdateApplying)
|
|
689
|
+
return undefined;
|
|
690
|
+
this.pluginUpdateApplying = true;
|
|
691
|
+
// Any check already in flight predates the install; its answer must not
|
|
692
|
+
// land on top of the install's verdict.
|
|
693
|
+
this.pluginUpdateGeneration += 1;
|
|
694
|
+
const previous = this.state.pluginUpdate;
|
|
695
|
+
if (previous !== undefined) {
|
|
696
|
+
this.mutate({ pluginUpdate: { ...previous, status: 'applying' } });
|
|
697
|
+
}
|
|
698
|
+
try {
|
|
699
|
+
const pluginUpdate = await this.call('plugin/update-apply', { version });
|
|
700
|
+
this.pluginUpdateFetchedAt = Date.now();
|
|
701
|
+
this.mutate({ pluginUpdate });
|
|
702
|
+
return pluginUpdate;
|
|
703
|
+
}
|
|
704
|
+
catch (error) {
|
|
705
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
706
|
+
// The disk state is UNKNOWN here: the install may have completed before
|
|
707
|
+
// the response was cut. Keep `latest` for display, drop `installed` so no
|
|
708
|
+
// surface can claim a disk version we never read, and reset the throttle
|
|
709
|
+
// so the next check actually asks the host instead of serving this
|
|
710
|
+
// unknown verdict for ten minutes.
|
|
711
|
+
const interrupted = previous !== undefined
|
|
712
|
+
? { ...previous, status: 'restart-required', installed: undefined, message }
|
|
713
|
+
: { status: 'restart-required', current: this.state.pluginVersion ?? '?', message };
|
|
714
|
+
this.pluginUpdateFetchedAt = 0;
|
|
715
|
+
this.mutate({
|
|
716
|
+
pluginUpdate: interrupted,
|
|
717
|
+
notice: this.locale('更新请求已中断;请刷新页面确认当前版本。', 'The update request was interrupted. Reload the page to confirm the installed version.'),
|
|
718
|
+
});
|
|
719
|
+
return undefined;
|
|
720
|
+
}
|
|
721
|
+
finally {
|
|
722
|
+
// Invalidate reads that STARTED during the install: their "applying"
|
|
723
|
+
// answer would otherwise land on top of this call's final verdict.
|
|
724
|
+
this.pluginUpdateGeneration += 1;
|
|
725
|
+
this.pluginUpdateApplying = false;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
573
728
|
async detectAgents() {
|
|
574
729
|
this.mutate({ detecting: true });
|
|
575
730
|
try {
|
|
@@ -1496,6 +1651,16 @@ export class AcpStore {
|
|
|
1496
1651
|
clearNotice() {
|
|
1497
1652
|
this.mutate({ notice: undefined });
|
|
1498
1653
|
}
|
|
1654
|
+
/** Drop every retained runtime-log line (the unread counter restarts too). */
|
|
1655
|
+
clearLogs() {
|
|
1656
|
+
this.mutate({ logs: [], logUnread: 0 });
|
|
1657
|
+
}
|
|
1658
|
+
/** The user looked at the log: clear the unread badge. */
|
|
1659
|
+
markLogsRead() {
|
|
1660
|
+
if (this.state.logUnread === 0)
|
|
1661
|
+
return;
|
|
1662
|
+
this.mutate({ logUnread: 0 });
|
|
1663
|
+
}
|
|
1499
1664
|
flashError(error) {
|
|
1500
1665
|
this.mutate({ notice: error instanceof Error ? error.message : String(error) });
|
|
1501
1666
|
}
|
|
@@ -1503,7 +1668,7 @@ export class AcpStore {
|
|
|
1503
1668
|
applyEvent(event) {
|
|
1504
1669
|
switch (event.type) {
|
|
1505
1670
|
case 'agent/notice': {
|
|
1506
|
-
this.mutate(
|
|
1671
|
+
this.mutate(this.logPatch(event.level, event.text, event.agentId));
|
|
1507
1672
|
return Promise.resolve();
|
|
1508
1673
|
}
|
|
1509
1674
|
case 'agent/status': {
|
|
@@ -1534,6 +1699,7 @@ export class AcpStore {
|
|
|
1534
1699
|
return Promise.resolve();
|
|
1535
1700
|
}
|
|
1536
1701
|
case 'session/closed': {
|
|
1702
|
+
const closingSession = this.state.sessions.find(s => s.sessionId === event.sessionId);
|
|
1537
1703
|
const sessions = this.state.sessions.filter(s => s.sessionId !== event.sessionId);
|
|
1538
1704
|
const active = this.state.activeSessionId === event.sessionId ? sessions[0]?.sessionId : this.state.activeSessionId;
|
|
1539
1705
|
this.openTurns.delete(event.sessionId);
|
|
@@ -1549,7 +1715,9 @@ export class AcpStore {
|
|
|
1549
1715
|
chatTranscripts: closedTranscripts,
|
|
1550
1716
|
...(chatSessions.length !== this.state.chatSessions.length ? { chatSessions } : {}),
|
|
1551
1717
|
permissions: this.state.permissions.filter(p => p.sessionId !== event.sessionId),
|
|
1552
|
-
...(event.reason === 'agent-died'
|
|
1718
|
+
...(event.reason === 'agent-died'
|
|
1719
|
+
? this.logPatch('error', this.locale('会话因 agent 进程退出而终止', 'session ended: agent process died'), closingSession?.agentId)
|
|
1720
|
+
: {}),
|
|
1553
1721
|
});
|
|
1554
1722
|
return Promise.resolve();
|
|
1555
1723
|
}
|
|
@@ -1566,6 +1734,22 @@ export class AcpStore {
|
|
|
1566
1734
|
const chatSessions = this.state.chatSessions.some(s => s.sessionId === event.sessionId)
|
|
1567
1735
|
? this.state.chatSessions.map(s => (s.sessionId === event.sessionId ? { ...s, released: true } : s))
|
|
1568
1736
|
: this.state.chatSessions;
|
|
1737
|
+
// One runtime-log line per release cause; `idle` stays silent on
|
|
1738
|
+
// purpose (an expected lifecycle transition, not a fault).
|
|
1739
|
+
const cause = event.cause ?? 'died';
|
|
1740
|
+
let log = {};
|
|
1741
|
+
if (cause === 'workspace') {
|
|
1742
|
+
log = this.logPatch('info', this.locale('agent 为切换工作区重启,会话已挂起;再次发送消息将自动恢复。', 'agent restarted for its workspace; the session is paused and will resume on your next message.'), event.session.agentId);
|
|
1743
|
+
}
|
|
1744
|
+
else if (cause === 'resume-failed') {
|
|
1745
|
+
log = this.logPatch('error', this.locale('会话恢复失败;再次发送消息将重试恢复。', 'session resume failed; your next message will retry it.'), event.session.agentId);
|
|
1746
|
+
}
|
|
1747
|
+
else if (cause === 'stalled') {
|
|
1748
|
+
log = this.logPatch('error', this.locale('agent 未响应卡死的回合,会话已挂起;再次发送消息将自动恢复。', 'agent did not acknowledge the stalled turn; the session is paused and will resume on your next message.'), event.session.agentId);
|
|
1749
|
+
}
|
|
1750
|
+
else if (cause === 'died') {
|
|
1751
|
+
log = this.logPatch('error', this.locale('agent 进程已退出,会话已挂起;再次发送消息将自动恢复。', 'agent process died; the session is paused and will resume on your next message.'), event.session.agentId);
|
|
1752
|
+
}
|
|
1569
1753
|
this.mutate({
|
|
1570
1754
|
sessions,
|
|
1571
1755
|
...(chatSessions !== this.state.chatSessions ? { chatSessions } : {}),
|
|
@@ -1573,7 +1757,7 @@ export class AcpStore {
|
|
|
1573
1757
|
// An observed transcript keeps its buffer but flips to released, so
|
|
1574
1758
|
// the ACP view can say "会恢复" instead of silently looking dead.
|
|
1575
1759
|
...this.watchedSessionPatch(event.sessionId, event.session),
|
|
1576
|
-
|
|
1760
|
+
...log,
|
|
1577
1761
|
});
|
|
1578
1762
|
return Promise.resolve();
|
|
1579
1763
|
}
|