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.
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/cordis.patch.yml +18 -18
- package/lib/bridge.js +120 -291
- package/lib/client/acp-ui.js +60 -32
- package/lib/client/index.js +17 -0
- package/lib/client/panel.js +37 -42
- package/lib/client/permission-dock.js +72 -0
- package/lib/client/session-controls.js +4 -4
- package/lib/client/store.js +58 -12
- package/lib/client.js +296 -95
- package/lib/client.js.map +1 -1
- package/lib/impl.js +30 -19
- package/lib/index.js +6 -51
- package/lib/types/bridge.d.ts +60 -154
- package/lib/types/client/panel.d.ts +12 -19
- package/lib/types/client/permission-dock.d.ts +31 -0
- package/lib/types/client/store.d.ts +42 -12
- package/lib/types/impl.d.ts +31 -17
- package/lib/types/protocol.d.ts +27 -12
- package/package.json +1 -1
- package/src/bridge.ts +147 -357
- package/src/client/acp-ui.tsx +60 -32
- package/src/client/index.tsx +18 -0
- package/src/client/panel.tsx +54 -58
- package/src/client/permission-dock.tsx +133 -0
- package/src/client/session-controls.tsx +4 -4
- package/src/client/store.ts +81 -25
- package/src/impl.ts +49 -29
- package/src/index.ts +6 -65
- package/src/protocol.ts +32 -12
package/src/client/acp-ui.tsx
CHANGED
|
@@ -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(--
|
|
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-
|
|
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-
|
|
872
|
-
.ntr-acp-menu-error { margin: 0 10px 6px; padding: 6px 8px; border-radius: 6px; background: #
|
|
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-
|
|
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-
|
|
882
|
-
.ntr-acp-menu-empty { padding: 10px; text-align: center; color: var(--dsw-alias-
|
|
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-
|
|
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: #
|
|
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: #
|
|
895
|
-
.ntr-acp-badge.is-idle { color: var(--dsw-alias-
|
|
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-
|
|
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-
|
|
910
|
-
.ntr-acp-tool-meta { font-size: 11px; color: var(--dsw-alias-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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: #
|
|
938
|
-
.ntr-acp-notice.is-info { background: var(--dsw-alias-bg-layer-2, #f3f4f6); color: var(--dsw-alias-
|
|
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-
|
|
942
|
-
.ntr-acp-empty-hint { color: var(--dsw-alias-
|
|
943
|
-
.ntr-acp-seat-diag { font-family: var(--
|
|
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
|
package/src/client/index.tsx
CHANGED
|
@@ -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 —
|
package/src/client/panel.tsx
CHANGED
|
@@ -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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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-
|
|
55
|
-
--ntr-danger:
|
|
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-
|
|
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-
|
|
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: #
|
|
96
|
-
.dwa-badge-warn { background: #
|
|
97
|
-
.dwa-badge-info { background: #
|
|
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-
|
|
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: #
|
|
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-
|
|
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: #
|
|
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-
|
|
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
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
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, '
|
|
477
|
-
|
|
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
|
-
{/*
|
|
885
|
-
|
|
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:
|
|
883
|
+
<div style={{ fontWeight: 600, marginBottom: 4 }}>
|
|
891
884
|
{acp.label('待处理权限请求', 'Pending Permission Requests')}
|
|
892
885
|
</div>
|
|
893
|
-
{
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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 }}>
|
|
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 ? '
|
|
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 {
|