@crowdedkingdoms/crowdyjs 15.12.1-test.1 → 16.0.0-dev.1
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/MIGRATION.md +55 -0
- package/README.md +67 -41
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +3 -0
- package/dist/crowdy-client.d.ts +2 -3
- package/dist/crowdy-client.d.ts.map +1 -1
- package/dist/crowdy-client.js +9 -11
- package/dist/crowdy-dsh/bridge.d.ts +156 -0
- package/dist/crowdy-dsh/bridge.d.ts.map +1 -0
- package/dist/crowdy-dsh/bridge.js +523 -0
- package/dist/crowdy-dsh/index.d.ts +11 -0
- package/dist/crowdy-dsh/index.d.ts.map +1 -0
- package/dist/crowdy-dsh/index.js +10 -0
- package/dist/crowdy-dsh/pane.d.ts +64 -0
- package/dist/crowdy-dsh/pane.d.ts.map +1 -0
- package/dist/crowdy-dsh/pane.js +247 -0
- package/dist/crowdy-dsh/protocol.d.ts +249 -0
- package/dist/crowdy-dsh/protocol.d.ts.map +1 -0
- package/dist/crowdy-dsh/protocol.js +39 -0
- package/dist/crowdy-dsh/transport.d.ts +66 -0
- package/dist/crowdy-dsh/transport.d.ts.map +1 -0
- package/dist/crowdy-dsh/transport.js +88 -0
- package/dist/crowdy-studio/controller.d.ts +8 -0
- package/dist/crowdy-studio/controller.d.ts.map +1 -1
- package/dist/crowdy-studio/controller.js +33 -1
- package/dist/crowdy-studio/dom-shell.d.ts +21 -4
- package/dist/crowdy-studio/dom-shell.d.ts.map +1 -1
- package/dist/crowdy-studio/dom-shell.js +32 -17
- package/dist/crowdy-studio/embed/index.d.ts +1 -1
- package/dist/crowdy-studio/embed/index.d.ts.map +1 -1
- package/dist/crowdy-studio/embed/panel.d.ts +18 -18
- package/dist/crowdy-studio/embed/panel.d.ts.map +1 -1
- package/dist/crowdy-studio/embed/panel.js +7 -21
- package/dist/crowdy-studio/index.d.ts +2 -3
- package/dist/crowdy-studio/index.d.ts.map +1 -1
- package/dist/crowdy-studio/index.js +1 -2
- package/dist/crowdy-studio/mount.d.ts +29 -14
- package/dist/crowdy-studio/mount.d.ts.map +1 -1
- package/dist/crowdy-studio/mount.js +39 -126
- package/dist/crowdy-studio/styles.d.ts +1 -1
- package/dist/crowdy-studio/styles.d.ts.map +1 -1
- package/dist/crowdy-studio/styles.js +5 -0
- package/dist/default-origin.d.ts +6 -6
- package/dist/default-origin.d.ts.map +1 -1
- package/dist/default-origin.js +6 -6
- package/dist/generated/graphql.d.ts +76 -2438
- package/dist/generated/graphql.d.ts.map +1 -1
- package/dist/generated/graphql.js +0 -50
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/kit/npcs.d.ts.map +1 -1
- package/dist/kit/social.d.ts.map +1 -1
- package/dist/{crowdy-agent/errors.d.ts → player-host/agent-errors.d.ts} +1 -1
- package/dist/player-host/agent-errors.d.ts.map +1 -0
- package/dist/player-host/agent-types.d.ts +18 -0
- package/dist/player-host/agent-types.d.ts.map +1 -0
- package/dist/player-host/agent-types.js +7 -0
- package/dist/player-host/index.d.ts +12 -4
- package/dist/player-host/index.d.ts.map +1 -1
- package/dist/player-host/index.js +11 -4
- package/dist/{crowdy-agent/schema.d.ts → player-host/json-schema.d.ts} +1 -1
- package/dist/player-host/json-schema.d.ts.map +1 -0
- package/dist/{crowdy-agent/schema.js → player-host/json-schema.js} +1 -1
- package/dist/player-host/schemas.d.ts +1 -1
- package/dist/player-host/schemas.d.ts.map +1 -1
- package/dist/player-host/schemas.js +2 -2
- package/dist/player-host/types.d.ts +2 -2
- package/dist/player-host/types.d.ts.map +1 -1
- package/package.json +5 -7
- package/dist/crowdy-agent/browser-dispatcher.d.ts +0 -41
- package/dist/crowdy-agent/browser-dispatcher.d.ts.map +0 -1
- package/dist/crowdy-agent/browser-dispatcher.js +0 -264
- package/dist/crowdy-agent/controller.d.ts +0 -152
- package/dist/crowdy-agent/controller.d.ts.map +0 -1
- package/dist/crowdy-agent/controller.js +0 -1289
- package/dist/crowdy-agent/errors.d.ts.map +0 -1
- package/dist/crowdy-agent/graphql-transport.d.ts +0 -154
- package/dist/crowdy-agent/graphql-transport.d.ts.map +0 -1
- package/dist/crowdy-agent/graphql-transport.js +0 -854
- package/dist/crowdy-agent/index.d.ts +0 -17
- package/dist/crowdy-agent/index.d.ts.map +0 -1
- package/dist/crowdy-agent/index.js +0 -16
- package/dist/crowdy-agent/registry.d.ts +0 -29
- package/dist/crowdy-agent/registry.d.ts.map +0 -1
- package/dist/crowdy-agent/registry.js +0 -281
- package/dist/crowdy-agent/schema.d.ts.map +0 -1
- package/dist/crowdy-agent/session-resume.d.ts +0 -24
- package/dist/crowdy-agent/session-resume.d.ts.map +0 -1
- package/dist/crowdy-agent/session-resume.js +0 -76
- package/dist/crowdy-agent/studio-tools.d.ts +0 -15
- package/dist/crowdy-agent/studio-tools.d.ts.map +0 -1
- package/dist/crowdy-agent/studio-tools.js +0 -280
- package/dist/crowdy-agent/tool-descriptors.d.ts +0 -4
- package/dist/crowdy-agent/tool-descriptors.d.ts.map +0 -1
- package/dist/crowdy-agent/tool-descriptors.js +0 -1049
- package/dist/crowdy-agent/transport.d.ts +0 -152
- package/dist/crowdy-agent/transport.d.ts.map +0 -1
- package/dist/crowdy-agent/transport.js +0 -27
- package/dist/crowdy-agent/types.d.ts +0 -328
- package/dist/crowdy-agent/types.d.ts.map +0 -1
- package/dist/crowdy-agent/types.js +0 -37
- package/dist/crowdy-studio/agent-dom-shell.d.ts +0 -68
- package/dist/crowdy-studio/agent-dom-shell.d.ts.map +0 -1
- package/dist/crowdy-studio/agent-dom-shell.js +0 -418
- package/dist/player-host/control-banner.d.ts +0 -40
- package/dist/player-host/control-banner.d.ts.map +0 -1
- package/dist/player-host/control-banner.js +0 -252
- package/dist/player-host/control-gate.d.ts +0 -77
- package/dist/player-host/control-gate.d.ts.map +0 -1
- package/dist/player-host/control-gate.js +0 -197
- package/dist/player-host/lease-manager.d.ts +0 -66
- package/dist/player-host/lease-manager.d.ts.map +0 -1
- package/dist/player-host/lease-manager.js +0 -428
- package/dist/player-host/tools.d.ts +0 -13
- package/dist/player-host/tools.d.ts.map +0 -1
- package/dist/player-host/tools.js +0 -79
- /package/dist/{crowdy-agent/errors.js → player-host/agent-errors.js} +0 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page half of the Studio ↔ harness bridge.
|
|
3
|
+
*
|
|
4
|
+
* Owns the iframe handshake (seed files, persistence scope, boot nonce), the
|
|
5
|
+
* `BroadcastChannel` the worker's tools call into, and the mapping from those
|
|
6
|
+
* calls onto the headless `CrowdyStudioController` and the game's
|
|
7
|
+
* `PlayerHostAdapterV1` (observe only). Everything the model asks for runs
|
|
8
|
+
* here with the player's own authority, through the same controller methods
|
|
9
|
+
* the human buttons use; a live deploy additionally waits for the player to
|
|
10
|
+
* confirm on the page.
|
|
11
|
+
*
|
|
12
|
+
* Where the player's app token goes, precisely: the harness worker needs it
|
|
13
|
+
* to call the metered model endpoint and the GraphQL API as the player. It is
|
|
14
|
+
* sent over the channel (`page.hello`, `page.token`), held in the worker's
|
|
15
|
+
* memory as `process.env.CROWDY_APP_TOKEN`, and never written to a seed file,
|
|
16
|
+
* the virtual filesystem or OPFS. The model has no tool that reads the
|
|
17
|
+
* environment, and the Crowdy filesystem backend serves only the project
|
|
18
|
+
* mount, so `read_file` cannot reach it either.
|
|
19
|
+
*
|
|
20
|
+
* The iframe is same-origin by design: `BroadcastChannel` and OPFS are both
|
|
21
|
+
* origin-scoped. A `sandbox` attribute therefore cannot isolate it, and the
|
|
22
|
+
* Content-Security-Policy the host serves on `/dsh/*` is the control that
|
|
23
|
+
* matters (see the-construct's `security-headers.mjs`). Moving the harness to
|
|
24
|
+
* its own origin with a `MessageChannel` relay is tracked as a follow-up.
|
|
25
|
+
*/
|
|
26
|
+
import { CROWDY_DSH_PROTOCOL_VERSION, isDshBridgeFrame, isDshFrameMessage, } from './protocol.js';
|
|
27
|
+
const DEFAULT_MAX_SIDE = 1280;
|
|
28
|
+
const HELLO_DEBOUNCE_MS = 1500;
|
|
29
|
+
export class StudioDshBridge {
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
/** Unguessable: any same-origin script can open a `BroadcastChannel` by name. */
|
|
33
|
+
this.channelName = `crowdy-dsh:${randomId()}`;
|
|
34
|
+
/**
|
|
35
|
+
* One-time secret carried by every frame on the channel. The iframe boot
|
|
36
|
+
* message delivers it to the worker; a host that boots the harness itself
|
|
37
|
+
* (tests, a native shell) reads it here and must pass it along.
|
|
38
|
+
*/
|
|
39
|
+
this.nonce = randomId();
|
|
40
|
+
this.channel = null;
|
|
41
|
+
this.frameListener = null;
|
|
42
|
+
this.booted = false;
|
|
43
|
+
this.lastHelloAt = 0;
|
|
44
|
+
this.lastToken = null;
|
|
45
|
+
this.unsubscribeState = null;
|
|
46
|
+
this.tokenTimer = null;
|
|
47
|
+
this.captureCounter = 0;
|
|
48
|
+
this.inflight = new Set();
|
|
49
|
+
this.models = [];
|
|
50
|
+
this.status = { phase: 'idle' };
|
|
51
|
+
/** One live-deploy question at a time: a second request must not replace the first prompt. */
|
|
52
|
+
this.liveDeployPending = false;
|
|
53
|
+
}
|
|
54
|
+
get currentStatus() {
|
|
55
|
+
return this.status;
|
|
56
|
+
}
|
|
57
|
+
/** The models the harness was booted with. */
|
|
58
|
+
get catalog() {
|
|
59
|
+
return this.models;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Attach to the iframe: answer its `ready` with the boot files and start the
|
|
63
|
+
* broadcast channel. Resolves once the harness reports it booted.
|
|
64
|
+
*/
|
|
65
|
+
async attach(frame) {
|
|
66
|
+
this.detach();
|
|
67
|
+
this.setStatus({ phase: 'waiting-for-frame' });
|
|
68
|
+
this.models = await this.options.transport.models(this.options.appId);
|
|
69
|
+
if (this.models.length === 0) {
|
|
70
|
+
throw new Error('No model is allowed for this app; ask the app owner to enable the Studio agent.');
|
|
71
|
+
}
|
|
72
|
+
this.connect();
|
|
73
|
+
await new Promise((resolve, reject) => {
|
|
74
|
+
const timeout = setTimeout(() => {
|
|
75
|
+
cleanup();
|
|
76
|
+
reject(new Error('The agent did not start within 60s.'));
|
|
77
|
+
}, 60000);
|
|
78
|
+
const cleanup = () => {
|
|
79
|
+
clearTimeout(timeout);
|
|
80
|
+
};
|
|
81
|
+
this.frameListener = (event) => {
|
|
82
|
+
if (event.source !== frame.contentWindow)
|
|
83
|
+
return;
|
|
84
|
+
if (event.origin !== location.origin)
|
|
85
|
+
return;
|
|
86
|
+
const data = event.data;
|
|
87
|
+
if (!isDshFrameMessage(data))
|
|
88
|
+
return;
|
|
89
|
+
if (data.type === 'crowdy-dsh:ready') {
|
|
90
|
+
if (this.booted)
|
|
91
|
+
return;
|
|
92
|
+
this.setStatus({ phase: 'booting' });
|
|
93
|
+
frame.contentWindow?.postMessage(this.bootMessage(), location.origin);
|
|
94
|
+
}
|
|
95
|
+
else if (data.type === 'crowdy-dsh:booted') {
|
|
96
|
+
this.booted = true;
|
|
97
|
+
cleanup();
|
|
98
|
+
this.setStatus({ phase: 'ready' });
|
|
99
|
+
this.hello(true);
|
|
100
|
+
resolve();
|
|
101
|
+
}
|
|
102
|
+
else if (data.type === 'crowdy-dsh:failed') {
|
|
103
|
+
cleanup();
|
|
104
|
+
this.setStatus({ phase: 'failed', message: data.message });
|
|
105
|
+
reject(new Error(data.message));
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
window.addEventListener('message', this.frameListener);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Join the broadcast channel and start mirroring controller state, without
|
|
113
|
+
* an iframe handshake. `attach` calls this; a host that boots the harness
|
|
114
|
+
* itself (tests, a native shell) calls it directly.
|
|
115
|
+
*/
|
|
116
|
+
connect() {
|
|
117
|
+
if (this.channel)
|
|
118
|
+
return;
|
|
119
|
+
this.openChannel();
|
|
120
|
+
this.watchController();
|
|
121
|
+
}
|
|
122
|
+
detach() {
|
|
123
|
+
if (this.frameListener)
|
|
124
|
+
window.removeEventListener('message', this.frameListener);
|
|
125
|
+
this.frameListener = null;
|
|
126
|
+
this.unsubscribeState?.();
|
|
127
|
+
this.unsubscribeState = null;
|
|
128
|
+
if (this.tokenTimer)
|
|
129
|
+
clearInterval(this.tokenTimer);
|
|
130
|
+
this.tokenTimer = null;
|
|
131
|
+
if (this.channel) {
|
|
132
|
+
this.emit('page.bye', {});
|
|
133
|
+
this.channel.close();
|
|
134
|
+
}
|
|
135
|
+
this.channel = null;
|
|
136
|
+
this.booted = false;
|
|
137
|
+
this.inflight.clear();
|
|
138
|
+
this.setStatus({ phase: 'idle' });
|
|
139
|
+
}
|
|
140
|
+
/** Push a capture the player took from the pane header into `captures/`. */
|
|
141
|
+
async shareCapture(label) {
|
|
142
|
+
const shot = await this.screenshot(label);
|
|
143
|
+
this.emit('page.capture', shot);
|
|
144
|
+
return shot;
|
|
145
|
+
}
|
|
146
|
+
/** Queue text into the agent's current session (e.g. "Fix with AI"). */
|
|
147
|
+
prompt(text, mode = 'queue') {
|
|
148
|
+
if (!this.channel) {
|
|
149
|
+
this.options.onWarning?.('The agent is not running yet; open the Agent pane first.');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this.emit('page.prompt', { text, mode });
|
|
153
|
+
}
|
|
154
|
+
/** Re-send the token (the page refreshed it). */
|
|
155
|
+
tokenChanged() {
|
|
156
|
+
const token = this.options.getToken();
|
|
157
|
+
if (token && token !== this.lastToken) {
|
|
158
|
+
this.lastToken = token;
|
|
159
|
+
this.emit('page.token', { appToken: token });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// ── boot ────────────────────────────────────────────────────────────────────
|
|
163
|
+
bootMessage() {
|
|
164
|
+
const projectId = this.options.controller.getState().project?.projectId ?? '';
|
|
165
|
+
// No token here: `crowdy.json` lands in the worker's filesystem. The
|
|
166
|
+
// token follows over the channel once the worker says it is ready.
|
|
167
|
+
const crowdy = {
|
|
168
|
+
graphqlUrl: this.options.graphqlUrl,
|
|
169
|
+
appId: this.options.appId,
|
|
170
|
+
projectId,
|
|
171
|
+
bridgeChannel: this.channelName,
|
|
172
|
+
bridgeNonce: this.nonce,
|
|
173
|
+
githubFirst: true,
|
|
174
|
+
root: '/dsh/workspace',
|
|
175
|
+
persistScope: this.options.persistScope,
|
|
176
|
+
};
|
|
177
|
+
return {
|
|
178
|
+
type: 'crowdy-dsh:boot',
|
|
179
|
+
files: {
|
|
180
|
+
'crowdy.json': JSON.stringify(crowdy, null, 2),
|
|
181
|
+
'settings.yaml': renderSettingsYaml(this.options.transport.modelBaseUrl, this.models),
|
|
182
|
+
},
|
|
183
|
+
nonce: this.nonce,
|
|
184
|
+
persistScope: this.options.persistScope,
|
|
185
|
+
mount: '/dsh/workspace',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
// ── channel ─────────────────────────────────────────────────────────────────
|
|
189
|
+
openChannel() {
|
|
190
|
+
this.channel = new BroadcastChannel(this.channelName);
|
|
191
|
+
this.channel.addEventListener('message', (event) => {
|
|
192
|
+
void this.handle(event.data);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
hello(force = false) {
|
|
196
|
+
const now = Date.now();
|
|
197
|
+
if (!force && now - this.lastHelloAt < HELLO_DEBOUNCE_MS)
|
|
198
|
+
return;
|
|
199
|
+
this.lastHelloAt = now;
|
|
200
|
+
const token = this.options.getToken();
|
|
201
|
+
this.emit('page.hello', {
|
|
202
|
+
appId: this.options.appId,
|
|
203
|
+
projectId: this.options.controller.getState().project?.projectId ?? null,
|
|
204
|
+
...(token ? { appToken: token } : {}),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
emit(event, payload) {
|
|
208
|
+
if (!this.channel)
|
|
209
|
+
return;
|
|
210
|
+
const frame = { v: CROWDY_DSH_PROTOCOL_VERSION, n: this.nonce, from: 'page', t: 'event', event, payload };
|
|
211
|
+
this.channel.postMessage(frame);
|
|
212
|
+
}
|
|
213
|
+
reply(id, result) {
|
|
214
|
+
const frame = { v: CROWDY_DSH_PROTOCOL_VERSION, n: this.nonce, from: 'page', t: 'res', id, result };
|
|
215
|
+
this.channel?.postMessage(frame);
|
|
216
|
+
}
|
|
217
|
+
fail(id, error) {
|
|
218
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
219
|
+
const frame = { v: CROWDY_DSH_PROTOCOL_VERSION, n: this.nonce, from: 'page', t: 'err', id, code: 'PAGE_ERROR', message };
|
|
220
|
+
this.channel?.postMessage(frame);
|
|
221
|
+
}
|
|
222
|
+
async handle(data) {
|
|
223
|
+
// Frames without this boot's nonce are dropped unread: the channel is
|
|
224
|
+
// reachable by any same-origin script, the nonce only by the worker we booted.
|
|
225
|
+
if (!isDshBridgeFrame(data, this.nonce) || data.from !== 'worker')
|
|
226
|
+
return;
|
|
227
|
+
if (data.t === 'event') {
|
|
228
|
+
this.onWorkerEvent(data.event, data.payload);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (data.t !== 'req')
|
|
232
|
+
return;
|
|
233
|
+
const pending = { id: data.id, method: data.method };
|
|
234
|
+
this.inflight.add(pending);
|
|
235
|
+
try {
|
|
236
|
+
const result = await this.dispatch(data.method, data.params);
|
|
237
|
+
this.reply(data.id, result);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
this.fail(data.id, error);
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
this.inflight.delete(pending);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
onWorkerEvent(event, payload) {
|
|
247
|
+
switch (event) {
|
|
248
|
+
case 'worker.ready': {
|
|
249
|
+
const ready = payload;
|
|
250
|
+
this.setStatus({ phase: 'ready', ...(ready.store ? { store: ready.store } : {}) });
|
|
251
|
+
this.hello();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
case 'worker.fileChanged': {
|
|
255
|
+
const change = payload;
|
|
256
|
+
void this.options.controller.reloadProject().then(() => this.options.onFileChanged?.(change), (error) => this.options.onWarning?.(`The agent changed ${change.path} but the editor could not reload: ${String(error)}`));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
case 'worker.warning': {
|
|
260
|
+
this.options.onWarning?.(payload.message);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
case 'worker.openFile': {
|
|
264
|
+
const ref = payload;
|
|
265
|
+
try {
|
|
266
|
+
this.options.controller.openFile({ source: 'PROJECT', target: ref.target, path: ref.path });
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// The file may not exist yet; the editor shows what it has.
|
|
270
|
+
}
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
default:
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// ── requests ────────────────────────────────────────────────────────────────
|
|
278
|
+
async dispatch(method, params) {
|
|
279
|
+
const controller = this.options.controller;
|
|
280
|
+
switch (method) {
|
|
281
|
+
case 'studio.screenshot':
|
|
282
|
+
return this.screenshot(params?.label);
|
|
283
|
+
case 'studio.draftTest':
|
|
284
|
+
return this.build('draft');
|
|
285
|
+
case 'studio.deployLive': {
|
|
286
|
+
const confirm = this.options.confirmLiveDeploy;
|
|
287
|
+
if (!confirm)
|
|
288
|
+
throw new Error('Live deploys from the agent are not enabled on this page; use the Deploy button in Crowdy Studio.');
|
|
289
|
+
// Serialized: two concurrent requests would share one prompt and the
|
|
290
|
+
// player could answer a question other than the one on screen.
|
|
291
|
+
if (this.liveDeployPending)
|
|
292
|
+
throw new Error('A live deploy is already waiting for the player to answer. Wait for that answer before asking again.');
|
|
293
|
+
this.liveDeployPending = true;
|
|
294
|
+
let approved;
|
|
295
|
+
try {
|
|
296
|
+
const projectName = controller.getState().project?.metadata.name ?? 'this project';
|
|
297
|
+
approved = await confirm({ projectName });
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
this.liveDeployPending = false;
|
|
301
|
+
}
|
|
302
|
+
if (!approved)
|
|
303
|
+
throw new Error('The player declined the live deploy. Keep working with draft tests.');
|
|
304
|
+
return this.build('live');
|
|
305
|
+
}
|
|
306
|
+
case 'studio.runtimeStatus':
|
|
307
|
+
return { runtime: this.runtimeStatus(controller.getState()) };
|
|
308
|
+
case 'studio.runtimeLogs': {
|
|
309
|
+
const limit = clampLimit(params?.limit, 40);
|
|
310
|
+
controller.setSurfaceVisible('logs', true);
|
|
311
|
+
const lines = controller.getState().logs.map(formatRunLine);
|
|
312
|
+
return { lines: lines.slice(-limit), truncated: lines.length > limit };
|
|
313
|
+
}
|
|
314
|
+
case 'studio.clientLogs': {
|
|
315
|
+
const limit = clampLimit(params?.limit, 80);
|
|
316
|
+
const lines = [...(this.options.host?.clientLogs?.() ?? [])];
|
|
317
|
+
return { lines: lines.slice(-limit), truncated: lines.length > limit };
|
|
318
|
+
}
|
|
319
|
+
case 'studio.projectList': {
|
|
320
|
+
const state = controller.getState();
|
|
321
|
+
return {
|
|
322
|
+
projects: state.projects.map(summarize),
|
|
323
|
+
currentProjectId: state.project?.projectId ?? null,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
case 'studio.projectOpen': {
|
|
327
|
+
const { projectId } = params;
|
|
328
|
+
await controller.switchProject(projectId);
|
|
329
|
+
const project = controller.getState().project;
|
|
330
|
+
if (!project)
|
|
331
|
+
throw new Error(`Project ${projectId} did not open.`);
|
|
332
|
+
this.emit('page.project', { projectId: project.projectId });
|
|
333
|
+
return { project: summarizeProject(project.projectId, project.metadata.name, project.kind, project.revision.savedAt) };
|
|
334
|
+
}
|
|
335
|
+
case 'studio.projectCreate': {
|
|
336
|
+
const { name, template } = params;
|
|
337
|
+
const kind = template === 'server' ? 'SERVER' : template === 'client' ? 'CLIENT' : 'FULL_STACK';
|
|
338
|
+
const project = await controller.createProject({ name, kind: kind });
|
|
339
|
+
this.emit('page.project', { projectId: project.projectId });
|
|
340
|
+
return { project: summarizeProject(project.projectId, project.metadata.name, project.kind, project.revision.savedAt) };
|
|
341
|
+
}
|
|
342
|
+
case 'game.observe': {
|
|
343
|
+
const host = this.options.host?.playerHost;
|
|
344
|
+
if (!host)
|
|
345
|
+
throw new Error('This game does not expose a player host; nothing to observe.');
|
|
346
|
+
const observation = await host.observe({ detail: 'STANDARD', maxNearbyActors: 16, maxNearbyVoxels: 64 });
|
|
347
|
+
this.emit('page.context', { observation });
|
|
348
|
+
return { observation, capturedAt: new Date().toISOString() };
|
|
349
|
+
}
|
|
350
|
+
default:
|
|
351
|
+
throw new Error(`Unsupported bridge method ${String(method)}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async build(mode) {
|
|
355
|
+
const controller = this.options.controller;
|
|
356
|
+
if (!controller.getState().project)
|
|
357
|
+
throw new Error('No project is open in Crowdy Studio.');
|
|
358
|
+
const result = mode === 'draft' ? await controller.testDraft() : await controller.deployLive();
|
|
359
|
+
const state = controller.getState();
|
|
360
|
+
const diagnostics = [...state.authoritativeDiagnostics, ...state.localDiagnostics].map(toDshDiagnostic);
|
|
361
|
+
const runtime = this.runtimeStatus(state);
|
|
362
|
+
const ok = result.status === 'RUNNING';
|
|
363
|
+
const build = {
|
|
364
|
+
ok,
|
|
365
|
+
mode,
|
|
366
|
+
summary: result.message,
|
|
367
|
+
diagnostics,
|
|
368
|
+
buildLog: state.buildOutput.slice(-12000),
|
|
369
|
+
runtime,
|
|
370
|
+
};
|
|
371
|
+
this.emit('page.context', { diagnostics, runtime });
|
|
372
|
+
if (ok && result.targets.includes('CLIENT') && this.options.host?.captureFrame) {
|
|
373
|
+
// Let one frame render with the new client module before capturing.
|
|
374
|
+
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
375
|
+
try {
|
|
376
|
+
build.screenshot = await this.screenshot(`after ${mode} ${mode === 'draft' ? 'test' : 'deploy'}`);
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
// A missing capture is not a failed build.
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return build;
|
|
383
|
+
}
|
|
384
|
+
runtimeStatus(state) {
|
|
385
|
+
const targets = state.project ? [...new Set(state.project.files.map((file) => file.target))] : [];
|
|
386
|
+
return targets.map((target) => ({
|
|
387
|
+
target,
|
|
388
|
+
phase: state.runtime.target === target || state.runtime.target === undefined ? state.runtime.phase : 'IDLE',
|
|
389
|
+
moduleName: target === 'SERVER' ? state.project?.metadata.serverModuleName : state.project?.metadata.clientModuleName,
|
|
390
|
+
runningRevision: state.runtimeSync.state === 'RUNNING_SAVED' || state.runtimeSync.state === 'RUNNING_STALE' ? state.runtimeSync.savedRevisionId : undefined,
|
|
391
|
+
savedRevision: state.project?.revision.id,
|
|
392
|
+
message: state.runtime.message,
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
async screenshot(label) {
|
|
396
|
+
const capture = this.options.host?.captureFrame;
|
|
397
|
+
if (!capture)
|
|
398
|
+
throw new Error('This game does not provide screenshots to the agent.');
|
|
399
|
+
const source = await capture();
|
|
400
|
+
if (!source)
|
|
401
|
+
throw new Error('Nothing to capture right now (no frame rendered).');
|
|
402
|
+
const encoded = await encodePng(source, this.options.maxCaptureSide ?? DEFAULT_MAX_SIDE);
|
|
403
|
+
this.captureCounter += 1;
|
|
404
|
+
const caption = [label, this.options.host?.describeView?.()].filter(Boolean).join(' — ');
|
|
405
|
+
return {
|
|
406
|
+
name: `capture-${String(this.captureCounter).padStart(4, '0')}.png`,
|
|
407
|
+
mediaType: 'image/png',
|
|
408
|
+
bytes: encoded.bytes,
|
|
409
|
+
width: encoded.width,
|
|
410
|
+
height: encoded.height,
|
|
411
|
+
...(caption ? { caption } : {}),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
// ── controller watch ────────────────────────────────────────────────────────
|
|
415
|
+
watchController() {
|
|
416
|
+
const controller = this.options.controller;
|
|
417
|
+
this.lastProjectId = controller.getState().project?.projectId ?? null;
|
|
418
|
+
this.lastSaveState = controller.getState().saveState;
|
|
419
|
+
this.unsubscribeState = controller.subscribe((state) => {
|
|
420
|
+
const projectId = state.project?.projectId ?? null;
|
|
421
|
+
if (projectId !== this.lastProjectId) {
|
|
422
|
+
this.lastProjectId = projectId;
|
|
423
|
+
this.emit('page.project', { projectId });
|
|
424
|
+
}
|
|
425
|
+
if (state.saveState === 'SAVED' && this.lastSaveState !== 'SAVED' && this.inflight.size === 0) {
|
|
426
|
+
this.emit('page.saved', { revision: state.project?.revision.id });
|
|
427
|
+
}
|
|
428
|
+
this.lastSaveState = state.saveState;
|
|
429
|
+
});
|
|
430
|
+
this.tokenTimer = setInterval(() => this.tokenChanged(), 20000);
|
|
431
|
+
}
|
|
432
|
+
setStatus(status) {
|
|
433
|
+
this.status = status;
|
|
434
|
+
this.options.onStatus?.(status);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
438
|
+
/** 128 bits from the platform CSPRNG, hex; falls back to `Math.random` only where `crypto` is absent. */
|
|
439
|
+
function randomId() {
|
|
440
|
+
const c = globalThis.crypto;
|
|
441
|
+
if (c?.randomUUID)
|
|
442
|
+
return c.randomUUID().replace(/-/g, '');
|
|
443
|
+
if (c?.getRandomValues) {
|
|
444
|
+
const bytes = c.getRandomValues(new Uint8Array(16));
|
|
445
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
446
|
+
}
|
|
447
|
+
return `${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
|
448
|
+
}
|
|
449
|
+
function clampLimit(value, fallback) {
|
|
450
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
451
|
+
return fallback;
|
|
452
|
+
return Math.max(1, Math.min(400, Math.floor(value)));
|
|
453
|
+
}
|
|
454
|
+
function formatRunLine(run) {
|
|
455
|
+
const status = run.success ? 'ok' : 'FAILED';
|
|
456
|
+
const error = run.errorMessage ? ` — ${run.errorMessage}` : '';
|
|
457
|
+
return `${run.startedAt} ${run.moduleName} ${run.triggerSource} ${status} ${Math.round(run.durationUs / 1000)}ms fuel=${run.fuelUsed}${error}`;
|
|
458
|
+
}
|
|
459
|
+
function toDshDiagnostic(diagnostic) {
|
|
460
|
+
return {
|
|
461
|
+
target: diagnostic.target,
|
|
462
|
+
path: diagnostic.path,
|
|
463
|
+
line: diagnostic.line,
|
|
464
|
+
column: diagnostic.column,
|
|
465
|
+
severity: diagnostic.severity === 'hint' ? 'info' : diagnostic.severity,
|
|
466
|
+
message: diagnostic.code ? `${diagnostic.message} [${diagnostic.code}]` : diagnostic.message,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function summarize(summary) {
|
|
470
|
+
return { projectId: summary.projectId, name: summary.name, kind: String(summary.kind), updatedAt: summary.updatedAt };
|
|
471
|
+
}
|
|
472
|
+
function summarizeProject(projectId, name, kind, updatedAt) {
|
|
473
|
+
return { projectId, name, kind, updatedAt };
|
|
474
|
+
}
|
|
475
|
+
/** Render the harness settings that point its native adapter at the metered endpoint. */
|
|
476
|
+
export function renderSettingsYaml(modelBaseUrl, models) {
|
|
477
|
+
const quote = (value) => JSON.stringify(value);
|
|
478
|
+
const lines = [
|
|
479
|
+
'# Written by Crowdy Studio at boot; the metered model endpoint fronts the platform key.',
|
|
480
|
+
'llm-deepseek:',
|
|
481
|
+
' apiKeyEnv: CROWDY_APP_TOKEN',
|
|
482
|
+
` baseURL: ${quote(modelBaseUrl)}`,
|
|
483
|
+
' thinking: disabled',
|
|
484
|
+
' models:',
|
|
485
|
+
];
|
|
486
|
+
for (const model of models) {
|
|
487
|
+
lines.push(` - id: ${quote(model.id)}`);
|
|
488
|
+
lines.push(` name: ${quote(model.name)}`);
|
|
489
|
+
if (model.contextWindow)
|
|
490
|
+
lines.push(` contextWindow: ${model.contextWindow}`);
|
|
491
|
+
lines.push(' maxTokens: 8192');
|
|
492
|
+
lines.push(` inputModalities: [${model.inputModalities.map(quote).join(', ')}]`);
|
|
493
|
+
}
|
|
494
|
+
const first = models[0];
|
|
495
|
+
if (first) {
|
|
496
|
+
lines.push('agent-default-model:', ' provider: deepseek-official', ` model: ${quote(first.id)}`);
|
|
497
|
+
}
|
|
498
|
+
return `${lines.join('\n')}\n`;
|
|
499
|
+
}
|
|
500
|
+
/** Downscale and PNG-encode a frame in the page. */
|
|
501
|
+
async function encodePng(source, maxSide) {
|
|
502
|
+
let bitmap;
|
|
503
|
+
if (source instanceof Blob)
|
|
504
|
+
bitmap = await createImageBitmap(source);
|
|
505
|
+
else
|
|
506
|
+
bitmap = source;
|
|
507
|
+
const scale = Math.min(1, maxSide / Math.max(bitmap.width, bitmap.height));
|
|
508
|
+
const width = Math.max(1, Math.round(bitmap.width * scale));
|
|
509
|
+
const height = Math.max(1, Math.round(bitmap.height * scale));
|
|
510
|
+
const canvas = document.createElement('canvas');
|
|
511
|
+
canvas.width = width;
|
|
512
|
+
canvas.height = height;
|
|
513
|
+
const context = canvas.getContext('2d');
|
|
514
|
+
if (!context)
|
|
515
|
+
throw new Error('2D canvas unavailable for the capture.');
|
|
516
|
+
context.drawImage(bitmap, 0, 0, width, height);
|
|
517
|
+
if (source instanceof Blob && 'close' in bitmap)
|
|
518
|
+
bitmap.close();
|
|
519
|
+
const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
|
|
520
|
+
if (!blob)
|
|
521
|
+
throw new Error('PNG encoding failed.');
|
|
522
|
+
return { bytes: await blob.arrayBuffer(), width, height };
|
|
523
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crowdy Studio agent pane: the DeepSeek Harness running in the player's
|
|
3
|
+
* browser, embedded beside the Studio editor and bridged to it.
|
|
4
|
+
*
|
|
5
|
+
* @module @crowdedkingdoms/crowdyjs/crowdy-dsh
|
|
6
|
+
*/
|
|
7
|
+
export { StudioDshBridge, renderSettingsYaml, type CrowdyStudioDshHost, type StudioDshBridgeOptions, type StudioDshBridgeStatus, } from './bridge.js';
|
|
8
|
+
export { CrowdyStudioDshPane, CROWDY_STUDIO_DSH_STYLES, type CrowdyStudioDshPaneOptions } from './pane.js';
|
|
9
|
+
export { CrowdyStudioDshTransport, type CrowdyStudioModelCatalogEntry, type CrowdyStudioModelUsage, type CrowdyStudioModelUsageEntry, type CrowdyStudioProviderConsent, } from './transport.js';
|
|
10
|
+
export * from './protocol.js';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/crowdy-dsh/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,GAC3B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,mBAAmB,EAAE,wBAAwB,EAAE,KAAK,0BAA0B,EAAE,MAAM,WAAW,CAAC;AAC3G,OAAO,EACL,wBAAwB,EACxB,KAAK,6BAA6B,EAClC,KAAK,sBAAsB,EAC3B,KAAK,2BAA2B,EAChC,KAAK,2BAA2B,GACjC,MAAM,gBAAgB,CAAC;AACxB,cAAc,eAAe,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crowdy Studio agent pane: the DeepSeek Harness running in the player's
|
|
3
|
+
* browser, embedded beside the Studio editor and bridged to it.
|
|
4
|
+
*
|
|
5
|
+
* @module @crowdedkingdoms/crowdyjs/crowdy-dsh
|
|
6
|
+
*/
|
|
7
|
+
export { StudioDshBridge, renderSettingsYaml, } from './bridge.js';
|
|
8
|
+
export { CrowdyStudioDshPane, CROWDY_STUDIO_DSH_STYLES } from './pane.js';
|
|
9
|
+
export { CrowdyStudioDshTransport, } from './transport.js';
|
|
10
|
+
export * from './protocol.js';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Studio agent pane: a same-origin iframe hosting the DeepSeek Harness
|
|
3
|
+
* web UI (running in a Web Worker) plus the Crowdy chrome around it — the
|
|
4
|
+
* provider-data notice the player accepts once, a Screenshot button, the
|
|
5
|
+
* payer / spend line, and the bridge status.
|
|
6
|
+
*
|
|
7
|
+
* The harness owns the conversation UI (chat, diffs, plan mode, approvals);
|
|
8
|
+
* this pane owns what the game knows: who pays, whether the player consented,
|
|
9
|
+
* and the capture button that pushes a frame into the model's `captures/`.
|
|
10
|
+
*/
|
|
11
|
+
import type { CrowdyStudioController } from '../crowdy-studio/controller.js';
|
|
12
|
+
import type { StudioLayoutController } from '../crowdy-studio/layout.js';
|
|
13
|
+
import { StudioDshBridge, type CrowdyStudioDshHost } from './bridge.js';
|
|
14
|
+
import { CrowdyStudioDshTransport } from './transport.js';
|
|
15
|
+
export interface CrowdyStudioDshPaneOptions {
|
|
16
|
+
controller: CrowdyStudioController;
|
|
17
|
+
transport: CrowdyStudioDshTransport;
|
|
18
|
+
appId: string;
|
|
19
|
+
/** Where the packed harness page is served, e.g. `/dsh/`. */
|
|
20
|
+
webBase: string;
|
|
21
|
+
/** Stable per-player key for session persistence (never a token). */
|
|
22
|
+
persistScope: string;
|
|
23
|
+
getToken(): string | null;
|
|
24
|
+
graphqlUrl: string;
|
|
25
|
+
host?: CrowdyStudioDshHost;
|
|
26
|
+
/** Studio origin for the wallet link; omitted hides the link. */
|
|
27
|
+
studioOrigin?: string;
|
|
28
|
+
layout?: StudioLayoutController;
|
|
29
|
+
onWarning?(message: string): void;
|
|
30
|
+
}
|
|
31
|
+
export declare class CrowdyStudioDshPane {
|
|
32
|
+
private readonly options;
|
|
33
|
+
readonly root: HTMLElement;
|
|
34
|
+
readonly bridge: StudioDshBridge;
|
|
35
|
+
private readonly frame;
|
|
36
|
+
private readonly status;
|
|
37
|
+
private readonly spend;
|
|
38
|
+
private readonly notice;
|
|
39
|
+
private readonly captureButton;
|
|
40
|
+
private readonly message;
|
|
41
|
+
private usageTimer;
|
|
42
|
+
private started;
|
|
43
|
+
private disposed;
|
|
44
|
+
constructor(parent: HTMLElement, options: CrowdyStudioDshPaneOptions);
|
|
45
|
+
/**
|
|
46
|
+
* The page-side gate on a live deploy. The harness UI has its own approval
|
|
47
|
+
* inside the iframe, but the page cannot see it and the channel is open to
|
|
48
|
+
* any same-origin script, so the player confirms here too. Auto-declines
|
|
49
|
+
* after a minute so a stale prompt cannot be accepted by accident later.
|
|
50
|
+
*/
|
|
51
|
+
private confirmLiveDeploy;
|
|
52
|
+
/** Boot once the pane is visible; the notice gates the first boot on consent. */
|
|
53
|
+
start(): Promise<void>;
|
|
54
|
+
dispose(): void;
|
|
55
|
+
private boot;
|
|
56
|
+
private renderNotice;
|
|
57
|
+
private renderStatus;
|
|
58
|
+
private refreshUsage;
|
|
59
|
+
private renderSpend;
|
|
60
|
+
private say;
|
|
61
|
+
private run;
|
|
62
|
+
}
|
|
63
|
+
export declare const CROWDY_STUDIO_DSH_STYLES = "\n.ck-crowdy-studio-dsh{flex:none;display:flex;flex-direction:column;min-width:0;min-height:0;border-left:1px solid var(--ck-line);background:#0b1220}\n.ck-crowdy-studio-dsh>header{display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid var(--ck-line)}\n.ck-crowdy-studio-dsh>header h2{margin:0;font-size:13px;flex:none}\n.ck-crowdy-studio-dsh-status{flex:1;min-width:0;color:var(--ck-muted);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.ck-crowdy-studio-dsh[data-phase=failed] .ck-crowdy-studio-dsh-status{color:#fca5a5}\n.ck-crowdy-studio-dsh-frame{flex:1;min-height:0;width:100%;border:0;background:#fff}\n.ck-crowdy-studio-dsh-notice{padding:10px;font-size:12px;color:var(--ck-text);border-bottom:1px solid var(--ck-line)}\n.ck-crowdy-studio-dsh-notice p{margin:0 0 8px}\n.ck-crowdy-studio-dsh-notice-actions{display:flex;gap:6px}\n.ck-crowdy-studio-dsh-spend{padding:4px 10px;font-size:11px;color:var(--ck-muted);border-top:1px solid var(--ck-line)}\n.ck-crowdy-studio-dsh-spend a{color:#bae6fd}\n.ck-crowdy-studio-dsh-message{padding:4px 10px;font-size:11px;color:#fde68a;border-top:1px solid var(--ck-line)}\n.ck-crowdy-studio-dsh-message button{font-size:11px;padding:2px 8px;margin-left:4px}\n.ck-crowdy-studio-dsh-confirm{background:#b91c1c;color:#fff;border:0;border-radius:3px}\n";
|
|
64
|
+
//# sourceMappingURL=pane.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pane.d.ts","sourceRoot":"","sources":["../../src/crowdy-dsh/pane.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAC7E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAAE,eAAe,EAAE,KAAK,mBAAmB,EAA8B,MAAM,aAAa,CAAC;AACpG,OAAO,EAAE,wBAAwB,EAA+B,MAAM,gBAAgB,CAAC;AAEvF,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,sBAAsB,CAAC;IACnC,SAAS,EAAE,wBAAwB,CAAC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,IAAI,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,mBAAmB,CAAC;IAC3B,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,sBAAsB,CAAC;IAChC,SAAS,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAID,qBAAa,mBAAmB;IAe5B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAd1B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAC1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoB;IAClD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAS;gBAGvB,MAAM,EAAE,WAAW,EACF,OAAO,EAAE,0BAA0B;IAgEtD;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;IA4BzB,iFAAiF;IAC3E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB5B,OAAO,IAAI,IAAI;YAOD,IAAI;IASlB,OAAO,CAAC,YAAY;IAuBpB,OAAO,CAAC,YAAY;YAqBN,YAAY;IAU1B,OAAO,CAAC,WAAW;IAmBnB,OAAO,CAAC,GAAG;YAKG,GAAG;CAOlB;AAQD,eAAO,MAAM,wBAAwB,u0CAepC,CAAC"}
|