gesso-framework 0.6.29 → 0.7.0

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/{FunctionComponent-CHtojpp7.d.ts → FunctionComponent-Czkex8fp.d.ts} +358 -114
  3. package/dist/{serveChannels-BlEag7WU.js → WorkerPorts-m_hLRoPj.js} +8 -240
  4. package/dist/WorkerPorts-m_hLRoPj.js.map +1 -0
  5. package/dist/agent/index.d.ts +53 -2
  6. package/dist/agent/index.js +5 -4
  7. package/dist/agent/index.js.map +1 -1
  8. package/dist/app-De3z58x7.js +638 -0
  9. package/dist/app-De3z58x7.js.map +1 -0
  10. package/dist/computed-_cYcdczA.js +577 -0
  11. package/dist/computed-_cYcdczA.js.map +1 -0
  12. package/dist/createComponent-DJads88q.js +47 -0
  13. package/dist/createComponent-DJads88q.js.map +1 -0
  14. package/dist/{index-DZbSb8IN.d.ts → index-BENndtaH.d.ts} +222 -96
  15. package/dist/index.d.ts +4 -5
  16. package/dist/index.js +440 -6645
  17. package/dist/index.js.map +1 -1
  18. package/dist/jsx/jsx-runtime.d.ts +8 -5
  19. package/dist/jsx/jsx-runtime.js +15 -6
  20. package/dist/jsx/jsx-runtime.js.map +1 -1
  21. package/dist/{persisted-fUzIU8FZ.js → persisted-8meJmsOh.js} +4 -576
  22. package/dist/persisted-8meJmsOh.js.map +1 -0
  23. package/dist/remote/index.d.ts +2 -2
  24. package/dist/remote/index.js +2 -1
  25. package/dist/remote/index.js.map +1 -1
  26. package/dist/{remote-BoB8zh7z.js → remote-B4bjdTzg.js} +19 -4
  27. package/dist/remote-B4bjdTzg.js.map +1 -0
  28. package/dist/render-cydQ10Nd.js +6795 -0
  29. package/dist/render-cydQ10Nd.js.map +1 -0
  30. package/dist/serveChannels-DReCmRxO.js +241 -0
  31. package/dist/serveChannels-DReCmRxO.js.map +1 -0
  32. package/dist/{createComponent-3n1ZSql8.js → show-D0MevHaV.js} +2 -46
  33. package/dist/show-D0MevHaV.js.map +1 -0
  34. package/dist/{ui-CHtNccR8.d.ts → tools-BFBwuinE.d.ts} +351 -8
  35. package/dist/{webmcp-CdDq2nzS.js → webmcp-D7IlA0fu.js} +3 -3
  36. package/dist/webmcp-D7IlA0fu.js.map +1 -0
  37. package/dist/worker/index.d.ts +3 -4
  38. package/dist/worker/index.js +5 -3
  39. package/dist/worker/index.js.map +1 -1
  40. package/package.json +2 -2
  41. package/dist/ChannelProtocol-ByNoHujM.d.ts +0 -247
  42. package/dist/app-Yig59Ygf.js +0 -326
  43. package/dist/app-Yig59Ygf.js.map +0 -1
  44. package/dist/createComponent-3n1ZSql8.js.map +0 -1
  45. package/dist/persisted-fUzIU8FZ.js.map +0 -1
  46. package/dist/remote-BoB8zh7z.js.map +0 -1
  47. package/dist/serveChannels-BlEag7WU.js.map +0 -1
  48. package/dist/webmcp-CdDq2nzS.js.map +0 -1
@@ -0,0 +1,638 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { a as WAITS_UP_TO, i as serveAgentPort, l as validate, n as combineSurfaces, o as agentSurface, r as remoteSurface, t as AGENT_PORT } from "./remote-B4bjdTzg.js";
3
+ import { colorToHex, darkTheme, lightTheme } from "gesso-core";
4
+ //#region src/agent/designedTheme.ts
5
+ /**
6
+ * The theme an agent's `theme` names, and what to call it back.
7
+ *
8
+ * `light` and `dark` are the application's design themes, or Gesso's
9
+ * where it has none. `app` is the theme the running application is
10
+ * drawn in. Asked for nothing, an application that has design themes
11
+ * means them, its light one before its dark, since what it is drawn in
12
+ * is not what is being designed; one without them means itself.
13
+ */
14
+ function designedTheme(asked, themes, appTheme) {
15
+ const name = asked ?? (themes.light !== void 0 ? "light" : themes.dark !== void 0 ? "dark" : "app");
16
+ if (name === "dark") return {
17
+ theme: themes.dark ?? darkTheme,
18
+ name
19
+ };
20
+ if (name === "light") return {
21
+ theme: themes.light ?? lightTheme,
22
+ name
23
+ };
24
+ return {
25
+ theme: appTheme() ?? lightTheme,
26
+ name
27
+ };
28
+ }
29
+ //#endregion
30
+ //#region src/agent/tools.ts
31
+ /** A tool's answer as text, and, given, the same as data. */
32
+ function agentText(text, structured) {
33
+ return {
34
+ content: [{
35
+ type: "text",
36
+ text
37
+ }],
38
+ ...structured === void 0 ? {} : { structuredContent: structured },
39
+ isError: false
40
+ };
41
+ }
42
+ /** A tool's failure, written for the agent to correct its call by. */
43
+ function agentError(text) {
44
+ return {
45
+ content: [{
46
+ type: "text",
47
+ text
48
+ }],
49
+ isError: true
50
+ };
51
+ }
52
+ /** A tool's answer as text and pictures, each a PNG's bytes. */
53
+ function agentImages(text, pictures, structured) {
54
+ const content = [{
55
+ type: "text",
56
+ text
57
+ }];
58
+ for (const png of pictures) content.push({
59
+ type: "image",
60
+ data: base64(png),
61
+ mimeType: "image/png"
62
+ });
63
+ return {
64
+ content,
65
+ ...structured === void 0 ? {} : { structuredContent: structured },
66
+ isError: false
67
+ };
68
+ }
69
+ /** Serves a list of tool definitions as a surface, read afresh each time so a list may change. */
70
+ function toolSurface(definitions) {
71
+ const listed = (definition) => ({
72
+ name: definition.name,
73
+ ...definition.title === void 0 ? {} : { title: definition.title },
74
+ description: definition.description,
75
+ inputSchema: definition.inputSchema,
76
+ annotations: {
77
+ ...definition.title === void 0 ? {} : { title: definition.title },
78
+ readOnlyHint: definition.readOnly === true,
79
+ ...definition.destructive === void 0 ? {} : { destructiveHint: definition.destructive },
80
+ ...definition.idempotent === void 0 ? {} : { idempotentHint: definition.idempotent },
81
+ openWorldHint: false
82
+ },
83
+ ...definition.waitsUpToMs === void 0 ? {} : { _meta: { [WAITS_UP_TO]: definition.waitsUpToMs } }
84
+ });
85
+ return {
86
+ tools: () => definitions().map(listed),
87
+ resources: () => [],
88
+ read: () => void 0,
89
+ call: async (name, args) => {
90
+ const definition = definitions().find((each) => each.name === name);
91
+ if (definition === void 0) return agentError(`There is no tool called ${name}.`);
92
+ const input = args ?? {};
93
+ const problem = validate(definition.inputSchema, input);
94
+ if (problem !== null) return agentError(`${name}: ${problem}`);
95
+ try {
96
+ return await definition.run(input);
97
+ } catch (error) {
98
+ return agentError(`${name} failed: ${error instanceof Error ? error.message : String(error)}`);
99
+ }
100
+ }
101
+ };
102
+ }
103
+ function base64(bytes) {
104
+ let binary = "";
105
+ const chunk = 32768;
106
+ for (let at = 0; at < bytes.length; at += chunk) binary += String.fromCharCode(...bytes.subarray(at, at + chunk));
107
+ return btoa(binary);
108
+ }
109
+ //#endregion
110
+ //#region src/agent/tokens.ts
111
+ function themeTokens$1(theme) {
112
+ const typography = {};
113
+ for (const [role, style] of Object.entries(theme.typography)) typography[role] = {
114
+ fontFamily: style.fontFamily,
115
+ fontSize: style.fontSize,
116
+ fontWeight: style.fontWeight,
117
+ lineHeight: style.lineHeight,
118
+ letterSpacing: style.letterSpacing
119
+ };
120
+ const shadows = {};
121
+ for (const [name, layers] of Object.entries(theme.shadows)) shadows[name] = layers.map(formatShadow).join(", ") || "none";
122
+ const extensions = {};
123
+ for (const [key, group] of theme.extensions ?? []) extensions[key.description ?? "extension"] = plain(group);
124
+ return {
125
+ colors: Object.fromEntries(Object.entries(theme.colors).map(([name, color]) => [name, hex(color)])),
126
+ typography,
127
+ spacing: { ...theme.spacing },
128
+ shapes: plain(theme.shapes),
129
+ shadows,
130
+ density: theme.density,
131
+ contrast: theme.contrast,
132
+ units: {
133
+ pixelsPerInch: theme.units.pixelsPerInch,
134
+ rootFontSize: theme.units.rootFontSize
135
+ },
136
+ extensions
137
+ };
138
+ }
139
+ /**
140
+ * The tokens as text, a few lines per group, for an agent to read.
141
+ * `which` says which theme they are: "The theme in force at the
142
+ * application's outermost content".
143
+ */
144
+ function formatThemeTokens(tokens, which) {
145
+ const lines = [
146
+ `${which}. Write these names rather than their values, so a screen follows the theme into dark mode, high contrast and a rebrand.`,
147
+ "",
148
+ "colors: any color prop takes the name (backgroundColor=\"surface\", color=\"primary\")."
149
+ ];
150
+ for (const [name, value] of Object.entries(tokens.colors)) lines.push(` ${name} ${value}`);
151
+ lines.push("", "typography: the textStyle prop takes the role (textStyle=\"title\"). family size/line-height weight");
152
+ for (const [role, style] of Object.entries(tokens.typography)) lines.push(` ${role} ${style.fontFamily} ${style.fontSize}/${style.lineHeight} ${style.fontWeight}`);
153
+ lines.push("", "shapes: borderRadius takes the name (borderRadius=\"medium\").", ` ${inline(tokens.shapes)}`, "", "spacing: the scale, in pixels, for gap, padding and margin.", ` ${inline(tokens.spacing)}`, "", "shadows:");
154
+ for (const [name, value] of Object.entries(tokens.shadows)) lines.push(` ${name} ${value}`);
155
+ lines.push("", `density ${tokens.density}, contrast ${tokens.contrast}`);
156
+ const { pixelsPerInch, rootFontSize } = tokens.units;
157
+ lines.push("", `units: every length above is pixels. 1in = ${pixelsPerInch}px, 1pt = ${round(pixelsPerInch / 72)}px, 1mm = ${round(pixelsPerInch / 25.4)}px, 1rem = ${rootFontSize}px. To write one in these units, convert with the theme's: const { rem, inch, pt, mm } = lengths(theme.units) from gesso-core.`);
158
+ const extensions = Object.entries(tokens.extensions);
159
+ if (extensions.length > 0) {
160
+ lines.push("", "extensions: token groups the application added.");
161
+ for (const [name, group] of extensions) lines.push(` ${name} ${JSON.stringify(group)}`);
162
+ }
163
+ return lines.join("\n");
164
+ }
165
+ function round(value) {
166
+ return Math.round(value * 1e3) / 1e3;
167
+ }
168
+ function inline(record) {
169
+ return Object.entries(record).map(([name, value]) => `${name} ${typeof value === "object" ? JSON.stringify(value) : String(value)}`).join(", ");
170
+ }
171
+ function formatShadow(shadow) {
172
+ const color = typeof shadow.color === "string" ? shadow.color : hex(shadow.color);
173
+ const parts = [
174
+ shadow.offsetX,
175
+ shadow.offsetY,
176
+ shadow.blurRadius,
177
+ shadow.spreadRadius
178
+ ].join(" ");
179
+ return `${shadow.inset === true ? "inset " : ""}${parts} ${color}`;
180
+ }
181
+ function isColor(value) {
182
+ if (typeof value !== "object" || value === null) return false;
183
+ const color = value;
184
+ return Object.keys(value).length === 4 && typeof color.r === "number" && typeof color.g === "number" && typeof color.b === "number" && typeof color.a === "number";
185
+ }
186
+ function hex(color) {
187
+ return colorToHex(color);
188
+ }
189
+ /** A token group as JSON can hold it: colours as hex, and nothing that is not data. */
190
+ function plain(value) {
191
+ if (isColor(value)) return hex(value);
192
+ if (Array.isArray(value)) return value.map(plain);
193
+ if (typeof value === "object" && value !== null) {
194
+ const out = {};
195
+ for (const [key, entry] of Object.entries(value)) if (typeof entry !== "function") out[key] = plain(entry);
196
+ return out;
197
+ }
198
+ return value;
199
+ }
200
+ //#endregion
201
+ //#region src/agent/ui.ts
202
+ const THEME = {
203
+ enum: [
204
+ "app",
205
+ "light",
206
+ "dark"
207
+ ],
208
+ description: "The light or dark theme of what is being designed, or the running application's own (app). By default the light one where the application designs in themes of its own, as a design editor does, else its own."
209
+ };
210
+ const TARGET = {
211
+ ref: {
212
+ type: "string",
213
+ description: "The ref ui_snapshot gave the control, such as \"e4\"."
214
+ },
215
+ role: {
216
+ type: "string",
217
+ description: "The control's role, such as \"button\" or \"textbox\"."
218
+ },
219
+ name: {
220
+ type: "string",
221
+ description: "The control's accessible name, as ui_snapshot shows it in quotes."
222
+ }
223
+ };
224
+ const TOOLS = [
225
+ {
226
+ name: "ui_snapshot",
227
+ title: "Read the screen",
228
+ description: "The screen as an outline of what a screen reader would announce: each control with its role, name, state, value and a ref to act on it by. Read it before acting, and again whenever an action did not do what you expected. Prefer a channel tool when one covers what you want; these act on the interface.",
229
+ inputSchema: {
230
+ type: "object",
231
+ properties: {},
232
+ additionalProperties: false
233
+ },
234
+ annotations: {
235
+ readOnlyHint: true,
236
+ openWorldHint: false
237
+ }
238
+ },
239
+ {
240
+ name: "ui_press",
241
+ title: "Press a control",
242
+ description: "Presses a button, link, checkbox, tab or any other control, as a click would. Name it by ref, or by role and name. Returns the screen afterwards.",
243
+ inputSchema: {
244
+ type: "object",
245
+ properties: TARGET,
246
+ additionalProperties: false
247
+ },
248
+ annotations: {
249
+ readOnlyHint: false,
250
+ destructiveHint: false,
251
+ openWorldHint: false
252
+ }
253
+ },
254
+ {
255
+ name: "ui_type",
256
+ title: "Fill a field",
257
+ description: "Replaces a text field's whole text, as if the person had selected it all and typed. Name the field by ref, or by role and name. Returns the screen afterwards; press Enter with ui_key to submit.",
258
+ inputSchema: {
259
+ type: "object",
260
+ properties: {
261
+ ...TARGET,
262
+ text: {
263
+ type: "string",
264
+ description: "The text the field should hold."
265
+ }
266
+ },
267
+ required: ["text"],
268
+ additionalProperties: false
269
+ },
270
+ annotations: {
271
+ readOnlyHint: false,
272
+ destructiveHint: false,
273
+ openWorldHint: false
274
+ }
275
+ },
276
+ {
277
+ name: "ui_focus",
278
+ title: "Focus a control",
279
+ description: "Moves keyboard focus to a control, by ref or by role and name. Returns the screen afterwards.",
280
+ inputSchema: {
281
+ type: "object",
282
+ properties: TARGET,
283
+ additionalProperties: false
284
+ },
285
+ annotations: {
286
+ readOnlyHint: false,
287
+ destructiveHint: false,
288
+ idempotentHint: true,
289
+ openWorldHint: false
290
+ }
291
+ },
292
+ {
293
+ name: "ui_key",
294
+ title: "Press a key",
295
+ description: "Presses a key where focus is, as the keyboard would: Enter, Escape, Tab, ArrowDown, a letter. Modifiers are optional. Returns the screen afterwards.",
296
+ inputSchema: {
297
+ type: "object",
298
+ properties: {
299
+ key: {
300
+ type: "string",
301
+ description: "A KeyboardEvent key name, such as \"Enter\", \"Escape\" or \"a\"."
302
+ },
303
+ shift: { type: "boolean" },
304
+ ctrl: { type: "boolean" },
305
+ alt: { type: "boolean" },
306
+ meta: { type: "boolean" }
307
+ },
308
+ required: ["key"],
309
+ additionalProperties: false
310
+ },
311
+ annotations: {
312
+ readOnlyHint: false,
313
+ destructiveHint: false,
314
+ openWorldHint: false
315
+ }
316
+ },
317
+ {
318
+ name: "tokens_view",
319
+ title: "Read the design tokens",
320
+ description: "The theme's colours, type styles, corner radii, spacing scale and shadows, by name and value, as the screen draws them. Read it before designing, and write the names rather than raw values. Name a control, by ref or by role and name, to read the theme where it is: part of a screen may have a theme of its own.",
321
+ inputSchema: {
322
+ type: "object",
323
+ properties: {
324
+ ...TARGET,
325
+ theme: THEME
326
+ },
327
+ additionalProperties: false
328
+ },
329
+ annotations: {
330
+ readOnlyHint: true,
331
+ openWorldHint: false
332
+ }
333
+ },
334
+ {
335
+ name: "render_frame",
336
+ title: "Draw a frame",
337
+ description: "Draws a module of the project to a PNG you can look at: a mock, or a component with inputs, at the width you ask for, in the theme being designed in. The pixels are the application's own: the same layout, text and paint. Draw what you build, at a phone width and a desktop one, and look before you call it done. Without a height it is as tall as its content, which suits a component or a section; give one for a whole screen, which then fills it.",
338
+ inputSchema: {
339
+ type: "object",
340
+ properties: {
341
+ module: {
342
+ type: "string",
343
+ description: "The module, from the project root, as library_overview gives it: \"src/mocks/Settings\"."
344
+ },
345
+ export: {
346
+ type: "string",
347
+ description: "The export to draw: a component or an element. By default the module's default, else its only component."
348
+ },
349
+ inputs: {
350
+ type: "object",
351
+ description: "Inputs for a component, by name, as library_get lists them."
352
+ },
353
+ width: {
354
+ type: "number",
355
+ description: "In logical pixels: 390 for a phone, 1280 for a desktop."
356
+ },
357
+ height: {
358
+ type: "number",
359
+ description: "In logical pixels. Leave it out to fit the content."
360
+ },
361
+ scale: {
362
+ type: "number",
363
+ description: "Device pixels per logical pixel, 1 to 3 (default 1)."
364
+ },
365
+ theme: THEME
366
+ },
367
+ required: ["module", "width"],
368
+ additionalProperties: false
369
+ },
370
+ annotations: {
371
+ readOnlyHint: true,
372
+ openWorldHint: false
373
+ }
374
+ }
375
+ ];
376
+ /**
377
+ * The screen's tools, by name: what a relay that carries only the screen
378
+ * (`gesso-electrobun`'s, to a desktop app's main process) lets through.
379
+ * Listed from the tools themselves, so a tool added here is relayed
380
+ * without the relay being told.
381
+ */
382
+ const SCREEN_TOOL_NAMES = TOOLS.map((tool) => tool.name);
383
+ /**
384
+ * Short names for record ids.
385
+ *
386
+ * A record's id is its path through the tree, `root:0:0:component:1:0:1`,
387
+ * which is exact and long, and an outline repeats one on every line an
388
+ * agent reads. A ref is `e` and a number, given the first time a record
389
+ * is seen and kept for as long as the surface lives, so the ref an agent
390
+ * read in one snapshot still names the same control in the next.
391
+ */
392
+ var UiRefs = class {
393
+ byId = /* @__PURE__ */ new Map();
394
+ byRef = /* @__PURE__ */ new Map();
395
+ refOf(id) {
396
+ let ref = this.byId.get(id);
397
+ if (ref === void 0) {
398
+ ref = `e${this.byId.size + 1}`;
399
+ this.byId.set(id, ref);
400
+ this.byRef.set(ref, id);
401
+ }
402
+ return ref;
403
+ }
404
+ /** The record id a ref names, or the input itself when it is not a ref this surface gave. */
405
+ idOf(ref) {
406
+ return this.byRef.get(ref) ?? ref;
407
+ }
408
+ };
409
+ /**
410
+ * The tools, over a host. `host` returns undefined until the
411
+ * application has started, and a tool called before then says so.
412
+ */
413
+ function uiSurface(host, options = {}) {
414
+ const quietMs = options.quietMs ?? 50;
415
+ const settleMs = options.settleMs ?? 1e3;
416
+ const refs = new UiRefs();
417
+ /** Waits until two readings of the screen agree, then returns the last. */
418
+ const settled = async (ui) => {
419
+ const start = Date.now();
420
+ let previous = "";
421
+ for (;;) {
422
+ ui.flush();
423
+ const current = outline(ui.semanticsTree(), ui.focusedNodeId(), refs);
424
+ if (current === previous || Date.now() - start >= settleMs) return current;
425
+ previous = current;
426
+ await new Promise((resolve) => setTimeout(resolve, quietMs));
427
+ }
428
+ };
429
+ const act = async (args, perform) => {
430
+ const ui = host();
431
+ if (ui === void 0) return failure("The application has not started yet.");
432
+ ui.flush();
433
+ const found = resolveTarget(ui.semanticsTree(), args, refs);
434
+ if (typeof found === "string") return failure(found);
435
+ if (found.disabled === true) return failure(`${describe(found)} is disabled.`);
436
+ const problem = perform(ui, found);
437
+ if (problem !== null) return failure(problem);
438
+ return success(await settled(ui));
439
+ };
440
+ return {
441
+ tools: () => TOOLS,
442
+ call: async (name, args = {}) => {
443
+ switch (name) {
444
+ case "ui_snapshot": {
445
+ const ui = host();
446
+ return ui === void 0 ? failure("The application has not started yet.") : success(await settled(ui));
447
+ }
448
+ case "ui_press": return act(args, (ui, record) => (ui.applySemanticsAction({
449
+ id: record.id,
450
+ action: "click"
451
+ }), null));
452
+ case "ui_focus": return act(args, (ui, record) => (ui.applySemanticsAction({
453
+ id: record.id,
454
+ action: "focus"
455
+ }), null));
456
+ case "ui_type":
457
+ if (typeof args.text !== "string") return failure("ui_type needs text: the text the field should hold.");
458
+ return act(args, (ui, record) => {
459
+ if (record.states?.includes("readonly")) return `${describe(record)} is read-only.`;
460
+ ui.applySemanticsAction({
461
+ id: record.id,
462
+ action: "setValue",
463
+ value: args.text
464
+ });
465
+ return null;
466
+ });
467
+ case "ui_key": {
468
+ const ui = host();
469
+ if (ui === void 0) return failure("The application has not started yet.");
470
+ if (typeof args.key !== "string" || args.key === "") return failure("ui_key needs key: a KeyboardEvent key name, such as \"Enter\".");
471
+ ui.key(args.key, {
472
+ shift: args.shift === true,
473
+ ctrl: args.ctrl === true,
474
+ alt: args.alt === true,
475
+ meta: args.meta === true
476
+ });
477
+ return success(await settled(ui));
478
+ }
479
+ case "tokens_view": {
480
+ const ui = host();
481
+ if (ui === void 0) return failure("The application has not started yet.");
482
+ if (ui.theme === void 0) return failure("This application does not report its theme to agents.");
483
+ let at;
484
+ if (args.ref !== void 0 || args.role !== void 0 || args.name !== void 0) {
485
+ ui.flush();
486
+ const found = resolveTarget(ui.semanticsTree(), args, refs);
487
+ if (typeof found === "string") return failure(found);
488
+ at = found;
489
+ }
490
+ let theme;
491
+ let which;
492
+ if (at !== void 0) {
493
+ theme = ui.theme(at.id);
494
+ which = `The theme in force at ${describe(at)}`;
495
+ } else {
496
+ const designed = designedTheme(args.theme, ui.designThemes?.() ?? {}, () => ui.theme?.());
497
+ theme = designed.name === "app" ? ui.theme() : designed.theme;
498
+ which = designed.name === "app" ? "The theme in force at the application's outermost content" : `The ${designed.name} theme being designed in`;
499
+ }
500
+ if (theme === void 0) return failure("The application has not drawn anything yet, so no theme is in force.");
501
+ const tokens = themeTokens$1(theme);
502
+ return {
503
+ content: [{
504
+ type: "text",
505
+ text: formatThemeTokens(tokens, which)
506
+ }],
507
+ structuredContent: tokens,
508
+ isError: false
509
+ };
510
+ }
511
+ case "render_frame": {
512
+ const ui = host();
513
+ if (ui === void 0) return failure("The application has not started yet.");
514
+ if (ui.render === void 0) return failure("This application cannot draw frames for agents.");
515
+ if (typeof args.module !== "string" || args.module === "") return failure("render_frame needs module: the module, from the project root, such as \"src/mocks/Settings\".");
516
+ if (typeof args.width !== "number" || !(args.width > 0)) return failure("render_frame needs width: a number of logical pixels above zero, such as 390.");
517
+ return ui.render(args);
518
+ }
519
+ default: return failure(`There is no tool called ${name}.`);
520
+ }
521
+ },
522
+ resources: () => [],
523
+ read: () => void 0,
524
+ dispose: () => {}
525
+ };
526
+ }
527
+ /**
528
+ * The tree as an indented outline, one control per line:
529
+ *
530
+ * - button "Add one" [e3]
531
+ * - textbox "Title" value="Groceries" [e4] (focused)
532
+ *
533
+ * Text, because an agent reads it, and an outline is a third the size
534
+ * of the same tree as JSON. A record with neither a role nor a name is
535
+ * structure, and its children are lifted to its depth.
536
+ */
537
+ function outline(tree, focused, refs = new UiRefs()) {
538
+ const depths = /* @__PURE__ */ new Map();
539
+ const lines = [];
540
+ for (const record of tree.values()) {
541
+ const parentDepth = record.parent === null ? -1 : depths.get(record.parent) ?? -1;
542
+ const shown = record.role !== void 0 || record.label !== void 0 && record.label !== "";
543
+ const depth = shown ? parentDepth + 1 : parentDepth;
544
+ depths.set(record.id, depth);
545
+ if (!shown) continue;
546
+ const parts = [`${" ".repeat(depth)}- ${describe(record)}`];
547
+ if (record.states !== void 0 && record.states.length > 0) parts.push(`[${record.states.join(", ")}]`);
548
+ const value = valueOf(record);
549
+ if (value !== null) parts.push(`value=${JSON.stringify(value)}`);
550
+ if (record.disabled === true) parts.push("(disabled)");
551
+ parts.push(`[${refs.refOf(record.id)}]`);
552
+ if (record.id === focused) parts.push("(focused)");
553
+ lines.push(parts.join(" "));
554
+ }
555
+ return lines.length === 0 ? "(the screen announces nothing)" : lines.join("\n");
556
+ }
557
+ function describe(record) {
558
+ const name = record.label === void 0 || record.label === "" ? "" : ` ${JSON.stringify(record.label)}`;
559
+ return `${record.role ?? "text"}${name}`;
560
+ }
561
+ function valueOf(record) {
562
+ if (record.valueText !== void 0 && record.valueText !== record.label) return record.valueText;
563
+ if (record.valueNow !== void 0) return String(record.valueNow);
564
+ return null;
565
+ }
566
+ /**
567
+ * The record an action names: by ref, or by role and name. A name
568
+ * matches exactly before it matches as a part, ignoring case, so
569
+ * "Save" finds the button called Save rather than "Save as". More than
570
+ * one match is an error listing them, because guessing which of two
571
+ * buttons an agent meant is how the wrong one gets pressed.
572
+ */
573
+ function resolveTarget(tree, args, refs = new UiRefs()) {
574
+ const ref = typeof args.ref === "string" ? args.ref : void 0;
575
+ const role = typeof args.role === "string" ? args.role : void 0;
576
+ const name = typeof args.name === "string" ? args.name : void 0;
577
+ if (ref !== void 0) return tree.get(refs.idOf(ref)) ?? `Nothing on the screen has the ref ${ref} now. Read ui_snapshot again for current refs.`;
578
+ if (role === void 0 && name === void 0) return "Say which control: a ref from ui_snapshot, or a role and a name.";
579
+ const candidates = [...tree.values()].filter((record) => role === void 0 || record.role === role);
580
+ let matches = candidates;
581
+ if (name !== void 0) {
582
+ const wanted = name.toLowerCase();
583
+ const exact = candidates.filter((record) => record.label?.toLowerCase() === wanted);
584
+ matches = exact.length > 0 ? exact : candidates.filter((record) => record.label?.toLowerCase().includes(wanted));
585
+ }
586
+ const wanted = [role, name === void 0 ? void 0 : JSON.stringify(name)].filter(Boolean).join(" ");
587
+ if (matches.length === 1) return matches[0];
588
+ if (matches.length === 0) {
589
+ const nearby = candidates.filter((record) => record.role !== void 0).slice(0, 10).map((record) => `${describe(record)} [${refs.refOf(record.id)}]`);
590
+ return `Nothing on the screen is ${wanted}.${nearby.length > 0 ? ` Some that are: ${nearby.join("; ")}.` : ""}`;
591
+ }
592
+ return `${matches.length} controls are ${wanted}: ${matches.slice(0, 10).map((record) => `${describe(record)} [${refs.refOf(record.id)}]`).join("; ")}. Use a ref.`;
593
+ }
594
+ function success(text) {
595
+ return {
596
+ content: [{
597
+ type: "text",
598
+ text
599
+ }],
600
+ isError: false
601
+ };
602
+ }
603
+ function failure(text) {
604
+ return {
605
+ content: [{
606
+ type: "text",
607
+ text
608
+ }],
609
+ isError: true
610
+ };
611
+ }
612
+ //#endregion
613
+ //#region src/agent/app.ts
614
+ var app_exports = /* @__PURE__ */ __exportAll({ serveApplicationAgent: () => serveApplicationAgent });
615
+ /**
616
+ * Answers an agent port with the whole application: the channels this
617
+ * thread feeds, whatever each worker behind it serves, and the screen.
618
+ *
619
+ * The thread that draws is the one that knows all three, which is the
620
+ * render worker in the worker configuration and the page in the single
621
+ * thread one; both answer with this. The screen comes last, so a
622
+ * channel tool keeps its name.
623
+ */
624
+ function serveApplicationAgent(port, parts) {
625
+ return serveAgentPort(port, (confirm) => {
626
+ const workers = [...parts.workers()].filter((worker) => worker.spawned).map((worker) => remoteSurface(worker.open(AGENT_PORT), { confirm }));
627
+ return combineSurfaces([
628
+ agentSurface(parts.channels(), { confirm }),
629
+ ...workers,
630
+ uiSurface(parts.ui),
631
+ ...parts.tools === void 0 ? [] : [toolSurface(parts.tools)]
632
+ ]);
633
+ });
634
+ }
635
+ //#endregion
636
+ export { outline as a, formatThemeTokens as c, agentImages as d, agentText as f, UiRefs as i, themeTokens$1 as l, designedTheme as m, serveApplicationAgent as n, resolveTarget as o, toolSurface as p, SCREEN_TOOL_NAMES as r, uiSurface as s, app_exports as t, agentError as u };
637
+
638
+ //# sourceMappingURL=app-De3z58x7.js.map