dsh-ntr 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +9 -6
- package/README.md +9 -6
- package/cordis.patch.yml +12 -0
- package/lib/bridge.js +654 -85
- package/lib/client/panel.js +224 -78
- package/lib/client/store.js +133 -1
- package/lib/client.js +702 -304
- 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 +208 -6
- package/lib/types/client/panel.d.ts +0 -1
- package/lib/types/client/store.d.ts +54 -1
- package/lib/types/impl.d.ts +26 -1
- package/lib/types/protocol.d.ts +73 -0
- package/lib/types/update.d.ts +183 -0
- package/lib/update.js +681 -0
- package/package.json +5 -1
- package/src/bridge.ts +700 -83
- package/src/client/panel.tsx +406 -205
- package/src/client/store.ts +146 -2
- package/src/impl.ts +66 -1
- package/src/index.ts +16 -1
- package/src/protocol.ts +76 -0
- package/src/update.ts +699 -0
package/src/client/panel.tsx
CHANGED
|
@@ -19,7 +19,7 @@ import { useEffect, useRef, useState, useSyncExternalStore } from 'react'
|
|
|
19
19
|
import { formatCommandLine, isAbsolutePathLike, splitCommandLine } from './acp-ui.tsx'
|
|
20
20
|
import { DEFAULT_KNOWN_PRESETS } from './store.ts'
|
|
21
21
|
import type { AcpStore, UiState } from './store.ts'
|
|
22
|
-
import type { AcpAgentDef, AcpApprovalDeadlines, AcpCatalogChoice, AcpMcpServerDef, AgentSavePayload, AgentView, DetectedAgentInfo, PermissionTimeoutMode } from '../protocol.ts'
|
|
22
|
+
import type { AcpAgentDef, AcpApprovalDeadlines, AcpCatalogChoice, AcpMcpServerDef, AgentSavePayload, AgentView, DetectedAgentInfo, PermissionTimeoutMode, PluginUpdateState } from '../protocol.ts'
|
|
23
23
|
|
|
24
24
|
interface WithStore { acp: AcpStore }
|
|
25
25
|
|
|
@@ -57,18 +57,34 @@ const STYLE = `
|
|
|
57
57
|
--ntr-success: color-mix(in srgb, var(--dsw-alias-state-success-primary, #16a34a) 72%, var(--dsw-alias-label-primary, #111827));
|
|
58
58
|
--ntr-accent: var(--dsw-alias-button-primary-fill, #2563eb);
|
|
59
59
|
width: 100%; max-width: 960px; box-sizing: border-box; margin: 0 auto;
|
|
60
|
-
padding:
|
|
60
|
+
padding: 28px 32px; min-width: 0; container-type: inline-size; font-size: var(--ntr-font); line-height: 1.55;
|
|
61
61
|
}
|
|
62
|
-
.dwa-root
|
|
62
|
+
.dwa-root [hidden] { display: none !important; }
|
|
63
|
+
.dwa-root button, .dwa-root input, .dwa-root select, .dwa-root textarea { font-family: inherit; }
|
|
64
|
+
.dwa-root input, .dwa-root textarea { min-width: 0; }
|
|
65
|
+
.dwa-tabs { display: flex; gap: 24px; flex-wrap: wrap; border-bottom: var(--ntr-line); margin-bottom: 24px; }
|
|
66
|
+
.dwa-tabs > button { font: inherit; color: var(--ntr-muted); background: transparent; border: 0; border-bottom: 2px solid transparent; padding: 0 0 12px; min-height: 40px; white-space: nowrap; cursor: pointer; }
|
|
67
|
+
.dwa-tabs > button:hover { color: var(--dsw-alias-label-primary, #111827); }
|
|
68
|
+
.dwa-tabs > button[aria-pressed="true"] { color: var(--dsw-alias-label-primary, #111827); border-bottom-color: currentColor; font-weight: 550; }
|
|
69
|
+
.dwa-detail-tabs { margin: 4px 0 20px; gap: 20px; }
|
|
70
|
+
.dwa-detail-tabs > button { font-size: var(--ntr-font-sm); }
|
|
71
|
+
.dwa-row-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
72
|
+
.dwa-notice { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
|
73
|
+
.dwa-notice > span { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
74
|
+
.dwa-empty { text-align: center; padding: 40px 16px; color: var(--ntr-muted); }
|
|
75
|
+
.dwa-empty h3 { font-size: 16px; font-weight: 550; color: var(--dsw-alias-label-primary, #111827); margin: 0 0 8px; }
|
|
76
|
+
.dwa-empty .dwa-btn { margin-top: 20px; }
|
|
77
|
+
.dwa-health { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-top: 24px; color: var(--ntr-muted); font-size: var(--ntr-font-sm); flex-wrap: wrap; }
|
|
78
|
+
.dwa-health-status { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
79
|
+
.dwa-health-status > span:last-child:not(:nth-child(2)) { padding-left: 12px; border-left: var(--ntr-line); }
|
|
80
|
+
.dwa-back { margin-bottom: 16px; }
|
|
63
81
|
.dwa-root button:focus-visible, .dwa-root input:focus-visible, .dwa-root select:focus-visible {
|
|
64
82
|
outline: 2px solid var(--ntr-accent); outline-offset: 2px;
|
|
65
83
|
}
|
|
66
|
-
.dwa-page-header { display: flex; align-items:
|
|
84
|
+
.dwa-page-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 28px; }
|
|
67
85
|
.dwa-page-heading { min-width: 0; }
|
|
68
|
-
.dwa-page-title { margin: 0; font-size: 20px; line-height: 1.35; font-weight:
|
|
86
|
+
.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; }
|
|
69
87
|
.dwa-page-description { margin: 5px 0 0; color: var(--ntr-muted); font-size: var(--ntr-font-sm); }
|
|
70
|
-
.dwa-page-status { display: flex; align-items: center; gap: 8px; margin-top: 10px; color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
|
|
71
|
-
.dwa-page-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
|
72
88
|
.dwa-btn {
|
|
73
89
|
min-height: 32px; box-sizing: border-box; font: inherit; border-radius: var(--ntr-radius-sm);
|
|
74
90
|
border: 1px solid var(--dsw-alias-border-l2, #d1d5db); background: var(--dsw-alias-bg-layer-1, #fff);
|
|
@@ -81,14 +97,16 @@ const STYLE = `
|
|
|
81
97
|
.dwa-btn-primary { background: var(--ntr-accent); color: var(--dsw-alias-label-primary-foreground, #fff); border-color: transparent; font-weight: 550; }
|
|
82
98
|
.dwa-btn-primary:hover { background: var(--ntr-accent); border-color: transparent; opacity: 0.88; }
|
|
83
99
|
.dwa-btn-secondary { background: var(--dsw-alias-bg-layer-2, #f3f4f6); }
|
|
100
|
+
.dwa-btn-quiet { background: transparent; border-color: transparent; color: var(--ntr-muted); }
|
|
101
|
+
.dwa-btn-quiet:hover { border-color: transparent; color: var(--dsw-alias-label-primary, #111827); }
|
|
84
102
|
.dwa-btn-danger { color: var(--ntr-danger); border-color: var(--dsw-alias-state-error-secondary, #fca5a5); }
|
|
85
103
|
.dwa-btn-danger:hover { background: var(--dsw-alias-interactive-bg-hover-danger, #fee2e2); }
|
|
86
104
|
.dwa-section { margin-bottom: 24px; }
|
|
87
105
|
.dwa-section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
|
|
88
|
-
.dwa-section-title { font-size:
|
|
106
|
+
.dwa-section-title { font-size: 13px; font-weight: 550; margin: 0; display: flex; align-items: center; gap: 8px; }
|
|
89
107
|
.dwa-section-copy { margin: 3px 0 0; font-size: var(--ntr-font-xs); color: var(--ntr-muted); }
|
|
90
108
|
.dwa-card { border: var(--ntr-line); border-radius: var(--ntr-radius); padding: 16px; background: var(--dsw-alias-bg-layer-1, #fff); font-size: var(--ntr-font); line-height: 1.55; }
|
|
91
|
-
.dwa-card-warn { border-
|
|
109
|
+
.dwa-card-warn { border-color: color-mix(in srgb, var(--ntr-warn) 30%, var(--dsw-alias-border-l2, #e5e7eb)); background: var(--dsw-alias-state-warn-tertiary, #fffbeb); }
|
|
92
110
|
.dwa-mode-banner { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 5px 16px; align-items: start; margin-bottom: 24px; padding: 14px 16px; }
|
|
93
111
|
.dwa-mode-title { display: flex; align-items: center; gap: 8px; font-weight: 650; }
|
|
94
112
|
.dwa-mode-copy { grid-column: 1 / -1; color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
|
|
@@ -97,26 +115,36 @@ const STYLE = `
|
|
|
97
115
|
.dwa-badge-success { background: var(--dsw-alias-state-success-tertiary, #dcfce7); color: var(--ntr-success); }
|
|
98
116
|
.dwa-badge-warn { background: var(--dsw-alias-state-warn-tertiary, #fef3c7); color: var(--ntr-warn); }
|
|
99
117
|
.dwa-badge-info { background: var(--dsw-alias-state-business-tertiary, #dbeafe); color: var(--dsw-alias-state-business-primary, #2563eb); }
|
|
118
|
+
/* The update badge is a button (it expands the update card): strip the
|
|
119
|
+
browser's default button chrome, keep the badge look. */
|
|
120
|
+
.dwa-update-badge { border: 0; font-family: inherit; cursor: pointer; }
|
|
121
|
+
.dwa-update-badge:hover { opacity: 0.85; }
|
|
122
|
+
/* The version badge doubles as the update entry: same badge look, no chrome. */
|
|
123
|
+
.dwa-version-btn { border: 0; font-family: inherit; cursor: pointer; }
|
|
124
|
+
.dwa-version-btn:hover { opacity: 0.85; }
|
|
125
|
+
.dwa-version { font-family: var(--ds-font-family-code, monospace); }
|
|
126
|
+
/* The update card sits between the page header and the permission mirror. */
|
|
127
|
+
.dwa-update-card { margin-bottom: 16px; }
|
|
128
|
+
.dwa-update-summary { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; font-weight: 550; }
|
|
129
|
+
.dwa-update-copy { margin: 6px 0 0; color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
|
|
130
|
+
.dwa-update-message { margin: 6px 0 0; color: var(--ntr-muted); font-size: var(--ntr-font-xs); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
131
|
+
.dwa-update-actions { margin-top: 10px; }
|
|
100
132
|
.dwa-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
|
|
101
|
-
.dwa-agent-list { display: flex; flex-direction: column;
|
|
102
|
-
.dwa-agent-card { border: var(--ntr-line);
|
|
103
|
-
.dwa-agent-
|
|
104
|
-
.dwa-agent-
|
|
133
|
+
.dwa-agent-list { display: flex; flex-direction: column; border-top: var(--ntr-line); }
|
|
134
|
+
.dwa-agent-card { border-bottom: var(--ntr-line); min-width: 0; }
|
|
135
|
+
.dwa-agent-row { display: grid; grid-template-columns: 38px minmax(0, 1fr) auto; align-items: center; gap: 14px; padding: 16px 0; }
|
|
136
|
+
.dwa-agent-avatar { display: grid; place-items: center; width: 38px; height: 38px; border: var(--ntr-line); border-radius: 9px; background: var(--dsw-alias-bg-layer-2, #f3f4f6); color: var(--ntr-muted); font: 12px var(--ds-font-family-code, monospace); }
|
|
105
137
|
.dwa-caret { font-size: 14px; line-height: 1; display: inline-flex; align-items: center; justify-content: center; width: 1em; }
|
|
106
138
|
.dwa-caret-btn { font: inherit; border: none; background: var(--dsw-alias-bg-layer-2, #f3f4f6); width: 28px; height: 28px; padding: 0; border-radius: var(--ntr-radius-sm); display: inline-flex; align-items: center; justify-content: center; color: var(--ntr-muted); cursor: pointer; }
|
|
107
139
|
.dwa-caret-btn:hover { background: var(--dsw-alias-interactive-bg-active, #e5e7eb); color: inherit; }
|
|
108
140
|
.dwa-agent-summary { min-width: 0; }
|
|
109
141
|
.dwa-agent-titleline { display: flex; align-items: center; gap: 8px; min-width: 0; flex-wrap: wrap; }
|
|
110
|
-
.dwa-agent-name { font-size: 13px; font-weight:
|
|
142
|
+
.dwa-agent-name { font-size: 13px; font-weight: 550; overflow-wrap: anywhere; }
|
|
111
143
|
.dwa-agent-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 4px; color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
|
|
112
144
|
.dwa-agent-meta-separator { opacity: 0.45; }
|
|
113
|
-
.dwa-agent-body {
|
|
114
|
-
.dwa-subsection {
|
|
115
|
-
.dwa-
|
|
116
|
-
.dwa-subsection-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
|
|
117
|
-
.dwa-subsection-title { margin: 0; font-size: 13px; font-weight: 650; }
|
|
118
|
-
.dwa-subsection-copy { margin: 3px 0 0; color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
|
|
119
|
-
.dwa-form-row { display: flex; gap: var(--ntr-gap-3); margin-bottom: 14px; align-items: flex-start; }
|
|
145
|
+
.dwa-agent-body { padding: 0 0 24px 52px; }
|
|
146
|
+
.dwa-subsection { min-width: 0; }
|
|
147
|
+
.dwa-form-row { display: flex; gap: var(--ntr-gap-3); margin-bottom: 14px; align-items: stretch; }
|
|
120
148
|
.dwa-form-col { flex: 1; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
|
121
149
|
.dwa-form-label { font-size: var(--ntr-font-sm); font-weight: 600; color: var(--dsw-alias-label-primary, #374151); }
|
|
122
150
|
.dwa-hint { font-size: var(--ntr-font-xs); line-height: 1.55; color: var(--ntr-muted); }
|
|
@@ -125,11 +153,13 @@ const STYLE = `
|
|
|
125
153
|
.dwa-select { min-height: 32px; font: inherit; font-size: var(--ntr-font-sm); padding: 5px 8px; border: 1px solid var(--dsw-alias-border-l2, #d1d5db); border-radius: var(--ntr-radius-sm); background: var(--dsw-alias-bg-layer-1, #fff); color: inherit; }
|
|
126
154
|
.dwa-input:hover, .dwa-select:hover { border-color: var(--dsw-alias-border-l1, #9ca3af); }
|
|
127
155
|
.dwa-input:focus, .dwa-select:focus { border-color: var(--ntr-accent); }
|
|
128
|
-
.dwa-input-mono, .dwa-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
|
|
156
|
+
.dwa-input-mono, .dwa-mono { font-family: var(--ds-font-family-code, ui-monospace), SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
|
|
129
157
|
.dwa-stderr { max-height: 120px; overflow: auto; font-family: monospace; font-size: var(--ntr-font-xs); white-space: pre-wrap; color: var(--ntr-warn); margin: 12px 0 0; }
|
|
130
158
|
.dwa-default-hint { background: var(--dsw-alias-bg-layer-2, #f9fafb); border: var(--ntr-line); border-radius: var(--ntr-radius-sm); padding: 10px 12px; font-size: var(--ntr-font-sm); display: flex; align-items: center; justify-content: space-between; margin: 12px 0 14px; gap: 12px; }
|
|
131
159
|
.dwa-form-actions { display: flex; align-items: center; gap: 8px; margin-top: 16px; padding-top: 14px; border-top: var(--ntr-line); flex-wrap: wrap; }
|
|
132
160
|
.dwa-form-actions .dwa-btn-primary { min-width: 88px; }
|
|
161
|
+
.dwa-form-actions > .dwa-hint:first-child { margin-right: auto; }
|
|
162
|
+
.dwa-form-actions > .dwa-hint:last-child:not(:first-child) { width: 100%; }
|
|
133
163
|
.dwa-menu-wrap { position: relative; }
|
|
134
164
|
.dwa-menu-wrap > .dwa-btn { width: 32px; padding: 0; }
|
|
135
165
|
.dwa-menu { position: absolute; right: 0; top: calc(100% + 4px); z-index: 20; min-width: 190px; border: var(--ntr-line); border-radius: var(--ntr-radius-sm); background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); padding: 4px; display: flex; flex-direction: column; }
|
|
@@ -137,10 +167,10 @@ const STYLE = `
|
|
|
137
167
|
.dwa-menu-item:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); }
|
|
138
168
|
.dwa-menu-item-danger { color: var(--ntr-danger); }
|
|
139
169
|
.dwa-menu-confirm { display: flex; align-items: center; gap: 4px; padding: 4px; }
|
|
140
|
-
.dwa-add-panel
|
|
141
|
-
.dwa-add-
|
|
142
|
-
.dwa-add-
|
|
143
|
-
.dwa-add-content {
|
|
170
|
+
.dwa-add-panel .dwa-agent-row { display: none; }
|
|
171
|
+
.dwa-add-panel .dwa-agent-body { padding: 0 0 24px; }
|
|
172
|
+
.dwa-add-panel .dwa-agent-card { border: 0; }
|
|
173
|
+
.dwa-add-content { padding-top: 10px; }
|
|
144
174
|
.dwa-chip { min-height: 30px; font: inherit; font-size: var(--ntr-font-sm); border: var(--ntr-line); border-radius: 999px; padding: 4px 12px; background: var(--dsw-alias-bg-layer-1, #fff); cursor: pointer; color: inherit; }
|
|
145
175
|
.dwa-chip:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); }
|
|
146
176
|
.dwa-chip-active { border-color: var(--ntr-accent); background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #2563eb) 8%, transparent); color: var(--dsw-alias-state-business-primary, #2563eb); font-weight: 550; }
|
|
@@ -172,13 +202,13 @@ const STYLE = `
|
|
|
172
202
|
.dwa-model-status { margin-left: auto; white-space: nowrap; }
|
|
173
203
|
.dwa-model-search { width: 200px; min-height: 30px; }
|
|
174
204
|
.dwa-model-empty { padding: 6px; }
|
|
175
|
-
.dwa-model-row { display: flex; align-items: center; gap:
|
|
205
|
+
.dwa-model-row { display: flex; align-items: center; gap: 8px; width: 100%; min-height: 38px; border: none; background: none; padding: 7px 8px; border-radius: 5px; cursor: pointer; color: inherit; font: inherit; font-size: var(--ntr-font-sm); text-align: left; }
|
|
176
206
|
.dwa-model-row:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); }
|
|
177
207
|
.dwa-model-row.is-on { background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #2563eb) 8%, transparent); color: var(--dsw-alias-state-business-primary, #2563eb); }
|
|
178
208
|
.dwa-model-row-mark { width: 12px; flex: none; text-align: center; opacity: 0.75; }
|
|
179
209
|
.dwa-model-row-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
180
210
|
/* Name and credit keep their room; the id is the only cell that collapses. */
|
|
181
|
-
.dwa-model-row-desc { flex: 0 0 auto; max-width: 45%; opacity: 0.75; font-size:
|
|
211
|
+
.dwa-model-row-desc { flex: 0 0 auto; max-width: 45%; opacity: 0.75; font-size: var(--ntr-font-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
182
212
|
/* The trailing 14px is reserved for the fade mask: while the id fits, the
|
|
183
213
|
gradient only covers empty padding; once it overflows, its tail dissolves
|
|
184
214
|
instead of ending in a hard ellipsis. armModelIdMarquee reads the same
|
|
@@ -192,12 +222,12 @@ const STYLE = `
|
|
|
192
222
|
}
|
|
193
223
|
.dwa-model-row-remove { flex: none; opacity: 0.5; padding: 0 2px; }
|
|
194
224
|
.dwa-model-row:hover .dwa-model-row-remove { opacity: 1; }
|
|
195
|
-
.dwa-seg { display: inline-flex; border-radius: var(--ntr-radius-sm); border:
|
|
196
|
-
.dwa-seg > button { min-height:
|
|
197
|
-
.dwa-seg > button + button { border-left: 1px solid var(--dsw-alias-border-l2, #d1d5db); }
|
|
225
|
+
.dwa-seg { display: inline-flex; gap: 4px; padding: 4px; box-sizing: border-box; border-radius: var(--ntr-radius-sm); border: var(--ntr-line); background: var(--dsw-alias-bg-layer-2, #f3f4f6); vertical-align: middle; }
|
|
226
|
+
.dwa-seg > button { min-height: 32px; font: inherit; font-size: var(--ntr-font-sm); color: var(--ntr-muted); border: 1px solid transparent; border-radius: 4px; background: transparent; padding: 4px 12px; cursor: pointer; white-space: nowrap; }
|
|
198
227
|
.dwa-seg > button:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); }
|
|
199
|
-
.dwa-seg > button.is-on { background: var(--ntr-
|
|
200
|
-
.dwa-
|
|
228
|
+
.dwa-seg > button.is-on { background: var(--dsw-alias-bg-layer-1, #fff); border: var(--ntr-line); color: var(--dsw-alias-label-primary, #111827); font-weight: 550; }
|
|
229
|
+
.dwa-preferences { border-top: var(--ntr-line); }
|
|
230
|
+
.dwa-preference-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 20px 0; }
|
|
201
231
|
.dwa-preference-row + .dwa-preference-row { border-top: var(--ntr-line); }
|
|
202
232
|
/* A control that needs the whole card: the label sits above it, not beside
|
|
203
233
|
it. A right-aligned segmented control squeezed its labels into two lines
|
|
@@ -222,13 +252,20 @@ const STYLE = `
|
|
|
222
252
|
.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; }
|
|
223
253
|
.dwa-deadline-reset { display: flex; justify-content: flex-end; margin-top: 8px; }
|
|
224
254
|
@media (max-width: 640px) {
|
|
225
|
-
.dwa-root { padding:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
.dwa-page-
|
|
255
|
+
.dwa-root { padding: 20px 16px; }
|
|
256
|
+
}
|
|
257
|
+
@container (max-width: 640px) {
|
|
258
|
+
.dwa-page-header { align-items: flex-start; flex-wrap: wrap; }
|
|
259
|
+
.dwa-agent-body { padding-left: 0; }
|
|
260
|
+
.dwa-agent-row { grid-template-columns: 32px minmax(0, 1fr) auto; gap: 10px; }
|
|
261
|
+
.dwa-agent-avatar { width: 32px; height: 32px; }
|
|
262
|
+
.dwa-section-heading { flex-wrap: wrap; }
|
|
263
|
+
.dwa-tabs { gap: 20px; }
|
|
264
|
+
.dwa-notice { flex-wrap: wrap; }
|
|
265
|
+
.dwa-seg-wide > button { padding-inline: 5px; }
|
|
229
266
|
.dwa-model-toolbar-actions { width: 100%; }
|
|
230
267
|
.dwa-model-search { flex: 1 1 auto; width: auto; }
|
|
231
|
-
.dwa-
|
|
268
|
+
.dwa-preference-row { align-items: flex-start; flex-direction: column; }
|
|
232
269
|
.dwa-default-hint { align-items: flex-start; flex-direction: column; }
|
|
233
270
|
/* The slider's scale: up to eight rungs with full unit words ("15 分钟")
|
|
234
271
|
need a smaller face before they can collide on a narrow panel. The rung
|
|
@@ -809,7 +846,7 @@ function ApprovalDeadlinesCard(props: { acp: AcpStore; ui: UiState }) {
|
|
|
809
846
|
</p>
|
|
810
847
|
</div>
|
|
811
848
|
</div>
|
|
812
|
-
<div className="dwa-
|
|
849
|
+
<div className="dwa-preferences">
|
|
813
850
|
<DeadlineModeBlock acp={acp} mode={deadlines.mode} override={modeNote} />
|
|
814
851
|
<DeadlineSliderBlock
|
|
815
852
|
acp={acp}
|
|
@@ -829,53 +866,238 @@ function ApprovalDeadlinesCard(props: { acp: AcpStore; ui: UiState }) {
|
|
|
829
866
|
)
|
|
830
867
|
}
|
|
831
868
|
|
|
832
|
-
|
|
869
|
+
type SettingsPage = 'agents' | 'add' | 'approvals' | 'appearance' | 'diagnostics'
|
|
870
|
+
|
|
871
|
+
/** The states that carry an update badge next to the version badge. */
|
|
872
|
+
function updateBadgeText(update: PluginUpdateState, acp: AcpStore): string | undefined {
|
|
873
|
+
if (update.status === 'available') {
|
|
874
|
+
return acp.label(`可更新 v${update.latest ?? update.current}`, `Update v${update.latest ?? update.current}`)
|
|
875
|
+
}
|
|
876
|
+
if (update.status === 'applying') return acp.label('更新中…', 'Updating…')
|
|
877
|
+
if (update.status === 'applied') {
|
|
878
|
+
const installed = update.installed ?? update.latest ?? update.current
|
|
879
|
+
return acp.label(`已更新 v${installed}`, `Updated v${installed}`)
|
|
880
|
+
}
|
|
881
|
+
if (update.status === 'restart-required') {
|
|
882
|
+
// Only a KNOWN disk version can be named. An interrupted install leaves
|
|
883
|
+
// `installed` unset, and a badge would have to invent a version.
|
|
884
|
+
if (update.installed === undefined) return undefined
|
|
885
|
+
// Neutral: the disk version can be newer OR older than the running one.
|
|
886
|
+
return acp.label(`待重启 v${update.installed}`, `Restart for v${update.installed}`)
|
|
887
|
+
}
|
|
888
|
+
return undefined
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* The version badge's update companion. Every badge is a button, `applying`
|
|
893
|
+
* included: the card is where the install's outcome is read, so the running
|
|
894
|
+
* install must be able to open it too. States without anything to act on get
|
|
895
|
+
* no badge at all — the version badge itself still opens the card.
|
|
896
|
+
*/
|
|
897
|
+
function PluginUpdateBadge(props: { acp: AcpStore; update: PluginUpdateState; open: boolean; onToggle: () => void }) {
|
|
898
|
+
const { acp, update, open, onToggle } = props
|
|
899
|
+
const text = updateBadgeText(update, acp)
|
|
900
|
+
if (text === undefined) return null
|
|
901
|
+
return (
|
|
902
|
+
<button
|
|
903
|
+
type="button"
|
|
904
|
+
className="dwa-badge dwa-badge-info dwa-update-badge"
|
|
905
|
+
aria-expanded={open}
|
|
906
|
+
aria-controls="ntr-plugin-update-card"
|
|
907
|
+
onClick={onToggle}
|
|
908
|
+
>
|
|
909
|
+
{text}
|
|
910
|
+
</button>
|
|
911
|
+
)
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* The update card. In-page, no dialog: the settings page already owns the one
|
|
916
|
+
* action (install the exact version the host reported), and a modal on top of
|
|
917
|
+
* a settings section would hide the version it is updating.
|
|
918
|
+
*
|
|
919
|
+
* The copy stays to one line per state (constraint 33); the host's raw error
|
|
920
|
+
* (`message`) is the only long text, and it wraps on its own line so a stack
|
|
921
|
+
* trace or a registry error is readable without stretching the card.
|
|
922
|
+
*/
|
|
923
|
+
function PluginUpdateCard(props: { acp: AcpStore; update: PluginUpdateState; onClose: () => void }) {
|
|
924
|
+
const { acp, update, onClose } = props
|
|
925
|
+
const applying = update.status === 'applying'
|
|
926
|
+
const latest = update.latest ?? update.current
|
|
927
|
+
const onDisk = update.installed !== undefined && update.installed !== update.current
|
|
928
|
+
? update.installed
|
|
929
|
+
: undefined
|
|
930
|
+
const published = update.publishedAt !== undefined && !Number.isNaN(new Date(update.publishedAt).getTime())
|
|
931
|
+
? new Date(update.publishedAt).toLocaleDateString()
|
|
932
|
+
: undefined
|
|
933
|
+
const copy = update.status === 'available'
|
|
934
|
+
? acp.label('更新会卸载并重装插件,之后可能需要重启 DSH。', 'Updating uninstalls and reinstalls the plugin, and may require restarting DSH.')
|
|
935
|
+
: update.status === 'applied'
|
|
936
|
+
? acp.label('已应用,刷新页面即可使用新版本。', 'Applied. Reload the page to use the new version.')
|
|
937
|
+
: update.status === 'restart-required'
|
|
938
|
+
? update.installed !== undefined
|
|
939
|
+
? acp.label(
|
|
940
|
+
`磁盘已是 v${update.installed},重启 DSH 后生效(桌面 App 退出重开;命令行重启 dsh web)。`,
|
|
941
|
+
`On disk v${update.installed}. Restart DSH to apply (quit and reopen the app, or restart dsh web).`,
|
|
942
|
+
)
|
|
943
|
+
: acp.label(
|
|
944
|
+
'更新可能已完成,请刷新页面确认当前版本。',
|
|
945
|
+
'The update may already be complete. Refresh the page to confirm the current version.',
|
|
946
|
+
)
|
|
947
|
+
: update.status === 'busy'
|
|
948
|
+
? acp.label('有进行中的对话,结束后才能更新。', 'Finish the running conversation before updating.')
|
|
949
|
+
: update.status === 'failed'
|
|
950
|
+
? acp.label('更新失败。', 'Update failed.')
|
|
951
|
+
: update.status === 'current'
|
|
952
|
+
? acp.label('已是最新版本。', 'Up to date.')
|
|
953
|
+
: update.status === 'source-install'
|
|
954
|
+
? acp.label(
|
|
955
|
+
'源码或本地安装,无法通过 registry 更新。',
|
|
956
|
+
'Source or local install; update it through its own checkout.',
|
|
957
|
+
)
|
|
958
|
+
: update.status === 'managed'
|
|
959
|
+
? acp.label('此安装由宿主管理,不能在这里更新。', 'This install is managed by the host and cannot be updated here.')
|
|
960
|
+
: undefined
|
|
961
|
+
const showMessage = (update.status === 'failed' || update.status === 'busy' || update.status === 'managed')
|
|
962
|
+
&& update.message !== undefined
|
|
963
|
+
const apply = async (): Promise<void> => {
|
|
964
|
+
if (update.latest === undefined) return
|
|
965
|
+
try {
|
|
966
|
+
await acp.applyPluginUpdate(update.latest)
|
|
967
|
+
} catch {
|
|
968
|
+
// The store keeps the "disk may have changed" state and writes its own notice.
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return (
|
|
972
|
+
<div
|
|
973
|
+
id="ntr-plugin-update-card"
|
|
974
|
+
className="dwa-card dwa-update-card"
|
|
975
|
+
role="group"
|
|
976
|
+
aria-label={acp.label('插件更新', 'Plugin update')}
|
|
977
|
+
>
|
|
978
|
+
<div className="dwa-update-summary">
|
|
979
|
+
<span>{acp.label(`当前 v${update.current}`, `Current v${update.current}`)}</span>
|
|
980
|
+
{onDisk !== undefined && <span className="dwa-hint">{acp.label(`磁盘 v${onDisk}`, `On disk v${onDisk}`)}</span>}
|
|
981
|
+
{update.latest !== undefined && (
|
|
982
|
+
<>
|
|
983
|
+
<span aria-hidden="true">→</span>
|
|
984
|
+
<span>{acp.label(`最新 v${latest}`, `Latest v${latest}`)}</span>
|
|
985
|
+
{published !== undefined && <span className="dwa-hint">{published}</span>}
|
|
986
|
+
</>
|
|
987
|
+
)}
|
|
988
|
+
</div>
|
|
989
|
+
{copy !== undefined && <p className="dwa-update-copy">{copy}</p>}
|
|
990
|
+
{showMessage && <div className="dwa-update-message">{update.message}</div>}
|
|
991
|
+
<div className="dwa-row-actions dwa-update-actions">
|
|
992
|
+
{update.status === 'available' && (
|
|
993
|
+
<button type="button" className="dwa-btn dwa-btn-primary" onClick={() => { void apply() }}>
|
|
994
|
+
{acp.label('更新', 'Update')}
|
|
995
|
+
</button>
|
|
996
|
+
)}
|
|
997
|
+
{applying && (
|
|
998
|
+
<button type="button" className="dwa-btn dwa-btn-primary" disabled>
|
|
999
|
+
{acp.label('正在更新…', 'Updating…')}
|
|
1000
|
+
</button>
|
|
1001
|
+
)}
|
|
1002
|
+
{update.status !== 'applying' && (
|
|
1003
|
+
<button type="button" className="dwa-btn" onClick={onClose}>{acp.label('关闭', 'Close')}</button>
|
|
1004
|
+
)}
|
|
1005
|
+
<button type="button" className="dwa-btn dwa-btn-quiet" disabled={applying} onClick={() => void acp.checkPluginUpdate(true)}>
|
|
1006
|
+
{acp.label('重新检查', 'Check again')}
|
|
1007
|
+
</button>
|
|
1008
|
+
</div>
|
|
1009
|
+
</div>
|
|
1010
|
+
)
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* The card is drawn for every state a host can report about this install,
|
|
1015
|
+
* except the two where there is no install to talk about at all.
|
|
1016
|
+
*/
|
|
1017
|
+
function updateCardVisible(update: PluginUpdateState | undefined, open: boolean): boolean {
|
|
1018
|
+
if (update === undefined || !open) return false
|
|
1019
|
+
return update.status !== 'unsupported' && update.status !== 'not-installed'
|
|
1020
|
+
}
|
|
1021
|
+
|
|
833
1022
|
export function AcpSettingsSection(props: WithStore) {
|
|
834
1023
|
const acp = props.acp
|
|
835
1024
|
ensureStyle()
|
|
836
1025
|
const ui = useUi(acp)
|
|
837
1026
|
const ledger = permissionLedgerCard(ui)
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
// selection until the next create resets it.
|
|
841
|
-
const [addOpen, setAddOpen] = useState(true)
|
|
1027
|
+
const [page, setPage] = useState<SettingsPage>('agents')
|
|
1028
|
+
const [addOpen, setAddOpen] = useState(false)
|
|
842
1029
|
const [addTarget, setAddTarget] = useState<DetectedAgentInfo | undefined>(undefined)
|
|
843
|
-
const [
|
|
1030
|
+
const [updateOpen, setUpdateOpen] = useState(false)
|
|
1031
|
+
// The store throttles this to one registry read per ten minutes, so every
|
|
1032
|
+
// remount (both settings entries mount this section) is cheap.
|
|
1033
|
+
useEffect(() => {
|
|
1034
|
+
void acp.checkPluginUpdate()
|
|
1035
|
+
}, [acp])
|
|
844
1036
|
const { configured, available } = partitionAgents(ui.detectedAgents, ui.agents)
|
|
845
1037
|
const selected = addTarget ?? available[0] ?? NEW_AGENT_SEED
|
|
1038
|
+
const openAdd = (): void => {
|
|
1039
|
+
setAddOpen(true)
|
|
1040
|
+
setPage('add')
|
|
1041
|
+
}
|
|
1042
|
+
const presetProblem = ui.preset !== undefined && (!ui.preset.installed || ui.preset.problem !== undefined)
|
|
1043
|
+
const update = ui.pluginUpdate
|
|
1044
|
+
// A source install cannot be replaced by this browser; the version badge
|
|
1045
|
+
// says so on hover. Neutral wording: a checkout may be git or not.
|
|
1046
|
+
const versionTitle = update?.status === 'source-install'
|
|
1047
|
+
? acp.label('源码或本地安装,无法通过 registry 更新。', 'Source or local install; update it through its own checkout.')
|
|
1048
|
+
: undefined
|
|
1049
|
+
// The running version is the update entry: every state with a real install
|
|
1050
|
+
// report can open the card, including "up to date" (which offers a re-check).
|
|
1051
|
+
const versionOpensUpdate = update !== undefined
|
|
1052
|
+
&& update.status !== 'unsupported'
|
|
1053
|
+
&& update.status !== 'not-installed'
|
|
846
1054
|
|
|
847
1055
|
return (
|
|
848
1056
|
<div className="dwa-root">
|
|
849
1057
|
<header className="dwa-page-header">
|
|
850
1058
|
<div className="dwa-page-heading">
|
|
851
|
-
<h2 className="dwa-page-title"
|
|
1059
|
+
<h2 className="dwa-page-title" aria-label={acp.label('NTR 设置', 'NTR settings')}>
|
|
1060
|
+
{acp.label('NTR 设置', 'NTR settings')}
|
|
1061
|
+
{ui.pluginVersion !== undefined && (
|
|
1062
|
+
versionOpensUpdate
|
|
1063
|
+
? (
|
|
1064
|
+
<button
|
|
1065
|
+
type="button"
|
|
1066
|
+
className="dwa-badge dwa-version dwa-version-btn"
|
|
1067
|
+
title={versionTitle}
|
|
1068
|
+
aria-expanded={updateOpen}
|
|
1069
|
+
aria-controls="ntr-plugin-update-card"
|
|
1070
|
+
onClick={() => setUpdateOpen(open => !open)}
|
|
1071
|
+
>
|
|
1072
|
+
{`v${ui.pluginVersion}`}
|
|
1073
|
+
</button>
|
|
1074
|
+
)
|
|
1075
|
+
: <span className="dwa-badge dwa-version" title={versionTitle}>{`v${ui.pluginVersion}`}</span>
|
|
1076
|
+
)}
|
|
1077
|
+
{update !== undefined && (
|
|
1078
|
+
<PluginUpdateBadge
|
|
1079
|
+
acp={acp}
|
|
1080
|
+
update={update}
|
|
1081
|
+
open={updateOpen}
|
|
1082
|
+
onToggle={() => setUpdateOpen(open => !open)}
|
|
1083
|
+
/>
|
|
1084
|
+
)}
|
|
1085
|
+
</h2>
|
|
852
1086
|
<p className="dwa-page-description">
|
|
853
|
-
{acp.label('
|
|
1087
|
+
{acp.label('连接智能体,设置你的对话偏好。', 'Connect agents and set your conversation preferences.')}
|
|
854
1088
|
</p>
|
|
855
|
-
<div className="dwa-page-status">
|
|
856
|
-
<span
|
|
857
|
-
className="dwa-dot"
|
|
858
|
-
style={{ background: ui.stream === 'live' ? 'var(--dsw-alias-state-success-primary, #22c55e)' : ui.stream === 'down' ? 'var(--dsw-alias-state-error-primary, #ef4444)' : 'var(--dsw-alias-state-idle-primary, #9ca3af)' }}
|
|
859
|
-
title={ui.stream}
|
|
860
|
-
/>
|
|
861
|
-
<span>{streamText(ui.stream, acp)}</span>
|
|
862
|
-
{ui.detectSummary !== undefined && <span>· {ui.detectSummary}</span>}
|
|
863
|
-
</div>
|
|
864
1089
|
</div>
|
|
865
|
-
|
|
866
|
-
<button
|
|
867
|
-
|
|
868
|
-
onClick={() => {
|
|
869
|
-
void acp.resync()
|
|
870
|
-
void acp.detectAgents()
|
|
871
|
-
}}
|
|
872
|
-
disabled={ui.detecting}
|
|
873
|
-
>
|
|
874
|
-
{ui.detecting ? acp.label('检测中...', 'Detecting...') : acp.label('重新检测与刷新', 'Re-detect & Refresh')}
|
|
1090
|
+
{page === 'agents' && configured.length > 0 && (
|
|
1091
|
+
<button type="button" className="dwa-btn dwa-btn-primary" onClick={openAdd}>
|
|
1092
|
+
{acp.label('添加智能体', 'Add agent')}
|
|
875
1093
|
</button>
|
|
876
|
-
|
|
1094
|
+
)}
|
|
877
1095
|
</header>
|
|
878
1096
|
|
|
1097
|
+
{update !== undefined && updateCardVisible(update, updateOpen) && (
|
|
1098
|
+
<PluginUpdateCard acp={acp} update={update} onClose={() => setUpdateOpen(false)} />
|
|
1099
|
+
)}
|
|
1100
|
+
|
|
879
1101
|
{/* The cross-session mirror: the conversation dock answers the asks
|
|
880
1102
|
belonging to the open conversation; this card answers the rest. */}
|
|
881
1103
|
{ledger.visible && (
|
|
@@ -900,14 +1122,15 @@ export function AcpSettingsSection(props: WithStore) {
|
|
|
900
1122
|
{permission.options.map(option => (
|
|
901
1123
|
<button
|
|
902
1124
|
key={option.optionId}
|
|
903
|
-
|
|
1125
|
+
type="button"
|
|
1126
|
+
className="dwa-btn"
|
|
904
1127
|
onClick={() => { void acp.respondPermission(permission.requestId, option.optionId).catch(() => undefined) }}
|
|
905
1128
|
>
|
|
906
1129
|
{option.name}
|
|
907
1130
|
</button>
|
|
908
1131
|
))}
|
|
909
|
-
<button className="dwa-btn" onClick={() => { void acp.respondPermission(permission.requestId).catch(() => undefined) }}>
|
|
910
|
-
{acp.label('
|
|
1132
|
+
<button type="button" className="dwa-btn dwa-btn-quiet" onClick={() => { void acp.respondPermission(permission.requestId).catch(() => undefined) }}>
|
|
1133
|
+
{acp.label('取消', 'Cancel')}
|
|
911
1134
|
</button>
|
|
912
1135
|
</div>
|
|
913
1136
|
))}
|
|
@@ -917,17 +1140,34 @@ export function AcpSettingsSection(props: WithStore) {
|
|
|
917
1140
|
{ui.notice !== undefined && (
|
|
918
1141
|
<div className="dwa-card dwa-card-warn" style={{ marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
919
1142
|
<span style={{ flex: 1 }}>{ui.notice}</span>
|
|
920
|
-
<button className="dwa-btn" onClick={() => acp.clearNotice()}>✕</button>
|
|
1143
|
+
<button type="button" className="dwa-btn dwa-btn-quiet" aria-label={acp.label('关闭通知', 'Dismiss notice')} onClick={() => acp.clearNotice()}>✕</button>
|
|
921
1144
|
</div>
|
|
922
1145
|
)}
|
|
923
1146
|
|
|
924
|
-
{
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
{
|
|
1147
|
+
{presetProblem && (
|
|
1148
|
+
<div className="dwa-card dwa-card-warn dwa-notice" role="status">
|
|
1149
|
+
<span>{acp.label('NTR 模式需要检查,当前可能无法开始对话。', 'NTR mode needs attention; new conversations may be unavailable.')}</span>
|
|
1150
|
+
<button type="button" className="dwa-btn" onClick={() => setPage('diagnostics')}>{acp.label('查看诊断', 'View diagnostics')}</button>
|
|
1151
|
+
</div>
|
|
1152
|
+
)}
|
|
1153
|
+
<nav className="dwa-tabs" aria-label={acp.label('NTR 设置分类', 'NTR settings categories')}>
|
|
1154
|
+
<button type="button" aria-pressed={page === 'agents'} onClick={() => setPage('agents')}>{acp.label('智能体', 'Agents')}</button>
|
|
1155
|
+
{ui.approvalDeadlines !== undefined && <button type="button" aria-pressed={page === 'approvals'} onClick={() => setPage('approvals')}>{acp.label('审批时限', 'Approval deadlines')}</button>}
|
|
1156
|
+
<button type="button" aria-pressed={page === 'appearance'} onClick={() => setPage('appearance')}>{acp.label('对话外观', 'Appearance')}</button>
|
|
1157
|
+
</nav>
|
|
1158
|
+
{page === 'diagnostics' && (
|
|
1159
|
+
<section className="dwa-section">
|
|
1160
|
+
<h3 className="dwa-section-title">{acp.label('连接诊断', 'Connection diagnostics')}</h3>
|
|
1161
|
+
<div className="dwa-preference-row"><span>{acp.label('宿主事件流', 'Host event stream')}</span><span className="dwa-hint">{streamText(ui.stream, acp)}</span></div>
|
|
1162
|
+
<div className="dwa-row-actions">
|
|
1163
|
+
<button type="button" className="dwa-btn" onClick={() => { void acp.resync(); void acp.detectAgents() }} disabled={ui.detecting}>
|
|
1164
|
+
{ui.detecting ? acp.label('检测中...', 'Detecting...') : acp.label('重新检测与刷新', 'Re-detect & Refresh')}
|
|
1165
|
+
</button>
|
|
1166
|
+
{ui.detectSummary !== undefined && <span className="dwa-hint" role="status">{ui.detectSummary}</span>}
|
|
1167
|
+
</div>
|
|
1168
|
+
</section>
|
|
1169
|
+
)}
|
|
1170
|
+
{page === 'diagnostics' && ui.preset !== undefined && (
|
|
931
1171
|
<div className="dwa-card dwa-mode-banner">
|
|
932
1172
|
<div className="dwa-mode-title">
|
|
933
1173
|
{acp.label('NTR 模式', 'NTR mode')}
|
|
@@ -950,24 +1190,24 @@ export function AcpSettingsSection(props: WithStore) {
|
|
|
950
1190
|
</div>
|
|
951
1191
|
)}
|
|
952
1192
|
|
|
953
|
-
{/*
|
|
954
|
-
<section className="dwa-section">
|
|
1193
|
+
{/* Keep editors mounted so navigation preserves unsaved values. */}
|
|
1194
|
+
<section className="dwa-section" hidden={page !== 'agents'} aria-label={acp.label('已接入的智能体', 'Connected agents')}>
|
|
955
1195
|
<div className="dwa-section-heading">
|
|
956
1196
|
<div>
|
|
957
1197
|
<h3 className="dwa-section-title">
|
|
958
1198
|
{acp.label('已接入的智能体', 'Connected agents')}
|
|
959
|
-
<span className="dwa-badge">{configured.length}</span>
|
|
1199
|
+
{ui.agentsLoaded && <span className="dwa-badge">{configured.length}</span>}
|
|
960
1200
|
</h3>
|
|
961
|
-
<p className="dwa-section-copy">{acp.label('展开智能体可管理模型、启动命令与工作区。', 'Expand an agent to manage its models, launch command, and workspace.')}</p>
|
|
962
1201
|
</div>
|
|
1202
|
+
{configured.length > 0 && <span className="dwa-hint">{acp.label('默认项用于新的 NTR 会话', 'The default serves new NTR conversations')}</span>}
|
|
963
1203
|
</div>
|
|
964
1204
|
|
|
965
|
-
{
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
)}
|
|
1205
|
+
{!ui.agentsLoaded && <div className="dwa-empty" role="status">{acp.label('正在读取智能体…', 'Loading agents…')}</div>}
|
|
1206
|
+
{ui.agentsLoaded && configured.length === 0 && (
|
|
1207
|
+
<div className="dwa-empty">
|
|
1208
|
+
<h3>{acp.label('接入第一个智能体', 'Connect your first agent')}</h3>
|
|
1209
|
+
<p className="dwa-section-copy">{acp.label('选择已有预设,或填写自己的 ACP 启动命令。', 'Choose a preset or enter your own ACP launch command.')}</p>
|
|
1210
|
+
<button type="button" className="dwa-btn dwa-btn-primary" onClick={openAdd}>{acp.label('添加智能体', 'Add agent')}</button>
|
|
971
1211
|
</div>
|
|
972
1212
|
)}
|
|
973
1213
|
|
|
@@ -982,24 +1222,22 @@ export function AcpSettingsSection(props: WithStore) {
|
|
|
982
1222
|
acp={acp}
|
|
983
1223
|
isDefault={ui.defaultAgent === info.id}
|
|
984
1224
|
synthesized={synthesized}
|
|
1225
|
+
active={page === 'agents'}
|
|
985
1226
|
/>
|
|
986
1227
|
)
|
|
987
1228
|
})}
|
|
988
1229
|
</div>
|
|
989
1230
|
</section>
|
|
990
1231
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
click. Selection follows the first available preset until pinned. */}
|
|
994
|
-
<section className="dwa-section">
|
|
1232
|
+
<section className="dwa-section" hidden={page !== 'add'} aria-label={acp.label('添加智能体', 'Add agent')}>
|
|
1233
|
+
<button type="button" className="dwa-btn dwa-btn-quiet dwa-back" onClick={() => setPage('agents')}>{acp.label('返回智能体列表', 'Back to agents')}</button>
|
|
995
1234
|
<div className="dwa-add-panel">
|
|
996
|
-
<
|
|
997
|
-
<
|
|
998
|
-
<
|
|
999
|
-
<
|
|
1000
|
-
</
|
|
1001
|
-
|
|
1002
|
-
</button>
|
|
1235
|
+
<div className="dwa-section-heading">
|
|
1236
|
+
<div>
|
|
1237
|
+
<h3 className="dwa-section-title">{acp.label('添加智能体', 'Add agent')}</h3>
|
|
1238
|
+
<p className="dwa-section-copy">{acp.label('从预设快速启用,或创建自定义连接。', 'Enable a preset or create a custom connection.')}</p>
|
|
1239
|
+
</div>
|
|
1240
|
+
</div>
|
|
1003
1241
|
{addOpen && (
|
|
1004
1242
|
<div className="dwa-add-content">
|
|
1005
1243
|
<div className="dwa-chip-list">
|
|
@@ -1028,19 +1266,17 @@ export function AcpSettingsSection(props: WithStore) {
|
|
|
1028
1266
|
</div>
|
|
1029
1267
|
)}
|
|
1030
1268
|
<AgentRow
|
|
1031
|
-
key={selected.id
|
|
1269
|
+
key={selected.id}
|
|
1032
1270
|
detected={selected}
|
|
1033
1271
|
acp={acp}
|
|
1034
1272
|
isDefault={false}
|
|
1035
1273
|
synthesized={false}
|
|
1036
1274
|
newCustom={selected.id === ''}
|
|
1275
|
+
active={page === 'add'}
|
|
1037
1276
|
onCreated={() => {
|
|
1038
|
-
// Creation done: drop the pin so the selection re-derives to
|
|
1039
|
-
// the next available preset (or a fresh custom card), and bump
|
|
1040
|
-
// the nonce so an all-custom reset still remounts empty
|
|
1041
|
-
// instead of keeping the just-saved values in the form.
|
|
1042
1277
|
setAddTarget(undefined)
|
|
1043
|
-
|
|
1278
|
+
setAddOpen(false)
|
|
1279
|
+
setPage('agents')
|
|
1044
1280
|
}}
|
|
1045
1281
|
/>
|
|
1046
1282
|
</div>
|
|
@@ -1048,24 +1284,16 @@ export function AcpSettingsSection(props: WithStore) {
|
|
|
1048
1284
|
</div>
|
|
1049
1285
|
</section>
|
|
1050
1286
|
|
|
1051
|
-
{
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
<ApprovalDeadlinesCard acp={acp} ui={ui} />
|
|
1055
|
-
|
|
1056
|
-
{/* Agent 挂牌光晕: how the composer tag shows a live agent. Plugin
|
|
1057
|
-
display preference — host-persisted with the agent registry, so it
|
|
1058
|
-
follows the profile rather than this one browser. Kept at the BOTTOM
|
|
1059
|
-
of the section: the cards above answer "what serves my turns", this
|
|
1060
|
-
one only decorates their nameplate. */}
|
|
1061
|
-
<section className="dwa-section">
|
|
1287
|
+
<div hidden={page !== 'approvals'}><ApprovalDeadlinesCard acp={acp} ui={ui} /></div>
|
|
1288
|
+
|
|
1289
|
+
<section className="dwa-section" hidden={page !== 'appearance'} aria-label={acp.label('对话外观', 'Appearance')}>
|
|
1062
1290
|
<div className="dwa-section-heading">
|
|
1063
1291
|
<div>
|
|
1064
1292
|
<h3 className="dwa-section-title">{acp.label('对话外观', 'Conversation appearance')}</h3>
|
|
1065
1293
|
<p className="dwa-section-copy">{acp.label('设置对话中 NTR 挂牌的进程存活提示。', 'Choose how the NTR tag indicates a running agent process.')}</p>
|
|
1066
1294
|
</div>
|
|
1067
1295
|
</div>
|
|
1068
|
-
<div className="dwa-
|
|
1296
|
+
<div className="dwa-preferences">
|
|
1069
1297
|
<div className="dwa-preference-row">
|
|
1070
1298
|
<div>
|
|
1071
1299
|
<div className="dwa-preference-label">{acp.label('挂牌光晕', 'Tag halo')}</div>
|
|
@@ -1106,6 +1334,14 @@ export function AcpSettingsSection(props: WithStore) {
|
|
|
1106
1334
|
)}
|
|
1107
1335
|
</div>
|
|
1108
1336
|
</section>
|
|
1337
|
+
<footer className="dwa-health">
|
|
1338
|
+
<div className="dwa-health-status">
|
|
1339
|
+
<span className="dwa-dot" style={{ background: ui.stream === 'live' ? 'var(--ntr-success)' : ui.stream === 'down' ? 'var(--ntr-danger)' : 'var(--ntr-muted)' }} />
|
|
1340
|
+
<span>{streamText(ui.stream, acp)}</span>
|
|
1341
|
+
{ui.preset !== undefined && <span>{ui.preset.installed && !presetProblem ? acp.label('NTR 模式已就绪', 'NTR mode ready') : acp.label('NTR 模式需检查', 'NTR mode needs attention')}</span>}
|
|
1342
|
+
</div>
|
|
1343
|
+
{page !== 'diagnostics' && <button type="button" className="dwa-btn dwa-btn-quiet" onClick={() => setPage('diagnostics')}>{acp.label('查看诊断', 'View diagnostics')}</button>}
|
|
1344
|
+
</footer>
|
|
1109
1345
|
</div>
|
|
1110
1346
|
)
|
|
1111
1347
|
}
|
|
@@ -1117,6 +1353,7 @@ function AgentRow({
|
|
|
1117
1353
|
acp,
|
|
1118
1354
|
isDefault,
|
|
1119
1355
|
synthesized,
|
|
1356
|
+
active,
|
|
1120
1357
|
newCustom = false,
|
|
1121
1358
|
onCreated,
|
|
1122
1359
|
}: {
|
|
@@ -1125,6 +1362,7 @@ function AgentRow({
|
|
|
1125
1362
|
acp: AcpStore
|
|
1126
1363
|
isDefault: boolean
|
|
1127
1364
|
synthesized: boolean
|
|
1365
|
+
active: boolean
|
|
1128
1366
|
/** From-scratch creation: the id is editable and no preset defaults exist. */
|
|
1129
1367
|
newCustom?: boolean
|
|
1130
1368
|
/** Fired after a creation-card save succeeds (the list re-derives its selection). */
|
|
@@ -1135,6 +1373,7 @@ function AgentRow({
|
|
|
1135
1373
|
// A configured agent starts collapsed; a creation card opens straight away
|
|
1136
1374
|
// (nothing to summarise yet).
|
|
1137
1375
|
const [expanded, setExpanded] = useState(!configured)
|
|
1376
|
+
const [detailPage, setDetailPage] = useState<'models' | 'connection' | 'advanced'>(configured ? 'models' : 'connection')
|
|
1138
1377
|
const [form, setForm] = useState<AgentForm>(() => initialForm(detected, activeAgent))
|
|
1139
1378
|
const [saving, setSaving] = useState(false)
|
|
1140
1379
|
const [savedSuccess, setSavedSuccess] = useState(false)
|
|
@@ -1169,7 +1408,8 @@ function AgentRow({
|
|
|
1169
1408
|
|
|
1170
1409
|
const payload = buildPayload(form, detected, newCustom)
|
|
1171
1410
|
const current = persistedDef(detected, activeAgent)
|
|
1172
|
-
const
|
|
1411
|
+
const clearingIdleOverride = payload.idleTimeoutMs === undefined && detected.currentConfig?.idleTimeoutMs !== undefined
|
|
1412
|
+
const dirty = current !== undefined && (!payloadEquals(payload, current) || clearingIdleOverride)
|
|
1173
1413
|
// A save stops the child and kills every live session (see BridgeManager.saveAgent).
|
|
1174
1414
|
const showSaveWarning = dirty && activeAgent !== undefined && activeAgent.state !== 'stopped'
|
|
1175
1415
|
|
|
@@ -1221,7 +1461,7 @@ function AgentRow({
|
|
|
1221
1461
|
: (activeAgent?.title ?? detected.currentConfig?.title ?? detected.name)
|
|
1222
1462
|
const stderr = activeAgent?.state === 'error' ? (activeAgent.stderrTail ?? []) : []
|
|
1223
1463
|
const errorSummary = stderr.length > 0 ? stderr[stderr.length - 1] : undefined
|
|
1224
|
-
const
|
|
1464
|
+
const selectedModelCount = form.exposed?.length ?? 0
|
|
1225
1465
|
const isPreset = PRESET_IDS.has(detected.id)
|
|
1226
1466
|
// An error row opens itself so the stderr tail is never behind a click.
|
|
1227
1467
|
const open = expanded || (activeAgent?.state === 'error' && configured)
|
|
@@ -1229,19 +1469,11 @@ function AgentRow({
|
|
|
1229
1469
|
return (
|
|
1230
1470
|
<div className={`dwa-agent-card${open ? ' is-open' : ''}`}>
|
|
1231
1471
|
<div className="dwa-agent-row">
|
|
1232
|
-
<
|
|
1233
|
-
type="button"
|
|
1234
|
-
className="dwa-caret-btn"
|
|
1235
|
-
aria-expanded={open}
|
|
1236
|
-
onClick={() => setExpanded(value => !value)}
|
|
1237
|
-
title={open ? acp.label('收起', 'Collapse') : acp.label('展开配置与模型', 'Expand configuration and models')}
|
|
1238
|
-
>
|
|
1239
|
-
<span className="dwa-caret" aria-hidden>{open ? '▾' : '▸'}</span>
|
|
1240
|
-
</button>
|
|
1472
|
+
<span className="dwa-agent-avatar" aria-hidden>{displayName.slice(0, 2).toUpperCase()}</span>
|
|
1241
1473
|
<div className="dwa-agent-summary">
|
|
1242
1474
|
<div className="dwa-agent-titleline">
|
|
1243
1475
|
<span className="dwa-agent-name" title={detected.executablePath ?? detected.description}>{displayName}</span>
|
|
1244
|
-
{isDefault && <span className="dwa-badge dwa-badge-
|
|
1476
|
+
{isDefault && <span className="dwa-badge dwa-badge-info">{acp.label('默认', 'Default')}</span>}
|
|
1245
1477
|
{!isPreset && !newCustom && <span className="dwa-badge">{acp.label('自定义', 'Custom')}</span>}
|
|
1246
1478
|
</div>
|
|
1247
1479
|
<div className="dwa-agent-meta">
|
|
@@ -1254,7 +1486,7 @@ function AgentRow({
|
|
|
1254
1486
|
{configured && (
|
|
1255
1487
|
<>
|
|
1256
1488
|
{state !== undefined && <span className="dwa-agent-meta-separator">·</span>}
|
|
1257
|
-
<span>{
|
|
1489
|
+
<span>{acp.label(`${String(selectedModelCount)} 个已选模型`, `${String(selectedModelCount)} selected models`)}</span>
|
|
1258
1490
|
</>
|
|
1259
1491
|
)}
|
|
1260
1492
|
{!synthesized && !newCustom && !detected.detected && (
|
|
@@ -1267,7 +1499,12 @@ function AgentRow({
|
|
|
1267
1499
|
<div className="dwa-error dwa-mono" style={{ marginTop: 6, wordBreak: 'break-all' }}>{errorSummary}</div>
|
|
1268
1500
|
)}
|
|
1269
1501
|
</div>
|
|
1270
|
-
<div
|
|
1502
|
+
<div className="dwa-row-actions">
|
|
1503
|
+
{configured && (
|
|
1504
|
+
<button type="button" className="dwa-btn dwa-btn-quiet" aria-expanded={open} aria-label={acp.label(`${open ? '收起' : '管理'} ${displayName}`, `${open ? 'Collapse' : 'Manage'} ${displayName}`)} onClick={() => setExpanded(value => !value)}>
|
|
1505
|
+
{open ? acp.label('收起', 'Collapse') : acp.label('管理', 'Manage')}
|
|
1506
|
+
</button>
|
|
1507
|
+
)}
|
|
1271
1508
|
{activeAgent?.state === 'error' && (
|
|
1272
1509
|
<button className="dwa-btn" disabled={testing} onClick={() => void handleTest()}>
|
|
1273
1510
|
{testing ? acp.label('测试中...', 'Testing...') : acp.label('测试', 'Test')}
|
|
@@ -1290,19 +1527,18 @@ function AgentRow({
|
|
|
1290
1527
|
survives collapsing the row. */}
|
|
1291
1528
|
<div className="dwa-agent-body" style={open ? undefined : { display: 'none' }}>
|
|
1292
1529
|
{stderr.length > 0 && <pre className="dwa-stderr">{stderr.join('\n')}</pre>}
|
|
1530
|
+
<nav className="dwa-tabs dwa-detail-tabs" aria-label={acp.label(`${displayName} 配置`, `${displayName} settings`)}>
|
|
1531
|
+
{configured && <button type="button" aria-pressed={detailPage === 'models'} onClick={() => setDetailPage('models')}>{acp.label('模型', 'Models')}</button>}
|
|
1532
|
+
<button type="button" aria-pressed={detailPage === 'connection'} onClick={() => setDetailPage('connection')}>{acp.label('连接', 'Connection')}</button>
|
|
1533
|
+
<button type="button" aria-pressed={detailPage === 'advanced'} onClick={() => setDetailPage('advanced')}>{acp.label('高级', 'Advanced')}</button>
|
|
1534
|
+
</nav>
|
|
1293
1535
|
{configured && (
|
|
1294
|
-
<section className="dwa-subsection">
|
|
1295
|
-
<div className="dwa-subsection-head">
|
|
1296
|
-
<div>
|
|
1297
|
-
<h4 className="dwa-subsection-title">{acp.label('模型与默认值', 'Models & defaults')}</h4>
|
|
1298
|
-
<p className="dwa-subsection-copy">{acp.label('检测模型目录,并控制对话中可选择的模型。', 'Detect the model catalog and choose what appears in conversations.')}</p>
|
|
1299
|
-
</div>
|
|
1300
|
-
</div>
|
|
1536
|
+
<section className="dwa-subsection" hidden={detailPage !== 'models'}>
|
|
1301
1537
|
<ModelCatalogBlock
|
|
1302
1538
|
agentId={detected.id}
|
|
1303
1539
|
detected={detected}
|
|
1304
1540
|
acp={acp}
|
|
1305
|
-
active={open}
|
|
1541
|
+
active={active && open && detailPage === 'models'}
|
|
1306
1542
|
exposed={form.exposed}
|
|
1307
1543
|
onExposedChange={exposed => patch({ exposed })}
|
|
1308
1544
|
autoDiscover={form.autoDiscoverModels}
|
|
@@ -1311,15 +1547,11 @@ function AgentRow({
|
|
|
1311
1547
|
/>
|
|
1312
1548
|
</section>
|
|
1313
1549
|
)}
|
|
1314
|
-
<section className="dwa-subsection">
|
|
1315
|
-
<div className="dwa-subsection-head">
|
|
1316
|
-
<div>
|
|
1317
|
-
<h4 className="dwa-subsection-title">{acp.label('连接配置', 'Connection')}</h4>
|
|
1318
|
-
<p className="dwa-subsection-copy">{acp.label('设置显示名与启动命令,高级选项按需展开。', 'Set the display name and launch command; expand advanced options only when needed.')}</p>
|
|
1319
|
-
</div>
|
|
1320
|
-
</div>
|
|
1550
|
+
<section className="dwa-subsection" hidden={detailPage === 'models'}>
|
|
1321
1551
|
<AgentDetails
|
|
1322
1552
|
acp={acp}
|
|
1553
|
+
advancedOpen={detailPage === 'advanced'}
|
|
1554
|
+
dirty={dirty}
|
|
1323
1555
|
detected={detected}
|
|
1324
1556
|
form={form}
|
|
1325
1557
|
patch={patch}
|
|
@@ -1502,6 +1734,8 @@ export function buildPayload(form: AgentForm, detected: DetectedAgentInfo, newCu
|
|
|
1502
1734
|
/** The expanded body: connection form, workspace allow-list, env, actions. */
|
|
1503
1735
|
function AgentDetails({
|
|
1504
1736
|
acp,
|
|
1737
|
+
advancedOpen,
|
|
1738
|
+
dirty,
|
|
1505
1739
|
detected,
|
|
1506
1740
|
form,
|
|
1507
1741
|
patch,
|
|
@@ -1524,6 +1758,8 @@ function AgentDetails({
|
|
|
1524
1758
|
onTest,
|
|
1525
1759
|
}: {
|
|
1526
1760
|
acp: AcpStore
|
|
1761
|
+
advancedOpen: boolean
|
|
1762
|
+
dirty: boolean
|
|
1527
1763
|
detected: DetectedAgentInfo
|
|
1528
1764
|
form: AgentForm
|
|
1529
1765
|
patch: (next: Partial<AgentForm>) => void
|
|
@@ -1545,8 +1781,6 @@ function AgentDetails({
|
|
|
1545
1781
|
onSave: () => void
|
|
1546
1782
|
onTest: () => void
|
|
1547
1783
|
}) {
|
|
1548
|
-
const ui = useUi(acp)
|
|
1549
|
-
const [advancedOpen, setAdvancedOpen] = useState(false)
|
|
1550
1784
|
// The same parse the host will run (`validateAgentDef` → `splitCommandLine`).
|
|
1551
1785
|
// Showing it is what turns "the first box swallowed my arguments" into a
|
|
1552
1786
|
// visible, fixable state instead of a spawn error.
|
|
@@ -1557,15 +1791,14 @@ function AgentDetails({
|
|
|
1557
1791
|
+ (parsedCommand.args.length === 0 ? '' : ` ${acp.label('参数', 'Arguments')}: ${parsedCommand.args.join(' ')}`)
|
|
1558
1792
|
return (
|
|
1559
1793
|
<div>
|
|
1560
|
-
{
|
|
1561
|
-
filled command+args, so a plain "enable this agent" flow needs
|
|
1562
|
-
nothing below. Everything else lives under 高级参数. */}
|
|
1794
|
+
<div hidden={advancedOpen}>
|
|
1563
1795
|
{newCustom && (
|
|
1564
1796
|
<div className="dwa-form-row">
|
|
1565
1797
|
<div className="dwa-form-col">
|
|
1566
1798
|
<label className="dwa-form-label">{acp.label('Agent ID(唯一标识,创建后不可改)', 'Agent ID (unique, immutable after creation)')}</label>
|
|
1567
1799
|
<input
|
|
1568
1800
|
className="dwa-input dwa-input-mono"
|
|
1801
|
+
aria-label={acp.label('Agent ID', 'Agent ID')}
|
|
1569
1802
|
value={form.agentId}
|
|
1570
1803
|
onChange={e => patch({ agentId: e.target.value })}
|
|
1571
1804
|
placeholder="e.g. my-agent"
|
|
@@ -1586,6 +1819,7 @@ function AgentDetails({
|
|
|
1586
1819
|
<label className="dwa-form-label">{acp.label('显示名', 'Display Name')}</label>
|
|
1587
1820
|
<input
|
|
1588
1821
|
className="dwa-input"
|
|
1822
|
+
aria-label={acp.label('显示名', 'Display Name')}
|
|
1589
1823
|
value={form.title}
|
|
1590
1824
|
onChange={e => patch({ title: e.target.value })}
|
|
1591
1825
|
placeholder={detected.name === '' ? acp.label('我的 Agent', 'My Agent') : detected.name}
|
|
@@ -1604,6 +1838,7 @@ function AgentDetails({
|
|
|
1604
1838
|
<label className="dwa-form-label">{acp.label('启动命令(程序 + 参数)', 'Launch Command (program + arguments)')}</label>
|
|
1605
1839
|
<input
|
|
1606
1840
|
className="dwa-input dwa-input-mono"
|
|
1841
|
+
aria-label={acp.label('启动命令(程序 + 参数)', 'Launch Command (program + arguments)')}
|
|
1607
1842
|
value={form.commandLine}
|
|
1608
1843
|
onChange={e => patch({ commandLine: e.target.value })}
|
|
1609
1844
|
placeholder={detected.defaultCommand === ''
|
|
@@ -1613,39 +1848,17 @@ function AgentDetails({
|
|
|
1613
1848
|
<span className="dwa-hint">{commandPreview}</span>
|
|
1614
1849
|
<span className="dwa-hint">
|
|
1615
1850
|
{acp.label(
|
|
1616
|
-
'
|
|
1617
|
-
'
|
|
1851
|
+
'含空格的程序路径请加引号。',
|
|
1852
|
+
'Quote the program path if it contains spaces.',
|
|
1618
1853
|
)}
|
|
1619
1854
|
</span>
|
|
1620
1855
|
</div>
|
|
1621
1856
|
</div>
|
|
1622
1857
|
|
|
1623
|
-
{
|
|
1624
|
-
unmounted) so an edit in progress survives the toggle. Path validity
|
|
1625
|
-
is surfaced on the toggle itself — a collapsed section must not hide
|
|
1626
|
-
an error that blocks saving. */}
|
|
1627
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 12, flexWrap: 'wrap' }}>
|
|
1628
|
-
<button
|
|
1629
|
-
type="button"
|
|
1630
|
-
className="dwa-btn dwa-btn-secondary"
|
|
1631
|
-
aria-expanded={advancedOpen}
|
|
1632
|
-
onClick={() => setAdvancedOpen(value => !value)}
|
|
1633
|
-
>
|
|
1634
|
-
<span className="dwa-caret" aria-hidden>{advancedOpen ? '▾' : '▸'}</span> {acp.label('高级参数', 'Advanced Parameters')}
|
|
1635
|
-
</button>
|
|
1636
|
-
{!advancedOpen && (cwdInvalid || rootsInvalid) && (
|
|
1637
|
-
<span className="dwa-error">
|
|
1638
|
-
{acp.label('里面有非法的工作空间路径', 'Contains an invalid workspace path')}
|
|
1639
|
-
</span>
|
|
1640
|
-
)}
|
|
1641
|
-
{!advancedOpen && mcpInvalid && (
|
|
1642
|
-
<span className="dwa-error">
|
|
1643
|
-
{acp.label('里面的 MCP 服务器配置不是合法 JSON', 'Contains an invalid MCP server list')}
|
|
1644
|
-
</span>
|
|
1645
|
-
)}
|
|
1858
|
+
<WorkspaceBlock acp={acp} form={form} patch={patch} cwdInvalid={cwdInvalid} rootsInvalid={rootsInvalid} />
|
|
1646
1859
|
</div>
|
|
1647
1860
|
|
|
1648
|
-
<div
|
|
1861
|
+
<div hidden={!advancedOpen}>
|
|
1649
1862
|
{!newCustom && (
|
|
1650
1863
|
<div className="dwa-default-hint">
|
|
1651
1864
|
<div>
|
|
@@ -1679,14 +1892,6 @@ function AgentDetails({
|
|
|
1679
1892
|
</div>
|
|
1680
1893
|
</div>
|
|
1681
1894
|
|
|
1682
|
-
<WorkspaceBlock
|
|
1683
|
-
acp={acp}
|
|
1684
|
-
form={form}
|
|
1685
|
-
patch={patch}
|
|
1686
|
-
cwdInvalid={cwdInvalid}
|
|
1687
|
-
rootsInvalid={rootsInvalid}
|
|
1688
|
-
/>
|
|
1689
|
-
|
|
1690
1895
|
{/* Idle release: scoped to agents with NO live session (see
|
|
1691
1896
|
AgentBridge.releaseIfIdle), so it reclaims the processes that the
|
|
1692
1897
|
model probe / post-save probe leave behind without ever costing a
|
|
@@ -1699,6 +1904,7 @@ function AgentDetails({
|
|
|
1699
1904
|
<label className="dwa-form-label">{acp.label('空闲释放(毫秒)', 'Idle Release (ms)')}</label>
|
|
1700
1905
|
<input
|
|
1701
1906
|
className="dwa-input dwa-input-mono"
|
|
1907
|
+
aria-label={acp.label('空闲释放(毫秒)', 'Idle Release (ms)')}
|
|
1702
1908
|
value={form.idleStr}
|
|
1703
1909
|
onChange={e => patch({ idleStr: e.target.value })}
|
|
1704
1910
|
placeholder={acp.label(
|
|
@@ -1715,18 +1921,8 @@ function AgentDetails({
|
|
|
1715
1921
|
: (
|
|
1716
1922
|
<>
|
|
1717
1923
|
{acp.label(
|
|
1718
|
-
'
|
|
1719
|
-
'
|
|
1720
|
-
)}
|
|
1721
|
-
<br />
|
|
1722
|
-
{acp.label(
|
|
1723
|
-
'0 = 永不释放;回合在跑、取消未结算或审批待答时不释放。',
|
|
1724
|
-
'0 = never; a process with a running turn, an unsettled cancel, or a pending approval is never released.',
|
|
1725
|
-
)}
|
|
1726
|
-
<br />
|
|
1727
|
-
{acp.label(
|
|
1728
|
-
'只对承载过会话的进程生效;探测进程固定 5 分钟释放,填 0 也拦不住。',
|
|
1729
|
-
'This window applies only to a process that has hosted a session; the model-probe process is always released after 5 minutes, and 0 does not stop it.',
|
|
1924
|
+
'承载过会话的进程空闲后挂起;0 不释放,探测进程固定 5 分钟。',
|
|
1925
|
+
'Park idle chat processes; 0 keeps them alive. Probe processes always release after 5 minutes.',
|
|
1730
1926
|
)}
|
|
1731
1927
|
</>
|
|
1732
1928
|
)}
|
|
@@ -1783,6 +1979,7 @@ function AgentDetails({
|
|
|
1783
1979
|
<textarea
|
|
1784
1980
|
className="dwa-input dwa-input-mono"
|
|
1785
1981
|
style={{ minHeight: 96, resize: 'vertical' }}
|
|
1982
|
+
aria-label={acp.label('MCP 服务器(JSON)', 'MCP Servers (JSON)')}
|
|
1786
1983
|
value={form.mcpText}
|
|
1787
1984
|
onChange={e => patch({ mcpText: e.target.value })}
|
|
1788
1985
|
spellCheck={false}
|
|
@@ -1795,14 +1992,17 @@ function AgentDetails({
|
|
|
1795
1992
|
)}
|
|
1796
1993
|
<span className="dwa-hint">
|
|
1797
1994
|
{acp.label(
|
|
1798
|
-
'
|
|
1799
|
-
'
|
|
1995
|
+
'额外追加给智能体的 MCP 服务器,不影响其已有配置。',
|
|
1996
|
+
'Additional MCP servers for the agent; its own configuration is unchanged.',
|
|
1800
1997
|
)}
|
|
1801
1998
|
</span>
|
|
1802
1999
|
</div>
|
|
1803
2000
|
</div>
|
|
1804
2001
|
|
|
2002
|
+
{!advancedOpen && (mcpInvalid || idleInvalid) && <p className="dwa-error" role="alert">{acp.label('请检查「高级」中的无效配置。', 'Check the invalid settings in Advanced.')}</p>}
|
|
2003
|
+
{advancedOpen && (!idValid || idTaken || cwdInvalid || rootsInvalid || parsedCommand.command === '') && <p className="dwa-error" role="alert">{acp.label('请检查「连接」中的未完成或无效配置。', 'Check the incomplete or invalid settings in Connection.')}</p>}
|
|
1805
2004
|
<div className="dwa-form-actions">
|
|
2005
|
+
<span className="dwa-hint">{isConfigured ? dirty ? acp.label('有未保存的更改', 'Unsaved changes') : acp.label('配置已保存', 'Settings saved') : ''}</span>
|
|
1806
2006
|
<button
|
|
1807
2007
|
className="dwa-btn"
|
|
1808
2008
|
onClick={onTest}
|
|
@@ -1815,14 +2015,14 @@ function AgentDetails({
|
|
|
1815
2015
|
<button
|
|
1816
2016
|
className="dwa-btn dwa-btn-primary"
|
|
1817
2017
|
onClick={onSave}
|
|
1818
|
-
disabled={saving || testing || formIncomplete}
|
|
2018
|
+
disabled={saving || testing || formIncomplete || (isConfigured && !dirty)}
|
|
1819
2019
|
>
|
|
1820
2020
|
{saving
|
|
1821
2021
|
? acp.label('保存中...', 'Saving...')
|
|
1822
2022
|
: savedSuccess
|
|
1823
2023
|
? acp.label('✓ 已保存', '✓ Saved')
|
|
1824
2024
|
: isConfigured
|
|
1825
|
-
? acp.label('
|
|
2025
|
+
? acp.label('保存更改', 'Save changes')
|
|
1826
2026
|
: acp.label('保存并启用', 'Save & Enable')}
|
|
1827
2027
|
</button>
|
|
1828
2028
|
|
|
@@ -1896,8 +2096,8 @@ function WorkspaceBlock({
|
|
|
1896
2096
|
)}
|
|
1897
2097
|
<span className="dwa-hint">
|
|
1898
2098
|
{acp.label(
|
|
1899
|
-
'
|
|
1900
|
-
'
|
|
2099
|
+
'聊天使用会话工作区;此处用于直连会话或宿主未提供工作区时。',
|
|
2100
|
+
'Chats use their session workspace; this applies to direct sessions or when the host provides no workspace.',
|
|
1901
2101
|
)}
|
|
1902
2102
|
</span>
|
|
1903
2103
|
{/* The effective allow-list stays visible in single-root mode too:
|
|
@@ -2360,7 +2560,7 @@ function ModelCatalogBlock({
|
|
|
2360
2560
|
'The NTR surfaces only list what is checked here; unchecked models still work, and the one currently in force is still listed with a "· current" row — the rest are not offered.',
|
|
2361
2561
|
)}
|
|
2362
2562
|
>
|
|
2363
|
-
{acp.label('
|
|
2563
|
+
{acp.label('仅影响菜单可见性', 'Menu visibility only')}
|
|
2364
2564
|
</span>
|
|
2365
2565
|
</span>
|
|
2366
2566
|
<span className="dwa-badge dwa-badge-info">{selectionBadge}</span>
|
|
@@ -2389,6 +2589,7 @@ function ModelCatalogBlock({
|
|
|
2389
2589
|
structurally taller, so the two could never share one height. */}
|
|
2390
2590
|
<input
|
|
2391
2591
|
className="dwa-input dwa-model-search"
|
|
2592
|
+
aria-label={acp.label('搜索模型', 'Search models')}
|
|
2392
2593
|
value={search}
|
|
2393
2594
|
onChange={e => setSearch(e.target.value)}
|
|
2394
2595
|
placeholder={acp.label('搜索模型…', 'Search models…')}
|
|
@@ -2488,7 +2689,7 @@ function ModelCatalogBlock({
|
|
|
2488
2689
|
<div className="dwa-model-list">
|
|
2489
2690
|
{selectedChoices.length === 0 && (
|
|
2490
2691
|
<span className="dwa-hint dwa-model-empty">
|
|
2491
|
-
{acp.label('
|
|
2692
|
+
{acp.label('尚未选择模型,从「全部模型」中加入。', 'No models selected. Add from All models.')}
|
|
2492
2693
|
</span>
|
|
2493
2694
|
)}
|
|
2494
2695
|
{selectedChoices.map(choice => (
|