@ddtcorex/dsh-maestro-config 0.1.1 → 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)(() => {
@@ -2596,31 +2620,6 @@ function ToggleField({ label, caption, checked, onChange }) {
2596
2620
  )
2597
2621
  );
2598
2622
  }
2599
- function ReviewHistoryPanel({ rpcCall }) {
2600
- const [entries, setEntries] = (0, import_react.useState)(null);
2601
- (0, import_react.useEffect)(() => {
2602
- rpcCall(MAESTRO_ENDPOINTS.reviewsList, {}).then((res) => {
2603
- if (res?.ok) setEntries(res.value ?? []);
2604
- }).catch(() => setEntries([]));
2605
- }, []);
2606
- if (entries === null) return (0, import_react.createElement)("p", { style: captionStyle }, "Loading review history\u2026");
2607
- if (entries.length === 0) return (0, import_react.createElement)("p", { style: captionStyle }, "No reviews recorded yet.");
2608
- const icon = (entry) => entry.status === "completed" ? "\u2705" : entry.status === "failed" ? "\u26A0\uFE0F" : "\u{1F440}";
2609
- return (0, import_react.createElement)(
2610
- "ul",
2611
- { style: { listStyle: "none", margin: 0, padding: 0 } },
2612
- entries.map((entry) => (0, import_react.createElement)(
2613
- "li",
2614
- { key: entry.id, style: { padding: "6px 0", borderBottom: "1px solid var(--dsw-alias-separator-default, #333)", fontSize: 13 } },
2615
- (0, import_react.createElement)("span", null, `${icon(entry)} ${entry.projectPath} !${entry.mrIid} \xB7 ${entry.mode}${entry.trigger !== "mention" ? ` \xB7 ${entry.trigger}` : ""}`),
2616
- (0, import_react.createElement)(
2617
- "div",
2618
- { style: captionStyle },
2619
- `${new Date(entry.startedAt).toLocaleString()}${entry.summary ? ` \u2014 ${entry.summary}` : ""}${entry.error ? ` \u2014 ${entry.error}` : ""}`
2620
- )
2621
- ))
2622
- );
2623
- }
2624
2623
  function LanAccess({ proxyStatus, lanPin }) {
2625
2624
  const urls = proxyStatus?.lanUrls ?? [];
2626
2625
  const [selected, setSelected] = (0, import_react.useState)(0);
@@ -2729,11 +2728,95 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
2729
2728
  const [lanPinEnabled, setLanPinEnabled] = (0, import_react.useState)(false);
2730
2729
  const [lanPin, setLanPin] = (0, import_react.useState)(null);
2731
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)({});
2732
2737
  const call = async (endpoint, payload) => {
2733
2738
  const res = await rpcCall(endpoint, payload);
2734
2739
  if (!res?.ok) throw new Error(res?.error?.message ?? "RPC failed");
2735
2740
  return res.value;
2736
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
+ };
2737
2820
  const refresh = async () => {
2738
2821
  try {
2739
2822
  setStatus(await call(MAESTRO_ENDPOINTS.status, {}));
@@ -2754,6 +2837,21 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
2754
2837
  }
2755
2838
  }).catch(() => {
2756
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
+ });
2757
2855
  }
2758
2856
  call(MAESTRO_ENDPOINTS.lanPinStatus, {}).then((value) => {
2759
2857
  setLanPinEnabled(value.enabled);
@@ -2971,8 +3069,7 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
2971
3069
  caption: "After a completed review, further pushes to the same MR trigger an automatic quick re-review.",
2972
3070
  checked: config.autoRereviewOnPush,
2973
3071
  onChange: (checked) => saveField("autoRereviewOnPush", checked)
2974
- }),
2975
- (0, import_react.createElement)(ReviewHistoryPanel, { rpcCall })
3072
+ })
2976
3073
  ),
2977
3074
  (0, import_react.createElement)(
2978
3075
  "div",
@@ -3008,6 +3105,149 @@ function MaestroSettingsTab({ rpcCall, configRpcCall }) {
3008
3105
  (0, import_react.createElement)("h4", { style: headingStyle }, "Projects"),
3009
3106
  (0, import_react.createElement)(ProjectMappingsEditor, { mappings: config.projectMappings ?? [], onChange: (mappings) => saveField("projectMappings", mappings), catalog, globalReviewModel: config.reviewModel ?? null })
3010
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
+ ),
3011
3251
  error && (0, import_react.createElement)("p", { style: errorStyle }, error)
3012
3252
  );
3013
3253
  }
@@ -3051,7 +3291,7 @@ function registerSettingsNavIcon(label, root) {
3051
3291
  // src/client/index.tsx
3052
3292
  var SETTINGS_NAV_CSS2 = `
3053
3293
 
3054
- /* 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 */
3055
3295
  [${SETTINGS_NAV_MARKER}] > svg:first-child {
3056
3296
  display: none;
3057
3297
  }
@@ -3062,10 +3302,11 @@ var SETTINGS_NAV_CSS2 = `
3062
3302
  width: 16px;
3063
3303
  height: 16px;
3064
3304
  background: currentColor;
3065
- -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;
3066
- 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;
3067
3307
  }
3068
3308
  `;
3309
+ var inject = ["slots", "connection"];
3069
3310
  function installNavIconStyle() {
3070
3311
  const tag = document.createElement("style");
3071
3312
  tag.dataset.plugin = "@ddtcorex/dsh-maestro-config";
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,CA+B1C"}
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 +1 @@
1
- {"version":3,"file":"maestro-card.d.ts","sourceRoot":"","sources":["../../../src/client/maestro-card.jsx"],"names":[],"mappings":"AAkeA;;;;;;;;gBAoRC"}
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,6 @@
1
1
  {
2
2
  "name": "@ddtcorex/dsh-maestro-config",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "description": "Maestro Config — shared settings service for the dsh-maestro-* suite over the single namespaced store (~/.dsh/maestro/settings.json)",
6
6
  "type": "module",
@@ -34,16 +34,14 @@
34
34
  },
35
35
  "peerDependencies": {
36
36
  "@deepseek-ai/cordis": "^4.0.1",
37
- "@deepseek-ai/dsh-client-connection": "0.1.0-rc.8",
38
- "@deepseek-ai/dsh-host-apiproxy": "0.1.0-rc.8"
37
+ "@deepseek-ai/dsh-client-connection": "0.1.1-rc.2"
39
38
  },
40
39
  "dependencies": {
41
- "@ddtcorex/dsh-maestro-config-lib": "^0.1.1"
40
+ "@ddtcorex/dsh-maestro-config-lib": "^0.1.2"
42
41
  },
43
42
  "devDependencies": {
44
43
  "@deepseek-ai/cordis": "^4.0.1",
45
- "@deepseek-ai/dsh-client-connection": "0.1.0-rc.8",
46
- "@deepseek-ai/dsh-host-apiproxy": "0.1.0-rc.8",
44
+ "@deepseek-ai/dsh-client-connection": "0.1.1-rc.2",
47
45
  "@types/node": "^22.0.0",
48
46
  "@types/qrcode": "^1.5.6",
49
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'
@@ -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)
@@ -373,26 +399,6 @@ function ToggleField({ label, caption, checked, onChange }) {
373
399
  )
374
400
  }
375
401
 
376
- /** Newest-first list of recorded review runs from the host's reviews.json. */
377
- function ReviewHistoryPanel({ rpcCall }) {
378
- const [entries, setEntries] = useState(null)
379
- useEffect(() => {
380
- rpcCall(MAESTRO_ENDPOINTS.reviewsList, {})
381
- .then(res => { if (res?.ok) setEntries(res.value ?? []) })
382
- .catch(() => setEntries([]))
383
- }, [])
384
- if (entries === null) return h('p', { style: captionStyle }, 'Loading review history…')
385
- if (entries.length === 0) return h('p', { style: captionStyle }, 'No reviews recorded yet.')
386
- const icon = entry => entry.status === 'completed' ? '✅' : entry.status === 'failed' ? '⚠️' : '👀'
387
- return h('ul', { style: { listStyle: 'none', margin: 0, padding: 0 } },
388
- entries.map(entry => h('li', { key: entry.id, style: { padding: '6px 0', borderBottom: '1px solid var(--dsw-alias-separator-default, #333)', fontSize: 13 } },
389
- h('span', null, `${icon(entry)} ${entry.projectPath} !${entry.mrIid} · ${entry.mode}${entry.trigger !== 'mention' ? ` · ${entry.trigger}` : ''}`),
390
- h('div', { style: captionStyle },
391
- `${new Date(entry.startedAt).toLocaleString()}${entry.summary ? ` — ${entry.summary}` : ''}${entry.error ? ` — ${entry.error}` : ''}`),
392
- )),
393
- )
394
- }
395
-
396
402
  /** One selectable LAN address chip + the QR of the currently selected URL. */
397
403
  function LanAccess({ proxyStatus, lanPin }) {
398
404
  const urls = proxyStatus?.lanUrls ?? []
@@ -492,6 +498,13 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }) {
492
498
  const [lanPinEnabled, setLanPinEnabled] = useState(false)
493
499
  const [lanPin, setLanPin] = useState(null)
494
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({})
495
508
 
496
509
  const call = async (endpoint, payload) => {
497
510
  const res = await rpcCall(endpoint, payload)
@@ -499,6 +512,60 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }) {
499
512
  return res.value
500
513
  }
501
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
+
502
569
  const refresh = async () => {
503
570
  try { setStatus(await call(MAESTRO_ENDPOINTS.status, {})) } catch { /* transient failure, ignore */ }
504
571
  try { setProxyStatus(await call(MAESTRO_ENDPOINTS.proxyStatus, {})) } catch { /* proxy row may be starting */ }
@@ -520,6 +587,21 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }) {
520
587
  }
521
588
  })
522
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(() => {})
523
605
  }
524
606
  call(MAESTRO_ENDPOINTS.lanPinStatus, {})
525
607
  .then(value => { setLanPinEnabled(value.enabled); if (value.enabled) setLanPin(value.pin ?? null) })
@@ -720,7 +802,6 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }) {
720
802
  checked: config.autoRereviewOnPush,
721
803
  onChange: checked => saveField('autoRereviewOnPush', checked),
722
804
  }),
723
- h(ReviewHistoryPanel, { rpcCall }),
724
805
  ),
725
806
 
726
807
  h('div', { style: sectionStyle },
@@ -754,6 +835,129 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }) {
754
835
  h(ProjectMappingsEditor, { mappings: config.projectMappings ?? [], onChange: mappings => saveField('projectMappings', mappings), catalog, globalReviewModel: config.reviewModel ?? null }),
755
836
  ),
756
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
+
757
961
  error && h('p', { style: errorStyle }, error),
758
962
  )
759
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') {