@ddtcorex/dsh-maestro-config 0.2.1 → 0.3.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
@@ -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
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 });
@@ -3139,16 +3304,12 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
3139
3304
  call(MAESTRO_ENDPOINTS.getConfig, {}).then((saved) => setConfig((prev) => ({ ...prev, ...saved }))).catch(() => {
3140
3305
  });
3141
3306
  if (configRpcCall) {
3142
- configRpcCall("get", { domain: "supervisor" }).then((res) => {
3143
- if (res?.ok && res.value?.model) setConfig((prev) => ({ ...prev, supervisorModel: res.value.model }));
3144
- }).catch(() => {
3145
- });
3146
3307
  Promise.all([cfgGet("guard").catch(() => ({})), cfgGet("guardBlacklist").catch(() => ({ patterns: [], placeholders: {} })), cfgGet("supervisor").catch(() => ({})), cfgGet("notifier").catch(() => ({}))]).then(([g, bl, sup, not]) => {
3147
3308
  setGuard(g ?? {});
3148
3309
  const pats = Array.isArray(bl?.patterns) ? bl.patterns : [];
3149
3310
  const ph = bl?.placeholders && typeof bl.placeholders === "object" ? bl.placeholders : {};
3150
3311
  setPatternsText(pats.join("\n"));
3151
- setPlaceholdersText(JSON.stringify(ph, null, 2));
3312
+ setPlaceholderRows(Object.entries(ph).map(([pattern, suggestion]) => ({ pattern, suggestion: String(suggestion) })));
3152
3313
  setSupervisorCfg(sup ?? {});
3153
3314
  setNotifierCfg(not ?? {});
3154
3315
  }).catch(() => {
@@ -3251,13 +3412,6 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
3251
3412
  const saveField = async (field, value) => {
3252
3413
  setError(null);
3253
3414
  setConfig((prev) => ({ ...prev, [field]: value }));
3254
- if (field === "supervisorModel" && configRpcCall) {
3255
- try {
3256
- const res = await configRpcCall("set", { domain: "supervisor", patch: { model: value } });
3257
- if (res?.ok) return;
3258
- } catch {
3259
- }
3260
- }
3261
3415
  try {
3262
3416
  await call(MAESTRO_ENDPOINTS.saveConfig, { [field]: value });
3263
3417
  } catch (err) {
@@ -3265,13 +3419,13 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
3265
3419
  }
3266
3420
  };
3267
3421
  const TABS = [
3268
- { id: "tunnel", label: "Tunnel" },
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" }
3422
+ { id: "tunnel", label: "Tunnel", icon: "globe" },
3423
+ { id: "gitlab", label: "GitLab", icon: "git" },
3424
+ { id: "review", label: "Review", icon: "search" },
3425
+ { id: "guard", label: "Guard", icon: "shield" },
3426
+ { id: "blacklist", label: "Blacklist", icon: "ban" },
3427
+ { id: "supervisor", label: "Supervisor", icon: "cpu" },
3428
+ { id: "notifier", label: "Notifier", icon: "bell" }
3275
3429
  ];
3276
3430
  const tabContents = {
3277
3431
  tunnel: (0, import_react.createElement)(
@@ -3309,7 +3463,6 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
3309
3463
  { style: { display: "flex", flexDirection: "column" } },
3310
3464
  (0, import_react.createElement)(ToggleRow, { title: "Re-review on push", description: "When new commits are pushed, trigger an automatic re-review.", checked: config.autoRereviewOnPush === true, onChange: (v) => saveField("autoRereviewOnPush", v) }),
3311
3465
  (0, import_react.createElement)(SettingRow, { title: "Global review model", description: "Model for automated reviews. Empty = DSH default.", control: (0, import_react.createElement)(ReviewModelSelector, { value: config.reviewModel ?? null, catalog, fallbackValue: catalog?.current ?? null, fallbackLabel: "Use DSH default", onChange: (v) => saveField("reviewModel", v), label: null }) }),
3312
- (0, import_react.createElement)(SettingRow, { title: "Supervisor model", description: "Model for auto-fixing DSH Web crashes.", control: (0, import_react.createElement)(ReviewModelSelector, { value: config.supervisorModel ?? null, catalog, fallbackValue: catalog?.current ?? null, fallbackLabel: "Use DSH default", onChange: (v) => saveField("supervisorModel", v), label: null }) }),
3313
3466
  (0, import_react.createElement)(ProjectMappingsEditor, { mappings: config.projectMappings ?? [], onChange: (mappings) => saveField("projectMappings", mappings), catalog, globalReviewModel: config.reviewModel ?? null })
3314
3467
  ),
3315
3468
  guard: (0, import_react.createElement)(
@@ -3335,11 +3488,17 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
3335
3488
  "div",
3336
3489
  { style: { ...rowStyle, flexDirection: "column", alignItems: "stretch", gap: 8, borderBottom: "none" } },
3337
3490
  (0, import_react.createElement)("div", { style: rowTitleStyle }, "Placeholder mappings"),
3338
- (0, import_react.createElement)("div", { style: rowDescStyle }, "JSON object mapping blocked patterns to placeholder suggestions."),
3339
- (0, import_react.createElement)(TextareaField, { value: placeholdersText, placeholder: '{"example-project":"my-project"}', onChange: (e) => setPlaceholdersText(e.target.value), onBlur: () => commitPlaceholders(), style: { minHeight: 80 } }),
3491
+ (0, import_react.createElement)("div", { style: rowDescStyle }, "Blocked pattern \u2192 placeholder suggestion. One mapping per row \u2014 no JSON needed."),
3492
+ (0, import_react.createElement)(PlaceholderMappingsEditor, {
3493
+ rows: placeholderRows,
3494
+ onChange: (rows) => {
3495
+ setPlaceholderRows(rows);
3496
+ commitPlaceholders(rows);
3497
+ }
3498
+ }),
3340
3499
  (0, import_react.createElement)("div", { style: { marginTop: 8 } }, (0, import_react.createElement)(Button, { variant: "outline", size: "sm", onClick: () => {
3341
3500
  commitBlacklistPatterns(patternsText);
3342
- commitPlaceholders();
3501
+ commitPlaceholders(placeholderRows);
3343
3502
  } }, "Save Blacklist"))
3344
3503
  )
3345
3504
  ),
@@ -3388,7 +3547,13 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
3388
3547
  (0, import_react.createElement)(
3389
3548
  "div",
3390
3549
  { "data-maestro-tabs": "", role: "tablist", "aria-label": "Maestro settings sections" },
3391
- ...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))
3550
+ ...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) => {
3551
+ setActiveTab(tab.id);
3552
+ try {
3553
+ e.currentTarget.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
3554
+ } catch {
3555
+ }
3556
+ } }, (0, import_react.createElement)(TabIcon, { name: tab.icon }), tab.label))
3392
3557
  ),
3393
3558
  (0, import_react.createElement)("div", { "data-maestro-panel": activeTab, style: { display: "flex", flexDirection: "column", gap: 10, minWidth: 0 } }, tabContents[activeTab]),
3394
3559
  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
@@ -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;;;;;;;;;mBA+XmE,GAAG;;gBAoBzK"}
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;;;;;;;;;mBAsYmE,GAAG;;gBAoBzK"}
@@ -1 +1 @@
1
- {"version":3,"file":"maestro-card.d.ts","sourceRoot":"","sources":["../../../src/client/maestro-card.jsx"],"names":[],"mappings":"AAweA;;;;;;;;gBA0dC"}
1
+ {"version":3,"file":"maestro-card.d.ts","sourceRoot":"","sources":["../../../src/client/maestro-card.jsx"],"names":[],"mappings":"AAweA;;;;;;;;gBA2bC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ddtcorex/dsh-maestro-config",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
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",
@@ -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
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 })
@@ -1220,18 +1400,13 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
1220
1400
  .then((saved) => setConfig((prev: any) => ({ ...prev, ...saved })))
1221
1401
  .catch(() => {})
1222
1402
  if (configRpcCall) {
1223
- configRpcCall('get', { domain: 'supervisor' })
1224
- .then((res: any) => {
1225
- if (res?.ok && res.value?.model) setConfig((prev: any) => ({ ...prev, supervisorModel: res.value.model }))
1226
- })
1227
- .catch(() => {})
1228
1403
  Promise.all([cfgGet('guard').catch(() => ({})), cfgGet('guardBlacklist').catch(() => ({ patterns: [], placeholders: {} })), cfgGet('supervisor').catch(() => ({})), cfgGet('notifier').catch(() => ({}))])
1229
1404
  .then(([g, bl, sup, not]) => {
1230
1405
  setGuard(g ?? {})
1231
1406
  const pats = Array.isArray((bl as any)?.patterns) ? (bl as any).patterns : []
1232
1407
  const ph = (bl as any)?.placeholders && typeof (bl as any).placeholders === 'object' ? (bl as any).placeholders : {}
1233
1408
  setPatternsText(pats.join('\n'))
1234
- setPlaceholdersText(JSON.stringify(ph, null, 2))
1409
+ setPlaceholderRows(Object.entries(ph).map(([pattern, suggestion]) => ({ pattern, suggestion: String(suggestion) })))
1235
1410
  setSupervisorCfg(sup ?? {})
1236
1411
  setNotifierCfg(not ?? {})
1237
1412
  })
@@ -1336,12 +1511,6 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
1336
1511
  const saveField = async (field: string, value: unknown) => {
1337
1512
  setError(null)
1338
1513
  setConfig((prev: any) => ({ ...prev, [field]: value }))
1339
- if (field === 'supervisorModel' && configRpcCall) {
1340
- try {
1341
- const res = await configRpcCall('set', { domain: 'supervisor', patch: { model: value } })
1342
- if (res?.ok) return
1343
- } catch {}
1344
- }
1345
1514
  try {
1346
1515
  await call(MAESTRO_ENDPOINTS.saveConfig, { [field]: value })
1347
1516
  } catch (err: any) {
@@ -1349,16 +1518,16 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
1349
1518
  }
1350
1519
  }
1351
1520
 
1352
- // Nested tabs like plugin marketplace — pill bar + single panel (market catsWrap catsRow pattern)
1353
- const TABS = [
1354
- { id: 'tunnel', label: 'Tunnel' },
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
1521
+ // Nested tabs unified pill bar with icons (maestro-design, matches dsh-maestro-jobs)
1522
+ const TABS: Array<{ id: string; label: string; icon: TabIcon }> = [
1523
+ { id: 'tunnel', label: 'Tunnel', icon: 'globe' },
1524
+ { id: 'gitlab', label: 'GitLab', icon: 'git' },
1525
+ { id: 'review', label: 'Review', icon: 'search' },
1526
+ { id: 'guard', label: 'Guard', icon: 'shield' },
1527
+ { id: 'blacklist', label: 'Blacklist', icon: 'ban' },
1528
+ { id: 'supervisor', label: 'Supervisor', icon: 'cpu' },
1529
+ { id: 'notifier', label: 'Notifier', icon: 'bell' },
1530
+ ]
1362
1531
 
1363
1532
  const tabContents: Record<string, unknown> = {
1364
1533
  tunnel: h(
@@ -1394,7 +1563,6 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
1394
1563
  { style: { display: 'flex', flexDirection: 'column' } },
1395
1564
  h(ToggleRow as any, { title: 'Re-review on push', description: 'When new commits are pushed, trigger an automatic re-review.', checked: config.autoRereviewOnPush === true, onChange: (v: boolean) => saveField('autoRereviewOnPush', v) }),
1396
1565
  h(SettingRow as any, { title: 'Global review model', description: 'Model for automated reviews. Empty = DSH default.', control: h(ReviewModelSelector as any, { value: config.reviewModel ?? null, catalog, fallbackValue: catalog?.current ?? null, fallbackLabel: 'Use DSH default', onChange: (v: any) => saveField('reviewModel', v), label: null }) }),
1397
- h(SettingRow as any, { title: 'Supervisor model', description: 'Model for auto-fixing DSH Web crashes.', control: h(ReviewModelSelector as any, { value: config.supervisorModel ?? null, catalog, fallbackValue: catalog?.current ?? null, fallbackLabel: 'Use DSH default', onChange: (v: any) => saveField('supervisorModel', v), label: null }) }),
1398
1566
  h(ProjectMappingsEditor as any, { mappings: config.projectMappings ?? [], onChange: (mappings: any) => saveField('projectMappings', mappings), catalog, globalReviewModel: config.reviewModel ?? null }),
1399
1567
  ),
1400
1568
  guard: h(
@@ -1416,9 +1584,15 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
1416
1584
  ),
1417
1585
  h('div', { style: { ...rowStyle, flexDirection:'column', alignItems:'stretch', gap: 8, borderBottom:'none' } as any },
1418
1586
  h('div', { style: rowTitleStyle }, 'Placeholder mappings'),
1419
- h('div', { style: rowDescStyle }, 'JSON object mapping blocked patterns to placeholder suggestions.'),
1420
- 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 }),
1421
- h('div', { style: { marginTop: 8 } }, h(Button as any, { variant: 'outline', size: 'sm', onClick: () => { commitBlacklistPatterns(patternsText); commitPlaceholders() } }, 'Save Blacklist')),
1587
+ h('div', { style: rowDescStyle }, 'Blocked pattern → placeholder suggestion. One mapping per row no JSON needed.'),
1588
+ h(PlaceholderMappingsEditor as any, {
1589
+ rows: placeholderRows,
1590
+ onChange: (rows: any) => {
1591
+ setPlaceholderRows(rows)
1592
+ commitPlaceholders(rows)
1593
+ },
1594
+ }),
1595
+ h('div', { style: { marginTop: 8 } }, h(Button as any, { variant: 'outline', size: 'sm', onClick: () => { commitBlacklistPatterns(patternsText); commitPlaceholders(placeholderRows) } }, 'Save Blacklist')),
1422
1596
  ),
1423
1597
  ),
1424
1598
  supervisor: h(
@@ -1455,7 +1629,7 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
1455
1629
  ),
1456
1630
  h('style', {}, '[data-maestro-logo]{background:#0A84FF !important; background-color:#0A84FF !important; color:#fff !important;}'),
1457
1631
  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))
1632
+ ...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
1633
  ),
1460
1634
  h('div', { 'data-maestro-panel': activeTab, style: { display:'flex', flexDirection:'column', gap:10, minWidth:0 } }, tabContents[activeTab] as any),
1461
1635
  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,
@@ -578,15 +578,7 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }) {
578
578
  call(MAESTRO_ENDPOINTS.getConfig, {})
579
579
  .then(saved => setConfig(prev => ({ ...prev, ...saved })))
580
580
  .catch(() => { /* first run, no config saved yet — keep defaults */ })
581
- // Supervisor model may be stored via generic config service when review not installed — try fallback
582
581
  if (configRpcCall) {
583
- configRpcCall('get', { domain: 'supervisor' })
584
- .then(res => {
585
- if (res?.ok && res.value?.model) {
586
- setConfig(prev => ({ ...prev, supervisorModel: res.value.model }))
587
- }
588
- })
589
- .catch(() => { /* supervisor domain not yet set or config service unavailable */ })
590
582
  // Task 3: load Guard/Blacklist/Supervisor/Notifier domains
591
583
  Promise.all([
592
584
  cfgGet('guard').catch(() => ({})),
@@ -688,16 +680,6 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }) {
688
680
  const saveField = async (field, value) => {
689
681
  setError(null)
690
682
  setConfig(prev => ({ ...prev, [field]: value }))
691
- // Supervisor model can be saved via generic config service when review not installed (independent install)
692
- if (field === 'supervisorModel' && configRpcCall) {
693
- try {
694
- const res = await configRpcCall('set', { domain: 'supervisor', patch: { model: value } })
695
- if (res?.ok) return
696
- // Fall through to review RPC if generic set fails
697
- } catch (e) {
698
- // Fall through to review RPC
699
- }
700
- }
701
683
  try {
702
684
  await call(MAESTRO_ENDPOINTS.saveConfig, { [field]: value })
703
685
  } catch (err) {
@@ -817,19 +799,6 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }) {
817
799
  }),
818
800
  ),
819
801
 
820
- h('div', { style: sectionStyle },
821
- h('h4', { style: headingStyle }, 'Supervisor LLM'),
822
- h('p', { style: captionStyle }, 'Model used by the supervisor debug-agent to auto-fix DSH Web crashes. Empty = DSH default (or Review model if set). Uses the same provider catalog as Review.'),
823
- h(ReviewModelSelector, {
824
- value: config.supervisorModel ?? null,
825
- catalog,
826
- fallbackValue: catalog?.current ?? null,
827
- fallbackLabel: 'Use DSH default',
828
- onChange: v => saveField('supervisorModel', v),
829
- label: 'Supervisor model',
830
- }),
831
- ),
832
-
833
802
  h('div', { style: sectionStyle },
834
803
  h('h4', { style: headingStyle }, 'Projects'),
835
804
  h(ProjectMappingsEditor, { mappings: config.projectMappings ?? [], onChange: mappings => saveField('projectMappings', mappings), catalog, globalReviewModel: config.reviewModel ?? null }),