dsh-ntr 0.2.0 → 0.2.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 +5 -2
- package/README.md +5 -2
- package/lib/bridge.js +14 -23
- package/lib/client/panel.js +96 -78
- package/lib/client.js +403 -301
- package/lib/client.js.map +1 -1
- package/lib/types/bridge.d.ts +3 -3
- package/lib/types/client/panel.d.ts +0 -1
- package/package.json +5 -1
- package/src/bridge.ts +16 -22
- package/src/client/panel.tsx +193 -204
package/src/client/panel.tsx
CHANGED
|
@@ -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; }
|
|
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); }
|
|
@@ -98,25 +116,21 @@ const STYLE = `
|
|
|
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); }
|
|
100
118
|
.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-
|
|
119
|
+
.dwa-agent-list { display: flex; flex-direction: column; border-top: var(--ntr-line); }
|
|
120
|
+
.dwa-agent-card { border-bottom: var(--ntr-line); min-width: 0; }
|
|
121
|
+
.dwa-agent-row { display: grid; grid-template-columns: 38px minmax(0, 1fr) auto; align-items: center; gap: 14px; padding: 16px 0; }
|
|
122
|
+
.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
123
|
.dwa-caret { font-size: 14px; line-height: 1; display: inline-flex; align-items: center; justify-content: center; width: 1em; }
|
|
106
124
|
.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
125
|
.dwa-caret-btn:hover { background: var(--dsw-alias-interactive-bg-active, #e5e7eb); color: inherit; }
|
|
108
126
|
.dwa-agent-summary { min-width: 0; }
|
|
109
127
|
.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:
|
|
128
|
+
.dwa-agent-name { font-size: 13px; font-weight: 550; overflow-wrap: anywhere; }
|
|
111
129
|
.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
130
|
.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; }
|
|
131
|
+
.dwa-agent-body { padding: 0 0 24px 52px; }
|
|
132
|
+
.dwa-subsection { min-width: 0; }
|
|
133
|
+
.dwa-form-row { display: flex; gap: var(--ntr-gap-3); margin-bottom: 14px; align-items: stretch; }
|
|
120
134
|
.dwa-form-col { flex: 1; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
|
121
135
|
.dwa-form-label { font-size: var(--ntr-font-sm); font-weight: 600; color: var(--dsw-alias-label-primary, #374151); }
|
|
122
136
|
.dwa-hint { font-size: var(--ntr-font-xs); line-height: 1.55; color: var(--ntr-muted); }
|
|
@@ -125,11 +139,13 @@ const STYLE = `
|
|
|
125
139
|
.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
140
|
.dwa-input:hover, .dwa-select:hover { border-color: var(--dsw-alias-border-l1, #9ca3af); }
|
|
127
141
|
.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; }
|
|
142
|
+
.dwa-input-mono, .dwa-mono { font-family: var(--ds-font-family-code, ui-monospace), SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
|
|
129
143
|
.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
144
|
.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
145
|
.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
146
|
.dwa-form-actions .dwa-btn-primary { min-width: 88px; }
|
|
147
|
+
.dwa-form-actions > .dwa-hint:first-child { margin-right: auto; }
|
|
148
|
+
.dwa-form-actions > .dwa-hint:last-child:not(:first-child) { width: 100%; }
|
|
133
149
|
.dwa-menu-wrap { position: relative; }
|
|
134
150
|
.dwa-menu-wrap > .dwa-btn { width: 32px; padding: 0; }
|
|
135
151
|
.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 +153,10 @@ const STYLE = `
|
|
|
137
153
|
.dwa-menu-item:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); }
|
|
138
154
|
.dwa-menu-item-danger { color: var(--ntr-danger); }
|
|
139
155
|
.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 {
|
|
156
|
+
.dwa-add-panel .dwa-agent-row { display: none; }
|
|
157
|
+
.dwa-add-panel .dwa-agent-body { padding: 0 0 24px; }
|
|
158
|
+
.dwa-add-panel .dwa-agent-card { border: 0; }
|
|
159
|
+
.dwa-add-content { padding-top: 10px; }
|
|
144
160
|
.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
161
|
.dwa-chip:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); }
|
|
146
162
|
.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 +188,13 @@ const STYLE = `
|
|
|
172
188
|
.dwa-model-status { margin-left: auto; white-space: nowrap; }
|
|
173
189
|
.dwa-model-search { width: 200px; min-height: 30px; }
|
|
174
190
|
.dwa-model-empty { padding: 6px; }
|
|
175
|
-
.dwa-model-row { display: flex; align-items: center; gap:
|
|
191
|
+
.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
192
|
.dwa-model-row:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); }
|
|
177
193
|
.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
194
|
.dwa-model-row-mark { width: 12px; flex: none; text-align: center; opacity: 0.75; }
|
|
179
195
|
.dwa-model-row-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
180
196
|
/* 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:
|
|
197
|
+
.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
198
|
/* The trailing 14px is reserved for the fade mask: while the id fits, the
|
|
183
199
|
gradient only covers empty padding; once it overflows, its tail dissolves
|
|
184
200
|
instead of ending in a hard ellipsis. armModelIdMarquee reads the same
|
|
@@ -192,12 +208,12 @@ const STYLE = `
|
|
|
192
208
|
}
|
|
193
209
|
.dwa-model-row-remove { flex: none; opacity: 0.5; padding: 0 2px; }
|
|
194
210
|
.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); }
|
|
211
|
+
.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; }
|
|
212
|
+
.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
213
|
.dwa-seg > button:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); }
|
|
199
|
-
.dwa-seg > button.is-on { background: var(--ntr-
|
|
200
|
-
.dwa-
|
|
214
|
+
.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; }
|
|
215
|
+
.dwa-preferences { border-top: var(--ntr-line); }
|
|
216
|
+
.dwa-preference-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 20px 0; }
|
|
201
217
|
.dwa-preference-row + .dwa-preference-row { border-top: var(--ntr-line); }
|
|
202
218
|
/* A control that needs the whole card: the label sits above it, not beside
|
|
203
219
|
it. A right-aligned segmented control squeezed its labels into two lines
|
|
@@ -222,13 +238,20 @@ const STYLE = `
|
|
|
222
238
|
.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
239
|
.dwa-deadline-reset { display: flex; justify-content: flex-end; margin-top: 8px; }
|
|
224
240
|
@media (max-width: 640px) {
|
|
225
|
-
.dwa-root { padding:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
.dwa-page-
|
|
241
|
+
.dwa-root { padding: 20px 16px; }
|
|
242
|
+
}
|
|
243
|
+
@container (max-width: 640px) {
|
|
244
|
+
.dwa-page-header { align-items: flex-start; flex-wrap: wrap; }
|
|
245
|
+
.dwa-agent-body { padding-left: 0; }
|
|
246
|
+
.dwa-agent-row { grid-template-columns: 32px minmax(0, 1fr) auto; gap: 10px; }
|
|
247
|
+
.dwa-agent-avatar { width: 32px; height: 32px; }
|
|
248
|
+
.dwa-section-heading { flex-wrap: wrap; }
|
|
249
|
+
.dwa-tabs { gap: 20px; }
|
|
250
|
+
.dwa-notice { flex-wrap: wrap; }
|
|
251
|
+
.dwa-seg-wide > button { padding-inline: 5px; }
|
|
229
252
|
.dwa-model-toolbar-actions { width: 100%; }
|
|
230
253
|
.dwa-model-search { flex: 1 1 auto; width: auto; }
|
|
231
|
-
.dwa-
|
|
254
|
+
.dwa-preference-row { align-items: flex-start; flex-direction: column; }
|
|
232
255
|
.dwa-default-hint { align-items: flex-start; flex-direction: column; }
|
|
233
256
|
/* The slider's scale: up to eight rungs with full unit words ("15 分钟")
|
|
234
257
|
need a smaller face before they can collide on a narrow panel. The rung
|
|
@@ -809,7 +832,7 @@ function ApprovalDeadlinesCard(props: { acp: AcpStore; ui: UiState }) {
|
|
|
809
832
|
</p>
|
|
810
833
|
</div>
|
|
811
834
|
</div>
|
|
812
|
-
<div className="dwa-
|
|
835
|
+
<div className="dwa-preferences">
|
|
813
836
|
<DeadlineModeBlock acp={acp} mode={deadlines.mode} override={modeNote} />
|
|
814
837
|
<DeadlineSliderBlock
|
|
815
838
|
acp={acp}
|
|
@@ -829,51 +852,38 @@ function ApprovalDeadlinesCard(props: { acp: AcpStore; ui: UiState }) {
|
|
|
829
852
|
)
|
|
830
853
|
}
|
|
831
854
|
|
|
832
|
-
|
|
855
|
+
type SettingsPage = 'agents' | 'add' | 'approvals' | 'appearance' | 'diagnostics'
|
|
856
|
+
|
|
833
857
|
export function AcpSettingsSection(props: WithStore) {
|
|
834
858
|
const acp = props.acp
|
|
835
859
|
ensureStyle()
|
|
836
860
|
const ui = useUi(acp)
|
|
837
861
|
const ledger = permissionLedgerCard(ui)
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
// selection until the next create resets it.
|
|
841
|
-
const [addOpen, setAddOpen] = useState(true)
|
|
862
|
+
const [page, setPage] = useState<SettingsPage>('agents')
|
|
863
|
+
const [addOpen, setAddOpen] = useState(false)
|
|
842
864
|
const [addTarget, setAddTarget] = useState<DetectedAgentInfo | undefined>(undefined)
|
|
843
|
-
const [createNonce, setCreateNonce] = useState(0)
|
|
844
865
|
const { configured, available } = partitionAgents(ui.detectedAgents, ui.agents)
|
|
845
866
|
const selected = addTarget ?? available[0] ?? NEW_AGENT_SEED
|
|
867
|
+
const openAdd = (): void => {
|
|
868
|
+
setAddOpen(true)
|
|
869
|
+
setPage('add')
|
|
870
|
+
}
|
|
871
|
+
const presetProblem = ui.preset !== undefined && (!ui.preset.installed || ui.preset.problem !== undefined)
|
|
846
872
|
|
|
847
873
|
return (
|
|
848
874
|
<div className="dwa-root">
|
|
849
875
|
<header className="dwa-page-header">
|
|
850
876
|
<div className="dwa-page-heading">
|
|
851
|
-
<h2 className="dwa-page-title">{acp.label('NTR
|
|
877
|
+
<h2 className="dwa-page-title">{acp.label('NTR 设置', 'NTR settings')}</h2>
|
|
852
878
|
<p className="dwa-page-description">
|
|
853
|
-
{acp.label('
|
|
879
|
+
{acp.label('连接智能体,设置你的对话偏好。', 'Connect agents and set your conversation preferences.')}
|
|
854
880
|
</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
881
|
</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')}
|
|
882
|
+
{page === 'agents' && configured.length > 0 && (
|
|
883
|
+
<button type="button" className="dwa-btn dwa-btn-primary" onClick={openAdd}>
|
|
884
|
+
{acp.label('添加智能体', 'Add agent')}
|
|
875
885
|
</button>
|
|
876
|
-
|
|
886
|
+
)}
|
|
877
887
|
</header>
|
|
878
888
|
|
|
879
889
|
{/* The cross-session mirror: the conversation dock answers the asks
|
|
@@ -900,14 +910,15 @@ export function AcpSettingsSection(props: WithStore) {
|
|
|
900
910
|
{permission.options.map(option => (
|
|
901
911
|
<button
|
|
902
912
|
key={option.optionId}
|
|
903
|
-
|
|
913
|
+
type="button"
|
|
914
|
+
className="dwa-btn"
|
|
904
915
|
onClick={() => { void acp.respondPermission(permission.requestId, option.optionId).catch(() => undefined) }}
|
|
905
916
|
>
|
|
906
917
|
{option.name}
|
|
907
918
|
</button>
|
|
908
919
|
))}
|
|
909
|
-
<button className="dwa-btn" onClick={() => { void acp.respondPermission(permission.requestId).catch(() => undefined) }}>
|
|
910
|
-
{acp.label('
|
|
920
|
+
<button type="button" className="dwa-btn dwa-btn-quiet" onClick={() => { void acp.respondPermission(permission.requestId).catch(() => undefined) }}>
|
|
921
|
+
{acp.label('取消', 'Cancel')}
|
|
911
922
|
</button>
|
|
912
923
|
</div>
|
|
913
924
|
))}
|
|
@@ -917,17 +928,34 @@ export function AcpSettingsSection(props: WithStore) {
|
|
|
917
928
|
{ui.notice !== undefined && (
|
|
918
929
|
<div className="dwa-card dwa-card-warn" style={{ marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
919
930
|
<span style={{ flex: 1 }}>{ui.notice}</span>
|
|
920
|
-
<button className="dwa-btn" onClick={() => acp.clearNotice()}>✕</button>
|
|
931
|
+
<button type="button" className="dwa-btn dwa-btn-quiet" aria-label={acp.label('关闭通知', 'Dismiss notice')} onClick={() => acp.clearNotice()}>✕</button>
|
|
921
932
|
</div>
|
|
922
933
|
)}
|
|
923
934
|
|
|
924
|
-
{
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
{
|
|
935
|
+
{presetProblem && (
|
|
936
|
+
<div className="dwa-card dwa-card-warn dwa-notice" role="status">
|
|
937
|
+
<span>{acp.label('NTR 模式需要检查,当前可能无法开始对话。', 'NTR mode needs attention; new conversations may be unavailable.')}</span>
|
|
938
|
+
<button type="button" className="dwa-btn" onClick={() => setPage('diagnostics')}>{acp.label('查看诊断', 'View diagnostics')}</button>
|
|
939
|
+
</div>
|
|
940
|
+
)}
|
|
941
|
+
<nav className="dwa-tabs" aria-label={acp.label('NTR 设置分类', 'NTR settings categories')}>
|
|
942
|
+
<button type="button" aria-pressed={page === 'agents'} onClick={() => setPage('agents')}>{acp.label('智能体', 'Agents')}</button>
|
|
943
|
+
{ui.approvalDeadlines !== undefined && <button type="button" aria-pressed={page === 'approvals'} onClick={() => setPage('approvals')}>{acp.label('审批时限', 'Approval deadlines')}</button>}
|
|
944
|
+
<button type="button" aria-pressed={page === 'appearance'} onClick={() => setPage('appearance')}>{acp.label('对话外观', 'Appearance')}</button>
|
|
945
|
+
</nav>
|
|
946
|
+
{page === 'diagnostics' && (
|
|
947
|
+
<section className="dwa-section">
|
|
948
|
+
<h3 className="dwa-section-title">{acp.label('连接诊断', 'Connection diagnostics')}</h3>
|
|
949
|
+
<div className="dwa-preference-row"><span>{acp.label('宿主事件流', 'Host event stream')}</span><span className="dwa-hint">{streamText(ui.stream, acp)}</span></div>
|
|
950
|
+
<div className="dwa-row-actions">
|
|
951
|
+
<button type="button" className="dwa-btn" onClick={() => { void acp.resync(); void acp.detectAgents() }} disabled={ui.detecting}>
|
|
952
|
+
{ui.detecting ? acp.label('检测中...', 'Detecting...') : acp.label('重新检测与刷新', 'Re-detect & Refresh')}
|
|
953
|
+
</button>
|
|
954
|
+
{ui.detectSummary !== undefined && <span className="dwa-hint" role="status">{ui.detectSummary}</span>}
|
|
955
|
+
</div>
|
|
956
|
+
</section>
|
|
957
|
+
)}
|
|
958
|
+
{page === 'diagnostics' && ui.preset !== undefined && (
|
|
931
959
|
<div className="dwa-card dwa-mode-banner">
|
|
932
960
|
<div className="dwa-mode-title">
|
|
933
961
|
{acp.label('NTR 模式', 'NTR mode')}
|
|
@@ -950,24 +978,24 @@ export function AcpSettingsSection(props: WithStore) {
|
|
|
950
978
|
</div>
|
|
951
979
|
)}
|
|
952
980
|
|
|
953
|
-
{/*
|
|
954
|
-
<section className="dwa-section">
|
|
981
|
+
{/* Keep editors mounted so navigation preserves unsaved values. */}
|
|
982
|
+
<section className="dwa-section" hidden={page !== 'agents'} aria-label={acp.label('已接入的智能体', 'Connected agents')}>
|
|
955
983
|
<div className="dwa-section-heading">
|
|
956
984
|
<div>
|
|
957
985
|
<h3 className="dwa-section-title">
|
|
958
986
|
{acp.label('已接入的智能体', 'Connected agents')}
|
|
959
|
-
<span className="dwa-badge">{configured.length}</span>
|
|
987
|
+
{ui.agentsLoaded && <span className="dwa-badge">{configured.length}</span>}
|
|
960
988
|
</h3>
|
|
961
|
-
<p className="dwa-section-copy">{acp.label('展开智能体可管理模型、启动命令与工作区。', 'Expand an agent to manage its models, launch command, and workspace.')}</p>
|
|
962
989
|
</div>
|
|
990
|
+
{configured.length > 0 && <span className="dwa-hint">{acp.label('默认项用于新的 NTR 会话', 'The default serves new NTR conversations')}</span>}
|
|
963
991
|
</div>
|
|
964
992
|
|
|
965
|
-
{
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
)}
|
|
993
|
+
{!ui.agentsLoaded && <div className="dwa-empty" role="status">{acp.label('正在读取智能体…', 'Loading agents…')}</div>}
|
|
994
|
+
{ui.agentsLoaded && configured.length === 0 && (
|
|
995
|
+
<div className="dwa-empty">
|
|
996
|
+
<h3>{acp.label('接入第一个智能体', 'Connect your first agent')}</h3>
|
|
997
|
+
<p className="dwa-section-copy">{acp.label('选择已有预设,或填写自己的 ACP 启动命令。', 'Choose a preset or enter your own ACP launch command.')}</p>
|
|
998
|
+
<button type="button" className="dwa-btn dwa-btn-primary" onClick={openAdd}>{acp.label('添加智能体', 'Add agent')}</button>
|
|
971
999
|
</div>
|
|
972
1000
|
)}
|
|
973
1001
|
|
|
@@ -982,24 +1010,22 @@ export function AcpSettingsSection(props: WithStore) {
|
|
|
982
1010
|
acp={acp}
|
|
983
1011
|
isDefault={ui.defaultAgent === info.id}
|
|
984
1012
|
synthesized={synthesized}
|
|
1013
|
+
active={page === 'agents'}
|
|
985
1014
|
/>
|
|
986
1015
|
)
|
|
987
1016
|
})}
|
|
988
1017
|
</div>
|
|
989
1018
|
</section>
|
|
990
1019
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
click. Selection follows the first available preset until pinned. */}
|
|
994
|
-
<section className="dwa-section">
|
|
1020
|
+
<section className="dwa-section" hidden={page !== 'add'} aria-label={acp.label('添加智能体', 'Add agent')}>
|
|
1021
|
+
<button type="button" className="dwa-btn dwa-btn-quiet dwa-back" onClick={() => setPage('agents')}>{acp.label('返回智能体列表', 'Back to agents')}</button>
|
|
995
1022
|
<div className="dwa-add-panel">
|
|
996
|
-
<
|
|
997
|
-
<
|
|
998
|
-
<
|
|
999
|
-
<
|
|
1000
|
-
</
|
|
1001
|
-
|
|
1002
|
-
</button>
|
|
1023
|
+
<div className="dwa-section-heading">
|
|
1024
|
+
<div>
|
|
1025
|
+
<h3 className="dwa-section-title">{acp.label('添加智能体', 'Add agent')}</h3>
|
|
1026
|
+
<p className="dwa-section-copy">{acp.label('从预设快速启用,或创建自定义连接。', 'Enable a preset or create a custom connection.')}</p>
|
|
1027
|
+
</div>
|
|
1028
|
+
</div>
|
|
1003
1029
|
{addOpen && (
|
|
1004
1030
|
<div className="dwa-add-content">
|
|
1005
1031
|
<div className="dwa-chip-list">
|
|
@@ -1028,19 +1054,17 @@ export function AcpSettingsSection(props: WithStore) {
|
|
|
1028
1054
|
</div>
|
|
1029
1055
|
)}
|
|
1030
1056
|
<AgentRow
|
|
1031
|
-
key={selected.id
|
|
1057
|
+
key={selected.id}
|
|
1032
1058
|
detected={selected}
|
|
1033
1059
|
acp={acp}
|
|
1034
1060
|
isDefault={false}
|
|
1035
1061
|
synthesized={false}
|
|
1036
1062
|
newCustom={selected.id === ''}
|
|
1063
|
+
active={page === 'add'}
|
|
1037
1064
|
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
1065
|
setAddTarget(undefined)
|
|
1043
|
-
|
|
1066
|
+
setAddOpen(false)
|
|
1067
|
+
setPage('agents')
|
|
1044
1068
|
}}
|
|
1045
1069
|
/>
|
|
1046
1070
|
</div>
|
|
@@ -1048,24 +1072,16 @@ export function AcpSettingsSection(props: WithStore) {
|
|
|
1048
1072
|
</div>
|
|
1049
1073
|
</section>
|
|
1050
1074
|
|
|
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">
|
|
1075
|
+
<div hidden={page !== 'approvals'}><ApprovalDeadlinesCard acp={acp} ui={ui} /></div>
|
|
1076
|
+
|
|
1077
|
+
<section className="dwa-section" hidden={page !== 'appearance'} aria-label={acp.label('对话外观', 'Appearance')}>
|
|
1062
1078
|
<div className="dwa-section-heading">
|
|
1063
1079
|
<div>
|
|
1064
1080
|
<h3 className="dwa-section-title">{acp.label('对话外观', 'Conversation appearance')}</h3>
|
|
1065
1081
|
<p className="dwa-section-copy">{acp.label('设置对话中 NTR 挂牌的进程存活提示。', 'Choose how the NTR tag indicates a running agent process.')}</p>
|
|
1066
1082
|
</div>
|
|
1067
1083
|
</div>
|
|
1068
|
-
<div className="dwa-
|
|
1084
|
+
<div className="dwa-preferences">
|
|
1069
1085
|
<div className="dwa-preference-row">
|
|
1070
1086
|
<div>
|
|
1071
1087
|
<div className="dwa-preference-label">{acp.label('挂牌光晕', 'Tag halo')}</div>
|
|
@@ -1106,6 +1122,14 @@ export function AcpSettingsSection(props: WithStore) {
|
|
|
1106
1122
|
)}
|
|
1107
1123
|
</div>
|
|
1108
1124
|
</section>
|
|
1125
|
+
<footer className="dwa-health">
|
|
1126
|
+
<div className="dwa-health-status">
|
|
1127
|
+
<span className="dwa-dot" style={{ background: ui.stream === 'live' ? 'var(--ntr-success)' : ui.stream === 'down' ? 'var(--ntr-danger)' : 'var(--ntr-muted)' }} />
|
|
1128
|
+
<span>{streamText(ui.stream, acp)}</span>
|
|
1129
|
+
{ui.preset !== undefined && <span>{ui.preset.installed && !presetProblem ? acp.label('NTR 模式已就绪', 'NTR mode ready') : acp.label('NTR 模式需检查', 'NTR mode needs attention')}</span>}
|
|
1130
|
+
</div>
|
|
1131
|
+
{page !== 'diagnostics' && <button type="button" className="dwa-btn dwa-btn-quiet" onClick={() => setPage('diagnostics')}>{acp.label('查看诊断', 'View diagnostics')}</button>}
|
|
1132
|
+
</footer>
|
|
1109
1133
|
</div>
|
|
1110
1134
|
)
|
|
1111
1135
|
}
|
|
@@ -1117,6 +1141,7 @@ function AgentRow({
|
|
|
1117
1141
|
acp,
|
|
1118
1142
|
isDefault,
|
|
1119
1143
|
synthesized,
|
|
1144
|
+
active,
|
|
1120
1145
|
newCustom = false,
|
|
1121
1146
|
onCreated,
|
|
1122
1147
|
}: {
|
|
@@ -1125,6 +1150,7 @@ function AgentRow({
|
|
|
1125
1150
|
acp: AcpStore
|
|
1126
1151
|
isDefault: boolean
|
|
1127
1152
|
synthesized: boolean
|
|
1153
|
+
active: boolean
|
|
1128
1154
|
/** From-scratch creation: the id is editable and no preset defaults exist. */
|
|
1129
1155
|
newCustom?: boolean
|
|
1130
1156
|
/** Fired after a creation-card save succeeds (the list re-derives its selection). */
|
|
@@ -1135,6 +1161,7 @@ function AgentRow({
|
|
|
1135
1161
|
// A configured agent starts collapsed; a creation card opens straight away
|
|
1136
1162
|
// (nothing to summarise yet).
|
|
1137
1163
|
const [expanded, setExpanded] = useState(!configured)
|
|
1164
|
+
const [detailPage, setDetailPage] = useState<'models' | 'connection' | 'advanced'>(configured ? 'models' : 'connection')
|
|
1138
1165
|
const [form, setForm] = useState<AgentForm>(() => initialForm(detected, activeAgent))
|
|
1139
1166
|
const [saving, setSaving] = useState(false)
|
|
1140
1167
|
const [savedSuccess, setSavedSuccess] = useState(false)
|
|
@@ -1169,7 +1196,8 @@ function AgentRow({
|
|
|
1169
1196
|
|
|
1170
1197
|
const payload = buildPayload(form, detected, newCustom)
|
|
1171
1198
|
const current = persistedDef(detected, activeAgent)
|
|
1172
|
-
const
|
|
1199
|
+
const clearingIdleOverride = payload.idleTimeoutMs === undefined && detected.currentConfig?.idleTimeoutMs !== undefined
|
|
1200
|
+
const dirty = current !== undefined && (!payloadEquals(payload, current) || clearingIdleOverride)
|
|
1173
1201
|
// A save stops the child and kills every live session (see BridgeManager.saveAgent).
|
|
1174
1202
|
const showSaveWarning = dirty && activeAgent !== undefined && activeAgent.state !== 'stopped'
|
|
1175
1203
|
|
|
@@ -1221,7 +1249,7 @@ function AgentRow({
|
|
|
1221
1249
|
: (activeAgent?.title ?? detected.currentConfig?.title ?? detected.name)
|
|
1222
1250
|
const stderr = activeAgent?.state === 'error' ? (activeAgent.stderrTail ?? []) : []
|
|
1223
1251
|
const errorSummary = stderr.length > 0 ? stderr[stderr.length - 1] : undefined
|
|
1224
|
-
const
|
|
1252
|
+
const selectedModelCount = form.exposed?.length ?? 0
|
|
1225
1253
|
const isPreset = PRESET_IDS.has(detected.id)
|
|
1226
1254
|
// An error row opens itself so the stderr tail is never behind a click.
|
|
1227
1255
|
const open = expanded || (activeAgent?.state === 'error' && configured)
|
|
@@ -1229,19 +1257,11 @@ function AgentRow({
|
|
|
1229
1257
|
return (
|
|
1230
1258
|
<div className={`dwa-agent-card${open ? ' is-open' : ''}`}>
|
|
1231
1259
|
<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>
|
|
1260
|
+
<span className="dwa-agent-avatar" aria-hidden>{displayName.slice(0, 2).toUpperCase()}</span>
|
|
1241
1261
|
<div className="dwa-agent-summary">
|
|
1242
1262
|
<div className="dwa-agent-titleline">
|
|
1243
1263
|
<span className="dwa-agent-name" title={detected.executablePath ?? detected.description}>{displayName}</span>
|
|
1244
|
-
{isDefault && <span className="dwa-badge dwa-badge-
|
|
1264
|
+
{isDefault && <span className="dwa-badge dwa-badge-info">{acp.label('默认', 'Default')}</span>}
|
|
1245
1265
|
{!isPreset && !newCustom && <span className="dwa-badge">{acp.label('自定义', 'Custom')}</span>}
|
|
1246
1266
|
</div>
|
|
1247
1267
|
<div className="dwa-agent-meta">
|
|
@@ -1254,7 +1274,7 @@ function AgentRow({
|
|
|
1254
1274
|
{configured && (
|
|
1255
1275
|
<>
|
|
1256
1276
|
{state !== undefined && <span className="dwa-agent-meta-separator">·</span>}
|
|
1257
|
-
<span>{
|
|
1277
|
+
<span>{acp.label(`${String(selectedModelCount)} 个已选模型`, `${String(selectedModelCount)} selected models`)}</span>
|
|
1258
1278
|
</>
|
|
1259
1279
|
)}
|
|
1260
1280
|
{!synthesized && !newCustom && !detected.detected && (
|
|
@@ -1267,7 +1287,12 @@ function AgentRow({
|
|
|
1267
1287
|
<div className="dwa-error dwa-mono" style={{ marginTop: 6, wordBreak: 'break-all' }}>{errorSummary}</div>
|
|
1268
1288
|
)}
|
|
1269
1289
|
</div>
|
|
1270
|
-
<div
|
|
1290
|
+
<div className="dwa-row-actions">
|
|
1291
|
+
{configured && (
|
|
1292
|
+
<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)}>
|
|
1293
|
+
{open ? acp.label('收起', 'Collapse') : acp.label('管理', 'Manage')}
|
|
1294
|
+
</button>
|
|
1295
|
+
)}
|
|
1271
1296
|
{activeAgent?.state === 'error' && (
|
|
1272
1297
|
<button className="dwa-btn" disabled={testing} onClick={() => void handleTest()}>
|
|
1273
1298
|
{testing ? acp.label('测试中...', 'Testing...') : acp.label('测试', 'Test')}
|
|
@@ -1290,19 +1315,18 @@ function AgentRow({
|
|
|
1290
1315
|
survives collapsing the row. */}
|
|
1291
1316
|
<div className="dwa-agent-body" style={open ? undefined : { display: 'none' }}>
|
|
1292
1317
|
{stderr.length > 0 && <pre className="dwa-stderr">{stderr.join('\n')}</pre>}
|
|
1318
|
+
<nav className="dwa-tabs dwa-detail-tabs" aria-label={acp.label(`${displayName} 配置`, `${displayName} settings`)}>
|
|
1319
|
+
{configured && <button type="button" aria-pressed={detailPage === 'models'} onClick={() => setDetailPage('models')}>{acp.label('模型', 'Models')}</button>}
|
|
1320
|
+
<button type="button" aria-pressed={detailPage === 'connection'} onClick={() => setDetailPage('connection')}>{acp.label('连接', 'Connection')}</button>
|
|
1321
|
+
<button type="button" aria-pressed={detailPage === 'advanced'} onClick={() => setDetailPage('advanced')}>{acp.label('高级', 'Advanced')}</button>
|
|
1322
|
+
</nav>
|
|
1293
1323
|
{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>
|
|
1324
|
+
<section className="dwa-subsection" hidden={detailPage !== 'models'}>
|
|
1301
1325
|
<ModelCatalogBlock
|
|
1302
1326
|
agentId={detected.id}
|
|
1303
1327
|
detected={detected}
|
|
1304
1328
|
acp={acp}
|
|
1305
|
-
active={open}
|
|
1329
|
+
active={active && open && detailPage === 'models'}
|
|
1306
1330
|
exposed={form.exposed}
|
|
1307
1331
|
onExposedChange={exposed => patch({ exposed })}
|
|
1308
1332
|
autoDiscover={form.autoDiscoverModels}
|
|
@@ -1311,15 +1335,11 @@ function AgentRow({
|
|
|
1311
1335
|
/>
|
|
1312
1336
|
</section>
|
|
1313
1337
|
)}
|
|
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>
|
|
1338
|
+
<section className="dwa-subsection" hidden={detailPage === 'models'}>
|
|
1321
1339
|
<AgentDetails
|
|
1322
1340
|
acp={acp}
|
|
1341
|
+
advancedOpen={detailPage === 'advanced'}
|
|
1342
|
+
dirty={dirty}
|
|
1323
1343
|
detected={detected}
|
|
1324
1344
|
form={form}
|
|
1325
1345
|
patch={patch}
|
|
@@ -1502,6 +1522,8 @@ export function buildPayload(form: AgentForm, detected: DetectedAgentInfo, newCu
|
|
|
1502
1522
|
/** The expanded body: connection form, workspace allow-list, env, actions. */
|
|
1503
1523
|
function AgentDetails({
|
|
1504
1524
|
acp,
|
|
1525
|
+
advancedOpen,
|
|
1526
|
+
dirty,
|
|
1505
1527
|
detected,
|
|
1506
1528
|
form,
|
|
1507
1529
|
patch,
|
|
@@ -1524,6 +1546,8 @@ function AgentDetails({
|
|
|
1524
1546
|
onTest,
|
|
1525
1547
|
}: {
|
|
1526
1548
|
acp: AcpStore
|
|
1549
|
+
advancedOpen: boolean
|
|
1550
|
+
dirty: boolean
|
|
1527
1551
|
detected: DetectedAgentInfo
|
|
1528
1552
|
form: AgentForm
|
|
1529
1553
|
patch: (next: Partial<AgentForm>) => void
|
|
@@ -1545,8 +1569,6 @@ function AgentDetails({
|
|
|
1545
1569
|
onSave: () => void
|
|
1546
1570
|
onTest: () => void
|
|
1547
1571
|
}) {
|
|
1548
|
-
const ui = useUi(acp)
|
|
1549
|
-
const [advancedOpen, setAdvancedOpen] = useState(false)
|
|
1550
1572
|
// The same parse the host will run (`validateAgentDef` → `splitCommandLine`).
|
|
1551
1573
|
// Showing it is what turns "the first box swallowed my arguments" into a
|
|
1552
1574
|
// visible, fixable state instead of a spawn error.
|
|
@@ -1557,15 +1579,14 @@ function AgentDetails({
|
|
|
1557
1579
|
+ (parsedCommand.args.length === 0 ? '' : ` ${acp.label('参数', 'Arguments')}: ${parsedCommand.args.join(' ')}`)
|
|
1558
1580
|
return (
|
|
1559
1581
|
<div>
|
|
1560
|
-
{
|
|
1561
|
-
filled command+args, so a plain "enable this agent" flow needs
|
|
1562
|
-
nothing below. Everything else lives under 高级参数. */}
|
|
1582
|
+
<div hidden={advancedOpen}>
|
|
1563
1583
|
{newCustom && (
|
|
1564
1584
|
<div className="dwa-form-row">
|
|
1565
1585
|
<div className="dwa-form-col">
|
|
1566
1586
|
<label className="dwa-form-label">{acp.label('Agent ID(唯一标识,创建后不可改)', 'Agent ID (unique, immutable after creation)')}</label>
|
|
1567
1587
|
<input
|
|
1568
1588
|
className="dwa-input dwa-input-mono"
|
|
1589
|
+
aria-label={acp.label('Agent ID', 'Agent ID')}
|
|
1569
1590
|
value={form.agentId}
|
|
1570
1591
|
onChange={e => patch({ agentId: e.target.value })}
|
|
1571
1592
|
placeholder="e.g. my-agent"
|
|
@@ -1586,6 +1607,7 @@ function AgentDetails({
|
|
|
1586
1607
|
<label className="dwa-form-label">{acp.label('显示名', 'Display Name')}</label>
|
|
1587
1608
|
<input
|
|
1588
1609
|
className="dwa-input"
|
|
1610
|
+
aria-label={acp.label('显示名', 'Display Name')}
|
|
1589
1611
|
value={form.title}
|
|
1590
1612
|
onChange={e => patch({ title: e.target.value })}
|
|
1591
1613
|
placeholder={detected.name === '' ? acp.label('我的 Agent', 'My Agent') : detected.name}
|
|
@@ -1604,6 +1626,7 @@ function AgentDetails({
|
|
|
1604
1626
|
<label className="dwa-form-label">{acp.label('启动命令(程序 + 参数)', 'Launch Command (program + arguments)')}</label>
|
|
1605
1627
|
<input
|
|
1606
1628
|
className="dwa-input dwa-input-mono"
|
|
1629
|
+
aria-label={acp.label('启动命令(程序 + 参数)', 'Launch Command (program + arguments)')}
|
|
1607
1630
|
value={form.commandLine}
|
|
1608
1631
|
onChange={e => patch({ commandLine: e.target.value })}
|
|
1609
1632
|
placeholder={detected.defaultCommand === ''
|
|
@@ -1613,39 +1636,17 @@ function AgentDetails({
|
|
|
1613
1636
|
<span className="dwa-hint">{commandPreview}</span>
|
|
1614
1637
|
<span className="dwa-hint">
|
|
1615
1638
|
{acp.label(
|
|
1616
|
-
'
|
|
1617
|
-
'
|
|
1639
|
+
'含空格的程序路径请加引号。',
|
|
1640
|
+
'Quote the program path if it contains spaces.',
|
|
1618
1641
|
)}
|
|
1619
1642
|
</span>
|
|
1620
1643
|
</div>
|
|
1621
1644
|
</div>
|
|
1622
1645
|
|
|
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
|
-
)}
|
|
1646
|
+
<WorkspaceBlock acp={acp} form={form} patch={patch} cwdInvalid={cwdInvalid} rootsInvalid={rootsInvalid} />
|
|
1646
1647
|
</div>
|
|
1647
1648
|
|
|
1648
|
-
<div
|
|
1649
|
+
<div hidden={!advancedOpen}>
|
|
1649
1650
|
{!newCustom && (
|
|
1650
1651
|
<div className="dwa-default-hint">
|
|
1651
1652
|
<div>
|
|
@@ -1679,14 +1680,6 @@ function AgentDetails({
|
|
|
1679
1680
|
</div>
|
|
1680
1681
|
</div>
|
|
1681
1682
|
|
|
1682
|
-
<WorkspaceBlock
|
|
1683
|
-
acp={acp}
|
|
1684
|
-
form={form}
|
|
1685
|
-
patch={patch}
|
|
1686
|
-
cwdInvalid={cwdInvalid}
|
|
1687
|
-
rootsInvalid={rootsInvalid}
|
|
1688
|
-
/>
|
|
1689
|
-
|
|
1690
1683
|
{/* Idle release: scoped to agents with NO live session (see
|
|
1691
1684
|
AgentBridge.releaseIfIdle), so it reclaims the processes that the
|
|
1692
1685
|
model probe / post-save probe leave behind without ever costing a
|
|
@@ -1699,6 +1692,7 @@ function AgentDetails({
|
|
|
1699
1692
|
<label className="dwa-form-label">{acp.label('空闲释放(毫秒)', 'Idle Release (ms)')}</label>
|
|
1700
1693
|
<input
|
|
1701
1694
|
className="dwa-input dwa-input-mono"
|
|
1695
|
+
aria-label={acp.label('空闲释放(毫秒)', 'Idle Release (ms)')}
|
|
1702
1696
|
value={form.idleStr}
|
|
1703
1697
|
onChange={e => patch({ idleStr: e.target.value })}
|
|
1704
1698
|
placeholder={acp.label(
|
|
@@ -1715,18 +1709,8 @@ function AgentDetails({
|
|
|
1715
1709
|
: (
|
|
1716
1710
|
<>
|
|
1717
1711
|
{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.',
|
|
1712
|
+
'承载过会话的进程空闲后挂起;0 不释放,探测进程固定 5 分钟。',
|
|
1713
|
+
'Park idle chat processes; 0 keeps them alive. Probe processes always release after 5 minutes.',
|
|
1730
1714
|
)}
|
|
1731
1715
|
</>
|
|
1732
1716
|
)}
|
|
@@ -1783,6 +1767,7 @@ function AgentDetails({
|
|
|
1783
1767
|
<textarea
|
|
1784
1768
|
className="dwa-input dwa-input-mono"
|
|
1785
1769
|
style={{ minHeight: 96, resize: 'vertical' }}
|
|
1770
|
+
aria-label={acp.label('MCP 服务器(JSON)', 'MCP Servers (JSON)')}
|
|
1786
1771
|
value={form.mcpText}
|
|
1787
1772
|
onChange={e => patch({ mcpText: e.target.value })}
|
|
1788
1773
|
spellCheck={false}
|
|
@@ -1795,14 +1780,17 @@ function AgentDetails({
|
|
|
1795
1780
|
)}
|
|
1796
1781
|
<span className="dwa-hint">
|
|
1797
1782
|
{acp.label(
|
|
1798
|
-
'
|
|
1799
|
-
'
|
|
1783
|
+
'额外追加给智能体的 MCP 服务器,不影响其已有配置。',
|
|
1784
|
+
'Additional MCP servers for the agent; its own configuration is unchanged.',
|
|
1800
1785
|
)}
|
|
1801
1786
|
</span>
|
|
1802
1787
|
</div>
|
|
1803
1788
|
</div>
|
|
1804
1789
|
|
|
1790
|
+
{!advancedOpen && (mcpInvalid || idleInvalid) && <p className="dwa-error" role="alert">{acp.label('请检查「高级」中的无效配置。', 'Check the invalid settings in Advanced.')}</p>}
|
|
1791
|
+
{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
1792
|
<div className="dwa-form-actions">
|
|
1793
|
+
<span className="dwa-hint">{isConfigured ? dirty ? acp.label('有未保存的更改', 'Unsaved changes') : acp.label('配置已保存', 'Settings saved') : ''}</span>
|
|
1806
1794
|
<button
|
|
1807
1795
|
className="dwa-btn"
|
|
1808
1796
|
onClick={onTest}
|
|
@@ -1815,14 +1803,14 @@ function AgentDetails({
|
|
|
1815
1803
|
<button
|
|
1816
1804
|
className="dwa-btn dwa-btn-primary"
|
|
1817
1805
|
onClick={onSave}
|
|
1818
|
-
disabled={saving || testing || formIncomplete}
|
|
1806
|
+
disabled={saving || testing || formIncomplete || (isConfigured && !dirty)}
|
|
1819
1807
|
>
|
|
1820
1808
|
{saving
|
|
1821
1809
|
? acp.label('保存中...', 'Saving...')
|
|
1822
1810
|
: savedSuccess
|
|
1823
1811
|
? acp.label('✓ 已保存', '✓ Saved')
|
|
1824
1812
|
: isConfigured
|
|
1825
|
-
? acp.label('
|
|
1813
|
+
? acp.label('保存更改', 'Save changes')
|
|
1826
1814
|
: acp.label('保存并启用', 'Save & Enable')}
|
|
1827
1815
|
</button>
|
|
1828
1816
|
|
|
@@ -1896,8 +1884,8 @@ function WorkspaceBlock({
|
|
|
1896
1884
|
)}
|
|
1897
1885
|
<span className="dwa-hint">
|
|
1898
1886
|
{acp.label(
|
|
1899
|
-
'
|
|
1900
|
-
'
|
|
1887
|
+
'聊天使用会话工作区;此处用于直连会话或宿主未提供工作区时。',
|
|
1888
|
+
'Chats use their session workspace; this applies to direct sessions or when the host provides no workspace.',
|
|
1901
1889
|
)}
|
|
1902
1890
|
</span>
|
|
1903
1891
|
{/* The effective allow-list stays visible in single-root mode too:
|
|
@@ -2360,7 +2348,7 @@ function ModelCatalogBlock({
|
|
|
2360
2348
|
'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
2349
|
)}
|
|
2362
2350
|
>
|
|
2363
|
-
{acp.label('
|
|
2351
|
+
{acp.label('仅影响菜单可见性', 'Menu visibility only')}
|
|
2364
2352
|
</span>
|
|
2365
2353
|
</span>
|
|
2366
2354
|
<span className="dwa-badge dwa-badge-info">{selectionBadge}</span>
|
|
@@ -2389,6 +2377,7 @@ function ModelCatalogBlock({
|
|
|
2389
2377
|
structurally taller, so the two could never share one height. */}
|
|
2390
2378
|
<input
|
|
2391
2379
|
className="dwa-input dwa-model-search"
|
|
2380
|
+
aria-label={acp.label('搜索模型', 'Search models')}
|
|
2392
2381
|
value={search}
|
|
2393
2382
|
onChange={e => setSearch(e.target.value)}
|
|
2394
2383
|
placeholder={acp.label('搜索模型…', 'Search models…')}
|
|
@@ -2488,7 +2477,7 @@ function ModelCatalogBlock({
|
|
|
2488
2477
|
<div className="dwa-model-list">
|
|
2489
2478
|
{selectedChoices.length === 0 && (
|
|
2490
2479
|
<span className="dwa-hint dwa-model-empty">
|
|
2491
|
-
{acp.label('
|
|
2480
|
+
{acp.label('尚未选择模型,从「全部模型」中加入。', 'No models selected. Add from All models.')}
|
|
2492
2481
|
</span>
|
|
2493
2482
|
)}
|
|
2494
2483
|
{selectedChoices.map(choice => (
|