@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/extension/manifest.json +1 -1
- package/extension/welcome.html +4 -0
- package/extension/welcome.js +10 -0
- package/lib/client.cjs +799 -254
- package/lib/index.js +2314 -1461
- package/lib/types/call.d.ts +23 -0
- package/lib/types/card.d.ts +1 -2
- package/lib/types/client.d.ts +20 -17
- package/lib/types/config.d.ts +25 -10
- package/lib/types/index.d.ts +23 -2
- package/lib/types/locales.d.ts +28 -2
- package/lib/types/location.d.ts +14 -0
- package/lib/types/location.js +3 -0
- package/lib/types/routes.d.ts +43 -0
- package/lib/types/row.d.ts +2 -4
- package/lib/types/service/browser.d.ts +16 -5
- package/lib/types/service/control.d.ts +5 -0
- package/lib/types/service/extension.d.ts +7 -0
- package/lib/types/service/launch-plan.d.ts +30 -0
- package/lib/types/service/locate.d.ts +81 -0
- package/lib/types/service/profile.d.ts +11 -0
- package/lib/types/service/service.d.ts +40 -6
- 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 +124 -0
- package/lib/types/typeprobe.d.ts +49 -0
- package/lib/types/types.d.ts +13 -1
- package/package.json +23 -30
- package/THIRD-PARTY-NOTICES +0 -23
- package/lib/typert.host.d.ts +0 -3
- package/lib/typert.host.js +0 -378
- package/lib/typert.remote-client.d.ts +0 -24
- package/lib/typert.remote-client.js +0 -52
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
|
|
10
|
-
|
|
8
|
+
//#region src/client/call.ts
|
|
9
|
+
const PACKAGE = "mh-browser";
|
|
11
10
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
}
|
|
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
|
-
/**
|
|
227
|
-
|
|
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 {
|
|
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
|
-
/**
|
|
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"
|
|
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: "
|
|
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: "
|
|
267
|
-
reveal: "
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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.
|
|
935
|
+
* order, not this list.
|
|
401
936
|
*/
|
|
402
937
|
const inject = [
|
|
403
938
|
"slots",
|
|
404
939
|
"locale",
|
|
405
|
-
"
|
|
406
|
-
"
|
|
940
|
+
"uiConversation",
|
|
941
|
+
"configForms"
|
|
407
942
|
];
|
|
408
|
-
|
|
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(
|
|
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) =>
|
|
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;
|