@xiaoso/dsh-tool-plus 0.1.0-beta.6 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -75,6 +75,15 @@ window.__ModuleLoader__.load({
75
75
  group: "behavior",
76
76
  tool: "bash"
77
77
  },
78
+ {
79
+ name: "rmSafe",
80
+ kind: "boolean",
81
+ default: true,
82
+ labelKey: "rmSafe",
83
+ hintKey: "rmSafeHint",
84
+ group: "behavior",
85
+ tool: "bash"
86
+ },
78
87
  {
79
88
  name: "useShellCommandWrapper",
80
89
  kind: "boolean",
@@ -726,57 +735,83 @@ window.__ModuleLoader__.load({
726
735
  TOOL_PLUS_FIELDS.map((field) => field.name);
727
736
  //#endregion
728
737
  //#region src/client/rows.tsx
729
- /** Shared row control CSS, keyed by `data-plugin-css` and injected once. */
738
+ /**
739
+ * Shared settings-row controls for the tool-plus settings surfaces: number
740
+ * input, select pill, and toggle switch rows, styled against the app's
741
+ * `--dsw-alias-*` design tokens. Extracted from the Bash card so the Plugins
742
+ * card and the Tool Plus settings section render identical controls.
743
+ * @module @xiaoso/dsh-tool-plus/client
744
+ */
745
+ /**
746
+ * Shared row control CSS, keyed by `data-plugin-css` and injected once.
747
+ *
748
+ * Motion discipline: every moving control animates transform/opacity only
749
+ * (GPU-composited); color transitions fire on hover/focus state changes
750
+ * only. Everything is disabled under prefers-reduced-motion. All colors are
751
+ * `--dsw-alias-*` host tokens (or color-mix of them), so light/dark themes
752
+ * adapt automatically.
753
+ */
730
754
  const SETTINGS_ROWS_CSS = `
731
- .tp-row{display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}
755
+ .tp-row{display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--dsw-alias-border-l1);transition:background .16s ease}
732
756
  .tp-row:last-child{border-bottom:none}
733
757
  .tp-rowText{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;padding-right:24px}
734
758
  .tp-rowTitle{font-size:14px;font-weight:400;line-height:22px;color:var(--dsw-alias-label-primary)}
735
759
  .tp-rowDesc{font-size:12px;font-weight:400;line-height:18px;color:var(--dsw-alias-label-tertiary)}
736
760
  .tp-badges{display:inline-flex;align-items:center;gap:8px}
737
- .tp-badge{border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;white-space:nowrap;font-weight:500;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
738
- .tp-reset{border:none;background:none;padding:0;font:inherit;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);cursor:pointer}
761
+ .tp-badge{display:inline-flex;align-items:center;gap:6px;padding:1px 0;font-size:11px;line-height:17px;white-space:nowrap;font-weight:500;color:var(--dsw-alias-label-tertiary)}
762
+ .tp-badge::before{content:'';flex:none;width:5px;height:5px;border-radius:999px;background:var(--dsw-alias-state-business-primary)}
763
+ .tp-reset{position:relative;border:none;background:none;padding:2px 0;font:inherit;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);cursor:pointer;transition:color .16s ease}
764
+ .tp-reset::after{content:'';position:absolute;left:0;right:0;bottom:0;height:1px;border-radius:1px;background:currentColor;opacity:0;transform:scaleX(.6);transform-origin:left center;transition:opacity .16s ease,transform .2s cubic-bezier(.22,1,.36,1)}
739
765
  .tp-reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
766
+ .tp-reset:hover:not(:disabled)::after{opacity:.6;transform:scaleX(1)}
740
767
  .tp-reset:disabled{cursor:default}
741
- .tp-numberField{display:flex;flex-direction:column;gap:6px;padding:12px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}
742
- .tp-numberField:last-child{border-bottom:none}
743
- .tp-numberHead{display:flex;align-items:center;gap:8px}
744
- .tp-numberLabel{flex:1;min-width:0;font-size:13px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary)}
745
- .tp-input{height:34px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-3);font:inherit;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary);width:100%;box-sizing:border-box}
746
- .tp-input:focus-visible{outline:none;border-color:var(--dsw-alias-brand-primary)}
768
+ .tp-reset:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px;border-radius:2px}
769
+ .tp-input{flex:none;width:128px;height:34px;padding:0 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);font:inherit;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary);box-sizing:border-box;transition:border-color .16s ease}
770
+ .tp-input:hover:not(:disabled):not(:focus-visible){border-color:var(--dsw-alias-border-l3)}
771
+ .tp-input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px;border-color:var(--dsw-alias-border-l3)}
747
772
  .tp-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
748
773
  .tp-input.tp-invalid{border-color:var(--dsw-alias-label-error)}
749
- .tp-invalidMsg{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)}
750
- .tp-hint{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
751
- .tp-selectTrigger{display:inline-flex;align-items:center;gap:12px;height:36px;padding:0 14px;border:none;border-radius:18px;background:var(--dsw-alias-bg-module-platform);font:inherit;font-size:14px;line-height:22px;color:var(--dsw-alias-label-primary);cursor:pointer;flex-shrink:0}
774
+ .tp-input.tp-invalid:focus-visible{outline-color:var(--dsw-alias-state-error-primary)}
775
+ .tp-invalidText{color:var(--dsw-alias-label-error);animation:tp-soft-in .18s ease both}
752
776
  .tp-selectAnchor{display:inline-flex;flex-shrink:0}
753
- .tp-selectTrigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
754
- .tp-selectTrigger:disabled{cursor:default}
755
- .tp-selectTrigger:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
756
- .tp-selectChevron{flex:none;color:var(--dsw-alias-label-tertiary);display:inline-flex;transition:transform .16s}
777
+ .tp-selectTrigger{display:inline-flex;align-items:center;gap:8px;height:34px;padding:0 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-module-platform);font:inherit;font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);cursor:pointer;flex-shrink:0;transition:background .16s ease,border-color .16s ease,transform .1s ease}
778
+ .tp-selectTrigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l3)}
779
+ .tp-selectTrigger:active:not(:disabled){transform:scale(.97)}
780
+ .tp-selectTrigger:disabled{opacity:.55;cursor:default}
781
+ .tp-selectTrigger:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
782
+ .tp-selectChevron{flex:none;color:var(--dsw-alias-label-tertiary);display:inline-flex;transition:transform .2s cubic-bezier(.22,1,.36,1)}
757
783
  .tp-selectTrigger[aria-expanded="true"] .tp-selectChevron{transform:rotate(180deg)}
758
- .tp-switch{position:relative;width:36px;height:20px;flex:none}
759
- .tp-switch input{position:absolute;opacity:0;width:100%;height:100%;margin:0;cursor:pointer}
760
- .tp-switchTrack{position:absolute;inset:0;border-radius:999px;background:var(--dsw-alias-border-l2);transition:background .16s;pointer-events:none}
784
+ .tp-switch{position:relative;width:40px;height:24px;flex:none}
785
+ .tp-switch input{position:absolute;opacity:0;width:100%;height:100%;margin:0;cursor:pointer;z-index:1}
786
+ .tp-switchTrack{position:absolute;inset:0;border-radius:999px;background:var(--dsw-alias-border-l2);transition:background .18s ease;pointer-events:none}
787
+ .tp-row:hover .tp-switchTrack{background:var(--dsw-alias-border-l3)}
761
788
  .tp-switch input:checked+.tp-switchTrack{background:var(--dsw-alias-brand-primary)}
762
- .tp-switch input:focus-visible+.tp-switchTrack{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
763
- .tp-thumb{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);transition:transform .16s;pointer-events:none}
764
- .tp-switch input:checked~.tp-thumb{transform:translateX(16px)}
765
- .tp-actionField{display:flex;flex-direction:column;gap:8px;padding:12px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}
789
+ .tp-row:hover .tp-switch input:checked+.tp-switchTrack{background:var(--dsw-alias-brand-primary)}
790
+ .tp-switch input:focus-visible+.tp-switchTrack{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
791
+ .tp-thumb{position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);box-shadow:0 1px 3px rgba(0,0,0,.14),0 1px 1px rgba(0,0,0,.08);transition:transform .2s cubic-bezier(.34,1.3,.5,1);transform-origin:left center;pointer-events:none}
792
+ .tp-switch input:checked~.tp-thumb{transform:translateX(16px);transform-origin:right center}
793
+ .tp-switch input:active:not(:disabled)~.tp-thumb{transform:scaleX(1.2)}
794
+ .tp-switch input:checked:active:not(:disabled)~.tp-thumb{transform:translateX(16px) scaleX(1.2)}
795
+ .tp-switch input:disabled{cursor:default}
796
+ .tp-switch input:disabled~.tp-switchTrack,.tp-switch input:disabled~.tp-thumb{opacity:.45}
797
+ .tp-actionField{display:flex;flex-direction:column;gap:8px;padding:12px 0;border-bottom:1px solid var(--dsw-alias-border-l1)}
766
798
  .tp-actionField:last-child{border-bottom:none}
767
799
  .tp-actionHead{display:flex;align-items:center;gap:12px}
768
800
  .tp-actionInfo{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
769
801
  .tp-actionTitle{font-size:14px;font-weight:400;line-height:22px;color:var(--dsw-alias-label-primary)}
770
802
  .tp-actionDesc{font-size:12px;font-weight:400;line-height:18px;color:var(--dsw-alias-label-tertiary)}
771
- .tp-actionButton{appearance:none;flex:none;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-3);padding:5px 14px;font:inherit;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary);cursor:pointer;transition:background .14s}
772
- .tp-actionButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
803
+ .tp-actionButton{appearance:none;flex:none;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);padding:5px 14px;font:inherit;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary);cursor:pointer;transition:background .16s ease,border-color .16s ease,transform .1s ease}
804
+ .tp-actionButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l3)}
805
+ .tp-actionButton:active:not(:disabled){transform:scale(.97)}
773
806
  .tp-actionButton:disabled{opacity:.55;cursor:default}
774
- .tp-actionButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
775
- .tp-actionResult{margin:0;font-size:12px;line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere}
807
+ .tp-actionButton:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
808
+ .tp-actionResult{margin:0;font-size:12px;line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere;animation:tp-soft-in .22s cubic-bezier(.22,1,.36,1) both}
776
809
  .tp-actionResult[data-state="ok"]{color:var(--dsw-alias-label-secondary)}
777
810
  .tp-actionResult[data-state="error"]{color:var(--dsw-alias-label-error)}
811
+ @keyframes tp-soft-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
778
812
  @media (prefers-reduced-motion: reduce){
779
- .tp-switch,.tp-switchTrack,.tp-thumb,.tp-selectTrigger,.tp-selectChevron{transition:none}
813
+ .tp-row,.tp-reset,.tp-reset::after,.tp-input,.tp-selectTrigger,.tp-selectChevron,.tp-switchTrack,.tp-thumb,.tp-actionButton{transition:none}
814
+ .tp-invalidText,.tp-actionResult{animation:none}
780
815
  }
781
816
  `;
782
817
  /** Inject the shared row stylesheet once per page. */
@@ -795,46 +830,51 @@ window.__ModuleLoader__.load({
795
830
  function hasOwn(obj, key) {
796
831
  return typeof obj === "object" && obj !== null && Object.prototype.hasOwnProperty.call(obj, key);
797
832
  }
798
- /** Number field: Header (label + override badge + reset), Input, Hint below. */
833
+ /**
834
+ * Number row: same left-text / right-control anatomy as toggle and select
835
+ * rows, with the input right-aligned at a fixed width. The title is wired to
836
+ * the input (htmlFor), so clicking the title focuses the input; the
837
+ * description and the rest of the row are inert — each side manages its own
838
+ * interaction.
839
+ */
799
840
  function NumberRow(props) {
800
841
  const { t, control, disabled, onEdit, onReset } = props;
842
+ const inputId = (0, react.useId)();
801
843
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
802
- className: "tp-numberField",
844
+ className: "tp-row",
803
845
  children: [
804
846
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
805
- className: "tp-numberHead",
847
+ className: "tp-rowText",
806
848
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
807
- className: "tp-numberLabel",
849
+ className: "tp-rowTitle",
850
+ htmlFor: inputId,
808
851
  children: t(control.labelKey)
809
- }), control.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
810
- className: "tp-badges",
811
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
812
- className: "tp-badge",
813
- children: t("overridden")
814
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
815
- type: "button",
816
- className: "tp-reset",
817
- disabled,
818
- onClick: () => onReset(control.field),
819
- children: t("reset")
820
- })]
821
- }) : null]
852
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
853
+ className: "tp-rowDesc" + (control.invalid ? " tp-invalidText" : ""),
854
+ role: control.invalid ? "status" : void 0,
855
+ children: control.invalid ? t("invalidNumber") : t(control.hintKey)
856
+ })]
822
857
  }),
858
+ control.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
859
+ className: "tp-badges",
860
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
861
+ className: "tp-badge",
862
+ children: t("overridden")
863
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
864
+ type: "button",
865
+ className: "tp-reset",
866
+ disabled,
867
+ onClick: () => onReset(control.field),
868
+ children: t("reset")
869
+ })]
870
+ }) : null,
823
871
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
872
+ id: inputId,
824
873
  type: "number",
825
- "aria-label": t(control.labelKey),
826
874
  className: "tp-input" + (control.invalid ? " tp-invalid" : ""),
827
875
  disabled,
828
876
  value: control.text,
829
877
  onChange: (e) => onEdit(control.field, e.target.value)
830
- }),
831
- control.invalid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
832
- className: "tp-invalidMsg",
833
- role: "status",
834
- children: t("invalidNumber")
835
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
836
- className: "tp-hint",
837
- children: t(control.hintKey)
838
878
  })
839
879
  ]
840
880
  });
@@ -1060,11 +1100,13 @@ window.__ModuleLoader__.load({
1060
1100
  .tp-card.tp-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
1061
1101
  .tp-header{width:100%;appearance:none;border:0;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer;display:flex;align-items:center;gap:12px;padding:14px 16px;border-radius:12px}
1062
1102
  .tp-header:active{background:var(--dsw-alias-bg-layer-2)}
1063
- .tp-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
1103
+ .tp-header:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
1064
1104
  .tp-headText{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
1065
1105
  .tp-name{font-size:15px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary)}
1066
1106
  .tp-desc{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
1067
- .tp-pending{flex:none;border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;font-weight:500;white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
1107
+ .tp-pending{display:inline-flex;align-items:center;gap:6px;flex:none;border-radius:999px;padding:2px 10px;font-size:11px;line-height:17px;font-weight:500;white-space:nowrap;background:var(--dsw-alias-state-business-tertiary);color:var(--dsw-alias-state-business-primary)}
1108
+ .tp-pending::before{content:'';flex:none;width:6px;height:6px;border-radius:999px;background:var(--dsw-alias-state-business-primary);animation:tp-pulse 1.8s ease-in-out infinite}
1109
+ @keyframes tp-pulse{0%,100%{opacity:.35;transform:scale(.85)}50%{opacity:1;transform:scale(1)}}
1068
1110
  .tp-chevron{flex:none;color:var(--dsw-alias-label-tertiary);transition:transform .16s;display:inline-flex}
1069
1111
  .tp-chevron.tp-open{transform:rotate(180deg)}
1070
1112
  .tp-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-top:4px;display:grid;grid-template-rows:0fr;opacity:0;transition:grid-template-rows .22s cubic-bezier(.22,.61,.36,1),opacity .16s ease}
@@ -1082,10 +1124,12 @@ window.__ModuleLoader__.load({
1082
1124
  .tp-discard{border-color:var(--dsw-alias-border-l2);background:none;color:var(--dsw-alias-label-secondary)}
1083
1125
  .tp-discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
1084
1126
  .tp-save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
1127
+ .tp-save:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}
1085
1128
  .tp-discard:disabled,.tp-save:disabled{opacity:.4;cursor:default}
1086
- .tp-discard:focus-visible,.tp-save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
1129
+ .tp-discard:focus-visible,.tp-save:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
1087
1130
  @media (prefers-reduced-motion: reduce){
1088
1131
  .tp-card,.tp-header,.tp-chevron,.tp-body,.tp-thumb,.tp-switchTrack,.tp-discard,.tp-save{transition:none}
1132
+ .tp-pending::before{animation:none}
1089
1133
  }
1090
1134
  `;
1091
1135
  /** Inject the card stylesheet once per page; the loader removes plugin-owned style tags on unload. */
@@ -1595,6 +1639,8 @@ window.__ModuleLoader__.load({
1595
1639
  */
1596
1640
  /** Logical RPC channel registered by the host half of this plugin. */
1597
1641
  const TOOL_PLUS_RPC_CHANNEL = "/tool-plus";
1642
+ /** Endpoint that reports the rmSafe injection status (query = ensure + verify). */
1643
+ const RM_SAFE_STATUS_ENDPOINT = "rmSafe/status";
1598
1644
  //#endregion
1599
1645
  //#region src/client/web-connection-rpc.ts
1600
1646
  /**
@@ -1983,6 +2029,27 @@ window.__ModuleLoader__.load({
1983
2029
  };
1984
2030
  }
1985
2031
  //#endregion
2032
+ //#region src/client/rm-safe-status-text.ts
2033
+ /** Locale key of one rmSafe failure reason. */
2034
+ function rmSafeReasonKey(reason) {
2035
+ switch (reason) {
2036
+ case "snapshot-unavailable": return "rmSafeReasonSnapshotUnavailable";
2037
+ case "cli-missing": return "rmSafeReasonCliMissing";
2038
+ case "script-write-failed": return "rmSafeReasonScriptWriteFailed";
2039
+ case "snapshot-write-failed": return "rmSafeReasonSnapshotWriteFailed";
2040
+ case "runtime-not-effective": return "rmSafeReasonRuntimeNotEffective";
2041
+ }
2042
+ }
2043
+ /**
2044
+ * Localized text of one rmSafe/status result. `disabled` maps to the empty
2045
+ * string — the caller decides not to surface it.
2046
+ */
2047
+ function rmSafeStatusText(t, value) {
2048
+ if (value.status === "failed") return t("rmSafeStatusFailed").replace("{reason}", t(rmSafeReasonKey(value.reason)));
2049
+ if (value.status === "disabled") return "";
2050
+ return value.runtime === "function" ? t("rmSafeStatusInjected") : t("rmSafeStatusInjectedUnverified");
2051
+ }
2052
+ //#endregion
1986
2053
  //#region src/client/ToolPlusSection.tsx
1987
2054
  /**
1988
2055
  * Tool Plus settings section — the plugin's own page in the Settings panel,
@@ -2002,37 +2069,69 @@ window.__ModuleLoader__.load({
2002
2069
  * Tab chrome mirrors the official Plugins settings section
2003
2070
  * (ui-settings-plugins): `role="tablist"` + tab/panel roles, aria wiring,
2004
2071
  * and arrow/Home/End keyboard roaming.
2072
+ *
2073
+ * The page is pinned to the host settings pane: the heading, intro and tablist
2074
+ * stay put, the save bar sits at the bottom, and the tab panel is the only
2075
+ * scroller — so the scrollbar spans just the band between them.
2005
2076
  * @module @xiaoso/dsh-tool-plus/client
2006
2077
  */
2007
- /** Page CSS, keyed by `data-plugin-css` and injected once. */
2078
+ /**
2079
+ * Page CSS, keyed by `data-plugin-css` and injected once.
2080
+ *
2081
+ * Design language: official settings-section rhythm (18/600 heading, 13px
2082
+ * tertiary intro, 760px content column) on the host's `--dsw-alias-*`
2083
+ * tokens, so both themes adapt automatically. Tab chrome is the official
2084
+ * underline tablist (like the Plugins section), upgraded with a single
2085
+ * sliding brand-blue underline; group cards float above the modal surface
2086
+ * (layer-3). No backdrop-filter anywhere — solid token colors only.
2087
+ * All motion is transform/opacity only (GPU-composited): the underline
2088
+ * FLIP is driven from JS, entrance stagger and panel fades are CSS
2089
+ * keyframes, and the unsaved dot pulses opacity/scale. Everything shuts
2090
+ * off under prefers-reduced-motion (CSS here, matchMedia for the
2091
+ * JS-driven underline).
2092
+ */
2008
2093
  const CSS = `
2009
- .tps{display:flex;flex-direction:column;gap:16px;padding:20px 24px}
2010
- .tps-heading{margin:0;font-size:18px;font-weight:600;line-height:26px;color:var(--dsw-alias-label-primary)}
2011
- .tps-intro{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary)}
2012
- .tps-tabs{display:flex;flex-wrap:wrap;gap:4px;padding:4px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-3)}
2013
- .tps-tab{appearance:none;border:none;background:none;font:inherit;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary);padding:6px 14px;border-radius:8px;cursor:pointer;transition:background .14s,color .14s}
2014
- .tps-tab:hover:not([data-active="true"]){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
2015
- .tps-tab[data-active="true"]{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 1px 2px var(--dsw-alias-border-l2)}
2016
- .tps-tab:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
2017
- .tps-panel{display:flex;flex-direction:column;gap:12px}
2018
- .tps-group{display:flex;flex-direction:column;gap:0;padding:4px 0}
2019
- .tps-groupTitle{font-size:13px;font-weight:600;line-height:20px;color:var(--dsw-alias-label-primary);margin:0 0 4px}
2020
- .tps-empty{border:1px dashed var(--dsw-alias-border-l2);border-radius:12px;padding:20px 16px;display:flex;flex-direction:column;gap:4px;background:var(--dsw-alias-bg-layer-3)}
2094
+ .tps-section{box-sizing:border-box;display:flex;flex-direction:column;gap:16px;padding-top:2px}
2095
+ .tps-heading{margin:0;font-size:18px;font-weight:600;line-height:26px;color:var(--dsw-alias-label-primary);animation:tps-rise .38s cubic-bezier(.22,1,.36,1) both}
2096
+ .tps-intro{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary);max-width:64ch;animation:tps-rise .38s cubic-bezier(.22,1,.36,1) .05s both}
2097
+ .tps-tabs{position:relative;display:flex;align-items:flex-end;gap:22px;border-bottom:1px solid var(--dsw-alias-border-l2);margin-top:2px;animation:tps-rise .38s cubic-bezier(.22,1,.36,1) .1s both}
2098
+ .tps-tab{position:relative;appearance:none;border:none;background:none;font:inherit;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary);padding:7px 1px 9px;cursor:pointer;white-space:nowrap;transition:color .16s ease}
2099
+ .tps-tab:hover:not([data-active="true"]){color:var(--dsw-alias-label-primary)}
2100
+ .tps-tab[data-active="true"]{color:var(--dsw-alias-label-primary)}
2101
+ .tps-tab:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px;border-radius:2px}
2102
+ .tps-tabIndicator{position:absolute;bottom:-1px;left:0;height:2px;border-radius:2px 2px 0 0;background:var(--dsw-alias-state-business-primary);pointer-events:none;transform-origin:left center;will-change:transform}
2103
+ .tps-panel{display:flex;flex:1 1 auto;flex-direction:column;gap:14px;min-width:0;min-height:0;overflow-y:auto;overscroll-behavior:contain;scrollbar-gutter:stable;scrollbar-width:thin;animation:tps-panel-in .24s cubic-bezier(.22,1,.36,1) both}
2104
+ .tps-panel::-webkit-scrollbar{width:10px}
2105
+ .tps-panel::-webkit-scrollbar-track{background:transparent}
2106
+ .tps-panel::-webkit-scrollbar-thumb{border:3px solid transparent;border-radius:999px;background-color:color-mix(in srgb, var(--dsw-alias-label-tertiary) 45%, transparent);background-clip:content-box}
2107
+ .tps-panel::-webkit-scrollbar-thumb:hover{background-color:color-mix(in srgb, var(--dsw-alias-label-tertiary) 75%, transparent)}
2108
+ .tps-group{display:flex;flex-direction:column}
2109
+ .tps-groupTitle{font-size:13px;font-weight:600;line-height:20px;color:var(--dsw-alias-label-primary);margin:0 0 6px;padding:0 2px}
2110
+ .tps-card{display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-3);padding:0 16px}
2111
+ .tps-empty{border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;padding:24px 16px;display:flex;flex-direction:column;align-items:center;text-align:center;gap:4px}
2021
2112
  .tps-emptyTitle{margin:0;font-size:14px;font-weight:600;line-height:22px;color:var(--dsw-alias-label-primary)}
2022
2113
  .tps-emptyHint{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
2023
- .tps-readOnly{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
2024
- .tps-footer{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 0 0;border-top:1px solid var(--dsw-alias-border-l2)}
2114
+ .tps-readOnly{margin:0;padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-3);font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
2115
+ .tps-footer{flex:none;display:flex;align-items:center;justify-content:flex-end;gap:10px;padding:12px 0;border-top:1px solid var(--dsw-alias-border-l2)}
2025
2116
  .tps-failed{flex:1;min-width:0;margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)}
2026
2117
  .tps-applies{flex:1;min-width:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
2027
- .tps-discard,.tps-save{appearance:none;border:1px solid transparent;border-radius:8px;padding:5px 14px;font:inherit;font-size:13px;line-height:1.5;cursor:pointer;transition:transform .1s ease}
2118
+ .tps-unsaved{display:inline-flex;align-items:center;gap:6px;flex:none;border-radius:999px;padding:2px 10px;font-size:11px;line-height:17px;font-weight:500;color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-tertiary)}
2119
+ .tps-unsavedDot{flex:none;width:6px;height:6px;border-radius:999px;background:var(--dsw-alias-state-business-primary);animation:tps-pulse 1.8s ease-in-out infinite}
2120
+ .tps-discard,.tps-save{appearance:none;border:1px solid transparent;border-radius:8px;padding:5px 14px;font:inherit;font-size:13px;line-height:1.5;cursor:pointer;transition:transform .1s ease,background .16s ease,border-color .16s ease,color .16s ease}
2028
2121
  .tps-discard:active:not(:disabled),.tps-save:active:not(:disabled){transform:scale(.97)}
2029
2122
  .tps-discard{border-color:var(--dsw-alias-border-l2);background:none;color:var(--dsw-alias-label-secondary)}
2030
- .tps-discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
2123
+ .tps-discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l4)}
2031
2124
  .tps-save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
2125
+ .tps-save:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}
2032
2126
  .tps-discard:disabled,.tps-save:disabled{opacity:.4;cursor:default}
2033
- .tps-discard:focus-visible,.tps-save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
2127
+ .tps-discard:focus-visible,.tps-save:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
2128
+ @keyframes tps-rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
2129
+ @keyframes tps-panel-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
2130
+ @keyframes tps-pulse{0%,100%{opacity:.35;transform:scale(.85)}50%{opacity:1;transform:scale(1)}}
2034
2131
  @media (prefers-reduced-motion: reduce){
2132
+ .tps-heading,.tps-intro,.tps-tabs,.tps-panel,.tps-unsavedDot{animation:none}
2035
2133
  .tps-tab,.tps-discard,.tps-save{transition:none}
2134
+ .tps-tabIndicator{will-change:auto}
2036
2135
  }
2037
2136
  `;
2038
2137
  /** Inject the section stylesheet once per page. */
@@ -2052,6 +2151,17 @@ window.__ModuleLoader__.load({
2052
2151
  const TOOL_TABS = TOOL_PLUS_TABS;
2053
2152
  /** All fields the page edits (one shared form). */
2054
2153
  const ALL_FIELDS = TOOL_PLUS_FIELDS;
2154
+ /** Floor for the pinned page height, so an unusually short pane never collapses the panel. */
2155
+ const MIN_PAGE_HEIGHT = 240;
2156
+ /**
2157
+ * Nearest ancestor that scrolls vertically — the host settings pane hosting
2158
+ * this section. The pane is app chrome with no stable class, so it is found by
2159
+ * walking up from the section root.
2160
+ */
2161
+ function scrollPaneOf(element) {
2162
+ for (let node = element.parentElement; node !== null; node = node.parentElement) if (/(auto|scroll|overlay)/.test(getComputedStyle(node).overflowY)) return node;
2163
+ return null;
2164
+ }
2055
2165
  /** Current effective value of a boolean/select control (staged or resolved). */
2056
2166
  function controlValue(form, field) {
2057
2167
  for (const group of form.groups) {
@@ -2124,48 +2234,51 @@ window.__ModuleLoader__.load({
2124
2234
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
2125
2235
  className: "tps-groupTitle",
2126
2236
  children: t(TOOL_PLUS_GROUP_LABELS[group.group] ?? "title")
2127
- }), group.fields.map((field) => {
2128
- if (field.kind === "action") {
2129
- const actionControl = {
2130
- field: field.name,
2131
- labelKey: field.labelKey,
2132
- hintKey: field.hintKey,
2133
- actionKey: field.actionKey ?? ""
2134
- };
2135
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionRow, {
2237
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2238
+ className: "tps-card",
2239
+ children: group.fields.map((field) => {
2240
+ if (field.kind === "action") {
2241
+ const actionControl = {
2242
+ field: field.name,
2243
+ labelKey: field.labelKey,
2244
+ hintKey: field.hintKey,
2245
+ actionKey: field.actionKey ?? ""
2246
+ };
2247
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionRow, {
2248
+ t,
2249
+ control: actionControl,
2250
+ disabled: !writable,
2251
+ running: actionState[field.name] === "running",
2252
+ result: actionResults[field.name] ?? null,
2253
+ onRun: (_actionKey) => runAction(field)
2254
+ }, field.name);
2255
+ }
2256
+ const entry = controlOf(field);
2257
+ if (entry === void 0) return null;
2258
+ if (entry.kind === "number") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberRow, {
2136
2259
  t,
2137
- control: actionControl,
2260
+ control: entry.control,
2138
2261
  disabled: !writable,
2139
- running: actionState[field.name] === "running",
2140
- result: actionResults[field.name] ?? null,
2141
- onRun: (_actionKey) => runAction(field)
2262
+ onEdit: form.actions.edit,
2263
+ onReset: form.actions.resetField
2142
2264
  }, field.name);
2143
- }
2144
- const entry = controlOf(field);
2145
- if (entry === void 0) return null;
2146
- if (entry.kind === "number") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberRow, {
2147
- t,
2148
- control: entry.control,
2149
- disabled: !writable,
2150
- onEdit: form.actions.edit,
2151
- onReset: form.actions.resetField
2152
- }, field.name);
2153
- if (entry.kind === "select") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectRow, {
2154
- t,
2155
- control: entry.control,
2156
- disabled: !writable,
2157
- open: openSelect === field.name,
2158
- onOpenChange,
2159
- onEdit: form.editSelect,
2160
- onReset: form.actions.resetField
2161
- }, field.name);
2162
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
2163
- t,
2164
- control: entry.control,
2165
- disabled: !writable,
2166
- onToggle: form.onToggle,
2167
- onReset: form.actions.resetField
2168
- }, field.name);
2265
+ if (entry.kind === "select") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectRow, {
2266
+ t,
2267
+ control: entry.control,
2268
+ disabled: !writable,
2269
+ open: openSelect === field.name,
2270
+ onOpenChange,
2271
+ onEdit: form.editSelect,
2272
+ onReset: form.actions.resetField
2273
+ }, field.name);
2274
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
2275
+ t,
2276
+ control: entry.control,
2277
+ disabled: !writable,
2278
+ onToggle: form.onToggle,
2279
+ onReset: form.actions.resetField
2280
+ }, field.name);
2281
+ })
2169
2282
  })]
2170
2283
  }, group.group)) });
2171
2284
  }
@@ -2173,12 +2286,34 @@ window.__ModuleLoader__.load({
2173
2286
  function ToolPlusSection(props) {
2174
2287
  const { t, scope } = props;
2175
2288
  const form = useToolForm(scope, ALL_FIELDS);
2289
+ const sectionRef = (0, react.useRef)(null);
2176
2290
  const tabsId = (0, react.useRef)(`tps-${Math.random().toString(36).slice(2, 8)}`);
2177
2291
  const tabRefs = (0, react.useRef)([]);
2178
2292
  const [activeId, setActiveId] = (0, react.useState)();
2179
2293
  const [openSelect, setOpenSelect] = (0, react.useState)(null);
2180
2294
  const [actionState, setActionState] = (0, react.useState)({});
2181
2295
  const [actionResults, setActionResults] = (0, react.useState)({});
2296
+ const [rmSafeToast, setRmSafeToast] = (0, react.useState)(null);
2297
+ const handleSave = (0, react.useCallback)(async () => {
2298
+ const before = Boolean(scope.getSnapshot().value?.rmSafe ?? true);
2299
+ await form.actions.save();
2300
+ const after = Boolean(scope.getSnapshot().value?.rmSafe ?? true);
2301
+ if (before === after || !after) return;
2302
+ try {
2303
+ const result = await createWebConnectionRpc().call(TOOL_PLUS_RPC_CHANNEL, RM_SAFE_STATUS_ENDPOINT, {});
2304
+ if (!result.ok) return;
2305
+ const value = result.value;
2306
+ if (value.status === "disabled") return;
2307
+ setRmSafeToast({
2308
+ text: rmSafeStatusText(t, value),
2309
+ ok: value.status === "injected"
2310
+ });
2311
+ } catch {}
2312
+ }, [
2313
+ scope,
2314
+ form,
2315
+ t
2316
+ ]);
2182
2317
  const runAction = (0, react.useCallback)((field) => {
2183
2318
  const actionKey = field.actionKey;
2184
2319
  if (!actionKey) return;
@@ -2231,12 +2366,88 @@ window.__ModuleLoader__.load({
2231
2366
  injectSectionCss();
2232
2367
  injectSettingsRowsCss();
2233
2368
  }, []);
2234
- if (!form.shell.available) return null;
2235
2369
  const active = TOOL_TABS.find((row) => row.id === activeId)?.id ?? TOOL_TABS[0]?.id;
2370
+ const tabsRef = (0, react.useRef)(null);
2371
+ const indicatorRef = (0, react.useRef)(null);
2372
+ const committedRect = (0, react.useRef)(null);
2373
+ const commitIndicator = (0, react.useCallback)((animate) => {
2374
+ const tabs = tabsRef.current;
2375
+ const indicator = indicatorRef.current;
2376
+ if (tabs === null || indicator === null || active === void 0) return;
2377
+ const tab = tabs.querySelector(`[data-tps-tab="${active}"]`);
2378
+ if (tab === null) return;
2379
+ const left = tab.offsetLeft;
2380
+ const width = tab.offsetWidth;
2381
+ const previous = committedRect.current;
2382
+ committedRect.current = {
2383
+ left,
2384
+ width
2385
+ };
2386
+ if (previous !== null && previous.left === left && previous.width === width) return;
2387
+ const reduce = typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
2388
+ if (!animate || reduce || previous === null) {
2389
+ indicator.style.transition = "none";
2390
+ indicator.style.width = `${width}px`;
2391
+ indicator.style.transform = `translateX(${left}px)`;
2392
+ return;
2393
+ }
2394
+ indicator.style.transition = "none";
2395
+ indicator.style.width = `${width}px`;
2396
+ indicator.style.transform = `translateX(${previous.left}px) scaleX(${previous.width / width})`;
2397
+ indicator.offsetWidth;
2398
+ indicator.style.transition = "transform .24s cubic-bezier(.22,1,.36,1)";
2399
+ indicator.style.transform = `translateX(${left}px) scaleX(1)`;
2400
+ }, [active]);
2401
+ (0, react.useLayoutEffect)(() => {
2402
+ commitIndicator(true);
2403
+ }, [commitIndicator]);
2404
+ const commitRef = (0, react.useRef)(commitIndicator);
2405
+ commitRef.current = commitIndicator;
2406
+ (0, react.useEffect)(() => {
2407
+ const tabs = tabsRef.current;
2408
+ if (tabs === null || typeof ResizeObserver === "undefined") return;
2409
+ const observer = new ResizeObserver(() => commitRef.current(false));
2410
+ observer.observe(tabs);
2411
+ for (const tab of Array.from(tabs.querySelectorAll("[data-tps-tab]"))) observer.observe(tab);
2412
+ return () => {
2413
+ observer.disconnect();
2414
+ };
2415
+ }, []);
2416
+ (0, react.useLayoutEffect)(() => {
2417
+ const root = sectionRef.current;
2418
+ if (root === null || typeof ResizeObserver === "undefined") return;
2419
+ const pane = scrollPaneOf(root);
2420
+ if (pane === null) return;
2421
+ let frame = 0;
2422
+ const measure = () => {
2423
+ frame = 0;
2424
+ const style = getComputedStyle(pane);
2425
+ const paddingTop = parseFloat(style.paddingTop) || 0;
2426
+ const contentHeight = pane.clientHeight - paddingTop - (parseFloat(style.paddingBottom) || 0);
2427
+ const offset = root.getBoundingClientRect().top - pane.getBoundingClientRect().top - (parseFloat(style.borderTopWidth) || 0) - paddingTop + pane.scrollTop;
2428
+ const next = `${Math.max(MIN_PAGE_HEIGHT, Math.round(contentHeight - offset))}px`;
2429
+ if (root.style.height !== next) root.style.height = next;
2430
+ };
2431
+ const schedule = () => {
2432
+ if (frame === 0) frame = requestAnimationFrame(measure);
2433
+ };
2434
+ measure();
2435
+ const observer = new ResizeObserver(schedule);
2436
+ observer.observe(pane);
2437
+ window.addEventListener("resize", schedule);
2438
+ return () => {
2439
+ if (frame !== 0) cancelAnimationFrame(frame);
2440
+ observer.disconnect();
2441
+ window.removeEventListener("resize", schedule);
2442
+ root.style.height = "";
2443
+ };
2444
+ }, [form.shell.available]);
2445
+ if (!form.shell.available) return null;
2236
2446
  const selectTab = (id) => setActiveId(id);
2237
2447
  const blocked = !form.shell.dirty || form.shell.invalid || form.shell.saving;
2238
2448
  const writable = form.shell.writable;
2239
2449
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2450
+ ref: sectionRef,
2240
2451
  className: "tps-section",
2241
2452
  children: [
2242
2453
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
@@ -2247,11 +2458,16 @@ window.__ModuleLoader__.load({
2247
2458
  className: "tps-intro",
2248
2459
  children: t("pageDescription")
2249
2460
  }),
2250
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2461
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2462
+ ref: tabsRef,
2251
2463
  className: "tps-tabs",
2252
2464
  role: "tablist",
2253
2465
  "aria-label": t("pageTitle"),
2254
- children: TOOL_TABS.map((row, index) => {
2466
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2467
+ ref: indicatorRef,
2468
+ className: "tps-tabIndicator",
2469
+ "aria-hidden": "true"
2470
+ }), TOOL_TABS.map((row, index) => {
2255
2471
  const selected = row.id === active;
2256
2472
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2257
2473
  ref: (element) => {
@@ -2264,6 +2480,7 @@ window.__ModuleLoader__.load({
2264
2480
  "aria-selected": selected,
2265
2481
  "aria-controls": `${tabsId.current}-panel-${row.id}`,
2266
2482
  "data-active": selected ? "true" : void 0,
2483
+ "data-tps-tab": row.id,
2267
2484
  tabIndex: selected ? 0 : -1,
2268
2485
  onClick: () => selectTab(row.id),
2269
2486
  onKeyDown: (event) => {
@@ -2291,7 +2508,7 @@ window.__ModuleLoader__.load({
2291
2508
  },
2292
2509
  children: t(row.labelKey)
2293
2510
  }, row.id);
2294
- })
2511
+ })]
2295
2512
  }),
2296
2513
  TOOL_TABS.filter((row) => row.id === active).map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2297
2514
  id: `${tabsId.current}-panel-${row.id}`,
@@ -2326,6 +2543,14 @@ window.__ModuleLoader__.load({
2326
2543
  className: "tps-applies",
2327
2544
  children: t("appliesTo")
2328
2545
  }) : null,
2546
+ !form.shell.failed && form.shell.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2547
+ className: "tps-unsaved",
2548
+ role: "status",
2549
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2550
+ className: "tps-unsavedDot",
2551
+ "aria-hidden": "true"
2552
+ }), t("unsaved")]
2553
+ }) : null,
2329
2554
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2330
2555
  type: "button",
2331
2556
  className: "tps-discard",
@@ -2337,10 +2562,17 @@ window.__ModuleLoader__.load({
2337
2562
  type: "button",
2338
2563
  className: "tps-save",
2339
2564
  disabled: blocked,
2340
- onClick: () => void form.actions.save(),
2565
+ onClick: () => void handleSave(),
2341
2566
  children: form.shell.saving ? t("saving") : t("save")
2342
2567
  })
2343
2568
  ]
2569
+ }),
2570
+ rmSafeToast !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
2571
+ text: rmSafeToast.text,
2572
+ icon: rmSafeToast.ok ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, {}),
2573
+ onDone: () => {
2574
+ setRmSafeToast(null);
2575
+ }
2344
2576
  })
2345
2577
  ]
2346
2578
  });
@@ -2480,6 +2712,16 @@ window.__ModuleLoader__.load({
2480
2712
  nonInteractiveEnvHint: "对非交互使用加固环境:关闭分页器与彩色输出,不弹交互提示。",
2481
2713
  snapshotEnabled: "Shell 快照",
2482
2714
  snapshotEnabledHint: "把用户的 rc 文件(别名、函数、选项)快照进会话 shell。",
2715
+ rmSafe: "安全 rm",
2716
+ rmSafeHint: "在会话 shell 里重定义 rm:删除的文件移入系统回收站而非永久删除;command rm 仍走真删除。",
2717
+ rmSafeStatusInjected: "安全 rm 已注入并验证:rm 进入回收站",
2718
+ rmSafeStatusInjectedUnverified: "安全 rm 已注入(运行时验证不可用)",
2719
+ rmSafeStatusFailed: "安全 rm 注入失败:{reason},rm 将永久删除",
2720
+ rmSafeReasonSnapshotUnavailable: "快照不可用",
2721
+ rmSafeReasonCliMissing: "trash-cli 产物缺失",
2722
+ rmSafeReasonScriptWriteFailed: "脚本写入失败",
2723
+ rmSafeReasonSnapshotWriteFailed: "快照不可写",
2724
+ rmSafeReasonRuntimeNotEffective: "运行时未生效",
2483
2725
  useShellCommandWrapper: "用 bash -c 包裹命令",
2484
2726
  useShellCommandWrapperHint: "把每条命令用 bash -c 包裹,以获得完整的 bash 环境。",
2485
2727
  outputTruncateStrategy: "截断按",
@@ -2651,6 +2893,16 @@ window.__ModuleLoader__.load({
2651
2893
  nonInteractiveEnvHint: "Harden the environment for non-interactive use: no pagers, no prompts, no color.",
2652
2894
  snapshotEnabled: "Shell snapshot",
2653
2895
  snapshotEnabledHint: "Capture the user's rc file (aliases, functions, options) into the session shell.",
2896
+ rmSafe: "Safe rm",
2897
+ rmSafeHint: "Redefine rm in the session shell: deleted files move to the system trash instead of being permanently removed; `command rm` still deletes for real.",
2898
+ rmSafeStatusInjected: "Safe rm injected and verified: rm moves to trash",
2899
+ rmSafeStatusInjectedUnverified: "Safe rm injected (runtime verification unavailable)",
2900
+ rmSafeStatusFailed: "Safe rm injection failed: {reason}, rm deletes permanently",
2901
+ rmSafeReasonSnapshotUnavailable: "snapshot unavailable",
2902
+ rmSafeReasonCliMissing: "trash-cli binary missing",
2903
+ rmSafeReasonScriptWriteFailed: "script write failed",
2904
+ rmSafeReasonSnapshotWriteFailed: "snapshot not writable",
2905
+ rmSafeReasonRuntimeNotEffective: "not effective at runtime",
2654
2906
  useShellCommandWrapper: "Wrap commands in bash -c",
2655
2907
  useShellCommandWrapperHint: "Wrap every command in bash -c for a full bash environment.",
2656
2908
  outputTruncateStrategy: "Truncate by",