@ddtcorex/dsh-maestro-config 0.2.0 → 0.3.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/lib/client.js CHANGED
@@ -2176,6 +2176,18 @@ var t = {
2176
2176
  shadowLv3: "var(--dsw-shadow-lv3)",
2177
2177
  scrollbarL2: "var(--dsw-alias-scrollbar-bg-l2)"
2178
2178
  };
2179
+ var TAB_ICON_PATHS = {
2180
+ globe: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20M2 12h20M12 2a15 15 0 0 1 4 10 15 15 0 0 1-4 10 15 15 0 0 1-4-10A15 15 0 0 1 12 2z",
2181
+ git: "M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5 0-1.2-.4-2.3-1-3 0-1 0-2 0-3s-1 0-2 1a11 11 0 0 0-6 0c-1-1-2-1-2-1 0 1 0 2 0 3-.6.7-1 1.8-1 3 0 3.5 3 5.5 6 5.5-.4.3-.7.7-.9 1.2-.2.5-.3 1-.3 1.5V22",
2182
+ search: "M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM20 20l-3.5-3.5",
2183
+ shield: "M12 2l7 4v5c0 5-3.5 7.5-7 9-3.5-1.5-7-4-7-9V6l7-4z",
2184
+ ban: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM4 12h16",
2185
+ cpu: "M5 12H2a2 2 0 0 1 2-2h2M12 5V2a2 2 0 0 1 2 2v2M19 12h2a2 2 0 0 1-2 2h-2M12 19v2a2 2 0 0 1-2-2v-2M8 8h8v8H8z",
2186
+ bell: "M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9M13.7 21a2 2 0 0 1-3.4 0"
2187
+ };
2188
+ function TabIcon({ name, size = 14 }) {
2189
+ return (0, import_react.createElement)("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", style: { flex: "none" } }, (0, import_react.createElement)("path", { d: TAB_ICON_PATHS[name] }));
2190
+ }
2179
2191
  function Button({
2180
2192
  variant = "ghost",
2181
2193
  size = "md",
@@ -2520,14 +2532,15 @@ function ReviewModelSelector({
2520
2532
  position: "absolute",
2521
2533
  top: "calc(100% + 8px)",
2522
2534
  left: "0",
2523
- minWidth: "300px",
2524
- maxWidth: "360px",
2535
+ minWidth: "min(300px, calc(100vw - 32px))",
2536
+ maxWidth: "min(360px, calc(100vw - 24px))",
2525
2537
  background: t.bgLayer2,
2526
2538
  border: `1px solid ${t.borderL2}`,
2527
2539
  borderRadius: "12px",
2528
2540
  boxShadow: t.shadowLv3,
2529
2541
  zIndex: "20",
2530
- padding: "6px"
2542
+ padding: "6px",
2543
+ boxSizing: "border-box"
2531
2544
  };
2532
2545
  const rowStyle2 = {
2533
2546
  width: "100%",
@@ -2551,7 +2564,7 @@ function ReviewModelSelector({
2551
2564
  const modelLabel = selectedProvider === "" ? "Select model" : value?.model ?? "Select model";
2552
2565
  return (0, import_react.createElement)(
2553
2566
  "div",
2554
- { ref: rootRef, "data-maestro-trigger-wrap": "", style: { position: "relative", display: "inline-block", maxWidth: "100%" } },
2567
+ { ref: rootRef, "data-maestro-trigger-wrap": "", style: { position: "relative", display: "inline-block", maxWidth: "100%", minWidth: 0, boxSizing: "border-box" } },
2555
2568
  label ? (0, import_react.createElement)("span", { style: fieldLabelStyle }, label) : null,
2556
2569
  (0, import_react.createElement)(
2557
2570
  "button",
@@ -2570,8 +2583,10 @@ function ReviewModelSelector({
2570
2583
  alignItems: "center",
2571
2584
  gap: 8,
2572
2585
  cursor: "pointer",
2573
- maxWidth: 320,
2574
- whiteSpace: "nowrap"
2586
+ maxWidth: "min(320px, 100%)",
2587
+ minWidth: 0,
2588
+ whiteSpace: "nowrap",
2589
+ boxSizing: "border-box"
2575
2590
  },
2576
2591
  onClick: () => {
2577
2592
  setOpen((v) => !v);
@@ -2977,6 +2992,143 @@ function NamedTunnelSetupNote() {
2977
2992
  (0, import_react.createElement)("ol", { style: { margin: "4px 0", paddingLeft: 20, fontSize: 12, color: t.labelSecondary, lineHeight: "18px" } }, (0, import_react.createElement)("li", null, "cloudflared tunnel login"), (0, import_react.createElement)("li", null, "cloudflared tunnel create dsh-maestro-webhook"), (0, import_react.createElement)("li", null, "cloudflared tunnel route dns dsh-maestro-webhook <your-hostname>"), (0, import_react.createElement)("li", null, "Paste the printed Tunnel ID, credentials file path and hostname below."))
2978
2993
  );
2979
2994
  }
2995
+ function PlaceholderMappingsEditor({ rows, onChange }) {
2996
+ const updateRow = (index, field, value) => {
2997
+ onChange(rows.map((row, i) => i === index ? { ...row, [field]: value } : row));
2998
+ };
2999
+ const removeRow = (index) => onChange(rows.filter((_, i) => i !== index));
3000
+ const addRow = () => onChange([...rows, { pattern: "", suggestion: "" }]);
3001
+ return (0, import_react.createElement)(
3002
+ "div",
3003
+ { "data-maestro-placeholders": "", style: { display: "flex", flexDirection: "column", gap: 12, paddingTop: 8 } },
3004
+ // Header — count + primary Add (stacks on mobile via CSS)
3005
+ (0, import_react.createElement)(
3006
+ "div",
3007
+ { "data-maestro-placeholders-header": "", style: { display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 12, padding: "4px 0" } },
3008
+ (0, import_react.createElement)(
3009
+ "div",
3010
+ { style: { flex: 1, minWidth: 0 } },
3011
+ (0, import_react.createElement)("div", { style: { fontSize: 14, fontWeight: 600, color: t.labelPrimary, lineHeight: "20px" } }, `Placeholder mappings \u2014 ${rows.length} mapped`),
3012
+ (0, import_react.createElement)("div", { style: { fontSize: 12, color: t.labelSecondary, lineHeight: "16px", marginTop: 2 } }, "Blocked pattern \u2192 placeholder suggestion, one mapping per row")
3013
+ ),
3014
+ (0, import_react.createElement)(Button, { variant: "primary", size: "md", onClick: addRow, "data-maestro-placeholders-add": "" }, "+ Add mapping")
3015
+ ),
3016
+ rows.length === 0 ? (0, import_react.createElement)(
3017
+ "div",
3018
+ {
3019
+ "data-maestro-placeholders-empty": "",
3020
+ style: {
3021
+ display: "flex",
3022
+ flexDirection: "column",
3023
+ alignItems: "center",
3024
+ gap: 10,
3025
+ padding: "20px 16px",
3026
+ borderRadius: 12,
3027
+ border: `1px dashed ${t.borderL2}`,
3028
+ background: "transparent",
3029
+ textAlign: "center"
3030
+ }
3031
+ },
3032
+ (0, import_react.createElement)("div", { style: { fontSize: 13, color: t.labelSecondary, lineHeight: "18px" } }, "No placeholder mappings yet \u2014 add your first mapping"),
3033
+ (0, import_react.createElement)(Button, { variant: "outline", size: "md", onClick: addRow }, "+ Add mapping")
3034
+ ) : (0, import_react.createElement)(
3035
+ "div",
3036
+ { "data-maestro-placeholders-list": "", style: { display: "flex", flexDirection: "column", gap: 12 } },
3037
+ ...rows.map(
3038
+ (row, i) => (0, import_react.createElement)(
3039
+ "div",
3040
+ {
3041
+ key: i,
3042
+ "data-maestro-mapping-card": "",
3043
+ style: {
3044
+ display: "flex",
3045
+ flexDirection: "column",
3046
+ gap: 12,
3047
+ padding: 12,
3048
+ borderRadius: 12,
3049
+ border: `1px solid ${t.borderL2}`,
3050
+ background: t.bgLayer1,
3051
+ boxSizing: "border-box"
3052
+ }
3053
+ },
3054
+ // Card header: index badge + preview + X (same as Review's project card header)
3055
+ (0, import_react.createElement)(
3056
+ "div",
3057
+ { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 } },
3058
+ (0, import_react.createElement)(
3059
+ "div",
3060
+ { style: { display: "flex", alignItems: "center", gap: 8, minWidth: 0, flex: 1 } },
3061
+ (0, import_react.createElement)(
3062
+ "span",
3063
+ {
3064
+ style: {
3065
+ flex: "none",
3066
+ width: 24,
3067
+ height: 24,
3068
+ borderRadius: 12,
3069
+ background: "var(--dsw-alias-bg-module-platform, #F5F6F7)",
3070
+ display: "inline-flex",
3071
+ alignItems: "center",
3072
+ justifyContent: "center",
3073
+ fontSize: 11,
3074
+ fontWeight: 600,
3075
+ color: t.labelSecondary
3076
+ }
3077
+ },
3078
+ String(i + 1)
3079
+ ),
3080
+ (0, import_react.createElement)(
3081
+ "span",
3082
+ {
3083
+ style: {
3084
+ fontSize: 12,
3085
+ fontFamily: "ui-monospace, monospace",
3086
+ color: row.pattern ? t.labelPrimary : t.labelTertiary,
3087
+ overflow: "hidden",
3088
+ textOverflow: "ellipsis",
3089
+ whiteSpace: "nowrap",
3090
+ minWidth: 0
3091
+ }
3092
+ },
3093
+ row.pattern ? row.suggestion ? `${row.pattern} \u2192 ${row.suggestion}` : `${row.pattern} \u2192 \u2026` : "New mapping"
3094
+ )
3095
+ ),
3096
+ (0, import_react.createElement)(Button, { variant: "outline", size: "sm", onClick: () => removeRow(i), "aria-label": `Remove mapping ${i + 1}`, title: "Remove mapping" }, "\u2715")
3097
+ ),
3098
+ // Fields grid — 2-col desktop, 1-col mobile (same as Review's project grid)
3099
+ (0, import_react.createElement)(
3100
+ "div",
3101
+ { "data-maestro-mapping-grid": "", style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 } },
3102
+ (0, import_react.createElement)(
3103
+ "label",
3104
+ { style: fieldLabelStyle, "data-maestro-field": "" },
3105
+ "Blocked pattern",
3106
+ (0, import_react.createElement)(FieldInput, {
3107
+ value: row.pattern,
3108
+ placeholder: "example-project",
3109
+ onChange: (e) => updateRow(i, "pattern", e.target.value),
3110
+ "aria-label": `Blocked pattern ${i + 1}`,
3111
+ style: { width: "100%" }
3112
+ })
3113
+ ),
3114
+ (0, import_react.createElement)(
3115
+ "label",
3116
+ { style: fieldLabelStyle, "data-maestro-field": "" },
3117
+ "Placeholder",
3118
+ (0, import_react.createElement)(FieldInput, {
3119
+ value: row.suggestion,
3120
+ placeholder: "my-project",
3121
+ onChange: (e) => updateRow(i, "suggestion", e.target.value),
3122
+ "aria-label": `Placeholder suggestion ${i + 1}`,
3123
+ style: { width: "100%" }
3124
+ })
3125
+ )
3126
+ )
3127
+ )
3128
+ )
3129
+ )
3130
+ );
3131
+ }
2980
3132
  function MaestroSettingsTab({ rpcCall, configRpcCall }) {
2981
3133
  const [status, setStatus] = (0, import_react.useState)(null);
2982
3134
  const [proxyStatus, setProxyStatus] = (0, import_react.useState)(null);
@@ -2991,34 +3143,49 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
2991
3143
  const [showLanPin, setShowLanPin] = (0, import_react.useState)(false);
2992
3144
  const [guard, setGuard] = (0, import_react.useState)({});
2993
3145
  const [patternsText, setPatternsText] = (0, import_react.useState)("");
2994
- const [placeholdersText, setPlaceholdersText] = (0, import_react.useState)("");
3146
+ const [placeholderRows, setPlaceholderRows] = (0, import_react.useState)([]);
2995
3147
  const [supervisorCfg, setSupervisorCfg] = (0, import_react.useState)({});
2996
3148
  const [notifierCfg, setNotifierCfg] = (0, import_react.useState)({});
2997
3149
  const [activeTab, setActiveTab] = (0, import_react.useState)("tunnel");
2998
3150
  (0, import_react.useEffect)(() => {
2999
3151
  const css = `
3000
- /* Maestro nested tabs \u2014 market-like pill bar + mobile fixes */
3001
- [data-maestro-tabs] { display:flex; gap:6px; overflow-x:auto; overflow-y:hidden; scrollbar-width:none; -webkit-overflow-scrolling:touch; overscroll-behavior-x:contain; touch-action:pan-x; padding-bottom:6px; margin-bottom:10px; border-bottom:1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.08)); }
3152
+ /* Model override dropdown \u2014 prevent right-edge overlap */
3153
+ [data-maestro-control] [data-maestro-menu] { left:auto !important; right:0 !important; }
3154
+ [data-maestro-project-card] [data-maestro-menu] { left:auto !important; right:0 !important; }
3155
+ [data-maestro-trigger-wrap] { min-width:0; }
3156
+ /* Maestro nested tabs \u2014 unified with dsh-maestro-jobs (maestro-design pill bar) */
3157
+ [data-maestro-tabs] { display:flex; gap:6px; overflow-x:auto; overflow-y:hidden; scrollbar-width:none; -webkit-overflow-scrolling:touch; overscroll-behavior-x:contain; touch-action:pan-x; padding:2px 2px 8px; margin:0 -2px 4px; border-bottom:1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.08)); }
3002
3158
  [data-maestro-tabs]::-webkit-scrollbar { display:none; width:0; height:0; }
3003
- [data-maestro-tab] { flex:none; height:32px; min-width:fit-content; padding:0 14px; border-radius:999px; border:1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.12)); background:transparent; color:var(--dsw-alias-label-primary); font-size:13px; line-height:20px; white-space:nowrap; display:inline-flex; align-items:center; justify-content:center; cursor:pointer; font-family:inherit; -webkit-tap-highlight-color:transparent; }
3004
- [data-maestro-tab][data-active="true"] { background:var(--dsw-specific-sidebar-nav-item-active, #EBEEF2); border-color:var(--dsw-specific-sidebar-nav-item-active, #EBEEF2); color:var(--dsw-alias-label-primary); }
3159
+ [data-maestro-tab] { flex:none; min-width:fit-content; min-height:40px; padding:0 14px; border-radius:999px; border:1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.12)); background:transparent; color:var(--dsw-alias-label-secondary); font-size:13px; line-height:20px; font-weight:500; font-family:inherit; white-space:nowrap; display:inline-flex; align-items:center; justify-content:center; gap:6px; cursor:pointer; -webkit-tap-highlight-color:transparent; transition: background 150ms ease, color 150ms ease, border-color 150ms ease; touch-action:manipulation; }
3160
+ [data-maestro-tab][data-active="true"] { background:var(--dsw-specific-sidebar-nav-item-active, #EBEEF2); border-color:var(--dsw-specific-sidebar-nav-item-active, #EBEEF2); color:var(--dsw-alias-label-primary); font-weight:600; }
3005
3161
  [data-maestro-tab]:hover { background:var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,.06)); }
3006
3162
  [data-maestro-tab][data-active="true"]:hover { background:var(--dsw-specific-sidebar-nav-item-active, #EBEEF2); }
3007
3163
  [data-maestro-tab]:focus-visible { outline:2px solid var(--dsw-alias-state-business-primary, #4f6ef7); outline-offset:1px; }
3164
+ [data-maestro-tab]:active { transform: scale(.97); }
3008
3165
  [data-maestro-panel] { width:100%; min-width:0; box-sizing:border-box; }
3009
- /* label \u2192 input d\xEDnh nhau fix: flex column gap + full width */
3166
+ /* label/input overlap fix: flex column gap + full width */
3010
3167
  [data-maestro-panel] label { gap:6px !important; }
3011
3168
  [data-maestro-row]:last-child { border-bottom:none !important; }
3012
3169
  [data-maestro-panel] label > span { width:100% !important; box-sizing:border-box !important; }
3170
+ @media (prefers-reduced-motion: reduce) {
3171
+ [data-maestro-tab] { transition: none !important; }
3172
+ [data-maestro-tab]:active { transform: none !important; }
3173
+ }
3013
3174
  @media (max-width: 640px) {
3014
3175
  [data-maestro-settings-card] { max-width:100% !important; gap:6px !important; padding:0 2px !important; }
3015
- [data-maestro-tabs] { gap:6px !important; padding:0 0 6px !important; margin:0 -2px 10px !important; }
3016
- [data-maestro-tab] { height:32px !important; padding:0 12px !important; font-size:13px !important; }
3017
- [data-maestro-mapping-row] { flex-direction:column !important; align-items:stretch !important; gap:8px !important; }
3018
- [data-maestro-mapping-row] > * { flex:1 1 100% !important; width:100% !important; max-width:100% !important; }
3176
+ [data-maestro-tabs] { gap:6px !important; padding:2px 2px 8px !important; margin:0 -2px 8px !important; }
3177
+ [data-maestro-tab] { min-height:40px !important; height:auto !important; padding:0 12px !important; font-size:13px !important; }
3178
+ /* Placeholder mappings \u2014 card design unified with Review project cards (header X, grid fields) */
3179
+ [data-maestro-placeholders-header] { flex-direction: column !important; align-items: stretch !important; }
3180
+ [data-maestro-placeholders-header] [data-maestro-placeholders-add] { width: 100% !important; justify-content: center !important; }
3181
+ [data-maestro-placeholders-list] { gap: 12px !important; }
3182
+ [data-maestro-mapping-card] { padding:10px !important; }
3183
+ [data-maestro-mapping-grid] { grid-template-columns:1fr !important; gap:12px !important; }
3019
3184
  [data-maestro-qr-row] { flex-direction:column !important; align-items:flex-start !important; }
3020
3185
  [data-maestro-trigger-wrap] { max-width:100% !important; }
3021
3186
  [data-maestro-menu] { min-width:0 !important; max-width:calc(100vw - 32px) !important; left:0 !important; right:auto !important; }
3187
+ [data-maestro-control] [data-maestro-menu] { left:0 !important; right:auto !important; }
3188
+ [data-maestro-project-card] [data-maestro-menu] { left:0 !important; right:auto !important; }
3022
3189
  [data-maestro-panel] label { gap:8px !important; }
3023
3190
  div[data-maestro-row] { flex-direction:column !important; align-items:stretch !important; padding:12px 0 !important; }
3024
3191
  [data-maestro-row-text] { padding-right:0 !important; }
@@ -3031,7 +3198,7 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
3031
3198
  [data-maestro-project-profile-row] > label { flex:1 1 100% !important; width:100% !important; }
3032
3199
  }
3033
3200
  @media (max-width: 390px) {
3034
- [data-maestro-tab] { height:30px !important; padding:0 10px !important; font-size:12px !important; }
3201
+ [data-maestro-tab] { min-height:38px !important; height:auto !important; padding:0 10px !important; font-size:12px !important; }
3035
3202
  }
3036
3203
  `;
3037
3204
  const tag = document.createElement("style");
@@ -3083,15 +3250,13 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
3083
3250
  setError(e.message ?? String(e));
3084
3251
  }
3085
3252
  };
3086
- const commitPlaceholders = async () => {
3253
+ const commitPlaceholders = async (rows) => {
3087
3254
  setError(null);
3088
- let obj = {};
3089
- try {
3090
- obj = placeholdersText.trim() ? JSON.parse(placeholdersText) : {};
3091
- if (typeof obj !== "object" || obj === null || Array.isArray(obj)) throw new Error("placeholders must be JSON object");
3092
- } catch (e) {
3093
- setError(`placeholders JSON invalid: ${e.message ?? String(e)}`);
3094
- return;
3255
+ const obj = {};
3256
+ for (const row of rows) {
3257
+ const pattern = (row.pattern ?? "").trim();
3258
+ if (!pattern) continue;
3259
+ obj[pattern] = (row.suggestion ?? "").trim();
3095
3260
  }
3096
3261
  try {
3097
3262
  await cfgSet("guardBlacklist", { placeholders: obj });
@@ -3114,6 +3279,7 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
3114
3279
  const next = { ...prev };
3115
3280
  for (const [k, v] of Object.entries(patch)) {
3116
3281
  if (k === "telegram" && typeof v === "object" && v !== null) next.telegram = { ...prev.telegram ?? {}, ...v };
3282
+ else if (k === "policy" && typeof v === "object" && v !== null) next.policy = { ...prev.policy ?? {}, ...v };
3117
3283
  else next[k] = v;
3118
3284
  }
3119
3285
  return next;
@@ -3147,7 +3313,7 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
3147
3313
  const pats = Array.isArray(bl?.patterns) ? bl.patterns : [];
3148
3314
  const ph = bl?.placeholders && typeof bl.placeholders === "object" ? bl.placeholders : {};
3149
3315
  setPatternsText(pats.join("\n"));
3150
- setPlaceholdersText(JSON.stringify(ph, null, 2));
3316
+ setPlaceholderRows(Object.entries(ph).map(([pattern, suggestion]) => ({ pattern, suggestion: String(suggestion) })));
3151
3317
  setSupervisorCfg(sup ?? {});
3152
3318
  setNotifierCfg(not ?? {});
3153
3319
  }).catch(() => {
@@ -3264,14 +3430,13 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
3264
3430
  }
3265
3431
  };
3266
3432
  const TABS = [
3267
- { id: "tunnel", label: "Tunnel" },
3268
- { id: "notify", label: "Notify" },
3269
- { id: "gitlab", label: "GitLab" },
3270
- { id: "review", label: "Review" },
3271
- { id: "guard", label: "Guard" },
3272
- { id: "blacklist", label: "Blacklist" },
3273
- { id: "supervisor", label: "Supervisor" },
3274
- { id: "notifier", label: "Notifier" }
3433
+ { id: "tunnel", label: "Tunnel", icon: "globe" },
3434
+ { id: "gitlab", label: "GitLab", icon: "git" },
3435
+ { id: "review", label: "Review", icon: "search" },
3436
+ { id: "guard", label: "Guard", icon: "shield" },
3437
+ { id: "blacklist", label: "Blacklist", icon: "ban" },
3438
+ { id: "supervisor", label: "Supervisor", icon: "cpu" },
3439
+ { id: "notifier", label: "Notifier", icon: "bell" }
3275
3440
  ];
3276
3441
  const tabContents = {
3277
3442
  tunnel: (0, import_react.createElement)(
@@ -3290,14 +3455,6 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
3290
3455
  (0, import_react.createElement)("div", { style: { ...cardInsetStyle, marginTop: "12px" } }, (0, import_react.createElement)("div", { style: { fontSize: 13, fontWeight: 600, color: t.labelPrimary } }, "Remote access \u2014 LAN"), (0, import_react.createElement)(LanAccess, { proxyStatus, lanPin: lanPinEnabled === null ? null : { enabled: lanPinEnabled, pin: lanPin, show: showLanPin, onShow: revealLanPin, onHide: () => setShowLanPin(false), onRotate: rotateLanPin, onToggle: toggleLanPin } })),
3291
3456
  (0, import_react.createElement)("div", { style: { ...cardInsetStyle, marginTop: "12px" } }, (0, import_react.createElement)("div", { style: { fontSize: 13, fontWeight: 600, color: t.labelPrimary } }, "Public access"), (0, import_react.createElement)(PublicAccess, { status, pin, showPin, onRevealPin: revealPin, onHidePin: () => setShowPin(false), onRotatePin: rotatePin }))
3292
3457
  ),
3293
- notify: (0, import_react.createElement)(
3294
- "div",
3295
- { style: { display: "flex", flexDirection: "column" } },
3296
- (0, import_react.createElement)("div", { style: { padding: "12px 0", borderBottom: `1px solid ${t.borderL2}` } }, (0, import_react.createElement)("p", { style: captionStyle }, "Sends one protected startup update with the current public-access PIN to a single chat. Leave either credential blank to disable.")),
3297
- (0, import_react.createElement)(SettingRow, { title: "Bot token", description: "Telegram bot token from @BotFather.", control: (0, import_react.createElement)("div", { style: { display: "flex", gap: 8, alignItems: "center" } }, (0, import_react.createElement)(FieldInput, { placeholder: "123456:ABC-DEF...", type: "password", autoComplete: "off", value: config.hasTelegramBotToken ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : "", onChange: (e) => saveField("telegramBotToken", e.target.value), style: { width: 220 } }), config.hasTelegramBotToken ? (0, import_react.createElement)(Button, { variant: "outline", size: "md", onClick: () => saveField("telegramBotToken", "") }, "Clear") : null) }),
3298
- (0, import_react.createElement)(SettingRow, { title: "Chat ID", description: "Target chat, e.g. -1001234567890.", control: (0, import_react.createElement)(FieldInput, { placeholder: "-1001234567890", value: config.telegramChatId ?? "", onChange: (e) => saveField("telegramChatId", e.target.value), style: { width: 220 } }) }),
3299
- (0, import_react.createElement)(ToggleRow, { title: "Also notify about reviews", description: "One message per review run with its outcome.", checked: config.telegramReviewNotifications === true, onChange: (v) => saveField("telegramReviewNotifications", v) })
3300
- ),
3301
3458
  gitlab: (0, import_react.createElement)(
3302
3459
  "div",
3303
3460
  { style: { display: "flex", flexDirection: "column" } },
@@ -3343,11 +3500,17 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
3343
3500
  "div",
3344
3501
  { style: { ...rowStyle, flexDirection: "column", alignItems: "stretch", gap: 8, borderBottom: "none" } },
3345
3502
  (0, import_react.createElement)("div", { style: rowTitleStyle }, "Placeholder mappings"),
3346
- (0, import_react.createElement)("div", { style: rowDescStyle }, "JSON object mapping blocked patterns to placeholder suggestions."),
3347
- (0, import_react.createElement)(TextareaField, { value: placeholdersText, placeholder: '{"example-project":"my-project"}', onChange: (e) => setPlaceholdersText(e.target.value), onBlur: () => commitPlaceholders(), style: { minHeight: 80 } }),
3503
+ (0, import_react.createElement)("div", { style: rowDescStyle }, "Blocked pattern \u2192 placeholder suggestion. One mapping per row \u2014 no JSON needed."),
3504
+ (0, import_react.createElement)(PlaceholderMappingsEditor, {
3505
+ rows: placeholderRows,
3506
+ onChange: (rows) => {
3507
+ setPlaceholderRows(rows);
3508
+ commitPlaceholders(rows);
3509
+ }
3510
+ }),
3348
3511
  (0, import_react.createElement)("div", { style: { marginTop: 8 } }, (0, import_react.createElement)(Button, { variant: "outline", size: "sm", onClick: () => {
3349
3512
  commitBlacklistPatterns(patternsText);
3350
- commitPlaceholders();
3513
+ commitPlaceholders(placeholderRows);
3351
3514
  } }, "Save Blacklist"))
3352
3515
  )
3353
3516
  ),
@@ -3367,9 +3530,10 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
3367
3530
  notifier: (0, import_react.createElement)(
3368
3531
  "div",
3369
3532
  { style: { display: "flex", flexDirection: "column" } },
3370
- (0, import_react.createElement)(SettingRow, { title: "Bot token", description: "Telegram bot token from @BotFather.", control: (0, import_react.createElement)(FieldInput, { type: "password", autoComplete: "off", value: notifierCfg.telegram?.botToken ?? "", placeholder: "123456:ABC-DEF...", onChange: (e) => saveNotifierCfg({ telegram: { botToken: e.target.value } }), style: { width: 260 } }) }),
3371
- (0, import_react.createElement)(SettingRow, { title: "Chat ID", description: "Target chat, e.g. -1001234567890.", control: (0, import_react.createElement)(FieldInput, { value: notifierCfg.telegram?.chatId ?? "", placeholder: "-1001234567890", onChange: (e) => saveNotifierCfg({ telegram: { chatId: e.target.value } }), style: { width: 260 } }) }),
3372
- (0, import_react.createElement)(ToggleRow, { title: "Review notifications", description: "Also notify about finished reviews.", checked: notifierCfg.telegram?.reviewNotifications === true || notifierCfg.policy?.reviewNotifications === true, onChange: (v) => saveNotifierCfg({ telegram: { reviewNotifications: v } }) })
3533
+ (0, import_react.createElement)("div", { style: { padding: "12px 0", borderBottom: `1px solid ${t.borderL2}` } }, (0, import_react.createElement)("p", { style: captionStyle }, "Telegram bot settings for notifications: startup PIN, review digests, PIN rotation. Leave blank to disable.")),
3534
+ (0, import_react.createElement)(SettingRow, { title: "Bot token", description: "Telegram bot token from @BotFather.", control: (0, import_react.createElement)("div", { style: { display: "flex", gap: 8, alignItems: "center" } }, (0, import_react.createElement)(FieldInput, { type: "password", autoComplete: "off", value: notifierCfg.telegram?.botToken ?? "", placeholder: "123456:ABC-DEF...", onChange: (e) => saveNotifierCfg({ telegram: { botToken: e.target.value } }), style: { width: 220 } }), notifierCfg.telegram?.botToken ? (0, import_react.createElement)(Button, { variant: "outline", size: "md", onClick: () => saveNotifierCfg({ telegram: { botToken: "" } }) }, "Clear") : null) }),
3535
+ (0, import_react.createElement)(SettingRow, { title: "Chat ID", description: "Target chat, e.g. -1001234567890.", control: (0, import_react.createElement)(FieldInput, { value: notifierCfg.telegram?.chatId ?? "", placeholder: "-1001234567890", onChange: (e) => saveNotifierCfg({ telegram: { chatId: e.target.value } }), style: { width: 220 } }) }),
3536
+ (0, import_react.createElement)(ToggleRow, { title: "Review notifications", description: "Also notify about finished reviews.", checked: notifierCfg.policy?.reviewNotifications === true, onChange: (v) => saveNotifierCfg({ policy: { reviewNotifications: v } }) })
3373
3537
  )
3374
3538
  };
3375
3539
  return (0, import_react.createElement)(
@@ -3377,14 +3541,31 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
3377
3541
  { "data-maestro-settings-card": "", style: { display: "flex", flexDirection: "column", gap: 8, width: "100%", maxWidth: 640, minWidth: 0, boxSizing: "border-box" } },
3378
3542
  (0, import_react.createElement)(
3379
3543
  "div",
3380
- { style: { padding: "2px 2px 8px" } },
3381
- (0, import_react.createElement)("div", { style: { fontSize: 15, fontWeight: 600, color: t.labelPrimary, lineHeight: "22px" } }, "Maestro"),
3382
- (0, import_react.createElement)("div", { style: { fontSize: 12, color: t.labelSecondary, lineHeight: "16px", marginTop: 2 } }, "Tunnel, access, review & guard \u2014 all via the shared Maestro store. Uses the same tokens and primitives as DSH settings.")
3544
+ { style: { padding: "2px 2px 8px", display: "flex", gap: 10, alignItems: "flex-start" } },
3545
+ // Shared BrandBadge same as dashboard sidebar/popup (BrandMark #0A84FF)
3546
+ (0, import_react.createElement)(
3547
+ "span",
3548
+ { "data-maestro-logo": "", style: { width: 28, height: 28, borderRadius: 8, display: "inline-flex", alignItems: "center", justifyContent: "center", background: "var(--dsw-alias-brand-primary, #0A84FF)", backgroundColor: "#0A84FF", color: "#fff", flex: "none", border: "1px solid rgba(0,0,0,0.08)", boxShadow: "0 0 0 1px var(--dsw-alias-border-l1)", boxSizing: "border-box", alignSelf: "flex-start", marginTop: 2 } },
3549
+ (0, import_react.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true" }, (0, import_react.createElement)("path", { d: "M2 11 L5 4 L8 9 L11 4 L14 11", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round" }))
3550
+ ),
3551
+ (0, import_react.createElement)(
3552
+ "div",
3553
+ { style: { display: "flex", flexDirection: "column", minWidth: 0 } },
3554
+ (0, import_react.createElement)("div", { style: { fontSize: 15, fontWeight: 600, color: t.labelPrimary, lineHeight: "22px" } }, "Maestro"),
3555
+ (0, import_react.createElement)("div", { style: { fontSize: 12, color: t.labelSecondary, lineHeight: "16px", marginTop: 2 } }, "Tunnel, access, review & guard \u2014 all via the shared Maestro store. Uses the same tokens and primitives as DSH settings.")
3556
+ )
3383
3557
  ),
3558
+ (0, import_react.createElement)("style", {}, "[data-maestro-logo]{background:#0A84FF !important; background-color:#0A84FF !important; color:#fff !important;}"),
3384
3559
  (0, import_react.createElement)(
3385
3560
  "div",
3386
3561
  { "data-maestro-tabs": "", role: "tablist", "aria-label": "Maestro settings sections" },
3387
- ...TABS.map((tab) => (0, import_react.createElement)("button", { key: tab.id, "data-maestro-tab": "", "data-active": String(activeTab === tab.id), role: "tab", "aria-selected": activeTab === tab.id, onClick: () => setActiveTab(tab.id) }, tab.label))
3562
+ ...TABS.map((tab) => (0, import_react.createElement)("button", { key: tab.id, "data-maestro-tab": "", "data-active": String(activeTab === tab.id), role: "tab", "aria-selected": activeTab === tab.id, onClick: (e) => {
3563
+ setActiveTab(tab.id);
3564
+ try {
3565
+ e.currentTarget.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
3566
+ } catch {
3567
+ }
3568
+ } }, (0, import_react.createElement)(TabIcon, { name: tab.icon }), tab.label))
3388
3569
  ),
3389
3570
  (0, import_react.createElement)("div", { "data-maestro-panel": activeTab, style: { display: "flex", flexDirection: "column", gap: 10, minWidth: 0 } }, tabContents[activeTab]),
3390
3571
  error ? (0, import_react.createElement)("p", { style: { color: t.stateError, fontSize: 12, margin: "8px 0 0", padding: "8px 10px", borderRadius: 8, background: "color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent)", border: `1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 30%, transparent)` } }, error) : null
@@ -3430,9 +3611,10 @@ function registerSettingsNavIcon(label, root) {
3430
3611
  // src/client/index.tsx
3431
3612
  var SETTINGS_NAV_CSS = `
3432
3613
 
3433
- /* maestro: replace the settings-nav fallback gear with the Maestro M-logo glyph */
3434
- [${SETTINGS_NAV_MARKER}] > svg:first-child {
3435
- display: none;
3614
+ /* maestro: replace the settings-nav fallback gear with the Maestro M-logo glyph \u2014 same mark as sidebar/popup BrandMark */
3615
+ [${SETTINGS_NAV_MARKER}] > svg:first-child,
3616
+ [${SETTINGS_NAV_MARKER}] > svg.zWKi1a_navIcon {
3617
+ display: none !important;
3436
3618
  }
3437
3619
 
3438
3620
  [${SETTINGS_NAV_MARKER}]::before {
@@ -3440,9 +3622,10 @@ var SETTINGS_NAV_CSS = `
3440
3622
  flex: none;
3441
3623
  width: 16px;
3442
3624
  height: 16px;
3625
+ display: inline-block;
3443
3626
  background: currentColor;
3444
- -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 11 L5 4 L8 9 L11 4 L14 11'/%3E%3C/svg%3E") center / contain no-repeat;
3445
- mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 11 L5 4 L8 9 L11 4 L14 11'/%3E%3C/svg%3E") center / contain no-repeat;
3627
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 11 L5 4 L8 9 L11 4 L14 11'/%3E%3C/svg%3E") center / contain no-repeat;
3628
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 11 L5 4 L8 9 L11 4 L14 11'/%3E%3C/svg%3E") center / contain no-repeat;
3446
3629
  }
3447
3630
  `;
3448
3631
  var inject = ["slots", "connection"];
@@ -1 +1 @@
1
- {"version":3,"file":"MaestroSettings.d.ts","sourceRoot":"","sources":["../../../src/client/MaestroSettings.tsx"],"names":[],"mappings":"AACA;;;;;;;;GAQG;AA0hCH,wBAAgB,kBAAkB,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,EAAE;IAAE,OAAO,EAAE,GAAG,CAAC;IAAC,aAAa,CAAC,EAAE,GAAG,CAAA;CAAE;;;;;;;;;mBAsYmE,GAAG;;gBAazK"}
1
+ {"version":3,"file":"MaestroSettings.d.ts","sourceRoot":"","sources":["../../../src/client/MaestroSettings.tsx"],"names":[],"mappings":"AACA;;;;;;;;GAQG;AAisCH,wBAAgB,kBAAkB,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,EAAE;IAAE,OAAO,EAAE,GAAG,CAAC;IAAC,aAAa,CAAC,EAAE,GAAG,CAAA;CAAE;;;;;;;;;mBAkZmE,GAAG;;gBAoBzK"}
@@ -0,0 +1,9 @@
1
+ export declare function MaestroMark(props: {
2
+ size?: number;
3
+ }): import("react").ReactSVGElement;
4
+ export declare function BrandBadge(props: {
5
+ size?: number;
6
+ outer?: number;
7
+ radius?: number;
8
+ }): import("react").DetailedReactHTMLElement<any, HTMLElement>;
9
+ //# sourceMappingURL=BrandMark.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BrandMark.d.ts","sourceRoot":"","sources":["../../../../src/client/components/BrandMark.tsx"],"names":[],"mappings":"AAEA,wBAAgB,WAAW,CAAC,KAAK,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,mCAKnD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,8DAYnF"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.tsx"],"names":[],"mappings":"AAwCA,UAAU,SAAS;IACjB,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;IAC3B,MAAM,CAAC,EAAE,EAAE,MAAM,MAAM,IAAI,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACtD;AAED,eAAO,MAAM,MAAM,kCAAmC,CAAA;AAatD,wBAAgB,KAAK,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI,CA+B1C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.tsx"],"names":[],"mappings":"AA0CA,UAAU,SAAS;IACjB,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;IAC3B,MAAM,CAAC,EAAE,EAAE,MAAM,MAAM,IAAI,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACtD;AAED,eAAO,MAAM,MAAM,kCAAmC,CAAA;AAatD,wBAAgB,KAAK,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI,CA+B1C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ddtcorex/dsh-maestro-config",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "Maestro Config — shared settings service for the dsh-maestro-* suite over the single namespaced store (~/.dsh/maestro/settings.json)",
6
6
  "type": "module",
@@ -34,14 +34,14 @@
34
34
  },
35
35
  "peerDependencies": {
36
36
  "@deepseek-ai/cordis": "^4.0.1",
37
- "@deepseek-ai/dsh-client-connection": "0.1.1-rc.2"
37
+ "@deepseek-ai/dsh-client-connection": "0.1.2-alpha.2"
38
38
  },
39
39
  "dependencies": {
40
40
  "@ddtcorex/dsh-maestro-config-lib": "^0.1.2"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@deepseek-ai/cordis": "^4.0.1",
44
- "@deepseek-ai/dsh-client-connection": "0.1.1-rc.2",
44
+ "@deepseek-ai/dsh-client-connection": "0.1.2-alpha.2",
45
45
  "@types/node": "^22.0.0",
46
46
  "@types/qrcode": "^1.5.6",
47
47
  "@types/react": "~18.3.1",
@@ -37,6 +37,26 @@ const t = {
37
37
  scrollbarL2: 'var(--dsw-alias-scrollbar-bg-l2)',
38
38
  }
39
39
 
40
+ // ---------------------------------------------------------------------------
41
+ // Nested pill tabs — unified with dsh-maestro-jobs (maestro-design: Minimalism & Swiss)
42
+ // Shared geometry: 40px min-height (44px jobs → 40px unified, touch-friendly ≥24px WCAG),
43
+ // 14px icon + 13px label gap 6, pill 999, border-l1, #EBEEF2 active, hover, focus ring.
44
+ // Icons: lucide-style 14px SVG, stroke 1.8, currentColor.
45
+ // ---------------------------------------------------------------------------
46
+ type TabIcon = 'globe' | 'git' | 'search' | 'shield' | 'ban' | 'cpu' | 'bell'
47
+ const TAB_ICON_PATHS: Record<TabIcon, string> = {
48
+ globe: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20M2 12h20M12 2a15 15 0 0 1 4 10 15 15 0 0 1-4 10 15 15 0 0 1-4-10A15 15 0 0 1 12 2z',
49
+ git: 'M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5 0-1.2-.4-2.3-1-3 0-1 0-2 0-3s-1 0-2 1a11 11 0 0 0-6 0c-1-1-2-1-2-1 0 1 0 2 0 3-.6.7-1 1.8-1 3 0 3.5 3 5.5 6 5.5-.4.3-.7.7-.9 1.2-.2.5-.3 1-.3 1.5V22',
50
+ search: 'M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM20 20l-3.5-3.5',
51
+ shield: 'M12 2l7 4v5c0 5-3.5 7.5-7 9-3.5-1.5-7-4-7-9V6l7-4z',
52
+ ban: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM4 12h16',
53
+ cpu: 'M5 12H2a2 2 0 0 1 2-2h2M12 5V2a2 2 0 0 1 2 2v2M19 12h2a2 2 0 0 1-2 2h-2M12 19v2a2 2 0 0 1-2-2v-2M8 8h8v8H8z',
54
+ bell: 'M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9M13.7 21a2 2 0 0 1-3.4 0',
55
+ }
56
+ function TabIcon({ name, size = 14 }: { name: TabIcon; size?: number }) {
57
+ return h('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': 'true', style: { flex: 'none' } }, h('path', { d: TAB_ICON_PATHS[name] }))
58
+ }
59
+
40
60
  // ---------------------------------------------------------------------------
41
61
  // Lightweight DSH primitive mirrors (geometry + tokens identical to host)
42
62
  // Usage is identical to @deepseek-ai/dsh-client-ui-primitives at runtime.
@@ -533,14 +553,15 @@ function ReviewModelSelector({
533
553
  position: 'absolute',
534
554
  top: 'calc(100% + 8px)',
535
555
  left: '0',
536
- minWidth: '300px',
537
- maxWidth: '360px',
556
+ minWidth: 'min(300px, calc(100vw - 32px))',
557
+ maxWidth: 'min(360px, calc(100vw - 24px))',
538
558
  background: t.bgLayer2 as string,
539
559
  border: `1px solid ${t.borderL2}`,
540
560
  borderRadius: '12px',
541
561
  boxShadow: t.shadowLv3 as string,
542
562
  zIndex: '20',
543
563
  padding: '6px',
564
+ boxSizing: 'border-box' as string,
544
565
  }
545
566
  const rowStyle: Record<string, any> = {
546
567
  width: '100%',
@@ -567,7 +588,7 @@ function ReviewModelSelector({
567
588
  const modelLabel = selectedProvider === '' ? 'Select model' : (value?.model ?? 'Select model')
568
589
  return h(
569
590
  'div',
570
- { ref: rootRef as any, 'data-maestro-trigger-wrap': '', style: { position: 'relative', display: 'inline-block', maxWidth: '100%' } },
591
+ { ref: rootRef as any, 'data-maestro-trigger-wrap': '', style: { position: 'relative', display: 'inline-block', maxWidth: '100%', minWidth: 0, boxSizing: 'border-box' as string } },
571
592
  label ? h('span', { style: fieldLabelStyle }, label) : null,
572
593
  h(
573
594
  'button',
@@ -586,8 +607,10 @@ function ReviewModelSelector({
586
607
  alignItems: 'center',
587
608
  gap: 8,
588
609
  cursor: 'pointer',
589
- maxWidth: 320,
610
+ maxWidth: 'min(320px, 100%)',
611
+ minWidth: 0,
590
612
  whiteSpace: 'nowrap',
613
+ boxSizing: 'border-box' as string,
591
614
  },
592
615
  onClick: () => {
593
616
  setOpen((v) => !v)
@@ -1054,6 +1077,150 @@ function NamedTunnelSetupNote() {
1054
1077
  )
1055
1078
  }
1056
1079
 
1080
+ // ---------------------------------------------------------------------------
1081
+ // PlaceholderMappingsEditor — row-based "blocked pattern → suggestion" mappings
1082
+ // (same interaction as ProjectMappingsEditor; no JSON editing for users)
1083
+ // ---------------------------------------------------------------------------
1084
+ function PlaceholderMappingsEditor({ rows, onChange }: { rows: { pattern: string; suggestion: string }[]; onChange: (rows: { pattern: string; suggestion: string }[]) => void }) {
1085
+ const updateRow = (index: number, field: 'pattern' | 'suggestion', value: string) => {
1086
+ onChange(rows.map((row, i) => (i === index ? { ...row, [field]: value } : row)))
1087
+ }
1088
+ const removeRow = (index: number) => onChange(rows.filter((_, i) => i !== index))
1089
+ const addRow = () => onChange([...rows, { pattern: '', suggestion: '' }])
1090
+ return h(
1091
+ 'div',
1092
+ { 'data-maestro-placeholders': '', style: { display: 'flex', flexDirection: 'column', gap: 12, paddingTop: 8 } },
1093
+ // Header — count + primary Add (stacks on mobile via CSS)
1094
+ h(
1095
+ 'div',
1096
+ { 'data-maestro-placeholders-header': '', style: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12, padding: '4px 0' } },
1097
+ h(
1098
+ 'div',
1099
+ { style: { flex: 1, minWidth: 0 } },
1100
+ h('div', { style: { fontSize: 14, fontWeight: 600, color: t.labelPrimary as string, lineHeight: '20px' } }, `Placeholder mappings — ${rows.length} mapped`),
1101
+ h('div', { style: { fontSize: 12, color: t.labelSecondary as string, lineHeight: '16px', marginTop: 2 } }, 'Blocked pattern → placeholder suggestion, one mapping per row'),
1102
+ ),
1103
+ h(Button as any, { variant: 'primary', size: 'md', onClick: addRow, 'data-maestro-placeholders-add': '' }, '+ Add mapping'),
1104
+ ),
1105
+ rows.length === 0
1106
+ ? h(
1107
+ 'div',
1108
+ {
1109
+ 'data-maestro-placeholders-empty': '',
1110
+ style: {
1111
+ display: 'flex',
1112
+ flexDirection: 'column',
1113
+ alignItems: 'center',
1114
+ gap: 10,
1115
+ padding: '20px 16px',
1116
+ borderRadius: 12,
1117
+ border: `1px dashed ${t.borderL2}`,
1118
+ background: 'transparent',
1119
+ textAlign: 'center' as const,
1120
+ },
1121
+ },
1122
+ h('div', { style: { fontSize: 13, color: t.labelSecondary as string, lineHeight: '18px' } }, 'No placeholder mappings yet — add your first mapping'),
1123
+ h(Button as any, { variant: 'outline', size: 'md', onClick: addRow }, '+ Add mapping'),
1124
+ )
1125
+ : h(
1126
+ 'div',
1127
+ { 'data-maestro-placeholders-list': '', style: { display: 'flex', flexDirection: 'column', gap: 12 } },
1128
+ ...rows.map((row, i) =>
1129
+ h(
1130
+ 'div',
1131
+ {
1132
+ key: i,
1133
+ 'data-maestro-mapping-card': '',
1134
+ style: {
1135
+ display: 'flex',
1136
+ flexDirection: 'column',
1137
+ gap: 12,
1138
+ padding: 12,
1139
+ borderRadius: 12,
1140
+ border: `1px solid ${t.borderL2}`,
1141
+ background: t.bgLayer1 as string,
1142
+ boxSizing: 'border-box' as const,
1143
+ },
1144
+ },
1145
+ // Card header: index badge + preview + X (same as Review's project card header)
1146
+ h(
1147
+ 'div',
1148
+ { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 } },
1149
+ h(
1150
+ 'div',
1151
+ { style: { display: 'flex', alignItems: 'center', gap: 8, minWidth: 0, flex: 1 } },
1152
+ h(
1153
+ 'span',
1154
+ {
1155
+ style: {
1156
+ flex: 'none',
1157
+ width: 24,
1158
+ height: 24,
1159
+ borderRadius: 12,
1160
+ background: 'var(--dsw-alias-bg-module-platform, #F5F6F7)',
1161
+ display: 'inline-flex',
1162
+ alignItems: 'center',
1163
+ justifyContent: 'center',
1164
+ fontSize: 11,
1165
+ fontWeight: 600,
1166
+ color: t.labelSecondary as string,
1167
+ },
1168
+ },
1169
+ String(i + 1),
1170
+ ),
1171
+ h(
1172
+ 'span',
1173
+ {
1174
+ style: {
1175
+ fontSize: 12,
1176
+ fontFamily: 'ui-monospace, monospace',
1177
+ color: row.pattern ? (t.labelPrimary as string) : (t.labelTertiary as string),
1178
+ overflow: 'hidden',
1179
+ textOverflow: 'ellipsis',
1180
+ whiteSpace: 'nowrap',
1181
+ minWidth: 0,
1182
+ },
1183
+ },
1184
+ row.pattern ? (row.suggestion ? `${row.pattern} → ${row.suggestion}` : `${row.pattern} → …`) : 'New mapping',
1185
+ ),
1186
+ ),
1187
+ h(Button as any, { variant: 'outline', size: 'sm', onClick: () => removeRow(i), 'aria-label': `Remove mapping ${i + 1}`, title: 'Remove mapping' }, '✕'),
1188
+ ),
1189
+ // Fields grid — 2-col desktop, 1-col mobile (same as Review's project grid)
1190
+ h(
1191
+ 'div',
1192
+ { 'data-maestro-mapping-grid': '', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 } },
1193
+ h(
1194
+ 'label',
1195
+ { style: fieldLabelStyle, 'data-maestro-field': '' },
1196
+ 'Blocked pattern',
1197
+ h(FieldInput as any, {
1198
+ value: row.pattern,
1199
+ placeholder: 'example-project',
1200
+ onChange: (e: any) => updateRow(i, 'pattern', e.target.value),
1201
+ 'aria-label': `Blocked pattern ${i + 1}`,
1202
+ style: { width: '100%' } as any,
1203
+ }),
1204
+ ),
1205
+ h(
1206
+ 'label',
1207
+ { style: fieldLabelStyle, 'data-maestro-field': '' },
1208
+ 'Placeholder',
1209
+ h(FieldInput as any, {
1210
+ value: row.suggestion,
1211
+ placeholder: 'my-project',
1212
+ onChange: (e: any) => updateRow(i, 'suggestion', e.target.value),
1213
+ 'aria-label': `Placeholder suggestion ${i + 1}`,
1214
+ style: { width: '100%' } as any,
1215
+ }),
1216
+ ),
1217
+ ),
1218
+ ),
1219
+ ),
1220
+ ),
1221
+ )
1222
+ }
1223
+
1057
1224
  // ---------------------------------------------------------------------------
1058
1225
  // Main — DSH-native grouped settings (DisclosureRow per domain)
1059
1226
  // ---------------------------------------------------------------------------
@@ -1071,35 +1238,50 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
1071
1238
  const [showLanPin, setShowLanPin] = useState(false)
1072
1239
  const [guard, setGuard] = useState<any>({})
1073
1240
  const [patternsText, setPatternsText] = useState('')
1074
- const [placeholdersText, setPlaceholdersText] = useState('')
1241
+ const [placeholderRows, setPlaceholderRows] = useState<{ pattern: string; suggestion: string }[]>([])
1075
1242
  const [supervisorCfg, setSupervisorCfg] = useState<any>({})
1076
1243
  const [notifierCfg, setNotifierCfg] = useState<any>({})
1077
1244
  const [activeTab, setActiveTab] = useState('tunnel')
1078
1245
  // Mobile: inject responsive overrides once (mirrors dsh-maestro-mobile settings-sheet pill pattern + market catsWrap)
1079
1246
  useEffect(() => {
1080
1247
  const css = `
1081
- /* Maestro nested tabsmarket-like pill bar + mobile fixes */
1082
- [data-maestro-tabs] { display:flex; gap:6px; overflow-x:auto; overflow-y:hidden; scrollbar-width:none; -webkit-overflow-scrolling:touch; overscroll-behavior-x:contain; touch-action:pan-x; padding-bottom:6px; margin-bottom:10px; border-bottom:1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.08)); }
1248
+ /* Model override dropdownprevent right-edge overlap */
1249
+ [data-maestro-control] [data-maestro-menu] { left:auto !important; right:0 !important; }
1250
+ [data-maestro-project-card] [data-maestro-menu] { left:auto !important; right:0 !important; }
1251
+ [data-maestro-trigger-wrap] { min-width:0; }
1252
+ /* Maestro nested tabs — unified with dsh-maestro-jobs (maestro-design pill bar) */
1253
+ [data-maestro-tabs] { display:flex; gap:6px; overflow-x:auto; overflow-y:hidden; scrollbar-width:none; -webkit-overflow-scrolling:touch; overscroll-behavior-x:contain; touch-action:pan-x; padding:2px 2px 8px; margin:0 -2px 4px; border-bottom:1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.08)); }
1083
1254
  [data-maestro-tabs]::-webkit-scrollbar { display:none; width:0; height:0; }
1084
- [data-maestro-tab] { flex:none; height:32px; min-width:fit-content; padding:0 14px; border-radius:999px; border:1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.12)); background:transparent; color:var(--dsw-alias-label-primary); font-size:13px; line-height:20px; white-space:nowrap; display:inline-flex; align-items:center; justify-content:center; cursor:pointer; font-family:inherit; -webkit-tap-highlight-color:transparent; }
1085
- [data-maestro-tab][data-active="true"] { background:var(--dsw-specific-sidebar-nav-item-active, #EBEEF2); border-color:var(--dsw-specific-sidebar-nav-item-active, #EBEEF2); color:var(--dsw-alias-label-primary); }
1255
+ [data-maestro-tab] { flex:none; min-width:fit-content; min-height:40px; padding:0 14px; border-radius:999px; border:1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.12)); background:transparent; color:var(--dsw-alias-label-secondary); font-size:13px; line-height:20px; font-weight:500; font-family:inherit; white-space:nowrap; display:inline-flex; align-items:center; justify-content:center; gap:6px; cursor:pointer; -webkit-tap-highlight-color:transparent; transition: background 150ms ease, color 150ms ease, border-color 150ms ease; touch-action:manipulation; }
1256
+ [data-maestro-tab][data-active="true"] { background:var(--dsw-specific-sidebar-nav-item-active, #EBEEF2); border-color:var(--dsw-specific-sidebar-nav-item-active, #EBEEF2); color:var(--dsw-alias-label-primary); font-weight:600; }
1086
1257
  [data-maestro-tab]:hover { background:var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,.06)); }
1087
1258
  [data-maestro-tab][data-active="true"]:hover { background:var(--dsw-specific-sidebar-nav-item-active, #EBEEF2); }
1088
1259
  [data-maestro-tab]:focus-visible { outline:2px solid var(--dsw-alias-state-business-primary, #4f6ef7); outline-offset:1px; }
1260
+ [data-maestro-tab]:active { transform: scale(.97); }
1089
1261
  [data-maestro-panel] { width:100%; min-width:0; box-sizing:border-box; }
1090
- /* labelinput dính nhau fix: flex column gap + full width */
1262
+ /* label/input overlap fix: flex column gap + full width */
1091
1263
  [data-maestro-panel] label { gap:6px !important; }
1092
1264
  [data-maestro-row]:last-child { border-bottom:none !important; }
1093
1265
  [data-maestro-panel] label > span { width:100% !important; box-sizing:border-box !important; }
1266
+ @media (prefers-reduced-motion: reduce) {
1267
+ [data-maestro-tab] { transition: none !important; }
1268
+ [data-maestro-tab]:active { transform: none !important; }
1269
+ }
1094
1270
  @media (max-width: 640px) {
1095
1271
  [data-maestro-settings-card] { max-width:100% !important; gap:6px !important; padding:0 2px !important; }
1096
- [data-maestro-tabs] { gap:6px !important; padding:0 0 6px !important; margin:0 -2px 10px !important; }
1097
- [data-maestro-tab] { height:32px !important; padding:0 12px !important; font-size:13px !important; }
1098
- [data-maestro-mapping-row] { flex-direction:column !important; align-items:stretch !important; gap:8px !important; }
1099
- [data-maestro-mapping-row] > * { flex:1 1 100% !important; width:100% !important; max-width:100% !important; }
1272
+ [data-maestro-tabs] { gap:6px !important; padding:2px 2px 8px !important; margin:0 -2px 8px !important; }
1273
+ [data-maestro-tab] { min-height:40px !important; height:auto !important; padding:0 12px !important; font-size:13px !important; }
1274
+ /* Placeholder mappings card design unified with Review project cards (header X, grid fields) */
1275
+ [data-maestro-placeholders-header] { flex-direction: column !important; align-items: stretch !important; }
1276
+ [data-maestro-placeholders-header] [data-maestro-placeholders-add] { width: 100% !important; justify-content: center !important; }
1277
+ [data-maestro-placeholders-list] { gap: 12px !important; }
1278
+ [data-maestro-mapping-card] { padding:10px !important; }
1279
+ [data-maestro-mapping-grid] { grid-template-columns:1fr !important; gap:12px !important; }
1100
1280
  [data-maestro-qr-row] { flex-direction:column !important; align-items:flex-start !important; }
1101
1281
  [data-maestro-trigger-wrap] { max-width:100% !important; }
1102
1282
  [data-maestro-menu] { min-width:0 !important; max-width:calc(100vw - 32px) !important; left:0 !important; right:auto !important; }
1283
+ [data-maestro-control] [data-maestro-menu] { left:0 !important; right:auto !important; }
1284
+ [data-maestro-project-card] [data-maestro-menu] { left:0 !important; right:auto !important; }
1103
1285
  [data-maestro-panel] label { gap:8px !important; }
1104
1286
  div[data-maestro-row] { flex-direction:column !important; align-items:stretch !important; padding:12px 0 !important; }
1105
1287
  [data-maestro-row-text] { padding-right:0 !important; }
@@ -1112,7 +1294,7 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
1112
1294
  [data-maestro-project-profile-row] > label { flex:1 1 100% !important; width:100% !important; }
1113
1295
  }
1114
1296
  @media (max-width: 390px) {
1115
- [data-maestro-tab] { height:30px !important; padding:0 10px !important; font-size:12px !important; }
1297
+ [data-maestro-tab] { min-height:38px !important; height:auto !important; padding:0 10px !important; font-size:12px !important; }
1116
1298
  }
1117
1299
  `
1118
1300
  const tag = document.createElement('style')
@@ -1165,15 +1347,13 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
1165
1347
  setError(e.message ?? String(e))
1166
1348
  }
1167
1349
  }
1168
- const commitPlaceholders = async () => {
1350
+ const commitPlaceholders = async (rows: { pattern: string; suggestion: string }[]) => {
1169
1351
  setError(null)
1170
- let obj: any = {}
1171
- try {
1172
- obj = placeholdersText.trim() ? JSON.parse(placeholdersText) : {}
1173
- if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) throw new Error('placeholders must be JSON object')
1174
- } catch (e: any) {
1175
- setError(`placeholders JSON invalid: ${e.message ?? String(e)}`)
1176
- return
1352
+ const obj: Record<string, string> = {}
1353
+ for (const row of rows) {
1354
+ const pattern = (row.pattern ?? '').trim()
1355
+ if (!pattern) continue
1356
+ obj[pattern] = (row.suggestion ?? '').trim()
1177
1357
  }
1178
1358
  try {
1179
1359
  await cfgSet('guardBlacklist', { placeholders: obj })
@@ -1196,6 +1376,7 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
1196
1376
  const next = { ...prev }
1197
1377
  for (const [k, v] of Object.entries(patch)) {
1198
1378
  if (k === 'telegram' && typeof v === 'object' && v !== null) (next as any).telegram = { ...((prev as any).telegram ?? {}), ...(v as any) }
1379
+ else if (k === 'policy' && typeof v === 'object' && v !== null) (next as any).policy = { ...((prev as any).policy ?? {}), ...(v as any) }
1199
1380
  else (next as any)[k] = v
1200
1381
  }
1201
1382
  return next
@@ -1230,7 +1411,7 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
1230
1411
  const pats = Array.isArray((bl as any)?.patterns) ? (bl as any).patterns : []
1231
1412
  const ph = (bl as any)?.placeholders && typeof (bl as any).placeholders === 'object' ? (bl as any).placeholders : {}
1232
1413
  setPatternsText(pats.join('\n'))
1233
- setPlaceholdersText(JSON.stringify(ph, null, 2))
1414
+ setPlaceholderRows(Object.entries(ph).map(([pattern, suggestion]) => ({ pattern, suggestion: String(suggestion) })))
1234
1415
  setSupervisorCfg(sup ?? {})
1235
1416
  setNotifierCfg(not ?? {})
1236
1417
  })
@@ -1348,17 +1529,16 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
1348
1529
  }
1349
1530
  }
1350
1531
 
1351
- // Nested tabs like plugin marketplace — pill bar + single panel (market catsWrap catsRow pattern)
1352
- const TABS = [
1353
- { id: 'tunnel', label: 'Tunnel' },
1354
- { id: 'notify', label: 'Notify' },
1355
- { id: 'gitlab', label: 'GitLab' },
1356
- { id: 'review', label: 'Review' },
1357
- { id: 'guard', label: 'Guard' },
1358
- { id: 'blacklist', label: 'Blacklist' },
1359
- { id: 'supervisor', label: 'Supervisor' },
1360
- { id: 'notifier', label: 'Notifier' },
1361
- ] as const
1532
+ // Nested tabs unified pill bar with icons (maestro-design, matches dsh-maestro-jobs)
1533
+ const TABS: Array<{ id: string; label: string; icon: TabIcon }> = [
1534
+ { id: 'tunnel', label: 'Tunnel', icon: 'globe' },
1535
+ { id: 'gitlab', label: 'GitLab', icon: 'git' },
1536
+ { id: 'review', label: 'Review', icon: 'search' },
1537
+ { id: 'guard', label: 'Guard', icon: 'shield' },
1538
+ { id: 'blacklist', label: 'Blacklist', icon: 'ban' },
1539
+ { id: 'supervisor', label: 'Supervisor', icon: 'cpu' },
1540
+ { id: 'notifier', label: 'Notifier', icon: 'bell' },
1541
+ ]
1362
1542
 
1363
1543
  const tabContents: Record<string, unknown> = {
1364
1544
  tunnel: h(
@@ -1377,14 +1557,6 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
1377
1557
  h('div', { style: { ...cardInsetStyle, marginTop: '12px' } }, h('div', { style: { fontSize: 13, fontWeight: 600, color: t.labelPrimary as string } }, 'Remote access — LAN'), h(LanAccess as any, { proxyStatus, lanPin: lanPinEnabled === null ? null : { enabled: lanPinEnabled, pin: lanPin, show: showLanPin, onShow: revealLanPin, onHide: () => setShowLanPin(false), onRotate: rotateLanPin, onToggle: toggleLanPin } })),
1378
1558
  h('div', { style: { ...cardInsetStyle, marginTop: '12px' } }, h('div', { style: { fontSize: 13, fontWeight: 600, color: t.labelPrimary as string } }, 'Public access'), h(PublicAccess as any, { status, pin, showPin, onRevealPin: revealPin, onHidePin: () => setShowPin(false), onRotatePin: rotatePin })),
1379
1559
  ),
1380
- notify: h(
1381
- 'div',
1382
- { style: { display: 'flex', flexDirection: 'column' } },
1383
- h('div', { style: { padding: '12px 0', borderBottom: `1px solid ${t.borderL2}` } }, h('p', { style: captionStyle }, 'Sends one protected startup update with the current public-access PIN to a single chat. Leave either credential blank to disable.')),
1384
- h(SettingRow as any, { title: 'Bot token', description: 'Telegram bot token from @BotFather.', control: h('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } }, h(FieldInput as any, { placeholder: '123456:ABC-DEF...', type: 'password', autoComplete: 'off', value: config.hasTelegramBotToken ? '••••••••' : '', onChange: (e: any) => saveField('telegramBotToken', e.target.value), style: { width: 220 } as any }), config.hasTelegramBotToken ? h(Button as any, { variant: 'outline', size: 'md', onClick: () => saveField('telegramBotToken', '') }, 'Clear') : null) }),
1385
- h(SettingRow as any, { title: 'Chat ID', description: 'Target chat, e.g. -1001234567890.', control: h(FieldInput as any, { placeholder: '-1001234567890', value: config.telegramChatId ?? '', onChange: (e: any) => saveField('telegramChatId', e.target.value), style: { width: 220 } as any }) }),
1386
- h(ToggleRow as any, { title: 'Also notify about reviews', description: 'One message per review run with its outcome.', checked: config.telegramReviewNotifications === true, onChange: (v: boolean) => saveField('telegramReviewNotifications', v) }),
1387
- ),
1388
1560
  gitlab: h(
1389
1561
  'div',
1390
1562
  { style: { display: 'flex', flexDirection: 'column' } },
@@ -1424,9 +1596,15 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
1424
1596
  ),
1425
1597
  h('div', { style: { ...rowStyle, flexDirection:'column', alignItems:'stretch', gap: 8, borderBottom:'none' } as any },
1426
1598
  h('div', { style: rowTitleStyle }, 'Placeholder mappings'),
1427
- h('div', { style: rowDescStyle }, 'JSON object mapping blocked patterns to placeholder suggestions.'),
1428
- h(TextareaField as any, { value: placeholdersText, placeholder: '{"example-project":"my-project"}', onChange: (e: any) => setPlaceholdersText(e.target.value), onBlur: () => commitPlaceholders(), style: { minHeight: 80 } as any }),
1429
- h('div', { style: { marginTop: 8 } }, h(Button as any, { variant: 'outline', size: 'sm', onClick: () => { commitBlacklistPatterns(patternsText); commitPlaceholders() } }, 'Save Blacklist')),
1599
+ h('div', { style: rowDescStyle }, 'Blocked pattern → placeholder suggestion. One mapping per row no JSON needed.'),
1600
+ h(PlaceholderMappingsEditor as any, {
1601
+ rows: placeholderRows,
1602
+ onChange: (rows: any) => {
1603
+ setPlaceholderRows(rows)
1604
+ commitPlaceholders(rows)
1605
+ },
1606
+ }),
1607
+ h('div', { style: { marginTop: 8 } }, h(Button as any, { variant: 'outline', size: 'sm', onClick: () => { commitBlacklistPatterns(patternsText); commitPlaceholders(placeholderRows) } }, 'Save Blacklist')),
1430
1608
  ),
1431
1609
  ),
1432
1610
  supervisor: h(
@@ -1439,9 +1617,10 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
1439
1617
  notifier: h(
1440
1618
  'div',
1441
1619
  { style: { display: 'flex', flexDirection: 'column' } },
1442
- h(SettingRow as any, { title: 'Bot token', description: 'Telegram bot token from @BotFather.', control: h(FieldInput as any, { type: 'password', autoComplete: 'off', value: notifierCfg.telegram?.botToken ?? '', placeholder: '123456:ABC-DEF...', onChange: (e: any) => saveNotifierCfg({ telegram: { botToken: e.target.value } }), style: { width: 260 } as any }) }),
1443
- h(SettingRow as any, { title: 'Chat ID', description: 'Target chat, e.g. -1001234567890.', control: h(FieldInput as any, { value: notifierCfg.telegram?.chatId ?? '', placeholder: '-1001234567890', onChange: (e: any) => saveNotifierCfg({ telegram: { chatId: e.target.value } }), style: { width: 260 } as any }) }),
1444
- h(ToggleRow as any, { title: 'Review notifications', description: 'Also notify about finished reviews.', checked: notifierCfg.telegram?.reviewNotifications === true || notifierCfg.policy?.reviewNotifications === true, onChange: (v: boolean) => saveNotifierCfg({ telegram: { reviewNotifications: v } }) }),
1620
+ h('div', { style: { padding: '12px 0', borderBottom: `1px solid ${t.borderL2}` } }, h('p', { style: captionStyle }, 'Telegram bot settings for notifications: startup PIN, review digests, PIN rotation. Leave blank to disable.')),
1621
+ h(SettingRow as any, { title: 'Bot token', description: 'Telegram bot token from @BotFather.', control: h('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } }, h(FieldInput as any, { type: 'password', autoComplete: 'off', value: notifierCfg.telegram?.botToken ?? '', placeholder: '123456:ABC-DEF...', onChange: (e: any) => saveNotifierCfg({ telegram: { botToken: e.target.value } }), style: { width: 220 } as any }), notifierCfg.telegram?.botToken ? h(Button as any, { variant: 'outline', size: 'md', onClick: () => saveNotifierCfg({ telegram: { botToken: '' } }) }, 'Clear') : null) }),
1622
+ h(SettingRow as any, { title: 'Chat ID', description: 'Target chat, e.g. -1001234567890.', control: h(FieldInput as any, { value: notifierCfg.telegram?.chatId ?? '', placeholder: '-1001234567890', onChange: (e: any) => saveNotifierCfg({ telegram: { chatId: e.target.value } }), style: { width: 220 } as any }) }),
1623
+ h(ToggleRow as any, { title: 'Review notifications', description: 'Also notify about finished reviews.', checked: notifierCfg.policy?.reviewNotifications === true, onChange: (v: boolean) => saveNotifierCfg({ policy: { reviewNotifications: v } }) }),
1445
1624
  ),
1446
1625
  }
1447
1626
 
@@ -1450,12 +1629,19 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
1450
1629
  { 'data-maestro-settings-card': '', style: { display: 'flex', flexDirection: 'column', gap: 8, width: '100%', maxWidth: 640, minWidth:0, boxSizing:'border-box' as any } },
1451
1630
  h(
1452
1631
  'div',
1453
- { style: { padding: '2px 2px 8px' } },
1454
- h('div', { style: { fontSize: 15, fontWeight: 600, color: t.labelPrimary as string, lineHeight: '22px' } }, 'Maestro'),
1455
- h('div', { style: { fontSize: 12, color: t.labelSecondary as string, lineHeight: '16px', marginTop: 2 } }, 'Tunnel, access, review & guard all via the shared Maestro store. Uses the same tokens and primitives as DSH settings.'),
1632
+ { style: { padding: '2px 2px 8px', display: 'flex', gap: 10, alignItems: 'flex-start' } },
1633
+ // Shared BrandBadge same as dashboard sidebar/popup (BrandMark #0A84FF)
1634
+ h('span', { 'data-maestro-logo': '', style: { width: 28, height: 28, borderRadius: 8, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', background: 'var(--dsw-alias-brand-primary, #0A84FF)', backgroundColor: '#0A84FF', color: '#fff', flex: 'none', border: '1px solid rgba(0,0,0,0.08)', boxShadow: '0 0 0 1px var(--dsw-alias-border-l1)', boxSizing: 'border-box' as any, alignSelf: 'flex-start' as any, marginTop: 2 } } as any,
1635
+ h('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': 'true' } as any, h('path', { d: 'M2 11 L5 4 L8 9 L11 4 L14 11', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round' } as any))
1636
+ ),
1637
+ h('div', { style: { display: 'flex', flexDirection: 'column', minWidth: 0 } },
1638
+ h('div', { style: { fontSize: 15, fontWeight: 600, color: t.labelPrimary as string, lineHeight: '22px' } }, 'Maestro'),
1639
+ h('div', { style: { fontSize: 12, color: t.labelSecondary as string, lineHeight: '16px', marginTop: 2 } }, 'Tunnel, access, review & guard — all via the shared Maestro store. Uses the same tokens and primitives as DSH settings.'),
1640
+ )
1456
1641
  ),
1642
+ h('style', {}, '[data-maestro-logo]{background:#0A84FF !important; background-color:#0A84FF !important; color:#fff !important;}'),
1457
1643
  h('div', { 'data-maestro-tabs': '', role:'tablist', 'aria-label':'Maestro settings sections' },
1458
- ...TABS.map(tab => h('button', { key: tab.id, 'data-maestro-tab':'', 'data-active': String(activeTab===tab.id), role:'tab', 'aria-selected': activeTab===tab.id, onClick: () => setActiveTab(tab.id) }, tab.label))
1644
+ ...TABS.map(tab => h('button', { key: tab.id, 'data-maestro-tab':'', 'data-active': String(activeTab===tab.id), role:'tab', 'aria-selected': activeTab===tab.id, onClick: (e: any) => { setActiveTab(tab.id); try { e.currentTarget.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }) } catch {} } }, h(TabIcon as any, { name: tab.icon }), tab.label))
1459
1645
  ),
1460
1646
  h('div', { 'data-maestro-panel': activeTab, style: { display:'flex', flexDirection:'column', gap:10, minWidth:0 } }, tabContents[activeTab] as any),
1461
1647
  error ? h('p', { style: { color: t.stateError as string, fontSize: 12, margin: '8px 0 0', padding: '8px 10px', borderRadius: 8, background: 'color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent)', border: `1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 30%, transparent)` } }, error) : null,
@@ -0,0 +1,22 @@
1
+ import { createElement as h } from 'react'
2
+
3
+ export function MaestroMark(props: { size?: number }) {
4
+ const s = props.size ?? 16
5
+ return h('svg', { width: s, height: s, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': 'true' } as any,
6
+ h('path', { d: 'M2 11 L5 4 L8 9 L11 4 L14 11', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round' } as any)
7
+ )
8
+ }
9
+
10
+ export function BrandBadge(props: { size?: number; outer?: number; radius?: number }) {
11
+ const outer = props.outer ?? 28
12
+ const size = props.size ?? 16
13
+ const radius = props.radius ?? 8
14
+ return h('span', {
15
+ 'data-maestro-logo': '',
16
+ style: {
17
+ width: outer, height: outer, borderRadius: radius, display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
18
+ background: 'var(--dsw-alias-brand-primary, #0A84FF)', backgroundColor: '#0A84FF', color: '#fff', flex: 'none',
19
+ border: '1px solid rgba(0,0,0,0.08)', boxShadow: '0 0 0 1px var(--dsw-alias-border-l1)', boxSizing: 'border-box' as any,
20
+ },
21
+ } as any, h(MaestroMark as any, { size }))
22
+ }
@@ -13,9 +13,10 @@ import { registerSettingsNavIcon, SETTINGS_NAV_MARKER } from './settings-nav-ico
13
13
  */
14
14
  const SETTINGS_NAV_CSS = `
15
15
 
16
- /* maestro: replace the settings-nav fallback gear with the Maestro M-logo glyph */
17
- [${SETTINGS_NAV_MARKER}] > svg:first-child {
18
- display: none;
16
+ /* maestro: replace the settings-nav fallback gear with the Maestro M-logo glyph — same mark as sidebar/popup BrandMark */
17
+ [${SETTINGS_NAV_MARKER}] > svg:first-child,
18
+ [${SETTINGS_NAV_MARKER}] > svg.zWKi1a_navIcon {
19
+ display: none !important;
19
20
  }
20
21
 
21
22
  [${SETTINGS_NAV_MARKER}]::before {
@@ -23,9 +24,10 @@ const SETTINGS_NAV_CSS = `
23
24
  flex: none;
24
25
  width: 16px;
25
26
  height: 16px;
27
+ display: inline-block;
26
28
  background: currentColor;
27
- -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 11 L5 4 L8 9 L11 4 L14 11'/%3E%3C/svg%3E") center / contain no-repeat;
28
- mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 11 L5 4 L8 9 L11 4 L14 11'/%3E%3C/svg%3E") center / contain no-repeat;
29
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 11 L5 4 L8 9 L11 4 L14 11'/%3E%3C/svg%3E") center / contain no-repeat;
30
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 11 L5 4 L8 9 L11 4 L14 11'/%3E%3C/svg%3E") center / contain no-repeat;
29
31
  }
30
32
  `
31
33