gesso-framework 0.6.6 → 0.6.8
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/CHANGELOG.md +18 -0
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +3 -3
- package/dist/{app-BvlIO1G9.js → app-bMsNOs1H.js} +2 -2
- package/dist/{app-BvlIO1G9.js.map → app-bMsNOs1H.js.map} +1 -1
- package/dist/{index-B8PU7Cf0.d.ts → index-D5bW2GUt.d.ts} +16 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +10 -8
- package/dist/index.js.map +1 -1
- package/dist/{persisted-pix1fS1D.js → persisted-i6TIWX8R.js} +5 -952
- package/dist/persisted-i6TIWX8R.js.map +1 -0
- package/dist/remote/index.d.ts +198 -0
- package/dist/remote/index.js +301 -0
- package/dist/remote/index.js.map +1 -0
- package/dist/{remote-xxij8zXe.js → remote-gRdwYo-q.js} +19 -4
- package/dist/remote-gRdwYo-q.js.map +1 -0
- package/dist/serveChannels-Cs22-p8X.js +952 -0
- package/dist/serveChannels-Cs22-p8X.js.map +1 -0
- package/dist/{ui-U39HjNFA.d.ts → ui-Bs6ywsAA.d.ts} +3 -2
- package/dist/{webmcp-CJVFvAHe.js → webmcp-F3OOy73w.js} +2 -2
- package/dist/{webmcp-CJVFvAHe.js.map → webmcp-F3OOy73w.js.map} +1 -1
- package/dist/worker/index.d.ts +2 -2
- package/dist/worker/index.js +2 -1
- package/package.json +6 -2
- package/dist/persisted-pix1fS1D.js.map +0 -1
- package/dist/remote-xxij8zXe.js.map +0 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { i as isHubMessage, n as serveChannels, o as isPortHandshake } from "../serveChannels-Cs22-p8X.js";
|
|
2
|
+
//#region src/remote/frames.ts
|
|
3
|
+
/**
|
|
4
|
+
* The whole wire format, which is deliberately smaller than the
|
|
5
|
+
* channel protocol it carries.
|
|
6
|
+
*
|
|
7
|
+
* Three frames in each direction. `open` says a named channel wants a
|
|
8
|
+
* stream, `data` carries one channel message, `close` ends a stream.
|
|
9
|
+
* Nothing here knows what a channel is, what a patch is, or what a
|
|
10
|
+
* command is: a frame's `body` is a string this module produced by
|
|
11
|
+
* serializing a value it never looked inside. That restraint is the
|
|
12
|
+
* rule the bridge keeps, and it is what keeps the page's main thread
|
|
13
|
+
* a transport rather than a router.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* How much of a serialized message goes in one frame.
|
|
17
|
+
*
|
|
18
|
+
* Electrobun's transport fails above roughly 8 MiB in a single
|
|
19
|
+
* message, and it fails badly: the main process throws while draining
|
|
20
|
+
* and the sender sees only a timeout, so the real error is in a log
|
|
21
|
+
* nobody is reading. A megabyte leaves eight times the headroom and
|
|
22
|
+
* costs nothing at the measured rates.
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_CHUNK_BYTES = 1048576;
|
|
25
|
+
function isGessoFrame(value) {
|
|
26
|
+
const kind = value?.kind;
|
|
27
|
+
return kind === "open" || kind === "data" || kind === "close" || kind === "control";
|
|
28
|
+
}
|
|
29
|
+
/** Wraps one control message. Never split: these are small by construction. */
|
|
30
|
+
function frameControl(name, payload) {
|
|
31
|
+
return {
|
|
32
|
+
kind: "control",
|
|
33
|
+
name,
|
|
34
|
+
body: JSON.stringify(payload ?? null) ?? "null"
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Serializes one channel message into the frames that carry it.
|
|
39
|
+
*
|
|
40
|
+
* JSON rather than structured clone, because the transport underneath
|
|
41
|
+
* is JSON either way: `Electroview.createTransport` stringifies every
|
|
42
|
+
* message before it encrypts it. That means `undefined` inside a value
|
|
43
|
+
* does not survive, which is true of this transport with or without
|
|
44
|
+
* this module, and which a view key cannot rely on anyway.
|
|
45
|
+
*/
|
|
46
|
+
function frameData(stream, value, chunkBytes = DEFAULT_CHUNK_BYTES) {
|
|
47
|
+
const body = JSON.stringify(value);
|
|
48
|
+
if (body === void 0) throw new Error(`A channel message for stream ${stream} could not be serialized. Only plain data crosses a channel; see requirePlainData.`);
|
|
49
|
+
if (body.length <= chunkBytes) return [{
|
|
50
|
+
kind: "data",
|
|
51
|
+
stream,
|
|
52
|
+
body
|
|
53
|
+
}];
|
|
54
|
+
const parts = Math.ceil(body.length / chunkBytes);
|
|
55
|
+
const frames = [];
|
|
56
|
+
for (let part = 0; part < parts; part++) frames.push({
|
|
57
|
+
kind: "data",
|
|
58
|
+
stream,
|
|
59
|
+
body: body.slice(part * chunkBytes, (part + 1) * chunkBytes),
|
|
60
|
+
part,
|
|
61
|
+
parts
|
|
62
|
+
});
|
|
63
|
+
return frames;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Puts split messages back together.
|
|
67
|
+
*
|
|
68
|
+
* The transport delivers in order (`Electroview` dispatches through a
|
|
69
|
+
* promise tail that preserves frame order), so a part that arrives out
|
|
70
|
+
* of turn is a bug rather than a race, and it says so instead of
|
|
71
|
+
* quietly assembling a corrupt message.
|
|
72
|
+
*/
|
|
73
|
+
var FrameAssembler = class {
|
|
74
|
+
partial = /* @__PURE__ */ new Map();
|
|
75
|
+
/**
|
|
76
|
+
* Returns the value a `data` frame completes, or `undefined` while
|
|
77
|
+
* more parts are still to come.
|
|
78
|
+
*/
|
|
79
|
+
take(frame) {
|
|
80
|
+
if (frame.parts === void 0) return JSON.parse(frame.body);
|
|
81
|
+
const held = this.partial.get(frame.stream) ?? {
|
|
82
|
+
parts: frame.parts,
|
|
83
|
+
chunks: []
|
|
84
|
+
};
|
|
85
|
+
const expected = held.chunks.length;
|
|
86
|
+
if (frame.part !== expected) {
|
|
87
|
+
this.partial.delete(frame.stream);
|
|
88
|
+
throw new Error(`Stream ${frame.stream} received part ${String(frame.part)} when part ${expected} was next. Frames are expected in order; a gap means the transport reordered or dropped one.`);
|
|
89
|
+
}
|
|
90
|
+
held.chunks.push(frame.body);
|
|
91
|
+
if (held.chunks.length < held.parts) {
|
|
92
|
+
this.partial.set(frame.stream, held);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.partial.delete(frame.stream);
|
|
96
|
+
return JSON.parse(held.chunks.join(""));
|
|
97
|
+
}
|
|
98
|
+
/** Drops anything half-received for a stream that has closed. */
|
|
99
|
+
forget(stream) {
|
|
100
|
+
this.partial.delete(stream);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/remote/bridge.ts
|
|
105
|
+
function createRemoteBridge(options) {
|
|
106
|
+
const chunkBytes = options.chunkBytes ?? 1048576;
|
|
107
|
+
const assembler = new FrameAssembler();
|
|
108
|
+
/** The render worker's port for each stream this side opened. */
|
|
109
|
+
const ports = /* @__PURE__ */ new Map();
|
|
110
|
+
let hub = null;
|
|
111
|
+
let nextStream = 1;
|
|
112
|
+
let disposed = false;
|
|
113
|
+
const openStream = (name, port) => {
|
|
114
|
+
const stream = nextStream++;
|
|
115
|
+
ports.set(stream, port);
|
|
116
|
+
port.onmessage = (event) => {
|
|
117
|
+
for (const frame of frameData(stream, event.data, chunkBytes)) options.send(frame);
|
|
118
|
+
};
|
|
119
|
+
port.start?.();
|
|
120
|
+
options.send({
|
|
121
|
+
kind: "open",
|
|
122
|
+
stream,
|
|
123
|
+
name
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
endpoint: {
|
|
128
|
+
postMessage(message, transfer) {
|
|
129
|
+
if (disposed) return;
|
|
130
|
+
if (isHubMessage(message)) {
|
|
131
|
+
const port = transfer?.[0];
|
|
132
|
+
if (port === void 0) throw new Error("The shell sent a hub message with no port attached.");
|
|
133
|
+
hub = port;
|
|
134
|
+
hub.onmessage = (event) => {
|
|
135
|
+
if (!isPortHandshake(event.data)) return;
|
|
136
|
+
const handshake = event.ports?.[0];
|
|
137
|
+
if (handshake === void 0) throw new Error(`Port handshake for '${event.data.key}' arrived with no port attached.`);
|
|
138
|
+
openStream(event.data.key, handshake);
|
|
139
|
+
};
|
|
140
|
+
hub.start?.();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
addEventListener() {},
|
|
145
|
+
removeEventListener() {}
|
|
146
|
+
},
|
|
147
|
+
openUrl(url) {
|
|
148
|
+
if (!disposed) options.send(frameControl("openUrl", { url }));
|
|
149
|
+
},
|
|
150
|
+
openRoute(url) {
|
|
151
|
+
if (!disposed) options.send(frameControl("openRoute", { url }));
|
|
152
|
+
},
|
|
153
|
+
receive(frame) {
|
|
154
|
+
if (disposed) return;
|
|
155
|
+
if (frame.kind === "control") {
|
|
156
|
+
if (frame.name === "colorScheme") {
|
|
157
|
+
const payload = JSON.parse(frame.body);
|
|
158
|
+
if (payload.scheme !== void 0) options.onColorScheme?.(payload.scheme);
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (frame.kind === "close") {
|
|
163
|
+
assembler.forget(frame.stream);
|
|
164
|
+
ports.get(frame.stream)?.close();
|
|
165
|
+
ports.delete(frame.stream);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (frame.kind !== "data") throw new Error(`The other process opened stream ${frame.stream}, which only the page may do.`);
|
|
169
|
+
const value = assembler.take(frame);
|
|
170
|
+
if (value === void 0) return;
|
|
171
|
+
const port = ports.get(frame.stream);
|
|
172
|
+
if (port === void 0) return;
|
|
173
|
+
port.postMessage(value);
|
|
174
|
+
},
|
|
175
|
+
dispose() {
|
|
176
|
+
disposed = true;
|
|
177
|
+
for (const [stream, port] of ports) {
|
|
178
|
+
options.send({
|
|
179
|
+
kind: "close",
|
|
180
|
+
stream
|
|
181
|
+
});
|
|
182
|
+
port.close();
|
|
183
|
+
}
|
|
184
|
+
ports.clear();
|
|
185
|
+
if (hub !== null) {
|
|
186
|
+
hub.onmessage = null;
|
|
187
|
+
hub.close();
|
|
188
|
+
hub = null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/remote/host.ts
|
|
195
|
+
/**
|
|
196
|
+
* The other process's half of a remote channel bridge.
|
|
197
|
+
*
|
|
198
|
+
* `serveChannels` publishes an application's channels over a handshake
|
|
199
|
+
* that arrives on a worker's global scope with a `MessagePort`
|
|
200
|
+
* attached. No process boundary outside a worker can carry a port, so
|
|
201
|
+
* this synthesises both: an `open` frame becomes the handshake, and
|
|
202
|
+
* the port it hands over writes back as frames on the same stream.
|
|
203
|
+
*
|
|
204
|
+
* Nothing in `serveChannels`, `provide` or a view model changes for
|
|
205
|
+
* this. That is the point of the seam: the application layer does not
|
|
206
|
+
* learn it is talking to a page in another process, whether that page
|
|
207
|
+
* is a desktop window or a browser tab on a WebSocket.
|
|
208
|
+
*/
|
|
209
|
+
/**
|
|
210
|
+
* Serves an application's channels to one page in another process.
|
|
211
|
+
*
|
|
212
|
+
* const host = serveRemoteChannels(
|
|
213
|
+
* [{ token: Catalogue, source: { view: { … }, commands: { … } } }],
|
|
214
|
+
* { send: frame => socket.send(JSON.stringify(frame)) }
|
|
215
|
+
* );
|
|
216
|
+
* // and, for each message from that page:
|
|
217
|
+
* host.receive(JSON.parse(message));
|
|
218
|
+
*/
|
|
219
|
+
function serveRemoteChannels(channels, options) {
|
|
220
|
+
const chunkBytes = options.chunkBytes ?? 1048576;
|
|
221
|
+
const assembler = new FrameAssembler();
|
|
222
|
+
/** The synthetic port each stream is served through. */
|
|
223
|
+
const ports = /* @__PURE__ */ new Map();
|
|
224
|
+
const host = { onmessage: null };
|
|
225
|
+
const stop = serveChannels(channels, host);
|
|
226
|
+
return {
|
|
227
|
+
setColorScheme(scheme) {
|
|
228
|
+
options.send(frameControl("colorScheme", { scheme }));
|
|
229
|
+
},
|
|
230
|
+
receive(frame) {
|
|
231
|
+
if (frame.kind === "control") {
|
|
232
|
+
if (frame.name === "openUrl" || frame.name === "openRoute") {
|
|
233
|
+
const payload = JSON.parse(frame.body);
|
|
234
|
+
if (typeof payload.url === "string") (frame.name === "openUrl" ? options.onOpenUrl : options.onOpenRoute)?.(payload.url);
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (frame.kind === "open") {
|
|
239
|
+
const port = new StreamPort(frame.stream, options.send, chunkBytes);
|
|
240
|
+
ports.set(frame.stream, port);
|
|
241
|
+
host.onmessage?.({
|
|
242
|
+
data: {
|
|
243
|
+
type: "gesso:port",
|
|
244
|
+
key: frame.name
|
|
245
|
+
},
|
|
246
|
+
ports: [port]
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (frame.kind === "close") {
|
|
251
|
+
assembler.forget(frame.stream);
|
|
252
|
+
ports.delete(frame.stream);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const value = assembler.take(frame);
|
|
256
|
+
if (value === void 0) return;
|
|
257
|
+
const port = ports.get(frame.stream);
|
|
258
|
+
if (port === void 0) throw new Error(`A frame arrived for stream ${frame.stream}, which was never opened. The page and this process disagree about what is running.`);
|
|
259
|
+
port.deliver(value);
|
|
260
|
+
},
|
|
261
|
+
dispose() {
|
|
262
|
+
for (const stream of ports.keys()) options.send({
|
|
263
|
+
kind: "close",
|
|
264
|
+
stream
|
|
265
|
+
});
|
|
266
|
+
ports.clear();
|
|
267
|
+
stop();
|
|
268
|
+
host.onmessage = null;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* One channel's port, as the application layer sees it.
|
|
274
|
+
*
|
|
275
|
+
* `provide` sets `onmessage` and calls `postMessage`, and that is the
|
|
276
|
+
* entire surface it uses, which is why a channel can be served over
|
|
277
|
+
* something that is not a `MessagePort` at all.
|
|
278
|
+
*/
|
|
279
|
+
var StreamPort = class {
|
|
280
|
+
stream;
|
|
281
|
+
send;
|
|
282
|
+
chunkBytes;
|
|
283
|
+
onmessage = null;
|
|
284
|
+
constructor(stream, send, chunkBytes) {
|
|
285
|
+
this.stream = stream;
|
|
286
|
+
this.send = send;
|
|
287
|
+
this.chunkBytes = chunkBytes;
|
|
288
|
+
}
|
|
289
|
+
postMessage(value) {
|
|
290
|
+
for (const frame of frameData(this.stream, value, this.chunkBytes)) this.send(frame);
|
|
291
|
+
}
|
|
292
|
+
deliver(value) {
|
|
293
|
+
this.onmessage?.({ data: value });
|
|
294
|
+
}
|
|
295
|
+
/** `provide` closes a port it is done with; there is nothing to close. */
|
|
296
|
+
close() {}
|
|
297
|
+
};
|
|
298
|
+
//#endregion
|
|
299
|
+
export { DEFAULT_CHUNK_BYTES, FrameAssembler, createRemoteBridge, frameData, isGessoFrame, serveRemoteChannels };
|
|
300
|
+
|
|
301
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/remote/frames.ts","../../src/remote/bridge.ts","../../src/remote/host.ts"],"sourcesContent":["/**\n * The whole wire format, which is deliberately smaller than the\n * channel protocol it carries.\n *\n * Three frames in each direction. `open` says a named channel wants a\n * stream, `data` carries one channel message, `close` ends a stream.\n * Nothing here knows what a channel is, what a patch is, or what a\n * command is: a frame's `body` is a string this module produced by\n * serializing a value it never looked inside. That restraint is the\n * rule the bridge keeps, and it is what keeps the page's main thread\n * a transport rather than a router.\n */\n\n/**\n * How much of a serialized message goes in one frame.\n *\n * Electrobun's transport fails above roughly 8 MiB in a single\n * message, and it fails badly: the main process throws while draining\n * and the sender sees only a timeout, so the real error is in a log\n * nobody is reading. A megabyte leaves eight times the headroom and\n * costs nothing at the measured rates.\n */\nexport const DEFAULT_CHUNK_BYTES = 1_048_576;\n\nexport type GessoFrame =\n | { readonly kind: 'open'; readonly stream: number; readonly name: string }\n | {\n readonly kind: 'data';\n readonly stream: number;\n readonly body: string;\n /** Absent unless the message was split; then 0-based. */\n readonly part?: number;\n /** Absent unless the message was split; then how many parts to expect. */\n readonly parts?: number;\n }\n | { readonly kind: 'close'; readonly stream: number }\n /**\n * The adapter's own traffic, which is not a channel: the appearance\n * the platform is in, a url the application wants opened outside the\n * window, and one of its own routes it wants opened in a new window.\n * It carries a name and a serialized payload for\n * the same reason a `data` frame carries a body, and it is a\n * separate kind so that nothing has to reserve a stream number.\n */\n | { readonly kind: 'control'; readonly name: string; readonly body: string };\n\nexport function isGessoFrame(value: unknown): value is GessoFrame {\n const kind = (value as { kind?: unknown } | null)?.kind;\n return kind === 'open' || kind === 'data' || kind === 'close' || kind === 'control';\n}\n\n/** Wraps one control message. Never split: these are small by construction. */\nexport function frameControl(name: string, payload: unknown): GessoFrame {\n return { kind: 'control', name, body: JSON.stringify(payload ?? null) ?? 'null' };\n}\n\n/**\n * Serializes one channel message into the frames that carry it.\n *\n * JSON rather than structured clone, because the transport underneath\n * is JSON either way: `Electroview.createTransport` stringifies every\n * message before it encrypts it. That means `undefined` inside a value\n * does not survive, which is true of this transport with or without\n * this module, and which a view key cannot rely on anyway.\n */\nexport function frameData(stream: number, value: unknown, chunkBytes = DEFAULT_CHUNK_BYTES): GessoFrame[] {\n const body = JSON.stringify(value);\n if (body === undefined) {\n throw new Error(\n `A channel message for stream ${stream} could not be serialized. Only plain data crosses a channel; see requirePlainData.`\n );\n }\n if (body.length <= chunkBytes) {\n return [{ kind: 'data', stream, body }];\n }\n const parts = Math.ceil(body.length / chunkBytes);\n const frames: GessoFrame[] = [];\n for (let part = 0; part < parts; part++) {\n frames.push({\n kind: 'data',\n stream,\n body: body.slice(part * chunkBytes, (part + 1) * chunkBytes),\n part,\n parts\n });\n }\n return frames;\n}\n\n/**\n * Puts split messages back together.\n *\n * The transport delivers in order (`Electroview` dispatches through a\n * promise tail that preserves frame order), so a part that arrives out\n * of turn is a bug rather than a race, and it says so instead of\n * quietly assembling a corrupt message.\n */\nexport class FrameAssembler {\n private readonly partial = new Map<number, { parts: number; chunks: string[] }>();\n\n /**\n * Returns the value a `data` frame completes, or `undefined` while\n * more parts are still to come.\n */\n take(frame: GessoFrame & { kind: 'data' }): unknown {\n if (frame.parts === undefined) {\n return JSON.parse(frame.body);\n }\n const held = this.partial.get(frame.stream) ?? { parts: frame.parts, chunks: [] };\n const expected = held.chunks.length;\n if (frame.part !== expected) {\n this.partial.delete(frame.stream);\n throw new Error(\n `Stream ${frame.stream} received part ${String(frame.part)} when part ${expected} was next. ` +\n 'Frames are expected in order; a gap means the transport reordered or dropped one.'\n );\n }\n held.chunks.push(frame.body);\n if (held.chunks.length < held.parts) {\n this.partial.set(frame.stream, held);\n return undefined;\n }\n this.partial.delete(frame.stream);\n return JSON.parse(held.chunks.join(''));\n }\n\n /** Drops anything half-received for a stream that has closed. */\n forget(stream: number): void {\n this.partial.delete(stream);\n }\n}\n","/**\n * The page's half of a remote channel bridge.\n *\n * A Gesso render worker opens a named port per channel and expects the\n * application layer at the other end of it. When the application layer\n * lives in another process (a desktop app's main process, a server on\n * the person's machine) it is reachable only from the page's main\n * thread, so this stands in its place: it accepts the handshakes the\n * render worker sends and pumps each one as a numbered stream of frames\n * over whatever the page has to the other process. Electrobun's RPC is\n * one such thing; a WebSocket is another.\n *\n * It is a transport and nothing else. It serializes a message it never\n * inspects, and it holds no channel, no token and no patch. Anything\n * that needs to understand a payload to route it belongs on the other\n * side of the bridge, and if that ever changes here, the wrong thing\n * is happening on the thread that must stay free for input.\n */\nimport type { AppLogicEndpoint } from '../app/worker/WorkerApp';\nimport { isHubMessage, isPortHandshake } from '../worker/WorkerPorts';\n\nimport { DEFAULT_CHUNK_BYTES, FrameAssembler, frameControl, frameData, type GessoFrame } from './frames';\n\nexport interface RemoteBridgeOptions {\n /**\n * Sends one frame to the other process: `view.rpc.send.<name>` in\n * an Electrobun window, `socket.send(JSON.stringify(frame))` over a\n * WebSocket. A function rather than the transport itself, so nothing\n * here depends on one, and it can be specified without any.\n */\n send: (frame: GessoFrame) => void;\n /** Overrides `DEFAULT_CHUNK_BYTES`. Only a test should need to. */\n chunkBytes?: number;\n /**\n * The appearance the platform is in, as the other process reports it.\n *\n * Wire it to `app.setColorScheme`. It exists because\n * `prefers-color-scheme` is not to be trusted in every webview: on\n * WebKitGTK it reported light on a desktop that was in dark mode,\n * and a shell that believes it is a browser gets the appearance\n * wrong there. A browser tab can leave it out.\n */\n onColorScheme?: (scheme: 'light' | 'dark') => void;\n}\n\nexport interface RemoteBridge {\n /**\n * Hand this to the shell as its application layer:\n *\n * createApp({ renderWorker: …, appLogicWorker: bridge.endpoint })\n *\n * The shell wires it exactly as it wires a worker it was handed, and\n * never closes it.\n */\n readonly endpoint: AppLogicEndpoint;\n /** Call with each frame that arrives from the other process. */\n receive(frame: GessoFrame): void;\n /**\n * Hands a url to the other process to open outside the page.\n *\n * Pass it as `onOpenUrl` to the shell: `window.open` in a webview\n * opens another webview or nothing at all, and a link in a desktop\n * application belongs in the person's browser.\n */\n openUrl(url: string): void;\n /**\n * Asks the other process to open this application at one of its own\n * urls somewhere new: a new window, in a desktop application.\n *\n * Pass it as `onOpenRoute` to the shell. It is what a Cmd-click on an\n * in-app link means in a desktop application: a window has no tabs,\n * and a `window.open` here would open a bare webview the application\n * does not serve. `createDesktopApp` opens the window, at that route,\n * unless the application said otherwise.\n */\n openRoute(url: string): void;\n /** Closes every stream and stops pumping. */\n dispose(): void;\n}\n\nexport function createRemoteBridge(options: RemoteBridgeOptions): RemoteBridge {\n const chunkBytes = options.chunkBytes ?? DEFAULT_CHUNK_BYTES;\n const assembler = new FrameAssembler();\n /** The render worker's port for each stream this side opened. */\n const ports = new Map<number, MessagePort>();\n let hub: MessagePort | null = null;\n let nextStream = 1;\n let disposed = false;\n\n const openStream = (name: string, port: MessagePort): void => {\n const stream = nextStream++;\n ports.set(stream, port);\n port.onmessage = event => {\n for (const frame of frameData(stream, event.data, chunkBytes)) {\n options.send(frame);\n }\n };\n port.start?.();\n options.send({ kind: 'open', stream, name });\n };\n\n const endpoint: AppLogicEndpoint = {\n postMessage(message: unknown, transfer?: Transferable[]): void {\n if (disposed) {\n return;\n }\n if (isHubMessage(message)) {\n const port = transfer?.[0] as MessagePort | undefined;\n if (port === undefined) {\n throw new Error('The shell sent a hub message with no port attached.');\n }\n hub = port;\n hub.onmessage = event => {\n if (!isPortHandshake(event.data)) {\n return;\n }\n const handshake = event.ports?.[0];\n if (handshake === undefined) {\n throw new Error(`Port handshake for '${event.data.key}' arrived with no port attached.`);\n }\n openStream(event.data.key, handshake);\n };\n hub.start?.();\n return;\n }\n // Everything else the shell sends an application worker is about\n // a worker in this page: console forwarding, today. The\n // application process's console is its own, and reaching it is\n // E1.2's business rather than the transport's.\n },\n addEventListener(): void {\n // The shell listens here for console entries from the\n // application worker. There is no worker, and nothing on the\n // other side of the bridge speaks that protocol yet, so a\n // listener would never be called and is not kept.\n },\n removeEventListener(): void {}\n };\n\n return {\n endpoint,\n openUrl(url: string): void {\n if (!disposed) {\n options.send(frameControl('openUrl', { url }));\n }\n },\n openRoute(url: string): void {\n if (!disposed) {\n options.send(frameControl('openRoute', { url }));\n }\n },\n receive(frame: GessoFrame): void {\n if (disposed) {\n return;\n }\n if (frame.kind === 'control') {\n if (frame.name === 'colorScheme') {\n const payload = JSON.parse(frame.body) as { scheme?: 'light' | 'dark' };\n if (payload.scheme !== undefined) {\n options.onColorScheme?.(payload.scheme);\n }\n }\n // An unknown control name is ignored rather than thrown on: the\n // other process may be newer than the page, which is ordinary\n // during a hot reload.\n return;\n }\n if (frame.kind === 'close') {\n assembler.forget(frame.stream);\n ports.get(frame.stream)?.close();\n ports.delete(frame.stream);\n return;\n }\n if (frame.kind !== 'data') {\n // `open` is this side's word. One arriving from the main\n // process means the two ends disagree about who starts a\n // stream, which is worth hearing about rather than ignoring.\n throw new Error(`The other process opened stream ${frame.stream}, which only the page may do.`);\n }\n const value = assembler.take(frame);\n if (value === undefined) {\n return;\n }\n const port = ports.get(frame.stream);\n if (port === undefined) {\n // A frame for a stream this side has closed. Ordinary during\n // teardown, because the far end may already have sent.\n return;\n }\n port.postMessage(value);\n },\n dispose(): void {\n disposed = true;\n for (const [stream, port] of ports) {\n options.send({ kind: 'close', stream });\n port.close();\n }\n ports.clear();\n if (hub !== null) {\n hub.onmessage = null;\n hub.close();\n hub = null;\n }\n }\n };\n}\n","/**\n * The other process's half of a remote channel bridge.\n *\n * `serveChannels` publishes an application's channels over a handshake\n * that arrives on a worker's global scope with a `MessagePort`\n * attached. No process boundary outside a worker can carry a port, so\n * this synthesises both: an `open` frame becomes the handshake, and\n * the port it hands over writes back as frames on the same stream.\n *\n * Nothing in `serveChannels`, `provide` or a view model changes for\n * this. That is the point of the seam: the application layer does not\n * learn it is talking to a page in another process, whether that page\n * is a desktop window or a browser tab on a WebSocket.\n */\nimport { serveChannels, type ServedChannel } from '../channel/serveChannels';\nimport type { PortHost } from '../worker/WorkerPorts';\n\nimport { DEFAULT_CHUNK_BYTES, FrameAssembler, frameControl, frameData, type GessoFrame } from './frames';\n\nexport interface RemoteChannelHostOptions {\n /**\n * Sends one frame to the page: `window.webview.rpc.send.<name>` for\n * an Electrobun window, `socket.send(JSON.stringify(frame))` for a\n * WebSocket. One host per page: each keeps its own streams, and\n * `provide` already keeps a separate record of what each client has\n * seen, so two pages agree without anything here arranging it.\n */\n send: (frame: GessoFrame) => void;\n /** Overrides `DEFAULT_CHUNK_BYTES`. Only a test should need to. */\n chunkBytes?: number;\n /**\n * A url the page asked to have opened outside itself.\n *\n * `Utils.openExternal(url)` is what an Electrobun application passes\n * here. It is not called for the application: opening something is\n * an act, and which urls an application is willing to hand to the\n * operating system is the application's decision.\n */\n onOpenUrl?: (url: string) => void;\n /**\n * One of the application's own urls the page asked to have opened\n * somewhere new: a Cmd-click on an in-app link, sent by the page\n * bridge's `openRoute`.\n *\n * Not called for the application either. `createDesktopApp` answers\n * it by opening a window at that route; a host built on this alone\n * decides for itself.\n */\n onOpenRoute?: (url: string) => void;\n}\n\nexport interface RemoteChannelHost {\n /** Call with each frame that arrives from the page. */\n receive(frame: GessoFrame): void;\n /** Tells the page which appearance the platform is in. */\n setColorScheme(scheme: 'light' | 'dark'): void;\n /** Stops serving and disposes every channel this host provided. */\n dispose(): void;\n}\n\n/**\n * Serves an application's channels to one page in another process.\n *\n * const host = serveRemoteChannels(\n * [{ token: Catalogue, source: { view: { … }, commands: { … } } }],\n * { send: frame => socket.send(JSON.stringify(frame)) }\n * );\n * // and, for each message from that page:\n * host.receive(JSON.parse(message));\n */\nexport function serveRemoteChannels(\n channels: readonly ServedChannel[],\n options: RemoteChannelHostOptions\n): RemoteChannelHost {\n const chunkBytes = options.chunkBytes ?? DEFAULT_CHUNK_BYTES;\n const assembler = new FrameAssembler();\n /** The synthetic port each stream is served through. */\n const ports = new Map<number, StreamPort>();\n\n // `servePorts` installs its chain on this and answers handshakes\n // delivered through `onmessage`. It is the whole of the shim: a\n // worker's global scope, as far as anything downstream can tell.\n const host: PortHost = { onmessage: null };\n const stop = serveChannels(channels, host);\n\n return {\n setColorScheme(scheme: 'light' | 'dark'): void {\n options.send(frameControl('colorScheme', { scheme }));\n },\n receive(frame: GessoFrame): void {\n if (frame.kind === 'control') {\n if (frame.name === 'openUrl' || frame.name === 'openRoute') {\n const payload = JSON.parse(frame.body) as { url?: string };\n if (typeof payload.url === 'string') {\n (frame.name === 'openUrl' ? options.onOpenUrl : options.onOpenRoute)?.(payload.url);\n }\n }\n return;\n }\n if (frame.kind === 'open') {\n const port = new StreamPort(frame.stream, options.send, chunkBytes);\n ports.set(frame.stream, port);\n host.onmessage?.({\n data: { type: 'gesso:port', key: frame.name },\n // A `StreamPort` is a `MessagePort` in the two ways anything\n // downstream uses one, and in no others. The cast is the\n // seam; widening `PortHost` to admit a structural port would\n // widen it for every worker as well.\n ports: [port as unknown as MessagePort]\n });\n return;\n }\n if (frame.kind === 'close') {\n assembler.forget(frame.stream);\n ports.delete(frame.stream);\n return;\n }\n const value = assembler.take(frame);\n if (value === undefined) {\n return;\n }\n const port = ports.get(frame.stream);\n if (port === undefined) {\n throw new Error(\n `A frame arrived for stream ${frame.stream}, which was never opened. The page and this process disagree about what is running.`\n );\n }\n port.deliver(value);\n },\n dispose(): void {\n for (const stream of ports.keys()) {\n options.send({ kind: 'close', stream });\n }\n ports.clear();\n stop();\n host.onmessage = null;\n }\n };\n}\n\n/**\n * One channel's port, as the application layer sees it.\n *\n * `provide` sets `onmessage` and calls `postMessage`, and that is the\n * entire surface it uses, which is why a channel can be served over\n * something that is not a `MessagePort` at all.\n */\nclass StreamPort {\n onmessage: ((event: { data: unknown }) => void) | null = null;\n\n constructor(\n private readonly stream: number,\n private readonly send: (frame: GessoFrame) => void,\n private readonly chunkBytes: number\n ) {}\n\n postMessage(value: unknown): void {\n for (const frame of frameData(this.stream, value, this.chunkBytes)) {\n this.send(frame);\n }\n }\n\n deliver(value: unknown): void {\n this.onmessage?.({ data: value });\n }\n\n /** `provide` closes a port it is done with; there is nothing to close. */\n close(): void {}\n}\n"],"mappings":"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"}
|
|
@@ -193,6 +193,8 @@ function agentSurface(channels, options = {}) {
|
|
|
193
193
|
const tools = [];
|
|
194
194
|
const handlers = /* @__PURE__ */ new Map();
|
|
195
195
|
let lastChange = 0;
|
|
196
|
+
/** Every change the view has seen, so a command can tell its own from what came before it. */
|
|
197
|
+
let changes = 0;
|
|
196
198
|
let following = false;
|
|
197
199
|
/**
|
|
198
200
|
* Subscribed on first use rather than at construction, so a surface
|
|
@@ -205,14 +207,26 @@ function agentSurface(channels, options = {}) {
|
|
|
205
207
|
for (const entry of entries) for (const [key, observable] of Object.entries(entry.served.source.view)) subscriptions.push(observable.subscribe((value) => {
|
|
206
208
|
entry.view[key] = value;
|
|
207
209
|
lastChange = Date.now();
|
|
210
|
+
changes++;
|
|
208
211
|
}));
|
|
209
212
|
};
|
|
210
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Waits for what a command sent when the view had seen `before`
|
|
215
|
+
* changes to reach the view.
|
|
216
|
+
*
|
|
217
|
+
* Quiet counts only once the view has changed since the command: a
|
|
218
|
+
* command that answers after a request has changed nothing yet when
|
|
219
|
+
* the first quiet window ends, and returning then would hand the
|
|
220
|
+
* agent the view from before it. A command that changes nothing at
|
|
221
|
+
* all waits out `settleMs`, which is the price of not being able to
|
|
222
|
+
* tell it from one that is still working.
|
|
223
|
+
*/
|
|
224
|
+
const settle = async (before) => {
|
|
211
225
|
const start = Date.now();
|
|
212
226
|
for (;;) {
|
|
213
227
|
await new Promise((resolve) => setTimeout(resolve, quietMs));
|
|
214
228
|
const now = Date.now();
|
|
215
|
-
if (now - lastChange >= quietMs || now - start >= settleMs) return;
|
|
229
|
+
if (changes > before && now - lastChange >= quietMs || now - start >= settleMs) return;
|
|
216
230
|
}
|
|
217
231
|
};
|
|
218
232
|
const add = (tool, handler) => {
|
|
@@ -290,12 +304,13 @@ function agentSurface(channels, options = {}) {
|
|
|
290
304
|
}
|
|
291
305
|
const handler = entry.served.source.commands?.[command];
|
|
292
306
|
if (handler === void 0) return failure(`${channel} has no command ${command}.`);
|
|
307
|
+
const before = changes;
|
|
293
308
|
try {
|
|
294
309
|
handler(...positional);
|
|
295
310
|
} catch (error) {
|
|
296
311
|
return failure(`${channel}.${command} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
297
312
|
}
|
|
298
|
-
await settle();
|
|
313
|
+
await settle(before);
|
|
299
314
|
return viewResult(entry, `Sent ${command} to ${channel}. The view afterwards:`);
|
|
300
315
|
};
|
|
301
316
|
return {
|
|
@@ -597,4 +612,4 @@ function combineSurfaces(surfaces) {
|
|
|
597
612
|
//#endregion
|
|
598
613
|
export { agentSurface as a, channelSchema as c, serveAgentPort as i, describeChannel as l, combineSurfaces as n, resourceUri as o, remoteSurface as r, validate as s, AGENT_PORT as t };
|
|
599
614
|
|
|
600
|
-
//# sourceMappingURL=remote-
|
|
615
|
+
//# sourceMappingURL=remote-gRdwYo-q.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-gRdwYo-q.js","names":[],"sources":["../src/channel/ChannelSchema.ts","../src/agent/binary.ts","../src/agent/validate.ts","../src/agent/AgentSurface.ts","../src/agent/remote.ts"],"sourcesContent":["/**\n * A channel described at run time: what its view holds and what its\n * commands take, as JSON Schema.\n *\n * A token's types are erased by the compiler, so a running application\n * cannot ask a channel what it accepts. That is no loss to a component,\n * which was typechecked against the token, and all of it to anything\n * that meets the application only at run time: an AI agent being\n * handed the channel as tools, a devtools panel, a test that drives an\n * app by its commands. The schema is the contract again, in the one\n * form those can read.\n *\n * `gesso-vite-plugin` writes it from the contract's own types and\n * JSDoc, so an application that uses the plugin describes its channels\n * by having written them. Without the plugin, `describeChannel` takes a\n * hand-written one.\n */\nimport type { ChannelToken } from './ChannelToken';\n\n/**\n * A JSON Schema (draft 2020-12) object.\n *\n * Deliberately loose: the framework stores and hands back schemas and\n * never interprets one, so a precise type would be a second JSON Schema\n * specification to keep in step with the real one.\n */\nexport type JsonSchema = { readonly [keyword: string]: unknown };\n\n/** One command, described. */\nexport interface CommandSchema {\n /** The command's JSDoc summary. */\n readonly description?: string;\n /**\n * The parameter names, in the order the command takes them.\n *\n * A command is called positionally, `move(from, to)`, while a tool\n * call names its arguments, so this is what maps one onto the other.\n */\n readonly parameters: readonly string[];\n /** The last parameter is a rest parameter, and its array is spread into the call. */\n readonly rest?: true;\n /** An object schema, one property per parameter, keyed by parameter name. */\n readonly input: JsonSchema;\n /** `@destructive`: what it does cannot be undone. */\n readonly destructive?: true;\n /** `@idempotent`: sending it twice changes nothing the first did not. */\n readonly idempotent?: true;\n /** `@confirm`: a person should approve it before anything else sends it. */\n readonly confirm?: true;\n /** `@hidden`: for the application's own components, not for an agent. */\n readonly hidden?: true;\n}\n\n/** A channel, described. */\nexport interface ChannelSchema {\n /** The token's JSDoc summary. */\n readonly description?: string;\n /** An object schema, one property per view key. */\n readonly view: JsonSchema;\n readonly commands: { readonly [name: string]: CommandSchema };\n}\n\n/**\n * The schemas, beside the tokens rather than on them.\n *\n * A token is a plain object both threads import and nothing should\n * grow on, and its interface is a public type that a schema field would\n * widen for every application, described or not. Keyed weakly, so a\n * token the application drops takes its schema with it.\n */\nconst schemas = new WeakMap<object, ChannelSchema>();\n\n/**\n * Attaches a schema to a token.\n *\n * `gesso-vite-plugin` calls this at the bottom of each contract module\n * it reads, so it runs once, when the module is first imported. Call it\n * yourself to describe a channel the plugin does not see. A second call\n * for the same token replaces the first, which is what a hot-replaced\n * contract module does.\n */\nexport function describeChannel(token: ChannelToken<object, object>, schema: ChannelSchema): void {\n schemas.set(token, schema);\n}\n\n/** The schema attached to a token, or undefined when nothing described it. */\nexport function channelSchema(token: ChannelToken<object, object>): ChannelSchema | undefined {\n return schemas.get(token);\n}\n","import type { JsonSchema } from '../channel/ChannelSchema';\n\n/**\n * Turns the base64 an agent wrote back into the bytes a command takes.\n *\n * A command may carry an `ArrayBuffer` or a typed array, which an agent\n * cannot write: it speaks JSON. `gesso-vite-plugin` describes such a\n * field as a base64 string tagged `x-gesso-binary` with the type the\n * command expects, the agent sends the string, and this walks the\n * arguments beside their schema and puts the bytes back before the\n * command is sent, so the application receives what its own components\n * would have sent it.\n *\n * Only what the schema tags is touched, and a value that is not a\n * string where bytes are expected is left for the command to see: the\n * validator has already said whether it was the right shape.\n */\nexport function decodeBinary(schema: JsonSchema | undefined, value: unknown, root: JsonSchema): unknown {\n if (schema === undefined) {\n return value;\n }\n const ref = schema.$ref;\n if (typeof ref === 'string') {\n const target = (root.$defs as Record<string, JsonSchema> | undefined)?.[ref.replace(/^#\\/\\$defs\\//, '')];\n return decodeBinary(target, value, root);\n }\n const kind = schema['x-gesso-binary'];\n if (typeof kind === 'string') {\n return typeof value === 'string' ? bytesOf(value, kind) : value;\n }\n if (Array.isArray(schema.anyOf)) {\n // A union holding bytes is nearly always `bytes | null`; the branch\n // that is bytes is the one a string can be.\n const binary = (schema.anyOf as JsonSchema[]).find(option => typeof option['x-gesso-binary'] === 'string');\n return binary !== undefined && typeof value === 'string' ? decodeBinary(binary, value, root) : value;\n }\n if (Array.isArray(value)) {\n const prefix = (schema.prefixItems as JsonSchema[] | undefined) ?? [];\n const items = schema.items as JsonSchema | boolean | undefined;\n return value.map((item, index) =>\n decodeBinary(index < prefix.length ? prefix[index] : typeof items === 'object' ? items : undefined, item, root)\n );\n }\n if (value !== null && typeof value === 'object') {\n const properties = (schema.properties as Record<string, JsonSchema> | undefined) ?? {};\n const out: Record<string, unknown> = {};\n for (const [key, field] of Object.entries(value)) {\n out[key] = decodeBinary(properties[key], field, root);\n }\n return out;\n }\n return value;\n}\n\nconst TYPED: Record<string, new (buffer: ArrayBuffer) => ArrayBufferView> = {\n Int8Array,\n Uint8Array,\n Uint8ClampedArray,\n Int16Array,\n Uint16Array,\n Int32Array,\n Uint32Array,\n Float32Array,\n Float64Array,\n BigInt64Array,\n BigUint64Array\n};\n\nfunction bytesOf(base64: string, kind: string): ArrayBuffer | ArrayBufferView {\n const text = atob(base64);\n const bytes = new Uint8Array(text.length);\n for (let index = 0; index < text.length; index++) {\n bytes[index] = text.charCodeAt(index);\n }\n if (kind === 'ArrayBuffer') {\n return bytes.buffer;\n }\n const Typed = TYPED[kind];\n return Typed === undefined || Typed === Uint8Array ? bytes : new Typed(bytes.buffer);\n}\n","import type { JsonSchema } from '../channel/ChannelSchema';\n\n/**\n * Checks a value against the JSON Schema a channel description uses.\n *\n * Not a general validator. It understands the keywords\n * `gesso-vite-plugin` writes (`type`, `enum`, `const`, `anyOf`,\n * `items`, `prefixItems`, `minItems`, `properties`, `required`,\n * `additionalProperties`, `not` and `$ref` into `$defs`), and passes\n * anything else. That is enough to stop the case that matters: an\n * agent sending `\"3\"` where a command takes a number, which the\n * command would accept, store, and fail on much later and somewhere\n * else.\n *\n * Returns the first problem as a sentence naming where it is, written\n * for the agent that sent the value so it can correct itself, or null.\n */\nexport function validate(schema: JsonSchema, value: unknown, root: JsonSchema = schema, at = 'input'): string | null {\n const ref = schema.$ref;\n if (typeof ref === 'string') {\n const name = ref.replace(/^#\\/\\$defs\\//, '');\n const target = (root.$defs as Record<string, JsonSchema> | undefined)?.[name];\n return target === undefined ? null : validate(target, value, root, at);\n }\n if (schema.not !== undefined && validate(schema.not as JsonSchema, value, root, at) === null) {\n return `${at} must not be given.`;\n }\n if ('const' in schema && value !== schema.const) {\n return `${at} must be ${JSON.stringify(schema.const)}.`;\n }\n if (Array.isArray(schema.enum) && !schema.enum.includes(value)) {\n return `${at} must be one of ${schema.enum.map(option => JSON.stringify(option)).join(', ')}.`;\n }\n if (Array.isArray(schema.anyOf)) {\n const options = schema.anyOf as JsonSchema[];\n if (options.every(option => validate(option, value, root, at) !== null)) {\n return `${at} does not match any of the ${options.length} shapes it may take.`;\n }\n return null;\n }\n const type = schema.type;\n if (typeof type === 'string' && !hasType(value, type)) {\n return `${at} must be ${article(type)}, not ${describe(value)}.`;\n }\n if (Array.isArray(value)) {\n return validateArray(schema, value, root, at);\n }\n if (type === 'object' && value !== null && typeof value === 'object') {\n return validateObject(schema, value as Record<string, unknown>, root, at);\n }\n return null;\n}\n\nfunction validateArray(schema: JsonSchema, value: readonly unknown[], root: JsonSchema, at: string): string | null {\n const prefix = (schema.prefixItems as JsonSchema[] | undefined) ?? [];\n if (typeof schema.minItems === 'number' && value.length < schema.minItems) {\n return `${at} must have at least ${schema.minItems} items.`;\n }\n for (let index = 0; index < value.length; index++) {\n const item = index < prefix.length ? prefix[index] : schema.items;\n if (item === false) {\n return `${at} must have at most ${prefix.length} items.`;\n }\n if (item !== undefined && item !== true) {\n const problem = validate(item as JsonSchema, value[index], root, `${at}[${index}]`);\n if (problem !== null) {\n return problem;\n }\n }\n }\n return null;\n}\n\nfunction validateObject(\n schema: JsonSchema,\n value: Record<string, unknown>,\n root: JsonSchema,\n at: string\n): string | null {\n const properties = (schema.properties as Record<string, JsonSchema> | undefined) ?? {};\n for (const key of (schema.required as string[] | undefined) ?? []) {\n if (value[key] === undefined) {\n return `${at}.${key} is required.`;\n }\n }\n for (const [key, property] of Object.entries(value)) {\n const declared = properties[key];\n if (declared !== undefined) {\n const problem = property === undefined ? null : validate(declared, property, root, `${at}.${key}`);\n if (problem !== null) {\n return problem;\n }\n continue;\n }\n const extra = schema.additionalProperties;\n if (extra === false) {\n const known = Object.keys(properties);\n return `${at}.${key} is not expected${known.length > 0 ? `; the fields are ${known.join(', ')}` : ''}.`;\n }\n if (extra !== undefined && extra !== true) {\n const problem = validate(extra as JsonSchema, property, root, `${at}.${key}`);\n if (problem !== null) {\n return problem;\n }\n }\n }\n return null;\n}\n\nfunction hasType(value: unknown, type: string): boolean {\n switch (type) {\n case 'string':\n return typeof value === 'string';\n case 'number':\n return typeof value === 'number' && Number.isFinite(value);\n case 'integer':\n return Number.isInteger(value);\n case 'boolean':\n return typeof value === 'boolean';\n case 'null':\n return value === null;\n case 'array':\n return Array.isArray(value);\n case 'object':\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n default:\n return true;\n }\n}\n\nfunction article(type: string): string {\n return /^[aeiou]/.test(type) ? `an ${type}` : `a ${type}`;\n}\n\nfunction describe(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (Array.isArray(value)) {\n return 'an array';\n }\n return typeof value === 'object' ? 'an object' : `${typeof value} ${JSON.stringify(value)}`;\n}\n","import type { Subscription } from 'rxjs';\n\nimport { channelSchema, type CommandSchema, type JsonSchema } from '../channel/ChannelSchema';\nimport type { ServedChannel } from '../channel/serveChannels';\nimport { decodeBinary } from './binary';\nimport { validate } from './validate';\n\n/**\n * An application's channels, as an AI agent sees them.\n *\n * A channel is already the shape an agent wants. Its view is what the\n * application currently holds, as plain data, and its commands are\n * the things it can be asked to do, by name, with typed arguments. A\n * component reaches both through a replica; this reaches them from the\n * other side, through the same `{ token, source }` the application\n * already hands to `serveChannels` or `createDesktopApp`, so an agent\n * sends a command to exactly the handler a click does and sees its\n * effect in exactly the view a screen draws.\n *\n * For each channel it offers:\n *\n * - a resource, `gesso://<channel>/view`, holding the view;\n * - a tool, `<channel>_view`, returning the same thing, because more\n * agents call tools than read resources;\n * - a tool per command, `<channel>_<command>`, whose input schema is\n * the command's parameters by name. Calling it sends the command,\n * waits for the view to settle, and returns the view as it now is,\n * so the agent sees what its call did without a second round trip.\n *\n * The descriptions come from `channelSchema(token)`, which\n * `gesso-vite-plugin` writes from the contract's JSDoc. A command\n * marked `@hidden` is not offered; `@destructive` and `@idempotent`\n * become hints a client shows; `@confirm` means the person is asked,\n * through `confirm`, before the command is sent, and a surface given\n * no way to ask refuses it. A channel nobody described is still\n * offered, with its commands taking their arguments as a positional\n * list, and says so in its description.\n *\n * Nothing here knows about a transport. `mcpHandler` serves it over\n * MCP's HTTP transport; anything else can call `tools`, `call`,\n * `resources` and `read` directly.\n */\n\n/** A tool, in the shape MCP's `tools/list` returns. */\nexport interface AgentTool {\n readonly name: string;\n readonly title?: string;\n readonly description: string;\n readonly inputSchema: JsonSchema;\n readonly outputSchema?: JsonSchema;\n readonly annotations: {\n readonly title?: string;\n readonly readOnlyHint: boolean;\n readonly destructiveHint?: boolean;\n readonly idempotentHint?: boolean;\n readonly openWorldHint: false;\n };\n}\n\n/** A resource, in the shape MCP's `resources/list` returns. */\nexport interface AgentResource {\n readonly uri: string;\n readonly name: string;\n readonly description?: string;\n readonly mimeType: 'application/json';\n}\n\n/** What a tool call returned, in the shape MCP's `tools/call` returns. */\nexport interface AgentToolResult {\n readonly content: readonly { readonly type: 'text'; readonly text: string }[];\n readonly structuredContent?: Record<string, unknown>;\n readonly isError: boolean;\n}\n\n/** A command an agent wants to send that its contract says a person should approve. */\nexport interface AgentConfirmation {\n readonly channel: string;\n readonly command: string;\n readonly description?: string;\n /** The arguments, by parameter name. */\n readonly arguments: Readonly<Record<string, unknown>>;\n readonly destructive: boolean;\n}\n\nexport interface AgentSurfaceOptions {\n /**\n * Asks the person whether a `@confirm` command may be sent. Resolve\n * true to send it. Without this, such a command is refused, which is\n * the safe reading of a contract that asked for a person.\n */\n confirm?: (request: AgentConfirmation) => boolean | Promise<boolean>;\n /**\n * How long the view must be quiet after a command before the call\n * returns it, in milliseconds (default 50). A command whose effect is\n * synchronous settles at once; one that waits on a request settles\n * when the patch lands, or at `settleMs`. A command that changes\n * nothing in the view returns at `settleMs`.\n */\n quietMs?: number;\n /** The longest a call waits for the view to settle (default 1000). */\n settleMs?: number;\n}\n\n/**\n * Any surface an MCP server can speak for: one in this thread, or one\n * across a port whose every answer is a promise.\n */\nexport interface AgentSurfaceLike {\n tools(): readonly AgentTool[] | Promise<readonly AgentTool[]>;\n call(name: string, args: Readonly<Record<string, unknown>> | undefined): Promise<AgentToolResult>;\n resources(): readonly AgentResource[] | Promise<readonly AgentResource[]>;\n read(uri: string): Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined>;\n dispose?(): void;\n}\n\nexport interface AgentSurface extends AgentSurfaceLike {\n tools(): readonly AgentTool[];\n /** Calls a tool by name. Never throws: a failure is a result with `isError`, which an agent can read and correct. */\n call(name: string, args: Readonly<Record<string, unknown>> | undefined): Promise<AgentToolResult>;\n resources(): readonly AgentResource[];\n /** The current view of the channel a resource names, or undefined for a URI this surface does not hold. */\n read(uri: string): Record<string, unknown> | undefined;\n /** Stops following every view. */\n dispose(): void;\n}\n\n/** The longest tool name MCP clients accept. */\nconst MAX_NAME = 64;\n\ninterface Entry {\n readonly served: ServedChannel;\n readonly name: string;\n readonly description?: string;\n readonly viewSchema?: JsonSchema;\n readonly view: Record<string, unknown>;\n readonly commands: ReadonlyMap<string, CommandSchema | null>;\n}\n\ntype Handler = (request: Readonly<Record<string, unknown>> | undefined) => Promise<AgentToolResult>;\n\nexport function agentSurface(channels: readonly ServedChannel[], options: AgentSurfaceOptions = {}): AgentSurface {\n const quietMs = options.quietMs ?? 50;\n const settleMs = options.settleMs ?? 1000;\n const subscriptions: Subscription[] = [];\n const entries: Entry[] = [];\n const tools: AgentTool[] = [];\n const handlers = new Map<string, Handler>();\n let lastChange = 0;\n /** Every change the view has seen, so a command can tell its own from what came before it. */\n let changes = 0;\n let following = false;\n\n /**\n * Subscribed on first use rather than at construction, so a surface\n * built at startup and never called costs the application nothing:\n * a view key may be a cold observable doing real work per subscriber.\n */\n const follow = () => {\n if (following) {\n return;\n }\n following = true;\n for (const entry of entries) {\n for (const [key, observable] of Object.entries(entry.served.source.view)) {\n subscriptions.push(\n observable.subscribe(value => {\n entry.view[key] = value;\n lastChange = Date.now();\n changes++;\n })\n );\n }\n }\n };\n\n /**\n * Waits for what a command sent when the view had seen `before`\n * changes to reach the view.\n *\n * Quiet counts only once the view has changed since the command: a\n * command that answers after a request has changed nothing yet when\n * the first quiet window ends, and returning then would hand the\n * agent the view from before it. A command that changes nothing at\n * all waits out `settleMs`, which is the price of not being able to\n * tell it from one that is still working.\n */\n const settle = async (before: number) => {\n const start = Date.now();\n for (;;) {\n await new Promise(resolve => setTimeout(resolve, quietMs));\n const now = Date.now();\n const changed = changes > before;\n if ((changed && now - lastChange >= quietMs) || now - start >= settleMs) {\n return;\n }\n }\n };\n\n const add = (tool: AgentTool, handler: Handler) => {\n if (handlers.has(tool.name)) {\n throw new Error(\n `Two tools would be called '${tool.name}'. A channel and command name pair has to be unique once joined ` +\n 'with an underscore, and a command cannot be called `view`.'\n );\n }\n tools.push(tool);\n handlers.set(tool.name, handler);\n };\n\n for (const served of channels) {\n const schema = channelSchema(served.token as never);\n const name = toolName(served.token.name);\n const commands = new Map<string, CommandSchema | null>();\n for (const command of Object.keys(served.source.commands ?? {})) {\n const described = schema?.commands[command];\n if (described?.hidden !== true) {\n commands.set(command, described ?? null);\n }\n }\n const entry: Entry = {\n served,\n name,\n description: schema?.description,\n viewSchema: schema?.view,\n view: { ...(served.token.initial as Record<string, unknown>) },\n commands\n };\n entries.push(entry);\n\n const about = entry.description === undefined ? '' : `\\n\\n${entry.description}`;\n add(\n {\n name: toolName(`${served.token.name}_view`),\n title: `Read ${served.token.name}`,\n description: `What the ${served.token.name} channel currently holds.${about}`,\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n ...(entry.viewSchema === undefined ? {} : { outputSchema: entry.viewSchema }),\n annotations: { readOnlyHint: true, openWorldHint: false }\n },\n async () => {\n follow();\n return viewResult(entry, `The ${served.token.name} view.`);\n }\n );\n\n for (const [command, described] of commands) {\n add(commandTool(entry, command, described), args => send(entry, command, described, args));\n }\n }\n\n const send = async (\n entry: Entry,\n command: string,\n described: CommandSchema | null,\n args: Readonly<Record<string, unknown>> | undefined\n ): Promise<AgentToolResult> => {\n follow();\n const channel = entry.served.token.name;\n const input = args ?? {};\n let positional: unknown[];\n if (described === null) {\n const list = input.arguments ?? [];\n if (!Array.isArray(list)) {\n return failure('arguments must be an array of the values the command takes, in order.');\n }\n positional = list;\n } else {\n const problem = validate(described.input, input);\n if (problem !== null) {\n return failure(problem);\n }\n // Bytes arrive as base64, which is all JSON can carry, and leave\n // as the typed array or buffer the command was written to take.\n const properties = (described.input.properties as Record<string, JsonSchema> | undefined) ?? {};\n positional = described.parameters.map(parameter =>\n decodeBinary(properties[parameter], input[parameter], described.input)\n );\n if (described.rest === true) {\n const spread = positional.pop();\n positional.push(...(Array.isArray(spread) ? spread : []));\n }\n while (positional.length > 0 && positional[positional.length - 1] === undefined) {\n positional.pop();\n }\n if (described.confirm === true) {\n if (options.confirm === undefined) {\n return failure(\n `${channel}.${command} asks for a person to approve it, and this application has given agents no way ` +\n 'to ask. Ask the person to do it themselves.'\n );\n }\n const approved = await options.confirm({\n channel,\n command,\n description: described.description,\n arguments: input,\n destructive: described.destructive === true\n });\n if (!approved) {\n return failure(`The person declined ${channel}.${command}. Do not send it again unless they ask.`);\n }\n }\n }\n const handler = entry.served.source.commands?.[command] as ((...values: unknown[]) => void) | undefined;\n if (handler === undefined) {\n return failure(`${channel} has no command ${command}.`);\n }\n const before = changes;\n try {\n handler(...positional);\n } catch (error) {\n return failure(`${channel}.${command} failed: ${error instanceof Error ? error.message : String(error)}`);\n }\n await settle(before);\n return viewResult(entry, `Sent ${command} to ${channel}. The view afterwards:`);\n };\n\n return {\n tools: () => tools,\n call: async (name, args) => {\n const handler = handlers.get(name);\n if (handler === undefined) {\n return failure(`There is no tool called ${name}. The tools are ${[...handlers.keys()].join(', ')}.`);\n }\n return handler(args);\n },\n resources: () =>\n entries.map(entry => ({\n uri: resourceUri(entry.served.token.name),\n name: entry.served.token.name,\n ...(entry.description === undefined ? {} : { description: entry.description }),\n mimeType: 'application/json' as const\n })),\n read: uri => {\n const entry = entries.find(candidate => resourceUri(candidate.served.token.name) === uri);\n if (entry === undefined) {\n return undefined;\n }\n follow();\n return { ...entry.view };\n },\n dispose: () => {\n for (const subscription of subscriptions) {\n subscription.unsubscribe();\n }\n subscriptions.length = 0;\n following = false;\n }\n };\n}\n\n/** The URI of a channel's view. */\nexport function resourceUri(channel: string): string {\n return `gesso://${encodeURIComponent(channel)}/view`;\n}\n\nfunction commandTool(entry: Entry, command: string, described: CommandSchema | null): AgentTool {\n const channel = entry.served.token.name;\n const destructive = described?.destructive === true;\n const lines = [described?.description ?? `Sends ${command} to the ${channel} channel.`];\n if (described === null) {\n lines.push(\n `This command was not described, so its arguments are a positional list. Read ${toolName(`${channel}_view`)} ` +\n 'first to see what the channel holds.'\n );\n }\n if (described?.confirm === true) {\n lines.push('The person is asked to approve this before it is sent.');\n }\n lines.push('Returns the view after the command has taken effect.');\n return {\n name: toolName(`${channel}_${command}`),\n title: `${channel}: ${command}`,\n description: lines.join('\\n\\n'),\n inputSchema:\n described?.input ??\n ({\n type: 'object',\n properties: { arguments: { type: 'array', description: 'The arguments, in order.' } },\n additionalProperties: false\n } satisfies JsonSchema),\n ...(entry.viewSchema === undefined ? {} : { outputSchema: entry.viewSchema }),\n annotations: {\n readOnlyHint: false,\n ...(destructive ? { destructiveHint: true } : { destructiveHint: false }),\n ...(described?.idempotent === true ? { idempotentHint: true } : {}),\n openWorldHint: false\n }\n };\n}\n\n/**\n * A tool name MCP clients accept: letters, digits, underscores and\n * dashes, at most 64 characters. A channel name is the application's\n * own string, so anything else in it becomes an underscore, and one\n * too long is an error at startup rather than a tool a client drops.\n */\nfunction toolName(raw: string): string {\n const name = raw.replace(/[^A-Za-z0-9_-]/g, '_');\n if (name.length > MAX_NAME) {\n throw new Error(`The tool name '${name}' is longer than ${MAX_NAME} characters. Shorten the channel's name.`);\n }\n return name;\n}\n\nfunction viewResult(entry: Entry, lead: string): AgentToolResult {\n const view = { ...entry.view };\n return {\n content: [{ type: 'text', text: `${lead}\\n${JSON.stringify(view, null, 2)}` }],\n structuredContent: view,\n isError: false\n };\n}\n\nfunction failure(text: string): AgentToolResult {\n return { content: [{ type: 'text', text }], isError: true };\n}\n","import type { AgentConfirmation, AgentResource, AgentSurfaceLike, AgentTool, AgentToolResult } from './AgentSurface';\n\n/**\n * An agent surface across a thread.\n *\n * A web application's channels are served in workers, and an agent\n * reaches the page, so the surface has to cross from one to the other.\n * The worker that serves channels answers a `gesso:agent` port with its\n * own surface (`serveAgentPort`); the page holds the other end as a\n * surface of its own (`remoteSurface`); and a thread that knows several\n * such ports, the render worker with its channel workers behind it,\n * offers them as one (`combineSurfaces`).\n *\n * The four operations cross as they are. MCP itself is spoken only at\n * the end that faces the agent, so nothing in a worker parses JSON-RPC.\n */\n\n/** The port key a thread answers with its agent surface. */\nexport const AGENT_PORT = 'gesso:agent';\n\n/** What crosses an agent port, page to worker. */\nexport type AgentPortRequest =\n | { id: number; op: 'tools' }\n | { id: number; op: 'resources' }\n | { id: number; op: 'call'; name: string; args?: Readonly<Record<string, unknown>> }\n | { id: number; op: 'read'; uri: string };\n\n/** A request before it is given an id. */\ntype Unsent<T> = T extends unknown ? Omit<T, 'id'> : never;\n\n/** What crosses back. */\nexport type AgentPortResponse = { id: number; result: unknown } | { id: number; error: string };\n\n/**\n * The question going the other way: a command marked `@confirm` needs\n * a person, the person is at the far end, so the thread that serves\n * the channel asks down the port it was asked on and waits.\n */\nexport type AgentPortConfirm = { confirm: number; request: AgentConfirmation } | { confirm: number; approved: boolean };\n\n/** How a thread asks the person, wherever the person is. */\nexport type AgentConfirm = (request: AgentConfirmation) => boolean | Promise<boolean>;\n\n/** A `MessagePort`, or anything that posts and receives like one. */\ntype AgentPort = Pick<MessagePort, 'postMessage' | 'onmessage'>;\n\n/**\n * Answers a port with a surface.\n *\n * The surface is made on the first request rather than when the port\n * arrives: a port is opened by a page that may never ask anything, and\n * a surface subscribes to every view key once it is used. It is given\n * a `confirm` that asks the far end of this port, so a command marked\n * `@confirm` reaches the person wherever they are; the far end answers\n * no when it has no way to ask.\n */\nexport function serveAgentPort(port: AgentPort, makeSurface: (confirm: AgentConfirm) => AgentSurfaceLike): () => void {\n let surface: AgentSurfaceLike | undefined;\n let nextConfirm = 1;\n const asking = new Map<number, (approved: boolean) => void>();\n const confirm: AgentConfirm = request =>\n new Promise(resolve => {\n const id = nextConfirm++;\n asking.set(id, resolve);\n port.postMessage({ confirm: id, request } satisfies AgentPortConfirm);\n });\n port.onmessage = async event => {\n const data = event.data as AgentPortRequest | AgentPortConfirm;\n if ('confirm' in data && 'approved' in data) {\n asking.get(data.confirm)?.(data.approved);\n asking.delete(data.confirm);\n return;\n }\n const request = data as AgentPortRequest;\n if (typeof request?.id !== 'number') {\n return;\n }\n surface ??= makeSurface(confirm);\n try {\n port.postMessage({ id: request.id, result: await answer(surface, request) } satisfies AgentPortResponse);\n } catch (error) {\n port.postMessage({\n id: request.id,\n error: error instanceof Error ? error.message : String(error)\n } satisfies AgentPortResponse);\n }\n };\n return () => {\n port.onmessage = null;\n surface?.dispose?.();\n for (const resolve of asking.values()) {\n resolve(false);\n }\n asking.clear();\n };\n}\n\nfunction answer(surface: AgentSurfaceLike, request: AgentPortRequest): unknown {\n switch (request.op) {\n case 'tools':\n return surface.tools();\n case 'resources':\n return surface.resources();\n case 'call':\n return surface.call(request.name, request.args);\n case 'read':\n return surface.read(request.uri) ?? null;\n }\n}\n\n/**\n * The surface at the other end of a port.\n *\n * A request nobody answers within `timeoutMs` (default 2000) is taken\n * as a thread with nothing to offer: a worker that serves no channels\n * never installs the answering side, and an agent asking the whole\n * application should hear about the threads that do rather than wait\n * on one that does not.\n */\nexport function remoteSurface(\n port: AgentPort,\n options: { timeoutMs?: number; confirm?: AgentConfirm } = {}\n): AgentSurfaceLike {\n const timeoutMs = options.timeoutMs ?? 2000;\n let next = 1;\n const pending = new Map<number, { resolve: (value: unknown) => void; reject: (error: Error) => void }>();\n port.onmessage = event => {\n const question = event.data as AgentPortConfirm;\n if (question !== null && typeof question === 'object' && 'request' in question) {\n // Asked to put a command to the person. With nobody to ask, the\n // answer is no, which is what a contract asking for a person means.\n void Promise.resolve(options.confirm?.(question.request) ?? false).then(\n approved => port.postMessage({ confirm: question.confirm, approved } satisfies AgentPortConfirm),\n () => port.postMessage({ confirm: question.confirm, approved: false } satisfies AgentPortConfirm)\n );\n return;\n }\n const response = event.data as AgentPortResponse;\n const waiting = pending.get(response?.id);\n if (waiting === undefined) {\n return;\n }\n pending.delete(response.id);\n if ('error' in response) {\n waiting.reject(new Error(response.error));\n } else {\n waiting.resolve(response.result);\n }\n };\n const ask = <T>(request: Unsent<AgentPortRequest>, fallback: T, timeout = timeoutMs): Promise<T> =>\n new Promise<T>((resolve, reject) => {\n const id = next++;\n const timer = setTimeout(() => {\n pending.delete(id);\n resolve(fallback);\n }, timeout);\n pending.set(id, {\n resolve: value => {\n clearTimeout(timer);\n resolve(value as T);\n },\n reject: error => {\n clearTimeout(timer);\n reject(error);\n }\n });\n port.postMessage({ ...request, id });\n });\n return {\n tools: () => ask<readonly AgentTool[]>({ op: 'tools' }, []),\n resources: () => ask<readonly AgentResource[]>({ op: 'resources' }, []),\n // A command may wait on the person through `confirm`, so a call is\n // given far longer than a listing before it is abandoned.\n call: (name, args) =>\n ask<AgentToolResult>(\n { op: 'call', name, args },\n { content: [{ type: 'text', text: `${name} did not answer in time.` }], isError: true },\n Math.max(timeoutMs, 120_000)\n ),\n read: async uri => (await ask<Record<string, unknown> | null>({ op: 'read', uri }, null)) ?? undefined\n };\n}\n\n/**\n * Several surfaces as one: tools and resources listed together, a call\n * or a read sent to whichever surface listed it. When two list the same\n * name, the first keeps it, as the order of the threads is the order\n * the application registered them in.\n */\nexport function combineSurfaces(surfaces: readonly AgentSurfaceLike[]): AgentSurfaceLike {\n const toolOwners = new Map<string, AgentSurfaceLike>();\n const resourceOwners = new Map<string, AgentSurfaceLike>();\n\n const tools = async () => {\n const lists = await Promise.all(surfaces.map(surface => surface.tools()));\n toolOwners.clear();\n const out: AgentTool[] = [];\n lists.forEach((list, index) => {\n for (const tool of list) {\n if (!toolOwners.has(tool.name)) {\n toolOwners.set(tool.name, surfaces[index]);\n out.push(tool);\n }\n }\n });\n return out;\n };\n\n const resources = async () => {\n const lists = await Promise.all(surfaces.map(surface => surface.resources()));\n resourceOwners.clear();\n const out: AgentResource[] = [];\n lists.forEach((list, index) => {\n for (const resource of list) {\n if (!resourceOwners.has(resource.uri)) {\n resourceOwners.set(resource.uri, surfaces[index]);\n out.push(resource);\n }\n }\n });\n return out;\n };\n\n return {\n tools,\n resources,\n call: async (name, args) => {\n if (!toolOwners.has(name)) {\n await tools();\n }\n const owner = toolOwners.get(name);\n if (owner === undefined) {\n const known = [...toolOwners.keys()].join(', ');\n return {\n content: [{ type: 'text', text: `There is no tool called ${name}. The tools are ${known}.` }],\n isError: true\n };\n }\n return owner.call(name, args);\n },\n read: async uri => {\n if (!resourceOwners.has(uri)) {\n await resources();\n }\n return resourceOwners.get(uri)?.read(uri);\n },\n dispose: () => {\n for (const surface of surfaces) {\n surface.dispose?.();\n }\n }\n };\n}\n"],"mappings":";;;;;;;;;AAsEA,MAAM,0BAAU,IAAI,QAA+B;;;;;;;;;;AAWnD,SAAgB,gBAAgB,OAAqC,QAA6B;CAChG,QAAQ,IAAI,OAAO,MAAM;AAC3B;;AAGA,SAAgB,cAAc,OAAgE;CAC5F,OAAO,QAAQ,IAAI,KAAK;AAC1B;;;;;;;;;;;;;;;;;;ACvEA,SAAgB,aAAa,QAAgC,OAAgB,MAA2B;CACtG,IAAI,WAAW,KAAA,GACb,OAAO;CAET,MAAM,MAAM,OAAO;CACnB,IAAI,OAAO,QAAQ,UAAU;EAC3B,MAAM,SAAU,KAAK,QAAmD,IAAI,QAAQ,gBAAgB,EAAE;EACtG,OAAO,aAAa,QAAQ,OAAO,IAAI;CACzC;CACA,MAAM,OAAO,OAAO;CACpB,IAAI,OAAO,SAAS,UAClB,OAAO,OAAO,UAAU,WAAW,QAAQ,OAAO,IAAI,IAAI;CAE5D,IAAI,MAAM,QAAQ,OAAO,KAAK,GAAG;EAG/B,MAAM,SAAU,OAAO,MAAuB,MAAK,WAAU,OAAO,OAAO,sBAAsB,QAAQ;EACzG,OAAO,WAAW,KAAA,KAAa,OAAO,UAAU,WAAW,aAAa,QAAQ,OAAO,IAAI,IAAI;CACjG;CACA,IAAI,MAAM,QAAQ,KAAK,GAAG;EACxB,MAAM,SAAU,OAAO,eAA4C,CAAC;EACpE,MAAM,QAAQ,OAAO;EACrB,OAAO,MAAM,KAAK,MAAM,UACtB,aAAa,QAAQ,OAAO,SAAS,OAAO,SAAS,OAAO,UAAU,WAAW,QAAQ,KAAA,GAAW,MAAM,IAAI,CAChH;CACF;CACA,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;EAC/C,MAAM,aAAc,OAAO,cAAyD,CAAC;EACrF,MAAM,MAA+B,CAAC;EACtC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,GAC7C,IAAI,OAAO,aAAa,WAAW,MAAM,OAAO,IAAI;EAEtD,OAAO;CACT;CACA,OAAO;AACT;AAEA,MAAM,QAAsE;CAC1E;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,SAAS,QAAQ,QAAgB,MAA6C;CAC5E,MAAM,OAAO,KAAK,MAAM;CACxB,MAAM,QAAQ,IAAI,WAAW,KAAK,MAAM;CACxC,KAAK,IAAI,QAAQ,GAAG,QAAQ,KAAK,QAAQ,SACvC,MAAM,SAAS,KAAK,WAAW,KAAK;CAEtC,IAAI,SAAS,eACX,OAAO,MAAM;CAEf,MAAM,QAAQ,MAAM;CACpB,OAAO,UAAU,KAAA,KAAa,UAAU,aAAa,QAAQ,IAAI,MAAM,MAAM,MAAM;AACrF;;;;;;;;;;;;;;;;;;AC9DA,SAAgB,SAAS,QAAoB,OAAgB,OAAmB,QAAQ,KAAK,SAAwB;CACnH,MAAM,MAAM,OAAO;CACnB,IAAI,OAAO,QAAQ,UAAU;EAC3B,MAAM,OAAO,IAAI,QAAQ,gBAAgB,EAAE;EAC3C,MAAM,SAAU,KAAK,QAAmD;EACxE,OAAO,WAAW,KAAA,IAAY,OAAO,SAAS,QAAQ,OAAO,MAAM,EAAE;CACvE;CACA,IAAI,OAAO,QAAQ,KAAA,KAAa,SAAS,OAAO,KAAmB,OAAO,MAAM,EAAE,MAAM,MACtF,OAAO,GAAG,GAAG;CAEf,IAAI,WAAW,UAAU,UAAU,OAAO,OACxC,OAAO,GAAG,GAAG,WAAW,KAAK,UAAU,OAAO,KAAK,EAAE;CAEvD,IAAI,MAAM,QAAQ,OAAO,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,KAAK,GAC3D,OAAO,GAAG,GAAG,kBAAkB,OAAO,KAAK,KAAI,WAAU,KAAK,UAAU,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE;CAE9F,IAAI,MAAM,QAAQ,OAAO,KAAK,GAAG;EAC/B,MAAM,UAAU,OAAO;EACvB,IAAI,QAAQ,OAAM,WAAU,SAAS,QAAQ,OAAO,MAAM,EAAE,MAAM,IAAI,GACpE,OAAO,GAAG,GAAG,6BAA6B,QAAQ,OAAO;EAE3D,OAAO;CACT;CACA,MAAM,OAAO,OAAO;CACpB,IAAI,OAAO,SAAS,YAAY,CAAC,QAAQ,OAAO,IAAI,GAClD,OAAO,GAAG,GAAG,WAAW,QAAQ,IAAI,EAAE,QAAQ,SAAS,KAAK,EAAE;CAEhE,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,cAAc,QAAQ,OAAO,MAAM,EAAE;CAE9C,IAAI,SAAS,YAAY,UAAU,QAAQ,OAAO,UAAU,UAC1D,OAAO,eAAe,QAAQ,OAAkC,MAAM,EAAE;CAE1E,OAAO;AACT;AAEA,SAAS,cAAc,QAAoB,OAA2B,MAAkB,IAA2B;CACjH,MAAM,SAAU,OAAO,eAA4C,CAAC;CACpE,IAAI,OAAO,OAAO,aAAa,YAAY,MAAM,SAAS,OAAO,UAC/D,OAAO,GAAG,GAAG,sBAAsB,OAAO,SAAS;CAErD,KAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS;EACjD,MAAM,OAAO,QAAQ,OAAO,SAAS,OAAO,SAAS,OAAO;EAC5D,IAAI,SAAS,OACX,OAAO,GAAG,GAAG,qBAAqB,OAAO,OAAO;EAElD,IAAI,SAAS,KAAA,KAAa,SAAS,MAAM;GACvC,MAAM,UAAU,SAAS,MAAoB,MAAM,QAAQ,MAAM,GAAG,GAAG,GAAG,MAAM,EAAE;GAClF,IAAI,YAAY,MACd,OAAO;EAEX;CACF;CACA,OAAO;AACT;AAEA,SAAS,eACP,QACA,OACA,MACA,IACe;CACf,MAAM,aAAc,OAAO,cAAyD,CAAC;CACrF,KAAK,MAAM,OAAQ,OAAO,YAAqC,CAAC,GAC9D,IAAI,MAAM,SAAS,KAAA,GACjB,OAAO,GAAG,GAAG,GAAG,IAAI;CAGxB,KAAK,MAAM,CAAC,KAAK,aAAa,OAAO,QAAQ,KAAK,GAAG;EACnD,MAAM,WAAW,WAAW;EAC5B,IAAI,aAAa,KAAA,GAAW;GAC1B,MAAM,UAAU,aAAa,KAAA,IAAY,OAAO,SAAS,UAAU,UAAU,MAAM,GAAG,GAAG,GAAG,KAAK;GACjG,IAAI,YAAY,MACd,OAAO;GAET;EACF;EACA,MAAM,QAAQ,OAAO;EACrB,IAAI,UAAU,OAAO;GACnB,MAAM,QAAQ,OAAO,KAAK,UAAU;GACpC,OAAO,GAAG,GAAG,GAAG,IAAI,kBAAkB,MAAM,SAAS,IAAI,oBAAoB,MAAM,KAAK,IAAI,MAAM,GAAG;EACvG;EACA,IAAI,UAAU,KAAA,KAAa,UAAU,MAAM;GACzC,MAAM,UAAU,SAAS,OAAqB,UAAU,MAAM,GAAG,GAAG,GAAG,KAAK;GAC5E,IAAI,YAAY,MACd,OAAO;EAEX;CACF;CACA,OAAO;AACT;AAEA,SAAS,QAAQ,OAAgB,MAAuB;CACtD,QAAQ,MAAR;EACE,KAAK,UACH,OAAO,OAAO,UAAU;EAC1B,KAAK,UACH,OAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK;EAC3D,KAAK,WACH,OAAO,OAAO,UAAU,KAAK;EAC/B,KAAK,WACH,OAAO,OAAO,UAAU;EAC1B,KAAK,QACH,OAAO,UAAU;EACnB,KAAK,SACH,OAAO,MAAM,QAAQ,KAAK;EAC5B,KAAK,UACH,OAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK;EAC5E,SACE,OAAO;CACX;AACF;AAEA,SAAS,QAAQ,MAAsB;CACrC,OAAO,WAAW,KAAK,IAAI,IAAI,MAAM,SAAS,KAAK;AACrD;AAEA,SAAS,SAAS,OAAwB;CACxC,IAAI,UAAU,MACZ,OAAO;CAET,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO;CAET,OAAO,OAAO,UAAU,WAAW,cAAc,GAAG,OAAO,MAAM,GAAG,KAAK,UAAU,KAAK;AAC1F;;;;ACfA,MAAM,WAAW;AAajB,SAAgB,aAAa,UAAoC,UAA+B,CAAC,GAAiB;CAChH,MAAM,UAAU,QAAQ,WAAW;CACnC,MAAM,WAAW,QAAQ,YAAY;CACrC,MAAM,gBAAgC,CAAC;CACvC,MAAM,UAAmB,CAAC;CAC1B,MAAM,QAAqB,CAAC;CAC5B,MAAM,2BAAW,IAAI,IAAqB;CAC1C,IAAI,aAAa;;CAEjB,IAAI,UAAU;CACd,IAAI,YAAY;;;;;;CAOhB,MAAM,eAAe;EACnB,IAAI,WACF;EAEF,YAAY;EACZ,KAAK,MAAM,SAAS,SAClB,KAAK,MAAM,CAAC,KAAK,eAAe,OAAO,QAAQ,MAAM,OAAO,OAAO,IAAI,GACrE,cAAc,KACZ,WAAW,WAAU,UAAS;GAC5B,MAAM,KAAK,OAAO;GAClB,aAAa,KAAK,IAAI;GACtB;EACF,CAAC,CACH;CAGN;;;;;;;;;;;;CAaA,MAAM,SAAS,OAAO,WAAmB;EACvC,MAAM,QAAQ,KAAK,IAAI;EACvB,SAAS;GACP,MAAM,IAAI,SAAQ,YAAW,WAAW,SAAS,OAAO,CAAC;GACzD,MAAM,MAAM,KAAK,IAAI;GAErB,IADgB,UAAU,UACV,MAAM,cAAc,WAAY,MAAM,SAAS,UAC7D;EAEJ;CACF;CAEA,MAAM,OAAO,MAAiB,YAAqB;EACjD,IAAI,SAAS,IAAI,KAAK,IAAI,GACxB,MAAM,IAAI,MACR,8BAA8B,KAAK,KAAK,6HAE1C;EAEF,MAAM,KAAK,IAAI;EACf,SAAS,IAAI,KAAK,MAAM,OAAO;CACjC;CAEA,KAAK,MAAM,UAAU,UAAU;EAC7B,MAAM,SAAS,cAAc,OAAO,KAAc;EAClD,MAAM,OAAO,SAAS,OAAO,MAAM,IAAI;EACvC,MAAM,2BAAW,IAAI,IAAkC;EACvD,KAAK,MAAM,WAAW,OAAO,KAAK,OAAO,OAAO,YAAY,CAAC,CAAC,GAAG;GAC/D,MAAM,YAAY,QAAQ,SAAS;GACnC,IAAI,WAAW,WAAW,MACxB,SAAS,IAAI,SAAS,aAAa,IAAI;EAE3C;EACA,MAAM,QAAe;GACnB;GACA;GACA,aAAa,QAAQ;GACrB,YAAY,QAAQ;GACpB,MAAM,EAAE,GAAI,OAAO,MAAM,QAAoC;GAC7D;EACF;EACA,QAAQ,KAAK,KAAK;EAElB,MAAM,QAAQ,MAAM,gBAAgB,KAAA,IAAY,KAAK,OAAO,MAAM;EAClE,IACE;GACE,MAAM,SAAS,GAAG,OAAO,MAAM,KAAK,MAAM;GAC1C,OAAO,QAAQ,OAAO,MAAM;GAC5B,aAAa,YAAY,OAAO,MAAM,KAAK,2BAA2B;GACtE,aAAa;IAAE,MAAM;IAAU,YAAY,CAAC;IAAG,sBAAsB;GAAM;GAC3E,GAAI,MAAM,eAAe,KAAA,IAAY,CAAC,IAAI,EAAE,cAAc,MAAM,WAAW;GAC3E,aAAa;IAAE,cAAc;IAAM,eAAe;GAAM;EAC1D,GACA,YAAY;GACV,OAAO;GACP,OAAO,WAAW,OAAO,OAAO,OAAO,MAAM,KAAK,OAAO;EAC3D,CACF;EAEA,KAAK,MAAM,CAAC,SAAS,cAAc,UACjC,IAAI,YAAY,OAAO,SAAS,SAAS,IAAG,SAAQ,KAAK,OAAO,SAAS,WAAW,IAAI,CAAC;CAE7F;CAEA,MAAM,OAAO,OACX,OACA,SACA,WACA,SAC6B;EAC7B,OAAO;EACP,MAAM,UAAU,MAAM,OAAO,MAAM;EACnC,MAAM,QAAQ,QAAQ,CAAC;EACvB,IAAI;EACJ,IAAI,cAAc,MAAM;GACtB,MAAM,OAAO,MAAM,aAAa,CAAC;GACjC,IAAI,CAAC,MAAM,QAAQ,IAAI,GACrB,OAAO,QAAQ,uEAAuE;GAExF,aAAa;EACf,OAAO;GACL,MAAM,UAAU,SAAS,UAAU,OAAO,KAAK;GAC/C,IAAI,YAAY,MACd,OAAO,QAAQ,OAAO;GAIxB,MAAM,aAAc,UAAU,MAAM,cAAyD,CAAC;GAC9F,aAAa,UAAU,WAAW,KAAI,cACpC,aAAa,WAAW,YAAY,MAAM,YAAY,UAAU,KAAK,CACvE;GACA,IAAI,UAAU,SAAS,MAAM;IAC3B,MAAM,SAAS,WAAW,IAAI;IAC9B,WAAW,KAAK,GAAI,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC,CAAE;GAC1D;GACA,OAAO,WAAW,SAAS,KAAK,WAAW,WAAW,SAAS,OAAO,KAAA,GACpE,WAAW,IAAI;GAEjB,IAAI,UAAU,YAAY,MAAM;IAC9B,IAAI,QAAQ,YAAY,KAAA,GACtB,OAAO,QACL,GAAG,QAAQ,GAAG,QAAQ,2HAExB;IASF,IAAI,CAAC,MAPkB,QAAQ,QAAQ;KACrC;KACA;KACA,aAAa,UAAU;KACvB,WAAW;KACX,aAAa,UAAU,gBAAgB;IACzC,CAAC,GAEC,OAAO,QAAQ,uBAAuB,QAAQ,GAAG,QAAQ,wCAAwC;GAErG;EACF;EACA,MAAM,UAAU,MAAM,OAAO,OAAO,WAAW;EAC/C,IAAI,YAAY,KAAA,GACd,OAAO,QAAQ,GAAG,QAAQ,kBAAkB,QAAQ,EAAE;EAExD,MAAM,SAAS;EACf,IAAI;GACF,QAAQ,GAAG,UAAU;EACvB,SAAS,OAAO;GACd,OAAO,QAAQ,GAAG,QAAQ,GAAG,QAAQ,WAAW,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAAG;EAC1G;EACA,MAAM,OAAO,MAAM;EACnB,OAAO,WAAW,OAAO,QAAQ,QAAQ,MAAM,QAAQ,uBAAuB;CAChF;CAEA,OAAO;EACL,aAAa;EACb,MAAM,OAAO,MAAM,SAAS;GAC1B,MAAM,UAAU,SAAS,IAAI,IAAI;GACjC,IAAI,YAAY,KAAA,GACd,OAAO,QAAQ,2BAA2B,KAAK,kBAAkB,CAAC,GAAG,SAAS,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,EAAE;GAErG,OAAO,QAAQ,IAAI;EACrB;EACA,iBACE,QAAQ,KAAI,WAAU;GACpB,KAAK,YAAY,MAAM,OAAO,MAAM,IAAI;GACxC,MAAM,MAAM,OAAO,MAAM;GACzB,GAAI,MAAM,gBAAgB,KAAA,IAAY,CAAC,IAAI,EAAE,aAAa,MAAM,YAAY;GAC5E,UAAU;EACZ,EAAE;EACJ,OAAM,QAAO;GACX,MAAM,QAAQ,QAAQ,MAAK,cAAa,YAAY,UAAU,OAAO,MAAM,IAAI,MAAM,GAAG;GACxF,IAAI,UAAU,KAAA,GACZ;GAEF,OAAO;GACP,OAAO,EAAE,GAAG,MAAM,KAAK;EACzB;EACA,eAAe;GACb,KAAK,MAAM,gBAAgB,eACzB,aAAa,YAAY;GAE3B,cAAc,SAAS;GACvB,YAAY;EACd;CACF;AACF;;AAGA,SAAgB,YAAY,SAAyB;CACnD,OAAO,WAAW,mBAAmB,OAAO,EAAE;AAChD;AAEA,SAAS,YAAY,OAAc,SAAiB,WAA4C;CAC9F,MAAM,UAAU,MAAM,OAAO,MAAM;CACnC,MAAM,cAAc,WAAW,gBAAgB;CAC/C,MAAM,QAAQ,CAAC,WAAW,eAAe,SAAS,QAAQ,UAAU,QAAQ,UAAU;CACtF,IAAI,cAAc,MAChB,MAAM,KACJ,gFAAgF,SAAS,GAAG,QAAQ,MAAM,EAAE,sCAE9G;CAEF,IAAI,WAAW,YAAY,MACzB,MAAM,KAAK,wDAAwD;CAErE,MAAM,KAAK,sDAAsD;CACjE,OAAO;EACL,MAAM,SAAS,GAAG,QAAQ,GAAG,SAAS;EACtC,OAAO,GAAG,QAAQ,IAAI;EACtB,aAAa,MAAM,KAAK,MAAM;EAC9B,aACE,WAAW,SACV;GACC,MAAM;GACN,YAAY,EAAE,WAAW;IAAE,MAAM;IAAS,aAAa;GAA2B,EAAE;GACpF,sBAAsB;EACxB;EACF,GAAI,MAAM,eAAe,KAAA,IAAY,CAAC,IAAI,EAAE,cAAc,MAAM,WAAW;EAC3E,aAAa;GACX,cAAc;GACd,GAAI,cAAc,EAAE,iBAAiB,KAAK,IAAI,EAAE,iBAAiB,MAAM;GACvE,GAAI,WAAW,eAAe,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAC;GACjE,eAAe;EACjB;CACF;AACF;;;;;;;AAQA,SAAS,SAAS,KAAqB;CACrC,MAAM,OAAO,IAAI,QAAQ,mBAAmB,GAAG;CAC/C,IAAI,KAAK,SAAS,UAChB,MAAM,IAAI,MAAM,kBAAkB,KAAK,mBAAmB,SAAS,yCAAyC;CAE9G,OAAO;AACT;AAEA,SAAS,WAAW,OAAc,MAA+B;CAC/D,MAAM,OAAO,EAAE,GAAG,MAAM,KAAK;CAC7B,OAAO;EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,GAAG,KAAK,IAAI,KAAK,UAAU,MAAM,MAAM,CAAC;EAAI,CAAC;EAC7E,mBAAmB;EACnB,SAAS;CACX;AACF;AAEA,SAAS,QAAQ,MAA+B;CAC9C,OAAO;EAAE,SAAS,CAAC;GAAE,MAAM;GAAQ;EAAK,CAAC;EAAG,SAAS;CAAK;AAC5D;;;;;;;;;;;;;;;;;;AC9YA,MAAa,aAAa;;;;;;;;;;;AAsC1B,SAAgB,eAAe,MAAiB,aAAsE;CACpH,IAAI;CACJ,IAAI,cAAc;CAClB,MAAM,yBAAS,IAAI,IAAyC;CAC5D,MAAM,WAAwB,YAC5B,IAAI,SAAQ,YAAW;EACrB,MAAM,KAAK;EACX,OAAO,IAAI,IAAI,OAAO;EACtB,KAAK,YAAY;GAAE,SAAS;GAAI;EAAQ,CAA4B;CACtE,CAAC;CACH,KAAK,YAAY,OAAM,UAAS;EAC9B,MAAM,OAAO,MAAM;EACnB,IAAI,aAAa,QAAQ,cAAc,MAAM;GAC3C,OAAO,IAAI,KAAK,OAAO,CAAC,GAAG,KAAK,QAAQ;GACxC,OAAO,OAAO,KAAK,OAAO;GAC1B;EACF;EACA,MAAM,UAAU;EAChB,IAAI,OAAO,SAAS,OAAO,UACzB;EAEF,YAAY,YAAY,OAAO;EAC/B,IAAI;GACF,KAAK,YAAY;IAAE,IAAI,QAAQ;IAAI,QAAQ,MAAM,OAAO,SAAS,OAAO;GAAE,CAA6B;EACzG,SAAS,OAAO;GACd,KAAK,YAAY;IACf,IAAI,QAAQ;IACZ,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAC9D,CAA6B;EAC/B;CACF;CACA,aAAa;EACX,KAAK,YAAY;EACjB,SAAS,UAAU;EACnB,KAAK,MAAM,WAAW,OAAO,OAAO,GAClC,QAAQ,KAAK;EAEf,OAAO,MAAM;CACf;AACF;AAEA,SAAS,OAAO,SAA2B,SAAoC;CAC7E,QAAQ,QAAQ,IAAhB;EACE,KAAK,SACH,OAAO,QAAQ,MAAM;EACvB,KAAK,aACH,OAAO,QAAQ,UAAU;EAC3B,KAAK,QACH,OAAO,QAAQ,KAAK,QAAQ,MAAM,QAAQ,IAAI;EAChD,KAAK,QACH,OAAO,QAAQ,KAAK,QAAQ,GAAG,KAAK;CACxC;AACF;;;;;;;;;;AAWA,SAAgB,cACd,MACA,UAA0D,CAAC,GACzC;CAClB,MAAM,YAAY,QAAQ,aAAa;CACvC,IAAI,OAAO;CACX,MAAM,0BAAU,IAAI,IAAmF;CACvG,KAAK,aAAY,UAAS;EACxB,MAAM,WAAW,MAAM;EACvB,IAAI,aAAa,QAAQ,OAAO,aAAa,YAAY,aAAa,UAAU;GAG9E,QAAa,QAAQ,QAAQ,UAAU,SAAS,OAAO,KAAK,KAAK,CAAC,CAAC,MACjE,aAAY,KAAK,YAAY;IAAE,SAAS,SAAS;IAAS;GAAS,CAA4B,SACzF,KAAK,YAAY;IAAE,SAAS,SAAS;IAAS,UAAU;GAAM,CAA4B,CAClG;GACA;EACF;EACA,MAAM,WAAW,MAAM;EACvB,MAAM,UAAU,QAAQ,IAAI,UAAU,EAAE;EACxC,IAAI,YAAY,KAAA,GACd;EAEF,QAAQ,OAAO,SAAS,EAAE;EAC1B,IAAI,WAAW,UACb,QAAQ,OAAO,IAAI,MAAM,SAAS,KAAK,CAAC;OAExC,QAAQ,QAAQ,SAAS,MAAM;CAEnC;CACA,MAAM,OAAU,SAAmC,UAAa,UAAU,cACxE,IAAI,SAAY,SAAS,WAAW;EAClC,MAAM,KAAK;EACX,MAAM,QAAQ,iBAAiB;GAC7B,QAAQ,OAAO,EAAE;GACjB,QAAQ,QAAQ;EAClB,GAAG,OAAO;EACV,QAAQ,IAAI,IAAI;GACd,UAAS,UAAS;IAChB,aAAa,KAAK;IAClB,QAAQ,KAAU;GACpB;GACA,SAAQ,UAAS;IACf,aAAa,KAAK;IAClB,OAAO,KAAK;GACd;EACF,CAAC;EACD,KAAK,YAAY;GAAE,GAAG;GAAS;EAAG,CAAC;CACrC,CAAC;CACH,OAAO;EACL,aAAa,IAA0B,EAAE,IAAI,QAAQ,GAAG,CAAC,CAAC;EAC1D,iBAAiB,IAA8B,EAAE,IAAI,YAAY,GAAG,CAAC,CAAC;EAGtE,OAAO,MAAM,SACX,IACE;GAAE,IAAI;GAAQ;GAAM;EAAK,GACzB;GAAE,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,GAAG,KAAK;GAA0B,CAAC;GAAG,SAAS;EAAK,GACtF,KAAK,IAAI,WAAW,IAAO,CAC7B;EACF,MAAM,OAAM,QAAQ,MAAM,IAAoC;GAAE,IAAI;GAAQ;EAAI,GAAG,IAAI,KAAM,KAAA;CAC/F;AACF;;;;;;;AAQA,SAAgB,gBAAgB,UAAyD;CACvF,MAAM,6BAAa,IAAI,IAA8B;CACrD,MAAM,iCAAiB,IAAI,IAA8B;CAEzD,MAAM,QAAQ,YAAY;EACxB,MAAM,QAAQ,MAAM,QAAQ,IAAI,SAAS,KAAI,YAAW,QAAQ,MAAM,CAAC,CAAC;EACxE,WAAW,MAAM;EACjB,MAAM,MAAmB,CAAC;EAC1B,MAAM,SAAS,MAAM,UAAU;GAC7B,KAAK,MAAM,QAAQ,MACjB,IAAI,CAAC,WAAW,IAAI,KAAK,IAAI,GAAG;IAC9B,WAAW,IAAI,KAAK,MAAM,SAAS,MAAM;IACzC,IAAI,KAAK,IAAI;GACf;EAEJ,CAAC;EACD,OAAO;CACT;CAEA,MAAM,YAAY,YAAY;EAC5B,MAAM,QAAQ,MAAM,QAAQ,IAAI,SAAS,KAAI,YAAW,QAAQ,UAAU,CAAC,CAAC;EAC5E,eAAe,MAAM;EACrB,MAAM,MAAuB,CAAC;EAC9B,MAAM,SAAS,MAAM,UAAU;GAC7B,KAAK,MAAM,YAAY,MACrB,IAAI,CAAC,eAAe,IAAI,SAAS,GAAG,GAAG;IACrC,eAAe,IAAI,SAAS,KAAK,SAAS,MAAM;IAChD,IAAI,KAAK,QAAQ;GACnB;EAEJ,CAAC;EACD,OAAO;CACT;CAEA,OAAO;EACL;EACA;EACA,MAAM,OAAO,MAAM,SAAS;GAC1B,IAAI,CAAC,WAAW,IAAI,IAAI,GACtB,MAAM,MAAM;GAEd,MAAM,QAAQ,WAAW,IAAI,IAAI;GACjC,IAAI,UAAU,KAAA,GAEZ,OAAO;IACL,SAAS,CAAC;KAAE,MAAM;KAAQ,MAAM,2BAA2B,KAAK,kBAFpD,CAAC,GAAG,WAAW,KAAK,CAAC,CAAC,CAAC,KAAK,IAE8C,EAAE;IAAG,CAAC;IAC5F,SAAS;GACX;GAEF,OAAO,MAAM,KAAK,MAAM,IAAI;EAC9B;EACA,MAAM,OAAM,QAAO;GACjB,IAAI,CAAC,eAAe,IAAI,GAAG,GACzB,MAAM,UAAU;GAElB,OAAO,eAAe,IAAI,GAAG,CAAC,EAAE,KAAK,GAAG;EAC1C;EACA,eAAe;GACb,KAAK,MAAM,WAAW,UACpB,QAAQ,UAAU;EAEtB;CACF;AACF"}
|