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.
@@ -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: 20px; overflow: auto; font-size: var(--ntr-font); line-height: 1.55;
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 button, .dwa-root input, .dwa-root select { font-family: inherit; }
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: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
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: 650; letter-spacing: -0.02em; }
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: 14px; font-weight: 650; margin: 0; display: flex; align-items: center; gap: 8px; }
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-left: 3px solid var(--dsw-alias-state-warn-primary, #d97706); background: var(--dsw-alias-state-warn-tertiary, #fffbeb); }
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; gap: 8px; }
102
- .dwa-agent-card { border: var(--ntr-line); border-radius: var(--ntr-radius); background: var(--dsw-alias-bg-layer-1, #fff); overflow: visible; transition: border-color 0.15s ease; }
103
- .dwa-agent-card.is-open { border-color: var(--dsw-alias-border-l1, #c7ccd1); }
104
- .dwa-agent-row { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 12px 14px; }
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: 650; overflow-wrap: anywhere; }
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 { border-top: var(--ntr-line); padding: 0 16px 16px; }
114
- .dwa-subsection { padding-top: 16px; }
115
- .dwa-subsection + .dwa-subsection { margin-top: 16px; border-top: var(--ntr-line); }
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 { border: var(--ntr-line); border-radius: var(--ntr-radius); background: var(--dsw-alias-bg-layer-1, #fff); }
141
- .dwa-add-toggle { width: 100%; border: 0; background: transparent; color: inherit; padding: 13px 16px; display: flex; align-items: center; justify-content: space-between; gap: 12px; text-align: left; cursor: pointer; border-radius: var(--ntr-radius); }
142
- .dwa-add-toggle:hover { background: var(--dsw-alias-interactive-bg-active, #f9fafb); }
143
- .dwa-add-content { border-top: var(--ntr-line); padding: 14px 16px 16px; }
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: 6px; width: 100%; border: none; background: none; padding: 5px 6px; border-radius: 5px; cursor: pointer; color: inherit; font: inherit; font-size: var(--ntr-font-sm); text-align: left; }
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: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
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: 1px solid var(--dsw-alias-border-l2, #d1d5db); overflow: hidden; vertical-align: middle; }
196
- .dwa-seg > button { min-height: 30px; font: inherit; font-size: var(--ntr-font-sm); color: inherit; border: none; background: var(--dsw-alias-bg-layer-1, #fff); padding: 4px 14px; cursor: pointer; }
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-accent); color: var(--dsw-alias-label-primary-foreground, #fff); font-weight: 550; }
200
- .dwa-preference-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 0; }
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: 14px; }
226
- .dwa-page-header { flex-direction: column; }
227
- .dwa-page-actions { width: 100%; }
228
- .dwa-page-actions .dwa-btn { width: 100%; }
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-subsection-head, .dwa-preference-row { align-items: flex-start; flex-direction: column; }
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-card" style={{ paddingTop: 4, paddingBottom: 4 }}>
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
- /** Settings configuration section for ACP agents. */
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
- // The agent list section opens by default. `addTarget === undefined` means
839
- // "follow the first available preset"; an explicit chip click pins the
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 智能体', 'NTR Agents')}</h2>
877
+ <h2 className="dwa-page-title">{acp.label('NTR 设置', 'NTR settings')}</h2>
852
878
  <p className="dwa-page-description">
853
- {acp.label('管理智能体连接、可用模型与对话显示偏好。', 'Manage agent connections, available models, and conversation display preferences.')}
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
- <div className="dwa-page-actions">
866
- <button
867
- className="dwa-btn"
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
- </div>
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
- className="dwa-btn dwa-btn-primary"
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('拒绝', 'Dismiss')}
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
- {/* ACP 模式: the agent preset the new-session picker offers.
925
-
926
- Nothing is installed from here — the preset is a declaration this
927
- package ships in its OWN bundle patch (`presets/acp/acp.patch.yml`),
928
- so the card is a read-only window onto the harness roster: it exists
929
- to say why the mode is missing when its activation failed. */}
930
- {ui.preset !== undefined && (
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
- {/* Connected ACP agents: one compact row per agent, details on demand */}
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
- {configured.length === 0 && (
966
- <div className="dwa-card" style={{ textAlign: 'center', color: 'var(--ntr-muted)', padding: 20 }}>
967
- {acp.label(
968
- '尚未启用任何 Agent。从下方「添加智能体」点一个检测到的预设(会展开预填命令的创建卡),再点「保存并启用」;也可以直接新建自定义 Agent。',
969
- 'No agents configured yet. Pick a detected preset from "Add agent" below (it opens a creation card prefilled with its command), then click "Save & Enable" — or create a custom one from scratch.',
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
- {/* Agent list: available presets plus the from-scratch creation form,
992
- open by default so the page answers "what can I enable" without a
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
- <button type="button" className="dwa-add-toggle" aria-expanded={addOpen} onClick={() => setAddOpen(open => !open)}>
997
- <span>
998
- <span className="dwa-section-title">{acp.label('添加智能体', 'Add agent')}</span>
999
- <span className="dwa-section-copy" style={{ display: 'block' }}>{acp.label('从预设快速启用,或创建自定义连接。', 'Enable a preset or create a custom connection.')}</span>
1000
- </span>
1001
- <span className="dwa-caret-btn" aria-hidden><span className="dwa-caret">{addOpen ? '▾' : '▸'}</span></span>
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 === '' ? `custom-${createNonce}` : 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
- setCreateNonce(nonce => nonce + 1)
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
- {/* Approval deadlines: the two windows that bound an ask nobody
1052
- answered. Placed above the appearance card because it changes what
1053
- happens to a turn, and that card only decorates a nameplate. */}
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-card" style={{ paddingTop: 4, paddingBottom: 4 }}>
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 dirty = current !== undefined && !payloadEquals(payload, current)
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 currentModel = ui.modelCatalogs[detected.id]?.catalog.model?.currentValue
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
- <button
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-success">{acp.label('默认', 'Default')}</span>}
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>{currentModel === undefined ? acp.label('未探测模型', 'Model not probed') : `${acp.label('模型', 'Model')}: ${currentModel}`}</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 style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
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
- {/* 基础参数: id / display name / command / args — the preset chip already
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
- '参数直接跟在程序后面即可;路径里含空格时用引号包住整条路径,例如 "C:\\Program Files\\nodejs\\node.exe" --acp。',
1617
- 'Arguments go right after the program; quote a whole path that contains spaces, e.g. "C:\\Program Files\\nodejs\\node.exe" --acp.',
1639
+ '含空格的程序路径请加引号。',
1640
+ 'Quote the program path if it contains spaces.',
1618
1641
  )}
1619
1642
  </span>
1620
1643
  </div>
1621
1644
  </div>
1622
1645
 
1623
- {/* 高级参数: collapsed by default, hidden with `display: none` (never
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 style={advancedOpen ? undefined : { display: 'none' }}>
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
- 'The process is parked once it has been idle this long (the session is kept and resumes on the next message).',
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
- '这些 MCP 服务器会在建会话时交给 agent,由 agent 自己拉起并使用(stdio 需填 command,可选 args / env;http 与 sse 需填 url,可选 headers)。agent 自己配置的 MCP 服务器不受影响,这里只是额外追加。',
1799
- 'These MCP servers are handed to the agent when a session is created; the agent launches and uses them itself (stdio needs command, with optional args / env; http and sse need url, with optional headers). The MCP servers the agent configures for itself are unaffected — this only adds more.',
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('保存', 'Save')
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
- '对话里的 NTR 会话以该 DSH 会话选中的工作区为工作目录(agent 进程也从那里启动);此项只在 /acp 直连会话与宿主未上报工作区时生效。',
1900
- 'An NTR session inside a conversation runs \u2014 and starts its agent process \u2014 in that DSH session\u2019s own workspace; this value applies to /acp-direct sessions and when the host reports no workspace.',
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('(NTR 座位 / 芯片的下拉列表)', '(the NTR seat and chip pickers)')}
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('还没有选择模型(点左侧的模型加入)', 'Nothing selected yet — pick models on the left')}
2480
+ {acp.label('尚未选择模型,从「全部模型」中加入。', 'No models selected. Add from All models.')}
2492
2481
  </span>
2493
2482
  )}
2494
2483
  {selectedChoices.map(choice => (