gesso-framework 0.6.30 → 0.8.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.
- package/CHANGELOG.md +98 -0
- package/dist/{FunctionComponent-CHtojpp7.d.ts → FunctionComponent-BDVrANP0.d.ts} +371 -115
- package/dist/{serveChannels-BlEag7WU.js → WorkerPorts-m_hLRoPj.js} +8 -240
- package/dist/WorkerPorts-m_hLRoPj.js.map +1 -0
- package/dist/agent/index.d.ts +55 -2
- package/dist/agent/index.js +8 -4
- package/dist/agent/index.js.map +1 -1
- package/dist/app-De3z58x7.js +638 -0
- package/dist/app-De3z58x7.js.map +1 -0
- package/dist/computed-DyW9SPpq.js +587 -0
- package/dist/computed-DyW9SPpq.js.map +1 -0
- package/dist/createComponent-DJads88q.js +47 -0
- package/dist/createComponent-DJads88q.js.map +1 -0
- package/dist/{index-DZbSb8IN.d.ts → index-DocVauCS.d.ts} +593 -119
- package/dist/index.d.ts +4 -5
- package/dist/index.js +609 -6641
- package/dist/index.js.map +1 -1
- package/dist/jsx/jsx-runtime.d.ts +8 -5
- package/dist/jsx/jsx-runtime.js +15 -6
- package/dist/jsx/jsx-runtime.js.map +1 -1
- package/dist/{persisted-fUzIU8FZ.js → persisted-qEBOHDnj.js} +4 -576
- package/dist/persisted-qEBOHDnj.js.map +1 -0
- package/dist/remote/index.d.ts +2 -2
- package/dist/remote/index.js +2 -1
- package/dist/remote/index.js.map +1 -1
- package/dist/{remote-BoB8zh7z.js → remote-B4bjdTzg.js} +19 -4
- package/dist/remote-B4bjdTzg.js.map +1 -0
- package/dist/render-W2XO444K.js +7310 -0
- package/dist/render-W2XO444K.js.map +1 -0
- package/dist/serveChannels-DReCmRxO.js +241 -0
- package/dist/serveChannels-DReCmRxO.js.map +1 -0
- package/dist/{createComponent-3n1ZSql8.js → show-D0MevHaV.js} +2 -46
- package/dist/show-D0MevHaV.js.map +1 -0
- package/dist/{ui-CHtNccR8.d.ts → tools-Bl8LZ7LR.d.ts} +351 -8
- package/dist/{webmcp-CdDq2nzS.js → webmcp-D7IlA0fu.js} +3 -3
- package/dist/webmcp-D7IlA0fu.js.map +1 -0
- package/dist/worker/index.d.ts +3 -4
- package/dist/worker/index.js +5 -3
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/ChannelProtocol-ByNoHujM.d.ts +0 -247
- package/dist/app-Yig59Ygf.js +0 -326
- package/dist/app-Yig59Ygf.js.map +0 -1
- package/dist/createComponent-3n1ZSql8.js.map +0 -1
- package/dist/persisted-fUzIU8FZ.js.map +0 -1
- package/dist/remote-BoB8zh7z.js.map +0 -1
- package/dist/serveChannels-BlEag7WU.js.map +0 -1
- 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
|