@chalky/chalky 0.0.0-stage → 0.1.4

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 (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -2
  3. package/app/SYSTEM.md +31 -0
  4. package/app/extensions/appdata/README.md +10 -0
  5. package/app/extensions/appdata/guide.md +4 -0
  6. package/app/extensions/appdata/index.ts +47 -0
  7. package/app/extensions/boards/browser.js +357 -0
  8. package/app/extensions/boards/guide.md +4 -0
  9. package/app/extensions/boards/index.ts +98 -0
  10. package/app/extensions/commands/browser.js +13 -0
  11. package/app/extensions/commands/guide.md +3 -0
  12. package/app/extensions/commands/index.ts +96 -0
  13. package/app/extensions/doodle/README.md +15 -0
  14. package/app/extensions/doodle/browser.js +398 -0
  15. package/app/extensions/doodle/guide.md +6 -0
  16. package/app/extensions/doodle/index.ts +54 -0
  17. package/app/extensions/files/browser.js +96 -0
  18. package/app/extensions/files/guide.md +7 -0
  19. package/app/extensions/files/index.ts +73 -0
  20. package/app/extensions/frames/README.md +37 -0
  21. package/app/extensions/frames/browser.js +166 -0
  22. package/app/extensions/frames/guide.md +7 -0
  23. package/app/extensions/frames/index.ts +85 -0
  24. package/app/extensions/login/browser.js +363 -0
  25. package/app/extensions/login/index.ts +214 -0
  26. package/app/extensions/mcp/browser.js +64 -0
  27. package/app/extensions/mcp/guide.md +5 -0
  28. package/app/extensions/mcp/index.ts +64 -0
  29. package/app/extensions/model/index.ts +20 -0
  30. package/app/extensions/page/index.ts +96 -0
  31. package/app/extensions/page/page.html +615 -0
  32. package/app/extensions/point/README.md +22 -0
  33. package/app/extensions/point/browser.js +247 -0
  34. package/app/extensions/point/guide.md +6 -0
  35. package/app/extensions/point/index.ts +112 -0
  36. package/app/extensions/prompt/index.ts +19 -0
  37. package/app/extensions/reload/index.ts +21 -0
  38. package/app/extensions/screen/browser.js +76 -0
  39. package/app/extensions/screen/index.ts +80 -0
  40. package/app/extensions/steps/index.ts +17 -0
  41. package/app/extensions/swarm/README.md +47 -0
  42. package/app/extensions/swarm/guide.md +11 -0
  43. package/app/extensions/swarm/index.ts +304 -0
  44. package/app/extensions/ui/README.md +69 -0
  45. package/app/extensions/ui/agents.js +45 -0
  46. package/app/extensions/ui/browser.js +364 -0
  47. package/app/extensions/ui/components/App.js +28 -0
  48. package/app/extensions/ui/components/Button.js +28 -0
  49. package/app/extensions/ui/components/Card.js +23 -0
  50. package/app/extensions/ui/components/Column.js +17 -0
  51. package/app/extensions/ui/components/Markdown.js +37 -0
  52. package/app/extensions/ui/components/Row.js +17 -0
  53. package/app/extensions/ui/components/Table.js +31 -0
  54. package/app/extensions/ui/components/Text.js +19 -0
  55. package/app/extensions/ui/components/TextField.js +30 -0
  56. package/app/extensions/ui/guide.md +7 -0
  57. package/app/extensions/ui/index.ts +291 -0
  58. package/app/extensions/ui/skills/agent-apps/SKILL.md +91 -0
  59. package/app/extensions/ui/skills/ui-check/SKILL.md +71 -0
  60. package/app/extensions/ui/skills/ui-check/scripts/check.mjs +306 -0
  61. package/app/extensions/ui/skills/ui-design/SKILL.md +123 -0
  62. package/app/extensions/ui/voice.js +114 -0
  63. package/app/extensions/ui_history/browser.js +97 -0
  64. package/app/extensions/ui_history/index.ts +109 -0
  65. package/app/extensions/versions/browser.js +58 -0
  66. package/app/extensions/versions/index.ts +168 -0
  67. package/app/extensions/viewer/README.md +12 -0
  68. package/app/extensions/viewer/browser.js +322 -0
  69. package/app/extensions/viewer/guide.md +5 -0
  70. package/app/extensions/viewer/index.ts +209 -0
  71. package/app/extensions/voice/browser.js +71 -0
  72. package/app/extensions/voice/index.ts +37 -0
  73. package/app/mcp.json +3 -0
  74. package/app/settings.json +3 -0
  75. package/app/skills/plugin-map/SKILL.md +49 -0
  76. package/app/skills/write-skill/SKILL.md +57 -0
  77. package/app/ui/apps/.keep +0 -0
  78. package/bin/chalky.mjs +11 -0
  79. package/node_modules/xlsx/CHANGELOG.md +361 -0
  80. package/node_modules/xlsx/LICENSE +201 -0
  81. package/node_modules/xlsx/README.md +41 -0
  82. package/node_modules/xlsx/bin/xlsx.njs +310 -0
  83. package/node_modules/xlsx/bower.json +22 -0
  84. package/node_modules/xlsx/dist/LICENSE +201 -0
  85. package/node_modules/xlsx/dist/cpexcel.d.ts +39 -0
  86. package/node_modules/xlsx/dist/cpexcel.full.mjs +1502 -0
  87. package/node_modules/xlsx/dist/cpexcel.js +1506 -0
  88. package/node_modules/xlsx/dist/shim.min.js +2 -0
  89. package/node_modules/xlsx/dist/xlsx.core.min.js +18 -0
  90. package/node_modules/xlsx/dist/xlsx.core.min.map +1 -0
  91. package/node_modules/xlsx/dist/xlsx.extendscript.js +28259 -0
  92. package/node_modules/xlsx/dist/xlsx.full.min.js +24 -0
  93. package/node_modules/xlsx/dist/xlsx.full.min.map +1 -0
  94. package/node_modules/xlsx/dist/xlsx.mini.min.js +10 -0
  95. package/node_modules/xlsx/dist/xlsx.mini.min.map +1 -0
  96. package/node_modules/xlsx/dist/xlsx.zahl.js +4 -0
  97. package/node_modules/xlsx/dist/xlsx.zahl.mjs +2 -0
  98. package/node_modules/xlsx/dist/zahl.d.ts +4 -0
  99. package/node_modules/xlsx/package.json +183 -0
  100. package/node_modules/xlsx/types/index.d.ts +1065 -0
  101. package/node_modules/xlsx/types/tsconfig.json +15 -0
  102. package/node_modules/xlsx/xlsx.js +28103 -0
  103. package/node_modules/xlsx/xlsx.mjs +28228 -0
  104. package/node_modules/xlsx/xlsxworker.js +14 -0
  105. package/package.json +42 -4
  106. package/server.ts +281 -0
@@ -0,0 +1,11 @@
1
+ # Your team
2
+ Your name: {name}. Your role: {role}. You work with other AI agents as peers; there is no manager.
3
+ - What you receive starts with "[<kind> from <name> · <time>]": a message (from the user or the harness), a
4
+ question (the sender waits for your answer), a note (the sender went on working), a reply (the answer to
5
+ your note) or an fyi (an update, no answer needed). Your final answer is delivered for you, to whoever the
6
+ header says: say it once, there.
7
+ - tell(to, text) sends a note and you keep working; its answer comes back as a reply. ask_agent(to, text) asks
8
+ and waits. list_agents shows who is doing what now. spawn_agent adds a member (at most {max_agents}).
9
+ - When all that is left is waiting, end your turn: replies wake you. Never sleep or poll.
10
+ - Done means checked: run or test it, and say what you ran and what it showed.
11
+ - Simple work: do it yourself. Before you spawn, split or hand out work, or take work from another agent, read README.md.
@@ -0,0 +1,304 @@
1
+ // Peer-to-peer swarm (port of the Python swarm plugin, on pi's pieces): no manager; anyone (the user or an agent)
2
+ // can talk to any agent. Every agent is its own pi session (host.createAgent). A message starts a turn, and the
3
+ // turn's final answer goes back to whoever is owed it. Nothing is said "to no one".
4
+ // - Kinds of item: message from the user or the harness; the answer is shown in the chat
5
+ // question ask_agent: the asker waits; the answer is its tool result
6
+ // note tell: the sender goes on; the answer comes back to it as a reply
7
+ // reply the answer to a note (or to a question that timed out)
8
+ // fyi a tell to the agent you owe your answer to: an update, not a request (no answer back)
9
+ // - Each item has a chain: who gets the answer, nearest first. A note from agent1 (working for the user) has
10
+ // chain [agent1, user]; agent2's answer goes to agent1 as a reply with chain [user], and agent1's answer to that
11
+ // reply goes to the user. So replies never bounce back and forth.
12
+ // - Notes, replies and fyis for an agent in the middle of a turn: pi's steer() (it sees them between its tool calls;
13
+ // no turn of their own). An fyi for an idle agent: pi's sendCustomMessage() (filed; the model sees it next turn).
14
+ // Messages and questions wait in the inbox for a turn of their own, one at a time.
15
+ // - Limits: asks + tells per user message, agents, inbox size, notes per minute, identical notes dropped, ask
16
+ // timeout. Asking an agent that is waiting on you is refused (use tell).
17
+ // Shared state is plain data on the host (host.shared "swarm"); the code that acts on it is this file's newest load
18
+ // (host.api.swarm), so an edited swarm/index.ts takes effect after a reload. Agents: host.agents (role, app, session).
19
+ // Pictures with a message (doodle's steps): send(..., images) takes png paths; they go to pi with the text (prompt
20
+ // or steer), read when the agent gets the message.
21
+ // Not ported: the tasks plugin's "blocked" state.
22
+ import fs from "node:fs";
23
+ import path from "node:path";
24
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
25
+ import { Type } from "typebox";
26
+
27
+ const host = (globalThis as any).chalky;
28
+ const MAX_MESSAGES = Number(process.env.SWARM_MAX_MESSAGES || 50); // ask_agent + tell calls per user message
29
+ const MAX_AGENTS = Number(process.env.SWARM_MAX_AGENTS || 8);
30
+ const ASK_TIMEOUT = Number(process.env.SWARM_ASK_TIMEOUT || 600); // seconds; then the answer comes as a reply
31
+ const INBOX_CAP = 50; // items waiting for one agent
32
+ const NOTES_PER_MINUTE = 20; // per sender
33
+ const APP_MESSAGES_PER_MINUTE = 30; // from apps (the page) to agents
34
+ const TEAM = fs.readFileSync(path.join(import.meta.dirname, "guide.md"), "utf8") // short; README.md: the whole of it
35
+ .replace("README.md", path.join(import.meta.dirname, "README.md"));
36
+
37
+ type Waiter = { resolve: (answer: string) => void } | null;
38
+ type Item = { kind: string; sender: string; text: string; chain: string[]; ts: number; waiter: Waiter; images?: string[] };
39
+
40
+ if (!host.shared.has("swarm"))
41
+ host.shared.set("swarm", { inbox: {}, running: new Set(), chain: {}, waiting: {}, doing: {}, sent: 0, recent: {},
42
+ lastNote: {}, appSent: [] });
43
+ const S = host.shared.get("swarm");
44
+ const inbox = (name: string): Item[] => (S.inbox[name] ??= []);
45
+ const isAgent = (name: string) => host.agents.has(name);
46
+ const target = (chain: string[]) => (chain[0] && isAgent(chain[0]) ? chain[0] : "user");
47
+
48
+ function header(item: Item, to?: string) {
49
+ let h = `[${item.kind} from ${item.sender} · ${new Date(item.ts).toTimeString().slice(0, 8)}]`;
50
+ if (item.kind === "question") h += " (they are waiting for your answer)";
51
+ if (item.kind === "reply" && to) h += ` (your final answer goes to ${to})`;
52
+ return h;
53
+ }
54
+
55
+ function changed(name?: string, doing?: string) {
56
+ if (name && doing !== undefined) S.doing[name] = [doing, Date.now() / 1000];
57
+ host.broadcast("status", { agents: status() });
58
+ }
59
+
60
+ // png paths -> pi's image content (a file gone since is left out)
61
+ const pictures = (paths: string[] = []) => paths.filter((p) => fs.existsSync(p))
62
+ .map((p) => ({ type: "image" as const, data: fs.readFileSync(p).toString("base64"), mimeType: "image/png" }));
63
+
64
+ function put(to: string, kind: string, sender: string, text: string, chain: string[], waiter: Waiter = null, images?: string[]) {
65
+ const item: Item = { kind, sender, text, chain, ts: Date.now(), waiter, images };
66
+ const session = host.agents.get(to).session;
67
+ if (["note", "reply", "fyi"].includes(kind) && session.isStreaming) void session.steer(`${header(item)}\n${text}`, pictures(images));
68
+ else if (kind === "fyi") // its turn ended before it was handed over: file it, don't start a turn
69
+ void session.sendCustomMessage({ customType: "inbox", content: `${header(item)}\n${text}`, display: true });
70
+ else {
71
+ inbox(to).push(item);
72
+ if (!S.running.has(to)) void work(to);
73
+ }
74
+ changed();
75
+ return item;
76
+ }
77
+
78
+ // One item -> one turn -> one answer, in order, while the inbox has items
79
+ async function work(name: string) {
80
+ S.running.add(name);
81
+ changed(name, "thinking");
82
+ try {
83
+ while (inbox(name).length) {
84
+ const item = inbox(name).shift()!;
85
+ const to = target(item.chain);
86
+ S.chain[name] = item.chain;
87
+ const answer = await host.prompt(name, `${header(item, to)}\n${item.text}`, to, pictures(item.images));
88
+ delete S.chain[name];
89
+ if (item.waiter) item.waiter.resolve(answer); // a question: the asker is waiting
90
+ else if (to !== "user" && to !== name && answer.trim()) put(to, "reply", name, answer, item.chain.slice(1));
91
+ }
92
+ } finally {
93
+ S.running.delete(name);
94
+ changed(name, "idle");
95
+ }
96
+ }
97
+
98
+ function status() {
99
+ const out: Record<string, any> = {};
100
+ for (const a of host.agents.values()) {
101
+ const [doing, since] = S.doing[a.name] ?? ["idle", Date.now() / 1000];
102
+ const waiting = [...(S.waiting[a.name] ?? [])].sort();
103
+ const busy = S.running.has(a.name);
104
+ out[a.name] = { role: a.role, app: a.app ?? null, state: waiting.length ? `waiting on ${waiting.join(", ")}` : busy ? "working" : "idle",
105
+ doing: busy ? doing : "", since, queued: inbox(a.name).length };
106
+ }
107
+ return out;
108
+ }
109
+
110
+ const owed = (name: string) => { const c = S.chain[name]; return c?.[0] && isAgent(c[0]) ? c[0] : null; };
111
+
112
+ function waitsOn(a: string, b: string) { // is agent a waiting on agent b, directly or through others?
113
+ const seen = new Set<string>(), todo = [a];
114
+ while (todo.length)
115
+ for (const x of S.waiting[todo.pop()!] ?? []) {
116
+ if (x === b) return true;
117
+ if (!seen.has(x)) { seen.add(x); todo.push(x); }
118
+ }
119
+ return false;
120
+ }
121
+
122
+ function ask(asker: string, to: string, text: string, signal?: AbortSignal) {
123
+ (S.waiting[asker] ??= new Set()).add(to);
124
+ changed();
125
+ return new Promise<string>((resolve) => {
126
+ const done = (answer: string) => {
127
+ clearTimeout(timer);
128
+ S.waiting[asker]?.delete(to);
129
+ changed();
130
+ resolve(answer);
131
+ };
132
+ const item = put(to, "question", asker, text, [asker, ...(S.chain[asker] ?? [])], { resolve: done });
133
+ const timer = setTimeout(() => {
134
+ item.waiter = null; // the answer will come as a reply instead
135
+ done(`error: no answer from ${to} within ${ASK_TIMEOUT} s. It is still working on it; its answer will come to you `
136
+ + "as a reply. Continue with something else.");
137
+ }, ASK_TIMEOUT * 1000);
138
+ signal?.addEventListener("abort", () => { item.waiter = null; done("error: stopped"); });
139
+ });
140
+ }
141
+
142
+ const api = {
143
+ status,
144
+ // From the user or the harness (the page's chat, apps, ui actions). The answer is shown in the chat.
145
+ // Returns the item (for withdraw). images: png paths sent with it.
146
+ send(sender: string, to: string, text: string, note = false, images?: string[]) {
147
+ if (sender === "user") S.sent = 0; // a new user message gives the team a fresh budget
148
+ return put(to, note ? "note" : "message", sender, text, [sender], null, images);
149
+ },
150
+ // An update for an agent that doesn't start a turn (filed if idle, between its tool calls if busy)
151
+ fyi(sender: string, to: string, text: string) { return put(to, "fyi", sender, text, [sender]); },
152
+ // Take back an item that is still waiting in an inbox: true if it was there (its agent never saw it)
153
+ withdraw(item: Item) {
154
+ for (const items of Object.values(S.inbox) as Item[][]) {
155
+ const i = items.indexOf(item);
156
+ if (i >= 0) { items.splice(i, 1); changed(); return true; }
157
+ }
158
+ return false;
159
+ },
160
+ // Drop all queued items and stop every running turn; agents stay, idle
161
+ async stop() {
162
+ for (const name of Object.keys(S.inbox))
163
+ for (const item of S.inbox[name].splice(0)) item.waiter?.resolve(`error: ${name} was stopped before replying`);
164
+ await Promise.all([...host.agents.keys()].map((n: string) => host.abort(n)));
165
+ for (const k of Object.keys(S.waiting)) delete S.waiting[k];
166
+ changed();
167
+ host.broadcast("stopped", { agent: "all" });
168
+ },
169
+ // Empty these agents' memories (roles kept): a new pi session each. An error string, or null when done.
170
+ async reset(names: string[]) {
171
+ const busy = names.filter((n) => S.running.has(n) || S.waiting[n]?.size || inbox(n).length);
172
+ if (busy.length) return `error: ${busy.join(", ")} still working or with messages waiting. Reset when they are idle.`;
173
+ for (const n of names) { const a = host.agents.get(n); await host.createAgent(n, a.role, a.app, true); }
174
+ return null;
175
+ },
176
+ };
177
+ host.api.swarm = api;
178
+
179
+ const result = (text: string) => ({ content: [{ type: "text" as const, text }], details: undefined });
180
+
181
+ export default function (pi: ExtensionAPI) {
182
+ const me = () => pi.getSessionName()!;
183
+ pi.on("before_agent_start", (event) => {
184
+ const a = host.agents.get(me());
185
+ if (a) event.systemPromptOptions.sections.team = TEAM.replace("{name}", me()).replace("{role}", a.role)
186
+ .replace("{max_agents}", String(MAX_AGENTS));
187
+ });
188
+ // What each agent is doing right now, for status
189
+ pi.on("tool_execution_start", (ev) => {
190
+ const args = ev.args ?? {};
191
+ changed(me(), `${ev.toolName} ${String(args.command ?? args.to ?? args.surface ?? args.path ?? "").slice(0, 60)}`.trim());
192
+ });
193
+ pi.on("turn_start", () => changed(me(), "thinking"));
194
+
195
+ const checkTo = (agent: string, to: string) => {
196
+ if (!isAgent(to)) return `error: no agent '${to}'. Agents: ${[...host.agents.keys()].join(", ")}`;
197
+ if (to === agent) return "error: that is you";
198
+ if (S.sent >= MAX_MESSAGES) return `error: message budget (${MAX_MESSAGES} asks/tells per user message) used up. Finish with what you have.`;
199
+ return null;
200
+ };
201
+
202
+ pi.registerTool({
203
+ name: "spawn_agent", label: "Spawn agent",
204
+ description: "Create a new peer agent with a role. It starts idle. Agents are numbered in creation order (agent1, agent2, ...); "
205
+ + "the result tells you the new name.",
206
+ parameters: Type.Object({
207
+ role: Type.String(),
208
+ name: Type.Optional(Type.String({ description: "Only if the user asked for a specific name. Otherwise leave out." })),
209
+ app: Type.Optional(Type.String({ description: "The surface of the app this agent is part of (a simulation's member, a "
210
+ + "character). Leave out for agents that work with you." })),
211
+ }),
212
+ async execute(_id, a) {
213
+ if (a.name && (isAgent(a.name) || a.name === "user")) return result(`error: name '${a.name}' is taken`);
214
+ if (host.agents.size >= MAX_AGENTS) return result(`error: the team is full (${MAX_AGENTS} agents). Give work to an existing agent (list_agents).`);
215
+ let name = a.name, n = host.agents.size + 1;
216
+ while (!name || isAgent(name)) name = `agent${n++}`; // next free number
217
+ await host.createAgent(name, a.role, a.app);
218
+ changed(name, "idle");
219
+ return result(`created agent '${name}'` + (a.app ? `, part of app '${a.app}'` : "") + ". It is idle until someone messages it.");
220
+ },
221
+ });
222
+
223
+ pi.registerTool({
224
+ name: "reset_agent", label: "Reset agent",
225
+ description: "Empty an agent's memory, keeping its role (for a new round that starts fresh). Give a name, or app to reset "
226
+ + "all agents of that app. Only when they are idle.",
227
+ parameters: Type.Object({ name: Type.Optional(Type.String()), app: Type.Optional(Type.String()) }),
228
+ async execute(_id, a) {
229
+ const names = [...host.agents.values()].filter((x: any) => x.name === a.name || (a.app && x.app === a.app)).map((x: any) => x.name);
230
+ if (!names.length) return result(a.name ? `error: no agent '${a.name}'` : `error: no agents in app '${a.app}'`);
231
+ if (names.includes(me())) return result("error: you can't reset yourself");
232
+ const error = await api.reset(names);
233
+ return result(error ?? `reset ${names.join(", ")}: each starts its next turn with an empty memory and the same role.`);
234
+ },
235
+ });
236
+
237
+ pi.registerTool({
238
+ name: "tell", label: "Tell",
239
+ description: "Send a note to another agent and keep working (don't wait). Its answer comes back to you later as a reply.",
240
+ parameters: Type.Object({ to: Type.String(), text: Type.String() }),
241
+ async execute(_id, a) {
242
+ const agent = me(), now = Date.now();
243
+ const error = checkTo(agent, a.to);
244
+ if (error) return result(error);
245
+ const recent = (S.recent[agent] ?? []).filter((t: number) => now - t < 60000);
246
+ if (S.lastNote[`${agent}>${a.to}`] === a.text) return result(`error: dropped, same as your last note to ${a.to}. It already has it.`);
247
+ if (recent.length >= NOTES_PER_MINUTE) return result(`error: slow down: at most ${NOTES_PER_MINUTE} notes a minute. Wait for replies instead.`);
248
+ if (inbox(a.to).length >= INBOX_CAP) return result(`error: ${a.to}'s inbox is full (${INBOX_CAP}). Wait for it to catch up.`);
249
+ S.recent[agent] = [...recent, now];
250
+ S.lastNote[`${agent}>${a.to}`] = a.text;
251
+ S.sent++;
252
+ if (owed(agent) === a.to) { // it's owed your final answer anyway: an update, no answer back
253
+ put(a.to, "fyi", agent, a.text, []);
254
+ return result(`sent as information only: ${a.to} gets your final answer anyway, so don't repeat this there. `
255
+ + `It won't answer this; if you need an answer from ${a.to}, use ask_agent.`);
256
+ }
257
+ put(a.to, "note", agent, a.text, [agent, ...(S.chain[agent] ?? [])]);
258
+ return result(`sent. ${a.to} is ${status()[a.to].state}; its answer will come to you as a reply. `
259
+ + "If all you have left is waiting, end your turn: the reply wakes you.");
260
+ },
261
+ });
262
+
263
+ pi.registerTool({
264
+ name: "ask_agent", label: "Ask agent",
265
+ description: "Ask another agent and wait for its answer (the result). For quick questions; use tell for longer work. "
266
+ + "Several ask_agent calls in one step run in parallel.",
267
+ parameters: Type.Object({ to: Type.String(), text: Type.String() }),
268
+ async execute(_id, a, signal) {
269
+ const agent = me();
270
+ const error = checkTo(agent, a.to);
271
+ if (error) return result(error);
272
+ if (waitsOn(a.to, agent)) return result(`error: '${a.to}' is waiting on you, so it can't answer now. Use tell, or answer from what you know.`);
273
+ S.sent++;
274
+ return result(await ask(agent, a.to, a.text, signal));
275
+ },
276
+ });
277
+
278
+ pi.registerTool({
279
+ name: "list_agents", label: "List agents", description: "List all agents: role, and what each is doing right now.",
280
+ parameters: Type.Object({}),
281
+ async execute() {
282
+ const now = Date.now() / 1000;
283
+ return result(Object.entries(status()).map(([n, s]: any) => `${n} (${s.role}): ${s.state}`
284
+ + (s.doing ? `, doing: ${s.doing} for ${Math.round(now - s.since)} s` : "")
285
+ + (s.queued ? `, ${s.queued} waiting in its inbox` : "")).join("\n"));
286
+ },
287
+ });
288
+
289
+ // From apps on the page (/ui/agents.js): a message to an app agent, or a fresh round for an app's agents
290
+ host.pageHandlers.set("swarm", async (msg: any) => {
291
+ if (msg.type === "app_reset") {
292
+ const names = [...host.agents.values()].filter((x: any) => x.app === msg.surface).map((x: any) => x.name);
293
+ const error = await api.reset(names);
294
+ if (error) host.broadcast("error", { agent: "app", text: `reset of ${msg.surface}: ${error}` });
295
+ } else if (msg.type === "app_message") {
296
+ const now = Date.now();
297
+ S.appSent = S.appSent.filter((t: number) => now - t < 60000);
298
+ if (!isAgent(msg.to)) host.broadcast("error", { agent: "app", text: `app message to unknown agent '${msg.to}'` });
299
+ else if (S.appSent.length >= APP_MESSAGES_PER_MINUTE)
300
+ host.broadcast("error", { agent: "app", text: `app message to ${msg.to} dropped: more than ${APP_MESSAGES_PER_MINUTE} a minute` });
301
+ else { S.appSent.push(now); api.send("app", msg.to, msg.text ?? ""); }
302
+ }
303
+ });
304
+ }
@@ -0,0 +1,69 @@
1
+ # Showing UI to the user (the ui extension)
2
+ Show forms, tables, dashboards or games as UI surfaces: cards in the page's main space, described as JSON.
3
+ Prefer a surface over editing the page ({code}/extensions/page/page.html). Aim for one screen; make it scroll only if the user asks.
4
+ A surface shows only what you build, with no frame: when asked for a thing (a smiley, a chart), show just
5
+ that thing. Add a background, border or Card only if the thing itself needs one. An app's page should keep
6
+ a transparent background, so it sits on the workspace.
7
+ Name what you build (a clear surface id, ids or data-name on elements): the user points at things by name.
8
+ Compose from existing components; write a new component only if none fits.
9
+ Before building or restyling what the user sees, load the ui-design skill (how it should look and read).
10
+ - ui_catalog: lists the components and their props. Read it first.
11
+ - ui_render(surface, components, data): draw or replace a surface. components is a flat list; each has
12
+ "id", "component" (a catalog name), its props, and "children": [ids] or "child": id. One has id "root".
13
+ A prop can be {"path": "/x"} to bind it to this surface's data (inputs write to it), or
14
+ {"ref": "other-surface", "path": "/x"} to read another surface's data live (no copy: it follows changes).
15
+ Errors come back to you; fix and retry.
16
+ - about="...": one or two sentences on what the surface is for and why it is built this way. The user reads
17
+ it on the surface's back side; you read it in {code}/ui/surfaces.json. Keep it true when you change things.
18
+ - options=[{"label", "components", "data"}, ...] instead of components: when you are not sure what the user
19
+ meant, offer 2-3 versions; they flip through them and keep one.
20
+ - Pinned surfaces (the user held their label): your ui_render, ui_update and ui_delete on them fail. Don't
21
+ work around it: make a new surface (made_from=[the pinned one]) or ask the user.
22
+ - Surfaces stack in the main space and are as wide as their content. place={"x", "y", "width"?, "height"?}
23
+ (px from the main space's top-left) pins one at a spot instead: use it when the user points at a spot
24
+ ("here") or draws an area (then fill that area: its x, y, width and height).
25
+ - Everything you show is draggable by default: the user moves a surface by its label (shown on hover), and
26
+ it stays where they put it (ui_render without place keeps it there; where things are now: the "place" of
27
+ each surface in {code}/ui/surfaces.json). Pass draggable=false only when the
28
+ user wants something fixed. In an app you build, make separate pieces draggable too, unless told otherwise.
29
+ - Everything you show is resizable by default too: the user drags its bottom-right corner (shown on hover),
30
+ and the surface's root component fills the new size. So build things that fill their space (sizes in %,
31
+ flex, or scaled to their box, e.g. an SVG with a viewBox), not fixed px. Pass resizable=false only when the
32
+ user wants a fixed size.
33
+ - ui_update(surface, path, value): change data only (fast; components keep their state). ui_delete(surface).
34
+ - Data (what users typed, rows, scores) is kept in {data}/ui-data.json (not versioned); the rest of each
35
+ surface in {code}/ui/surfaces.json. The user can undo any change (theirs or yours), so don't keep backups.
36
+ - When the user clicks a Button you get "[ui action] <name> ..." with the surface's data.
37
+ - Voice input (dictation, meeting notes): import /ui/voice.js in the app (read its header), never the
38
+ browser's speech API or a provider directly: it uses the sandbox's speech-to-text API, or the browser's.
39
+ - A new component: {code}/extensions/ui/components/<Name>.js. Start with a comment line "// Name: what it does" (more
40
+ comment lines if needed), then its about block, then
41
+ `export default class extends HTMLElement { update(props, ui) {...} }`.
42
+ The about block of a component (strict JSON after `export const about = `):
43
+ "version": 1 the shape of its props; raise it when they change, and export
44
+ upgrade(props, fromVersion) -> props, so surfaces saved earlier still draw
45
+ "props": {"name": {"type": "text" | "number" | "boolean" | "choice" | "list" | "rows" | "object" | "action",
46
+ "options": [...] (for choice), "default": ..., "doc": "what it is"}}
47
+ "children": "none" | "one" | "many"
48
+ "levers": ["Shown as {variant}."] the back side: plain sentences; each {prop} becomes a control the user
49
+ can change there (choice and boolean cycle, text and number are typed)
50
+ "accepts": "rows with a number column" optional: what it can take when something is dropped on it
51
+ "example": {...props} one sample, for the catalog and for checks
52
+ When you change a component's props in a way old surfaces would break on, raise "version" and export
53
+ upgrade(props, fromVersion) returning props in the new shape.
54
+ update() runs on every change, on the same element: keep internal state there (e.g. a game loop).
55
+ ui.children: child elements, placed with ui.mount(container); ui.set(prop, value) writes a bound prop;
56
+ ui.action(name, context) sends an action to you. Make it general so it can be reused. New components work
57
+ immediately; edited components and App files show when your turn ends (the page refreshes itself). Big multi-file apps: {code}/ui/apps/<name>/, shown with App.
58
+ Pointing inside a canvas: give the element (or an App page's window) a partAt(x, y) function returning
59
+ {"name", ...} for what is at that spot, so the user can point at parts (an enemy, a cell).
60
+ - Agents inside what you build (a simulation's members, a debate, characters): load the agent-apps
61
+ skill first. They are real agents that belong to the app, not workers helping you build it.
62
+ - Libraries: components import them by name (import { marked } from "marked"), as mapped in the import map
63
+ at the top of {code}/extensions/page/page.html; installed ones are in {code}/extensions/ui/package.json. To add
64
+ one, download its ES-module build into {code}/extensions/ui/node_modules/<pkg>/ and add a line to the import map.
65
+ - Component pitfalls (you can't see the browser, so avoid them up front):
66
+ keep your own state in one object, this.state = {...}: fields like this.dir, this.title, this.hidden,
67
+ this.id, this.style, this.lang are built-in HTMLElement properties and silently break;
68
+ set a canvas's width/height BEFORE drawing (resizing clears it); don't create things twice in update();
69
+ remove document-level listeners/timers when the element is no longer connected.
@@ -0,0 +1,45 @@
1
+ // Bridge from an app ({code}/ui/apps/<name>/, shown by the App component) to the agents. An app imports it:
2
+ // import agents from "/ui/agents.js";
3
+ // agents.send(to, text) a message to an agent, from "app"; its answer comes as a "message" event
4
+ // (role "assistant", no tool_calls). Each one is a model call: limited per minute.
5
+ // agents.action(name, context) an action for the agent that owns this app's surface, like a Button
6
+ // agents.on(type, fn) events as the page gets them, past ones first (replayed on connect), then live:
7
+ // "message" {agent, role, content, tool_calls, ts}: every message of every agent,
8
+ // incl. its ask_agent / tell calls (who talks to whom); "status" {agents}: who is busy
9
+ // agents.list() {name: {role, app, state, doing, queued}}, from the latest "status" event
10
+ // agents.reset() empty the memories of this app's agents (spawned with app=<this surface>),
11
+ // roles kept: a fresh round. Refused (an "error" event) while one is busy.
12
+ // agents.load() this app's saved data ({} at first), from the appdata plugin (on the server)
13
+ // agents.save(data) replace it; every page and app frame then gets "app_data" {app, data}, so
14
+ // the same app in other surfaces updates (check ev.app === agents.app)
15
+ // agents.app this app's name: its folder in {code}/ui/apps
16
+ // It opens its own WebSocket to /ws, the page's protocol (the page extension).
17
+ const ws = new WebSocket((location.protocol === "https:" ? "wss://" : "ws://") + location.host + "/ws");
18
+ const open = new Promise((resolve) => ws.addEventListener("open", resolve));
19
+ const listeners = {};
20
+ let status = {};
21
+
22
+ ws.onmessage = (e) => {
23
+ const ev = JSON.parse(e.data);
24
+ if (ev.type === "status") status = ev.agents;
25
+ for (const fn of listeners[ev.type] || []) fn(ev);
26
+ };
27
+
28
+ // In the versions plugin's screenshot (/?shot) the app is only looked at: send nothing, no model calls.
29
+ const shot = new URLSearchParams(window.top.location.search).has("shot");
30
+ const post = async (obj) => { if (shot) return; await open; ws.send(JSON.stringify(obj)); };
31
+ const app = location.pathname.split("/")[3]; // /ui/apps/<name>/...
32
+ const store = `/ui-api/app-data/${app}`;
33
+ // the surface this app is shown in (the App component's frame sits inside it on the page)
34
+ const surface = () => window.frameElement?.closest("[data-surface]")?.dataset.surface;
35
+
36
+ export default {
37
+ send: (to, text) => post({ type: "app_message", to, text }),
38
+ action: (name, context = {}) => post({ type: "ui_action", surface: surface(), component: "app", name, context }),
39
+ on: (type, fn) => (listeners[type] ??= []).push(fn),
40
+ list: () => status,
41
+ reset: () => post({ type: "app_reset", surface: surface() }),
42
+ app,
43
+ load: async () => (await fetch(store, { cache: "no-store" })).json(),
44
+ save: async (data) => { if (!shot) await fetch(store, { method: "PUT", body: JSON.stringify(data) }); },
45
+ };