@mind-harness/mh-browser 0.2.5 → 0.2.7

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.cjs CHANGED
@@ -4,236 +4,92 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- let zod = require("zod");
8
7
  let react = require("react");
9
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-typert-protocol@0.2.2-rc.1_@deepseek-ai+cordis@4.1.0/node_modules/@deepseek-ai/dsh-typert-protocol/lib/types/remote-info.js
10
- /** Lazy assembly of generated Remote definitions using caller-supplied Zod; no runtime imports. */
8
+ //#region src/client/call.ts
9
+ const PACKAGE = "mh-browser";
11
10
  /**
12
- * Retain one generated definition per endpoint without constructing parsers or projections.
13
- * The class instance builds its shared descriptor/method index on first access;
14
- * input schemas and JSON Schema projections are cached only when requested.
15
- * @param spec - compiler-produced method facts and Zod factories.
16
- * @param zod - the generated artifact's Zod runtime for named-input parsers.
17
- * @returns a lazy, mountable info instance with no registration or transport effects.
11
+ * `document.baseURI` keeps a mounted page on its prefix. dsh's file upload
12
+ * resolves the same way.
18
13
  */
19
- function createTypertRemoteInfo(spec, zod) {
20
- return new RemoteInfo(spec, zod);
21
- }
22
- var RemoteInfo = class {
23
- spec;
24
- zod;
25
- index;
26
- constructor(spec, zod) {
27
- this.spec = spec;
28
- this.zod = zod;
29
- }
30
- get package() {
31
- return this.spec.package;
32
- }
33
- get descriptors() {
34
- return this.resolve().descriptors;
35
- }
36
- get methods() {
37
- return this.resolve().methods;
38
- }
39
- resolve() {
40
- if (this.index !== void 0) return this.index;
41
- const zod = this.zod;
42
- const descriptors = [];
43
- const methods = Object.create(null);
44
- for (const [endpoint, definition] of Object.entries(this.spec.methods)) {
45
- const separator = endpoint.indexOf("/");
46
- const namespace = endpoint.slice(0, separator);
47
- const method = endpoint.slice(separator + 1);
48
- const parameters = definition.parameters.map((parameter) => ({
49
- ...parameter,
50
- wire: parameter.wire ?? parameter.name
51
- }));
52
- const descriptor = {
53
- ...definition,
54
- id: `${this.spec.package}#${endpoint}`,
55
- namespace,
56
- method,
57
- parameters,
58
- result: definition.result.description === void 0 ? definition.result : new ResultValue(definition.result)
59
- };
60
- if (definition.invocation.kind === "direct") {
61
- const inputs = [];
62
- methods[endpoint] = Object.assign(descriptor, {
63
- input(options) {
64
- const strict = options?.strict === true;
65
- return inputs[strict ? 1 : 0] ??= new NamedInput(parameters, strict, zod);
66
- },
67
- invoke(remote, input, signal) {
68
- const named = input;
69
- const values = parameters.map((parameter) => named[parameter.wire]);
70
- if (definition.cancellation !== void 0) values.push(signal);
71
- const receiver = Reflect.get(remote, namespace);
72
- return Reflect.get(receiver, method).apply(receiver, values);
73
- }
74
- });
75
- }
76
- descriptors.push(descriptor);
77
- }
78
- return this.index = {
79
- descriptors,
80
- methods
81
- };
82
- }
83
- };
84
- function optional(parameter) {
85
- return parameter.source === "json" && parameter.acceptsUndefined === true;
86
- }
87
- var NamedInput = class {
88
- parameters;
89
- strict;
90
- zod;
91
- schema;
92
- projection;
93
- constructor(parameters, strict, zod) {
94
- this.parameters = parameters;
95
- this.strict = strict;
96
- this.zod = zod;
97
- }
98
- codec(parameter) {
99
- return this.strict ? parameter.strictCodec : parameter.codec;
100
- }
101
- create = () => this.schema ??= this.zod.strictObject(Object.fromEntries(this.parameters.map((parameter) => {
102
- const value = this.codec(parameter).create();
103
- return [parameter.wire, optional(parameter) ? value.optional() : value.nonoptional()];
104
- })));
105
- get jsonSchema() {
106
- return this.project().jsonSchema;
107
- }
108
- get jsonSchemaError() {
109
- return this.project().jsonSchemaError;
110
- }
111
- project() {
112
- if (this.projection !== void 0) return this.projection;
113
- const unavailable = this.parameters.find((parameter) => this.codec(parameter).jsonSchema === void 0);
114
- if (unavailable !== void 0) return this.projection = { jsonSchemaError: this.codec(unavailable).jsonSchemaError ?? `No JSON Schema for parameter ${unavailable.wire}` };
115
- return this.projection = { jsonSchema: {
116
- type: "object",
117
- properties: Object.fromEntries(this.parameters.map((parameter) => [parameter.wire, {
118
- ...this.codec(parameter).jsonSchema,
119
- ...parameter.description === void 0 ? {} : { description: parameter.description }
120
- }])),
121
- required: this.parameters.filter((parameter) => !optional(parameter)).map((parameter) => parameter.wire),
122
- additionalProperties: false
123
- } };
124
- }
125
- };
126
- var ResultValue = class {
127
- spec;
128
- projection;
129
- constructor(spec) {
130
- this.spec = spec;
131
- }
132
- get mode() {
133
- return this.spec.mode;
134
- }
135
- get typeSymbol() {
136
- return this.spec.typeSymbol;
137
- }
138
- get create() {
139
- return this.spec.create;
140
- }
141
- get decode() {
142
- return this.spec.decode;
143
- }
144
- get encode() {
145
- return this.spec.encode;
146
- }
147
- get description() {
148
- return this.spec.description;
149
- }
150
- get jsonSchemaError() {
151
- return this.spec.jsonSchemaError;
14
+ async function call(segment, init, signal) {
15
+ const url = new URL(`${PACKAGE}/api/${segment}`, document.baseURI);
16
+ if (init.query !== void 0) for (const [key, value] of Object.entries(init.query)) url.searchParams.set(key, value);
17
+ const request = {
18
+ method: init.method,
19
+ credentials: "same-origin",
20
+ cache: "no-store"
21
+ };
22
+ if (signal !== void 0) request.signal = signal;
23
+ if (init.method === "POST") {
24
+ request.headers = { "content-type": "application/json" };
25
+ request.body = JSON.stringify(init.body ?? {});
152
26
  }
153
- get jsonSchema() {
154
- if (this.projection === void 0) {
155
- const schema = this.spec.jsonSchema;
156
- this.projection = { value: schema === void 0 ? void 0 : {
157
- ...schema,
158
- description: this.spec.description
159
- } };
160
- }
161
- return this.projection.value;
27
+ try {
28
+ const payload = await (await fetch(url, request)).json();
29
+ if (isCallResult(payload)) return payload;
30
+ return fail("unreachable", "response was not understood");
31
+ } catch (error) {
32
+ if (signal?.aborted === true) return fail("cancelled", "cancelled");
33
+ return fail("unreachable", error instanceof Error ? error.message : "request failed");
162
34
  }
163
- };
164
- //#endregion
165
- //#region lib/typert.remote-client.js
166
- let _mind_harness_mh_browser_mhBrowser_revealHandover_parameter_0$schema$value;
167
- const _mind_harness_mh_browser_mhBrowser_revealHandover_parameter_0$schema = () => _mind_harness_mh_browser_mhBrowser_revealHandover_parameter_0$schema$value ??= zod.z.string();
168
- let _mind_harness_mh_browser_mhBrowser_revealHandover_result$schema$value;
169
- const _mind_harness_mh_browser_mhBrowser_revealHandover_result$schema = () => _mind_harness_mh_browser_mhBrowser_revealHandover_result$schema$value ??= zod.z.object({ "status": zod.z.union([
170
- zod.z.literal("revealed"),
171
- zod.z.literal("not-handing-over"),
172
- zod.z.literal("no-tab")
173
- ]) });
174
- let _mind_harness_mh_browser_mhBrowser_revealHandover_parameter_0_strict$schema$value;
175
- const _mind_harness_mh_browser_mhBrowser_revealHandover_parameter_0_strict$schema = () => _mind_harness_mh_browser_mhBrowser_revealHandover_parameter_0_strict$schema$value ??= zod.z.string();
176
- const TYPERT_REMOTE = createTypertRemoteInfo({
177
- package: "@mind-harness/mh-browser",
178
- methods: { "mhBrowser/revealHandover": {
179
- service: "mhBrowser",
180
- description: "The person asked to see the page this Session handed over.",
181
- invocation: { kind: "direct" },
182
- parameters: [{
183
- name: "sessionId",
184
- source: "json",
185
- codec: {
186
- mode: "strict",
187
- typeSymbol: "@mind-harness/mh-browser#mhBrowser/revealHandover:sessionId",
188
- create: _mind_harness_mh_browser_mhBrowser_revealHandover_parameter_0$schema,
189
- jsonSchema: { "type": "string" }
190
- },
191
- strictCodec: {
192
- mode: "strict",
193
- typeSymbol: "@mind-harness/mh-browser#mhBrowser/revealHandover:sessionId",
194
- create: _mind_harness_mh_browser_mhBrowser_revealHandover_parameter_0_strict$schema,
195
- jsonSchema: { "type": "string" }
196
- }
197
- }],
198
- cancellation: { parameter: "signal" },
199
- result: {
200
- mode: "strict",
201
- typeSymbol: "@mind-harness/mh-browser#mhBrowser/revealHandover:result",
202
- create: _mind_harness_mh_browser_mhBrowser_revealHandover_result$schema,
203
- jsonSchema: {
204
- "type": "object",
205
- "properties": { "status": {
206
- "type": "string",
207
- "enum": [
208
- "revealed",
209
- "not-handing-over",
210
- "no-tab"
211
- ]
212
- } },
213
- "required": ["status"],
214
- "additionalProperties": true
215
- }
216
- },
217
- sourceLocation: {
218
- "file": "packages/mh-browser/src/service/service.ts",
219
- "line": 684,
220
- "column": 9
35
+ }
36
+ function fail(code, message) {
37
+ return {
38
+ ok: false,
39
+ error: {
40
+ code: `${PACKAGE}/${code}`,
41
+ message
221
42
  }
222
- } }
223
- }, zod.z);
43
+ };
44
+ }
45
+ function isCallResult(value) {
46
+ if (typeof value !== "object" || value === null || !("ok" in value)) return false;
47
+ if (value.ok === true) return "value" in value;
48
+ if (value.ok !== false || !("error" in value)) return false;
49
+ const error = value.error;
50
+ if (typeof error !== "object" || error === null) return false;
51
+ return "code" in error && typeof error.code === "string" && "message" in error && typeof error.message === "string";
52
+ }
224
53
  //#endregion
225
54
  //#region src/client/card.ts
226
- /** Host theme tokens only; the rules unmount with the row. */
227
- const STYLE = `
55
+ /**
56
+ * Host theme tokens only; the rules unmount with the row.
57
+ * The button copies the host primary control at its default size, and the
58
+ * right-up outline that control uses for leaving the app. This row loads no
59
+ * host client package.
60
+ */
61
+ const STYLE$1 = `
228
62
  .mh-br { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; color: var(--dsw-alias-label-primary); font-size: 14px; line-height: 20px; }
229
63
  .mh-br-line { margin: 0; }
230
64
  .mh-br-status { margin: 0; color: var(--dsw-alias-label-secondary); }
231
- .mh-br-button { padding: 4px 10px; border: 0.5px solid var(--dsw-alias-border-l3); border-radius: var(--dsw-radius-md); background: transparent; color: var(--dsw-alias-label-primary); font: inherit; cursor: pointer; }
65
+ .mh-br-button { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 4px; height: 36px; padding: 0 14px; border: none; border-radius: var(--dsw-radius-md); background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-label-primary-foreground); font-size: 14px; line-height: 22px; cursor: pointer; }
66
+ @media (hover: hover) { .mh-br-button:hover { background: var(--dsw-alias-button-primary-hover); } }
67
+ .mh-br-icon { display: inline-flex; width: 16px; height: 16px; align-items: center; justify-content: center; }
232
68
  `;
233
- /** The open handover: the reason and the button that raises its window. */
69
+ /** Corner of the host right-up outline, 16px viewBox, 1px stroke. */
70
+ const RIGHT_UP_CORNER = "M11.7256 2.77441C12.5538 2.77469 13.2256 3.44616 13.2256 4.27441V10.1416H12.2256V4.27441C12.2256 3.99844 12.0015 3.77469 11.7256 3.77441H5.7207V2.77441H11.7256Z";
71
+ /** Stem of the host right-up outline. */
72
+ const RIGHT_UP_STEM = "M2.77441 13.2255L12.3756 3.62427";
73
+ function revealIcon() {
74
+ return (0, react.createElement)("span", { className: "mh-br-icon" }, (0, react.createElement)("svg", {
75
+ width: 14,
76
+ height: 14,
77
+ viewBox: "0 0 16 16",
78
+ fill: "none",
79
+ strokeWidth: 1,
80
+ "aria-hidden": true
81
+ }, (0, react.createElement)("path", {
82
+ d: RIGHT_UP_CORNER,
83
+ fill: "currentColor"
84
+ }), (0, react.createElement)("path", {
85
+ d: RIGHT_UP_STEM,
86
+ stroke: "currentColor"
87
+ })));
88
+ }
89
+ /** The open handover: a short line and the button that raises its window. */
234
90
  function HandoverRow(props) {
235
91
  const [note, setNote] = (0, react.useState)(void 0);
236
- return (0, react.createElement)("section", { className: "mh-br" }, (0, react.createElement)("style", null, STYLE), (0, react.createElement)("p", { className: "mh-br-line" }, props.t("waiting", { reason: props.reason })), (0, react.createElement)("button", {
92
+ return (0, react.createElement)("section", { className: "mh-br" }, (0, react.createElement)("style", null, STYLE$1), (0, react.createElement)("p", { className: "mh-br-line" }, props.t("waiting")), (0, react.createElement)("button", {
237
93
  type: "button",
238
94
  className: "mh-br-button",
239
95
  onClick: () => {
@@ -243,7 +99,7 @@ window.__ModuleLoader__.load({
243
99
  setNote(props.t("failed"));
244
100
  });
245
101
  }
246
- }, props.t("reveal")), note !== void 0 ? (0, react.createElement)("p", { className: "mh-br-status" }, note) : null);
102
+ }, revealIcon(), props.t("reveal")), note !== void 0 ? (0, react.createElement)("p", { className: "mh-br-status" }, note) : null);
247
103
  }
248
104
  /** `revealed` adds no status line. */
249
105
  function noteFor(result, t) {
@@ -256,18 +112,70 @@ window.__ModuleLoader__.load({
256
112
  /** Dictionary namespace for the handover row. */
257
113
  const NS = "mh-browser";
258
114
  const zh = {
259
- waiting: "浏览器交给你了:{reason}",
260
- reveal: "去浏览器",
115
+ waiting: "这一步要你在浏览器里做",
116
+ reveal: "打开浏览器里的页面",
261
117
  notHandingOver: "这次交接已经结束",
262
118
  noTab: "没有可以去的页面",
263
- failed: "没能切到浏览器"
119
+ failed: "没能切到浏览器",
120
+ browserLabel: "浏览器",
121
+ browserHint: "保存后,浏览器空闲时会关掉再按这个选择打开。已经打开的标签页会关掉。",
122
+ pathLabel: "浏览器路径",
123
+ pathHint: "留空就用自动找到的。可以填绝对路径,也可以用 ~。保存后同样在空闲时重启。",
124
+ pathInvalid: "需要绝对路径,或以 ~ 开头",
125
+ pathSearching: "正在查找…",
126
+ pathMissing: "未找到{name},请填写路径",
127
+ pathHelp: "说明",
128
+ pathDoesNotExist: "不存在",
129
+ pathIsDirectory: "是目录",
130
+ pathDenied: "没有权限({code})",
131
+ auto: "自动",
132
+ browserUsing: "当前使用:{name}。",
133
+ browserNone: "没有找到 Chrome 或 Edge。",
134
+ installChrome: "安装 Chrome",
135
+ chrome: "Google Chrome",
136
+ msedge: "Microsoft Edge",
137
+ chromium: "Chromium",
138
+ overridden: "已覆盖",
139
+ reset: "恢复",
140
+ save: "保存",
141
+ saving: "保存中",
142
+ settingsUnavailable: "这个配置现在不可用",
143
+ settingsReadOnly: "这次启动的配置不能在页面上改",
144
+ settingsSaveFailed: "没有保存:设置可能在别处改过,或这个值不被接受。修改还在,检查后可以再保存",
145
+ discard: "放弃修改"
264
146
  };
265
147
  const en = {
266
- waiting: "The browser is yours: {reason}",
267
- reveal: "Go to the browser",
148
+ waiting: "This step needs you in the browser",
149
+ reveal: "Open the page in the browser",
268
150
  notHandingOver: "This handover has already ended",
269
151
  noTab: "There is no page to open",
270
- failed: "Could not switch to the browser"
152
+ failed: "Could not switch to the browser",
153
+ browserLabel: "Browser",
154
+ browserHint: "After saving, the browser restarts once it is idle. Tabs that are open will close.",
155
+ pathLabel: "Browser path",
156
+ pathHint: "Leave empty to use the browser that was found. An absolute path, or one starting with ~, is accepted. Saving restarts the browser once it is idle.",
157
+ pathInvalid: "Use an absolute path, or one starting with ~",
158
+ pathSearching: "Looking…",
159
+ pathMissing: "{name} was not found. Enter its path",
160
+ pathHelp: "Details",
161
+ pathDoesNotExist: "does not exist",
162
+ pathIsDirectory: "is a directory",
163
+ pathDenied: "permission denied ({code})",
164
+ auto: "Automatic",
165
+ browserUsing: "In use: {name}.",
166
+ browserNone: "Chrome and Edge were not found.",
167
+ installChrome: "Install Chrome",
168
+ chrome: "Google Chrome",
169
+ msedge: "Microsoft Edge",
170
+ chromium: "Chromium",
171
+ overridden: "Overridden",
172
+ reset: "Reset",
173
+ save: "Save",
174
+ saving: "Saving",
175
+ settingsUnavailable: "This setting is not available",
176
+ settingsReadOnly: "This launch keeps the setting read-only",
177
+ settingsSaveFailed: "Not saved: the setting may have changed elsewhere, or the value was not accepted. Your changes are kept; check them and save again.",
178
+ discard: "Discard changes"
271
179
  };
272
180
  //#endregion
273
181
  //#region src/client/row.ts
@@ -278,43 +186,33 @@ window.__ModuleLoader__.load({
278
186
  */
279
187
  const HANDOVER_KIND = "browser-handover";
280
188
  const HANDOVER_TOOL = "browser_handover";
281
- function isRecord(value) {
189
+ const ROW_DATA = {};
190
+ function isRecord$1(value) {
282
191
  return typeof value === "object" && value !== null && !Array.isArray(value);
283
192
  }
284
193
  function turnOf(data) {
285
- if (!isRecord(data) || typeof data.turn !== "number") return void 0;
194
+ if (!isRecord$1(data) || typeof data.turn !== "number") return void 0;
286
195
  return data.turn;
287
196
  }
288
- function reasonOf(value) {
289
- if (typeof value !== "string") return "";
290
- try {
291
- const parsed = JSON.parse(value);
292
- if (!isRecord(parsed) || typeof parsed.reason !== "string") return "";
293
- return parsed.reason;
294
- } catch {
295
- return "";
296
- }
297
- }
298
197
  function callOf(data) {
299
- if (!isRecord(data) || typeof data.callId !== "string" || typeof data.name !== "string") return void 0;
198
+ if (!isRecord$1(data) || typeof data.callId !== "string" || typeof data.name !== "string") return void 0;
300
199
  return {
301
200
  callId: data.callId,
302
- name: data.name,
303
- arguments: data.arguments
201
+ name: data.name
304
202
  };
305
203
  }
306
204
  /** A result names its call on `message.source` and again on `message.toolCallId`. */
307
205
  function callIdOf(data) {
308
- if (!isRecord(data) || !isRecord(data.message)) return void 0;
206
+ if (!isRecord$1(data) || !isRecord$1(data.message)) return void 0;
309
207
  const message = data.message;
310
- if (isRecord(message.source) && typeof message.source.callId === "string" && message.source.callId !== "") return message.source.callId;
208
+ if (isRecord$1(message.source) && typeof message.source.callId === "string" && message.source.callId !== "") return message.source.callId;
311
209
  if (typeof message.toolCallId === "string" && message.toolCallId !== "") return message.toolCallId;
312
210
  }
313
211
  function hidden(current) {
314
212
  return {
315
213
  ...current,
316
214
  visibility: "hidden",
317
- data: { reason: "" }
215
+ data: ROW_DATA
318
216
  };
319
217
  }
320
218
  /**
@@ -335,7 +233,7 @@ window.__ModuleLoader__.load({
335
233
  location: context.start?.location ?? { kind: "unresolved" },
336
234
  visibility: open === void 0 ? "hidden" : "visible",
337
235
  groupTree: "turn",
338
- data: { reason: open?.reason ?? "" }
236
+ data: ROW_DATA
339
237
  };
340
238
  }
341
239
  const handoverRowDefinition = {
@@ -373,7 +271,6 @@ window.__ModuleLoader__.load({
373
271
  return {
374
272
  open: {
375
273
  callId: call.callId,
376
- reason: reasonOf(call.arguments),
377
274
  seq: event.seq
378
275
  },
379
276
  anchorSeq: event.seq
@@ -394,51 +291,699 @@ window.__ModuleLoader__.load({
394
291
  }
395
292
  };
396
293
  //#endregion
294
+ //#region src/client/settings-form.ts
295
+ /** Drafts over one entry's fields, written together on save. */
296
+ var StagedForm = class {
297
+ scope;
298
+ specs;
299
+ staged = /* @__PURE__ */ new Map();
300
+ listeners = /* @__PURE__ */ new Set();
301
+ baseline;
302
+ saving = false;
303
+ failed = false;
304
+ unsubscribe;
305
+ constructor(scope, specs) {
306
+ this.scope = scope;
307
+ this.specs = new Map(specs.map((spec) => [spec.field, spec]));
308
+ this.unsubscribe = scope.subscribe(() => {
309
+ this.publish();
310
+ });
311
+ }
312
+ /** A store over `project`, rebuilt whenever the entry or a draft changes. */
313
+ bind(project) {
314
+ let current = project();
315
+ const subscribers = /* @__PURE__ */ new Set();
316
+ this.listeners.add(() => {
317
+ current = project();
318
+ for (const listener of subscribers) listener();
319
+ });
320
+ return {
321
+ getSnapshot: () => current,
322
+ subscribe: (listener) => {
323
+ subscribers.add(listener);
324
+ return () => {
325
+ subscribers.delete(listener);
326
+ };
327
+ }
328
+ };
329
+ }
330
+ shell() {
331
+ const snapshot = this.scope.getSnapshot();
332
+ const plan = this.plan();
333
+ return {
334
+ available: snapshot.status === "ready",
335
+ writable: snapshot.writable,
336
+ dirty: plan.length > 0,
337
+ invalid: plan.some((item) => item.op === void 0),
338
+ saving: this.saving,
339
+ failed: this.failed
340
+ };
341
+ }
342
+ field(field) {
343
+ const spec = this.spec(field);
344
+ const staged = this.staged.get(field);
345
+ if (staged === void 0) return {
346
+ text: spec.format(this.read("value", field)),
347
+ overridden: this.stored(field),
348
+ invalid: false
349
+ };
350
+ const write = staged.clear ? { kind: "clear" } : spec.parse(staged.text);
351
+ return {
352
+ text: staged.text,
353
+ overridden: write?.kind === "set",
354
+ invalid: write === void 0
355
+ };
356
+ }
357
+ edit(field, text) {
358
+ this.stage(field, {
359
+ text,
360
+ clear: false
361
+ });
362
+ }
363
+ /** Stage removing the field from the user layer. The draft shows what it falls back to. */
364
+ reset(field) {
365
+ this.stage(field, {
366
+ text: this.spec(field).format(this.read("base", field)),
367
+ clear: true
368
+ });
369
+ }
370
+ discard() {
371
+ if (this.staged.size === 0 && !this.failed) return;
372
+ this.staged.clear();
373
+ this.baseline = void 0;
374
+ this.failed = false;
375
+ this.publish();
376
+ }
377
+ /**
378
+ * Write every staged edit. A save the Host refuses keeps the drafts, and the
379
+ * next save is fenced on what the Host holds now, so trying again can land.
380
+ */
381
+ async save() {
382
+ const plan = this.plan();
383
+ if (plan.length === 0 || this.saving || !this.scope.getSnapshot().writable) return;
384
+ if (plan.some((item) => item.op === void 0)) return;
385
+ this.saving = true;
386
+ this.failed = false;
387
+ this.publish();
388
+ try {
389
+ const ops = plan.flatMap((item) => item.op === void 0 ? [] : [item.op]);
390
+ const landed = ops.length === 0 || await this.scope.mutate(ops, this.baseline?.revision);
391
+ if (landed) {
392
+ this.staged.clear();
393
+ this.baseline = void 0;
394
+ } else this.baseline = this.scope.getSnapshot();
395
+ this.failed = !landed;
396
+ } catch {
397
+ this.baseline = this.scope.getSnapshot();
398
+ this.failed = true;
399
+ } finally {
400
+ this.saving = false;
401
+ this.publish();
402
+ }
403
+ }
404
+ dispose() {
405
+ this.unsubscribe();
406
+ this.listeners.clear();
407
+ }
408
+ plan() {
409
+ const plan = [];
410
+ for (const [field, staged] of this.staged) {
411
+ const spec = this.spec(field);
412
+ if (staged.clear) {
413
+ if (this.stored(field)) plan.push({
414
+ field,
415
+ op: {
416
+ op: "unset",
417
+ path: [field]
418
+ }
419
+ });
420
+ continue;
421
+ }
422
+ if (staged.text === spec.format(this.read("value", field))) continue;
423
+ const write = spec.parse(staged.text);
424
+ if (write === void 0) plan.push({ field });
425
+ else if (write.kind === "clear") plan.push({
426
+ field,
427
+ op: {
428
+ op: "unset",
429
+ path: [field]
430
+ }
431
+ });
432
+ else plan.push({
433
+ field,
434
+ op: {
435
+ op: "set",
436
+ path: [field],
437
+ value: write.value
438
+ }
439
+ });
440
+ }
441
+ return plan;
442
+ }
443
+ stage(field, edit) {
444
+ this.baseline ??= this.scope.getSnapshot();
445
+ this.staged.set(field, edit);
446
+ this.failed = false;
447
+ this.publish();
448
+ }
449
+ spec(field) {
450
+ const spec = this.specs.get(field);
451
+ if (spec === void 0) throw new Error(`settings form has no field ${field}`);
452
+ return spec;
453
+ }
454
+ read(layer, field) {
455
+ const section = this.scope.getSnapshot()[layer];
456
+ return isRecord(section) ? section[field] : void 0;
457
+ }
458
+ stored(field) {
459
+ const user = this.scope.getSnapshot().user;
460
+ return isRecord(user) && Object.hasOwn(user, field);
461
+ }
462
+ publish() {
463
+ for (const listener of this.listeners) listener();
464
+ }
465
+ };
466
+ /** Free text. An empty draft clears the field. */
467
+ function textField(field) {
468
+ return {
469
+ field,
470
+ format: (value) => typeof value === "string" ? value : "",
471
+ parse: (text) => {
472
+ const trimmed = text.trim();
473
+ return trimmed === "" ? { kind: "clear" } : {
474
+ kind: "set",
475
+ value: trimmed
476
+ };
477
+ }
478
+ };
479
+ }
480
+ const STYLE = `
481
+ .mh-browser-form { display: flex; flex-direction: column; }
482
+ .mh-browser-status { margin: 0 0 12px; font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-tertiary); }
483
+ .mh-browser-footer { display: flex; align-items: flex-start; gap: 12px; padding-top: 16px; }
484
+ .mh-browser-save { flex: none; }
485
+ .mh-browser-failure { flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 2px; padding-top: 5px; }
486
+ .mh-browser-failed { margin: 0; font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-error); }
487
+ .mh-browser-save { appearance: none; border: 1px solid transparent; border-radius: var(--dsw-radius-md); padding: 5px 14px; font: inherit; font-size: 13px; line-height: 1.5; cursor: pointer; background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-layer-3); }
488
+ .mh-browser-save:disabled { opacity: 0.4; cursor: default; }
489
+ .mh-browser-save:focus-visible, .mh-browser-help-button:focus-visible { outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 1px; }
490
+ .mh-browser-field { display: flex; flex-direction: column; gap: 6px; padding: 12px 0; }
491
+ .mh-browser-field + .mh-browser-field { border-top: 0.5px solid var(--dsw-alias-border-l2); }
492
+ .mh-browser-head { display: flex; align-items: center; gap: 8px; }
493
+ .mh-browser-label-group { display: flex; align-items: center; gap: 4px; flex: 1; min-width: 0; }
494
+ .mh-browser-label { flex: 0 1 auto; min-width: 0; font-size: 13px; font-weight: 500; line-height: 1.5; color: var(--dsw-alias-label-primary); }
495
+ .mh-browser-help-button { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 24px; height: 24px; padding: 0; border: 0; border-radius: var(--dsw-radius-sm); background: none; color: var(--dsw-alias-label-tertiary); cursor: pointer; }
496
+ .mh-browser-help-button[aria-expanded='true'] { background: var(--dsw-alias-bg-layer-4); color: var(--dsw-alias-label-secondary); }
497
+ .mh-browser-help { padding: 10px 0 0; font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); white-space: pre-wrap; }
498
+ .mh-browser-badges { display: inline-flex; align-items: center; gap: 8px; }
499
+ .mh-browser-tag { display: inline-flex; align-items: center; border-radius: 999px; padding: 1px 8px; font-size: 11px; line-height: 17px; font-weight: 500; white-space: nowrap; background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-secondary); }
500
+ .mh-browser-reset { border: none; background: none; padding: 0; font: inherit; font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-secondary); cursor: pointer; }
501
+ .mh-browser-reset:disabled { cursor: default; }
502
+ .mh-browser-input { box-sizing: border-box; width: 100%; height: 34px; padding: 0 12px; border: 0.5px solid var(--dsw-alias-border-l4); border-radius: var(--dsw-radius-md); background: var(--dsw-alias-bg-layer-3); font: inherit; font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-primary); }
503
+ .mh-browser-input:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
504
+ .mh-browser-input:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
505
+ .mh-browser-input[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
506
+ .mh-browser-choice { position: relative; display: flex; }
507
+ .mh-browser-choice .mh-browser-input { appearance: none; padding-right: 32px; cursor: pointer; }
508
+ .mh-browser-choice .mh-browser-input:disabled { cursor: default; }
509
+ .mh-browser-choice option { background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-primary); }
510
+ .mh-browser-chevron { position: absolute; top: 50%; right: 12px; transform: translateY(-50%); color: var(--dsw-alias-label-tertiary); pointer-events: none; }
511
+ .mh-browser-invalid { margin: 0; font-size: 12px; line-height: 1.5; color: var(--dsw-alias-state-error-primary); }
512
+ .mh-browser-hint { margin: 0; font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-tertiary); }
513
+ .mh-browser-hint a { color: var(--dsw-alias-label-secondary); }
514
+ @media (hover: hover) {
515
+ .mh-browser-help-button:hover { background: var(--dsw-alias-bg-layer-4); color: var(--dsw-alias-label-secondary); }
516
+ .mh-browser-reset:hover:not(:disabled) { color: var(--dsw-alias-label-primary); }
517
+ }
518
+ `;
519
+ /** The page frame: read-only notice, the fields, and Save. Unmounting drops every draft. */
520
+ function SettingsFrame(props) {
521
+ const discard = (0, react.useRef)(props.onDiscard);
522
+ discard.current = props.onDiscard;
523
+ (0, react.useEffect)(() => () => {
524
+ discard.current();
525
+ }, []);
526
+ const { state, labels } = props;
527
+ if (!state.available) return (0, react.createElement)("div", null, (0, react.createElement)("style", null, STYLE), (0, react.createElement)("p", {
528
+ className: "mh-browser-status",
529
+ role: "status"
530
+ }, labels.unavailable));
531
+ const blocked = !state.dirty || state.invalid || state.saving || !state.writable;
532
+ return (0, react.createElement)("div", { className: "mh-browser-form" }, (0, react.createElement)("style", null, STYLE), state.writable ? null : (0, react.createElement)("p", {
533
+ className: "mh-browser-status",
534
+ role: "status"
535
+ }, labels.readOnly), props.children, (0, react.createElement)("div", { className: "mh-browser-footer" }, (0, react.createElement)("button", {
536
+ type: "button",
537
+ className: "mh-browser-save",
538
+ disabled: blocked,
539
+ onClick: props.onSave
540
+ }, state.saving ? labels.saving : labels.save), state.failed ? (0, react.createElement)("div", { className: "mh-browser-failure" }, (0, react.createElement)("p", {
541
+ className: "mh-browser-failed",
542
+ role: "status"
543
+ }, labels.saveFailed), (0, react.createElement)("button", {
544
+ type: "button",
545
+ className: "mh-browser-reset",
546
+ onClick: props.onDiscard
547
+ }, labels.discard)) : null));
548
+ }
549
+ function TextField(props) {
550
+ return (0, react.createElement)(Field, {
551
+ ...props,
552
+ control: (describedBy) => (0, react.createElement)("input", {
553
+ id: props.id,
554
+ className: "mh-browser-input",
555
+ type: "text",
556
+ ...props.invalid ? { "aria-invalid": true } : {},
557
+ "aria-describedby": describedBy,
558
+ value: props.text,
559
+ placeholder: props.placeholder ?? "",
560
+ disabled: props.disabled,
561
+ onChange: (event) => {
562
+ props.onEdit(event.target.value);
563
+ }
564
+ })
565
+ });
566
+ }
567
+ function ChoiceField(props) {
568
+ return (0, react.createElement)(Field, {
569
+ ...props,
570
+ control: (describedBy) => (0, react.createElement)("div", { className: "mh-browser-choice" }, (0, react.createElement)("select", {
571
+ id: props.id,
572
+ className: "mh-browser-input",
573
+ "aria-describedby": describedBy,
574
+ value: props.text,
575
+ disabled: props.disabled,
576
+ onChange: (event) => {
577
+ props.onEdit(event.target.value);
578
+ }
579
+ }, props.options.map((option) => (0, react.createElement)("option", {
580
+ key: option.value,
581
+ value: option.value
582
+ }, option.label))), (0, react.createElement)(Chevron))
583
+ });
584
+ }
585
+ function Field(props) {
586
+ const [helpOpen, setHelpOpen] = (0, react.useState)(false);
587
+ const helpId = `${props.id}-help`;
588
+ const messageId = `${props.id}-message`;
589
+ const invalid = props.invalid && props.invalidLabel !== void 0;
590
+ const hasMessage = invalid || props.hint !== void 0 && props.hint !== "";
591
+ const describedBy = [hasMessage ? messageId : "", helpOpen ? helpId : ""].filter((id) => id !== "").join(" ");
592
+ return (0, react.createElement)("div", { className: "mh-browser-field" }, (0, react.createElement)("div", { className: "mh-browser-head" }, (0, react.createElement)("div", { className: "mh-browser-label-group" }, (0, react.createElement)("label", {
593
+ className: "mh-browser-label",
594
+ htmlFor: props.id
595
+ }, props.label), props.help === void 0 ? null : (0, react.createElement)("button", {
596
+ type: "button",
597
+ className: "mh-browser-help-button",
598
+ "aria-label": props.help.label,
599
+ "aria-expanded": helpOpen,
600
+ "aria-controls": helpId,
601
+ onClick: () => {
602
+ setHelpOpen(!helpOpen);
603
+ }
604
+ }, (0, react.createElement)(InfoIcon))), props.overridden ? (0, react.createElement)("span", { className: "mh-browser-badges" }, (0, react.createElement)("span", { className: "mh-browser-tag" }, props.overriddenLabel), (0, react.createElement)("button", {
605
+ type: "button",
606
+ className: "mh-browser-reset",
607
+ disabled: props.disabled,
608
+ onClick: props.onReset
609
+ }, props.resetLabel)) : null), props.control(describedBy === "" ? void 0 : describedBy), hasMessage ? (0, react.createElement)("p", {
610
+ id: messageId,
611
+ className: invalid ? "mh-browser-invalid" : "mh-browser-hint"
612
+ }, invalid ? props.invalidLabel : props.hint) : null, props.help !== void 0 && helpOpen ? (0, react.createElement)("div", {
613
+ id: helpId,
614
+ className: "mh-browser-help",
615
+ role: "region",
616
+ "aria-label": props.help.label
617
+ }, props.help.content) : null);
618
+ }
619
+ function InfoIcon() {
620
+ return (0, react.createElement)("svg", {
621
+ width: 12,
622
+ height: 12,
623
+ viewBox: "0 0 12 12",
624
+ fill: "none",
625
+ stroke: "currentColor",
626
+ strokeWidth: 1,
627
+ strokeLinecap: "round",
628
+ "aria-hidden": true
629
+ }, (0, react.createElement)("circle", {
630
+ cx: 6,
631
+ cy: 6,
632
+ r: 5
633
+ }), (0, react.createElement)("path", { d: "M6 5.5v3" }), (0, react.createElement)("circle", {
634
+ cx: 6,
635
+ cy: 3.75,
636
+ r: .5,
637
+ fill: "currentColor",
638
+ stroke: "none"
639
+ }));
640
+ }
641
+ function Chevron() {
642
+ return (0, react.createElement)("svg", {
643
+ className: "mh-browser-chevron",
644
+ width: 12,
645
+ height: 12,
646
+ viewBox: "0 0 12 12",
647
+ fill: "none",
648
+ stroke: "currentColor",
649
+ strokeWidth: 1.25,
650
+ strokeLinecap: "round",
651
+ strokeLinejoin: "round",
652
+ "aria-hidden": true
653
+ }, (0, react.createElement)("path", { d: "M3 4.5l3 3 3-3" }));
654
+ }
655
+ function isRecord(value) {
656
+ return typeof value === "object" && value !== null;
657
+ }
658
+ //#endregion
659
+ //#region src/client/settings.ts
660
+ /** Loader row id, and the settings namespace for that row. */
661
+ const ENTRY_ID = "mh-browser";
662
+ /** `plugins.row.config` key: the bundle package, then the row id. */
663
+ const ROW_KEY = "@mind-harness/mh-bundle#mh-browser";
664
+ const BROWSER_CHOICES = [
665
+ "auto",
666
+ "chrome",
667
+ "msedge",
668
+ "chromium"
669
+ ];
670
+ /** Google's download page. Edge is not offered: its developer-mode prompt can disable the agent extension. */
671
+ const CHROME_INSTALL = "https://www.google.com/chrome/";
672
+ const LOADING = { status: "loading" };
673
+ /** Empty text clears the field. Anything else must be absolute, or start with `~`. */
674
+ function parseExecutable(text) {
675
+ const trimmed = text.trim();
676
+ if (trimmed === "") return { kind: "clear" };
677
+ if (trimmed === "~" || trimmed.startsWith("~/") || trimmed.startsWith("~\\")) return {
678
+ kind: "set",
679
+ value: trimmed
680
+ };
681
+ if (trimmed.startsWith("/") || /^[A-Za-z]:[\\/]/.test(trimmed)) return {
682
+ kind: "set",
683
+ value: trimmed
684
+ };
685
+ }
686
+ /** Placeholder for an empty path field: the path an empty value would use. */
687
+ function pathPlaceholder(lookup, browser, t) {
688
+ if (lookup.status === "loading") return t("pathSearching");
689
+ if (lookup.status === "found") return lookup.path;
690
+ if (lookup.status === "missing" && browser !== "auto") return t("pathMissing", { name: t(browser) });
691
+ }
692
+ /** The sentence under the browser menu: which browser automatic selection is using. */
693
+ function browserStatus(lookup, browser, t) {
694
+ const restart = t("browserHint");
695
+ if (browser !== "auto") return restart;
696
+ if (lookup.status === "found" && lookup.browser !== void 0) return `${t("browserUsing", { name: t(lookup.browser) })} ${restart}`;
697
+ if (lookup.status === "missing") return `${t("browserNone")} ${restart}`;
698
+ return restart;
699
+ }
700
+ /** The full path, or every place that was tried. Absent while looking or after a failed lookup. */
701
+ function pathHelpText(lookup, t) {
702
+ if (lookup.status === "found") return lookup.path;
703
+ if (lookup.status !== "missing") return void 0;
704
+ return lookup.tried.map((item) => `${item.path} (${reasonText(item.reason, t)})`).join("\n");
705
+ }
706
+ /**
707
+ * One browser name's lookup, kept until the settings page unmounts.
708
+ * A newer lookup cancels the one still running.
709
+ */
710
+ var ExecutableLookup = class {
711
+ locate;
712
+ cache = /* @__PURE__ */ new Map();
713
+ inflight;
714
+ controller;
715
+ listeners = /* @__PURE__ */ new Set();
716
+ constructor(locate) {
717
+ this.locate = locate;
718
+ }
719
+ subscribe = (listener) => {
720
+ this.listeners.add(listener);
721
+ return () => {
722
+ this.listeners.delete(listener);
723
+ };
724
+ };
725
+ /** Cached answer for `browser`, or loading when that answer is not in yet. */
726
+ view(browser) {
727
+ return this.cache.get(browser) ?? LOADING;
728
+ }
729
+ watch(browser) {
730
+ if (this.cache.has(browser) || this.inflight === browser) return;
731
+ this.controller?.abort();
732
+ const controller = new AbortController();
733
+ this.controller = controller;
734
+ this.inflight = browser;
735
+ this.locate(browser, controller.signal).then((result) => {
736
+ if (controller.signal.aborted) return;
737
+ if (this.inflight === browser) this.inflight = void 0;
738
+ this.cache.set(browser, viewOf(result));
739
+ this.publish();
740
+ }, () => {
741
+ if (controller.signal.aborted) return;
742
+ if (this.inflight === browser) this.inflight = void 0;
743
+ this.cache.set(browser, { status: "failed" });
744
+ this.publish();
745
+ });
746
+ }
747
+ dispose() {
748
+ this.controller?.abort();
749
+ this.controller = void 0;
750
+ this.inflight = void 0;
751
+ this.cache.clear();
752
+ }
753
+ publish() {
754
+ for (const listener of this.listeners) listener();
755
+ }
756
+ };
757
+ /** Staged browser choice and path. A save the host accepts restarts the browser when it is idle. */
758
+ var BrowserForm = class {
759
+ form;
760
+ lookup;
761
+ state;
762
+ constructor(scope, locate) {
763
+ this.form = new StagedForm(scope, [browserField(), executableField()]);
764
+ this.lookup = new ExecutableLookup(locate);
765
+ this.state = this.form.bind(() => ({
766
+ ...this.form.shell(),
767
+ browser: this.form.field("browser"),
768
+ executablePath: this.form.field("executablePath")
769
+ }));
770
+ }
771
+ edit(field, text) {
772
+ this.form.edit(field, text);
773
+ }
774
+ reset(field) {
775
+ this.form.reset(field);
776
+ }
777
+ save() {
778
+ this.form.save();
779
+ }
780
+ discard() {
781
+ this.form.discard();
782
+ }
783
+ dispose() {
784
+ this.lookup.dispose();
785
+ this.form.dispose();
786
+ }
787
+ };
788
+ function registerSettings(ctx) {
789
+ const form = new BrowserForm(ctx.configForms.get(ENTRY_ID), (browser, signal) => call("locate-for", {
790
+ method: "POST",
791
+ body: { browser }
792
+ }, signal));
793
+ ctx.effect(() => () => {
794
+ form.dispose();
795
+ });
796
+ ctx.slots.inject("plugins.row.config", () => ctx.slots.register({
797
+ name: "plugins.row.config",
798
+ key: ROW_KEY,
799
+ locale: NS
800
+ }, (props) => {
801
+ if (props.view === "summary") return null;
802
+ return (0, react.createElement)(BrowserSettings, {
803
+ t: props.t,
804
+ form
805
+ });
806
+ }));
807
+ }
808
+ function BrowserSettings(props) {
809
+ const state = (0, react.useSyncExternalStore)(props.form.state.subscribe, props.form.state.getSnapshot, props.form.state.getSnapshot);
810
+ const browser = choiceOf(state.browser.text);
811
+ const lookup = (0, react.useSyncExternalStore)(props.form.lookup.subscribe, () => props.form.lookup.view(browser), () => props.form.lookup.view(browser));
812
+ (0, react.useEffect)(() => {
813
+ props.form.lookup.watch(browser);
814
+ }, [browser, props.form]);
815
+ (0, react.useEffect)(() => {
816
+ const lookup = props.form.lookup;
817
+ return () => {
818
+ lookup.dispose();
819
+ };
820
+ }, [props.form]);
821
+ const disabled = !state.writable || state.saving;
822
+ const help = helpOf(lookup, browser, props.t);
823
+ const placeholder = state.executablePath.text === "" ? pathPlaceholder(lookup, browser, props.t) : void 0;
824
+ const offerChrome = browser === "auto" && lookup.status === "missing";
825
+ return (0, react.createElement)(SettingsFrame, {
826
+ labels: formLabels(props.t),
827
+ state,
828
+ onSave: () => props.form.save(),
829
+ onDiscard: () => props.form.discard()
830
+ }, (0, react.createElement)(ChoiceField, {
831
+ id: "mh-browser-name",
832
+ label: props.t("browserLabel"),
833
+ hint: browserStatus(lookup, browser, props.t),
834
+ ...state.browser,
835
+ text: browser,
836
+ options: BROWSER_CHOICES.map((name) => ({
837
+ value: name,
838
+ label: props.t(name)
839
+ })),
840
+ overriddenLabel: props.t("overridden"),
841
+ resetLabel: props.t("reset"),
842
+ disabled,
843
+ onEdit: (value) => props.form.edit("browser", value),
844
+ onReset: () => props.form.reset("browser")
845
+ }), offerChrome ? (0, react.createElement)("p", { className: "mh-browser-hint" }, (0, react.createElement)("a", {
846
+ href: CHROME_INSTALL,
847
+ target: "_blank",
848
+ rel: "noopener noreferrer"
849
+ }, props.t("installChrome"))) : null, (0, react.createElement)(TextField, {
850
+ id: "mh-browser-path",
851
+ label: props.t("pathLabel"),
852
+ hint: props.t("pathHint"),
853
+ ...state.executablePath,
854
+ overriddenLabel: props.t("overridden"),
855
+ resetLabel: props.t("reset"),
856
+ invalidLabel: props.t("pathInvalid"),
857
+ disabled,
858
+ ...placeholder === void 0 ? {} : { placeholder },
859
+ ...help === void 0 ? {} : { help },
860
+ onEdit: (text) => props.form.edit("executablePath", text),
861
+ onReset: () => props.form.reset("executablePath")
862
+ }));
863
+ }
864
+ function browserField() {
865
+ return {
866
+ field: "browser",
867
+ format: (value) => typeof value === "string" && isChoice(value) ? value : "auto",
868
+ parse: (text) => isChoice(text) ? {
869
+ kind: "set",
870
+ value: text
871
+ } : void 0
872
+ };
873
+ }
874
+ function executableField() {
875
+ return {
876
+ ...textField("executablePath"),
877
+ parse: parseExecutable
878
+ };
879
+ }
880
+ function formLabels(t) {
881
+ return {
882
+ unavailable: t("settingsUnavailable"),
883
+ readOnly: t("settingsReadOnly"),
884
+ saveFailed: t("settingsSaveFailed"),
885
+ discard: t("discard"),
886
+ save: t("save"),
887
+ saving: t("saving")
888
+ };
889
+ }
890
+ function choiceOf(text) {
891
+ return isChoice(text) ? text : "auto";
892
+ }
893
+ function isChoice(value) {
894
+ return BROWSER_CHOICES.includes(value);
895
+ }
896
+ function viewOf(result) {
897
+ if (!result.ok) return { status: "failed" };
898
+ if ("found" in result.value) return {
899
+ status: "found",
900
+ path: result.value.found,
901
+ ...result.value.browser !== void 0 ? { browser: result.value.browser } : {}
902
+ };
903
+ return {
904
+ status: "missing",
905
+ tried: result.value.missing
906
+ };
907
+ }
908
+ function helpOf(lookup, browser, t) {
909
+ const text = pathHelpText(lookup, t);
910
+ if (text === void 0) return void 0;
911
+ if (browser === "auto" && lookup.status === "missing") return {
912
+ label: t("pathHelp"),
913
+ content: (0, react.createElement)("span", null, text, " ", (0, react.createElement)("a", {
914
+ href: CHROME_INSTALL,
915
+ target: "_blank",
916
+ rel: "noopener noreferrer"
917
+ }, t("installChrome")))
918
+ };
919
+ return {
920
+ label: t("pathHelp"),
921
+ content: text
922
+ };
923
+ }
924
+ function reasonText(reason, t) {
925
+ if (reason === "does not exist") return t("pathDoesNotExist");
926
+ if (reason === "is a directory") return t("pathIsDirectory");
927
+ const code = /^permission denied: (\S+)$/.exec(reason)?.[1];
928
+ if (code !== void 0) return t("pathDenied", { code });
929
+ return reason;
930
+ }
931
+ //#endregion
397
932
  //#region src/client/client.ts
398
933
  /**
399
934
  * Cordis services this client reads. `dsh.client.inject` is package load
400
- * order, not this list. `remote.mhBrowser` is absent until `$mount`.
935
+ * order, not this list.
401
936
  */
402
937
  const inject = [
403
938
  "slots",
404
939
  "locale",
405
- "remote",
406
- "uiConversation"
940
+ "uiConversation",
941
+ "configForms"
407
942
  ];
408
- const namespaceInject = [...inject, "remote.mhBrowser"];
943
+ /** Host calls used by the handover row and the settings lookup. */
944
+ function browserApi() {
945
+ return {
946
+ revealHandover: (sessionId, signal) => call("reveal-handover", {
947
+ method: "POST",
948
+ body: { sessionId }
949
+ }, signal),
950
+ locateFor: (browser, signal) => call("locate-for", {
951
+ method: "POST",
952
+ body: { browser }
953
+ }, signal)
954
+ };
955
+ }
409
956
  async function apply(ctx) {
410
- const unmount = await ctx.remote.$mount(TYPERT_REMOTE);
411
957
  let release = () => void 0;
412
- await ctx.inject(namespaceInject, (inner) => {
958
+ await ctx.inject([...inject], (inner) => {
413
959
  release = start(inner);
960
+ registerSettings(inner);
414
961
  });
415
962
  return async () => {
416
963
  release();
417
- await unmount();
418
964
  };
419
965
  }
420
966
  /** Register the handover row. The tool call itself keeps the generic tool row. */
421
- function start(ctx) {
967
+ function start(ctx, api = browserApi()) {
422
968
  ctx.effect(() => ctx.locale.register(NS, {
423
969
  zh,
424
970
  en
425
971
  }));
426
972
  ctx.effect(() => ctx.uiConversation.events.register(handoverRowDefinition));
427
- const remote = ctx.remote.mhBrowser;
428
973
  ctx.slots.inject("conversation.chat.node", () => ctx.slots.register({
429
974
  name: "conversation.chat.node",
430
975
  key: HANDOVER_KIND,
431
976
  locale: NS
432
977
  }, (props) => (0, react.createElement)(HandoverRow, {
433
978
  sessionId: props.sessionId,
434
- reason: props.node.data.reason,
435
979
  t: props.t,
436
- reveal: (sessionId, signal) => remote.revealHandover(sessionId, signal)
980
+ reveal: (sessionId, signal) => api.revealHandover(sessionId, signal)
437
981
  })));
438
982
  return () => void 0;
439
983
  }
440
984
  //#endregion
441
985
  exports.apply = apply;
986
+ exports.browserApi = browserApi;
442
987
  exports.inject = inject;
443
988
  exports.start = start;
444
989
  return module.exports;