@mind-harness/mh-browser 0.2.5 → 0.2.6
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/extension/manifest.json +1 -1
- package/lib/client.cjs +772 -51
- package/lib/index.js +449 -49
- package/lib/typert.host.js +88 -4
- package/lib/typert.remote-client.d.ts +4 -0
- package/lib/typert.remote-client.js +45 -1
- package/lib/types/client.d.ts +15 -1
- package/lib/types/config.d.ts +23 -10
- package/lib/types/index.d.ts +23 -2
- package/lib/types/locales.d.ts +22 -0
- package/lib/types/location.d.ts +12 -0
- package/lib/types/location.js +3 -0
- package/lib/types/service/browser.d.ts +11 -5
- package/lib/types/service/control.d.ts +5 -0
- package/lib/types/service/locate.d.ts +65 -0
- package/lib/types/service/service.d.ts +34 -3
- package/lib/types/service/test-hooks.d.ts +3 -0
- package/lib/types/settings-form.d.ts +142 -0
- package/lib/types/settings.d.ts +121 -0
- package/lib/types/typeprobe.d.ts +49 -0
- package/package.json +10 -2
package/lib/client.cjs
CHANGED
|
@@ -163,6 +163,17 @@ window.__ModuleLoader__.load({
|
|
|
163
163
|
};
|
|
164
164
|
//#endregion
|
|
165
165
|
//#region lib/typert.remote-client.js
|
|
166
|
+
let _mind_harness_mh_browser_mhBrowser_locateFor_parameter_0$schema$value;
|
|
167
|
+
const _mind_harness_mh_browser_mhBrowser_locateFor_parameter_0$schema = () => _mind_harness_mh_browser_mhBrowser_locateFor_parameter_0$schema$value ??= zod.z.union([
|
|
168
|
+
zod.z.literal("chrome"),
|
|
169
|
+
zod.z.literal("msedge"),
|
|
170
|
+
zod.z.literal("chromium")
|
|
171
|
+
]);
|
|
172
|
+
let _mind_harness_mh_browser_mhBrowser_locateFor_result$schema$value;
|
|
173
|
+
const _mind_harness_mh_browser_mhBrowser_locateFor_result$schema = () => _mind_harness_mh_browser_mhBrowser_locateFor_result$schema$value ??= zod.z.union([zod.z.object({ "found": zod.z.string() }), zod.z.object({ "missing": zod.z.array(zod.z.object({
|
|
174
|
+
"path": zod.z.string(),
|
|
175
|
+
"reason": zod.z.string()
|
|
176
|
+
})) })]);
|
|
166
177
|
let _mind_harness_mh_browser_mhBrowser_revealHandover_parameter_0$schema$value;
|
|
167
178
|
const _mind_harness_mh_browser_mhBrowser_revealHandover_parameter_0$schema = () => _mind_harness_mh_browser_mhBrowser_revealHandover_parameter_0$schema$value ??= zod.z.string();
|
|
168
179
|
let _mind_harness_mh_browser_mhBrowser_revealHandover_result$schema$value;
|
|
@@ -171,60 +182,115 @@ window.__ModuleLoader__.load({
|
|
|
171
182
|
zod.z.literal("not-handing-over"),
|
|
172
183
|
zod.z.literal("no-tab")
|
|
173
184
|
]) });
|
|
185
|
+
let _mind_harness_mh_browser_mhBrowser_locateFor_parameter_0_strict$schema$value;
|
|
186
|
+
const _mind_harness_mh_browser_mhBrowser_locateFor_parameter_0_strict$schema = () => _mind_harness_mh_browser_mhBrowser_locateFor_parameter_0_strict$schema$value ??= zod.z.union([
|
|
187
|
+
zod.z.literal("chrome"),
|
|
188
|
+
zod.z.literal("msedge"),
|
|
189
|
+
zod.z.literal("chromium")
|
|
190
|
+
]);
|
|
174
191
|
let _mind_harness_mh_browser_mhBrowser_revealHandover_parameter_0_strict$schema$value;
|
|
175
192
|
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
193
|
const TYPERT_REMOTE = createTypertRemoteInfo({
|
|
177
194
|
package: "@mind-harness/mh-browser",
|
|
178
|
-
methods: {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
195
|
+
methods: {
|
|
196
|
+
"mhBrowser/locateFor": {
|
|
197
|
+
service: "mhBrowser",
|
|
198
|
+
description: "Where `browser` would be launched from when no path is configured. The configured path is not consulted: the answer is what an empty path uses.",
|
|
199
|
+
invocation: { kind: "direct" },
|
|
200
|
+
parameters: [{
|
|
201
|
+
name: "browser",
|
|
202
|
+
source: "json",
|
|
203
|
+
codec: {
|
|
204
|
+
mode: "strict",
|
|
205
|
+
typeSymbol: "@mind-harness/mh-browser/location#BrowserName",
|
|
206
|
+
create: _mind_harness_mh_browser_mhBrowser_locateFor_parameter_0$schema,
|
|
207
|
+
jsonSchema: {
|
|
208
|
+
"type": "string",
|
|
209
|
+
"enum": [
|
|
210
|
+
"chrome",
|
|
211
|
+
"msedge",
|
|
212
|
+
"chromium"
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
strictCodec: {
|
|
217
|
+
mode: "strict",
|
|
218
|
+
typeSymbol: "@mind-harness/mh-browser/location#BrowserName",
|
|
219
|
+
create: _mind_harness_mh_browser_mhBrowser_locateFor_parameter_0_strict$schema,
|
|
220
|
+
jsonSchema: {
|
|
221
|
+
"type": "string",
|
|
222
|
+
"enum": [
|
|
223
|
+
"chrome",
|
|
224
|
+
"msedge",
|
|
225
|
+
"chromium"
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}],
|
|
230
|
+
cancellation: { parameter: "signal" },
|
|
231
|
+
result: {
|
|
186
232
|
mode: "strict",
|
|
187
|
-
typeSymbol: "@mind-harness/mh-browser#
|
|
188
|
-
create:
|
|
189
|
-
|
|
233
|
+
typeSymbol: "@mind-harness/mh-browser/location#BrowserLocation",
|
|
234
|
+
create: _mind_harness_mh_browser_mhBrowser_locateFor_result$schema,
|
|
235
|
+
jsonSchemaError: "typert tool schema: selected union branches overlap or have no required discriminator"
|
|
190
236
|
},
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
237
|
+
sourceLocation: {
|
|
238
|
+
"file": "packages/mh-browser/src/service/service.ts",
|
|
239
|
+
"line": 474,
|
|
240
|
+
"column": 9
|
|
215
241
|
}
|
|
216
242
|
},
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
"
|
|
220
|
-
"
|
|
243
|
+
"mhBrowser/revealHandover": {
|
|
244
|
+
service: "mhBrowser",
|
|
245
|
+
description: "The person asked to see the page this Session handed over.",
|
|
246
|
+
invocation: { kind: "direct" },
|
|
247
|
+
parameters: [{
|
|
248
|
+
name: "sessionId",
|
|
249
|
+
source: "json",
|
|
250
|
+
codec: {
|
|
251
|
+
mode: "strict",
|
|
252
|
+
typeSymbol: "@mind-harness/mh-browser#mhBrowser/revealHandover:sessionId",
|
|
253
|
+
create: _mind_harness_mh_browser_mhBrowser_revealHandover_parameter_0$schema,
|
|
254
|
+
jsonSchema: { "type": "string" }
|
|
255
|
+
},
|
|
256
|
+
strictCodec: {
|
|
257
|
+
mode: "strict",
|
|
258
|
+
typeSymbol: "@mind-harness/mh-browser#mhBrowser/revealHandover:sessionId",
|
|
259
|
+
create: _mind_harness_mh_browser_mhBrowser_revealHandover_parameter_0_strict$schema,
|
|
260
|
+
jsonSchema: { "type": "string" }
|
|
261
|
+
}
|
|
262
|
+
}],
|
|
263
|
+
cancellation: { parameter: "signal" },
|
|
264
|
+
result: {
|
|
265
|
+
mode: "strict",
|
|
266
|
+
typeSymbol: "@mind-harness/mh-browser#mhBrowser/revealHandover:result",
|
|
267
|
+
create: _mind_harness_mh_browser_mhBrowser_revealHandover_result$schema,
|
|
268
|
+
jsonSchema: {
|
|
269
|
+
"type": "object",
|
|
270
|
+
"properties": { "status": {
|
|
271
|
+
"type": "string",
|
|
272
|
+
"enum": [
|
|
273
|
+
"revealed",
|
|
274
|
+
"not-handing-over",
|
|
275
|
+
"no-tab"
|
|
276
|
+
]
|
|
277
|
+
} },
|
|
278
|
+
"required": ["status"],
|
|
279
|
+
"additionalProperties": true
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
sourceLocation: {
|
|
283
|
+
"file": "packages/mh-browser/src/service/service.ts",
|
|
284
|
+
"line": 734,
|
|
285
|
+
"column": 9
|
|
286
|
+
}
|
|
221
287
|
}
|
|
222
|
-
}
|
|
288
|
+
}
|
|
223
289
|
}, zod.z);
|
|
224
290
|
//#endregion
|
|
225
291
|
//#region src/client/card.ts
|
|
226
292
|
/** Host theme tokens only; the rules unmount with the row. */
|
|
227
|
-
const STYLE = `
|
|
293
|
+
const STYLE$1 = `
|
|
228
294
|
.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
295
|
.mh-br-line { margin: 0; }
|
|
230
296
|
.mh-br-status { margin: 0; color: var(--dsw-alias-label-secondary); }
|
|
@@ -233,7 +299,7 @@ window.__ModuleLoader__.load({
|
|
|
233
299
|
/** The open handover: the reason and the button that raises its window. */
|
|
234
300
|
function HandoverRow(props) {
|
|
235
301
|
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", {
|
|
302
|
+
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", { reason: props.reason })), (0, react.createElement)("button", {
|
|
237
303
|
type: "button",
|
|
238
304
|
className: "mh-br-button",
|
|
239
305
|
onClick: () => {
|
|
@@ -260,14 +326,58 @@ window.__ModuleLoader__.load({
|
|
|
260
326
|
reveal: "去浏览器",
|
|
261
327
|
notHandingOver: "这次交接已经结束",
|
|
262
328
|
noTab: "没有可以去的页面",
|
|
263
|
-
failed: "没能切到浏览器"
|
|
329
|
+
failed: "没能切到浏览器",
|
|
330
|
+
browserLabel: "浏览器",
|
|
331
|
+
browserHint: "保存后,浏览器空闲时会关掉再按这个选择打开。已经打开的标签页会关掉。",
|
|
332
|
+
pathLabel: "浏览器路径",
|
|
333
|
+
pathHint: "留空就用自动找到的。可以填绝对路径,也可以用 ~。保存后同样在空闲时重启。",
|
|
334
|
+
pathInvalid: "需要绝对路径,或以 ~ 开头",
|
|
335
|
+
pathSearching: "正在查找…",
|
|
336
|
+
pathMissing: "未找到{name},请填写路径",
|
|
337
|
+
pathHelp: "说明",
|
|
338
|
+
pathDoesNotExist: "不存在",
|
|
339
|
+
pathIsDirectory: "是目录",
|
|
340
|
+
pathDenied: "没有权限({code})",
|
|
341
|
+
chrome: "Google Chrome",
|
|
342
|
+
msedge: "Microsoft Edge",
|
|
343
|
+
chromium: "Chromium",
|
|
344
|
+
overridden: "已覆盖",
|
|
345
|
+
reset: "恢复",
|
|
346
|
+
save: "保存",
|
|
347
|
+
saving: "保存中",
|
|
348
|
+
settingsUnavailable: "这个配置现在不可用",
|
|
349
|
+
settingsReadOnly: "这次启动的配置不能在页面上改",
|
|
350
|
+
settingsSaveFailed: "没有保存:设置可能在别处改过,或这个值不被接受。修改还在,检查后可以再保存",
|
|
351
|
+
discard: "放弃修改"
|
|
264
352
|
};
|
|
265
353
|
const en = {
|
|
266
354
|
waiting: "The browser is yours: {reason}",
|
|
267
355
|
reveal: "Go to the browser",
|
|
268
356
|
notHandingOver: "This handover has already ended",
|
|
269
357
|
noTab: "There is no page to open",
|
|
270
|
-
failed: "Could not switch to the browser"
|
|
358
|
+
failed: "Could not switch to the browser",
|
|
359
|
+
browserLabel: "Browser",
|
|
360
|
+
browserHint: "After saving, the browser restarts once it is idle. Tabs that are open will close.",
|
|
361
|
+
pathLabel: "Browser path",
|
|
362
|
+
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.",
|
|
363
|
+
pathInvalid: "Use an absolute path, or one starting with ~",
|
|
364
|
+
pathSearching: "Looking…",
|
|
365
|
+
pathMissing: "{name} was not found. Enter its path",
|
|
366
|
+
pathHelp: "Details",
|
|
367
|
+
pathDoesNotExist: "does not exist",
|
|
368
|
+
pathIsDirectory: "is a directory",
|
|
369
|
+
pathDenied: "permission denied ({code})",
|
|
370
|
+
chrome: "Google Chrome",
|
|
371
|
+
msedge: "Microsoft Edge",
|
|
372
|
+
chromium: "Chromium",
|
|
373
|
+
overridden: "Overridden",
|
|
374
|
+
reset: "Reset",
|
|
375
|
+
save: "Save",
|
|
376
|
+
saving: "Saving",
|
|
377
|
+
settingsUnavailable: "This setting is not available",
|
|
378
|
+
settingsReadOnly: "This launch keeps the setting read-only",
|
|
379
|
+
settingsSaveFailed: "Not saved: the setting may have changed elsewhere, or the value was not accepted. Your changes are kept; check them and save again.",
|
|
380
|
+
discard: "Discard changes"
|
|
271
381
|
};
|
|
272
382
|
//#endregion
|
|
273
383
|
//#region src/client/row.ts
|
|
@@ -278,25 +388,25 @@ window.__ModuleLoader__.load({
|
|
|
278
388
|
*/
|
|
279
389
|
const HANDOVER_KIND = "browser-handover";
|
|
280
390
|
const HANDOVER_TOOL = "browser_handover";
|
|
281
|
-
function isRecord(value) {
|
|
391
|
+
function isRecord$1(value) {
|
|
282
392
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
283
393
|
}
|
|
284
394
|
function turnOf(data) {
|
|
285
|
-
if (!isRecord(data) || typeof data.turn !== "number") return void 0;
|
|
395
|
+
if (!isRecord$1(data) || typeof data.turn !== "number") return void 0;
|
|
286
396
|
return data.turn;
|
|
287
397
|
}
|
|
288
398
|
function reasonOf(value) {
|
|
289
399
|
if (typeof value !== "string") return "";
|
|
290
400
|
try {
|
|
291
401
|
const parsed = JSON.parse(value);
|
|
292
|
-
if (!isRecord(parsed) || typeof parsed.reason !== "string") return "";
|
|
402
|
+
if (!isRecord$1(parsed) || typeof parsed.reason !== "string") return "";
|
|
293
403
|
return parsed.reason;
|
|
294
404
|
} catch {
|
|
295
405
|
return "";
|
|
296
406
|
}
|
|
297
407
|
}
|
|
298
408
|
function callOf(data) {
|
|
299
|
-
if (!isRecord(data) || typeof data.callId !== "string" || typeof data.name !== "string") return void 0;
|
|
409
|
+
if (!isRecord$1(data) || typeof data.callId !== "string" || typeof data.name !== "string") return void 0;
|
|
300
410
|
return {
|
|
301
411
|
callId: data.callId,
|
|
302
412
|
name: data.name,
|
|
@@ -305,9 +415,9 @@ window.__ModuleLoader__.load({
|
|
|
305
415
|
}
|
|
306
416
|
/** A result names its call on `message.source` and again on `message.toolCallId`. */
|
|
307
417
|
function callIdOf(data) {
|
|
308
|
-
if (!isRecord(data) || !isRecord(data.message)) return void 0;
|
|
418
|
+
if (!isRecord$1(data) || !isRecord$1(data.message)) return void 0;
|
|
309
419
|
const message = data.message;
|
|
310
|
-
if (isRecord(message.source) && typeof message.source.callId === "string" && message.source.callId !== "") return message.source.callId;
|
|
420
|
+
if (isRecord$1(message.source) && typeof message.source.callId === "string" && message.source.callId !== "") return message.source.callId;
|
|
311
421
|
if (typeof message.toolCallId === "string" && message.toolCallId !== "") return message.toolCallId;
|
|
312
422
|
}
|
|
313
423
|
function hidden(current) {
|
|
@@ -394,6 +504,615 @@ window.__ModuleLoader__.load({
|
|
|
394
504
|
}
|
|
395
505
|
};
|
|
396
506
|
//#endregion
|
|
507
|
+
//#region src/client/settings-form.ts
|
|
508
|
+
/** Drafts over one entry's fields, written together on save. */
|
|
509
|
+
var StagedForm = class {
|
|
510
|
+
scope;
|
|
511
|
+
specs;
|
|
512
|
+
staged = /* @__PURE__ */ new Map();
|
|
513
|
+
listeners = /* @__PURE__ */ new Set();
|
|
514
|
+
baseline;
|
|
515
|
+
saving = false;
|
|
516
|
+
failed = false;
|
|
517
|
+
unsubscribe;
|
|
518
|
+
constructor(scope, specs) {
|
|
519
|
+
this.scope = scope;
|
|
520
|
+
this.specs = new Map(specs.map((spec) => [spec.field, spec]));
|
|
521
|
+
this.unsubscribe = scope.subscribe(() => {
|
|
522
|
+
this.publish();
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
/** A store over `project`, rebuilt whenever the entry or a draft changes. */
|
|
526
|
+
bind(project) {
|
|
527
|
+
let current = project();
|
|
528
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
529
|
+
this.listeners.add(() => {
|
|
530
|
+
current = project();
|
|
531
|
+
for (const listener of subscribers) listener();
|
|
532
|
+
});
|
|
533
|
+
return {
|
|
534
|
+
getSnapshot: () => current,
|
|
535
|
+
subscribe: (listener) => {
|
|
536
|
+
subscribers.add(listener);
|
|
537
|
+
return () => {
|
|
538
|
+
subscribers.delete(listener);
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
shell() {
|
|
544
|
+
const snapshot = this.scope.getSnapshot();
|
|
545
|
+
const plan = this.plan();
|
|
546
|
+
return {
|
|
547
|
+
available: snapshot.status === "ready",
|
|
548
|
+
writable: snapshot.writable,
|
|
549
|
+
dirty: plan.length > 0,
|
|
550
|
+
invalid: plan.some((item) => item.op === void 0),
|
|
551
|
+
saving: this.saving,
|
|
552
|
+
failed: this.failed
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
field(field) {
|
|
556
|
+
const spec = this.spec(field);
|
|
557
|
+
const staged = this.staged.get(field);
|
|
558
|
+
if (staged === void 0) return {
|
|
559
|
+
text: spec.format(this.read("value", field)),
|
|
560
|
+
overridden: this.stored(field),
|
|
561
|
+
invalid: false
|
|
562
|
+
};
|
|
563
|
+
const write = staged.clear ? { kind: "clear" } : spec.parse(staged.text);
|
|
564
|
+
return {
|
|
565
|
+
text: staged.text,
|
|
566
|
+
overridden: write?.kind === "set",
|
|
567
|
+
invalid: write === void 0
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
edit(field, text) {
|
|
571
|
+
this.stage(field, {
|
|
572
|
+
text,
|
|
573
|
+
clear: false
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
/** Stage removing the field from the user layer. The draft shows what it falls back to. */
|
|
577
|
+
reset(field) {
|
|
578
|
+
this.stage(field, {
|
|
579
|
+
text: this.spec(field).format(this.read("base", field)),
|
|
580
|
+
clear: true
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
discard() {
|
|
584
|
+
if (this.staged.size === 0 && !this.failed) return;
|
|
585
|
+
this.staged.clear();
|
|
586
|
+
this.baseline = void 0;
|
|
587
|
+
this.failed = false;
|
|
588
|
+
this.publish();
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Write every staged edit. A save the Host refuses keeps the drafts, and the
|
|
592
|
+
* next save is fenced on what the Host holds now, so trying again can land.
|
|
593
|
+
*/
|
|
594
|
+
async save() {
|
|
595
|
+
const plan = this.plan();
|
|
596
|
+
if (plan.length === 0 || this.saving || !this.scope.getSnapshot().writable) return;
|
|
597
|
+
if (plan.some((item) => item.op === void 0)) return;
|
|
598
|
+
this.saving = true;
|
|
599
|
+
this.failed = false;
|
|
600
|
+
this.publish();
|
|
601
|
+
try {
|
|
602
|
+
const ops = plan.flatMap((item) => item.op === void 0 ? [] : [item.op]);
|
|
603
|
+
const landed = ops.length === 0 || await this.scope.mutate(ops, this.baseline?.revision);
|
|
604
|
+
if (landed) {
|
|
605
|
+
this.staged.clear();
|
|
606
|
+
this.baseline = void 0;
|
|
607
|
+
} else this.baseline = this.scope.getSnapshot();
|
|
608
|
+
this.failed = !landed;
|
|
609
|
+
} catch {
|
|
610
|
+
this.baseline = this.scope.getSnapshot();
|
|
611
|
+
this.failed = true;
|
|
612
|
+
} finally {
|
|
613
|
+
this.saving = false;
|
|
614
|
+
this.publish();
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
dispose() {
|
|
618
|
+
this.unsubscribe();
|
|
619
|
+
this.listeners.clear();
|
|
620
|
+
}
|
|
621
|
+
plan() {
|
|
622
|
+
const plan = [];
|
|
623
|
+
for (const [field, staged] of this.staged) {
|
|
624
|
+
const spec = this.spec(field);
|
|
625
|
+
if (staged.clear) {
|
|
626
|
+
if (this.stored(field)) plan.push({
|
|
627
|
+
field,
|
|
628
|
+
op: {
|
|
629
|
+
op: "unset",
|
|
630
|
+
path: [field]
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
if (staged.text === spec.format(this.read("value", field))) continue;
|
|
636
|
+
const write = spec.parse(staged.text);
|
|
637
|
+
if (write === void 0) plan.push({ field });
|
|
638
|
+
else if (write.kind === "clear") plan.push({
|
|
639
|
+
field,
|
|
640
|
+
op: {
|
|
641
|
+
op: "unset",
|
|
642
|
+
path: [field]
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
else plan.push({
|
|
646
|
+
field,
|
|
647
|
+
op: {
|
|
648
|
+
op: "set",
|
|
649
|
+
path: [field],
|
|
650
|
+
value: write.value
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
return plan;
|
|
655
|
+
}
|
|
656
|
+
stage(field, edit) {
|
|
657
|
+
this.baseline ??= this.scope.getSnapshot();
|
|
658
|
+
this.staged.set(field, edit);
|
|
659
|
+
this.failed = false;
|
|
660
|
+
this.publish();
|
|
661
|
+
}
|
|
662
|
+
spec(field) {
|
|
663
|
+
const spec = this.specs.get(field);
|
|
664
|
+
if (spec === void 0) throw new Error(`settings form has no field ${field}`);
|
|
665
|
+
return spec;
|
|
666
|
+
}
|
|
667
|
+
read(layer, field) {
|
|
668
|
+
const section = this.scope.getSnapshot()[layer];
|
|
669
|
+
return isRecord(section) ? section[field] : void 0;
|
|
670
|
+
}
|
|
671
|
+
stored(field) {
|
|
672
|
+
const user = this.scope.getSnapshot().user;
|
|
673
|
+
return isRecord(user) && Object.hasOwn(user, field);
|
|
674
|
+
}
|
|
675
|
+
publish() {
|
|
676
|
+
for (const listener of this.listeners) listener();
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
/** Free text. An empty draft clears the field. */
|
|
680
|
+
function textField(field) {
|
|
681
|
+
return {
|
|
682
|
+
field,
|
|
683
|
+
format: (value) => typeof value === "string" ? value : "",
|
|
684
|
+
parse: (text) => {
|
|
685
|
+
const trimmed = text.trim();
|
|
686
|
+
return trimmed === "" ? { kind: "clear" } : {
|
|
687
|
+
kind: "set",
|
|
688
|
+
value: trimmed
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
const STYLE = `
|
|
694
|
+
.mh-browser-form { display: flex; flex-direction: column; }
|
|
695
|
+
.mh-browser-status { margin: 0 0 12px; font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-tertiary); }
|
|
696
|
+
.mh-browser-footer { display: flex; align-items: flex-start; gap: 12px; padding-top: 16px; }
|
|
697
|
+
.mh-browser-save { flex: none; }
|
|
698
|
+
.mh-browser-failure { flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 2px; padding-top: 5px; }
|
|
699
|
+
.mh-browser-failed { margin: 0; font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-error); }
|
|
700
|
+
.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); }
|
|
701
|
+
.mh-browser-save:disabled { opacity: 0.4; cursor: default; }
|
|
702
|
+
.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; }
|
|
703
|
+
.mh-browser-field { display: flex; flex-direction: column; gap: 6px; padding: 12px 0; }
|
|
704
|
+
.mh-browser-field + .mh-browser-field { border-top: 0.5px solid var(--dsw-alias-border-l2); }
|
|
705
|
+
.mh-browser-head { display: flex; align-items: center; gap: 8px; }
|
|
706
|
+
.mh-browser-label-group { display: flex; align-items: center; gap: 4px; flex: 1; min-width: 0; }
|
|
707
|
+
.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); }
|
|
708
|
+
.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; }
|
|
709
|
+
.mh-browser-help-button[aria-expanded='true'] { background: var(--dsw-alias-bg-layer-4); color: var(--dsw-alias-label-secondary); }
|
|
710
|
+
.mh-browser-help { padding: 10px 0 0; font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); white-space: pre-wrap; }
|
|
711
|
+
.mh-browser-badges { display: inline-flex; align-items: center; gap: 8px; }
|
|
712
|
+
.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); }
|
|
713
|
+
.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; }
|
|
714
|
+
.mh-browser-reset:disabled { cursor: default; }
|
|
715
|
+
.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); }
|
|
716
|
+
.mh-browser-input:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
|
|
717
|
+
.mh-browser-input:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
|
|
718
|
+
.mh-browser-input[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
|
|
719
|
+
.mh-browser-choice { position: relative; display: flex; }
|
|
720
|
+
.mh-browser-choice .mh-browser-input { appearance: none; padding-right: 32px; cursor: pointer; }
|
|
721
|
+
.mh-browser-choice .mh-browser-input:disabled { cursor: default; }
|
|
722
|
+
.mh-browser-choice option { background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-primary); }
|
|
723
|
+
.mh-browser-chevron { position: absolute; top: 50%; right: 12px; transform: translateY(-50%); color: var(--dsw-alias-label-tertiary); pointer-events: none; }
|
|
724
|
+
.mh-browser-invalid { margin: 0; font-size: 12px; line-height: 1.5; color: var(--dsw-alias-state-error-primary); }
|
|
725
|
+
.mh-browser-hint { margin: 0; font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-tertiary); }
|
|
726
|
+
@media (hover: hover) {
|
|
727
|
+
.mh-browser-help-button:hover { background: var(--dsw-alias-bg-layer-4); color: var(--dsw-alias-label-secondary); }
|
|
728
|
+
.mh-browser-reset:hover:not(:disabled) { color: var(--dsw-alias-label-primary); }
|
|
729
|
+
}
|
|
730
|
+
`;
|
|
731
|
+
/** The page frame: read-only notice, the fields, and Save. Unmounting drops every draft. */
|
|
732
|
+
function SettingsFrame(props) {
|
|
733
|
+
const discard = (0, react.useRef)(props.onDiscard);
|
|
734
|
+
discard.current = props.onDiscard;
|
|
735
|
+
(0, react.useEffect)(() => () => {
|
|
736
|
+
discard.current();
|
|
737
|
+
}, []);
|
|
738
|
+
const { state, labels } = props;
|
|
739
|
+
if (!state.available) return (0, react.createElement)("div", null, (0, react.createElement)("style", null, STYLE), (0, react.createElement)("p", {
|
|
740
|
+
className: "mh-browser-status",
|
|
741
|
+
role: "status"
|
|
742
|
+
}, labels.unavailable));
|
|
743
|
+
const blocked = !state.dirty || state.invalid || state.saving || !state.writable;
|
|
744
|
+
return (0, react.createElement)("div", { className: "mh-browser-form" }, (0, react.createElement)("style", null, STYLE), state.writable ? null : (0, react.createElement)("p", {
|
|
745
|
+
className: "mh-browser-status",
|
|
746
|
+
role: "status"
|
|
747
|
+
}, labels.readOnly), props.children, (0, react.createElement)("div", { className: "mh-browser-footer" }, (0, react.createElement)("button", {
|
|
748
|
+
type: "button",
|
|
749
|
+
className: "mh-browser-save",
|
|
750
|
+
disabled: blocked,
|
|
751
|
+
onClick: props.onSave
|
|
752
|
+
}, state.saving ? labels.saving : labels.save), state.failed ? (0, react.createElement)("div", { className: "mh-browser-failure" }, (0, react.createElement)("p", {
|
|
753
|
+
className: "mh-browser-failed",
|
|
754
|
+
role: "status"
|
|
755
|
+
}, labels.saveFailed), (0, react.createElement)("button", {
|
|
756
|
+
type: "button",
|
|
757
|
+
className: "mh-browser-reset",
|
|
758
|
+
onClick: props.onDiscard
|
|
759
|
+
}, labels.discard)) : null));
|
|
760
|
+
}
|
|
761
|
+
function TextField(props) {
|
|
762
|
+
return (0, react.createElement)(Field, {
|
|
763
|
+
...props,
|
|
764
|
+
control: (describedBy) => (0, react.createElement)("input", {
|
|
765
|
+
id: props.id,
|
|
766
|
+
className: "mh-browser-input",
|
|
767
|
+
type: "text",
|
|
768
|
+
...props.invalid ? { "aria-invalid": true } : {},
|
|
769
|
+
"aria-describedby": describedBy,
|
|
770
|
+
value: props.text,
|
|
771
|
+
placeholder: props.placeholder ?? "",
|
|
772
|
+
disabled: props.disabled,
|
|
773
|
+
onChange: (event) => {
|
|
774
|
+
props.onEdit(event.target.value);
|
|
775
|
+
}
|
|
776
|
+
})
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
function ChoiceField(props) {
|
|
780
|
+
return (0, react.createElement)(Field, {
|
|
781
|
+
...props,
|
|
782
|
+
control: (describedBy) => (0, react.createElement)("div", { className: "mh-browser-choice" }, (0, react.createElement)("select", {
|
|
783
|
+
id: props.id,
|
|
784
|
+
className: "mh-browser-input",
|
|
785
|
+
"aria-describedby": describedBy,
|
|
786
|
+
value: props.text,
|
|
787
|
+
disabled: props.disabled,
|
|
788
|
+
onChange: (event) => {
|
|
789
|
+
props.onEdit(event.target.value);
|
|
790
|
+
}
|
|
791
|
+
}, props.options.map((option) => (0, react.createElement)("option", {
|
|
792
|
+
key: option.value,
|
|
793
|
+
value: option.value
|
|
794
|
+
}, option.label))), (0, react.createElement)(Chevron))
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
function Field(props) {
|
|
798
|
+
const [helpOpen, setHelpOpen] = (0, react.useState)(false);
|
|
799
|
+
const helpId = `${props.id}-help`;
|
|
800
|
+
const messageId = `${props.id}-message`;
|
|
801
|
+
const invalid = props.invalid && props.invalidLabel !== void 0;
|
|
802
|
+
const hasMessage = invalid || props.hint !== void 0 && props.hint !== "";
|
|
803
|
+
const describedBy = [hasMessage ? messageId : "", helpOpen ? helpId : ""].filter((id) => id !== "").join(" ");
|
|
804
|
+
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", {
|
|
805
|
+
className: "mh-browser-label",
|
|
806
|
+
htmlFor: props.id
|
|
807
|
+
}, props.label), props.help === void 0 ? null : (0, react.createElement)("button", {
|
|
808
|
+
type: "button",
|
|
809
|
+
className: "mh-browser-help-button",
|
|
810
|
+
"aria-label": props.help.label,
|
|
811
|
+
"aria-expanded": helpOpen,
|
|
812
|
+
"aria-controls": helpId,
|
|
813
|
+
onClick: () => {
|
|
814
|
+
setHelpOpen(!helpOpen);
|
|
815
|
+
}
|
|
816
|
+
}, (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", {
|
|
817
|
+
type: "button",
|
|
818
|
+
className: "mh-browser-reset",
|
|
819
|
+
disabled: props.disabled,
|
|
820
|
+
onClick: props.onReset
|
|
821
|
+
}, props.resetLabel)) : null), props.control(describedBy === "" ? void 0 : describedBy), hasMessage ? (0, react.createElement)("p", {
|
|
822
|
+
id: messageId,
|
|
823
|
+
className: invalid ? "mh-browser-invalid" : "mh-browser-hint"
|
|
824
|
+
}, invalid ? props.invalidLabel : props.hint) : null, props.help !== void 0 && helpOpen ? (0, react.createElement)("div", {
|
|
825
|
+
id: helpId,
|
|
826
|
+
className: "mh-browser-help",
|
|
827
|
+
role: "region",
|
|
828
|
+
"aria-label": props.help.label
|
|
829
|
+
}, props.help.content) : null);
|
|
830
|
+
}
|
|
831
|
+
function InfoIcon() {
|
|
832
|
+
return (0, react.createElement)("svg", {
|
|
833
|
+
width: 12,
|
|
834
|
+
height: 12,
|
|
835
|
+
viewBox: "0 0 12 12",
|
|
836
|
+
fill: "none",
|
|
837
|
+
stroke: "currentColor",
|
|
838
|
+
strokeWidth: 1,
|
|
839
|
+
strokeLinecap: "round",
|
|
840
|
+
"aria-hidden": true
|
|
841
|
+
}, (0, react.createElement)("circle", {
|
|
842
|
+
cx: 6,
|
|
843
|
+
cy: 6,
|
|
844
|
+
r: 5
|
|
845
|
+
}), (0, react.createElement)("path", { d: "M6 5.5v3" }), (0, react.createElement)("circle", {
|
|
846
|
+
cx: 6,
|
|
847
|
+
cy: 3.75,
|
|
848
|
+
r: .5,
|
|
849
|
+
fill: "currentColor",
|
|
850
|
+
stroke: "none"
|
|
851
|
+
}));
|
|
852
|
+
}
|
|
853
|
+
function Chevron() {
|
|
854
|
+
return (0, react.createElement)("svg", {
|
|
855
|
+
className: "mh-browser-chevron",
|
|
856
|
+
width: 12,
|
|
857
|
+
height: 12,
|
|
858
|
+
viewBox: "0 0 12 12",
|
|
859
|
+
fill: "none",
|
|
860
|
+
stroke: "currentColor",
|
|
861
|
+
strokeWidth: 1.25,
|
|
862
|
+
strokeLinecap: "round",
|
|
863
|
+
strokeLinejoin: "round",
|
|
864
|
+
"aria-hidden": true
|
|
865
|
+
}, (0, react.createElement)("path", { d: "M3 4.5l3 3 3-3" }));
|
|
866
|
+
}
|
|
867
|
+
function isRecord(value) {
|
|
868
|
+
return typeof value === "object" && value !== null;
|
|
869
|
+
}
|
|
870
|
+
//#endregion
|
|
871
|
+
//#region src/client/settings.ts
|
|
872
|
+
/** Loader row id, and the settings namespace for that row. */
|
|
873
|
+
const ENTRY_ID = "mh-browser";
|
|
874
|
+
/** `plugins.row.config` key: the bundle package, then the row id. */
|
|
875
|
+
const ROW_KEY = "@mind-harness/mh-bundle#mh-browser";
|
|
876
|
+
const BROWSER_CHOICES = [
|
|
877
|
+
"chrome",
|
|
878
|
+
"msedge",
|
|
879
|
+
"chromium"
|
|
880
|
+
];
|
|
881
|
+
const LOADING = { status: "loading" };
|
|
882
|
+
/** Empty text clears the field. Anything else must be absolute, or start with `~`. */
|
|
883
|
+
function parseExecutable(text) {
|
|
884
|
+
const trimmed = text.trim();
|
|
885
|
+
if (trimmed === "") return { kind: "clear" };
|
|
886
|
+
if (trimmed === "~" || trimmed.startsWith("~/") || trimmed.startsWith("~\\")) return {
|
|
887
|
+
kind: "set",
|
|
888
|
+
value: trimmed
|
|
889
|
+
};
|
|
890
|
+
if (trimmed.startsWith("/") || /^[A-Za-z]:[\\/]/.test(trimmed)) return {
|
|
891
|
+
kind: "set",
|
|
892
|
+
value: trimmed
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
/** Placeholder for an empty path field: the path an empty value would use. */
|
|
896
|
+
function pathPlaceholder(lookup, browser, t) {
|
|
897
|
+
if (lookup.status === "loading") return t("pathSearching");
|
|
898
|
+
if (lookup.status === "found") return lookup.path;
|
|
899
|
+
if (lookup.status === "missing") return t("pathMissing", { name: t(browser) });
|
|
900
|
+
}
|
|
901
|
+
/** The full path, or every place that was tried. Absent while looking or after a failed lookup. */
|
|
902
|
+
function pathHelpText(lookup, t) {
|
|
903
|
+
if (lookup.status === "found") return lookup.path;
|
|
904
|
+
if (lookup.status !== "missing") return void 0;
|
|
905
|
+
return lookup.tried.map((item) => `${item.path} (${reasonText(item.reason, t)})`).join("\n");
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* One browser name's lookup, kept until the settings page unmounts.
|
|
909
|
+
* A newer lookup cancels the one still running.
|
|
910
|
+
*/
|
|
911
|
+
var ExecutableLookup = class {
|
|
912
|
+
locate;
|
|
913
|
+
cache = /* @__PURE__ */ new Map();
|
|
914
|
+
inflight;
|
|
915
|
+
controller;
|
|
916
|
+
listeners = /* @__PURE__ */ new Set();
|
|
917
|
+
constructor(locate) {
|
|
918
|
+
this.locate = locate;
|
|
919
|
+
}
|
|
920
|
+
subscribe = (listener) => {
|
|
921
|
+
this.listeners.add(listener);
|
|
922
|
+
return () => {
|
|
923
|
+
this.listeners.delete(listener);
|
|
924
|
+
};
|
|
925
|
+
};
|
|
926
|
+
/** Cached answer for `browser`, or loading when that answer is not in yet. */
|
|
927
|
+
view(browser) {
|
|
928
|
+
return this.cache.get(browser) ?? LOADING;
|
|
929
|
+
}
|
|
930
|
+
watch(browser) {
|
|
931
|
+
if (this.cache.has(browser) || this.inflight === browser) return;
|
|
932
|
+
this.controller?.abort();
|
|
933
|
+
const controller = new AbortController();
|
|
934
|
+
this.controller = controller;
|
|
935
|
+
this.inflight = browser;
|
|
936
|
+
this.locate(browser, controller.signal).then((result) => {
|
|
937
|
+
if (controller.signal.aborted) return;
|
|
938
|
+
if (this.inflight === browser) this.inflight = void 0;
|
|
939
|
+
this.cache.set(browser, viewOf(result));
|
|
940
|
+
this.publish();
|
|
941
|
+
}, () => {
|
|
942
|
+
if (controller.signal.aborted) return;
|
|
943
|
+
if (this.inflight === browser) this.inflight = void 0;
|
|
944
|
+
this.cache.set(browser, { status: "failed" });
|
|
945
|
+
this.publish();
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
dispose() {
|
|
949
|
+
this.controller?.abort();
|
|
950
|
+
this.controller = void 0;
|
|
951
|
+
this.inflight = void 0;
|
|
952
|
+
this.cache.clear();
|
|
953
|
+
}
|
|
954
|
+
publish() {
|
|
955
|
+
for (const listener of this.listeners) listener();
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
/** Staged browser choice and path. A save the host accepts restarts the browser when it is idle. */
|
|
959
|
+
var BrowserForm = class {
|
|
960
|
+
form;
|
|
961
|
+
lookup;
|
|
962
|
+
state;
|
|
963
|
+
constructor(scope, locate) {
|
|
964
|
+
this.form = new StagedForm(scope, [browserField(), executableField()]);
|
|
965
|
+
this.lookup = new ExecutableLookup(locate);
|
|
966
|
+
this.state = this.form.bind(() => ({
|
|
967
|
+
...this.form.shell(),
|
|
968
|
+
browser: this.form.field("browser"),
|
|
969
|
+
executablePath: this.form.field("executablePath")
|
|
970
|
+
}));
|
|
971
|
+
}
|
|
972
|
+
edit(field, text) {
|
|
973
|
+
this.form.edit(field, text);
|
|
974
|
+
}
|
|
975
|
+
reset(field) {
|
|
976
|
+
this.form.reset(field);
|
|
977
|
+
}
|
|
978
|
+
save() {
|
|
979
|
+
this.form.save();
|
|
980
|
+
}
|
|
981
|
+
discard() {
|
|
982
|
+
this.form.discard();
|
|
983
|
+
}
|
|
984
|
+
dispose() {
|
|
985
|
+
this.lookup.dispose();
|
|
986
|
+
this.form.dispose();
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
function registerSettings(ctx) {
|
|
990
|
+
const form = new BrowserForm(ctx.configForms.get(ENTRY_ID), (browser, signal) => ctx.remote.mhBrowser.locateFor(browser, signal));
|
|
991
|
+
ctx.effect(() => () => {
|
|
992
|
+
form.dispose();
|
|
993
|
+
});
|
|
994
|
+
ctx.slots.inject("plugins.row.config", () => ctx.slots.register({
|
|
995
|
+
name: "plugins.row.config",
|
|
996
|
+
key: ROW_KEY,
|
|
997
|
+
locale: NS
|
|
998
|
+
}, (props) => {
|
|
999
|
+
if (props.view === "summary") return null;
|
|
1000
|
+
return (0, react.createElement)(BrowserSettings, {
|
|
1001
|
+
t: props.t,
|
|
1002
|
+
form
|
|
1003
|
+
});
|
|
1004
|
+
}));
|
|
1005
|
+
}
|
|
1006
|
+
function BrowserSettings(props) {
|
|
1007
|
+
const state = (0, react.useSyncExternalStore)(props.form.state.subscribe, props.form.state.getSnapshot, props.form.state.getSnapshot);
|
|
1008
|
+
const browser = choiceOf(state.browser.text);
|
|
1009
|
+
const lookup = (0, react.useSyncExternalStore)(props.form.lookup.subscribe, () => props.form.lookup.view(browser), () => props.form.lookup.view(browser));
|
|
1010
|
+
(0, react.useEffect)(() => {
|
|
1011
|
+
props.form.lookup.watch(browser);
|
|
1012
|
+
}, [browser, props.form]);
|
|
1013
|
+
(0, react.useEffect)(() => {
|
|
1014
|
+
const lookup = props.form.lookup;
|
|
1015
|
+
return () => {
|
|
1016
|
+
lookup.dispose();
|
|
1017
|
+
};
|
|
1018
|
+
}, [props.form]);
|
|
1019
|
+
const disabled = !state.writable || state.saving;
|
|
1020
|
+
const help = helpOf(lookup, props.t);
|
|
1021
|
+
const placeholder = state.executablePath.text === "" ? pathPlaceholder(lookup, browser, props.t) : void 0;
|
|
1022
|
+
return (0, react.createElement)(SettingsFrame, {
|
|
1023
|
+
labels: formLabels(props.t),
|
|
1024
|
+
state,
|
|
1025
|
+
onSave: () => props.form.save(),
|
|
1026
|
+
onDiscard: () => props.form.discard()
|
|
1027
|
+
}, (0, react.createElement)(ChoiceField, {
|
|
1028
|
+
id: "mh-browser-name",
|
|
1029
|
+
label: props.t("browserLabel"),
|
|
1030
|
+
hint: props.t("browserHint"),
|
|
1031
|
+
...state.browser,
|
|
1032
|
+
text: browser,
|
|
1033
|
+
options: BROWSER_CHOICES.map((name) => ({
|
|
1034
|
+
value: name,
|
|
1035
|
+
label: props.t(name)
|
|
1036
|
+
})),
|
|
1037
|
+
overriddenLabel: props.t("overridden"),
|
|
1038
|
+
resetLabel: props.t("reset"),
|
|
1039
|
+
disabled,
|
|
1040
|
+
onEdit: (value) => props.form.edit("browser", value),
|
|
1041
|
+
onReset: () => props.form.reset("browser")
|
|
1042
|
+
}), (0, react.createElement)(TextField, {
|
|
1043
|
+
id: "mh-browser-path",
|
|
1044
|
+
label: props.t("pathLabel"),
|
|
1045
|
+
hint: props.t("pathHint"),
|
|
1046
|
+
...state.executablePath,
|
|
1047
|
+
overriddenLabel: props.t("overridden"),
|
|
1048
|
+
resetLabel: props.t("reset"),
|
|
1049
|
+
invalidLabel: props.t("pathInvalid"),
|
|
1050
|
+
disabled,
|
|
1051
|
+
...placeholder === void 0 ? {} : { placeholder },
|
|
1052
|
+
...help === void 0 ? {} : { help },
|
|
1053
|
+
onEdit: (text) => props.form.edit("executablePath", text),
|
|
1054
|
+
onReset: () => props.form.reset("executablePath")
|
|
1055
|
+
}));
|
|
1056
|
+
}
|
|
1057
|
+
function browserField() {
|
|
1058
|
+
return {
|
|
1059
|
+
field: "browser",
|
|
1060
|
+
format: (value) => typeof value === "string" && isChoice(value) ? value : "chrome",
|
|
1061
|
+
parse: (text) => isChoice(text) ? {
|
|
1062
|
+
kind: "set",
|
|
1063
|
+
value: text
|
|
1064
|
+
} : void 0
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
function executableField() {
|
|
1068
|
+
return {
|
|
1069
|
+
...textField("executablePath"),
|
|
1070
|
+
parse: parseExecutable
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
function formLabels(t) {
|
|
1074
|
+
return {
|
|
1075
|
+
unavailable: t("settingsUnavailable"),
|
|
1076
|
+
readOnly: t("settingsReadOnly"),
|
|
1077
|
+
saveFailed: t("settingsSaveFailed"),
|
|
1078
|
+
discard: t("discard"),
|
|
1079
|
+
save: t("save"),
|
|
1080
|
+
saving: t("saving")
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
function choiceOf(text) {
|
|
1084
|
+
return isChoice(text) ? text : "chrome";
|
|
1085
|
+
}
|
|
1086
|
+
function isChoice(value) {
|
|
1087
|
+
return BROWSER_CHOICES.includes(value);
|
|
1088
|
+
}
|
|
1089
|
+
function viewOf(result) {
|
|
1090
|
+
if (!result.ok) return { status: "failed" };
|
|
1091
|
+
if ("found" in result.value) return {
|
|
1092
|
+
status: "found",
|
|
1093
|
+
path: result.value.found
|
|
1094
|
+
};
|
|
1095
|
+
return {
|
|
1096
|
+
status: "missing",
|
|
1097
|
+
tried: result.value.missing
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
function helpOf(lookup, t) {
|
|
1101
|
+
const text = pathHelpText(lookup, t);
|
|
1102
|
+
if (text === void 0) return void 0;
|
|
1103
|
+
return {
|
|
1104
|
+
label: t("pathHelp"),
|
|
1105
|
+
content: text
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
function reasonText(reason, t) {
|
|
1109
|
+
if (reason === "does not exist") return t("pathDoesNotExist");
|
|
1110
|
+
if (reason === "is a directory") return t("pathIsDirectory");
|
|
1111
|
+
const code = /^permission denied: (\S+)$/.exec(reason)?.[1];
|
|
1112
|
+
if (code !== void 0) return t("pathDenied", { code });
|
|
1113
|
+
return reason;
|
|
1114
|
+
}
|
|
1115
|
+
//#endregion
|
|
397
1116
|
//#region src/client/client.ts
|
|
398
1117
|
/**
|
|
399
1118
|
* Cordis services this client reads. `dsh.client.inject` is package load
|
|
@@ -403,7 +1122,8 @@ window.__ModuleLoader__.load({
|
|
|
403
1122
|
"slots",
|
|
404
1123
|
"locale",
|
|
405
1124
|
"remote",
|
|
406
|
-
"uiConversation"
|
|
1125
|
+
"uiConversation",
|
|
1126
|
+
"configForms"
|
|
407
1127
|
];
|
|
408
1128
|
const namespaceInject = [...inject, "remote.mhBrowser"];
|
|
409
1129
|
async function apply(ctx) {
|
|
@@ -411,6 +1131,7 @@ window.__ModuleLoader__.load({
|
|
|
411
1131
|
let release = () => void 0;
|
|
412
1132
|
await ctx.inject(namespaceInject, (inner) => {
|
|
413
1133
|
release = start(inner);
|
|
1134
|
+
registerSettings(inner);
|
|
414
1135
|
});
|
|
415
1136
|
return async () => {
|
|
416
1137
|
release();
|