dsh-ntr 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -485,9 +485,9 @@ const STYLE = `
485
485
  .ntr-acp-seat:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-active, #0000000d); }
486
486
  .ntr-acp-seat:disabled { opacity: 0.5; cursor: default; }
487
487
  .ntr-acp-seat.is-open { border-color: var(--dsw-alias-state-business-primary, #2563eb); color: var(--dsw-alias-state-business-primary, #2563eb); }
488
- .ntr-acp-seat.is-detached { border-color: var(--ntr-warn, #d97706); color: var(--ntr-warn, #d97706); }
488
+ .ntr-acp-seat.is-detached { border-color: var(--dsw-alias-state-warn-label, #d97706); color: var(--dsw-alias-state-warn-label, #d97706); }
489
489
  .ntr-acp-seat-main { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
490
- .ntr-acp-seat-sub { color: var(--dsw-alias-text-secondary, #6b7280); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
490
+ .ntr-acp-seat-sub { color: var(--dsw-alias-label-secondary, #6b7280); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
491
491
  .ntr-acp-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
492
492
  .ntr-acp-tick { margin-left: auto; color: var(--dsw-alias-state-business-primary, #2563eb); }
493
493
  .ntr-acp-check { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 13px; height: 13px; border: 1px solid var(--dsw-alias-border-l2, #d1d5db); border-radius: 3px; font-size: 10px; line-height: 1; color: var(--dsw-alias-state-business-primary, #2563eb); }
@@ -496,80 +496,108 @@ const STYLE = `
496
496
  .ntr-acp-menu-head { padding: 8px 10px 6px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
497
497
  .ntr-acp-menu-title { flex: 1; }
498
498
  .ntr-acp-menu-spin { opacity: 0.5; }
499
- .ntr-acp-menu-note { padding: 0 10px 6px; font-size: 11px; color: var(--dsw-alias-text-secondary, #6b7280); line-height: 1.5; }
500
- .ntr-acp-menu-error { margin: 0 10px 6px; padding: 6px 8px; border-radius: 6px; background: #fee2e2; color: #b91c1c; font-size: 11px; }
499
+ .ntr-acp-menu-note { padding: 0 10px 6px; font-size: 11px; color: var(--dsw-alias-label-secondary, #6b7280); line-height: 1.5; }
500
+ .ntr-acp-menu-error { margin: 0 10px 6px; padding: 6px 8px; border-radius: 6px; background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #b91c1c) 12%, transparent); color: var(--dsw-alias-state-error-primary, #b91c1c); font-size: 11px; }
501
501
  .ntr-acp-menu-body { overflow: auto; padding: 0 4px 6px; }
502
502
  .ntr-acp-menu-section { display: flex; flex-direction: column; padding-top: 4px; }
503
- .ntr-acp-menu-section-title { padding: 6px 8px 4px; font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--dsw-alias-text-secondary, #6b7280); }
503
+ .ntr-acp-menu-section-title { padding: 6px 8px 4px; font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--dsw-alias-label-secondary, #6b7280); }
504
504
  .ntr-acp-menu-item { display: flex; align-items: center; gap: 6px; font: inherit; font-size: 12px; text-align: left; padding: 6px 8px; border: none; border-radius: 6px; background: none; color: inherit; cursor: pointer; width: 100%; }
505
505
  .ntr-acp-menu-item:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-active, #0000000d); }
506
506
  .ntr-acp-menu-item:disabled { opacity: 0.45; cursor: default; }
507
507
  .ntr-acp-menu-item.is-active { font-weight: 600; }
508
508
  .ntr-acp-menu-item-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
509
- .ntr-acp-menu-item-detail { color: var(--dsw-alias-text-secondary, #6b7280); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; text-align: right; }
510
- .ntr-acp-menu-empty { padding: 10px; text-align: center; color: var(--dsw-alias-text-secondary, #6b7280); }
509
+ .ntr-acp-menu-item-detail { color: var(--dsw-alias-label-secondary, #6b7280); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; text-align: right; }
510
+ .ntr-acp-menu-empty { padding: 10px; text-align: center; color: var(--dsw-alias-label-secondary, #6b7280); }
511
511
  /* ---- ACP view tab ---- */
512
512
  .ntr-acp-view { display: flex; flex-direction: column; height: 100%; min-height: 0; font-size: 13px; line-height: 1.6; }
513
- .ntr-acp-view-head { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--dsw-alias-border-l2, #e5e7eb); font-size: 12px; color: var(--dsw-alias-text-secondary, #6b7280); }
513
+ .ntr-acp-view-head { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--dsw-alias-border-l2, #e5e7eb); font-size: 12px; color: var(--dsw-alias-label-secondary, #6b7280); }
514
514
  .ntr-acp-view-agent { font-weight: 600; color: inherit; }
515
515
  .ntr-acp-view-meta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 40%; direction: rtl; text-align: left; }
516
516
  .ntr-acp-view-head .ntr-acp-more { margin-left: auto; }
517
517
  .ntr-acp-stream { flex: 1; min-height: 0; overflow: auto; padding: 12px 14px 24px; display: flex; flex-direction: column; gap: 10px; }
518
518
  .ntr-acp-badge { font-size: 11px; padding: 1px 8px; border-radius: 999px; border: 1px solid transparent; }
519
- .ntr-acp-badge.is-done { color: #15803d; background: #dcfce7; }
520
- .ntr-acp-badge.is-busy { color: #1d4ed8; background: #dbeafe; }
521
- .ntr-acp-badge.is-warn { color: #b45309; background: #fef3c7; }
522
- .ntr-acp-badge.is-fail { color: #b91c1c; background: #fee2e2; }
523
- .ntr-acp-badge.is-idle { color: var(--dsw-alias-text-secondary, #6b7280); background: var(--dsw-alias-bg-layer-2, #f3f4f6); }
519
+ .ntr-acp-badge.is-done { color: color-mix(in srgb, var(--dsw-alias-state-success-primary, #22c55e) 72%, var(--dsw-alias-label-primary, #111827)); background: var(--dsw-alias-state-success-tertiary, #dcfce7); }
520
+ .ntr-acp-badge.is-busy { color: var(--dsw-alias-state-business-primary, #1d4ed8); background: var(--dsw-alias-state-business-tertiary, #dbeafe); }
521
+ .ntr-acp-badge.is-warn { color: var(--dsw-alias-state-warn-label, #b45309); background: var(--dsw-alias-state-warn-tertiary, #fef3c7); }
522
+ .ntr-acp-badge.is-fail { color: var(--dsw-alias-state-error-primary, #b91c1c); background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #b91c1c) 12%, transparent); }
523
+ .ntr-acp-badge.is-idle { color: var(--dsw-alias-label-secondary, #6b7280); background: var(--dsw-alias-bg-layer-2, #f3f4f6); }
524
524
  .ntr-acp-bubble { max-width: 88%; padding: 8px 12px; border-radius: 12px; white-space: pre-wrap; word-break: break-word; }
525
525
  .ntr-acp-bubble.is-user { align-self: flex-end; background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #2563eb) 12%, transparent); }
526
526
  .ntr-acp-bubble.is-agent { align-self: flex-start; background: var(--dsw-alias-bg-layer-2, #f3f4f6); }
527
527
  .ntr-acp-inline-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; padding: 1px 4px; border-radius: 4px; background: color-mix(in srgb, currentColor 10%, transparent); }
528
528
  .ntr-acp-code { margin: 6px 0; padding: 8px 10px; border-radius: 8px; background: #0f172a; color: #e2e8f0; overflow: auto; font-size: 12px; line-height: 1.5; }
529
- .ntr-acp-thought { border-left: 3px solid var(--dsw-alias-border-l2, #d1d5db); padding-left: 8px; color: var(--dsw-alias-text-secondary, #6b7280); font-size: 12px; }
529
+ .ntr-acp-thought { border-left: 3px solid var(--dsw-alias-border-l2, #d1d5db); padding-left: 8px; color: var(--dsw-alias-label-secondary, #6b7280); font-size: 12px; }
530
530
  .ntr-acp-thought > summary { cursor: pointer; font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; }
531
531
  .ntr-acp-thought-body { white-space: pre-wrap; padding: 4px 0 2px; }
532
532
  .ntr-acp-tool { border: 1px solid var(--dsw-alias-border-l2, #e5e7eb); border-radius: 10px; background: var(--dsw-alias-bg-layer-1, #fff); overflow: hidden; }
533
- .ntr-acp-tool.is-fail { border-color: #fca5a5; }
533
+ .ntr-acp-tool.is-fail { border-color: var(--dsw-alias-state-error-secondary, #fca5a5); }
534
534
  .ntr-acp-tool-head { display: flex; align-items: center; gap: 8px; padding: 7px 10px; font-size: 12px; }
535
535
  .ntr-acp-tool-title { flex: 1; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
536
536
  .ntr-acp-tool-body { margin: 0; padding: 8px 10px; border-top: 1px solid var(--dsw-alias-border-l2, #e5e7eb); background: var(--dsw-alias-bg-layer-2, #f9fafb); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; line-height: 1.55; white-space: pre-wrap; word-break: break-word; max-height: 340px; overflow: auto; }
537
- .ntr-acp-tool-input { background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-text-primary, #111827); max-height: 240px; }
538
- .ntr-acp-tool-meta { font-size: 11px; color: var(--dsw-alias-text-secondary, #6b7280); }
537
+ .ntr-acp-tool-input { background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, #111827); max-height: 240px; }
538
+ .ntr-acp-tool-meta { font-size: 11px; color: var(--dsw-alias-label-secondary, #6b7280); }
539
539
  .ntr-acp-diff { border-top: 1px solid var(--dsw-alias-border-l2, #e5e7eb); }
540
540
  .ntr-acp-diff-head { display: flex; align-items: center; gap: 8px; padding: 5px 10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; background: var(--dsw-alias-bg-layer-2, #f9fafb); }
541
541
  .ntr-acp-diff-path { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
542
- .ntr-acp-diff-stats .is-add { color: #15803d; }
543
- .ntr-acp-diff-stats .is-del { color: #b91c1c; }
542
+ .ntr-acp-diff-stats .is-add { color: var(--dsw-alias-state-success-primary, #15803d); }
543
+ .ntr-acp-diff-stats .is-del { color: var(--dsw-alias-state-error-primary, #b91c1c); }
544
544
  .ntr-acp-diff-body { max-height: 360px; overflow: auto; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; line-height: 1.55; }
545
545
  .ntr-acp-diff-row { display: flex; gap: 8px; padding: 0 10px; white-space: pre; }
546
- .ntr-acp-diff-row.is-add { background: #dcfce7; }
547
- .ntr-acp-diff-row.is-del { background: #fee2e2; }
548
- .ntr-acp-diff-row.is-ctx { color: var(--dsw-alias-text-secondary, #6b7280); }
546
+ .ntr-acp-diff-row.is-add { background: var(--dsw-alias-file-diff-added-bg, #dcfce7); }
547
+ .ntr-acp-diff-row.is-del { background: var(--dsw-alias-file-diff-deleted-bg, #fee2e2); }
548
+ .ntr-acp-diff-row.is-ctx { color: var(--dsw-alias-label-secondary, #6b7280); }
549
549
  .ntr-acp-diff-sign { width: 1ch; flex: none; opacity: 0.7; }
550
550
  .ntr-acp-diff-text { overflow-x: auto; }
551
- .ntr-acp-diff-note { padding: 4px 10px 6px; font-size: 11px; color: var(--dsw-alias-text-secondary, #6b7280); }
551
+ .ntr-acp-diff-note { padding: 4px 10px 6px; font-size: 11px; color: var(--dsw-alias-label-secondary, #6b7280); }
552
552
  .ntr-acp-more { font: inherit; font-size: 11.5px; padding: 3px 10px; border-radius: 6px; border: 1px solid var(--dsw-alias-border-l2, #d1d5db); background: var(--dsw-alias-bg-layer-2, #f3f4f6); color: inherit; cursor: pointer; }
553
553
  .ntr-acp-more:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-active, #0000000d); }
554
554
  .ntr-acp-more:disabled { opacity: 0.5; cursor: default; }
555
- .ntr-acp-more.is-primary { border-color: transparent; background: var(--dsw-alias-button-primary-fill, #2563eb); color: var(--dsw-alias-button-primary-invert, #fff); }
555
+ .ntr-acp-more.is-primary { border-color: transparent; background: var(--dsw-alias-button-primary-fill, #2563eb); color: var(--dsw-alias-label-primary-foreground, #fff); }
556
556
  .ntr-acp-tool > .ntr-acp-more, .ntr-acp-empty-actions .ntr-acp-more { margin: 6px 10px; }
557
557
  .ntr-acp-plan { border: 1px solid var(--dsw-alias-border-l2, #e5e7eb); border-radius: 10px; padding: 8px 10px; background: var(--dsw-alias-bg-layer-1, #fff); }
558
- .ntr-acp-plan-head { font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--dsw-alias-text-secondary, #6b7280); margin-bottom: 4px; }
558
+ .ntr-acp-plan-head { font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--dsw-alias-label-secondary, #6b7280); margin-bottom: 4px; }
559
559
  .ntr-acp-plan-row { display: flex; align-items: baseline; gap: 8px; font-size: 12.5px; padding: 1px 0; }
560
- .ntr-acp-plan-row.is-completed { color: var(--dsw-alias-text-secondary, #6b7280); text-decoration: line-through; }
560
+ .ntr-acp-plan-row.is-completed { color: var(--dsw-alias-label-secondary, #6b7280); text-decoration: line-through; }
561
561
  .ntr-acp-plan-row.is-in_progress { font-weight: 600; }
562
562
  .ntr-acp-plan-glyph { width: 1ch; flex: none; }
563
- .ntr-acp-plan-prio { margin-left: auto; font-size: 10px; text-transform: uppercase; color: var(--dsw-alias-text-secondary, #6b7280); }
563
+ .ntr-acp-plan-prio { margin-left: auto; font-size: 10px; text-transform: uppercase; color: var(--dsw-alias-label-secondary, #6b7280); }
564
564
  .ntr-acp-notice { padding: 6px 10px; border-radius: 8px; font-size: 12px; }
565
- .ntr-acp-notice.is-error { background: #fee2e2; color: #b91c1c; }
566
- .ntr-acp-notice.is-info { background: var(--dsw-alias-bg-layer-2, #f3f4f6); color: var(--dsw-alias-text-secondary, #6b7280); }
565
+ .ntr-acp-notice.is-error { background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #b91c1c) 12%, transparent); color: var(--dsw-alias-state-error-primary, #b91c1c); }
566
+ .ntr-acp-notice.is-info { background: var(--dsw-alias-bg-layer-2, #f3f4f6); color: var(--dsw-alias-label-secondary, #6b7280); }
567
567
  .ntr-acp-empty { margin: auto; max-width: 460px; text-align: center; display: flex; flex-direction: column; gap: 10px; padding: 24px; }
568
568
  .ntr-acp-empty-title { font-size: 14px; font-weight: 600; }
569
- .ntr-acp-empty-body { margin: 0; color: var(--dsw-alias-text-secondary, #6b7280); font-size: 12.5px; line-height: 1.7; }
570
- .ntr-acp-empty-hint { color: var(--dsw-alias-text-secondary, #6b7280); font-size: 12px; }
571
- .ntr-acp-seat-diag { font-family: var(--dsw-alias-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); opacity: 0.72; word-break: break-word; }
569
+ .ntr-acp-empty-body { margin: 0; color: var(--dsw-alias-label-secondary, #6b7280); font-size: 12.5px; line-height: 1.7; }
570
+ .ntr-acp-empty-hint { color: var(--dsw-alias-label-secondary, #6b7280); font-size: 12px; }
571
+ .ntr-acp-seat-diag { font-family: var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace); opacity: 0.72; word-break: break-word; }
572
572
  .ntr-acp-empty-actions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
573
+ /* ---- Pending permission asks (conversation.input.dock) ---- */
574
+ /* The dock slot is the full conversation column, but the composer card inside
575
+ it is narrower (InputBar: side clearance + card max width). Mirror that
576
+ formula so each ask lines up with the input box instead of spanning the
577
+ column. The list owns a bounded height and scrolls so a burst of asks cannot
578
+ push the composer off screen. */
579
+ .ntr-permission-dock {
580
+ box-sizing: border-box;
581
+ width: calc(100% - var(--dsh-composer-side-clearance, 16px) - var(--dsh-composer-side-clearance, 16px));
582
+ max-width: var(--dsh-composer-card-max-width, 920px);
583
+ margin: 0 auto;
584
+ display: flex; flex-direction: column; gap: 6px;
585
+ max-height: min(38vh, 320px); overflow-y: auto;
586
+ font-size: 12px; line-height: 1.5;
587
+ }
588
+ .ntr-permission-card { display: flex; flex-direction: column; gap: 6px; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-l2, #d1d5db); border-left: 3px solid var(--dsw-alias-state-warn-primary, #d97706); border-radius: 10px; background: var(--dsw-alias-bg-layer-1, #fff); }
589
+ .ntr-permission-head { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
590
+ .ntr-permission-tool { flex: none; font-weight: 600; }
591
+ .ntr-permission-reason { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-secondary, #6b7280); }
592
+ .ntr-permission-deadline { flex: none; font-size: 11px; color: var(--dsw-alias-label-secondary, #6b7280); font-variant-numeric: tabular-nums; }
593
+ .ntr-permission-deadline.is-expired { color: var(--dsw-alias-state-warn-label, #b45309); font-weight: 600; }
594
+ .ntr-permission-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
595
+ .ntr-permission-option, .ntr-permission-cancel { font: inherit; font-size: 12px; line-height: 1.4; padding: 4px 10px; border-radius: 7px; border: 1px solid var(--dsw-alias-border-l2, #d1d5db); background: var(--dsw-alias-bg-layer-2, #f3f4f6); color: inherit; cursor: pointer; white-space: normal; text-align: left; }
596
+ .ntr-permission-option:hover:not(:disabled), .ntr-permission-cancel:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-active, #0000000d); }
597
+ .ntr-permission-option:disabled, .ntr-permission-cancel:disabled { opacity: 0.5; cursor: default; }
598
+ .ntr-permission-option:focus-visible, .ntr-permission-cancel:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #2563eb); outline-offset: 2px; }
599
+ .ntr-permission-cancel { margin-left: auto; color: var(--dsw-alias-label-secondary, #6b7280); }
600
+ .ntr-permission-error { padding: 4px 8px; border-radius: 6px; background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #b91c1c) 12%, transparent); color: var(--dsw-alias-state-error-primary, #b91c1c); font-size: 11px; word-break: break-word; }
573
601
  /* ---- NTR settings dialog (shell.overlay) ---- */
574
602
  /* Frame-wide surface opened from the agent base's empty-roster menu. The
575
603
  overlay layer itself is click-through, so the layer opts back in and blocks
@@ -11,6 +11,7 @@ import { AcpComposerMode } from "./composer-mode.js";
11
11
  import { AcpComposerAgentTag, AcpSessionControls, AcpSessionMenu } from "./session-controls.js";
12
12
  import { AcpConversationView } from "./acp-view.js";
13
13
  import { AcpSettingsDialog } from "./settings-dialog.js";
14
+ import { PermissionDock } from "./permission-dock.js";
14
15
  import { createModelSeatClaim, createPermissionSeatClaim, MODEL_SEAT_KEY, PERMISSION_SEAT_KEY, } from "./model-seat.js";
15
16
  import { acpModeFromPresetEvent, rowAgentPreset, stagedIsAcpMode, stagedSessionOf, } from "./acp-ui.js";
16
17
  import { createSlashIsolation, slashAcpVerdict } from "./slash-isolation.js";
@@ -161,6 +162,22 @@ export function apply(ctx) {
161
162
  order: 5,
162
163
  inject: (sessionId) => chatFace(sessionId),
163
164
  }, AcpComposerAgentTag)), 'dsh-ntr: composer agent tag');
165
+ // Pending permission asks for the staged conversation, above the composer
166
+ // card (`conversation.input.dock` — a session-scoped list slot; the host's
167
+ // own queue/todo docks register the same way). The slot spans the whole
168
+ // conversation column, so the dock component applies the composer's own
169
+ // width formula to line up with the input box. Every ask is dispatched as
170
+ // broadcast as `permission/request`; the dock renders only the ones carrying
171
+ // this conversation's id, so it needs no ACP-mode gate. Asks without a chat
172
+ // identity stay on the settings-panel ledger.
173
+ ctx.effect(() => ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
174
+ name: 'conversation.input.dock',
175
+ id: 'acp-permissions',
176
+ order: 10,
177
+ // The positional inject argument is the session binding key — the chat
178
+ // session id this dock belongs to (same contract as every other entry).
179
+ inject: (sessionId) => chatFace(sessionId),
180
+ }, PermissionDock)), 'dsh-ntr: conversation permission dock');
164
181
  /**
165
182
  * The client runtime's session list store, read structurally. Used to find
166
183
  * the session the composer is staged on and whether it runs the ACP preset —
@@ -3,11 +3,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  * React surface for ACP: the settings configuration section.
4
4
  *
5
5
  * Scope is deliberately narrow — this is a CONFIGURATION surface. Runtime
6
- * interaction lives in the conversation: permission asks are answered in the
7
- * chat composer through the host approval chain. The ledger card answers
8
- * whatever did NOT reach the conversation — a host without the chain, a
9
- * session with no chat identity behind it, or an ask whose native route
10
- * degraded to unavailable (see `permissionLedgerCard`).
6
+ * interaction lives in the conversation: every permission ask with a chat
7
+ * identity is answered on a card above that conversation's composer
8
+ * (`permission-dock.tsx`). The ledger card here is the cross-session mirror —
9
+ * asks whose conversation is not open, or which have no chat identity at all —
10
+ * and is where those are answered (see `permissionLedgerCard`).
11
11
  *
12
12
  * Each configured agent is one compact row (status + current model + badges)
13
13
  * that expands into the full connection form and model catalog. The row stays
@@ -42,8 +42,10 @@ const STYLE = `
42
42
  --ntr-radius: 10px; --ntr-radius-sm: 7px;
43
43
  --ntr-font: 13px; --ntr-font-sm: 12px; --ntr-font-xs: 11px;
44
44
  --ntr-line: 1px solid var(--dsw-alias-border-l2, #e5e7eb);
45
- --ntr-muted: var(--dsw-alias-text-secondary, #6b7280);
46
- --ntr-danger: #dc2626; --ntr-warn: #b45309;
45
+ --ntr-muted: var(--dsw-alias-label-secondary, #6b7280);
46
+ --ntr-danger: var(--dsw-alias-state-error-primary, #dc2626);
47
+ --ntr-warn: var(--dsw-alias-state-warn-label, #b45309);
48
+ --ntr-success: color-mix(in srgb, var(--dsw-alias-state-success-primary, #16a34a) 72%, var(--dsw-alias-label-primary, #111827));
47
49
  --ntr-accent: var(--dsw-alias-button-primary-fill, #2563eb);
48
50
  width: 100%; max-width: 960px; box-sizing: border-box; margin: 0 auto;
49
51
  padding: 20px; overflow: auto; font-size: var(--ntr-font); line-height: 1.55;
@@ -67,25 +69,25 @@ const STYLE = `
67
69
  }
68
70
  .dwa-btn:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); border-color: var(--dsw-alias-border-l1, #9ca3af); }
69
71
  .dwa-btn:disabled { opacity: 0.5; cursor: not-allowed; }
70
- .dwa-btn-primary { background: var(--ntr-accent); color: var(--dsw-alias-button-primary-invert, #fff); border-color: transparent; font-weight: 550; }
72
+ .dwa-btn-primary { background: var(--ntr-accent); color: var(--dsw-alias-label-primary-foreground, #fff); border-color: transparent; font-weight: 550; }
71
73
  .dwa-btn-primary:hover { background: var(--ntr-accent); border-color: transparent; opacity: 0.88; }
72
74
  .dwa-btn-secondary { background: var(--dsw-alias-bg-layer-2, #f3f4f6); }
73
- .dwa-btn-danger { color: var(--ntr-danger); border-color: #fca5a5; }
74
- .dwa-btn-danger:hover { background: #fee2e2; }
75
+ .dwa-btn-danger { color: var(--ntr-danger); border-color: var(--dsw-alias-state-error-secondary, #fca5a5); }
76
+ .dwa-btn-danger:hover { background: var(--dsw-alias-interactive-bg-hover-danger, #fee2e2); }
75
77
  .dwa-section { margin-bottom: 24px; }
76
78
  .dwa-section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
77
79
  .dwa-section-title { font-size: 14px; font-weight: 650; margin: 0; display: flex; align-items: center; gap: 8px; }
78
80
  .dwa-section-copy { margin: 3px 0 0; font-size: var(--ntr-font-xs); color: var(--ntr-muted); }
79
81
  .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; }
80
- .dwa-card-warn { border-left: 3px solid var(--dsw-alias-state-warn-primary, #d97706); background: var(--dsw-alias-state-warn-secondary, #fffbeb); }
82
+ .dwa-card-warn { border-left: 3px solid var(--dsw-alias-state-warn-primary, #d97706); background: var(--dsw-alias-state-warn-tertiary, #fffbeb); }
81
83
  .dwa-mode-banner { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 5px 16px; align-items: start; margin-bottom: 24px; padding: 14px 16px; }
82
84
  .dwa-mode-title { display: flex; align-items: center; gap: 8px; font-weight: 650; }
83
85
  .dwa-mode-copy { grid-column: 1 / -1; color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
84
86
  .dwa-mode-source { display: block; margin-top: 5px; overflow-wrap: anywhere; opacity: 0.85; }
85
87
  .dwa-badge { font-size: var(--ntr-font-xs); line-height: 1.4; border-radius: 999px; padding: 2px 8px; background: var(--dsw-alias-bg-layer-2, #f3f4f6); color: var(--ntr-muted); font-weight: 550; display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
86
- .dwa-badge-success { background: #22c55e1c; color: #15803d; }
87
- .dwa-badge-warn { background: #f59e0b1f; color: #b45309; }
88
- .dwa-badge-info { background: #3b82f61a; color: #2563eb; }
88
+ .dwa-badge-success { background: var(--dsw-alias-state-success-tertiary, #dcfce7); color: var(--ntr-success); }
89
+ .dwa-badge-warn { background: var(--dsw-alias-state-warn-tertiary, #fef3c7); color: var(--ntr-warn); }
90
+ .dwa-badge-info { background: var(--dsw-alias-state-business-tertiary, #dbeafe); color: var(--dsw-alias-state-business-primary, #2563eb); }
89
91
  .dwa-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
90
92
  .dwa-agent-list { display: flex; flex-direction: column; gap: 8px; }
91
93
  .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; }
@@ -107,7 +109,7 @@ const STYLE = `
107
109
  .dwa-subsection-copy { margin: 3px 0 0; color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
108
110
  .dwa-form-row { display: flex; gap: var(--ntr-gap-3); margin-bottom: 14px; align-items: flex-start; }
109
111
  .dwa-form-col { flex: 1; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
110
- .dwa-form-label { font-size: var(--ntr-font-sm); font-weight: 600; color: var(--dsw-alias-text-primary, #374151); }
112
+ .dwa-form-label { font-size: var(--ntr-font-sm); font-weight: 600; color: var(--dsw-alias-label-primary, #374151); }
111
113
  .dwa-hint { font-size: var(--ntr-font-xs); line-height: 1.55; color: var(--ntr-muted); }
112
114
  .dwa-error { font-size: var(--ntr-font-xs); color: var(--ntr-danger); }
113
115
  .dwa-input { min-height: 36px; font: inherit; font-size: var(--ntr-font-sm); padding: 7px 10px; 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; width: 100%; box-sizing: border-box; }
@@ -132,7 +134,7 @@ const STYLE = `
132
134
  .dwa-add-content { border-top: var(--ntr-line); padding: 14px 16px 16px; }
133
135
  .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; }
134
136
  .dwa-chip:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); }
135
- .dwa-chip-active { border-color: var(--ntr-accent); background: #3b82f614; color: #2563eb; font-weight: 550; }
137
+ .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; }
136
138
  .dwa-chip-list { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin: 0 0 12px; }
137
139
  .dwa-switch { display: inline-flex; align-items: center; gap: 7px; font-size: var(--ntr-font-sm); cursor: pointer; user-select: none; }
138
140
  .dwa-inline-setting { display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px; border-radius: var(--ntr-radius-sm); background: var(--dsw-alias-bg-layer-2, #f9fafb); }
@@ -145,7 +147,7 @@ const STYLE = `
145
147
  /* Tall enough for a comfortable selection (about ten rows) before scrolling;
146
148
  the cap keeps the two stacked panes from pushing the rest of the card away. */
147
149
  .dwa-model-pane { min-width: 0; max-height: 360px; display: flex; flex-direction: column; border: var(--ntr-line); border-radius: var(--ntr-radius-sm); background: var(--dsw-alias-bg-layer-1, #fff); overflow: hidden; }
148
- .dwa-model-pane-head { display: flex; align-items: center; gap: 6px; padding: 7px 9px; border-bottom: var(--ntr-line); font-size: var(--ntr-font-xs); font-weight: 600; color: var(--dsw-alias-text-secondary, #4b5563); }
150
+ .dwa-model-pane-head { display: flex; align-items: center; gap: 6px; padding: 7px 9px; border-bottom: var(--ntr-line); font-size: var(--ntr-font-xs); font-weight: 600; color: var(--dsw-alias-label-secondary, #4b5563); }
149
151
  .dwa-model-pane-head .dwa-btn { margin-left: auto; min-height: 26px; padding: 2px 8px; font-size: var(--ntr-font-xs); }
150
152
  .dwa-model-pane-head .dwa-caret-btn { flex: none; width: 22px; height: 22px; background: transparent; }
151
153
  .dwa-model-pane-head .dwa-caret-btn:hover { background: var(--dsw-alias-interactive-bg-active, #e5e7eb); }
@@ -163,7 +165,7 @@ const STYLE = `
163
165
  .dwa-model-empty { padding: 6px; }
164
166
  .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; }
165
167
  .dwa-model-row:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); }
166
- .dwa-model-row.is-on { background: #3b82f614; color: #2563eb; }
168
+ .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); }
167
169
  .dwa-model-row-mark { width: 12px; flex: none; text-align: center; opacity: 0.75; }
168
170
  .dwa-model-row-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
169
171
  /* Name and credit keep their room; the id is the only cell that collapses. */
@@ -185,7 +187,7 @@ const STYLE = `
185
187
  .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; }
186
188
  .dwa-seg > button + button { border-left: 1px solid var(--dsw-alias-border-l2, #d1d5db); }
187
189
  .dwa-seg > button:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); }
188
- .dwa-seg > button.is-on { background: var(--ntr-accent); color: var(--dsw-alias-button-primary-invert, #fff); font-weight: 550; }
190
+ .dwa-seg > button.is-on { background: var(--ntr-accent); color: var(--dsw-alias-label-primary-foreground, #fff); font-weight: 550; }
189
191
  .dwa-preference-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 0; }
190
192
  .dwa-preference-row + .dwa-preference-row { border-top: var(--ntr-line); }
191
193
  /* A control that needs the whole card: the label sits above it, not beside
@@ -244,9 +246,9 @@ function useUi(acp) {
244
246
  }
245
247
  function stateVisual(state, acp) {
246
248
  switch (state) {
247
- case 'ready': return { color: '#22c55e', text: acp.label('已连接', 'Connected') };
248
- case 'starting': return { color: '#eab308', text: acp.label('启动中', 'Starting') };
249
- case 'error': return { color: '#ef4444', text: acp.label('错误', 'Error') };
249
+ case 'ready': return { color: 'var(--dsw-alias-state-success-primary, #22c55e)', text: acp.label('已连接', 'Connected') };
250
+ case 'starting': return { color: 'var(--dsw-alias-state-warn-primary, #eab308)', text: acp.label('启动中', 'Starting') };
251
+ case 'error': return { color: 'var(--dsw-alias-state-error-primary, #ef4444)', text: acp.label('错误', 'Error') };
250
252
  // A configured-but-resting agent is the normal state, not a fault: the
251
253
  // process is not kept resident and connects on demand the first time a DSH
252
254
  // chat turn uses it. Surfacing a runtime lifecycle here ("stopped"/"idle")
@@ -254,7 +256,7 @@ function stateVisual(state, acp) {
254
256
  // which the config-time 测试连接 (agent/test) already answers. So the
255
257
  // resting state just says 已配置.
256
258
  default: return {
257
- color: '#9ca3af',
259
+ color: 'var(--dsw-alias-state-idle-primary, #9ca3af)',
258
260
  text: acp.label('已配置', 'Configured'),
259
261
  title: acp.label('已配置,进程不常驻。在 DSH 聊天中首次使用时自动连接;展开区可用「测试连接」验证命令能否正常起来。', 'Configured; the process is not kept resident. It connects automatically the first time a DSH chat turn uses it — use “Test connection” in the expanded panel to verify the command starts.'),
260
262
  };
@@ -457,22 +459,15 @@ export function payloadEquals(payload, current) {
457
459
  /**
458
460
  * The settings panel's permission-ledger rule.
459
461
  *
460
- * A pending ask ALWAYS gets an answerable card here, whatever `approvalRouted`
461
- * says. That flag reports that the host HAS an approval service — it does not
462
- * promise THIS ask reached the conversation. An ask lands in `ui.permissions`
463
- * only on the ledger path (`AgentBridge.handlePermissionRequest` broadcasts
464
- * `permission/request` there and nowhere else), so a pending entry is by
465
- * definition a question the conversation is not showing. The previous rule
466
- * (`approvalRouted` → a read-only count, no buttons) hid the only answer
467
- * entry exactly when the native route had degraded to unavailable, and the
468
- * ask then hung until its timeout.
469
- *
470
- * `fellBackFromConversation` is the case the card has to explain: the host
471
- * can route asks, and this one still did not make it.
462
+ * Pending asks are shown in the conversation card while that conversation is
463
+ * open; this card is the cross-session mirror for the ones it cannot show —
464
+ * asks belonging to another conversation, and `/acp` RPC-only asks with no
465
+ * chat identity at all. It therefore appears whenever ANY ask is pending: a
466
+ * pending entry with no conversation card would otherwise have no answer
467
+ * surface anywhere.
472
468
  */
473
469
  export function permissionLedgerCard(ui) {
474
- const visible = ui.permissions.length > 0;
475
- return { visible, fellBackFromConversation: visible && ui.approvalRouted };
470
+ return { visible: ui.permissions.length > 0 };
476
471
  }
477
472
  /**
478
473
  * The ONE wait window, as the rungs the slider snaps to.
@@ -708,10 +703,10 @@ export function AcpSettingsSection(props) {
708
703
  const [createNonce, setCreateNonce] = useState(0);
709
704
  const { configured, available } = partitionAgents(ui.detectedAgents, ui.agents);
710
705
  const selected = addTarget ?? available[0] ?? NEW_AGENT_SEED;
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: () => {
706
+ 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' ? 'var(--dsw-alias-state-success-primary, #22c55e)' : ui.stream === 'down' ? 'var(--dsw-alias-state-error-primary, #ef4444)' : 'var(--dsw-alias-state-idle-primary, #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: () => {
712
707
  void acp.resync();
713
708
  void acp.detectAgents();
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
709
+ }, 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: 4 }, children: acp.label('待处理权限请求', 'Pending Permission Requests') }), _jsx("div", { className: "dwa-hint", style: { marginBottom: 8 }, children: acp.label('对话打开时在那边的卡片上回答;这里兜底显示其他会话的询问。', 'Answer in the conversation card while that conversation is open; this list covers the asks whose conversation is not.') }), ui.permissions.map(permission => (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6, flexWrap: 'wrap' }, children: [_jsxs("span", { style: { flex: 1, minWidth: 160 }, children: [_jsx("span", { style: { fontWeight: 600 }, children: permission.toolName }), permission.reason !== undefined && (_jsxs("span", { style: { color: 'var(--ntr-muted)' }, children: [" \u00B7 ", permission.reason] }))] }), permission.options.map(option => (_jsx("button", { className: "dwa-btn dwa-btn-primary", onClick: () => { void acp.respondPermission(permission.requestId, option.optionId).catch(() => undefined); }, children: option.name }, option.optionId))), _jsx("button", { className: "dwa-btn", onClick: () => { void acp.respondPermission(permission.requestId).catch(() => undefined); }, 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
715
710
  ? acp.label('已加入宿主模式列表,新建会话时选择「NTR 模式」即可使用。', 'Added to the harness roster. Pick “NTR mode” when creating a session.')
716
711
  : 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 }) => {
717
712
  const activeAgent = ui.agents.find(a => a.id === info.id);
@@ -956,7 +951,7 @@ function AgentDetails({ acp, detected, form, patch, isConfigured, newCustom, for
956
951
  ? acp.label('小写字母或数字开头,可用 a-z 0-9 . _ -,最长 64 字符。', 'Starts with a lowercase letter or digit; a-z 0-9 . _ - allowed; max 64 chars.')
957
952
  : 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 === ''
958
953
  ? 'e.g. codebuddy --acp'
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
954
+ : 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: 'var(--dsw-alias-markdown-inline-code, #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
960
955
  ? acp.label('必须是 0(永不释放)或 [1000, 86400000] 之间的整数毫秒。', 'Must be 0 (never release) or an integer in [1000, 86400000] ms.')
961
956
  : (_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
962
957
  ? acp.label('保存中...', 'Saving...')
@@ -969,7 +964,7 @@ function AgentDetails({ acp, detected, form, patch, isConfigured, newCustom, for
969
964
  fontSize: 12,
970
965
  whiteSpace: 'pre-wrap',
971
966
  wordBreak: 'break-all',
972
- color: testResult.ok ? '#16a34a' : 'var(--ntr-danger)',
967
+ color: testResult.ok ? 'var(--ntr-success)' : 'var(--ntr-danger)',
973
968
  }, children: [testResult.ok ? '✓ ' : '✕ ', testResult.text] }))] }));
974
969
  }
975
970
  /**
@@ -1226,7 +1221,7 @@ function ModelCatalogBlock({ agentId, detected, acp, active, exposed, onExposedC
1226
1221
  : acp.label('该 Agent 未暴露模型选择器(category: model)。', 'This agent exposes no model selector (category: model).');
1227
1222
  return (_jsxs("div", { children: [!isDefault && (_jsx("div", { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }, children: _jsx("button", { className: "dwa-btn", onClick: () => void acp.setDefaultAgent(agentId), children: acp.label('设为默认智能体', 'Set as default agent') }) })), isDefault && (
1228
1223
  // This default belongs to the plugin, not the deployment model route.
1229
- _jsx("div", { className: "dwa-hint", style: { marginTop: 8 }, children: acp.label('当前为默认智能体:新的 NTR 会话由它服务,不影响其他模式的默认模型。', "This is the default agent for new NTR sessions; other modes' default models are untouched.") })), _jsxs("div", { className: "dwa-inline-setting", style: { marginTop: 12 }, children: [_jsxs("label", { className: "dwa-switch", title: acp.label('Agent 新发布的模型会自动加入右侧已选列表;首次检测到的目录只作为基线,不会一次性全选。', 'Models the agent newly publishes are added to the selected list; the first observed catalog is only a baseline and is not selected wholesale.'), children: [_jsx("input", { type: "checkbox", checked: autoDiscover, onChange: e => void toggleAutoDiscover(e.target.checked), style: { margin: 0, accentColor: '#2563eb' } }), acp.label('自动发现新模型', 'Auto-discover new models')] }), _jsx("span", { className: "dwa-setting-copy", children: acp.label('新增模型自动加入已选列表,现有勾选不受影响。', 'New models join the selected list automatically; existing picks are untouched.') })] }), saveError !== undefined && _jsx("div", { className: "dwa-error", style: { marginTop: 4 }, children: saveError }), probeError !== undefined && (_jsxs("div", { className: "dwa-error", style: { marginTop: 8, wordBreak: 'break-word' }, children: ["\u26A0 ", acp.label('模型检测失败:', 'Model detection failed: '), probeError] })), _jsxs("div", { className: "dwa-model-section", children: [_jsxs("div", { className: "dwa-model-toolbar", children: [_jsxs("span", { className: "dwa-form-label", children: [acp.label('对话中可见的模型', 'Models offered in the conversation'), _jsx("span", { className: "dwa-hint", style: { marginLeft: 6, fontWeight: 400 }, title: acp.label('NTR 模式下的对话只列出这里勾选的模型;未勾选的仍可用——正在生效的那个也会以「· 当前」列出来,其余不列出。', '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.'), children: acp.label('(NTR 座位 / 芯片的下拉列表)', '(the NTR seat and chip pickers)') })] }), _jsx("span", { className: "dwa-badge dwa-badge-info", children: selectionBadge }), catalogStatus !== undefined && _jsx("span", { className: "dwa-hint dwa-model-status", children: catalogStatus }), _jsxs("div", { className: "dwa-model-toolbar-actions", children: [_jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", disabled: probing, onClick: () => void detect(), title: acp.label('重新读取 Agent 的模型目录:新增的加入左侧,已下架的从左侧移除;已选列表不受影响。', 'Re-read the agent catalog: newly published models join the left list, withdrawn ones drop out. The selection is untouched.'), children: probing ? acp.label('模型检测中...', 'Probing models...') : acp.label('检测可用模型', 'Detect available models') }), _jsx("input", { className: "dwa-input dwa-model-search", value: search, onChange: e => setSearch(e.target.value), placeholder: acp.label('搜索模型…', 'Search models…') })] })] }), _jsxs("div", { className: "dwa-model-transfer", children: [_jsxs("div", { className: `dwa-model-pane${allCollapsed ? ' is-collapsed' : ''}`, children: [_jsxs("div", { className: "dwa-model-pane-head", children: [_jsx("span", { children: acp.label('全部模型', 'All models') }), result !== undefined && _jsx("span", { className: "dwa-badge", children: modelChoices.length }), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", disabled: visibleChoices.length === 0, onClick: addVisible, title: acp.label('把当前搜索范围内的模型全部加入右侧', 'Add every model matching the current search'), children: acp.label('全部加入', 'Add all') }), _jsx("button", { type: "button", className: "dwa-caret-btn", "aria-expanded": !allCollapsed, onClick: () => setAllCollapsed(value => !value), title: allCollapsed ? acp.label('展开全部模型', 'Expand all models') : acp.label('收起全部模型', 'Collapse all models'), children: _jsx("span", { className: "dwa-caret", "aria-hidden": true, children: allCollapsed ? '▸' : '▾' }) })] }), _jsxs("div", { className: "dwa-model-list", children: [visibleChoices.map(choice => {
1224
+ _jsx("div", { className: "dwa-hint", style: { marginTop: 8 }, children: acp.label('当前为默认智能体:新的 NTR 会话由它服务,不影响其他模式的默认模型。', "This is the default agent for new NTR sessions; other modes' default models are untouched.") })), _jsxs("div", { className: "dwa-inline-setting", style: { marginTop: 12 }, children: [_jsxs("label", { className: "dwa-switch", title: acp.label('Agent 新发布的模型会自动加入右侧已选列表;首次检测到的目录只作为基线,不会一次性全选。', 'Models the agent newly publishes are added to the selected list; the first observed catalog is only a baseline and is not selected wholesale.'), children: [_jsx("input", { type: "checkbox", checked: autoDiscover, onChange: e => void toggleAutoDiscover(e.target.checked), style: { margin: 0, accentColor: 'var(--dsw-alias-state-business-primary, #2563eb)' } }), acp.label('自动发现新模型', 'Auto-discover new models')] }), _jsx("span", { className: "dwa-setting-copy", children: acp.label('新增模型自动加入已选列表,现有勾选不受影响。', 'New models join the selected list automatically; existing picks are untouched.') })] }), saveError !== undefined && _jsx("div", { className: "dwa-error", style: { marginTop: 4 }, children: saveError }), probeError !== undefined && (_jsxs("div", { className: "dwa-error", style: { marginTop: 8, wordBreak: 'break-word' }, children: ["\u26A0 ", acp.label('模型检测失败:', 'Model detection failed: '), probeError] })), _jsxs("div", { className: "dwa-model-section", children: [_jsxs("div", { className: "dwa-model-toolbar", children: [_jsxs("span", { className: "dwa-form-label", children: [acp.label('对话中可见的模型', 'Models offered in the conversation'), _jsx("span", { className: "dwa-hint", style: { marginLeft: 6, fontWeight: 400 }, title: acp.label('NTR 模式下的对话只列出这里勾选的模型;未勾选的仍可用——正在生效的那个也会以「· 当前」列出来,其余不列出。', '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.'), children: acp.label('(NTR 座位 / 芯片的下拉列表)', '(the NTR seat and chip pickers)') })] }), _jsx("span", { className: "dwa-badge dwa-badge-info", children: selectionBadge }), catalogStatus !== undefined && _jsx("span", { className: "dwa-hint dwa-model-status", children: catalogStatus }), _jsxs("div", { className: "dwa-model-toolbar-actions", children: [_jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", disabled: probing, onClick: () => void detect(), title: acp.label('重新读取 Agent 的模型目录:新增的加入左侧,已下架的从左侧移除;已选列表不受影响。', 'Re-read the agent catalog: newly published models join the left list, withdrawn ones drop out. The selection is untouched.'), children: probing ? acp.label('模型检测中...', 'Probing models...') : acp.label('检测可用模型', 'Detect available models') }), _jsx("input", { className: "dwa-input dwa-model-search", value: search, onChange: e => setSearch(e.target.value), placeholder: acp.label('搜索模型…', 'Search models…') })] })] }), _jsxs("div", { className: "dwa-model-transfer", children: [_jsxs("div", { className: `dwa-model-pane${allCollapsed ? ' is-collapsed' : ''}`, children: [_jsxs("div", { className: "dwa-model-pane-head", children: [_jsx("span", { children: acp.label('全部模型', 'All models') }), result !== undefined && _jsx("span", { className: "dwa-badge", children: modelChoices.length }), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", disabled: visibleChoices.length === 0, onClick: addVisible, title: acp.label('把当前搜索范围内的模型全部加入右侧', 'Add every model matching the current search'), children: acp.label('全部加入', 'Add all') }), _jsx("button", { type: "button", className: "dwa-caret-btn", "aria-expanded": !allCollapsed, onClick: () => setAllCollapsed(value => !value), title: allCollapsed ? acp.label('展开全部模型', 'Expand all models') : acp.label('收起全部模型', 'Collapse all models'), children: _jsx("span", { className: "dwa-caret", "aria-hidden": true, children: allCollapsed ? '▸' : '▾' }) })] }), _jsxs("div", { className: "dwa-model-list", children: [visibleChoices.map(choice => {
1230
1225
  const checked = selected.has(choice.value);
1231
1226
  // group 前缀仅来自 agent 返回的 SessionConfigSelectGroup(见 impl.ts
1232
1227
  // pushCatalogChoice);供应商可见性兜底靠原样展示 value,严禁从
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Pending ACP permission asks for the staged DSH conversation, rendered as
4
+ * composer-width entries above the composer card (`conversation.input.dock`).
5
+ * The slot itself spans the conversation column; the dock mirrors the host's
6
+ * InputBar width formula so the cards line up with the input box.
7
+ *
8
+ * The host broadcasts every ask as `permission/request`, carrying the DSH chat
9
+ * session id when the ask belongs to a conversation. This dock is the primary
10
+ * answer surface: it renders exactly the asks whose `dshSessionId` is the
11
+ * conversation it was mounted for, and nothing otherwise. There is no
12
+ * ACP-mode gate — the frame set itself is the gate, so an ordinary
13
+ * conversation shows this card if and only if it actually has an ask.
14
+ *
15
+ * The ask's description and options are the agent's own: buttons appear in the
16
+ * agent's order with the agent's names (including "Always Allow"), and
17
+ * `kind` is not rendered. A separate cancel button answers with no option,
18
+ * which the host forwards as `cancelled`. No Enter/Escape handlers: the
19
+ * composer owns its keyboard, and these are ordinary focusable buttons.
20
+ *
21
+ * Multiple asks stack in ARRIVAL order, oldest first: the question ignored
22
+ * longest stays at the top. The list is capped in height by CSS and scrolls.
23
+ * Asks without a chat identity never appear here — they stay in the
24
+ * settings-panel ledger, the cross-session mirror.
25
+ */
26
+ import { useEffect, useState } from 'react';
27
+ import { ensureAcpStyle, useUi } from "./acp-ui.js";
28
+ import { deadlineRemainingLabel, PERMISSION_DEADLINE_EXPIRED, permissionsForSession, } from "./store.js";
29
+ /** Tick interval for the countdown (seconds are all the label shows). */
30
+ const COUNTDOWN_TICK_MS = 1000;
31
+ export function PermissionDock({ acp, sessionId }) {
32
+ ensureAcpStyle();
33
+ const ui = useUi(acp);
34
+ const dshSessionId = String(sessionId);
35
+ const pending = permissionsForSession(ui.permissions, dshSessionId);
36
+ const counting = pending.some(ask => ask.timeoutMode !== 'wait' && ask.deadlineAt !== undefined);
37
+ const [now, setNow] = useState(() => Date.now());
38
+ useEffect(() => {
39
+ if (!counting)
40
+ return;
41
+ setNow(Date.now());
42
+ const timer = setInterval(() => { setNow(Date.now()); }, COUNTDOWN_TICK_MS);
43
+ return () => { clearInterval(timer); };
44
+ }, [counting]);
45
+ if (pending.length === 0)
46
+ return null;
47
+ return (_jsx("div", { className: "ntr-permission-dock", role: "region", "aria-label": acp.label('待处理的权限请求', 'Pending permission requests'), children: pending.map(permission => (_jsx(PermissionAskCard, { acp: acp, permission: permission, now: now }, permission.requestId))) }));
48
+ }
49
+ function PermissionAskCard(props) {
50
+ const { acp, permission, now } = props;
51
+ const [busy, setBusy] = useState(false);
52
+ const [error, setError] = useState();
53
+ const remaining = deadlineRemainingLabel(permission.deadlineAt, now, permission.timeoutMode);
54
+ const expired = remaining === PERMISSION_DEADLINE_EXPIRED;
55
+ const respond = (optionId) => {
56
+ if (busy)
57
+ return;
58
+ setBusy(true);
59
+ setError(undefined);
60
+ void acp.respondPermission(permission.requestId, optionId)
61
+ // `respondPermission` reports to the store's notice too, but that notice
62
+ // lives on the settings page; this card pins the failure where the user
63
+ // is looking.
64
+ .catch((failure) => {
65
+ setError(failure instanceof Error ? failure.message : String(failure));
66
+ })
67
+ .finally(() => { setBusy(false); });
68
+ };
69
+ return (_jsxs("div", { className: "ntr-permission-card", children: [_jsxs("div", { className: "ntr-permission-head", children: [_jsx("span", { className: "ntr-permission-tool", children: permission.toolName }), permission.reason !== undefined && (_jsx("span", { className: "ntr-permission-reason", title: permission.reason, children: permission.reason })), remaining !== undefined && (_jsx("span", { className: `ntr-permission-deadline${expired ? ' is-expired' : ''}`, children: expired
70
+ ? acp.label('已超时', 'expired')
71
+ : acp.label(`剩余 ${remaining}`, `${remaining} left`) }))] }), _jsxs("div", { className: "ntr-permission-actions", children: [permission.options.map(option => (_jsx("button", { type: "button", className: "ntr-permission-option", disabled: busy, title: option.name, onClick: () => { respond(option.optionId); }, children: option.name }, option.optionId))), _jsx("button", { type: "button", className: "ntr-permission-cancel", disabled: busy, title: acp.label('取消这次询问(不选择任何选项)', 'Cancel this ask without choosing an option'), onClick: () => { respond(); }, children: acp.label('取消', 'Cancel') })] }), error !== undefined && (_jsx("div", { className: "ntr-permission-error", role: "alert", children: error }))] }));
72
+ }
@@ -61,10 +61,10 @@ import { useEffect, useState } from 'react';
61
61
  import { AcpChip, AcpMenu, AcpShatterParts, buildAcpModelId, closeAcpMenu, ensureAcpStyle, openAcpMenu, openAcpSettingsDialog, setAcpMenuError, splitAcpModelId, useAcpMenu, useUi, } from "./acp-ui.js";
62
62
  import { playAgentLockAnim, useAgentLockPhase } from "./agent-lock-anim.js";
63
63
  const STATE_COLORS = {
64
- ready: '#22c55e',
65
- starting: '#eab308',
66
- error: '#ef4444',
67
- stopped: '#9ca3af',
64
+ ready: 'var(--dsw-alias-state-success-primary, #22c55e)',
65
+ starting: 'var(--dsw-alias-state-warn-primary, #eab308)',
66
+ error: 'var(--dsw-alias-state-error-primary, #ef4444)',
67
+ stopped: 'var(--dsw-alias-state-idle-primary, #9ca3af)',
68
68
  };
69
69
  function agentStateText(acp, state) {
70
70
  switch (state) {