@ddtcorex/dsh-maestro-config 0.1.0 → 0.1.2

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
@@ -2117,7 +2117,8 @@ var require_browser = __commonJS({
2117
2117
  // src/client/index.tsx
2118
2118
  var index_exports = {};
2119
2119
  __export(index_exports, {
2120
- apply: () => apply
2120
+ apply: () => apply,
2121
+ inject: () => inject
2121
2122
  });
2122
2123
  module.exports = __toCommonJS(index_exports);
2123
2124
 
@@ -2221,6 +2222,29 @@ var codeStyle = {
2221
2222
  color: "var(--dsw-alias-label-primary)",
2222
2223
  wordBreak: "break-all"
2223
2224
  };
2225
+ var textareaStyle = {
2226
+ ...inputStyle,
2227
+ height: 120,
2228
+ padding: "8px 12px",
2229
+ resize: "vertical"
2230
+ };
2231
+ var tabBarStyle = {
2232
+ display: "flex",
2233
+ gap: 8,
2234
+ borderBottom: "1px solid var(--dsw-alias-border-l2)",
2235
+ marginBottom: 16
2236
+ };
2237
+ var tabButtonStyle = (active) => ({
2238
+ padding: "8px 14px",
2239
+ border: "none",
2240
+ borderBottom: active ? "2px solid var(--dsw-alias-button-primary-fill)" : "2px solid transparent",
2241
+ background: "transparent",
2242
+ color: active ? "var(--dsw-alias-label-primary)" : "var(--dsw-alias-label-secondary)",
2243
+ font: "inherit",
2244
+ fontSize: 13,
2245
+ fontWeight: active ? 600 : 400,
2246
+ cursor: "pointer"
2247
+ });
2224
2248
  function QrImage({ url, size = 104 }) {
2225
2249
  const [dataUrl, setDataUrl] = (0, import_react.useState)(null);
2226
2250
  (0, import_react.useEffect)(() => {
@@ -2299,6 +2323,29 @@ function ReviewModelSelector({ value, catalog, fallbackValue, fallbackLabel, onC
2299
2323
  document.removeEventListener("keydown", onKey);
2300
2324
  };
2301
2325
  }, [open]);
2326
+ const getModelId = (m) => typeof m === "string" ? m : m.id;
2327
+ const getModelName = (m) => typeof m === "string" ? m : m.name ?? m.id;
2328
+ const selectedModelInfo = (() => {
2329
+ if (!selectedProvider || !value?.model) return null;
2330
+ const raw = (providerGroup?.models ?? []).find((mm) => getModelId(mm) === value.model);
2331
+ if (raw === void 0) return null;
2332
+ if (typeof raw === "string") return { id: raw, supportsReasoning: false, reasoningEfforts: [] };
2333
+ return raw;
2334
+ })();
2335
+ const supportsReasoning = (() => {
2336
+ if (!selectedModelInfo) return false;
2337
+ if (typeof selectedModelInfo.supportsReasoning === "boolean") return selectedModelInfo.supportsReasoning;
2338
+ const efforts = selectedModelInfo.reasoningEfforts ?? selectedModelInfo.reasoning?.efforts?.map((e) => e.id) ?? [];
2339
+ return efforts.filter((e) => e !== "off").length > 0;
2340
+ })();
2341
+ const availableEfforts = (() => {
2342
+ if (!supportsReasoning) return [];
2343
+ const efforts = selectedModelInfo?.reasoningEfforts ?? selectedModelInfo?.reasoning?.efforts?.map((e) => e.id) ?? [];
2344
+ const filtered = efforts.filter((e) => e !== "off" && e !== "");
2345
+ if (filtered.length > 0) return filtered;
2346
+ return ["low", "medium", "high"];
2347
+ })();
2348
+ const warning = selectedEffort !== "" && !supportsReasoning && selectedModelInfo !== null ? `\u26A0\uFE0F This model does not support reasoning effort "${selectedEffort}" \u2014 reviews will fail. Clear effort or choose a reasoning-capable model.` : null;
2302
2349
  const update = (field, newVal) => {
2303
2350
  if (newVal === "" && field === "provider") {
2304
2351
  onChange(null);
@@ -2309,8 +2356,9 @@ function ReviewModelSelector({ value, catalog, fallbackValue, fallbackLabel, onC
2309
2356
  const next = { provider: value?.provider ?? "", model: value?.model ?? "", ...value?.reasoningEffort ? { reasoningEffort: value.reasoningEffort } : {} };
2310
2357
  if (field === "provider") {
2311
2358
  const g = groups.find((x) => x.provider === newVal);
2359
+ const first = g?.models[0];
2312
2360
  next.provider = newVal;
2313
- next.model = g?.models[0] ?? "";
2361
+ next.model = first !== void 0 ? getModelId(first) : "";
2314
2362
  } else if (field === "model") {
2315
2363
  next.model = newVal;
2316
2364
  } else if (field === "reasoningEffort") {
@@ -2418,7 +2466,8 @@ function ReviewModelSelector({ value, catalog, fallbackValue, fallbackLabel, onC
2418
2466
  (0, import_react.createElement)("span", { style: { display: "flex", alignItems: "center", gap: 8, color: "var(--dsw-alias-label-secondary)" } }, effortLabel, chevronRight)
2419
2467
  ),
2420
2468
  value && (0, import_react.createElement)("p", { style: { ...captionStyle, margin: "8px 4px 2px" } }, `Selected: ${value.provider} / ${value.model}${value.reasoningEffort ? ` (${value.reasoningEffort})` : ""}`),
2421
- !value && effectiveFallback && (0, import_react.createElement)("p", { style: { ...captionStyle, margin: "8px 4px 2px" } }, `${effectiveFallbackLabel === "Use Global" ? "Using Global" : "Using DSH default"}: ${effectiveFallback.provider} / ${effectiveFallback.model}${effectiveFallback.reasoningEffort ? ` (${effectiveFallback.reasoningEffort})` : ""}`)
2469
+ !value && effectiveFallback && (0, import_react.createElement)("p", { style: { ...captionStyle, margin: "8px 4px 2px" } }, `${effectiveFallbackLabel === "Use Global" ? "Using Global" : "Using DSH default"}: ${effectiveFallback.provider} / ${effectiveFallback.model}${effectiveFallback.reasoningEffort ? ` (${effectiveFallback.reasoningEffort})` : ""}`),
2470
+ warning && (0, import_react.createElement)("p", { style: { ...captionStyle, margin: "4px 4px 2px", color: "var(--dsw-alias-state-error-primary)" } }, warning)
2422
2471
  ),
2423
2472
  pane === "model" && (0, import_react.createElement)(
2424
2473
  "div",
@@ -2437,15 +2486,20 @@ function ReviewModelSelector({ value, catalog, fallbackValue, fallbackLabel, onC
2437
2486
  ms.length === 0 ? (0, import_react.createElement)("p", { style: { ...captionStyle, padding: "2px 10px 2px 28px" } }, "No models") : (0, import_react.createElement)(
2438
2487
  "div",
2439
2488
  { style: { marginLeft: 12, borderLeft: "1px solid var(--dsw-alias-border-l2)", paddingLeft: 6, display: "flex", flexDirection: "column", gap: 2 } },
2440
- ms.map((m) => (0, import_react.createElement)("button", { key: m, type: "button", style: { ...rowStyle, paddingLeft: 10, background: value?.provider === p && value?.model === m ? "var(--dsw-alias-bg-layer-2)" : "transparent" }, onClick: () => {
2441
- update("model", m);
2442
- if (value?.provider !== p) update("provider", p);
2443
- else {
2444
- const next = { provider: p, model: m, ...selectedEffort ? { reasoningEffort: selectedEffort } : {} };
2445
- onChange(next);
2446
- setPane("root");
2447
- }
2448
- } }, (0, import_react.createElement)("span", { style: { overflow: "hidden", textOverflow: "ellipsis" } }, m), check(value?.provider === p && value?.model === m)))
2489
+ ms.map((m) => {
2490
+ const mid = getModelId(m);
2491
+ const mname = getModelName(m);
2492
+ const active = value?.provider === p && value?.model === mid;
2493
+ return (0, import_react.createElement)("button", { key: mid, type: "button", style: { ...rowStyle, paddingLeft: 10, background: active ? "var(--dsw-alias-bg-layer-2)" : "transparent" }, onClick: () => {
2494
+ update("model", mid);
2495
+ if (value?.provider !== p) update("provider", p);
2496
+ else {
2497
+ const next = { provider: p, model: mid, ...selectedEffort ? { reasoningEffort: selectedEffort } : {} };
2498
+ onChange(next);
2499
+ setPane("root");
2500
+ }
2501
+ } }, (0, import_react.createElement)("span", { style: { overflow: "hidden", textOverflow: "ellipsis" } }, mname), check(active));
2502
+ })
2449
2503
  )
2450
2504
  );
2451
2505
  })
@@ -2455,18 +2509,31 @@ function ReviewModelSelector({ value, catalog, fallbackValue, fallbackLabel, onC
2455
2509
  "div",
2456
2510
  null,
2457
2511
  (0, import_react.createElement)("button", { type: "button", style: { ...rowStyle, color: "var(--dsw-alias-label-secondary)" }, onClick: () => setPane("root") }, (0, import_react.createElement)("span", null, "\u2190 Back"), (0, import_react.createElement)("span", { style: { fontSize: 12 } }, "Effort")),
2458
- (0, import_react.createElement)(
2512
+ selectedModelInfo === null ? (0, import_react.createElement)("p", { style: { ...captionStyle, margin: "8px 4px 2px" } }, "Select a model first to configure effort.") : !supportsReasoning ? (0, import_react.createElement)(
2513
+ "div",
2514
+ null,
2515
+ (0, import_react.createElement)("p", { style: { ...captionStyle, margin: "8px 4px 6px" } }, "This model does not support reasoning effort \u2014 using provider default"),
2516
+ (0, import_react.createElement)(
2517
+ "div",
2518
+ { style: { marginTop: 4 } },
2519
+ [{ id: "", label: "Default effort" }].map((e) => (0, import_react.createElement)("button", { key: e.id || "default", type: "button", style: { ...rowStyle, background: selectedEffort === e.id ? "var(--dsw-alias-bg-layer-2)" : "transparent" }, onClick: () => {
2520
+ update("reasoningEffort", e.id);
2521
+ setPane("root");
2522
+ } }, (0, import_react.createElement)("span", null, e.label), check(selectedEffort === e.id)))
2523
+ ),
2524
+ warning && (0, import_react.createElement)("p", { style: { ...captionStyle, margin: "8px 4px 2px", color: "var(--dsw-alias-state-error-primary)" } }, warning)
2525
+ ) : (0, import_react.createElement)(
2459
2526
  "div",
2460
- { style: { marginTop: 4 } },
2461
- [
2462
- { id: "", label: "Default effort" },
2463
- { id: "low", label: "low" },
2464
- { id: "medium", label: "medium" },
2465
- { id: "high", label: "high" }
2466
- ].map((e) => (0, import_react.createElement)("button", { key: e.id || "default", type: "button", style: { ...rowStyle, background: selectedEffort === e.id ? "var(--dsw-alias-bg-layer-2)" : "transparent" }, onClick: () => {
2467
- update("reasoningEffort", e.id);
2468
- setPane("root");
2469
- } }, (0, import_react.createElement)("span", null, e.label), check(selectedEffort === e.id)))
2527
+ null,
2528
+ (0, import_react.createElement)(
2529
+ "div",
2530
+ { style: { marginTop: 4 } },
2531
+ [{ id: "", label: "Default effort" }, ...availableEfforts.map((id) => ({ id, label: id }))].map((e) => (0, import_react.createElement)("button", { key: e.id || "default", type: "button", style: { ...rowStyle, background: selectedEffort === e.id ? "var(--dsw-alias-bg-layer-2)" : "transparent" }, onClick: () => {
2532
+ update("reasoningEffort", e.id);
2533
+ setPane("root");
2534
+ } }, (0, import_react.createElement)("span", null, e.label), check(selectedEffort === e.id)))
2535
+ ),
2536
+ warning && (0, import_react.createElement)("p", { style: { ...captionStyle, margin: "8px 4px 2px", color: "var(--dsw-alias-state-error-primary)" } }, warning)
2470
2537
  )
2471
2538
  )
2472
2539
  )
@@ -2553,31 +2620,6 @@ function ToggleField({ label, caption, checked, onChange }) {
2553
2620
  )
2554
2621
  );
2555
2622
  }
2556
- function ReviewHistoryPanel({ rpcCall }) {
2557
- const [entries, setEntries] = (0, import_react.useState)(null);
2558
- (0, import_react.useEffect)(() => {
2559
- rpcCall(MAESTRO_ENDPOINTS.reviewsList, {}).then((res) => {
2560
- if (res?.ok) setEntries(res.value ?? []);
2561
- }).catch(() => setEntries([]));
2562
- }, []);
2563
- if (entries === null) return (0, import_react.createElement)("p", { style: captionStyle }, "Loading review history\u2026");
2564
- if (entries.length === 0) return (0, import_react.createElement)("p", { style: captionStyle }, "No reviews recorded yet.");
2565
- const icon = (entry) => entry.status === "completed" ? "\u2705" : entry.status === "failed" ? "\u26A0\uFE0F" : "\u{1F440}";
2566
- return (0, import_react.createElement)(
2567
- "ul",
2568
- { style: { listStyle: "none", margin: 0, padding: 0 } },
2569
- entries.map((entry) => (0, import_react.createElement)(
2570
- "li",
2571
- { key: entry.id, style: { padding: "6px 0", borderBottom: "1px solid var(--dsw-alias-separator-default, #333)", fontSize: 13 } },
2572
- (0, import_react.createElement)("span", null, `${icon(entry)} ${entry.projectPath} !${entry.mrIid} \xB7 ${entry.mode}${entry.trigger !== "mention" ? ` \xB7 ${entry.trigger}` : ""}`),
2573
- (0, import_react.createElement)(
2574
- "div",
2575
- { style: captionStyle },
2576
- `${new Date(entry.startedAt).toLocaleString()}${entry.summary ? ` \u2014 ${entry.summary}` : ""}${entry.error ? ` \u2014 ${entry.error}` : ""}`
2577
- )
2578
- ))
2579
- );
2580
- }
2581
2623
  function LanAccess({ proxyStatus, lanPin }) {
2582
2624
  const urls = proxyStatus?.lanUrls ?? [];
2583
2625
  const [selected, setSelected] = (0, import_react.useState)(0);
@@ -2674,7 +2716,7 @@ function PublicAccess({ status, pin, showPin, onRevealPin, onHidePin, onRotatePi
2674
2716
  (0, import_react.createElement)("p", { style: captionStyle }, "Stays the same across tunnel and DSH restarts; use Rotate when you need a new PIN.")
2675
2717
  );
2676
2718
  }
2677
- function MaestroSettingsTab({ rpcCall }) {
2719
+ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
2678
2720
  const [status, setStatus] = (0, import_react.useState)(null);
2679
2721
  const [proxyStatus, setProxyStatus] = (0, import_react.useState)(null);
2680
2722
  const [config, setConfig] = (0, import_react.useState)({ tunnelMode: "quick", projectMappings: [] });
@@ -2686,11 +2728,95 @@ function MaestroSettingsTab({ rpcCall }) {
2686
2728
  const [lanPinEnabled, setLanPinEnabled] = (0, import_react.useState)(false);
2687
2729
  const [lanPin, setLanPin] = (0, import_react.useState)(null);
2688
2730
  const [showLanPin, setShowLanPin] = (0, import_react.useState)(false);
2731
+ const [activeTab, setActiveTab] = (0, import_react.useState)("guard");
2732
+ const [guard, setGuard] = (0, import_react.useState)({});
2733
+ const [patternsText, setPatternsText] = (0, import_react.useState)("");
2734
+ const [placeholdersText, setPlaceholdersText] = (0, import_react.useState)("");
2735
+ const [supervisorCfg, setSupervisorCfg] = (0, import_react.useState)({});
2736
+ const [notifierCfg, setNotifierCfg] = (0, import_react.useState)({});
2689
2737
  const call = async (endpoint, payload) => {
2690
2738
  const res = await rpcCall(endpoint, payload);
2691
2739
  if (!res?.ok) throw new Error(res?.error?.message ?? "RPC failed");
2692
2740
  return res.value;
2693
2741
  };
2742
+ const unwrap = (res) => {
2743
+ if (res && typeof res === "object" && "ok" in res) {
2744
+ if (res.ok) return res.value;
2745
+ throw new Error(res.error?.message ?? "RPC failed");
2746
+ }
2747
+ return res;
2748
+ };
2749
+ const cfgGet = async (domain) => {
2750
+ if (!configRpcCall) throw new Error("config RPC not available");
2751
+ const res = await configRpcCall("get", { domain });
2752
+ return unwrap(res);
2753
+ };
2754
+ const cfgSet = async (domain, patch) => {
2755
+ if (!configRpcCall) throw new Error("config RPC not available");
2756
+ const res = await configRpcCall("set", { domain, patch });
2757
+ return unwrap(res);
2758
+ };
2759
+ const saveGuard = async (patch) => {
2760
+ setError(null);
2761
+ const next = { ...guard, ...patch };
2762
+ if (patch.gitProtection && guard.gitProtection) next.gitProtection = { ...guard.gitProtection, ...patch.gitProtection };
2763
+ setGuard(next);
2764
+ try {
2765
+ await cfgSet("guard", patch);
2766
+ } catch (e) {
2767
+ setError(e.message ?? String(e));
2768
+ }
2769
+ };
2770
+ const commitBlacklistPatterns = async (text) => {
2771
+ const patterns = text.split("\n").map((s) => s.trim()).filter(Boolean);
2772
+ setError(null);
2773
+ try {
2774
+ await cfgSet("guardBlacklist", { patterns });
2775
+ } catch (e) {
2776
+ setError(e.message ?? String(e));
2777
+ }
2778
+ };
2779
+ const commitPlaceholders = async () => {
2780
+ setError(null);
2781
+ let obj = {};
2782
+ try {
2783
+ obj = placeholdersText.trim() ? JSON.parse(placeholdersText) : {};
2784
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) throw new Error("placeholders must be JSON object");
2785
+ } catch (e) {
2786
+ setError(`placeholders JSON invalid: ${e.message ?? String(e)}`);
2787
+ return;
2788
+ }
2789
+ try {
2790
+ await cfgSet("guardBlacklist", { placeholders: obj });
2791
+ } catch (e) {
2792
+ setError(e.message ?? String(e));
2793
+ }
2794
+ };
2795
+ const saveSupervisorCfg = async (patch) => {
2796
+ setError(null);
2797
+ setSupervisorCfg((prev) => ({ ...prev, ...patch }));
2798
+ try {
2799
+ await cfgSet("supervisor", patch);
2800
+ } catch (e) {
2801
+ setError(e.message ?? String(e));
2802
+ }
2803
+ };
2804
+ const saveNotifierCfg = async (patch) => {
2805
+ setError(null);
2806
+ setNotifierCfg((prev) => {
2807
+ const next = { ...prev };
2808
+ for (const [k, v] of Object.entries(patch)) {
2809
+ if (k === "telegram" && typeof v === "object" && v !== null) next.telegram = { ...prev.telegram ?? {}, ...v };
2810
+ else next[k] = v;
2811
+ }
2812
+ return next;
2813
+ });
2814
+ try {
2815
+ await cfgSet("notifier", patch);
2816
+ } catch (e) {
2817
+ setError(e.message ?? String(e));
2818
+ }
2819
+ };
2694
2820
  const refresh = async () => {
2695
2821
  try {
2696
2822
  setStatus(await call(MAESTRO_ENDPOINTS.status, {}));
@@ -2704,6 +2830,29 @@ function MaestroSettingsTab({ rpcCall }) {
2704
2830
  (0, import_react.useEffect)(() => {
2705
2831
  call(MAESTRO_ENDPOINTS.getConfig, {}).then((saved) => setConfig((prev) => ({ ...prev, ...saved }))).catch(() => {
2706
2832
  });
2833
+ if (configRpcCall) {
2834
+ configRpcCall("get", { domain: "supervisor" }).then((res) => {
2835
+ if (res?.ok && res.value?.model) {
2836
+ setConfig((prev) => ({ ...prev, supervisorModel: res.value.model }));
2837
+ }
2838
+ }).catch(() => {
2839
+ });
2840
+ Promise.all([
2841
+ cfgGet("guard").catch(() => ({})),
2842
+ cfgGet("guardBlacklist").catch(() => ({ patterns: [], placeholders: {} })),
2843
+ cfgGet("supervisor").catch(() => ({})),
2844
+ cfgGet("notifier").catch(() => ({}))
2845
+ ]).then(([g, bl, sup, not]) => {
2846
+ setGuard(g ?? {});
2847
+ const pats = Array.isArray(bl?.patterns) ? bl.patterns : [];
2848
+ const ph = bl?.placeholders && typeof bl.placeholders === "object" ? bl.placeholders : {};
2849
+ setPatternsText(pats.join("\n"));
2850
+ setPlaceholdersText(JSON.stringify(ph, null, 2));
2851
+ setSupervisorCfg(sup ?? {});
2852
+ setNotifierCfg(not ?? {});
2853
+ }).catch(() => {
2854
+ });
2855
+ }
2707
2856
  call(MAESTRO_ENDPOINTS.lanPinStatus, {}).then((value) => {
2708
2857
  setLanPinEnabled(value.enabled);
2709
2858
  if (value.enabled) setLanPin(value.pin ?? null);
@@ -2801,6 +2950,13 @@ function MaestroSettingsTab({ rpcCall }) {
2801
2950
  const saveField = async (field, value) => {
2802
2951
  setError(null);
2803
2952
  setConfig((prev) => ({ ...prev, [field]: value }));
2953
+ if (field === "supervisorModel" && configRpcCall) {
2954
+ try {
2955
+ const res = await configRpcCall("set", { domain: "supervisor", patch: { model: value } });
2956
+ if (res?.ok) return;
2957
+ } catch (e) {
2958
+ }
2959
+ }
2804
2960
  try {
2805
2961
  await call(MAESTRO_ENDPOINTS.saveConfig, { [field]: value });
2806
2962
  } catch (err) {
@@ -2913,8 +3069,7 @@ function MaestroSettingsTab({ rpcCall }) {
2913
3069
  caption: "After a completed review, further pushes to the same MR trigger an automatic quick re-review.",
2914
3070
  checked: config.autoRereviewOnPush,
2915
3071
  onChange: (checked) => saveField("autoRereviewOnPush", checked)
2916
- }),
2917
- (0, import_react.createElement)(ReviewHistoryPanel, { rpcCall })
3072
+ })
2918
3073
  ),
2919
3074
  (0, import_react.createElement)(
2920
3075
  "div",
@@ -2930,12 +3085,169 @@ function MaestroSettingsTab({ rpcCall }) {
2930
3085
  label: "Global review model"
2931
3086
  })
2932
3087
  ),
3088
+ (0, import_react.createElement)(
3089
+ "div",
3090
+ { style: sectionStyle },
3091
+ (0, import_react.createElement)("h4", { style: headingStyle }, "Supervisor LLM"),
3092
+ (0, import_react.createElement)("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."),
3093
+ (0, import_react.createElement)(ReviewModelSelector, {
3094
+ value: config.supervisorModel ?? null,
3095
+ catalog,
3096
+ fallbackValue: catalog?.current ?? null,
3097
+ fallbackLabel: "Use DSH default",
3098
+ onChange: (v) => saveField("supervisorModel", v),
3099
+ label: "Supervisor model"
3100
+ })
3101
+ ),
2933
3102
  (0, import_react.createElement)(
2934
3103
  "div",
2935
3104
  { style: sectionStyle },
2936
3105
  (0, import_react.createElement)("h4", { style: headingStyle }, "Projects"),
2937
3106
  (0, import_react.createElement)(ProjectMappingsEditor, { mappings: config.projectMappings ?? [], onChange: (mappings) => saveField("projectMappings", mappings), catalog, globalReviewModel: config.reviewModel ?? null })
2938
3107
  ),
3108
+ // Task 3: Guard/Blacklist/Supervisor/Notifier tabs — data-driven over guard domains
3109
+ (0, import_react.createElement)(
3110
+ "div",
3111
+ { style: sectionStyle },
3112
+ (0, import_react.createElement)("h4", { style: headingStyle }, "Guard / Blacklist / Supervisor / Notifier"),
3113
+ (0, import_react.createElement)(
3114
+ "div",
3115
+ { style: tabBarStyle },
3116
+ (0, import_react.createElement)("button", { type: "button", style: tabButtonStyle(activeTab === "guard"), onClick: () => setActiveTab("guard") }, "Guard"),
3117
+ (0, import_react.createElement)("button", { type: "button", style: tabButtonStyle(activeTab === "blacklist"), onClick: () => setActiveTab("blacklist") }, "Blacklist"),
3118
+ (0, import_react.createElement)("button", { type: "button", style: tabButtonStyle(activeTab === "supervisor"), onClick: () => setActiveTab("supervisor") }, "Supervisor"),
3119
+ (0, import_react.createElement)("button", { type: "button", style: tabButtonStyle(activeTab === "notifier"), onClick: () => setActiveTab("notifier") }, "Notifier")
3120
+ ),
3121
+ activeTab === "guard" && (0, import_react.createElement)(
3122
+ "div",
3123
+ { "data-tab": "guard" },
3124
+ (0, import_react.createElement)("p", { style: captionStyle }, "Enforce publish block, git protection and cwd containment."),
3125
+ (0, import_react.createElement)(ToggleField, {
3126
+ label: "publishBlocked",
3127
+ caption: "Block publish-related commands when enabled.",
3128
+ checked: guard.publishBlocked === true,
3129
+ onChange: (v) => saveGuard({ publishBlocked: v })
3130
+ }),
3131
+ (0, import_react.createElement)(ToggleField, {
3132
+ label: "gitProtection.enabled",
3133
+ caption: "Protect pushes to protected branches.",
3134
+ checked: guard.gitProtection?.enabled === true,
3135
+ onChange: (v) => saveGuard({ gitProtection: { enabled: v, branches: guard.gitProtection?.branches ?? ["master", "main"] } })
3136
+ }),
3137
+ (0, import_react.createElement)("label", { style: fieldLabelStyle }, "gitProtection.branches (comma separated)"),
3138
+ (0, import_react.createElement)("input", {
3139
+ style: inputStyle,
3140
+ value: (guard.gitProtection?.branches ?? ["master", "main"]).join(", "),
3141
+ placeholder: "master, main",
3142
+ onChange: (e) => {
3143
+ const branches = e.target.value.split(",").map((s) => s.trim()).filter(Boolean);
3144
+ saveGuard({ gitProtection: { enabled: guard.gitProtection?.enabled ?? true, branches } });
3145
+ }
3146
+ }),
3147
+ (0, import_react.createElement)(ToggleField, {
3148
+ label: "cwdContainment",
3149
+ caption: "Contain file operations to the session cwd.",
3150
+ checked: guard.cwdContainment === true,
3151
+ onChange: (v) => saveGuard({ cwdContainment: v })
3152
+ }),
3153
+ (0, import_react.createElement)("label", { style: fieldLabelStyle }, "credentialPaths (comma separated)"),
3154
+ (0, import_react.createElement)("input", {
3155
+ style: inputStyle,
3156
+ value: (guard.credentialPaths ?? []).join(", "),
3157
+ placeholder: "~/.config/credentials.yaml, ~/.config/cloudflared",
3158
+ onChange: (e) => {
3159
+ const credentialPaths = e.target.value.split(",").map((s) => s.trim()).filter(Boolean);
3160
+ saveGuard({ credentialPaths });
3161
+ }
3162
+ })
3163
+ ),
3164
+ activeTab === "blacklist" && (0, import_react.createElement)(
3165
+ "div",
3166
+ { "data-tab": "blacklist" },
3167
+ (0, import_react.createElement)("p", { style: captionStyle }, "One pattern per line. These are blocked from being committed or published."),
3168
+ (0, import_react.createElement)("label", { style: fieldLabelStyle }, "patterns (one per line)"),
3169
+ (0, import_react.createElement)("textarea", {
3170
+ style: textareaStyle,
3171
+ value: patternsText,
3172
+ placeholder: "example-project\nacme-shop",
3173
+ onChange: (e) => setPatternsText(e.target.value),
3174
+ onBlur: (e) => commitBlacklistPatterns(e.target.value)
3175
+ }),
3176
+ (0, import_react.createElement)("label", { style: fieldLabelStyle }, "placeholders JSON"),
3177
+ (0, import_react.createElement)("textarea", {
3178
+ style: { ...textareaStyle, height: 90 },
3179
+ value: placeholdersText,
3180
+ placeholder: '{"example-project":"my-project"}',
3181
+ onChange: (e) => setPlaceholdersText(e.target.value),
3182
+ onBlur: () => commitPlaceholders()
3183
+ }),
3184
+ (0, import_react.createElement)("p", { style: captionStyle }, "Map blocked patterns to their placeholder suggestions."),
3185
+ (0, import_react.createElement)("button", { type: "button", style: { ...secondaryButtonStyle, marginTop: 8 }, onClick: () => {
3186
+ commitBlacklistPatterns(patternsText);
3187
+ commitPlaceholders();
3188
+ } }, "Save Blacklist")
3189
+ ),
3190
+ activeTab === "supervisor" && (0, import_react.createElement)(
3191
+ "div",
3192
+ { "data-tab": "supervisor" },
3193
+ (0, import_react.createElement)("p", { style: captionStyle }, "Background daemon that auto-resumes crashed sessions."),
3194
+ (0, import_react.createElement)("label", { style: fieldLabelStyle }, "intervalMs"),
3195
+ (0, import_react.createElement)("input", {
3196
+ type: "number",
3197
+ style: inputStyle,
3198
+ value: supervisorCfg.intervalMs ?? "",
3199
+ placeholder: "5000",
3200
+ onChange: (e) => {
3201
+ const v = e.target.value === "" ? void 0 : Number(e.target.value);
3202
+ saveSupervisorCfg({ intervalMs: v });
3203
+ }
3204
+ }),
3205
+ (0, import_react.createElement)("label", { style: fieldLabelStyle }, "downThreshold"),
3206
+ (0, import_react.createElement)("input", {
3207
+ type: "number",
3208
+ style: inputStyle,
3209
+ value: supervisorCfg.downThreshold ?? "",
3210
+ placeholder: "3",
3211
+ onChange: (e) => {
3212
+ const v = e.target.value === "" ? void 0 : Number(e.target.value);
3213
+ saveSupervisorCfg({ downThreshold: v });
3214
+ }
3215
+ }),
3216
+ (0, import_react.createElement)(ToggleField, {
3217
+ label: "autoResumeEnabled",
3218
+ caption: "Automatically resume down sessions.",
3219
+ checked: supervisorCfg.autoResumeEnabled === true,
3220
+ onChange: (v) => saveSupervisorCfg({ autoResumeEnabled: v })
3221
+ })
3222
+ ),
3223
+ activeTab === "notifier" && (0, import_react.createElement)(
3224
+ "div",
3225
+ { "data-tab": "notifier" },
3226
+ (0, import_react.createElement)("p", { style: captionStyle }, "Telegram notifications for Maestro events."),
3227
+ (0, import_react.createElement)("label", { style: fieldLabelStyle }, "telegram.botToken"),
3228
+ (0, import_react.createElement)("input", {
3229
+ type: "password",
3230
+ autoComplete: "off",
3231
+ style: inputStyle,
3232
+ value: notifierCfg.telegram?.botToken ?? "",
3233
+ placeholder: "123456:ABC-DEF...",
3234
+ onChange: (e) => saveNotifierCfg({ telegram: { botToken: e.target.value } })
3235
+ }),
3236
+ (0, import_react.createElement)("label", { style: fieldLabelStyle }, "telegram.chatId"),
3237
+ (0, import_react.createElement)("input", {
3238
+ style: inputStyle,
3239
+ value: notifierCfg.telegram?.chatId ?? "",
3240
+ placeholder: "-1001234567890",
3241
+ onChange: (e) => saveNotifierCfg({ telegram: { chatId: e.target.value } })
3242
+ }),
3243
+ (0, import_react.createElement)(ToggleField, {
3244
+ label: "telegram.reviewNotifications",
3245
+ caption: "Also notify about finished reviews.",
3246
+ checked: notifierCfg.telegram?.reviewNotifications === true || notifierCfg.policy?.reviewNotifications === true,
3247
+ onChange: (v) => saveNotifierCfg({ telegram: { reviewNotifications: v } })
3248
+ })
3249
+ )
3250
+ ),
2939
3251
  error && (0, import_react.createElement)("p", { style: errorStyle }, error)
2940
3252
  );
2941
3253
  }
@@ -2979,7 +3291,7 @@ function registerSettingsNavIcon(label, root) {
2979
3291
  // src/client/index.tsx
2980
3292
  var SETTINGS_NAV_CSS2 = `
2981
3293
 
2982
- /* maestro: replace the settings-nav fallback gear with the maestro glyph */
3294
+ /* maestro: replace the settings-nav fallback gear with the Maestro M-logo glyph */
2983
3295
  [${SETTINGS_NAV_MARKER}] > svg:first-child {
2984
3296
  display: none;
2985
3297
  }
@@ -2990,10 +3302,11 @@ var SETTINGS_NAV_CSS2 = `
2990
3302
  width: 16px;
2991
3303
  height: 16px;
2992
3304
  background: currentColor;
2993
- -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 10v3'/%3E%3Cpath d='M6 4v16'/%3E%3Cpath d='M10 8v8'/%3E%3Cpath d='M14 4v16'/%3E%3Cpath d='M18 6v12'/%3E%3Cpath d='M22 10v3'/%3E%3C/svg%3E") center / contain no-repeat;
2994
- mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 10v3'/%3E%3Cpath d='M6 4v16'/%3E%3Cpath d='M10 8v8'/%3E%3Cpath d='M14 4v16'/%3E%3Cpath d='M18 6v12'/%3E%3Cpath d='M22 10v3'/%3E%3C/svg%3E") center / contain no-repeat;
3305
+ -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;
3306
+ 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;
2995
3307
  }
2996
3308
  `;
3309
+ var inject = ["slots", "connection"];
2997
3310
  function installNavIconStyle() {
2998
3311
  const tag = document.createElement("style");
2999
3312
  tag.dataset.plugin = "@ddtcorex/dsh-maestro-config";
@@ -3012,12 +3325,17 @@ function apply(ctx) {
3012
3325
  if (!connection?.rpc?.call) return Promise.reject(new Error("RPC not available"));
3013
3326
  return connection.rpc.call(MAESTRO_RPC_CHANNEL, endpoint, payload, signal);
3014
3327
  };
3328
+ const configRpcCall = (endpoint, payload, signal) => {
3329
+ const connection = ctx.get?.("connection");
3330
+ if (!connection?.rpc?.call) return Promise.reject(new Error("RPC not available"));
3331
+ return connection.rpc.call("/dsh-maestro-config", endpoint, payload, signal);
3332
+ };
3015
3333
  ctx.effect(() => registerSettingsNavIcon(() => "Maestro"), "maestro: settings nav icon");
3016
3334
  ctx.effect(installNavIconStyle, "maestro: settings nav css");
3017
3335
  slots.inject(
3018
3336
  "settings.section",
3019
3337
  () => slots.register(
3020
- { name: "settings.section", id: "maestro", order: 25, label: () => "Maestro", inject: () => ({ rpcCall }) },
3338
+ { name: "settings.section", id: "maestro", order: 25, label: () => "Maestro", inject: () => ({ rpcCall, configRpcCall }) },
3021
3339
  MaestroSettingsTab
3022
3340
  )
3023
3341
  );
package/lib/index.d.ts CHANGED
@@ -1,12 +1,35 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
+ type RpcResult<T> = {
3
+ ok: true;
4
+ value: T;
5
+ } | {
6
+ ok: false;
7
+ error: {
8
+ code: string;
9
+ message: string;
10
+ details: object;
11
+ };
12
+ };
2
13
  import { type MaestroConfigService } from './service.ts';
3
14
  export declare const name = "maestro-config";
4
15
  export declare const inject: string[];
5
16
  declare module '@deepseek-ai/cordis' {
6
17
  interface Context {
7
18
  maestroConfig: MaestroConfigService;
19
+ connection: {
20
+ rpc: {
21
+ handle: (channel: string, handler: (endpoint: string, payload: unknown) => Promise<RpcResult<unknown>>, opts?: unknown) => () => void;
22
+ };
23
+ };
8
24
  }
9
25
  }
10
- /** Publish maestroConfig over the shared store + loopback RPC for clients. */
26
+ /**
27
+ * Publish maestroConfig over the shared store + loopback RPC for clients.
28
+ * Exposes guard/guardBlacklist/supervisor/notifier domains (Task 1 validators)
29
+ * via generic get/set — validation is delegated to the lib's domain validators.
30
+ * Host also handles '/dsh-maestro-config/get' and '/dsh-maestro-config/set'
31
+ * style calls through the single channel with endpoint dispatch.
32
+ */
11
33
  export declare function apply(ctx: Context): void;
34
+ export {};
12
35
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/host/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,EAA8B,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAEpF,eAAO,MAAM,IAAI,mBAAmB,CAAA;AACpC,eAAO,MAAM,MAAM,UAAiB,CAAA;AAIpC,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,aAAa,EAAE,oBAAoB,CAAA;KACpC;CACF;AAmBD,8EAA8E;AAC9E,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAuBxC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/host/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,KAAK,SAAS,CAAC,CAAC,IAAI;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAA;AAErH,OAAO,EAA8B,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAEpF,eAAO,MAAM,IAAI,mBAAmB,CAAA;AACpC,eAAO,MAAM,MAAM,UAAiB,CAAA;AAIpC,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,aAAa,EAAE,oBAAoB,CAAA;QACnC,UAAU,EAAE;YAAE,GAAG,EAAE;gBAAE,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,MAAM,IAAI,CAAA;aAAE,CAAA;SAAE,CAAA;KAC/J;CACF;AAmBD;;;;;;GAMG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAwBxC"}
package/lib/index.js CHANGED
@@ -17,7 +17,13 @@ function fail(message) {
17
17
  },
18
18
  };
19
19
  }
20
- /** Publish maestroConfig over the shared store + loopback RPC for clients. */
20
+ /**
21
+ * Publish maestroConfig over the shared store + loopback RPC for clients.
22
+ * Exposes guard/guardBlacklist/supervisor/notifier domains (Task 1 validators)
23
+ * via generic get/set — validation is delegated to the lib's domain validators.
24
+ * Host also handles '/dsh-maestro-config/get' and '/dsh-maestro-config/set'
25
+ * style calls through the single channel with endpoint dispatch.
26
+ */
21
27
  export function apply(ctx) {
22
28
  const svc = createMaestroConfigService();
23
29
  ctx.provide('maestroConfig', svc);
@@ -29,6 +35,7 @@ export function apply(ctx) {
29
35
  if (endpoint === 'get') {
30
36
  if (typeof body.domain !== 'string')
31
37
  return fail('domain (string) is required');
38
+ // guard / guardBlacklist / supervisor / notifier are all valid domains here
32
39
  return ok(await svc.get(body.domain));
33
40
  }
34
41
  if (endpoint === 'set') {
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/host/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,0BAA0B,EAA6B,MAAM,cAAc,CAAA;AAEpF,MAAM,CAAC,MAAM,IAAI,GAAG,gBAAgB,CAAA;AACpC,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,YAAY,CAAC,CAAA;AAEpC,MAAM,WAAW,GAAG,qBAAqB,CAAA;AAQzC,SAAS,EAAE,CAAI,KAAQ;IACrB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;AAC5B,CAAC;AAED,SAAS,IAAI,CAAC,OAAe;IAC3B,OAAO;QACL,EAAE,EAAE,KAAK;QACT,KAAK,EAAE;YACL,IAAI,EAAE,aAAa;YACnB,OAAO;YACP,sEAAsE;YACtE,qEAAqE;YACrE,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAuC;SACxE;KACF,CAAA;AACH,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,KAAK,CAAC,GAAY;IAChC,MAAM,GAAG,GAAG,0BAA0B,EAAE,CAAA;IACxC,GAAG,CAAC,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC,CAAA;IACjC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,CACd,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,EAAE,QAAgB,EAAE,OAAgB,EAA+B,EAAE;QAC/G,MAAM,IAAI,GAAG,CAAC,OAAO,IAAI,EAAE,CAAwC,CAAA;QACnE,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;YACxB,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;QACjD,CAAC;QACD,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;YACvB,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAC,6BAA6B,CAAC,CAAA;YAC/E,OAAO,EAAE,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;QACvC,CAAC;QACD,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;YACvB,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;gBAC7F,OAAO,IAAI,CAAC,iDAAiD,CAAC,CAAA;YAChE,CAAC;YACD,MAAM,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;YACtC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAA;QACjB,CAAC;QACD,OAAO,IAAI,CAAC,qBAAqB,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;IACtD,CAAC,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAC9B,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/host/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,0BAA0B,EAA6B,MAAM,cAAc,CAAA;AAEpF,MAAM,CAAC,MAAM,IAAI,GAAG,gBAAgB,CAAA;AACpC,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,YAAY,CAAC,CAAA;AAEpC,MAAM,WAAW,GAAG,qBAAqB,CAAA;AASzC,SAAS,EAAE,CAAI,KAAQ;IACrB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;AAC5B,CAAC;AAED,SAAS,IAAI,CAAC,OAAe;IAC3B,OAAO;QACL,EAAE,EAAE,KAAK;QACT,KAAK,EAAE;YACL,IAAI,EAAE,aAAa;YACnB,OAAO;YACP,sEAAsE;YACtE,qEAAqE;YACrE,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAuC;SACxE;KACF,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,KAAK,CAAC,GAAY;IAChC,MAAM,GAAG,GAAG,0BAA0B,EAAE,CAAA;IACxC,GAAG,CAAC,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC,CAAA;IACjC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,CACd,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,EAAE,QAAgB,EAAE,OAAgB,EAA+B,EAAE;QAC/G,MAAM,IAAI,GAAG,CAAC,OAAO,IAAI,EAAE,CAAwC,CAAA;QACnE,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;YACxB,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;QACjD,CAAC;QACD,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;YACvB,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAC,6BAA6B,CAAC,CAAA;YAC/E,4EAA4E;YAC5E,OAAO,EAAE,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;QACvC,CAAC;QACD,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;YACvB,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;gBAC7F,OAAO,IAAI,CAAC,iDAAiD,CAAC,CAAA;YAChE,CAAC;YACD,MAAM,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;YACtC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAA;QACjB,CAAC;QACD,OAAO,IAAI,CAAC,qBAAqB,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;IACtD,CAAC,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAC9B,CAAA;AACH,CAAC"}
@@ -0,0 +1,11 @@
1
+ type RpcCall = (endpoint: string, payload?: unknown, signal?: AbortSignal) => Promise<unknown>;
2
+ export declare function Settings({ configRpcCall }: {
3
+ configRpcCall: RpcCall;
4
+ }): import("react").DetailedReactHTMLElement<{
5
+ 'data-maestro-guard-settings': string;
6
+ style: {
7
+ maxWidth: number;
8
+ };
9
+ }, HTMLElement>;
10
+ export default Settings;
11
+ //# sourceMappingURL=Settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Settings.d.ts","sourceRoot":"","sources":["../../../src/client/Settings.tsx"],"names":[],"mappings":"AAIA,KAAK,OAAO,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;AAiF9F,wBAAgB,QAAQ,CAAC,EAAE,aAAa,EAAE,EAAE;IAAE,aAAa,EAAE,OAAO,CAAA;CAAE;;;;;gBAiRrE;AAGD,eAAe,QAAQ,CAAA"}
@@ -2,6 +2,7 @@ interface ClientCtx {
2
2
  get?(name: string): unknown;
3
3
  effect(fn: () => () => void, label?: string): unknown;
4
4
  }
5
+ export declare const inject: readonly ["slots", "connection"];
5
6
  export declare function apply(ctx: ClientCtx): void;
6
7
  export {};
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.tsx"],"names":[],"mappings":"AAuCA,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;AAaD,wBAAgB,KAAK,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI,CAuB1C"}
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,5 +1,6 @@
1
- export function MaestroSettingsTab({ rpcCall }: {
1
+ export function MaestroSettingsTab({ rpcCall, configRpcCall }: {
2
2
  rpcCall: any;
3
+ configRpcCall: any;
3
4
  }): import("react").DetailedReactHTMLElement<{
4
5
  'data-maestro-settings-card': string;
5
6
  style: {
@@ -1 +1 @@
1
- {"version":3,"file":"maestro-card.d.ts","sourceRoot":"","sources":["../../../src/client/maestro-card.jsx"],"names":[],"mappings":"AA6bA;;;;;;;gBAmPC"}
1
+ {"version":3,"file":"maestro-card.d.ts","sourceRoot":"","sources":["../../../src/client/maestro-card.jsx"],"names":[],"mappings":"AAweA;;;;;;;;gBA0dC"}
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@ddtcorex/dsh-maestro-config",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
+ "private": false,
4
5
  "description": "Maestro Config — shared settings service for the dsh-maestro-* suite over the single namespaced store (~/.dsh/maestro/settings.json)",
5
6
  "type": "module",
6
7
  "main": "./lib/index.js",
@@ -33,16 +34,14 @@
33
34
  },
34
35
  "peerDependencies": {
35
36
  "@deepseek-ai/cordis": "^4.0.1",
36
- "@deepseek-ai/dsh-client-connection": "0.1.0-rc.8",
37
- "@deepseek-ai/dsh-host-apiproxy": "0.1.0-rc.8"
37
+ "@deepseek-ai/dsh-client-connection": "0.1.1-rc.2"
38
38
  },
39
39
  "dependencies": {
40
- "@ddtcorex/dsh-maestro-config-lib": "^0.1.0"
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.0-rc.8",
45
- "@deepseek-ai/dsh-host-apiproxy": "0.1.0-rc.8",
44
+ "@deepseek-ai/dsh-client-connection": "0.1.1-rc.2",
46
45
  "@types/node": "^22.0.0",
47
46
  "@types/qrcode": "^1.5.6",
48
47
  "@types/react": "~18.3.1",
@@ -7,12 +7,13 @@ import { registerSettingsNavIcon, SETTINGS_NAV_MARKER } from './settings-nav-ico
7
7
  * DSH 0.1.x gives external settings sections a generic gear and exposes no
8
8
  * icon field in the settings.section contract (mirrors dsh-better-sidebar):
9
9
  * the marker only claims this plugin's localized row and this CSS paints the
10
- * Lucide "audio-lines" glyph as a currentColor mask so it follows native nav
11
- * hover/active colors at the shell's 16px icon rhythm.
10
+ * Maestro M-logo glyph as a currentColor mask so it follows native nav
11
+ * hover/active colors at the shell's 16px icon rhythm. The path matches the
12
+ * sidebar MaestroTrigger (trigger.tsx#MaestroLogo) — M2 11 L5 4 L8 9 L11 4 L14 11.
12
13
  */
13
14
  const SETTINGS_NAV_CSS = `
14
15
 
15
- /* maestro: replace the settings-nav fallback gear with the maestro glyph */
16
+ /* maestro: replace the settings-nav fallback gear with the Maestro M-logo glyph */
16
17
  [${SETTINGS_NAV_MARKER}] > svg:first-child {
17
18
  display: none;
18
19
  }
@@ -23,8 +24,8 @@ const SETTINGS_NAV_CSS = `
23
24
  width: 16px;
24
25
  height: 16px;
25
26
  background: currentColor;
26
- -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 10v3'/%3E%3Cpath d='M6 4v16'/%3E%3Cpath d='M10 8v8'/%3E%3Cpath d='M14 4v16'/%3E%3Cpath d='M18 6v12'/%3E%3Cpath d='M22 10v3'/%3E%3C/svg%3E") center / contain no-repeat;
27
- mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 10v3'/%3E%3Cpath d='M6 4v16'/%3E%3Cpath d='M10 8v8'/%3E%3Cpath d='M14 4v16'/%3E%3Cpath d='M18 6v12'/%3E%3Cpath d='M22 10v3'/%3E%3C/svg%3E") center / contain no-repeat;
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;
28
29
  }
29
30
  `
30
31
 
@@ -42,6 +43,8 @@ interface ClientCtx {
42
43
  effect(fn: () => () => void, label?: string): unknown
43
44
  }
44
45
 
46
+ export const inject = ['slots', 'connection'] as const
47
+
45
48
  function installNavIconStyle(): () => void {
46
49
  const tag = document.createElement('style')
47
50
  tag.dataset.plugin = '@ddtcorex/dsh-maestro-config'
@@ -65,6 +68,14 @@ export function apply(ctx: ClientCtx): void {
65
68
  if (!connection?.rpc?.call) return Promise.reject(new Error('RPC not available'))
66
69
  return connection.rpc.call(MAESTRO_RPC_CHANNEL, endpoint, payload, signal)
67
70
  }
71
+ // Generic config RPC for supervisor (independent of review — works when review not installed)
72
+ const configRpcCall: RpcCall = (endpoint, payload, signal) => {
73
+ const connection = ctx.get?.('connection') as
74
+ | { rpc: { call(ch: string, ep: string, p?: unknown, s?: AbortSignal): Promise<unknown> } }
75
+ | undefined
76
+ if (!connection?.rpc?.call) return Promise.reject(new Error('RPC not available'))
77
+ return connection.rpc.call('/dsh-maestro-config', endpoint, payload, signal)
78
+ }
68
79
 
69
80
  // Reversible effects: nav-row marker observer + owned style tag.
70
81
  ctx.effect(() => registerSettingsNavIcon(() => 'Maestro'), 'maestro: settings nav icon')
@@ -72,8 +83,8 @@ export function apply(ctx: ClientCtx): void {
72
83
 
73
84
  slots.inject('settings.section', () =>
74
85
  slots.register(
75
- { name: 'settings.section', id: 'maestro', order: 25, label: () => 'Maestro', inject: () => ({ rpcCall }) },
76
- MaestroSettingsTab,
86
+ { name: 'settings.section', id: 'maestro', order: 25, label: () => 'Maestro', inject: () => ({ rpcCall, configRpcCall }) },
87
+ MaestroSettingsTab as unknown as (props: { rpcCall: RpcCall }) => unknown,
77
88
  ),
78
89
  )
79
90
  }
@@ -86,6 +86,32 @@ const codeStyle = {
86
86
  wordBreak: 'break-all',
87
87
  }
88
88
 
89
+ const textareaStyle = {
90
+ ...inputStyle,
91
+ height: 120,
92
+ padding: '8px 12px',
93
+ resize: 'vertical',
94
+ }
95
+
96
+ const tabBarStyle = {
97
+ display: 'flex',
98
+ gap: 8,
99
+ borderBottom: '1px solid var(--dsw-alias-border-l2)',
100
+ marginBottom: 16,
101
+ }
102
+
103
+ const tabButtonStyle = (active) => ({
104
+ padding: '8px 14px',
105
+ border: 'none',
106
+ borderBottom: active ? '2px solid var(--dsw-alias-button-primary-fill)' : '2px solid transparent',
107
+ background: 'transparent',
108
+ color: active ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-secondary)',
109
+ font: 'inherit',
110
+ fontSize: 13,
111
+ fontWeight: active ? 600 : 400,
112
+ cursor: 'pointer',
113
+ })
114
+
89
115
  /** QR code centered in a light tile with an even scanner-friendly quiet zone. */
90
116
  function QrImage({ url, size = 104 }) {
91
117
  const [dataUrl, setDataUrl] = useState(null)
@@ -147,10 +173,35 @@ function ReviewModelSelector({ value, catalog, fallbackValue, fallbackLabel, onC
147
173
  document.addEventListener('keydown', onKey)
148
174
  return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey) }
149
175
  }, [open])
176
+ const getModelId = (m) => typeof m === 'string' ? m : m.id
177
+ const getModelName = (m) => typeof m === 'string' ? m : (m.name ?? m.id)
178
+ const selectedModelInfo = (() => {
179
+ if (!selectedProvider || !value?.model) return null
180
+ const raw = (providerGroup?.models ?? []).find(mm => getModelId(mm) === value.model)
181
+ if (raw === undefined) return null
182
+ if (typeof raw === 'string') return { id: raw, supportsReasoning: false, reasoningEfforts: [] }
183
+ return raw
184
+ })()
185
+ const supportsReasoning = (() => {
186
+ if (!selectedModelInfo) return false
187
+ if (typeof selectedModelInfo.supportsReasoning === 'boolean') return selectedModelInfo.supportsReasoning
188
+ const efforts = selectedModelInfo.reasoningEfforts ?? selectedModelInfo.reasoning?.efforts?.map(e => e.id) ?? []
189
+ return efforts.filter(e => e !== 'off').length > 0
190
+ })()
191
+ const availableEfforts = (() => {
192
+ if (!supportsReasoning) return []
193
+ const efforts = selectedModelInfo?.reasoningEfforts ?? selectedModelInfo?.reasoning?.efforts?.map(e => e.id) ?? []
194
+ const filtered = efforts.filter(e => e !== 'off' && e !== '')
195
+ if (filtered.length > 0) return filtered
196
+ return ['low', 'medium', 'high']
197
+ })()
198
+ const warning = selectedEffort !== '' && !supportsReasoning && selectedModelInfo !== null
199
+ ? `⚠️ This model does not support reasoning effort "${selectedEffort}" — reviews will fail. Clear effort or choose a reasoning-capable model.`
200
+ : null
150
201
  const update = (field, newVal) => {
151
202
  if (newVal === '' && field === 'provider') { onChange(null); setOpen(false); setPane('root'); return }
152
203
  const next = { provider: value?.provider ?? '', model: value?.model ?? '', ...(value?.reasoningEffort ? { reasoningEffort: value.reasoningEffort } : {}) }
153
- if (field === 'provider') { const g = groups.find(x => x.provider === newVal); next.provider = newVal; next.model = g?.models[0] ?? '' }
204
+ if (field === 'provider') { const g = groups.find(x => x.provider === newVal); const first = g?.models[0]; next.provider = newVal; next.model = first !== undefined ? getModelId(first) : '' }
154
205
  else if (field === 'model') { next.model = newVal }
155
206
  else if (field === 'reasoningEffort') { if (newVal === '') delete next.reasoningEffort; else next.reasoningEffort = newVal }
156
207
  if (!next.provider || !next.model) { onChange(null) } else { onChange(next) }
@@ -235,6 +286,7 @@ function ReviewModelSelector({ value, catalog, fallbackValue, fallbackLabel, onC
235
286
  ),
236
287
  value && h('p', { style: { ...captionStyle, margin: '8px 4px 2px' } }, `Selected: ${value.provider} / ${value.model}${value.reasoningEffort ? ` (${value.reasoningEffort})` : ''}`),
237
288
  !value && effectiveFallback && h('p', { style: { ...captionStyle, margin: '8px 4px 2px' } }, `${effectiveFallbackLabel === 'Use Global' ? 'Using Global' : 'Using DSH default'}: ${effectiveFallback.provider} / ${effectiveFallback.model}${effectiveFallback.reasoningEffort ? ` (${effectiveFallback.reasoningEffort})` : ''}`),
289
+ warning && h('p', { style: { ...captionStyle, margin: '4px 4px 2px', color: 'var(--dsw-alias-state-error-primary)' } }, warning),
238
290
  ),
239
291
  pane === 'model' && h('div', null,
240
292
  h('button', { type: 'button', style: { ...rowStyle, color: 'var(--dsw-alias-label-secondary)' }, onClick: () => setPane('root') }, h('span', null, '← Back'), h('span', { style: { fontSize: 12 } }, 'Model')),
@@ -247,20 +299,31 @@ function ReviewModelSelector({ value, catalog, fallbackValue, fallbackLabel, onC
247
299
  h('div', { style: { fontSize: 11, fontWeight: 600, color: 'var(--dsw-alias-label-secondary)', padding: '6px 10px 2px', textTransform: 'uppercase', letterSpacing: 0.4, display: 'flex', alignItems: 'center', gap: 6 } }, h('span', { style: { width: 6, height: 6, borderRadius: 3, background: 'var(--dsw-alias-border-l2)', flex: 'none' } }), g?.name ?? p),
248
300
  ms.length === 0 ? h('p', { style: { ...captionStyle, padding: '2px 10px 2px 28px' } }, 'No models') :
249
301
  h('div', { style: { marginLeft: 12, borderLeft: '1px solid var(--dsw-alias-border-l2)', paddingLeft: 6, display: 'flex', flexDirection: 'column', gap: 2 } },
250
- ms.map(m => h('button', { key: m, type: 'button', style: { ...rowStyle, paddingLeft: 10, background: value?.provider === p && value?.model === m ? 'var(--dsw-alias-bg-layer-2)' : 'transparent' }, onClick: () => { update('model', m); if (value?.provider !== p) update('provider', p); else { const next = { provider: p, model: m, ...(selectedEffort ? { reasoningEffort: selectedEffort } : {}) }; onChange(next); setPane('root') } } }, h('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis' } }, m), check(value?.provider === p && value?.model === m)))),
302
+ ms.map(m => {
303
+ const mid = getModelId(m)
304
+ const mname = getModelName(m)
305
+ const active = value?.provider === p && value?.model === mid
306
+ return h('button', { key: mid, type: 'button', style: { ...rowStyle, paddingLeft: 10, background: active ? 'var(--dsw-alias-bg-layer-2)' : 'transparent' }, onClick: () => { update('model', mid); if (value?.provider !== p) update('provider', p); else { const next = { provider: p, model: mid, ...(selectedEffort ? { reasoningEffort: selectedEffort } : {}) }; onChange(next); setPane('root') } } }, h('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis' } }, mname), check(active))
307
+ })),
251
308
  )
252
309
  }),
253
310
  ),
254
311
  ),
255
312
  pane === 'effort' && h('div', null,
256
313
  h('button', { type: 'button', style: { ...rowStyle, color: 'var(--dsw-alias-label-secondary)' }, onClick: () => setPane('root') }, h('span', null, '← Back'), h('span', { style: { fontSize: 12 } }, 'Effort')),
257
- h('div', { style: { marginTop: 4 } },
258
- [
259
- { id: '', label: 'Default effort' },
260
- { id: 'low', label: 'low' },
261
- { id: 'medium', label: 'medium' },
262
- { id: 'high', label: 'high' },
263
- ].map(e => h('button', { key: e.id || 'default', type: 'button', style: { ...rowStyle, background: selectedEffort === e.id ? 'var(--dsw-alias-bg-layer-2)' : 'transparent' }, onClick: () => { update('reasoningEffort', e.id); setPane('root') } }, h('span', null, e.label), check(selectedEffort === e.id))),
314
+ selectedModelInfo === null ? h('p', { style: { ...captionStyle, margin: '8px 4px 2px' } }, 'Select a model first to configure effort.') :
315
+ !supportsReasoning ? h('div', null,
316
+ h('p', { style: { ...captionStyle, margin: '8px 4px 6px' } }, 'This model does not support reasoning effort — using provider default'),
317
+ h('div', { style: { marginTop: 4 } },
318
+ [{ id: '', label: 'Default effort' }].map(e => h('button', { key: e.id || 'default', type: 'button', style: { ...rowStyle, background: selectedEffort === e.id ? 'var(--dsw-alias-bg-layer-2)' : 'transparent' }, onClick: () => { update('reasoningEffort', e.id); setPane('root') } }, h('span', null, e.label), check(selectedEffort === e.id))),
319
+ ),
320
+ warning && h('p', { style: { ...captionStyle, margin: '8px 4px 2px', color: 'var(--dsw-alias-state-error-primary)' } }, warning),
321
+ ) :
322
+ h('div', null,
323
+ h('div', { style: { marginTop: 4 } },
324
+ [{ id: '', label: 'Default effort' }, ...availableEfforts.map(id => ({ id, label: id }))].map(e => h('button', { key: e.id || 'default', type: 'button', style: { ...rowStyle, background: selectedEffort === e.id ? 'var(--dsw-alias-bg-layer-2)' : 'transparent' }, onClick: () => { update('reasoningEffort', e.id); setPane('root') } }, h('span', null, e.label), check(selectedEffort === e.id))),
325
+ ),
326
+ warning && h('p', { style: { ...captionStyle, margin: '8px 4px 2px', color: 'var(--dsw-alias-state-error-primary)' } }, warning),
264
327
  ),
265
328
  ),
266
329
  ),
@@ -336,26 +399,6 @@ function ToggleField({ label, caption, checked, onChange }) {
336
399
  )
337
400
  }
338
401
 
339
- /** Newest-first list of recorded review runs from the host's reviews.json. */
340
- function ReviewHistoryPanel({ rpcCall }) {
341
- const [entries, setEntries] = useState(null)
342
- useEffect(() => {
343
- rpcCall(MAESTRO_ENDPOINTS.reviewsList, {})
344
- .then(res => { if (res?.ok) setEntries(res.value ?? []) })
345
- .catch(() => setEntries([]))
346
- }, [])
347
- if (entries === null) return h('p', { style: captionStyle }, 'Loading review history…')
348
- if (entries.length === 0) return h('p', { style: captionStyle }, 'No reviews recorded yet.')
349
- const icon = entry => entry.status === 'completed' ? '✅' : entry.status === 'failed' ? '⚠️' : '👀'
350
- return h('ul', { style: { listStyle: 'none', margin: 0, padding: 0 } },
351
- entries.map(entry => h('li', { key: entry.id, style: { padding: '6px 0', borderBottom: '1px solid var(--dsw-alias-separator-default, #333)', fontSize: 13 } },
352
- h('span', null, `${icon(entry)} ${entry.projectPath} !${entry.mrIid} · ${entry.mode}${entry.trigger !== 'mention' ? ` · ${entry.trigger}` : ''}`),
353
- h('div', { style: captionStyle },
354
- `${new Date(entry.startedAt).toLocaleString()}${entry.summary ? ` — ${entry.summary}` : ''}${entry.error ? ` — ${entry.error}` : ''}`),
355
- )),
356
- )
357
- }
358
-
359
402
  /** One selectable LAN address chip + the QR of the currently selected URL. */
360
403
  function LanAccess({ proxyStatus, lanPin }) {
361
404
  const urls = proxyStatus?.lanUrls ?? []
@@ -443,7 +486,7 @@ function PublicAccess({ status, pin, showPin, onRevealPin, onHidePin, onRotatePi
443
486
  )
444
487
  }
445
488
 
446
- export function MaestroSettingsTab({ rpcCall }) {
489
+ export function MaestroSettingsTab({ rpcCall, configRpcCall }) {
447
490
  const [status, setStatus] = useState(null)
448
491
  const [proxyStatus, setProxyStatus] = useState(null)
449
492
  const [config, setConfig] = useState({ tunnelMode: 'quick', projectMappings: [] })
@@ -455,6 +498,13 @@ export function MaestroSettingsTab({ rpcCall }) {
455
498
  const [lanPinEnabled, setLanPinEnabled] = useState(false)
456
499
  const [lanPin, setLanPin] = useState(null)
457
500
  const [showLanPin, setShowLanPin] = useState(false)
501
+ // Task 3: Guard/Blacklist/Supervisor/Notifier tabs state
502
+ const [activeTab, setActiveTab] = useState('guard')
503
+ const [guard, setGuard] = useState({})
504
+ const [patternsText, setPatternsText] = useState('')
505
+ const [placeholdersText, setPlaceholdersText] = useState('')
506
+ const [supervisorCfg, setSupervisorCfg] = useState({})
507
+ const [notifierCfg, setNotifierCfg] = useState({})
458
508
 
459
509
  const call = async (endpoint, payload) => {
460
510
  const res = await rpcCall(endpoint, payload)
@@ -462,6 +512,60 @@ export function MaestroSettingsTab({ rpcCall }) {
462
512
  return res.value
463
513
  }
464
514
 
515
+ // Helpers for guard/supervisor/notifier domains via generic config RPC (Task 3)
516
+ const unwrap = (res) => {
517
+ if (res && typeof res === 'object' && 'ok' in res) {
518
+ if (res.ok) return res.value
519
+ throw new Error(res.error?.message ?? 'RPC failed')
520
+ }
521
+ return res
522
+ }
523
+ const cfgGet = async (domain) => {
524
+ if (!configRpcCall) throw new Error('config RPC not available')
525
+ const res = await configRpcCall('get', { domain })
526
+ return unwrap(res)
527
+ }
528
+ const cfgSet = async (domain, patch) => {
529
+ if (!configRpcCall) throw new Error('config RPC not available')
530
+ const res = await configRpcCall('set', { domain, patch })
531
+ return unwrap(res)
532
+ }
533
+ const saveGuard = async (patch) => {
534
+ setError(null)
535
+ const next = { ...guard, ...patch }
536
+ if (patch.gitProtection && guard.gitProtection) next.gitProtection = { ...guard.gitProtection, ...patch.gitProtection }
537
+ setGuard(next)
538
+ try { await cfgSet('guard', patch) } catch (e) { setError(e.message ?? String(e)) }
539
+ }
540
+ const commitBlacklistPatterns = async (text) => {
541
+ const patterns = text.split('\n').map(s => s.trim()).filter(Boolean)
542
+ setError(null)
543
+ try { await cfgSet('guardBlacklist', { patterns }) } catch (e) { setError(e.message ?? String(e)) }
544
+ }
545
+ const commitPlaceholders = async () => {
546
+ setError(null)
547
+ let obj = {}
548
+ try { obj = placeholdersText.trim() ? JSON.parse(placeholdersText) : {}; if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) throw new Error('placeholders must be JSON object') } catch (e) { setError(`placeholders JSON invalid: ${e.message ?? String(e)}`); return }
549
+ try { await cfgSet('guardBlacklist', { placeholders: obj }) } catch (e) { setError(e.message ?? String(e)) }
550
+ }
551
+ const saveSupervisorCfg = async (patch) => {
552
+ setError(null)
553
+ setSupervisorCfg(prev => ({ ...prev, ...patch }))
554
+ try { await cfgSet('supervisor', patch) } catch (e) { setError(e.message ?? String(e)) }
555
+ }
556
+ const saveNotifierCfg = async (patch) => {
557
+ setError(null)
558
+ setNotifierCfg(prev => {
559
+ const next = { ...prev }
560
+ for (const [k, v] of Object.entries(patch)) {
561
+ if (k === 'telegram' && typeof v === 'object' && v !== null) next.telegram = { ...(prev.telegram ?? {}), ...v }
562
+ else next[k] = v
563
+ }
564
+ return next
565
+ })
566
+ try { await cfgSet('notifier', patch) } catch (e) { setError(e.message ?? String(e)) }
567
+ }
568
+
465
569
  const refresh = async () => {
466
570
  try { setStatus(await call(MAESTRO_ENDPOINTS.status, {})) } catch { /* transient failure, ignore */ }
467
571
  try { setProxyStatus(await call(MAESTRO_ENDPOINTS.proxyStatus, {})) } catch { /* proxy row may be starting */ }
@@ -474,6 +578,31 @@ export function MaestroSettingsTab({ rpcCall }) {
474
578
  call(MAESTRO_ENDPOINTS.getConfig, {})
475
579
  .then(saved => setConfig(prev => ({ ...prev, ...saved })))
476
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
+ 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
+ // Task 3: load Guard/Blacklist/Supervisor/Notifier domains
591
+ Promise.all([
592
+ cfgGet('guard').catch(() => ({})),
593
+ cfgGet('guardBlacklist').catch(() => ({ patterns: [], placeholders: {} })),
594
+ cfgGet('supervisor').catch(() => ({})),
595
+ cfgGet('notifier').catch(() => ({})),
596
+ ]).then(([g, bl, sup, not]) => {
597
+ setGuard(g ?? {})
598
+ const pats = Array.isArray(bl?.patterns) ? bl.patterns : []
599
+ const ph = bl?.placeholders && typeof bl.placeholders === 'object' ? bl.placeholders : {}
600
+ setPatternsText(pats.join('\n'))
601
+ setPlaceholdersText(JSON.stringify(ph, null, 2))
602
+ setSupervisorCfg(sup ?? {})
603
+ setNotifierCfg(not ?? {})
604
+ }).catch(() => {})
605
+ }
477
606
  call(MAESTRO_ENDPOINTS.lanPinStatus, {})
478
607
  .then(value => { setLanPinEnabled(value.enabled); if (value.enabled) setLanPin(value.pin ?? null) })
479
608
  .catch(() => { /* host without the LAN PIN endpoints — keep the row hidden */ })
@@ -559,6 +688,16 @@ export function MaestroSettingsTab({ rpcCall }) {
559
688
  const saveField = async (field, value) => {
560
689
  setError(null)
561
690
  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
+ }
562
701
  try {
563
702
  await call(MAESTRO_ENDPOINTS.saveConfig, { [field]: value })
564
703
  } catch (err) {
@@ -663,7 +802,6 @@ export function MaestroSettingsTab({ rpcCall }) {
663
802
  checked: config.autoRereviewOnPush,
664
803
  onChange: checked => saveField('autoRereviewOnPush', checked),
665
804
  }),
666
- h(ReviewHistoryPanel, { rpcCall }),
667
805
  ),
668
806
 
669
807
  h('div', { style: sectionStyle },
@@ -679,11 +817,147 @@ export function MaestroSettingsTab({ rpcCall }) {
679
817
  }),
680
818
  ),
681
819
 
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
+
682
833
  h('div', { style: sectionStyle },
683
834
  h('h4', { style: headingStyle }, 'Projects'),
684
835
  h(ProjectMappingsEditor, { mappings: config.projectMappings ?? [], onChange: mappings => saveField('projectMappings', mappings), catalog, globalReviewModel: config.reviewModel ?? null }),
685
836
  ),
686
837
 
838
+ // Task 3: Guard/Blacklist/Supervisor/Notifier tabs — data-driven over guard domains
839
+ h('div', { style: sectionStyle },
840
+ h('h4', { style: headingStyle }, 'Guard / Blacklist / Supervisor / Notifier'),
841
+ h('div', { style: tabBarStyle },
842
+ h('button', { type: 'button', style: tabButtonStyle(activeTab === 'guard'), onClick: () => setActiveTab('guard') }, 'Guard'),
843
+ h('button', { type: 'button', style: tabButtonStyle(activeTab === 'blacklist'), onClick: () => setActiveTab('blacklist') }, 'Blacklist'),
844
+ h('button', { type: 'button', style: tabButtonStyle(activeTab === 'supervisor'), onClick: () => setActiveTab('supervisor') }, 'Supervisor'),
845
+ h('button', { type: 'button', style: tabButtonStyle(activeTab === 'notifier'), onClick: () => setActiveTab('notifier') }, 'Notifier'),
846
+ ),
847
+ activeTab === 'guard' && h('div', { 'data-tab': 'guard' },
848
+ h('p', { style: captionStyle }, 'Enforce publish block, git protection and cwd containment.'),
849
+ h(ToggleField, {
850
+ label: 'publishBlocked',
851
+ caption: 'Block publish-related commands when enabled.',
852
+ checked: guard.publishBlocked === true,
853
+ onChange: v => saveGuard({ publishBlocked: v }),
854
+ }),
855
+ h(ToggleField, {
856
+ label: 'gitProtection.enabled',
857
+ caption: 'Protect pushes to protected branches.',
858
+ checked: guard.gitProtection?.enabled === true,
859
+ onChange: v => saveGuard({ gitProtection: { enabled: v, branches: guard.gitProtection?.branches ?? ['master', 'main'] } }),
860
+ }),
861
+ h('label', { style: fieldLabelStyle }, 'gitProtection.branches (comma separated)'),
862
+ h('input', {
863
+ style: inputStyle,
864
+ value: (guard.gitProtection?.branches ?? ['master', 'main']).join(', '),
865
+ placeholder: 'master, main',
866
+ onChange: e => {
867
+ const branches = e.target.value.split(',').map(s => s.trim()).filter(Boolean)
868
+ saveGuard({ gitProtection: { enabled: guard.gitProtection?.enabled ?? true, branches } })
869
+ },
870
+ }),
871
+ h(ToggleField, {
872
+ label: 'cwdContainment',
873
+ caption: 'Contain file operations to the session cwd.',
874
+ checked: guard.cwdContainment === true,
875
+ onChange: v => saveGuard({ cwdContainment: v }),
876
+ }),
877
+ h('label', { style: fieldLabelStyle }, 'credentialPaths (comma separated)'),
878
+ h('input', {
879
+ style: inputStyle,
880
+ value: (guard.credentialPaths ?? []).join(', '),
881
+ placeholder: '~/.config/credentials.yaml, ~/.config/cloudflared',
882
+ onChange: e => {
883
+ const credentialPaths = e.target.value.split(',').map(s => s.trim()).filter(Boolean)
884
+ saveGuard({ credentialPaths })
885
+ },
886
+ }),
887
+ ),
888
+ activeTab === 'blacklist' && h('div', { 'data-tab': 'blacklist' },
889
+ h('p', { style: captionStyle }, 'One pattern per line. These are blocked from being committed or published.'),
890
+ h('label', { style: fieldLabelStyle }, 'patterns (one per line)'),
891
+ h('textarea', {
892
+ style: textareaStyle,
893
+ value: patternsText,
894
+ placeholder: 'example-project\nacme-shop',
895
+ onChange: e => setPatternsText(e.target.value),
896
+ onBlur: e => commitBlacklistPatterns(e.target.value),
897
+ }),
898
+ h('label', { style: fieldLabelStyle }, 'placeholders JSON'),
899
+ h('textarea', {
900
+ style: { ...textareaStyle, height: 90 },
901
+ value: placeholdersText,
902
+ placeholder: '{"example-project":"my-project"}',
903
+ onChange: e => setPlaceholdersText(e.target.value),
904
+ onBlur: () => commitPlaceholders(),
905
+ }),
906
+ h('p', { style: captionStyle }, 'Map blocked patterns to their placeholder suggestions.'),
907
+ h('button', { type: 'button', style: { ...secondaryButtonStyle, marginTop: 8 }, onClick: () => { commitBlacklistPatterns(patternsText); commitPlaceholders() } }, 'Save Blacklist'),
908
+ ),
909
+ activeTab === 'supervisor' && h('div', { 'data-tab': 'supervisor' },
910
+ h('p', { style: captionStyle }, 'Background daemon that auto-resumes crashed sessions.'),
911
+ h('label', { style: fieldLabelStyle }, 'intervalMs'),
912
+ h('input', {
913
+ type: 'number',
914
+ style: inputStyle,
915
+ value: supervisorCfg.intervalMs ?? '',
916
+ placeholder: '5000',
917
+ onChange: e => { const v = e.target.value === '' ? undefined : Number(e.target.value); saveSupervisorCfg({ intervalMs: v }) },
918
+ }),
919
+ h('label', { style: fieldLabelStyle }, 'downThreshold'),
920
+ h('input', {
921
+ type: 'number',
922
+ style: inputStyle,
923
+ value: supervisorCfg.downThreshold ?? '',
924
+ placeholder: '3',
925
+ onChange: e => { const v = e.target.value === '' ? undefined : Number(e.target.value); saveSupervisorCfg({ downThreshold: v }) },
926
+ }),
927
+ h(ToggleField, {
928
+ label: 'autoResumeEnabled',
929
+ caption: 'Automatically resume down sessions.',
930
+ checked: supervisorCfg.autoResumeEnabled === true,
931
+ onChange: v => saveSupervisorCfg({ autoResumeEnabled: v }),
932
+ }),
933
+ ),
934
+ activeTab === 'notifier' && h('div', { 'data-tab': 'notifier' },
935
+ h('p', { style: captionStyle }, 'Telegram notifications for Maestro events.'),
936
+ h('label', { style: fieldLabelStyle }, 'telegram.botToken'),
937
+ h('input', {
938
+ type: 'password',
939
+ autoComplete: 'off',
940
+ style: inputStyle,
941
+ value: notifierCfg.telegram?.botToken ?? '',
942
+ placeholder: '123456:ABC-DEF...',
943
+ onChange: e => saveNotifierCfg({ telegram: { botToken: e.target.value } }),
944
+ }),
945
+ h('label', { style: fieldLabelStyle }, 'telegram.chatId'),
946
+ h('input', {
947
+ style: inputStyle,
948
+ value: notifierCfg.telegram?.chatId ?? '',
949
+ placeholder: '-1001234567890',
950
+ onChange: e => saveNotifierCfg({ telegram: { chatId: e.target.value } }),
951
+ }),
952
+ h(ToggleField, {
953
+ label: 'telegram.reviewNotifications',
954
+ caption: 'Also notify about finished reviews.',
955
+ checked: notifierCfg.telegram?.reviewNotifications === true || notifierCfg.policy?.reviewNotifications === true,
956
+ onChange: v => saveNotifierCfg({ telegram: { reviewNotifications: v } }),
957
+ }),
958
+ ),
959
+ ),
960
+
687
961
  error && h('p', { style: errorStyle }, error),
688
962
  )
689
963
  }
package/src/host/index.ts CHANGED
@@ -1,6 +1,7 @@
1
- import type {} from '@deepseek-ai/dsh-client-connection'
2
1
  import type { Context } from '@deepseek-ai/cordis'
3
- import type { RpcErrorDetailsMap, RpcResult } from '@deepseek-ai/dsh-host-apiproxy/api'
2
+
3
+ type RpcResult<T> = { ok: true; value: T } | { ok: false; error: { code: string; message: string; details: object } }
4
+ type RpcErrorDetailsMap = { 'bad-request': { issues: object[] } }
4
5
  import { createMaestroConfigService, type MaestroConfigService } from './service.ts'
5
6
 
6
7
  export const name = 'maestro-config'
@@ -11,6 +12,7 @@ const RPC_CHANNEL = '/dsh-maestro-config'
11
12
  declare module '@deepseek-ai/cordis' {
12
13
  interface Context {
13
14
  maestroConfig: MaestroConfigService
15
+ connection: { rpc: { handle: (channel: string, handler: (endpoint: string, payload: unknown) => Promise<RpcResult<unknown>>, opts?: unknown) => () => void } }
14
16
  }
15
17
  }
16
18
 
@@ -31,7 +33,13 @@ function fail(message: string): RpcResult<never> {
31
33
  }
32
34
  }
33
35
 
34
- /** Publish maestroConfig over the shared store + loopback RPC for clients. */
36
+ /**
37
+ * Publish maestroConfig over the shared store + loopback RPC for clients.
38
+ * Exposes guard/guardBlacklist/supervisor/notifier domains (Task 1 validators)
39
+ * via generic get/set — validation is delegated to the lib's domain validators.
40
+ * Host also handles '/dsh-maestro-config/get' and '/dsh-maestro-config/set'
41
+ * style calls through the single channel with endpoint dispatch.
42
+ */
35
43
  export function apply(ctx: Context): void {
36
44
  const svc = createMaestroConfigService()
37
45
  ctx.provide('maestroConfig', svc)
@@ -43,6 +51,7 @@ export function apply(ctx: Context): void {
43
51
  }
44
52
  if (endpoint === 'get') {
45
53
  if (typeof body.domain !== 'string') return fail('domain (string) is required')
54
+ // guard / guardBlacklist / supervisor / notifier are all valid domains here
46
55
  return ok(await svc.get(body.domain))
47
56
  }
48
57
  if (endpoint === 'set') {