dsh-ntr 0.1.4 → 0.1.6

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.
@@ -188,8 +188,28 @@ const STYLE = `
188
188
  .dwa-seg > button.is-on { background: var(--ntr-accent); color: var(--dsw-alias-button-primary-invert, #fff); font-weight: 550; }
189
189
  .dwa-preference-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 0; }
190
190
  .dwa-preference-row + .dwa-preference-row { border-top: var(--ntr-line); }
191
+ /* A control that needs the whole card: the label sits above it, not beside
192
+ it. A right-aligned segmented control squeezed its labels into two lines
193
+ each (中/止, 自动推/进), which read as broken rather than as choices. */
194
+ .dwa-preference-block { padding: 12px 0; }
195
+ .dwa-preference-block + .dwa-preference-block { border-top: var(--ntr-line); }
191
196
  .dwa-preference-label { font-size: var(--ntr-font-sm); font-weight: 600; }
192
197
  .dwa-preference-copy { margin-top: 2px; color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
198
+ .dwa-seg-wide { display: flex; width: 100%; margin-top: 10px; }
199
+ .dwa-seg-wide > button { flex: 1; }
200
+ /* The wait window, as a slider: the number is the headline, the rungs are the
201
+ scale under the track. */
202
+ .dwa-deadline-value { margin-top: 8px; font-size: 20px; font-weight: 600; }
203
+ .dwa-deadline-range { width: 100%; margin-top: 6px; accent-color: var(--ntr-accent); }
204
+ .dwa-deadline-range:disabled { opacity: 0.4; }
205
+ .dwa-deadline-ticks { display: flex; justify-content: space-between; margin-top: 2px; color: var(--ntr-muted); font-size: 10px; }
206
+ .dwa-deadline-ticks > span { white-space: nowrap; }
207
+ /* The facts that make a number here meaningless, in warning ink rather than
208
+ the muted gray every other hint uses: the value the config file is
209
+ outranking, on the row it belongs to. */
210
+ .dwa-deadline-flag { margin-top: 4px; font-size: var(--ntr-font-xs); font-weight: 550; color: var(--ntr-warn); }
211
+ .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; }
212
+ .dwa-deadline-reset { display: flex; justify-content: flex-end; margin-top: 8px; }
193
213
  @media (max-width: 640px) {
194
214
  .dwa-root { padding: 14px; }
195
215
  .dwa-page-header { flex-direction: column; }
@@ -199,6 +219,11 @@ const STYLE = `
199
219
  .dwa-model-search { flex: 1 1 auto; width: auto; }
200
220
  .dwa-subsection-head, .dwa-preference-row { align-items: flex-start; flex-direction: column; }
201
221
  .dwa-default-hint { align-items: flex-start; flex-direction: column; }
222
+ /* The slider's scale: up to eight rungs with full unit words ("15 分钟")
223
+ need a smaller face before they can collide on a narrow panel. The rung
224
+ count follows the mode (自动推进 adds 不等待), and the row is a flex
225
+ space-between, so a shorter scale just spreads wider. */
226
+ .dwa-deadline-ticks { font-size: 9px; }
202
227
  }
203
228
  @media (prefers-reduced-motion: reduce) {
204
229
  .dwa-root *, .dwa-root *::before, .dwa-root *::after { transition: none !important; }
@@ -260,6 +285,76 @@ function sameStringMap(a, b) {
260
285
  return false;
261
286
  return keys.every(key => a[key] === b[key]);
262
287
  }
288
+ /**
289
+ * Canonical comparable form of the MCP server list. Unset and empty mean the
290
+ * same thing host-side (no extra MCP servers), so they share a key — treating
291
+ * them as different would leave a card permanently dirty.
292
+ *
293
+ * The list is compared by CANONICAL JSON (object keys sorted, the servers
294
+ * themselves sorted by name): reformatting the text — reordering keys,
295
+ * re-indenting, listing the servers in another order — is not a change. The
296
+ * ARGV array inside a server keeps its order, because there it is the command
297
+ * line. The browser half needs no copy of the host's per-server validation: a
298
+ * list the host would reject simply never matches, and the host's own error is
299
+ * what the user sees.
300
+ */
301
+ function mcpKey(servers) {
302
+ if (servers === undefined || servers.length === 0)
303
+ return 'none';
304
+ const canonical = servers
305
+ .map(canonicalJson)
306
+ .sort((a, b) => {
307
+ const nameA = String(a.name ?? '');
308
+ const nameB = String(b.name ?? '');
309
+ return nameA < nameB ? -1 : nameA > nameB ? 1 : 0;
310
+ });
311
+ return JSON.stringify(canonical);
312
+ }
313
+ /** One value in canonical JSON form: object keys sorted, arrays kept in order. */
314
+ function canonicalJson(value) {
315
+ if (Array.isArray(value))
316
+ return value.map(canonicalJson);
317
+ if (value === null || typeof value !== 'object')
318
+ return value;
319
+ const entries = Object.entries(value)
320
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
321
+ .map(([key, entry]) => [key, canonicalJson(entry)]);
322
+ return Object.fromEntries(entries);
323
+ }
324
+ /**
325
+ * The MCP server list as the form holds it: the raw JSON text the user typed.
326
+ *
327
+ * Parsing happens on the way OUT (`buildPayload`), never on the way in, so a
328
+ * half-typed value never throws inside a render. An unparseable text yields
329
+ * `undefined`, which the form reports as invalid (`mcpInvalid`) and refuses to
330
+ * save — better than sending a truncated list.
331
+ */
332
+ function parseMcpServers(text) {
333
+ const trimmed = text.trim();
334
+ if (trimmed === '')
335
+ return undefined;
336
+ try {
337
+ const parsed = JSON.parse(trimmed);
338
+ if (!Array.isArray(parsed))
339
+ return undefined;
340
+ return parsed;
341
+ }
342
+ catch {
343
+ return undefined;
344
+ }
345
+ }
346
+ /** Whether the MCP server text parses as a list at all (the host validates the rest). */
347
+ function mcpTextValid(text) {
348
+ const trimmed = text.trim();
349
+ if (trimmed === '')
350
+ return true;
351
+ try {
352
+ return Array.isArray(JSON.parse(trimmed));
353
+ }
354
+ catch {
355
+ return false;
356
+ }
357
+ }
263
358
  /**
264
359
  * Detected presets split by configured-ness, plus configured agents missing
265
360
  * from detection (custom / legacy persisted defs) synthesized into card
@@ -332,7 +427,7 @@ function persistedDef(detected, activeAgent) {
332
427
  * about; `exposedModels === undefined` means "offer no variants", which is the
333
428
  * same state as an empty list.
334
429
  */
335
- function payloadEquals(payload, current) {
430
+ export function payloadEquals(payload, current) {
336
431
  if (payload.command !== current.command)
337
432
  return false;
338
433
  if ((payload.title ?? payload.id) !== (current.title ?? current.id))
@@ -349,6 +444,8 @@ function payloadEquals(payload, current) {
349
444
  return false;
350
445
  if (exposureKey(payload.exposedModels) !== exposureKey(current.exposedModels))
351
446
  return false;
447
+ if (mcpKey(payload.mcpServers) !== mcpKey(current.mcpServers))
448
+ return false;
352
449
  if ((payload.autoDiscoverModels !== false) !== (current.autoDiscoverModels !== false))
353
450
  return false;
354
451
  // An absent field means "use the host default" — which is what `current`
@@ -377,6 +474,226 @@ export function permissionLedgerCard(ui) {
377
474
  const visible = ui.permissions.length > 0;
378
475
  return { visible, fellBackFromConversation: visible && ui.approvalRouted };
379
476
  }
477
+ /**
478
+ * The ONE wait window, as the rungs the slider snaps to.
479
+ *
480
+ * Shortest wait first, and `0` (不等待) FIRST: the slider reads left to right
481
+ * as "wait longer", and "do not wait at all" is the shortest wait there is.
482
+ * Never expiring is NOT on this scale — that is the `wait` MODE's business,
483
+ * and having 「一直等待」 in both places made the card say the same thing twice
484
+ * in two ways.
485
+ *
486
+ * A window the config file names that is not on this ladder (45 seconds, say)
487
+ * still runs as configured: the card prints the exact number and parks the
488
+ * thumb on the nearest rung, so the slider never pretends a number the host
489
+ * is not using.
490
+ */
491
+ const DEADLINE_LADDER = [
492
+ 0,
493
+ 30_000,
494
+ 60_000,
495
+ 300_000,
496
+ 900_000,
497
+ 1_800_000,
498
+ 3_600_000,
499
+ 21_600_000,
500
+ ];
501
+ /**
502
+ * The shortest REAL wait — the first rung above 不等待.
503
+ *
504
+ * Hand-copied from the host half (`APPROVAL_DEADLINE_MIN_WAIT_MS` in
505
+ * `src/impl.ts`): the browser half may not import the host module, so the
506
+ * equivalence is pinned by a test instead (see `tests/acp-view.spec.ts`).
507
+ */
508
+ export const DEADLINE_MIN_WAIT_MS = 30_000;
509
+ /**
510
+ * The window the card shows under a mode.
511
+ *
512
+ * 不等待 (`0`) is the **auto** mode's rung: it answers with the option the
513
+ * agent's own kinds imply, so the agent CONTINUES. `abort` has no such rung —
514
+ * refusing every ask the instant it arrives stops the agent at every gate it
515
+ * meets, which is not a timeout but a wall. So under `abort` the window floors
516
+ * to the shortest real wait, and the card never shows a zero it cannot run.
517
+ *
518
+ * `undefined` (the `wait` mode) stays undefined: there is no window at all.
519
+ */
520
+ export function deadlineValueFor(mode, ms) {
521
+ if (ms === undefined)
522
+ return undefined;
523
+ return mode === 'abort' ? Math.max(ms, DEADLINE_MIN_WAIT_MS) : ms;
524
+ }
525
+ /**
526
+ * The rungs a mode offers.
527
+ *
528
+ * 不等待 (`0`) is the **auto** mode's rung: it answers with the option the
529
+ * agent's own kinds imply, so the agent CONTINUES. `abort` has no such rung —
530
+ * refusing every ask the instant it arrives stops the agent at every gate it
531
+ * meets, which is not a timeout but a wall. So `abort` starts at the shortest
532
+ * real wait.
533
+ */
534
+ function deadlineLadder(mode) {
535
+ return mode === 'auto' ? DEADLINE_LADDER : DEADLINE_LADDER.filter(ms => ms > 0);
536
+ }
537
+ /**
538
+ * The rung the slider shows for a window. Exact hits keep their rung; anything
539
+ * else takes the nearest one. A tie goes to the SHORTER wait: the thumb must
540
+ * never promise more patience than the window has.
541
+ */
542
+ export function deadlineSliderIndex(ms, mode = 'auto') {
543
+ const ladder = deadlineLadder(mode);
544
+ const exact = ladder.indexOf(ms);
545
+ if (exact >= 0)
546
+ return exact;
547
+ let best = 0;
548
+ let bestDistance = Number.POSITIVE_INFINITY;
549
+ for (let i = 0; i < ladder.length; i++) {
550
+ const distance = Math.abs(ms - ladder[i]);
551
+ if (distance < bestDistance) {
552
+ bestDistance = distance;
553
+ best = i;
554
+ }
555
+ }
556
+ return best;
557
+ }
558
+ /** The three modes, in the order a user reads them: refuse, answer, wait. */
559
+ const DEADLINE_MODES = ['abort', 'auto', 'wait'];
560
+ /** `5 分钟` / `30 秒` / `不等待` — the readout and the tick label, one formatter. */
561
+ function deadlineText(ms, acp) {
562
+ if (ms <= 0)
563
+ return acp.label('不等待', 'No wait');
564
+ const seconds = Math.round(ms / 1000);
565
+ if (seconds < 60)
566
+ return acp.label(`${String(seconds)} 秒`, `${String(seconds)}s`);
567
+ const minutes = Math.round(seconds / 60);
568
+ if (minutes < 60)
569
+ return acp.label(`${String(minutes)} 分钟`, `${String(minutes)}m`);
570
+ const hours = Math.round(minutes / 60);
571
+ return acp.label(`${String(hours)} 小时`, `${String(hours)}h`);
572
+ }
573
+ /** The mode's label — one formatter, so the picker and the override note agree. */
574
+ function modeText(mode, acp) {
575
+ if (mode === 'abort')
576
+ return acp.label('超时中止', 'Abort');
577
+ if (mode === 'auto')
578
+ return acp.label('自动推进', 'Auto');
579
+ return acp.label('一直等待', 'Wait');
580
+ }
581
+ /**
582
+ * The mode block: what happens when nobody answers.
583
+ *
584
+ * Three values and no more — refuse, answer, wait — so the picker is a
585
+ * segmented control, not a dropdown. The buttons stretch across the whole
586
+ * card: a right-aligned segmented control squeezed the labels into two lines
587
+ * each (中/止, 自动推/进), which read as broken rather than as choices.
588
+ *
589
+ * The copy under the buttons says what the SELECTED mode does, in one
590
+ * sentence — the labels alone ("超时中止") cannot. `auto` carries a warning of
591
+ * its own: it approves things nobody looked at.
592
+ */
593
+ function DeadlineModeBlock(props) {
594
+ const { acp, mode, override } = props;
595
+ const copy = mode === 'wait'
596
+ ? acp.label('一直等待应答,不设超时时间。', 'Wait until you answer — no deadline.')
597
+ : mode === 'auto'
598
+ ? acp.label('超过设定时间未应答,自动代替你选择一个选项让 agent 继续执行。', 'Past the deadline, an option is chosen for you and the agent continues.')
599
+ : acp.label('超过设定时间未应答,自动拒绝该操作,agent 继续执行。', 'Past the deadline, the operation is refused and the agent continues.');
600
+ return (_jsxs("div", { className: "dwa-preference-block", children: [_jsx("div", { className: "dwa-preference-label", children: acp.label('超时处理方式', 'When nobody answers') }), _jsx("div", { className: "dwa-seg dwa-seg-wide", children: DEADLINE_MODES.map(option => (_jsx("button", { type: "button", className: mode === option ? 'is-on' : '', onClick: () => void acp.setApprovalTimeoutMode(option), children: modeText(option, acp) }, option))) }), _jsx("div", { className: "dwa-preference-copy", children: copy }), mode === 'auto' && (_jsx("div", { className: "dwa-deadline-warn", children: acp.label('请确保你的 agent 值得信任,请谨慎选择。', 'Only pick this if you trust your agent — choose it carefully.') })), override !== undefined && (_jsxs("div", { className: "dwa-deadline-flag", children: ["\u26A0 ", override] }))] }));
601
+ }
602
+ /**
603
+ * The duration block: how long to wait before the deadline fires.
604
+ *
605
+ * ONE slider, because there is ONE window: an ask in the conversation and an
606
+ * ask in the settings ledger are the same question, so they get the same
607
+ * answer. The slider's rungs are the windows people actually pick; a window
608
+ * the config file names outside them still runs as configured (the readout
609
+ * prints the exact number and the thumb parks on the nearest rung).
610
+ *
611
+ * `value === undefined` is the `wait` mode: there is no deadline at all, so
612
+ * the scale has nothing to say — the slider is not drawn, and the readout
613
+ * names the state that is actually in force. The stored window is NOT touched
614
+ * by that (the mode write carries only the mode), so switching back restores
615
+ * it.
616
+ */
617
+ function DeadlineSliderBlock(props) {
618
+ const { acp, value, mode, override } = props;
619
+ // 不等待 belongs to `auto`. Under `abort` the window floors to the shortest
620
+ // real wait, so the card never shows a zero it cannot run.
621
+ const effective = deadlineValueFor(mode, value);
622
+ const ladder = deadlineLadder(mode);
623
+ const index = effective === undefined ? 0 : deadlineSliderIndex(effective, mode);
624
+ const copy = effective === undefined
625
+ ? acp.label('选择「一直等待」时不设超时时长。', '“Wait” sets no deadline.')
626
+ : effective <= 0
627
+ ? acp.label('不设等待时间,请求到达即按上方所选的方式处理。', 'No wait: the ask is settled on arrival, by whatever you picked above.')
628
+ : acp.label('超过该时长仍未应答,将按上方所选的方式处理。', 'Past this, whatever you picked above is what happens.');
629
+ return (_jsxs("div", { className: "dwa-preference-block", children: [_jsx("div", { className: "dwa-preference-label", children: acp.label('等待时长', 'How long to wait') }), _jsx("div", { className: "dwa-deadline-value", children: effective === undefined ? acp.label('一直等待', 'Always') : deadlineText(effective, acp) }), effective !== undefined && (_jsxs(_Fragment, { children: [_jsx("input", { className: "dwa-deadline-range", type: "range", min: 0, max: ladder.length - 1, step: 1, value: index, "aria-label": acp.label('等待时长', 'How long to wait'), onChange: event => {
630
+ const next = ladder[Number(event.target.value)];
631
+ if (next !== undefined && next !== effective)
632
+ void acp.setApprovalDeadline(next);
633
+ } }), _jsx("div", { className: "dwa-deadline-ticks", children: ladder.map(rung => (_jsx("span", { children: deadlineText(rung, acp) }, rung))) })] })), _jsx("div", { className: "dwa-preference-copy", children: copy }), override !== undefined && (_jsxs("div", { className: "dwa-deadline-flag", children: ["\u26A0 ", override] }))] }));
634
+ }
635
+ /**
636
+ * The facts the card has to show, decided from what the host reported.
637
+ *
638
+ * The card carries TWO controls, and they are orthogonal: one window (how
639
+ * long) and one mode (what happens). An override can sit on either of them,
640
+ * so each is decided on its own.
641
+ *
642
+ * `overridden` is what gates the reset button. A baseline that AGREES with the
643
+ * running value is not an override, and offering a reset for it would be a
644
+ * button that does nothing (observed live).
645
+ */
646
+ export function approvalDeadlineFacts(deadlines) {
647
+ const configured = deadlines.configured?.permissionTimeoutMs;
648
+ const baseline = configured === undefined || configured === deadlines.permissionTimeoutMs
649
+ ? undefined
650
+ : configured;
651
+ const modeBaseline = deadlines.configuredMode !== undefined && deadlines.configuredMode !== deadlines.mode
652
+ ? deadlines.configuredMode
653
+ : undefined;
654
+ return {
655
+ baseline,
656
+ modeBaseline,
657
+ overridden: baseline !== undefined || modeBaseline !== undefined,
658
+ };
659
+ }
660
+ /**
661
+ * The approval-deadline card.
662
+ *
663
+ * TWO controls, and they are orthogonal: one window (how long) and one mode
664
+ * (what happens). One window covers both places an ask can wait — the
665
+ * conversation's approval card and the settings panel's ledger — because they
666
+ * ask the same question and the answer should not differ by where the card
667
+ * happens to be drawn.
668
+ *
669
+ * The two controls are ONE system, and the card says so in a sentence: the
670
+ * window says how long to wait, the mode says what happens when nobody
671
+ * answered. That sentence is the thing the previous three-row layout kept
672
+ * missing.
673
+ *
674
+ * The facts that make a number here meaningless are shown, not swallowed: the
675
+ * deployment baseline this card outranks. Each rides on the control it belongs
676
+ * to, and the reset button appears only when something is actually being
677
+ * outranked.
678
+ */
679
+ function ApprovalDeadlinesCard(props) {
680
+ const { acp, ui } = props;
681
+ const deadlines = ui.approvalDeadlines;
682
+ // Older host half: no card rather than a number this browser invented.
683
+ if (deadlines === undefined)
684
+ return null;
685
+ const facts = approvalDeadlineFacts(deadlines);
686
+ const baselineNote = facts.baseline === undefined
687
+ ? undefined
688
+ : acp.label(`配置文件(cordis.patch.yml)设为 ${deadlineText(facts.baseline, acp)},此处设置优先。`, `The config file (cordis.patch.yml) asks for ${deadlineText(facts.baseline, acp)}; the value set here wins.`);
689
+ const modeNote = facts.modeBaseline === undefined
690
+ ? undefined
691
+ : acp.label(`配置文件(cordis.patch.yml)设为「${modeText(facts.modeBaseline, acp)}」,此处设置优先。`, `The config file (cordis.patch.yml) asks for "${modeText(facts.modeBaseline, acp)}"; the value set here wins.`);
692
+ // In `wait` mode there is no deadline at all, so the slider shows the one
693
+ // thing that is true: no deadline.
694
+ const waitMode = deadlines.mode === 'wait';
695
+ return (_jsxs("section", { className: "dwa-section", children: [_jsx("div", { className: "dwa-section-heading", children: _jsxs("div", { children: [_jsx("h3", { className: "dwa-section-title", children: acp.label('审批时限', 'Approval deadlines') }), _jsx("p", { className: "dwa-section-copy", children: acp.label('权限请求的等待时长与超时处理方式。', 'How long to wait for a permission request, and what happens when nobody answers.') })] }) }), _jsxs("div", { className: "dwa-card", style: { paddingTop: 4, paddingBottom: 4 }, children: [_jsx(DeadlineModeBlock, { acp: acp, mode: deadlines.mode, override: modeNote }), _jsx(DeadlineSliderBlock, { acp: acp, value: waitMode ? undefined : deadlines.permissionTimeoutMs, mode: deadlines.mode, override: baselineNote }), facts.overridden && (_jsx("div", { className: "dwa-deadline-reset", children: _jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", onClick: () => void acp.resetApprovalDeadlines(), children: acp.label('恢复配置文件的值', 'Use config file value') }) }))] })] }));
696
+ }
380
697
  /** Settings configuration section for ACP agents. */
381
698
  export function AcpSettingsSection(props) {
382
699
  const acp = props.acp;
@@ -394,7 +711,7 @@ export function AcpSettingsSection(props) {
394
711
  return (_jsxs("div", { className: "dwa-root", children: [_jsxs("header", { className: "dwa-page-header", children: [_jsxs("div", { className: "dwa-page-heading", children: [_jsx("h2", { className: "dwa-page-title", children: acp.label('NTR 智能体', 'NTR Agents') }), _jsx("p", { className: "dwa-page-description", children: acp.label('管理智能体连接、可用模型与对话显示偏好。', 'Manage agent connections, available models, and conversation display preferences.') }), _jsxs("div", { className: "dwa-page-status", children: [_jsx("span", { className: "dwa-dot", style: { background: ui.stream === 'live' ? '#22c55e' : ui.stream === 'down' ? '#ef4444' : '#9ca3af' }, title: ui.stream }), _jsx("span", { children: streamText(ui.stream, acp) }), ui.detectSummary !== undefined && _jsxs("span", { children: ["\u00B7 ", ui.detectSummary] })] })] }), _jsx("div", { className: "dwa-page-actions", children: _jsx("button", { className: "dwa-btn", onClick: () => {
395
712
  void acp.resync();
396
713
  void acp.detectAgents();
397
- }, disabled: ui.detecting, children: ui.detecting ? acp.label('检测中...', 'Detecting...') : acp.label('重新检测与刷新', 'Re-detect & Refresh') }) })] }), ledger.visible && (_jsxs("div", { className: "dwa-card dwa-card-warn", style: { marginBottom: 16 }, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 8 }, children: acp.label('待处理权限请求', 'Pending Permission Requests') }), ledger.fellBackFromConversation && (_jsx("div", { className: "dwa-hint", style: { marginBottom: 8 }, children: acp.label('这些询问没有进入对话(审批服务本次不可用,或会话没有对话身份),请在此回答。', 'These asks did not reach the conversation (the approval service was unavailable, or the session has no chat identity) — answer them here.') })), ui.permissions.map(permission => (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6, flexWrap: 'wrap' }, children: [_jsx("span", { style: { flex: 1, minWidth: 160 }, children: permission.title }), permission.options.map(option => (_jsx("button", { className: "dwa-btn dwa-btn-primary", onClick: () => void acp.respondPermission(permission.requestId, option.optionId), children: option.name }, option.optionId))), _jsx("button", { className: "dwa-btn", onClick: () => void acp.respondPermission(permission.requestId), children: acp.label('拒绝', 'Dismiss') })] }, permission.requestId)))] })), ui.notice !== undefined && (_jsxs("div", { className: "dwa-card dwa-card-warn", style: { marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }, children: [_jsx("span", { style: { flex: 1 }, children: ui.notice }), _jsx("button", { className: "dwa-btn", onClick: () => acp.clearNotice(), children: "\u2715" })] })), ui.preset !== undefined && (_jsxs("div", { className: "dwa-card dwa-mode-banner", children: [_jsxs("div", { className: "dwa-mode-title", children: [acp.label('NTR 模式', 'NTR mode'), _jsx("span", { className: "dwa-badge", children: ui.preset.id })] }), _jsx("span", { className: `dwa-badge ${ui.preset.installed ? 'dwa-badge-success' : 'dwa-badge-warn'}`, children: ui.preset.installed ? acp.label('已就绪', 'Ready') : acp.label('需检查', 'Needs attention') }), _jsxs("div", { className: "dwa-mode-copy", children: [ui.preset.installed
714
+ }, disabled: ui.detecting, children: ui.detecting ? acp.label('检测中...', 'Detecting...') : acp.label('重新检测与刷新', 'Re-detect & Refresh') }) })] }), ledger.visible && (_jsxs("div", { className: "dwa-card dwa-card-warn", style: { marginBottom: 16 }, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 8 }, children: acp.label('待处理权限请求', 'Pending Permission Requests') }), ledger.fellBackFromConversation && (_jsx("div", { className: "dwa-hint", style: { marginBottom: 8 }, children: acp.label('这些询问没有进入对话(审批服务本次不可用、问题从未到达对话,或会话没有对话身份),请在此回答。', 'These asks did not reach the conversation (the approval service was unavailable, the question never reached the conversation, or the session has no chat identity) — answer them here.') })), ui.permissions.map(permission => (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6, flexWrap: 'wrap' }, children: [_jsx("span", { style: { flex: 1, minWidth: 160 }, children: permission.title }), permission.options.map(option => (_jsx("button", { className: "dwa-btn dwa-btn-primary", onClick: () => void acp.respondPermission(permission.requestId, option.optionId), children: option.name }, option.optionId))), _jsx("button", { className: "dwa-btn", onClick: () => void acp.respondPermission(permission.requestId), children: acp.label('拒绝', 'Dismiss') })] }, permission.requestId)))] })), ui.notice !== undefined && (_jsxs("div", { className: "dwa-card dwa-card-warn", style: { marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }, children: [_jsx("span", { style: { flex: 1 }, children: ui.notice }), _jsx("button", { className: "dwa-btn", onClick: () => acp.clearNotice(), children: "\u2715" })] })), ui.preset !== undefined && (_jsxs("div", { className: "dwa-card dwa-mode-banner", children: [_jsxs("div", { className: "dwa-mode-title", children: [acp.label('NTR 模式', 'NTR mode'), _jsx("span", { className: "dwa-badge", children: ui.preset.id })] }), _jsx("span", { className: `dwa-badge ${ui.preset.installed ? 'dwa-badge-success' : 'dwa-badge-warn'}`, children: ui.preset.installed ? acp.label('已就绪', 'Ready') : acp.label('需检查', 'Needs attention') }), _jsxs("div", { className: "dwa-mode-copy", children: [ui.preset.installed
398
715
  ? acp.label('已加入宿主模式列表,新建会话时选择「NTR 模式」即可使用。', 'Added to the harness roster. Pick “NTR mode” when creating a session.')
399
716
  : acp.label('宿主模式列表中没有可用的「NTR 模式」,请检查预设声明。', 'The harness roster has no usable “NTR mode”. Check the preset declaration.'), _jsx("code", { className: "dwa-mode-source dwa-mono", children: ui.preset.source })] }), ui.preset.problem !== undefined && (_jsx("div", { className: "dwa-hint", style: { gridColumn: '1 / -1', color: 'var(--ntr-warn)' }, children: acp.label(`宿主报告:${ui.preset.problem}`, `Harness reported: ${ui.preset.problem}`) }))] })), _jsxs("section", { className: "dwa-section", children: [_jsx("div", { className: "dwa-section-heading", children: _jsxs("div", { children: [_jsxs("h3", { className: "dwa-section-title", children: [acp.label('已接入的智能体', 'Connected agents'), _jsx("span", { className: "dwa-badge", children: configured.length })] }), _jsx("p", { className: "dwa-section-copy", children: acp.label('展开智能体可管理模型、启动命令与工作区。', 'Expand an agent to manage its models, launch command, and workspace.') })] }) }), configured.length === 0 && (_jsx("div", { className: "dwa-card", style: { textAlign: 'center', color: 'var(--ntr-muted)', padding: 20 }, children: acp.label('尚未启用任何 Agent。从下方「添加智能体」点一个检测到的预设(会展开预填命令的创建卡),再点「保存并启用」;也可以直接新建自定义 Agent。', '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.') })), _jsx("div", { className: "dwa-agent-list", children: configured.map(({ info, synthesized }) => {
400
717
  const activeAgent = ui.agents.find(a => a.id === info.id);
@@ -406,7 +723,7 @@ export function AcpSettingsSection(props) {
406
723
  // instead of keeping the just-saved values in the form.
407
724
  setAddTarget(undefined);
408
725
  setCreateNonce(nonce => nonce + 1);
409
- } }, selected.id === '' ? `custom-${createNonce}` : selected.id)] }))] }) }), _jsxs("section", { className: "dwa-section", children: [_jsx("div", { className: "dwa-section-heading", children: _jsxs("div", { children: [_jsx("h3", { className: "dwa-section-title", children: acp.label('对话外观', 'Conversation appearance') }), _jsx("p", { className: "dwa-section-copy", children: acp.label('设置对话中 NTR 挂牌的进程存活提示。', 'Choose how the NTR tag indicates a running agent process.') })] }) }), _jsxs("div", { className: "dwa-card", style: { paddingTop: 4, paddingBottom: 4 }, children: [_jsxs("div", { className: "dwa-preference-row", children: [_jsxs("div", { children: [_jsx("div", { className: "dwa-preference-label", children: acp.label('挂牌光晕', 'Tag halo') }), _jsx("div", { className: "dwa-preference-copy", children: acp.label('呼吸为脉动光,常亮为恒定光。', 'Breathe pulses gently; steady keeps a constant glow.') })] }), _jsx("div", { className: "dwa-seg", children: ['breath', 'steady'].map(mode => (_jsx("button", { type: "button", className: ui.tagHalo.mode === mode ? 'is-on' : '', onClick: () => void acp.setTagHalo({ ...ui.tagHalo, mode }), children: mode === 'breath' ? acp.label('呼吸', 'Breathe') : acp.label('常亮', 'Steady') }, mode))) })] }), ui.tagHalo.mode === 'breath' && (_jsxs("div", { className: "dwa-preference-row", children: [_jsxs("div", { children: [_jsx("div", { className: "dwa-preference-label", children: acp.label('呼吸频率', 'Breathing speed') }), _jsx("div", { className: "dwa-preference-copy", children: acp.label('控制光晕的脉动速度。', 'Controls how quickly the halo pulses.') })] }), _jsx("div", { className: "dwa-seg", children: ['slow', 'normal', 'fast'].map(speed => (_jsx("button", { type: "button", className: ui.tagHalo.speed === speed ? 'is-on' : '', onClick: () => void acp.setTagHalo({ ...ui.tagHalo, speed }), children: speed === 'slow' ? acp.label('慢速', 'Slow') : speed === 'normal' ? acp.label('正常', 'Normal') : acp.label('快速', 'Fast') }, speed))) })] }))] })] })] }));
726
+ } }, selected.id === '' ? `custom-${createNonce}` : selected.id)] }))] }) }), _jsx(ApprovalDeadlinesCard, { acp: acp, ui: ui }), _jsxs("section", { className: "dwa-section", children: [_jsx("div", { className: "dwa-section-heading", children: _jsxs("div", { children: [_jsx("h3", { className: "dwa-section-title", children: acp.label('对话外观', 'Conversation appearance') }), _jsx("p", { className: "dwa-section-copy", children: acp.label('设置对话中 NTR 挂牌的进程存活提示。', 'Choose how the NTR tag indicates a running agent process.') })] }) }), _jsxs("div", { className: "dwa-card", style: { paddingTop: 4, paddingBottom: 4 }, children: [_jsxs("div", { className: "dwa-preference-row", children: [_jsxs("div", { children: [_jsx("div", { className: "dwa-preference-label", children: acp.label('挂牌光晕', 'Tag halo') }), _jsx("div", { className: "dwa-preference-copy", children: acp.label('呼吸为脉动光,常亮为恒定光。', 'Breathe pulses gently; steady keeps a constant glow.') })] }), _jsx("div", { className: "dwa-seg", children: ['breath', 'steady'].map(mode => (_jsx("button", { type: "button", className: ui.tagHalo.mode === mode ? 'is-on' : '', onClick: () => void acp.setTagHalo({ ...ui.tagHalo, mode }), children: mode === 'breath' ? acp.label('呼吸', 'Breathe') : acp.label('常亮', 'Steady') }, mode))) })] }), ui.tagHalo.mode === 'breath' && (_jsxs("div", { className: "dwa-preference-row", children: [_jsxs("div", { children: [_jsx("div", { className: "dwa-preference-label", children: acp.label('呼吸频率', 'Breathing speed') }), _jsx("div", { className: "dwa-preference-copy", children: acp.label('控制光晕的脉动速度。', 'Controls how quickly the halo pulses.') })] }), _jsx("div", { className: "dwa-seg", children: ['slow', 'normal', 'fast'].map(speed => (_jsx("button", { type: "button", className: ui.tagHalo.speed === speed ? 'is-on' : '', onClick: () => void acp.setTagHalo({ ...ui.tagHalo, speed }), children: speed === 'slow' ? acp.label('慢速', 'Slow') : speed === 'normal' ? acp.label('正常', 'Normal') : acp.label('快速', 'Fast') }, speed))) })] }))] })] })] }));
410
727
  }
411
728
  /** One row per agent: compact when collapsed, the full form when expanded. */
412
729
  function AgentRow({ detected, activeAgent, acp, isDefault, synthesized, newCustom = false, onCreated, }) {
@@ -441,7 +758,8 @@ function AgentRow({ detected, activeAgent, acp, isDefault, synthesized, newCusto
441
758
  const rootValues = form.roots.map(root => root.trim()).filter(root => root !== '');
442
759
  const rootsInvalid = form.rootsOpen && (rootValues.length === 0 || rootValues.some(root => !isAbsolutePathLike(root)));
443
760
  const idleInvalid = !idleValid(form.idleStr);
444
- const formIncomplete = splitCommandLine(form.commandLine).command === '' || !idValid || idTaken || cwdInvalid || rootsInvalid || idleInvalid;
761
+ const mcpInvalid = !mcpTextValid(form.mcpText);
762
+ const formIncomplete = splitCommandLine(form.commandLine).command === '' || !idValid || idTaken || cwdInvalid || rootsInvalid || idleInvalid || mcpInvalid;
445
763
  const payload = buildPayload(form, detected, newCustom);
446
764
  const current = persistedDef(detected, activeAgent);
447
765
  const dirty = current !== undefined && !payloadEquals(payload, current);
@@ -504,7 +822,7 @@ function AgentRow({ detected, activeAgent, acp, isDefault, synthesized, newCusto
504
822
  return (_jsxs("div", { className: `dwa-agent-card${open ? ' is-open' : ''}`, children: [_jsxs("div", { className: "dwa-agent-row", children: [_jsx("button", { type: "button", className: "dwa-caret-btn", "aria-expanded": open, onClick: () => setExpanded(value => !value), title: open ? acp.label('收起', 'Collapse') : acp.label('展开配置与模型', 'Expand configuration and models'), children: _jsx("span", { className: "dwa-caret", "aria-hidden": true, children: open ? '▾' : '▸' }) }), _jsxs("div", { className: "dwa-agent-summary", children: [_jsxs("div", { className: "dwa-agent-titleline", children: [_jsx("span", { className: "dwa-agent-name", title: detected.executablePath ?? detected.description, children: displayName }), isDefault && _jsx("span", { className: "dwa-badge dwa-badge-success", children: acp.label('默认', 'Default') }), !isPreset && !newCustom && _jsx("span", { className: "dwa-badge", children: acp.label('自定义', 'Custom') })] }), _jsxs("div", { className: "dwa-agent-meta", children: [state !== undefined && (_jsxs("span", { title: state.title ?? activeAgent?.state, style: { display: 'inline-flex', alignItems: 'center', gap: 6 }, children: [_jsx("span", { className: "dwa-dot", style: { background: state.color } }), state.text] })), configured && (_jsxs(_Fragment, { children: [state !== undefined && _jsx("span", { className: "dwa-agent-meta-separator", children: "\u00B7" }), _jsx("span", { children: currentModel === undefined ? acp.label('未探测模型', 'Model not probed') : `${acp.label('模型', 'Model')}: ${currentModel}` })] })), !synthesized && !newCustom && !detected.detected && (_jsxs("span", { style: { color: 'var(--ntr-warn)' }, title: detected.description, children: ["\u00B7 ", acp.label('未在 PATH 中检测到', 'Not in PATH')] }))] }), errorSummary !== undefined && (_jsx("div", { className: "dwa-error dwa-mono", style: { marginTop: 6, wordBreak: 'break-all' }, children: errorSummary }))] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [activeAgent?.state === 'error' && (_jsx("button", { className: "dwa-btn", disabled: testing, onClick: () => void handleTest(), children: testing ? acp.label('测试中...', 'Testing...') : acp.label('测试', 'Test') })), configured && (_jsx(RowMenu, { acp: acp, canStop: activeAgent !== undefined && activeAgent.state !== 'stopped', onStop: () => void acp.stopAgent(detected.id),
505
823
  // The store already surfaced the failure as a notice; swallowing it
506
824
  // here only prevents an unhandled rejection.
507
- onRemove: async () => { await acp.deleteAgent(detected.id).catch(() => { }); } }))] })] }), _jsxs("div", { className: "dwa-agent-body", style: open ? undefined : { display: 'none' }, children: [stderr.length > 0 && _jsx("pre", { className: "dwa-stderr", children: stderr.join('\n') }), configured && (_jsxs("section", { className: "dwa-subsection", children: [_jsx("div", { className: "dwa-subsection-head", children: _jsxs("div", { children: [_jsx("h4", { className: "dwa-subsection-title", children: acp.label('模型与默认值', 'Models & defaults') }), _jsx("p", { className: "dwa-subsection-copy", children: acp.label('检测模型目录,并控制对话中可选择的模型。', 'Detect the model catalog and choose what appears in conversations.') })] }) }), _jsx(ModelCatalogBlock, { agentId: detected.id, detected: detected, acp: acp, active: open, exposed: form.exposed, onExposedChange: exposed => patch({ exposed }), autoDiscover: form.autoDiscoverModels, onAutoDiscoverChange: autoDiscoverModels => patch({ autoDiscoverModels }), isDefault: isDefault })] })), _jsxs("section", { className: "dwa-subsection", children: [_jsx("div", { className: "dwa-subsection-head", children: _jsxs("div", { children: [_jsx("h4", { className: "dwa-subsection-title", children: acp.label('连接配置', 'Connection') }), _jsx("p", { className: "dwa-subsection-copy", children: acp.label('设置显示名与启动命令,高级选项按需展开。', 'Set the display name and launch command; expand advanced options only when needed.') })] }) }), _jsx(AgentDetails, { acp: acp, detected: detected, form: form, patch: patch, isConfigured: configured, newCustom: newCustom, formIncomplete: formIncomplete, idValid: idValid, idTaken: idTaken, cwdInvalid: cwdInvalid, rootsInvalid: rootsInvalid, idleInvalid: idleInvalid, saving: saving, savedSuccess: savedSuccess, testing: testing, testResult: testResult, showSaveWarning: showSaveWarning, onUseDefault: handleUseDefault, onSave: () => void handleSave(), onTest: () => void handleTest() })] })] })] }));
825
+ onRemove: async () => { await acp.deleteAgent(detected.id).catch(() => { }); } }))] })] }), _jsxs("div", { className: "dwa-agent-body", style: open ? undefined : { display: 'none' }, children: [stderr.length > 0 && _jsx("pre", { className: "dwa-stderr", children: stderr.join('\n') }), configured && (_jsxs("section", { className: "dwa-subsection", children: [_jsx("div", { className: "dwa-subsection-head", children: _jsxs("div", { children: [_jsx("h4", { className: "dwa-subsection-title", children: acp.label('模型与默认值', 'Models & defaults') }), _jsx("p", { className: "dwa-subsection-copy", children: acp.label('检测模型目录,并控制对话中可选择的模型。', 'Detect the model catalog and choose what appears in conversations.') })] }) }), _jsx(ModelCatalogBlock, { agentId: detected.id, detected: detected, acp: acp, active: open, exposed: form.exposed, onExposedChange: exposed => patch({ exposed }), autoDiscover: form.autoDiscoverModels, onAutoDiscoverChange: autoDiscoverModels => patch({ autoDiscoverModels }), isDefault: isDefault })] })), _jsxs("section", { className: "dwa-subsection", children: [_jsx("div", { className: "dwa-subsection-head", children: _jsxs("div", { children: [_jsx("h4", { className: "dwa-subsection-title", children: acp.label('连接配置', 'Connection') }), _jsx("p", { className: "dwa-subsection-copy", children: acp.label('设置显示名与启动命令,高级选项按需展开。', 'Set the display name and launch command; expand advanced options only when needed.') })] }) }), _jsx(AgentDetails, { acp: acp, detected: detected, form: form, patch: patch, isConfigured: configured, newCustom: newCustom, formIncomplete: formIncomplete, idValid: idValid, idTaken: idTaken, cwdInvalid: cwdInvalid, rootsInvalid: rootsInvalid, idleInvalid: idleInvalid, mcpInvalid: mcpInvalid, saving: saving, savedSuccess: savedSuccess, testing: testing, testResult: testResult, showSaveWarning: showSaveWarning, onUseDefault: handleUseDefault, onSave: () => void handleSave(), onTest: () => void handleTest() })] })] })] }));
508
826
  }
509
827
  /**
510
828
  * Hand copy of `DEFAULT_TUNING.idleTimeoutMs` (src/bridge.ts): the browser
@@ -520,7 +838,7 @@ const DEFAULT_IDLE_RELEASE_MS = 1_800_000;
520
838
  * name both windows. Keep the two in sync.
521
839
  */
522
840
  const DEFAULT_PROBE_RELEASE_MS = 300_000;
523
- function initialForm(detected, activeAgent) {
841
+ export function initialForm(detected, activeAgent) {
524
842
  const config = detected.currentConfig;
525
843
  const roots = [...(activeAgent?.allowedCwdRoots ?? config?.allowedCwdRoots ?? [])];
526
844
  return {
@@ -539,6 +857,10 @@ function initialForm(detected, activeAgent) {
539
857
  autoDiscoverModels: config?.autoDiscoverModels !== false,
540
858
  // Prefer the host's resolved value: it is the one actually in effect.
541
859
  idleStr: idleText(activeAgent?.idleTimeoutMs ?? config?.idleTimeoutMs),
860
+ // The MCP list comes back from the host in its NORMALIZED shape (defaults
861
+ // filled in), so re-serializing it reproduces exactly what is stored —
862
+ // that is what keeps the card from being permanently dirty.
863
+ mcpText: config?.mcpServers === undefined ? '' : JSON.stringify(config.mcpServers, null, 2),
542
864
  };
543
865
  }
544
866
  function idleText(value) {
@@ -569,7 +891,7 @@ function idleValid(raw) {
569
891
  const value = Number(text);
570
892
  return value === 0 || (value >= 1_000 && value <= 86_400_000);
571
893
  }
572
- function buildPayload(form, detected, newCustom) {
894
+ export function buildPayload(form, detected, newCustom) {
573
895
  const env = {};
574
896
  for (const row of form.envRows) {
575
897
  const key = row.key.trim();
@@ -582,6 +904,9 @@ function buildPayload(form, detected, newCustom) {
582
904
  // The field is a command line; the host validates it the same way, so what
583
905
  // the preview under the input shows is what will actually be spawned.
584
906
  const parsed = splitCommandLine(form.commandLine);
907
+ // An unparseable MCP text yields `undefined` here and `mcpInvalid` in the
908
+ // form, which disables saving — so this never throws inside a render.
909
+ const mcpServers = parseMcpServers(form.mcpText);
585
910
  const payload = {
586
911
  id: newCustom ? form.agentId.trim() : detected.id,
587
912
  ...(form.title.trim() === '' ? {} : { title: form.title.trim() }),
@@ -595,6 +920,9 @@ function buildPayload(form, detected, newCustom) {
595
920
  ...(form.autoDiscoverModels ? {} : { autoDiscoverModels: false }),
596
921
  // `''` sends nothing: the host then applies its own default.
597
922
  ...(idleRaw === '' || !idleValid(idleRaw) ? {} : { idleTimeoutMs: Number(idleRaw) }),
923
+ // The MCP list is OPTIONAL: absent = the agent connects only to the
924
+ // servers it configures for itself.
925
+ ...(mcpServers === undefined ? {} : { mcpServers }),
598
926
  };
599
927
  if (form.rootsOpen) {
600
928
  if (cwd !== '')
@@ -611,7 +939,7 @@ function buildPayload(form, detected, newCustom) {
611
939
  return payload;
612
940
  }
613
941
  /** The expanded body: connection form, workspace allow-list, env, actions. */
614
- function AgentDetails({ acp, detected, form, patch, isConfigured, newCustom, formIncomplete, idValid, idTaken, cwdInvalid, rootsInvalid, idleInvalid, saving, savedSuccess, testing, testResult, showSaveWarning, onUseDefault, onSave, onTest, }) {
942
+ function AgentDetails({ acp, detected, form, patch, isConfigured, newCustom, formIncomplete, idValid, idTaken, cwdInvalid, rootsInvalid, idleInvalid, mcpInvalid, saving, savedSuccess, testing, testResult, showSaveWarning, onUseDefault, onSave, onTest, }) {
615
943
  const ui = useUi(acp);
616
944
  const [advancedOpen, setAdvancedOpen] = useState(false);
617
945
  // The same parse the host will run (`validateAgentDef` → `splitCommandLine`).
@@ -628,9 +956,9 @@ function AgentDetails({ acp, detected, form, patch, isConfigured, newCustom, for
628
956
  ? acp.label('小写字母或数字开头,可用 a-z 0-9 . _ -,最长 64 字符。', 'Starts with a lowercase letter or digit; a-z 0-9 . _ - allowed; max 64 chars.')
629
957
  : acp.label('ID 格式不合法:小写字母或数字开头,可用 a-z 0-9 . _ -。', 'Invalid id: start with a lowercase letter or digit; only a-z 0-9 . _ - allowed.') })] }) })), _jsx("div", { className: "dwa-form-row", children: _jsxs("div", { className: "dwa-form-col", children: [_jsx("label", { className: "dwa-form-label", children: acp.label('显示名', 'Display Name') }), _jsx("input", { className: "dwa-input", value: form.title, onChange: e => patch({ title: e.target.value }), placeholder: detected.name === '' ? acp.label('我的 Agent', 'My Agent') : detected.name })] }) }), _jsx("div", { className: "dwa-form-row", children: _jsxs("div", { className: "dwa-form-col", children: [_jsx("label", { className: "dwa-form-label", children: acp.label('启动命令(程序 + 参数)', 'Launch Command (program + arguments)') }), _jsx("input", { className: "dwa-input dwa-input-mono", value: form.commandLine, onChange: e => patch({ commandLine: e.target.value }), placeholder: detected.defaultCommand === ''
630
958
  ? 'e.g. codebuddy --acp'
631
- : formatCommandLine(detected.defaultCommand, detected.defaultArgs) }), _jsx("span", { className: "dwa-hint", children: commandPreview }), _jsx("span", { className: "dwa-hint", children: acp.label('参数直接跟在程序后面即可;路径里含空格时用引号包住整条路径,例如 "C:\\Program Files\\nodejs\\node.exe" --acp。', 'Arguments go right after the program; quote a whole path that contains spaces, e.g. "C:\\Program Files\\nodejs\\node.exe" --acp.') })] }) }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 12, flexWrap: 'wrap' }, children: [_jsxs("button", { type: "button", className: "dwa-btn dwa-btn-secondary", "aria-expanded": advancedOpen, onClick: () => setAdvancedOpen(value => !value), children: [_jsx("span", { className: "dwa-caret", "aria-hidden": true, children: advancedOpen ? '▾' : '▸' }), " ", acp.label('高级参数', 'Advanced Parameters')] }), !advancedOpen && (cwdInvalid || rootsInvalid) && (_jsx("span", { className: "dwa-error", children: acp.label('里面有非法的工作空间路径', 'Contains an invalid workspace path') }))] }), _jsxs("div", { style: advancedOpen ? undefined : { display: 'none' }, children: [!newCustom && (_jsxs("div", { className: "dwa-default-hint", children: [_jsxs("div", { children: [_jsx("span", { style: { fontWeight: 600, marginRight: 6 }, children: acp.label('官方测试默认命令:', 'Tested Default Command:') }), _jsxs("code", { className: "dwa-input-mono", style: { background: '#0000000a', padding: '2px 6px', borderRadius: 4 }, children: [detected.defaultCommand, " ", detected.defaultArgs.join(' ')] })] }), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", onClick: onUseDefault, title: acp.label('把命令与参数重置为官方验证值', 'Reset command and arguments to the tested default'), children: acp.label('使用预设值', 'Use Preset') })] })), _jsx("div", { className: "dwa-form-row", children: _jsxs("div", { className: "dwa-form-col", children: [_jsx("label", { className: "dwa-form-label", children: acp.label('npm 包名(PATH 中找不到时自动安装)', 'npm Package (auto-installed when missing from PATH)') }), _jsx("input", { className: "dwa-input dwa-input-mono", value: form.npmPackage, onChange: e => patch({ npmPackage: e.target.value }), placeholder: acp.label('原生支持 ACP 的 agent 无需填写', 'Not needed for agents with native ACP support') })] }) }), _jsx(WorkspaceBlock, { acp: acp, form: form, patch: patch, cwdInvalid: cwdInvalid, rootsInvalid: rootsInvalid }), _jsx("div", { className: "dwa-form-row", children: _jsxs("div", { className: "dwa-form-col", children: [_jsx("label", { className: "dwa-form-label", children: acp.label('空闲释放(毫秒)', 'Idle Release (ms)') }), _jsx("input", { className: "dwa-input dwa-input-mono", value: form.idleStr, onChange: e => patch({ idleStr: e.target.value }), placeholder: acp.label(`留空 = 宿主默认(承载会话 ${String(DEFAULT_IDLE_RELEASE_MS / 60_000)} 分钟 / 探测进程 ${String(DEFAULT_PROBE_RELEASE_MS / 60_000)} 分钟)`, `Blank = host default (${String(DEFAULT_IDLE_RELEASE_MS / 60_000)} min for a chat process / ${String(DEFAULT_PROBE_RELEASE_MS / 60_000)} min for a probe)`) }), _jsx("span", { className: idleInvalid ? 'dwa-error' : 'dwa-hint', children: idleInvalid
959
+ : formatCommandLine(detected.defaultCommand, detected.defaultArgs) }), _jsx("span", { className: "dwa-hint", children: commandPreview }), _jsx("span", { className: "dwa-hint", children: acp.label('参数直接跟在程序后面即可;路径里含空格时用引号包住整条路径,例如 "C:\\Program Files\\nodejs\\node.exe" --acp。', 'Arguments go right after the program; quote a whole path that contains spaces, e.g. "C:\\Program Files\\nodejs\\node.exe" --acp.') })] }) }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 12, flexWrap: 'wrap' }, children: [_jsxs("button", { type: "button", className: "dwa-btn dwa-btn-secondary", "aria-expanded": advancedOpen, onClick: () => setAdvancedOpen(value => !value), children: [_jsx("span", { className: "dwa-caret", "aria-hidden": true, children: advancedOpen ? '▾' : '▸' }), " ", acp.label('高级参数', 'Advanced Parameters')] }), !advancedOpen && (cwdInvalid || rootsInvalid) && (_jsx("span", { className: "dwa-error", children: acp.label('里面有非法的工作空间路径', 'Contains an invalid workspace path') })), !advancedOpen && mcpInvalid && (_jsx("span", { className: "dwa-error", children: acp.label('里面的 MCP 服务器配置不是合法 JSON', 'Contains an invalid MCP server list') }))] }), _jsxs("div", { style: advancedOpen ? undefined : { display: 'none' }, children: [!newCustom && (_jsxs("div", { className: "dwa-default-hint", children: [_jsxs("div", { children: [_jsx("span", { style: { fontWeight: 600, marginRight: 6 }, children: acp.label('官方测试默认命令:', 'Tested Default Command:') }), _jsxs("code", { className: "dwa-input-mono", style: { background: '#0000000a', padding: '2px 6px', borderRadius: 4 }, children: [detected.defaultCommand, " ", detected.defaultArgs.join(' ')] })] }), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", onClick: onUseDefault, title: acp.label('把命令与参数重置为官方验证值', 'Reset command and arguments to the tested default'), children: acp.label('使用预设值', 'Use Preset') })] })), _jsx("div", { className: "dwa-form-row", children: _jsxs("div", { className: "dwa-form-col", children: [_jsx("label", { className: "dwa-form-label", children: acp.label('npm 包名(PATH 中找不到时自动安装)', 'npm Package (auto-installed when missing from PATH)') }), _jsx("input", { className: "dwa-input dwa-input-mono", value: form.npmPackage, onChange: e => patch({ npmPackage: e.target.value }), placeholder: acp.label('原生支持 ACP 的 agent 无需填写', 'Not needed for agents with native ACP support') })] }) }), _jsx(WorkspaceBlock, { acp: acp, form: form, patch: patch, cwdInvalid: cwdInvalid, rootsInvalid: rootsInvalid }), _jsx("div", { className: "dwa-form-row", children: _jsxs("div", { className: "dwa-form-col", children: [_jsx("label", { className: "dwa-form-label", children: acp.label('空闲释放(毫秒)', 'Idle Release (ms)') }), _jsx("input", { className: "dwa-input dwa-input-mono", value: form.idleStr, onChange: e => patch({ idleStr: e.target.value }), placeholder: acp.label(`留空 = 宿主默认(承载会话 ${String(DEFAULT_IDLE_RELEASE_MS / 60_000)} 分钟 / 探测进程 ${String(DEFAULT_PROBE_RELEASE_MS / 60_000)} 分钟)`, `Blank = host default (${String(DEFAULT_IDLE_RELEASE_MS / 60_000)} min for a chat process / ${String(DEFAULT_PROBE_RELEASE_MS / 60_000)} min for a probe)`) }), _jsx("span", { className: idleInvalid ? 'dwa-error' : 'dwa-hint', children: idleInvalid
632
960
  ? acp.label('必须是 0(永不释放)或 [1000, 86400000] 之间的整数毫秒。', 'Must be 0 (never release) or an integer in [1000, 86400000] ms.')
633
- : acp.label('没有进行中的回合或待答审批时,空闲超过此时长就挂起进程(会话保留,下一条消息自动重启并恢复);0 = 永不释放;有回合在跑、取消尚未结算或审批待答的进程不会被释放。此窗口只对承载过会话的进程生效:保存后拉起的模型探测进程(面板显示「已连接」的那个)固定 5 分钟释放,填 0 也拦不住。', 'With no turn in flight or approval awaiting an answer, the process is parked once it has been idle this long (sessions are kept and resume on the next message); 0 = never; a process with a running turn, an unsettled cancel, or a pending approval is never released. This window applies only to a process that has hosted a session: the model-probe process (the one the panel shows as "connected" after a save) is always released after 5 minutes, and 0 does not stop it.') })] }) }), _jsxs("div", { className: "dwa-form-row", style: { flexDirection: 'column', gap: 6 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsx("span", { className: "dwa-form-label", children: acp.label('环境变量', 'Environment Variables') }), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", onClick: () => patch({ envRows: [...form.envRows, { key: '', value: '' }] }), children: acp.label('添加', 'Add') })] }), form.envRows.map((row, index) => (_jsxs("div", { style: { display: 'flex', gap: 6, alignItems: 'center' }, children: [_jsx("input", { className: "dwa-input dwa-input-mono", style: { flex: 2 }, value: row.key, onChange: e => patch({ envRows: form.envRows.map((r, i) => (i === index ? { ...r, key: e.target.value } : r)) }), placeholder: "KEY" }), _jsx("input", { className: "dwa-input dwa-input-mono", style: { flex: 3 }, value: row.value, onChange: e => patch({ envRows: form.envRows.map((r, i) => (i === index ? { ...r, value: e.target.value } : r)) }), placeholder: "VALUE" }), _jsx("button", { type: "button", className: "dwa-btn", title: acp.label('删除该变量', 'Remove this variable'), onClick: () => patch({ envRows: form.envRows.filter((_, i) => i !== index) }), children: "\u2715" })] }, index))), form.envRows.length === 0 && (_jsx("span", { className: "dwa-hint", children: acp.label('无自定义变量;子进程继承宿主环境。', 'No custom variables; the child inherits the host environment.') }))] })] }), _jsxs("div", { className: "dwa-form-actions", children: [_jsx("button", { className: "dwa-btn", onClick: onTest, disabled: testing || saving || formIncomplete, title: acp.label('用当前表单值拉起一次性进程并完成 ACP 握手,不保存配置', 'Spawns a throwaway process from the form values and completes the ACP handshake; nothing is saved'), children: testing ? acp.label('测试中...', 'Testing...') : acp.label('测试连接', 'Test Connection') }), _jsx("button", { className: "dwa-btn dwa-btn-primary", onClick: onSave, disabled: saving || testing || formIncomplete, children: saving
961
+ : (_jsxs(_Fragment, { children: [acp.label('空闲超过此时长就挂起进程(会话保留,下一条消息自动恢复)。', 'The process is parked once it has been idle this long (the session is kept and resumes on the next message).'), _jsx("br", {}), acp.label('0 = 永不释放;回合在跑、取消未结算或审批待答时不释放。', '0 = never; a process with a running turn, an unsettled cancel, or a pending approval is never released.'), _jsx("br", {}), acp.label('只对承载过会话的进程生效;探测进程固定 5 分钟释放,填 0 也拦不住。', '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.')] })) })] }) }), _jsxs("div", { className: "dwa-form-row", style: { flexDirection: 'column', gap: 6 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsx("span", { className: "dwa-form-label", children: acp.label('环境变量', 'Environment Variables') }), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", onClick: () => patch({ envRows: [...form.envRows, { key: '', value: '' }] }), children: acp.label('添加', 'Add') })] }), form.envRows.map((row, index) => (_jsxs("div", { style: { display: 'flex', gap: 6, alignItems: 'center' }, children: [_jsx("input", { className: "dwa-input dwa-input-mono", style: { flex: 2 }, value: row.key, onChange: e => patch({ envRows: form.envRows.map((r, i) => (i === index ? { ...r, key: e.target.value } : r)) }), placeholder: "KEY" }), _jsx("input", { className: "dwa-input dwa-input-mono", style: { flex: 3 }, value: row.value, onChange: e => patch({ envRows: form.envRows.map((r, i) => (i === index ? { ...r, value: e.target.value } : r)) }), placeholder: "VALUE" }), _jsx("button", { type: "button", className: "dwa-btn", title: acp.label('删除该变量', 'Remove this variable'), onClick: () => patch({ envRows: form.envRows.filter((_, i) => i !== index) }), children: "\u2715" })] }, index))), form.envRows.length === 0 && (_jsx("span", { className: "dwa-hint", children: acp.label('无自定义变量;子进程继承宿主环境。', 'No custom variables; the child inherits the host environment.') }))] }), _jsxs("div", { className: "dwa-form-row", style: { flexDirection: 'column', gap: 6 }, children: [_jsx("span", { className: "dwa-form-label", children: acp.label('MCP 服务器(JSON)', 'MCP Servers (JSON)') }), _jsx("textarea", { className: "dwa-input dwa-input-mono", style: { minHeight: 96, resize: 'vertical' }, value: form.mcpText, onChange: e => patch({ mcpText: e.target.value }), spellCheck: false, placeholder: '[\n { "name": "example", "transport": "stdio", "command": "/abs/path/to/server", "args": ["--mcp"] }\n]' }), mcpInvalid && (_jsx("span", { className: "dwa-error", children: acp.label('必须是一个 JSON 数组;每个条目写明 name 与 transport。', 'Must be a JSON array; every entry needs a name and a transport.') })), _jsx("span", { className: "dwa-hint", children: acp.label('这些 MCP 服务器会在建会话时交给 agent,由 agent 自己拉起并使用(stdio 需填 command,可选 args / env;http 与 sse 需填 url,可选 headers)。agent 自己配置的 MCP 服务器不受影响,这里只是额外追加。', '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.') })] })] }), _jsxs("div", { className: "dwa-form-actions", children: [_jsx("button", { className: "dwa-btn", onClick: onTest, disabled: testing || saving || formIncomplete, title: acp.label('用当前表单值拉起一次性进程并完成 ACP 握手,不保存配置', 'Spawns a throwaway process from the form values and completes the ACP handshake; nothing is saved'), children: testing ? acp.label('测试中...', 'Testing...') : acp.label('测试连接', 'Test Connection') }), _jsx("button", { className: "dwa-btn dwa-btn-primary", onClick: onSave, disabled: saving || testing || formIncomplete, children: saving
634
962
  ? acp.label('保存中...', 'Saving...')
635
963
  : savedSuccess
636
964
  ? acp.label('✓ 已保存', '✓ Saved')