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.
@@ -857,9 +857,9 @@ const STYLE = `
857
857
  .ntr-acp-seat:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-active, #0000000d); }
858
858
  .ntr-acp-seat:disabled { opacity: 0.5; cursor: default; }
859
859
  .ntr-acp-seat.is-open { border-color: var(--dsw-alias-state-business-primary, #2563eb); color: var(--dsw-alias-state-business-primary, #2563eb); }
860
- .ntr-acp-seat.is-detached { border-color: var(--ntr-warn, #d97706); color: var(--ntr-warn, #d97706); }
860
+ .ntr-acp-seat.is-detached { border-color: var(--dsw-alias-state-warn-label, #d97706); color: var(--dsw-alias-state-warn-label, #d97706); }
861
861
  .ntr-acp-seat-main { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
862
- .ntr-acp-seat-sub { color: var(--dsw-alias-text-secondary, #6b7280); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
862
+ .ntr-acp-seat-sub { color: var(--dsw-alias-label-secondary, #6b7280); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
863
863
  .ntr-acp-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
864
864
  .ntr-acp-tick { margin-left: auto; color: var(--dsw-alias-state-business-primary, #2563eb); }
865
865
  .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); }
@@ -868,80 +868,108 @@ const STYLE = `
868
868
  .ntr-acp-menu-head { padding: 8px 10px 6px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
869
869
  .ntr-acp-menu-title { flex: 1; }
870
870
  .ntr-acp-menu-spin { opacity: 0.5; }
871
- .ntr-acp-menu-note { padding: 0 10px 6px; font-size: 11px; color: var(--dsw-alias-text-secondary, #6b7280); line-height: 1.5; }
872
- .ntr-acp-menu-error { margin: 0 10px 6px; padding: 6px 8px; border-radius: 6px; background: #fee2e2; color: #b91c1c; font-size: 11px; }
871
+ .ntr-acp-menu-note { padding: 0 10px 6px; font-size: 11px; color: var(--dsw-alias-label-secondary, #6b7280); line-height: 1.5; }
872
+ .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; }
873
873
  .ntr-acp-menu-body { overflow: auto; padding: 0 4px 6px; }
874
874
  .ntr-acp-menu-section { display: flex; flex-direction: column; padding-top: 4px; }
875
- .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); }
875
+ .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); }
876
876
  .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%; }
877
877
  .ntr-acp-menu-item:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-active, #0000000d); }
878
878
  .ntr-acp-menu-item:disabled { opacity: 0.45; cursor: default; }
879
879
  .ntr-acp-menu-item.is-active { font-weight: 600; }
880
880
  .ntr-acp-menu-item-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
881
- .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; }
882
- .ntr-acp-menu-empty { padding: 10px; text-align: center; color: var(--dsw-alias-text-secondary, #6b7280); }
881
+ .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; }
882
+ .ntr-acp-menu-empty { padding: 10px; text-align: center; color: var(--dsw-alias-label-secondary, #6b7280); }
883
883
  /* ---- ACP view tab ---- */
884
884
  .ntr-acp-view { display: flex; flex-direction: column; height: 100%; min-height: 0; font-size: 13px; line-height: 1.6; }
885
- .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); }
885
+ .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); }
886
886
  .ntr-acp-view-agent { font-weight: 600; color: inherit; }
887
887
  .ntr-acp-view-meta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 40%; direction: rtl; text-align: left; }
888
888
  .ntr-acp-view-head .ntr-acp-more { margin-left: auto; }
889
889
  .ntr-acp-stream { flex: 1; min-height: 0; overflow: auto; padding: 12px 14px 24px; display: flex; flex-direction: column; gap: 10px; }
890
890
  .ntr-acp-badge { font-size: 11px; padding: 1px 8px; border-radius: 999px; border: 1px solid transparent; }
891
- .ntr-acp-badge.is-done { color: #15803d; background: #dcfce7; }
892
- .ntr-acp-badge.is-busy { color: #1d4ed8; background: #dbeafe; }
893
- .ntr-acp-badge.is-warn { color: #b45309; background: #fef3c7; }
894
- .ntr-acp-badge.is-fail { color: #b91c1c; background: #fee2e2; }
895
- .ntr-acp-badge.is-idle { color: var(--dsw-alias-text-secondary, #6b7280); background: var(--dsw-alias-bg-layer-2, #f3f4f6); }
891
+ .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); }
892
+ .ntr-acp-badge.is-busy { color: var(--dsw-alias-state-business-primary, #1d4ed8); background: var(--dsw-alias-state-business-tertiary, #dbeafe); }
893
+ .ntr-acp-badge.is-warn { color: var(--dsw-alias-state-warn-label, #b45309); background: var(--dsw-alias-state-warn-tertiary, #fef3c7); }
894
+ .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); }
895
+ .ntr-acp-badge.is-idle { color: var(--dsw-alias-label-secondary, #6b7280); background: var(--dsw-alias-bg-layer-2, #f3f4f6); }
896
896
  .ntr-acp-bubble { max-width: 88%; padding: 8px 12px; border-radius: 12px; white-space: pre-wrap; word-break: break-word; }
897
897
  .ntr-acp-bubble.is-user { align-self: flex-end; background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #2563eb) 12%, transparent); }
898
898
  .ntr-acp-bubble.is-agent { align-self: flex-start; background: var(--dsw-alias-bg-layer-2, #f3f4f6); }
899
899
  .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); }
900
900
  .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; }
901
- .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; }
901
+ .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; }
902
902
  .ntr-acp-thought > summary { cursor: pointer; font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; }
903
903
  .ntr-acp-thought-body { white-space: pre-wrap; padding: 4px 0 2px; }
904
904
  .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; }
905
- .ntr-acp-tool.is-fail { border-color: #fca5a5; }
905
+ .ntr-acp-tool.is-fail { border-color: var(--dsw-alias-state-error-secondary, #fca5a5); }
906
906
  .ntr-acp-tool-head { display: flex; align-items: center; gap: 8px; padding: 7px 10px; font-size: 12px; }
907
907
  .ntr-acp-tool-title { flex: 1; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
908
908
  .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; }
909
- .ntr-acp-tool-input { background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-text-primary, #111827); max-height: 240px; }
910
- .ntr-acp-tool-meta { font-size: 11px; color: var(--dsw-alias-text-secondary, #6b7280); }
909
+ .ntr-acp-tool-input { background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, #111827); max-height: 240px; }
910
+ .ntr-acp-tool-meta { font-size: 11px; color: var(--dsw-alias-label-secondary, #6b7280); }
911
911
  .ntr-acp-diff { border-top: 1px solid var(--dsw-alias-border-l2, #e5e7eb); }
912
912
  .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); }
913
913
  .ntr-acp-diff-path { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
914
- .ntr-acp-diff-stats .is-add { color: #15803d; }
915
- .ntr-acp-diff-stats .is-del { color: #b91c1c; }
914
+ .ntr-acp-diff-stats .is-add { color: var(--dsw-alias-state-success-primary, #15803d); }
915
+ .ntr-acp-diff-stats .is-del { color: var(--dsw-alias-state-error-primary, #b91c1c); }
916
916
  .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; }
917
917
  .ntr-acp-diff-row { display: flex; gap: 8px; padding: 0 10px; white-space: pre; }
918
- .ntr-acp-diff-row.is-add { background: #dcfce7; }
919
- .ntr-acp-diff-row.is-del { background: #fee2e2; }
920
- .ntr-acp-diff-row.is-ctx { color: var(--dsw-alias-text-secondary, #6b7280); }
918
+ .ntr-acp-diff-row.is-add { background: var(--dsw-alias-file-diff-added-bg, #dcfce7); }
919
+ .ntr-acp-diff-row.is-del { background: var(--dsw-alias-file-diff-deleted-bg, #fee2e2); }
920
+ .ntr-acp-diff-row.is-ctx { color: var(--dsw-alias-label-secondary, #6b7280); }
921
921
  .ntr-acp-diff-sign { width: 1ch; flex: none; opacity: 0.7; }
922
922
  .ntr-acp-diff-text { overflow-x: auto; }
923
- .ntr-acp-diff-note { padding: 4px 10px 6px; font-size: 11px; color: var(--dsw-alias-text-secondary, #6b7280); }
923
+ .ntr-acp-diff-note { padding: 4px 10px 6px; font-size: 11px; color: var(--dsw-alias-label-secondary, #6b7280); }
924
924
  .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; }
925
925
  .ntr-acp-more:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-active, #0000000d); }
926
926
  .ntr-acp-more:disabled { opacity: 0.5; cursor: default; }
927
- .ntr-acp-more.is-primary { border-color: transparent; background: var(--dsw-alias-button-primary-fill, #2563eb); color: var(--dsw-alias-button-primary-invert, #fff); }
927
+ .ntr-acp-more.is-primary { border-color: transparent; background: var(--dsw-alias-button-primary-fill, #2563eb); color: var(--dsw-alias-label-primary-foreground, #fff); }
928
928
  .ntr-acp-tool > .ntr-acp-more, .ntr-acp-empty-actions .ntr-acp-more { margin: 6px 10px; }
929
929
  .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); }
930
- .ntr-acp-plan-head { font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--dsw-alias-text-secondary, #6b7280); margin-bottom: 4px; }
930
+ .ntr-acp-plan-head { font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--dsw-alias-label-secondary, #6b7280); margin-bottom: 4px; }
931
931
  .ntr-acp-plan-row { display: flex; align-items: baseline; gap: 8px; font-size: 12.5px; padding: 1px 0; }
932
- .ntr-acp-plan-row.is-completed { color: var(--dsw-alias-text-secondary, #6b7280); text-decoration: line-through; }
932
+ .ntr-acp-plan-row.is-completed { color: var(--dsw-alias-label-secondary, #6b7280); text-decoration: line-through; }
933
933
  .ntr-acp-plan-row.is-in_progress { font-weight: 600; }
934
934
  .ntr-acp-plan-glyph { width: 1ch; flex: none; }
935
- .ntr-acp-plan-prio { margin-left: auto; font-size: 10px; text-transform: uppercase; color: var(--dsw-alias-text-secondary, #6b7280); }
935
+ .ntr-acp-plan-prio { margin-left: auto; font-size: 10px; text-transform: uppercase; color: var(--dsw-alias-label-secondary, #6b7280); }
936
936
  .ntr-acp-notice { padding: 6px 10px; border-radius: 8px; font-size: 12px; }
937
- .ntr-acp-notice.is-error { background: #fee2e2; color: #b91c1c; }
938
- .ntr-acp-notice.is-info { background: var(--dsw-alias-bg-layer-2, #f3f4f6); color: var(--dsw-alias-text-secondary, #6b7280); }
937
+ .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); }
938
+ .ntr-acp-notice.is-info { background: var(--dsw-alias-bg-layer-2, #f3f4f6); color: var(--dsw-alias-label-secondary, #6b7280); }
939
939
  .ntr-acp-empty { margin: auto; max-width: 460px; text-align: center; display: flex; flex-direction: column; gap: 10px; padding: 24px; }
940
940
  .ntr-acp-empty-title { font-size: 14px; font-weight: 600; }
941
- .ntr-acp-empty-body { margin: 0; color: var(--dsw-alias-text-secondary, #6b7280); font-size: 12.5px; line-height: 1.7; }
942
- .ntr-acp-empty-hint { color: var(--dsw-alias-text-secondary, #6b7280); font-size: 12px; }
943
- .ntr-acp-seat-diag { font-family: var(--dsw-alias-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); opacity: 0.72; word-break: break-word; }
941
+ .ntr-acp-empty-body { margin: 0; color: var(--dsw-alias-label-secondary, #6b7280); font-size: 12.5px; line-height: 1.7; }
942
+ .ntr-acp-empty-hint { color: var(--dsw-alias-label-secondary, #6b7280); font-size: 12px; }
943
+ .ntr-acp-seat-diag { font-family: var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace); opacity: 0.72; word-break: break-word; }
944
944
  .ntr-acp-empty-actions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
945
+ /* ---- Pending permission asks (conversation.input.dock) ---- */
946
+ /* The dock slot is the full conversation column, but the composer card inside
947
+ it is narrower (InputBar: side clearance + card max width). Mirror that
948
+ formula so each ask lines up with the input box instead of spanning the
949
+ column. The list owns a bounded height and scrolls so a burst of asks cannot
950
+ push the composer off screen. */
951
+ .ntr-permission-dock {
952
+ box-sizing: border-box;
953
+ width: calc(100% - var(--dsh-composer-side-clearance, 16px) - var(--dsh-composer-side-clearance, 16px));
954
+ max-width: var(--dsh-composer-card-max-width, 920px);
955
+ margin: 0 auto;
956
+ display: flex; flex-direction: column; gap: 6px;
957
+ max-height: min(38vh, 320px); overflow-y: auto;
958
+ font-size: 12px; line-height: 1.5;
959
+ }
960
+ .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); }
961
+ .ntr-permission-head { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
962
+ .ntr-permission-tool { flex: none; font-weight: 600; }
963
+ .ntr-permission-reason { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-secondary, #6b7280); }
964
+ .ntr-permission-deadline { flex: none; font-size: 11px; color: var(--dsw-alias-label-secondary, #6b7280); font-variant-numeric: tabular-nums; }
965
+ .ntr-permission-deadline.is-expired { color: var(--dsw-alias-state-warn-label, #b45309); font-weight: 600; }
966
+ .ntr-permission-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
967
+ .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; }
968
+ .ntr-permission-option:hover:not(:disabled), .ntr-permission-cancel:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-active, #0000000d); }
969
+ .ntr-permission-option:disabled, .ntr-permission-cancel:disabled { opacity: 0.5; cursor: default; }
970
+ .ntr-permission-option:focus-visible, .ntr-permission-cancel:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #2563eb); outline-offset: 2px; }
971
+ .ntr-permission-cancel { margin-left: auto; color: var(--dsw-alias-label-secondary, #6b7280); }
972
+ .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; }
945
973
  /* ---- NTR settings dialog (shell.overlay) ---- */
946
974
  /* Frame-wide surface opened from the agent base's empty-roster menu. The
947
975
  overlay layer itself is click-through, so the layer opts back in and blocks
@@ -19,6 +19,7 @@ import { AcpComposerMode } from './composer-mode.tsx'
19
19
  import { AcpComposerAgentTag, AcpSessionControls, AcpSessionMenu } from './session-controls.tsx'
20
20
  import { AcpConversationView } from './acp-view.tsx'
21
21
  import { AcpSettingsDialog } from './settings-dialog.tsx'
22
+ import { PermissionDock } from './permission-dock.tsx'
22
23
  import {
23
24
  createModelSeatClaim,
24
25
  createPermissionSeatClaim,
@@ -230,6 +231,23 @@ export function apply(ctx: Context): void {
230
231
  inject: (sessionId: unknown) => chatFace(sessionId),
231
232
  }, AcpComposerAgentTag as unknown as SlotComponent<any>)), 'dsh-ntr: composer agent tag')
232
233
 
234
+ // Pending permission asks for the staged conversation, above the composer
235
+ // card (`conversation.input.dock` — a session-scoped list slot; the host's
236
+ // own queue/todo docks register the same way). The slot spans the whole
237
+ // conversation column, so the dock component applies the composer's own
238
+ // width formula to line up with the input box. Every ask is dispatched as
239
+ // broadcast as `permission/request`; the dock renders only the ones carrying
240
+ // this conversation's id, so it needs no ACP-mode gate. Asks without a chat
241
+ // identity stay on the settings-panel ledger.
242
+ ctx.effect(() => ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
243
+ name: 'conversation.input.dock',
244
+ id: 'acp-permissions',
245
+ order: 10,
246
+ // The positional inject argument is the session binding key — the chat
247
+ // session id this dock belongs to (same contract as every other entry).
248
+ inject: (sessionId: unknown) => chatFace(sessionId),
249
+ }, PermissionDock as unknown as SlotComponent<any>)), 'dsh-ntr: conversation permission dock')
250
+
233
251
  /**
234
252
  * The client runtime's session list store, read structurally. Used to find
235
253
  * the session the composer is staged on and whether it runs the ACP preset —
@@ -2,11 +2,11 @@
2
2
  * React surface for ACP: the settings configuration section.
3
3
  *
4
4
  * Scope is deliberately narrow — this is a CONFIGURATION surface. Runtime
5
- * interaction lives in the conversation: permission asks are answered in the
6
- * chat composer through the host approval chain. The ledger card answers
7
- * whatever did NOT reach the conversation — a host without the chain, a
8
- * session with no chat identity behind it, or an ask whose native route
9
- * degraded to unavailable (see `permissionLedgerCard`).
5
+ * interaction lives in the conversation: every permission ask with a chat
6
+ * identity is answered on a card above that conversation's composer
7
+ * (`permission-dock.tsx`). The ledger card here is the cross-session mirror —
8
+ * asks whose conversation is not open, or which have no chat identity at all —
9
+ * and is where those are answered (see `permissionLedgerCard`).
10
10
  *
11
11
  * Each configured agent is one compact row (status + current model + badges)
12
12
  * that expands into the full connection form and model catalog. The row stays
@@ -51,8 +51,10 @@ const STYLE = `
51
51
  --ntr-radius: 10px; --ntr-radius-sm: 7px;
52
52
  --ntr-font: 13px; --ntr-font-sm: 12px; --ntr-font-xs: 11px;
53
53
  --ntr-line: 1px solid var(--dsw-alias-border-l2, #e5e7eb);
54
- --ntr-muted: var(--dsw-alias-text-secondary, #6b7280);
55
- --ntr-danger: #dc2626; --ntr-warn: #b45309;
54
+ --ntr-muted: var(--dsw-alias-label-secondary, #6b7280);
55
+ --ntr-danger: var(--dsw-alias-state-error-primary, #dc2626);
56
+ --ntr-warn: var(--dsw-alias-state-warn-label, #b45309);
57
+ --ntr-success: color-mix(in srgb, var(--dsw-alias-state-success-primary, #16a34a) 72%, var(--dsw-alias-label-primary, #111827));
56
58
  --ntr-accent: var(--dsw-alias-button-primary-fill, #2563eb);
57
59
  width: 100%; max-width: 960px; box-sizing: border-box; margin: 0 auto;
58
60
  padding: 20px; overflow: auto; font-size: var(--ntr-font); line-height: 1.55;
@@ -76,25 +78,25 @@ const STYLE = `
76
78
  }
77
79
  .dwa-btn:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); border-color: var(--dsw-alias-border-l1, #9ca3af); }
78
80
  .dwa-btn:disabled { opacity: 0.5; cursor: not-allowed; }
79
- .dwa-btn-primary { background: var(--ntr-accent); color: var(--dsw-alias-button-primary-invert, #fff); border-color: transparent; font-weight: 550; }
81
+ .dwa-btn-primary { background: var(--ntr-accent); color: var(--dsw-alias-label-primary-foreground, #fff); border-color: transparent; font-weight: 550; }
80
82
  .dwa-btn-primary:hover { background: var(--ntr-accent); border-color: transparent; opacity: 0.88; }
81
83
  .dwa-btn-secondary { background: var(--dsw-alias-bg-layer-2, #f3f4f6); }
82
- .dwa-btn-danger { color: var(--ntr-danger); border-color: #fca5a5; }
83
- .dwa-btn-danger:hover { background: #fee2e2; }
84
+ .dwa-btn-danger { color: var(--ntr-danger); border-color: var(--dsw-alias-state-error-secondary, #fca5a5); }
85
+ .dwa-btn-danger:hover { background: var(--dsw-alias-interactive-bg-hover-danger, #fee2e2); }
84
86
  .dwa-section { margin-bottom: 24px; }
85
87
  .dwa-section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
86
88
  .dwa-section-title { font-size: 14px; font-weight: 650; margin: 0; display: flex; align-items: center; gap: 8px; }
87
89
  .dwa-section-copy { margin: 3px 0 0; font-size: var(--ntr-font-xs); color: var(--ntr-muted); }
88
90
  .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; }
89
- .dwa-card-warn { border-left: 3px solid var(--dsw-alias-state-warn-primary, #d97706); background: var(--dsw-alias-state-warn-secondary, #fffbeb); }
91
+ .dwa-card-warn { border-left: 3px solid var(--dsw-alias-state-warn-primary, #d97706); background: var(--dsw-alias-state-warn-tertiary, #fffbeb); }
90
92
  .dwa-mode-banner { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 5px 16px; align-items: start; margin-bottom: 24px; padding: 14px 16px; }
91
93
  .dwa-mode-title { display: flex; align-items: center; gap: 8px; font-weight: 650; }
92
94
  .dwa-mode-copy { grid-column: 1 / -1; color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
93
95
  .dwa-mode-source { display: block; margin-top: 5px; overflow-wrap: anywhere; opacity: 0.85; }
94
96
  .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; }
95
- .dwa-badge-success { background: #22c55e1c; color: #15803d; }
96
- .dwa-badge-warn { background: #f59e0b1f; color: #b45309; }
97
- .dwa-badge-info { background: #3b82f61a; color: #2563eb; }
97
+ .dwa-badge-success { background: var(--dsw-alias-state-success-tertiary, #dcfce7); color: var(--ntr-success); }
98
+ .dwa-badge-warn { background: var(--dsw-alias-state-warn-tertiary, #fef3c7); color: var(--ntr-warn); }
99
+ .dwa-badge-info { background: var(--dsw-alias-state-business-tertiary, #dbeafe); color: var(--dsw-alias-state-business-primary, #2563eb); }
98
100
  .dwa-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
99
101
  .dwa-agent-list { display: flex; flex-direction: column; gap: 8px; }
100
102
  .dwa-agent-card { border: var(--ntr-line); border-radius: var(--ntr-radius); background: var(--dsw-alias-bg-layer-1, #fff); overflow: visible; transition: border-color 0.15s ease; }
@@ -116,7 +118,7 @@ const STYLE = `
116
118
  .dwa-subsection-copy { margin: 3px 0 0; color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
117
119
  .dwa-form-row { display: flex; gap: var(--ntr-gap-3); margin-bottom: 14px; align-items: flex-start; }
118
120
  .dwa-form-col { flex: 1; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
119
- .dwa-form-label { font-size: var(--ntr-font-sm); font-weight: 600; color: var(--dsw-alias-text-primary, #374151); }
121
+ .dwa-form-label { font-size: var(--ntr-font-sm); font-weight: 600; color: var(--dsw-alias-label-primary, #374151); }
120
122
  .dwa-hint { font-size: var(--ntr-font-xs); line-height: 1.55; color: var(--ntr-muted); }
121
123
  .dwa-error { font-size: var(--ntr-font-xs); color: var(--ntr-danger); }
122
124
  .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; }
@@ -141,7 +143,7 @@ const STYLE = `
141
143
  .dwa-add-content { border-top: var(--ntr-line); padding: 14px 16px 16px; }
142
144
  .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; }
143
145
  .dwa-chip:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); }
144
- .dwa-chip-active { border-color: var(--ntr-accent); background: #3b82f614; color: #2563eb; font-weight: 550; }
146
+ .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; }
145
147
  .dwa-chip-list { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin: 0 0 12px; }
146
148
  .dwa-switch { display: inline-flex; align-items: center; gap: 7px; font-size: var(--ntr-font-sm); cursor: pointer; user-select: none; }
147
149
  .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); }
@@ -154,7 +156,7 @@ const STYLE = `
154
156
  /* Tall enough for a comfortable selection (about ten rows) before scrolling;
155
157
  the cap keeps the two stacked panes from pushing the rest of the card away. */
156
158
  .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; }
157
- .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); }
159
+ .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); }
158
160
  .dwa-model-pane-head .dwa-btn { margin-left: auto; min-height: 26px; padding: 2px 8px; font-size: var(--ntr-font-xs); }
159
161
  .dwa-model-pane-head .dwa-caret-btn { flex: none; width: 22px; height: 22px; background: transparent; }
160
162
  .dwa-model-pane-head .dwa-caret-btn:hover { background: var(--dsw-alias-interactive-bg-active, #e5e7eb); }
@@ -172,7 +174,7 @@ const STYLE = `
172
174
  .dwa-model-empty { padding: 6px; }
173
175
  .dwa-model-row { display: flex; align-items: center; gap: 6px; width: 100%; border: none; background: none; padding: 5px 6px; border-radius: 5px; cursor: pointer; color: inherit; font: inherit; font-size: var(--ntr-font-sm); text-align: left; }
174
176
  .dwa-model-row:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); }
175
- .dwa-model-row.is-on { background: #3b82f614; color: #2563eb; }
177
+ .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); }
176
178
  .dwa-model-row-mark { width: 12px; flex: none; text-align: center; opacity: 0.75; }
177
179
  .dwa-model-row-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
178
180
  /* Name and credit keep their room; the id is the only cell that collapses. */
@@ -194,7 +196,7 @@ const STYLE = `
194
196
  .dwa-seg > button { min-height: 30px; font: inherit; font-size: var(--ntr-font-sm); color: inherit; border: none; background: var(--dsw-alias-bg-layer-1, #fff); padding: 4px 14px; cursor: pointer; }
195
197
  .dwa-seg > button + button { border-left: 1px solid var(--dsw-alias-border-l2, #d1d5db); }
196
198
  .dwa-seg > button:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); }
197
- .dwa-seg > button.is-on { background: var(--ntr-accent); color: var(--dsw-alias-button-primary-invert, #fff); font-weight: 550; }
199
+ .dwa-seg > button.is-on { background: var(--ntr-accent); color: var(--dsw-alias-label-primary-foreground, #fff); font-weight: 550; }
198
200
  .dwa-preference-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 0; }
199
201
  .dwa-preference-row + .dwa-preference-row { border-top: var(--ntr-line); }
200
202
  /* A control that needs the whole card: the label sits above it, not beside
@@ -255,9 +257,9 @@ function useUi(acp: AcpStore) {
255
257
 
256
258
  function stateVisual(state: AgentView['state'], acp: AcpStore): { color: string; text: string; title?: string } {
257
259
  switch (state) {
258
- case 'ready': return { color: '#22c55e', text: acp.label('已连接', 'Connected') }
259
- case 'starting': return { color: '#eab308', text: acp.label('启动中', 'Starting') }
260
- case 'error': return { color: '#ef4444', text: acp.label('错误', 'Error') }
260
+ case 'ready': return { color: 'var(--dsw-alias-state-success-primary, #22c55e)', text: acp.label('已连接', 'Connected') }
261
+ case 'starting': return { color: 'var(--dsw-alias-state-warn-primary, #eab308)', text: acp.label('启动中', 'Starting') }
262
+ case 'error': return { color: 'var(--dsw-alias-state-error-primary, #ef4444)', text: acp.label('错误', 'Error') }
261
263
  // A configured-but-resting agent is the normal state, not a fault: the
262
264
  // process is not kept resident and connects on demand the first time a DSH
263
265
  // chat turn uses it. Surfacing a runtime lifecycle here ("stopped"/"idle")
@@ -265,7 +267,7 @@ function stateVisual(state: AgentView['state'], acp: AcpStore): { color: string;
265
267
  // which the config-time 测试连接 (agent/test) already answers. So the
266
268
  // resting state just says 已配置.
267
269
  default: return {
268
- color: '#9ca3af',
270
+ color: 'var(--dsw-alias-state-idle-primary, #9ca3af)',
269
271
  text: acp.label('已配置', 'Configured'),
270
272
  title: acp.label(
271
273
  '已配置,进程不常驻。在 DSH 聊天中首次使用时自动连接;展开区可用「测试连接」验证命令能否正常起来。',
@@ -460,22 +462,15 @@ export function payloadEquals(payload: AgentSavePayload, current: AcpAgentDef):
460
462
  /**
461
463
  * The settings panel's permission-ledger rule.
462
464
  *
463
- * A pending ask ALWAYS gets an answerable card here, whatever `approvalRouted`
464
- * says. That flag reports that the host HAS an approval service — it does not
465
- * promise THIS ask reached the conversation. An ask lands in `ui.permissions`
466
- * only on the ledger path (`AgentBridge.handlePermissionRequest` broadcasts
467
- * `permission/request` there and nowhere else), so a pending entry is by
468
- * definition a question the conversation is not showing. The previous rule
469
- * (`approvalRouted` → a read-only count, no buttons) hid the only answer
470
- * entry exactly when the native route had degraded to unavailable, and the
471
- * ask then hung until its timeout.
472
- *
473
- * `fellBackFromConversation` is the case the card has to explain: the host
474
- * can route asks, and this one still did not make it.
465
+ * Pending asks are shown in the conversation card while that conversation is
466
+ * open; this card is the cross-session mirror for the ones it cannot show —
467
+ * asks belonging to another conversation, and `/acp` RPC-only asks with no
468
+ * chat identity at all. It therefore appears whenever ANY ask is pending: a
469
+ * pending entry with no conversation card would otherwise have no answer
470
+ * surface anywhere.
475
471
  */
476
- export function permissionLedgerCard(ui: Pick<UiState, 'approvalRouted' | 'permissions'>): { visible: boolean; fellBackFromConversation: boolean } {
477
- const visible = ui.permissions.length > 0
478
- return { visible, fellBackFromConversation: visible && ui.approvalRouted }
472
+ export function permissionLedgerCard(ui: Pick<UiState, 'permissions'>): { visible: boolean } {
473
+ return { visible: ui.permissions.length > 0 }
479
474
  }
480
475
 
481
476
  /**
@@ -860,7 +855,7 @@ export function AcpSettingsSection(props: WithStore) {
860
855
  <div className="dwa-page-status">
861
856
  <span
862
857
  className="dwa-dot"
863
- style={{ background: ui.stream === 'live' ? '#22c55e' : ui.stream === 'down' ? '#ef4444' : '#9ca3af' }}
858
+ style={{ background: ui.stream === 'live' ? 'var(--dsw-alias-state-success-primary, #22c55e)' : ui.stream === 'down' ? 'var(--dsw-alias-state-error-primary, #ef4444)' : 'var(--dsw-alias-state-idle-primary, #9ca3af)' }}
864
859
  title={ui.stream}
865
860
  />
866
861
  <span>{streamText(ui.stream, acp)}</span>
@@ -881,36 +876,37 @@ export function AcpSettingsSection(props: WithStore) {
881
876
  </div>
882
877
  </header>
883
878
 
884
- {/* Any pending ask gets an answerable card here, whatever
885
- `approvalRouted` says (see `permissionLedgerCard`): an ask is in
886
- `ui.permissions` only because it fell back to the ledger, which
887
- means the conversation is NOT showing it. */}
879
+ {/* The cross-session mirror: the conversation dock answers the asks
880
+ belonging to the open conversation; this card answers the rest. */}
888
881
  {ledger.visible && (
889
882
  <div className="dwa-card dwa-card-warn" style={{ marginBottom: 16 }}>
890
- <div style={{ fontWeight: 600, marginBottom: 8 }}>
883
+ <div style={{ fontWeight: 600, marginBottom: 4 }}>
891
884
  {acp.label('待处理权限请求', 'Pending Permission Requests')}
892
885
  </div>
893
- {ledger.fellBackFromConversation && (
894
- <div className="dwa-hint" style={{ marginBottom: 8 }}>
895
- {acp.label(
896
- '这些询问没有进入对话(审批服务本次不可用、问题从未到达对话,或会话没有对话身份),请在此回答。',
897
- '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.',
898
- )}
899
- </div>
900
- )}
886
+ <div className="dwa-hint" style={{ marginBottom: 8 }}>
887
+ {acp.label(
888
+ '对话打开时在那边的卡片上回答;这里兜底显示其他会话的询问。',
889
+ 'Answer in the conversation card while that conversation is open; this list covers the asks whose conversation is not.',
890
+ )}
891
+ </div>
901
892
  {ui.permissions.map(permission => (
902
893
  <div key={permission.requestId} style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6, flexWrap: 'wrap' }}>
903
- <span style={{ flex: 1, minWidth: 160 }}>{permission.title}</span>
894
+ <span style={{ flex: 1, minWidth: 160 }}>
895
+ <span style={{ fontWeight: 600 }}>{permission.toolName}</span>
896
+ {permission.reason !== undefined && (
897
+ <span style={{ color: 'var(--ntr-muted)' }}> · {permission.reason}</span>
898
+ )}
899
+ </span>
904
900
  {permission.options.map(option => (
905
901
  <button
906
902
  key={option.optionId}
907
903
  className="dwa-btn dwa-btn-primary"
908
- onClick={() => void acp.respondPermission(permission.requestId, option.optionId)}
904
+ onClick={() => { void acp.respondPermission(permission.requestId, option.optionId).catch(() => undefined) }}
909
905
  >
910
906
  {option.name}
911
907
  </button>
912
908
  ))}
913
- <button className="dwa-btn" onClick={() => void acp.respondPermission(permission.requestId)}>
909
+ <button className="dwa-btn" onClick={() => { void acp.respondPermission(permission.requestId).catch(() => undefined) }}>
914
910
  {acp.label('拒绝', 'Dismiss')}
915
911
  </button>
916
912
  </div>
@@ -1656,7 +1652,7 @@ function AgentDetails({
1656
1652
  <span style={{ fontWeight: 600, marginRight: 6 }}>
1657
1653
  {acp.label('官方测试默认命令:', 'Tested Default Command:')}
1658
1654
  </span>
1659
- <code className="dwa-input-mono" style={{ background: '#0000000a', padding: '2px 6px', borderRadius: 4 }}>
1655
+ <code className="dwa-input-mono" style={{ background: 'var(--dsw-alias-markdown-inline-code, #0000000a)', padding: '2px 6px', borderRadius: 4 }}>
1660
1656
  {detected.defaultCommand} {detected.defaultArgs.join(' ')}
1661
1657
  </code>
1662
1658
  </div>
@@ -1845,7 +1841,7 @@ function AgentDetails({
1845
1841
  fontSize: 12,
1846
1842
  whiteSpace: 'pre-wrap',
1847
1843
  wordBreak: 'break-all',
1848
- color: testResult.ok ? '#16a34a' : 'var(--ntr-danger)',
1844
+ color: testResult.ok ? 'var(--ntr-success)' : 'var(--ntr-danger)',
1849
1845
  }}
1850
1846
  >
1851
1847
  {testResult.ok ? '✓ ' : '✕ '}{testResult.text}
@@ -2333,7 +2329,7 @@ function ModelCatalogBlock({
2333
2329
  type="checkbox"
2334
2330
  checked={autoDiscover}
2335
2331
  onChange={e => void toggleAutoDiscover(e.target.checked)}
2336
- style={{ margin: 0, accentColor: '#2563eb' }}
2332
+ style={{ margin: 0, accentColor: 'var(--dsw-alias-state-business-primary, #2563eb)' }}
2337
2333
  />
2338
2334
  {acp.label('自动发现新模型', 'Auto-discover new models')}
2339
2335
  </label>
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Pending ACP permission asks for the staged DSH conversation, rendered as
3
+ * composer-width entries above the composer card (`conversation.input.dock`).
4
+ * The slot itself spans the conversation column; the dock mirrors the host's
5
+ * InputBar width formula so the cards line up with the input box.
6
+ *
7
+ * The host broadcasts every ask as `permission/request`, carrying the DSH chat
8
+ * session id when the ask belongs to a conversation. This dock is the primary
9
+ * answer surface: it renders exactly the asks whose `dshSessionId` is the
10
+ * conversation it was mounted for, and nothing otherwise. There is no
11
+ * ACP-mode gate — the frame set itself is the gate, so an ordinary
12
+ * conversation shows this card if and only if it actually has an ask.
13
+ *
14
+ * The ask's description and options are the agent's own: buttons appear in the
15
+ * agent's order with the agent's names (including "Always Allow"), and
16
+ * `kind` is not rendered. A separate cancel button answers with no option,
17
+ * which the host forwards as `cancelled`. No Enter/Escape handlers: the
18
+ * composer owns its keyboard, and these are ordinary focusable buttons.
19
+ *
20
+ * Multiple asks stack in ARRIVAL order, oldest first: the question ignored
21
+ * longest stays at the top. The list is capped in height by CSS and scrolls.
22
+ * Asks without a chat identity never appear here — they stay in the
23
+ * settings-panel ledger, the cross-session mirror.
24
+ */
25
+
26
+ import { useEffect, useState } from 'react'
27
+ import { ensureAcpStyle, useUi } from './acp-ui.tsx'
28
+ import {
29
+ deadlineRemainingLabel,
30
+ PERMISSION_DEADLINE_EXPIRED,
31
+ permissionsForSession,
32
+ } from './store.ts'
33
+ import type { AcpStore, PendingPermission } from './store.ts'
34
+
35
+ export interface PermissionDockProps {
36
+ /** Injected face: the store plus the chat session id this dock belongs to. */
37
+ acp: AcpStore
38
+ sessionId: string
39
+ }
40
+
41
+ /** Tick interval for the countdown (seconds are all the label shows). */
42
+ const COUNTDOWN_TICK_MS = 1000
43
+
44
+ export function PermissionDock({ acp, sessionId }: PermissionDockProps) {
45
+ ensureAcpStyle()
46
+ const ui = useUi(acp)
47
+ const dshSessionId = String(sessionId)
48
+ const pending = permissionsForSession(ui.permissions, dshSessionId)
49
+ const counting = pending.some(ask => ask.timeoutMode !== 'wait' && ask.deadlineAt !== undefined)
50
+ const [now, setNow] = useState(() => Date.now())
51
+ useEffect(() => {
52
+ if (!counting) return
53
+ setNow(Date.now())
54
+ const timer = setInterval(() => { setNow(Date.now()) }, COUNTDOWN_TICK_MS)
55
+ return () => { clearInterval(timer) }
56
+ }, [counting])
57
+ if (pending.length === 0) return null
58
+ return (
59
+ <div
60
+ className="ntr-permission-dock"
61
+ role="region"
62
+ aria-label={acp.label('待处理的权限请求', 'Pending permission requests')}
63
+ >
64
+ {pending.map(permission => (
65
+ <PermissionAskCard key={permission.requestId} acp={acp} permission={permission} now={now} />
66
+ ))}
67
+ </div>
68
+ )
69
+ }
70
+
71
+ function PermissionAskCard(props: { acp: AcpStore; permission: PendingPermission; now: number }) {
72
+ const { acp, permission, now } = props
73
+ const [busy, setBusy] = useState(false)
74
+ const [error, setError] = useState<string>()
75
+ const remaining = deadlineRemainingLabel(permission.deadlineAt, now, permission.timeoutMode)
76
+ const expired = remaining === PERMISSION_DEADLINE_EXPIRED
77
+ const respond = (optionId?: string): void => {
78
+ if (busy) return
79
+ setBusy(true)
80
+ setError(undefined)
81
+ void acp.respondPermission(permission.requestId, optionId)
82
+ // `respondPermission` reports to the store's notice too, but that notice
83
+ // lives on the settings page; this card pins the failure where the user
84
+ // is looking.
85
+ .catch((failure: unknown) => {
86
+ setError(failure instanceof Error ? failure.message : String(failure))
87
+ })
88
+ .finally(() => { setBusy(false) })
89
+ }
90
+ return (
91
+ <div className="ntr-permission-card">
92
+ <div className="ntr-permission-head">
93
+ <span className="ntr-permission-tool">{permission.toolName}</span>
94
+ {permission.reason !== undefined && (
95
+ <span className="ntr-permission-reason" title={permission.reason}>{permission.reason}</span>
96
+ )}
97
+ {remaining !== undefined && (
98
+ <span className={`ntr-permission-deadline${expired ? ' is-expired' : ''}`}>
99
+ {expired
100
+ ? acp.label('已超时', 'expired')
101
+ : acp.label(`剩余 ${remaining}`, `${remaining} left`)}
102
+ </span>
103
+ )}
104
+ </div>
105
+ <div className="ntr-permission-actions">
106
+ {permission.options.map(option => (
107
+ <button
108
+ key={option.optionId}
109
+ type="button"
110
+ className="ntr-permission-option"
111
+ disabled={busy}
112
+ title={option.name}
113
+ onClick={() => { respond(option.optionId) }}
114
+ >
115
+ {option.name}
116
+ </button>
117
+ ))}
118
+ <button
119
+ type="button"
120
+ className="ntr-permission-cancel"
121
+ disabled={busy}
122
+ title={acp.label('取消这次询问(不选择任何选项)', 'Cancel this ask without choosing an option')}
123
+ onClick={() => { respond() }}
124
+ >
125
+ {acp.label('取消', 'Cancel')}
126
+ </button>
127
+ </div>
128
+ {error !== undefined && (
129
+ <div className="ntr-permission-error" role="alert">{error}</div>
130
+ )}
131
+ </div>
132
+ )
133
+ }
@@ -88,10 +88,10 @@ export interface AcpClusterProps extends AcpChatInjected {
88
88
  }
89
89
 
90
90
  const STATE_COLORS: Record<AgentView['state'], string> = {
91
- ready: '#22c55e',
92
- starting: '#eab308',
93
- error: '#ef4444',
94
- stopped: '#9ca3af',
91
+ ready: 'var(--dsw-alias-state-success-primary, #22c55e)',
92
+ starting: 'var(--dsw-alias-state-warn-primary, #eab308)',
93
+ error: 'var(--dsw-alias-state-error-primary, #ef4444)',
94
+ stopped: 'var(--dsw-alias-state-idle-primary, #9ca3af)',
95
95
  }
96
96
 
97
97
  function agentStateText(acp: AcpChatInjected['acp'], state: AgentView['state']): string {