@carljia/omd-dsh 0.1.7 → 0.1.9

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 ADDED
@@ -0,0 +1,427 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@carljia/omd-dsh",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ "use strict";
8
+ var __create = Object.create;
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __getProtoOf = Object.getPrototypeOf;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
15
+ var __export = (target, all) => {
16
+ for (var name in all)
17
+ __defProp(target, name, { get: all[name], enumerable: true });
18
+ };
19
+ var __copyProps = (to, from, except, desc) => {
20
+ if (from && typeof from === "object" || typeof from === "function") {
21
+ for (let key of __getOwnPropNames(from))
22
+ if (!__hasOwnProp.call(to, key) && key !== except)
23
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
24
+ }
25
+ return to;
26
+ };
27
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
28
+ // If the importer is in node compatibility mode or this is not an ESM
29
+ // file that has been converted to a CommonJS file using a Babel-
30
+ // compatible transform (i.e. "__esModule" has not been set), then set
31
+ // "default" to the CommonJS "module.exports" for node compatibility.
32
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
33
+ mod
34
+ ));
35
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
36
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
37
+
38
+ // src/client.tsx
39
+ var client_exports = {};
40
+ __export(client_exports, {
41
+ OmdModelAllocationSection: () => OmdModelAllocationSection,
42
+ apply: () => apply,
43
+ inject: () => inject
44
+ });
45
+ module.exports = __toCommonJS(client_exports);
46
+ var React = __toESM(require("react"), 1);
47
+
48
+ // src/omd-matrix-controller.ts
49
+ var OMD_SETTINGS_NS = "omd-model-allocation";
50
+ function cloneMatrix(value) {
51
+ if (value === void 0) return null;
52
+ return JSON.parse(JSON.stringify(value));
53
+ }
54
+ function messageOf(error) {
55
+ return error instanceof Error ? error.message : String(error);
56
+ }
57
+ var OmdMatrixController = class {
58
+ constructor(api, scope, describe) {
59
+ __publicField(this, "api", api);
60
+ __publicField(this, "scope", scope);
61
+ __publicField(this, "describe", describe);
62
+ __publicField(this, "draft");
63
+ __publicField(this, "status", "idle");
64
+ __publicField(this, "error", null);
65
+ __publicField(this, "saved", false);
66
+ __publicField(this, "listeners", /* @__PURE__ */ new Set());
67
+ __publicField(this, "unsubscribeScope");
68
+ /**
69
+ * Cached snapshot handed to `useSyncExternalStore`. React requires
70
+ * `getSnapshot()` to return the SAME reference between changes — a fresh
71
+ * object per call throws "The result of getSnapshot should be cached" and
72
+ * crashes the whole settings entry (blank page). Rebuilt only in `emit()`.
73
+ */
74
+ __publicField(this, "snapshot");
75
+ this.draft = cloneMatrix(scope.getSnapshot().value);
76
+ this.snapshot = { draft: this.draft, status: this.status, error: this.error, saved: this.saved };
77
+ this.unsubscribeScope = scope.subscribe(() => this.onScopeChange());
78
+ }
79
+ /** Release the scope subscription (plugin fiber teardown). */
80
+ dispose() {
81
+ this.unsubscribeScope();
82
+ this.listeners.clear();
83
+ }
84
+ /** Stable snapshot reference (uSES-safe): the SAME object until state changes. */
85
+ getSnapshot() {
86
+ return this.snapshot;
87
+ }
88
+ subscribe(listener) {
89
+ this.listeners.add(listener);
90
+ return () => {
91
+ this.listeners.delete(listener);
92
+ };
93
+ }
94
+ emit() {
95
+ this.snapshot = { draft: this.draft, status: this.status, error: this.error, saved: this.saved };
96
+ for (const listener of [...this.listeners]) listener();
97
+ }
98
+ /** External change (mirror reload after a rejected write, another window):
99
+ * rebuild the draft from the fresh value, unless a save is in flight. */
100
+ onScopeChange() {
101
+ if (this.status === "saving") return;
102
+ this.refreshDraft();
103
+ this.emit();
104
+ }
105
+ refreshDraft() {
106
+ const value = this.scope.getSnapshot().value;
107
+ if (value !== void 0) this.draft = cloneMatrix(value);
108
+ }
109
+ /** Whether the Save / Reset controls may run. */
110
+ get canSave() {
111
+ const snap = this.scope.getSnapshot();
112
+ return snap.status === "ready" && snap.writable && this.status !== "saving" && this.draft !== null;
113
+ }
114
+ /** Whether the namespace accepts writes at all (read-only provider / memory mode). */
115
+ get writable() {
116
+ return this.scope.getSnapshot().writable;
117
+ }
118
+ /** Edit one mode's top-level field; an empty string removes the override
119
+ * (the field re-inherits the composition base / schema default). */
120
+ patchMode(modeId, field, value) {
121
+ const draft = this.draft;
122
+ if (draft === null) return;
123
+ const modes = { ...draft.modes };
124
+ const cfg = { ...modes[modeId] ?? {} };
125
+ if (value === "") delete cfg[field];
126
+ else cfg[field] = value;
127
+ modes[modeId] = cfg;
128
+ this.draft = { ...draft, modes };
129
+ this.saved = false;
130
+ this.emit();
131
+ }
132
+ /** Edit one tier's provider/model; an empty string removes the override. */
133
+ patchTier(modeId, tier, field, value) {
134
+ const draft = this.draft;
135
+ if (draft === null) return;
136
+ const modes = { ...draft.modes };
137
+ const cfg = { ...modes[modeId] ?? {} };
138
+ const tiers = { ...cfg.tiers ?? {} };
139
+ const tierCfg = { ...tiers[tier] ?? {} };
140
+ if (value === "") delete tierCfg[field];
141
+ else tierCfg[field] = value;
142
+ tiers[tier] = tierCfg;
143
+ cfg.tiers = tiers;
144
+ modes[modeId] = cfg;
145
+ this.draft = { ...draft, modes };
146
+ this.saved = false;
147
+ this.emit();
148
+ }
149
+ /**
150
+ * Persist the whole draft matrix through `settings.replace` (wholesale
151
+ * section replacement — the removal/reset path merge cannot express). The
152
+ * revision fence makes a stale editor fail loudly instead of overwriting.
153
+ * @returns once the write settled and the snapshot reflects the outcome.
154
+ */
155
+ async save() {
156
+ if (!this.canSave || this.draft === null) return;
157
+ const snap = this.scope.getSnapshot();
158
+ this.status = "saving";
159
+ this.error = null;
160
+ this.emit();
161
+ const response = await this.replace({ section: this.draft, revision: snap.revision });
162
+ if (response.ok) this.saved = true;
163
+ else {
164
+ this.error = response.message;
165
+ await this.describe.load();
166
+ }
167
+ this.refreshDraft();
168
+ this.status = "idle";
169
+ this.emit();
170
+ }
171
+ /** Restore the shipped default matrix: replace with an empty section. */
172
+ async reset() {
173
+ if (!this.canSave) return;
174
+ const snap = this.scope.getSnapshot();
175
+ this.status = "saving";
176
+ this.error = null;
177
+ this.emit();
178
+ const response = await this.replace({ section: {}, revision: snap.revision });
179
+ if (response.ok) this.saved = true;
180
+ else this.error = response.message;
181
+ await this.describe.load();
182
+ this.refreshDraft();
183
+ this.status = "idle";
184
+ this.emit();
185
+ }
186
+ async replace(request) {
187
+ try {
188
+ const response = await this.api.settings.replace({
189
+ ns: OMD_SETTINGS_NS,
190
+ section: request.section,
191
+ ...request.revision === void 0 ? {} : { expectedRevision: request.revision }
192
+ });
193
+ if (response.result.ok) return { ok: true };
194
+ return { ok: false, message: response.result.error.message };
195
+ } catch (error) {
196
+ return { ok: false, message: messageOf(error) };
197
+ }
198
+ }
199
+ };
200
+
201
+ // src/client.tsx
202
+ var NS = "settings.omd";
203
+ var MODE_ORDER = ["executor", "ultraworker", "planner", "reviewer", "explorer", "librarian", "chat"];
204
+ var en = {
205
+ nav: "OMD model allocation",
206
+ title: "OMD model allocation",
207
+ intro: "Model routes for the 7 OMD agent presets. Changes apply to sessions you start from now on \u2014 running sessions keep the models they began with.",
208
+ loading: "Loading the model matrix\u2026",
209
+ unavailable: "The model matrix is not available from this browser (settings are process-local for remote connections).",
210
+ readOnly: "The settings document is read-only here; the current matrix is shown for reference.",
211
+ mode: "Mode",
212
+ provider: "Provider",
213
+ model: "Model",
214
+ reasoningEffort: "Reasoning effort",
215
+ reasoningEffortHint: "Optional; leave empty to let the provider decide.",
216
+ tiers: "Tiers",
217
+ tier: "Tier",
218
+ hint: "Purpose",
219
+ save: "Save",
220
+ saving: "Saving\u2026",
221
+ reset: "Restore defaults",
222
+ saved: "Saved \u2014 new sessions will use the new matrix.",
223
+ errorPrefix: "Save failed:",
224
+ renderError: "The model matrix form could not be rendered:",
225
+ conflict: "The matrix changed on the host; the form was reloaded with the latest values.",
226
+ executor: "Executor",
227
+ ultraworker: "Ultraworker",
228
+ planner: "Planner",
229
+ reviewer: "Reviewer",
230
+ explorer: "Explorer",
231
+ librarian: "Librarian",
232
+ chat: "Chat"
233
+ };
234
+ var zh = {
235
+ nav: "omd\u6A21\u578B\u5206\u914D",
236
+ title: "omd \u6A21\u578B\u5206\u914D",
237
+ intro: "7 \u4E2A OMD agent \u9884\u8BBE\u7684\u6A21\u578B\u8DEF\u7531\u3002\u4FDD\u5B58\u540E\u5BF9\u4E4B\u540E\u65B0\u5EFA\u7684\u4F1A\u8BDD\u751F\u6548\u2014\u2014\u8FD0\u884C\u4E2D\u7684\u4F1A\u8BDD\u4FDD\u6301\u5B83\u5F00\u59CB\u65F6\u7684\u6A21\u578B\u3002",
238
+ loading: "\u6B63\u5728\u52A0\u8F7D\u6A21\u578B\u77E9\u9635\u2026",
239
+ unavailable: "\u5F53\u524D\u6D4F\u89C8\u5668\u65E0\u6CD5\u8BBF\u95EE\u6A21\u578B\u77E9\u9635\uFF08\u8FDC\u7A0B\u8FDE\u63A5\u65F6\u8BBE\u7F6E\u4EC5\u5728\u672C\u673A\u8FDB\u7A0B\u5185\u6709\u6548\uFF09\u3002",
240
+ readOnly: "\u8BBE\u7F6E\u6587\u6863\u5F53\u524D\u53EA\u8BFB\uFF1B\u4EE5\u4E0B\u5C55\u793A\u73B0\u6709\u77E9\u9635\uFF0C\u4EC5\u4F9B\u53C2\u8003\u3002",
241
+ mode: "\u6A21\u5F0F",
242
+ provider: "Provider",
243
+ model: "Model",
244
+ reasoningEffort: "\u63A8\u7406\u5F3A\u5EA6",
245
+ reasoningEffortHint: "\u53EF\u9009\uFF1B\u7559\u7A7A\u4EA4\u7ED9 provider \u9ED8\u8BA4\u3002",
246
+ tiers: "\u6863\u4F4D\uFF08Tiers\uFF09",
247
+ tier: "\u6863\u4F4D",
248
+ hint: "\u7528\u9014",
249
+ save: "\u4FDD\u5B58",
250
+ saving: "\u4FDD\u5B58\u4E2D\u2026",
251
+ reset: "\u6062\u590D\u9ED8\u8BA4",
252
+ saved: "\u5DF2\u4FDD\u5B58\u2014\u2014\u65B0\u4F1A\u8BDD\u5C06\u6309\u65B0\u77E9\u9635\u8DEF\u7531\u6A21\u578B\u3002",
253
+ errorPrefix: "\u4FDD\u5B58\u5931\u8D25\uFF1A",
254
+ renderError: "\u6A21\u578B\u77E9\u9635\u8868\u5355\u6E32\u67D3\u5931\u8D25\uFF1A",
255
+ conflict: "\u77E9\u9635\u5DF2\u5728\u5BBF\u4E3B\u4FA7\u53D8\u66F4\uFF0C\u8868\u5355\u5DF2\u6309\u6700\u65B0\u503C\u91CD\u65B0\u52A0\u8F7D\u3002",
256
+ executor: "\u6267\u884C\u8005",
257
+ ultraworker: "\u8D85\u80FD\u5DE5\u4F5C\u8005",
258
+ planner: "\u89C4\u5212\u8005",
259
+ reviewer: "\u8BC4\u5BA1\u8005",
260
+ explorer: "\u63A2\u7D22\u8005",
261
+ librarian: "\u56FE\u4E66\u7BA1\u7406\u5458",
262
+ chat: "\u5BF9\u8BDD"
263
+ };
264
+ var inject = ["slots", "locale", "connection", "settingsScope"];
265
+ function apply(ctx) {
266
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "omd-dsh: settings section dictionaries");
267
+ const { api } = ctx.get("connection");
268
+ const describe = ctx.settingsScope.describe();
269
+ const scope = ctx.settingsScope.bind({ namespace: OMD_SETTINGS_NS });
270
+ const controller = new OmdMatrixController(api, scope, describe);
271
+ const t = ctx.locale.bind(NS);
272
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
273
+ name: "settings.section",
274
+ id: "omd-model-allocation",
275
+ order: 30,
276
+ // after Models (10) and Agent presets (20)
277
+ label: () => t("nav"),
278
+ locale: NS,
279
+ inject: () => ({ scope, controller, t })
280
+ }, OmdModelAllocationSection));
281
+ }
282
+ var styles = {
283
+ section: { maxWidth: 720, color: "var(--dsw-alias-label-primary)", display: "flex", flexDirection: "column", gap: 12 },
284
+ title: { margin: 0, fontSize: 18, fontWeight: 600 },
285
+ intro: { color: "var(--dsw-alias-label-tertiary)", margin: 0, fontSize: 13, lineHeight: "20px" },
286
+ notice: { color: "var(--dsw-alias-state-warn-label)", margin: 0, fontSize: 12, lineHeight: "18px" },
287
+ savedNotice: { color: "var(--dsw-alias-state-success-primary)", margin: 0, fontSize: 12, lineHeight: "18px" },
288
+ error: { color: "var(--dsw-alias-state-error-primary)", margin: 0, fontSize: 12, lineHeight: "18px" },
289
+ card: { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", borderRadius: 12, padding: "14px 16px", display: "flex", flexDirection: "column", gap: 10 },
290
+ cardHead: { display: "flex", alignItems: "baseline", gap: 8 },
291
+ cardTitle: { margin: 0, fontSize: 15, fontWeight: 600 },
292
+ cardRoute: { color: "var(--dsw-alias-label-tertiary)", fontSize: 12, lineHeight: "18px" },
293
+ field: { display: "flex", flexDirection: "column", gap: 4 },
294
+ fieldLabel: { color: "var(--dsw-alias-label-secondary)", fontSize: 12, fontWeight: 500, lineHeight: "18px" },
295
+ input: { boxSizing: "border-box", border: "1px solid var(--dsw-alias-border-l2)", width: "100%", height: 32, font: "inherit", background: "var(--dsw-alias-bg-layer-1)", color: "var(--dsw-alias-label-primary)", borderRadius: 8, padding: "0 10px", fontSize: 14, lineHeight: "22px" },
296
+ inputDisabled: { opacity: 0.6, cursor: "default" },
297
+ tierRow: { borderTop: "1px solid var(--dsw-alias-border-l2)", paddingTop: 10, display: "flex", flexDirection: "column", gap: 8 },
298
+ tierHead: { display: "flex", alignItems: "baseline", gap: 8 },
299
+ tierName: { margin: 0, fontSize: 13, fontWeight: 600 },
300
+ tierHint: { color: "var(--dsw-alias-label-tertiary)", fontSize: 12, lineHeight: "18px", flex: 1, overflowWrap: "anywhere" },
301
+ grid2: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", gap: 8 },
302
+ actions: { display: "flex", alignItems: "center", gap: 8, marginTop: 4 },
303
+ primaryButton: { boxSizing: "border-box", height: 36, font: "inherit", cursor: "pointer", border: "none", borderRadius: 18, padding: "0 14px", fontSize: 14, lineHeight: "22px", background: "var(--dsw-alias-button-primary-fill)", color: "var(--dsw-alias-label-primary-foreground)" },
304
+ secondaryButton: { boxSizing: "border-box", height: 36, font: "inherit", cursor: "pointer", border: "1px solid var(--dsw-alias-border-l2)", background: "transparent", color: "var(--dsw-alias-label-primary)", borderRadius: 18, padding: "0 14px", fontSize: 14, lineHeight: "22px" },
305
+ buttonDisabled: { opacity: 0.4, cursor: "default" }
306
+ };
307
+ var OmdRenderBoundary = class extends React.Component {
308
+ constructor() {
309
+ super(...arguments);
310
+ __publicField(this, "state", { error: null });
311
+ }
312
+ static getDerivedStateFromError(error) {
313
+ return { error };
314
+ }
315
+ render() {
316
+ if (this.state.error !== null) {
317
+ return /* @__PURE__ */ React.createElement("div", { style: styles.section }, /* @__PURE__ */ React.createElement("h2", { style: styles.title }, this.props.t("nav")), /* @__PURE__ */ React.createElement("p", { style: styles.error, role: "alert" }, this.props.t("renderError"), " ", this.state.error instanceof Error ? this.state.error.message : String(this.state.error)));
318
+ }
319
+ return this.props.children ?? null;
320
+ }
321
+ };
322
+ function OmdModelAllocationSection(props) {
323
+ return /* @__PURE__ */ React.createElement(OmdRenderBoundary, { t: props.t }, /* @__PURE__ */ React.createElement(OmdModelAllocationBody, { ...props }));
324
+ }
325
+ function OmdModelAllocationBody(props) {
326
+ const { scope, controller, t } = props;
327
+ const scopeSnapshot = React.useSyncExternalStore(
328
+ React.useCallback((listener) => scope.subscribe(listener), [scope]),
329
+ React.useCallback(() => scope.getSnapshot(), [scope]),
330
+ React.useCallback(() => scope.getSnapshot(), [scope])
331
+ // SSR/test renderers; no-op in the browser
332
+ );
333
+ const ui = React.useSyncExternalStore(
334
+ React.useCallback((listener) => controller.subscribe(listener), [controller]),
335
+ React.useCallback(() => controller.getSnapshot(), [controller]),
336
+ React.useCallback(() => controller.getSnapshot(), [controller])
337
+ );
338
+ if (scopeSnapshot.status === "unavailable") {
339
+ return /* @__PURE__ */ React.createElement("div", { style: styles.section }, /* @__PURE__ */ React.createElement("h2", { style: styles.title }, t("nav")), /* @__PURE__ */ React.createElement("p", { style: styles.intro }, t("unavailable")));
340
+ }
341
+ if (scopeSnapshot.status !== "ready" || ui.draft === null) {
342
+ return /* @__PURE__ */ React.createElement("div", { style: styles.section }, /* @__PURE__ */ React.createElement("h2", { style: styles.title }, t("nav")), /* @__PURE__ */ React.createElement("p", { style: styles.intro }, t("loading")));
343
+ }
344
+ const matrix = ui.draft;
345
+ const readOnly = !controller.writable;
346
+ return /* @__PURE__ */ React.createElement("div", { style: styles.section }, /* @__PURE__ */ React.createElement("h2", { style: styles.title }, t("nav")), /* @__PURE__ */ React.createElement("p", { style: styles.intro }, t("intro")), readOnly ? /* @__PURE__ */ React.createElement("p", { style: styles.notice }, t("readOnly")) : null, ui.saved ? /* @__PURE__ */ React.createElement("p", { style: styles.savedNotice, role: "status" }, t("saved")) : null, ui.error !== null ? /* @__PURE__ */ React.createElement("p", { style: styles.error, role: "alert" }, t("errorPrefix"), " ", ui.error, ui.error.toLowerCase().includes("revision") || ui.error.toLowerCase().includes("changed") ? ` ${t("conflict")}` : "") : null, MODE_ORDER.map((modeId) => {
347
+ const cfg = matrix.modes[modeId] ?? {};
348
+ const route = cfg.provider !== void 0 || cfg.model !== void 0 ? `${cfg.provider ?? "?"}/${cfg.model ?? "?"}` : "";
349
+ const modeName = t(modeId) ?? modeId;
350
+ return /* @__PURE__ */ React.createElement("div", { key: modeId, style: styles.card }, /* @__PURE__ */ React.createElement("div", { style: styles.cardHead }, /* @__PURE__ */ React.createElement("h3", { style: styles.cardTitle }, modeName), /* @__PURE__ */ React.createElement("span", { style: styles.cardRoute }, modeId, route !== "" ? ` \xB7 ${route}` : "")), /* @__PURE__ */ React.createElement("div", { style: styles.grid2 }, /* @__PURE__ */ React.createElement("label", { style: styles.field }, /* @__PURE__ */ React.createElement("span", { style: styles.fieldLabel }, t("provider")), /* @__PURE__ */ React.createElement(
351
+ "input",
352
+ {
353
+ style: { ...styles.input, ...readOnly ? styles.inputDisabled : {} },
354
+ value: cfg.provider ?? "",
355
+ disabled: readOnly || ui.status === "saving",
356
+ spellCheck: false,
357
+ placeholder: "deepseek-official",
358
+ onChange: (event) => controller.patchMode(modeId, "provider", event.target.value)
359
+ }
360
+ )), /* @__PURE__ */ React.createElement("label", { style: styles.field }, /* @__PURE__ */ React.createElement("span", { style: styles.fieldLabel }, t("model")), /* @__PURE__ */ React.createElement(
361
+ "input",
362
+ {
363
+ style: { ...styles.input, ...readOnly ? styles.inputDisabled : {} },
364
+ value: cfg.model ?? "",
365
+ disabled: readOnly || ui.status === "saving",
366
+ spellCheck: false,
367
+ placeholder: "deepseek-v4-pro",
368
+ onChange: (event) => controller.patchMode(modeId, "model", event.target.value)
369
+ }
370
+ ))), /* @__PURE__ */ React.createElement("label", { style: styles.field }, /* @__PURE__ */ React.createElement("span", { style: styles.fieldLabel }, t("reasoningEffort")), /* @__PURE__ */ React.createElement(
371
+ "input",
372
+ {
373
+ style: { ...styles.input, ...readOnly ? styles.inputDisabled : {} },
374
+ value: cfg.reasoningEffort ?? "",
375
+ disabled: readOnly || ui.status === "saving",
376
+ spellCheck: false,
377
+ placeholder: "",
378
+ title: t("reasoningEffortHint"),
379
+ onChange: (event) => controller.patchMode(modeId, "reasoningEffort", event.target.value)
380
+ }
381
+ )), cfg.tiers !== void 0 && Object.keys(cfg.tiers).length > 0 ? /* @__PURE__ */ React.createElement("div", { style: styles.tierRow }, /* @__PURE__ */ React.createElement("span", { style: styles.fieldLabel }, t("tiers")), Object.entries(cfg.tiers).map(([tierName, tier]) => /* @__PURE__ */ React.createElement("div", { key: tierName, style: styles.tierRow }, /* @__PURE__ */ React.createElement("div", { style: styles.tierHead }, /* @__PURE__ */ React.createElement("h4", { style: styles.tierName }, tierName), /* @__PURE__ */ React.createElement("span", { style: styles.tierHint }, tier.hint ?? "")), /* @__PURE__ */ React.createElement("div", { style: styles.grid2 }, /* @__PURE__ */ React.createElement("label", { style: styles.field }, /* @__PURE__ */ React.createElement("span", { style: styles.fieldLabel }, t("provider")), /* @__PURE__ */ React.createElement(
382
+ "input",
383
+ {
384
+ style: { ...styles.input, ...readOnly ? styles.inputDisabled : {} },
385
+ value: tier.provider ?? "",
386
+ disabled: readOnly || ui.status === "saving",
387
+ spellCheck: false,
388
+ onChange: (event) => controller.patchTier(modeId, tierName, "provider", event.target.value)
389
+ }
390
+ )), /* @__PURE__ */ React.createElement("label", { style: styles.field }, /* @__PURE__ */ React.createElement("span", { style: styles.fieldLabel }, t("model")), /* @__PURE__ */ React.createElement(
391
+ "input",
392
+ {
393
+ style: { ...styles.input, ...readOnly ? styles.inputDisabled : {} },
394
+ value: tier.model ?? "",
395
+ disabled: readOnly || ui.status === "saving",
396
+ spellCheck: false,
397
+ onChange: (event) => controller.patchTier(modeId, tierName, "model", event.target.value)
398
+ }
399
+ )))))) : null);
400
+ }), !readOnly ? /* @__PURE__ */ React.createElement("div", { style: styles.actions }, /* @__PURE__ */ React.createElement(
401
+ "button",
402
+ {
403
+ type: "button",
404
+ style: { ...styles.primaryButton, ...!controller.canSave ? styles.buttonDisabled : {} },
405
+ disabled: !controller.canSave,
406
+ onClick: () => {
407
+ controller.save();
408
+ }
409
+ },
410
+ ui.status === "saving" ? t("saving") : t("save")
411
+ ), /* @__PURE__ */ React.createElement(
412
+ "button",
413
+ {
414
+ type: "button",
415
+ style: { ...styles.secondaryButton, ...!controller.canSave ? styles.buttonDisabled : {} },
416
+ disabled: !controller.canSave,
417
+ onClick: () => {
418
+ controller.reset();
419
+ }
420
+ },
421
+ t("reset")
422
+ )) : null);
423
+ }
424
+
425
+ return module.exports;
426
+ }
427
+ });
package/lib/host.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ import type { Matrix } from "./sync.js";
2
+ /**
3
+ * @module @carljia/omd-dsh
4
+ *
5
+ * omd-dsh host row (package root entry, injected by cordis.patch.yml):
6
+ *
7
+ * 1. registers the `omd-model-allocation` settings namespace (base = the
8
+ * shipped default matrix, `applies: "live"`), making the settings
9
+ * document (<DSH_HOME>/settings.yaml) the authoritative "online edit"
10
+ * store for the model matrix;
11
+ * 2. reconciles once at startup: if <DSH_HOME>/omd-matrix.json exists and
12
+ * differs from the namespace's resolved value, its content is imported
13
+ * into the namespace (`replace`) — CLI edits and pre-settings installs
14
+ * survive the migration, and CLI edits are picked up on the next restart;
15
+ * 3. renders the 7 presets from the namespace's resolved matrix and mirrors
16
+ * that matrix back to omd-matrix.json (the export mirror + CLI face);
17
+ * 4. watches the namespace: every UI save re-renders the presets and
18
+ * re-mirrors the file. New sessions route with the new matrix; running
19
+ * sessions keep the combination they started with.
20
+ *
21
+ * The browser half (lib/client.js) binds the same namespace and renders the
22
+ * "omd模型分配" settings section (see src/client.tsx). Because the client
23
+ * plugin discovery (dsh-client-modules) matches loader entries by package
24
+ * name, the bundle patch injects THIS row under the bare package name
25
+ * `@carljia/omd-dsh` — not a `/boot` subpath.
26
+ *
27
+ * Failure policy: a broken settings document / namespace must never brick the
28
+ * host's boot — any failure here falls back to the plain file sync (the old
29
+ * boot behavior) and is logged.
30
+ */
31
+ /** Cordis plugin name used by loader diagnostics. */
32
+ export declare const name = "omd-dsh";
33
+ /** Requires the settings service (provided by dsh-base → dsh-settings-file in every profile). */
34
+ export declare const inject: string[];
35
+ /**
36
+ * Startup reconciliation decision (pure, unit-tested): the file matrix is
37
+ * imported into the settings namespace exactly when it exists AND differs
38
+ * from the namespace's currently resolved value. Equal values need no write;
39
+ * a missing file leaves the namespace (defaults / user section) authoritative.
40
+ */
41
+ export declare function shouldImportFile(fileMatrix: Matrix | undefined, resolved: Matrix): boolean;
42
+ export declare function apply(ctx: any): Promise<void>;
package/lib/host.js ADDED
@@ -0,0 +1,103 @@
1
+ import { settingsNamespace } from "@deepseek-ai/dsh-settings";
2
+ import { readDefaultMatrix, readMatrixFileIfExists, matrixEquals, runSyncWithMatrix, runSync, saveMatrix, MatrixSchema, MATRIX_PATH, } from "./sync.js";
3
+ /**
4
+ * @module @carljia/omd-dsh
5
+ *
6
+ * omd-dsh host row (package root entry, injected by cordis.patch.yml):
7
+ *
8
+ * 1. registers the `omd-model-allocation` settings namespace (base = the
9
+ * shipped default matrix, `applies: "live"`), making the settings
10
+ * document (<DSH_HOME>/settings.yaml) the authoritative "online edit"
11
+ * store for the model matrix;
12
+ * 2. reconciles once at startup: if <DSH_HOME>/omd-matrix.json exists and
13
+ * differs from the namespace's resolved value, its content is imported
14
+ * into the namespace (`replace`) — CLI edits and pre-settings installs
15
+ * survive the migration, and CLI edits are picked up on the next restart;
16
+ * 3. renders the 7 presets from the namespace's resolved matrix and mirrors
17
+ * that matrix back to omd-matrix.json (the export mirror + CLI face);
18
+ * 4. watches the namespace: every UI save re-renders the presets and
19
+ * re-mirrors the file. New sessions route with the new matrix; running
20
+ * sessions keep the combination they started with.
21
+ *
22
+ * The browser half (lib/client.js) binds the same namespace and renders the
23
+ * "omd模型分配" settings section (see src/client.tsx). Because the client
24
+ * plugin discovery (dsh-client-modules) matches loader entries by package
25
+ * name, the bundle patch injects THIS row under the bare package name
26
+ * `@carljia/omd-dsh` — not a `/boot` subpath.
27
+ *
28
+ * Failure policy: a broken settings document / namespace must never brick the
29
+ * host's boot — any failure here falls back to the plain file sync (the old
30
+ * boot behavior) and is logged.
31
+ */
32
+ /** Cordis plugin name used by loader diagnostics. */
33
+ export const name = "omd-dsh";
34
+ /** Requires the settings service (provided by dsh-base → dsh-settings-file in every profile). */
35
+ export const inject = ["settings"];
36
+ const NS = settingsNamespace("omd-model-allocation");
37
+ /**
38
+ * Startup reconciliation decision (pure, unit-tested): the file matrix is
39
+ * imported into the settings namespace exactly when it exists AND differs
40
+ * from the namespace's currently resolved value. Equal values need no write;
41
+ * a missing file leaves the namespace (defaults / user section) authoritative.
42
+ */
43
+ export function shouldImportFile(fileMatrix, resolved) {
44
+ return fileMatrix !== undefined && !matrixEquals(fileMatrix, resolved);
45
+ }
46
+ function makeLog(ctx) {
47
+ return {
48
+ info: (message) => { try {
49
+ ctx?.logger?.info?.(message);
50
+ }
51
+ catch {
52
+ console.log(message);
53
+ } },
54
+ error: (message) => { try {
55
+ ctx?.logger?.error?.(message);
56
+ }
57
+ catch {
58
+ console.error(message);
59
+ } },
60
+ };
61
+ }
62
+ export async function apply(ctx) {
63
+ const log = makeLog(ctx);
64
+ const flags = { dryRun: false, verbose: false };
65
+ try {
66
+ const settings = ctx.settings; // SettingsProvider (dsh-base provides it in every profile)
67
+ const scope = settings.register(NS, MatrixSchema, {
68
+ base: readDefaultMatrix(),
69
+ applies: "live",
70
+ });
71
+ // 启动期调和:文件 → 命名空间(幂等;导入 CLI/旧版自定义,CLI 手改在下次重启被采纳)
72
+ const fileMatrix = readMatrixFileIfExists();
73
+ if (shouldImportFile(fileMatrix, scope.get())) {
74
+ log.info("omd-dsh: importing " + MATRIX_PATH + " into settings namespace " + NS);
75
+ await scope.replace(fileMatrix);
76
+ }
77
+ // 用命名空间解析出的完整矩阵渲染 presets,并把该矩阵写回镜像文件
78
+ const resolved = scope.get();
79
+ saveMatrix(resolved);
80
+ await runSyncWithMatrix(resolved, flags, log.info);
81
+ // 后续 UI 编辑:重渲染 + 更新镜像(watch 仅在解析值真正变化时触发)
82
+ scope.watch((next) => {
83
+ log.info("omd-dsh: matrix changed via settings — re-syncing presets and mirroring omd-matrix.json");
84
+ try {
85
+ saveMatrix(next);
86
+ }
87
+ catch (error) {
88
+ log.error("omd-dsh: failed to mirror " + MATRIX_PATH + ": " + (error instanceof Error ? error.message : String(error)));
89
+ }
90
+ runSyncWithMatrix(next, flags, log.info).catch((error) => log.error("omd-dsh re-sync failed: " + (error instanceof Error ? error.message : String(error))));
91
+ });
92
+ }
93
+ catch (error) {
94
+ // settings 不可用 / 命名空间已损坏时退回纯文件 sync(保持老 boot 行为)
95
+ log.error("omd-dsh: settings namespace failed, falling back to file sync: " + (error instanceof Error ? error.message : String(error)));
96
+ try {
97
+ await runSync(flags, log.info);
98
+ }
99
+ catch (fallbackError) {
100
+ log.error("omd-dsh: file sync fallback failed: " + (fallbackError instanceof Error ? fallbackError.message : String(fallbackError)));
101
+ }
102
+ }
103
+ }
package/lib/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
- import { scopeOf } from "@deepseek-ai/dsh-scope";
3
2
  import { setModeOverride } from "./shared.js";
4
3
  /**
5
4
  * @module @carljia/omd-dsh
@@ -81,10 +80,10 @@ function presetSwitchedAfterLastRequest(session) {
81
80
  return lastSwitch > lastHeader;
82
81
  }
83
82
  function apply(ctx, config) {
84
- if (scopeOf(ctx) === undefined) {
85
- throw new Error("omd-mode: refusing to mount outside a scoped context (mode '" + config.mode + "'). " +
86
- "Mount this row inside an agent preset; a global mount would pin the model for every agent in the process.");
87
- }
83
+ // 无 scope 守卫:本行以自包含 bundle(.omd-vendor/omd-mode.mjs)分发,bundle 内
84
+ // 自带的 dsh-scope 副本永远读不到 harness 实例写入的 kScope Symbol,守卫会误报
85
+ // (曾导致全部 omd preset 挂载失败)。preset 挂载由 harness 的 loader 保证作用域;
86
+ // 若误挂到全局组合,后果(进程级钉模型)立即可见。见 docs/ARCHITECTURE.md。
88
87
  const pinned = config.provider !== undefined && config.model !== undefined
89
88
  ? {
90
89
  provider: config.provider,
package/lib/mode.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
2
- import { scopeOf } from "@deepseek-ai/dsh-scope";
3
2
  /**
4
3
  * @module @carljia/omd-dsh/mode
5
4
  *
@@ -127,9 +126,7 @@ async function executeSwitch(ctx, invocation) {
127
126
  }
128
127
  }
129
128
  function apply(ctx) {
130
- if (scopeOf(ctx) === undefined) {
131
- throw new Error("omd-mode-switch: refusing to mount outside a scoped context; mount this row inside an agent preset");
132
- }
129
+ // 无 scope 守卫:见 src/index.ts 的说明(自包含 bundle 无法读取 harness 的 kScope)。
133
130
  ctx.inject(["commands"], (commandCtx) => {
134
131
  commandCtx.commands.register({
135
132
  name: "mode",