@chalky/chalky 0.1.4 → 0.1.6

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 (137) hide show
  1. package/README.md +16 -5
  2. package/agent/SYSTEM.md +36 -0
  3. package/agent/extensions/app/README.md +72 -0
  4. package/{app/extensions/ui → agent/extensions/app}/agents.js +22 -16
  5. package/agent/extensions/app/guide.md +5 -0
  6. package/agent/extensions/app/index.ts +66 -0
  7. package/{app/extensions/ui/skills/agent-apps → agent/extensions/app/skills/app-agents}/SKILL.md +17 -18
  8. package/agent/extensions/app/skills/test-your-app/SKILL.md +23 -0
  9. package/{app/extensions/boards → agent/extensions/board}/browser.js +30 -32
  10. package/{app/extensions/boards → agent/extensions/board}/guide.md +1 -1
  11. package/{app/extensions/boards → agent/extensions/board}/index.ts +32 -30
  12. package/agent/extensions/card/README.md +88 -0
  13. package/agent/extensions/card/blocks/Button.js +34 -0
  14. package/{app/extensions/ui/components → agent/extensions/card/blocks}/Column.js +2 -2
  15. package/agent/extensions/card/blocks/Markdown.js +37 -0
  16. package/{app/extensions/ui/components/Card.js → agent/extensions/card/blocks/Panel.js} +3 -3
  17. package/{app/extensions/ui/components → agent/extensions/card/blocks}/Row.js +2 -2
  18. package/{app/extensions/ui/components → agent/extensions/card/blocks}/TextField.js +3 -3
  19. package/{app/extensions/ui → agent/extensions/card}/browser.js +155 -129
  20. package/agent/extensions/card/guide.md +13 -0
  21. package/agent/extensions/card/index.ts +370 -0
  22. package/{app/extensions/commands → agent/extensions/command}/browser.js +3 -3
  23. package/agent/extensions/command/guide.md +2 -0
  24. package/{app/extensions/commands → agent/extensions/command}/index.ts +14 -16
  25. package/agent/extensions/doodle/README.md +57 -0
  26. package/agent/extensions/doodle/browser.js +1013 -0
  27. package/agent/extensions/doodle/guide.md +22 -0
  28. package/agent/extensions/doodle/index.ts +274 -0
  29. package/agent/extensions/file/browser.js +182 -0
  30. package/agent/extensions/file/guide.md +5 -0
  31. package/{app/extensions/files → agent/extensions/file}/index.ts +37 -18
  32. package/{app/extensions/frames → agent/extensions/frame}/README.md +13 -13
  33. package/{app/extensions/frames → agent/extensions/frame}/browser.js +20 -18
  34. package/agent/extensions/frame/guide.md +3 -0
  35. package/agent/extensions/frame/index.ts +90 -0
  36. package/{app/extensions/ui_history → agent/extensions/history}/browser.js +18 -18
  37. package/{app/extensions/ui_history → agent/extensions/history}/index.ts +34 -33
  38. package/{app → agent}/extensions/mcp/browser.js +1 -1
  39. package/agent/extensions/mcp/guide.md +3 -0
  40. package/{app → agent}/extensions/mcp/index.ts +2 -4
  41. package/{app/extensions/login → agent/extensions/model}/browser.js +67 -67
  42. package/{app → agent}/extensions/model/index.ts +8 -4
  43. package/{app/extensions/login/index.ts → agent/extensions/model/login.ts} +47 -46
  44. package/agent/extensions/note/README.md +37 -0
  45. package/agent/extensions/note/browser.js +382 -0
  46. package/agent/extensions/note/guide.md +9 -0
  47. package/agent/extensions/note/index.ts +215 -0
  48. package/agent/extensions/page/browser.js +453 -0
  49. package/{app → agent}/extensions/page/index.ts +72 -3
  50. package/{app → agent}/extensions/page/page.html +102 -49
  51. package/agent/extensions/page/skills/interact-with-page/SKILL.md +59 -0
  52. package/agent/extensions/prompt/index.ts +38 -0
  53. package/{app → agent}/extensions/reload/index.ts +3 -3
  54. package/{app → agent}/extensions/screen/browser.js +10 -23
  55. package/{app → agent}/extensions/screen/index.ts +11 -11
  56. package/{app/extensions/steps → agent/extensions/step}/index.ts +1 -1
  57. package/{app/extensions/swarm → agent/extensions/team}/README.md +19 -19
  58. package/{app/extensions/swarm → agent/extensions/team}/guide.md +4 -4
  59. package/{app/extensions/swarm → agent/extensions/team}/index.ts +41 -66
  60. package/{app/extensions/versions → agent/extensions/version}/browser.js +19 -10
  61. package/agent/extensions/version/index.ts +205 -0
  62. package/{app → agent}/extensions/viewer/README.md +4 -4
  63. package/{app → agent}/extensions/viewer/browser.js +8 -8
  64. package/agent/extensions/viewer/guide.md +3 -0
  65. package/{app → agent}/extensions/viewer/index.ts +22 -17
  66. package/{app → agent}/extensions/voice/browser.js +4 -3
  67. package/{app → agent}/extensions/voice/index.ts +8 -6
  68. package/{app/extensions/ui → agent/extensions/voice}/voice.js +5 -5
  69. package/agent/settings.json +8 -0
  70. package/{app/extensions/ui/skills/ui-check → agent/skills/browser-check}/SKILL.md +4 -4
  71. package/{app/extensions/ui/skills/ui-check → agent/skills/browser-check}/scripts/check.mjs +1 -1
  72. package/{app/extensions/ui/skills/ui-design → agent/skills/design}/SKILL.md +15 -15
  73. package/agent/skills/extension-map/SKILL.md +56 -0
  74. package/agent/skills/show-me/SKILL.md +66 -0
  75. package/{app → agent}/skills/write-skill/SKILL.md +1 -1
  76. package/bin/chalky.mjs +2 -2
  77. package/package.json +9 -8
  78. package/server.ts +313 -58
  79. package/app/SYSTEM.md +0 -31
  80. package/app/extensions/appdata/README.md +0 -10
  81. package/app/extensions/appdata/guide.md +0 -4
  82. package/app/extensions/appdata/index.ts +0 -47
  83. package/app/extensions/commands/guide.md +0 -3
  84. package/app/extensions/doodle/README.md +0 -15
  85. package/app/extensions/doodle/browser.js +0 -398
  86. package/app/extensions/doodle/guide.md +0 -6
  87. package/app/extensions/doodle/index.ts +0 -54
  88. package/app/extensions/files/browser.js +0 -96
  89. package/app/extensions/files/guide.md +0 -7
  90. package/app/extensions/frames/guide.md +0 -7
  91. package/app/extensions/frames/index.ts +0 -85
  92. package/app/extensions/mcp/guide.md +0 -5
  93. package/app/extensions/point/README.md +0 -22
  94. package/app/extensions/point/browser.js +0 -247
  95. package/app/extensions/point/guide.md +0 -6
  96. package/app/extensions/point/index.ts +0 -112
  97. package/app/extensions/prompt/index.ts +0 -19
  98. package/app/extensions/ui/README.md +0 -69
  99. package/app/extensions/ui/components/App.js +0 -28
  100. package/app/extensions/ui/components/Button.js +0 -28
  101. package/app/extensions/ui/components/Markdown.js +0 -37
  102. package/app/extensions/ui/guide.md +0 -7
  103. package/app/extensions/ui/index.ts +0 -291
  104. package/app/extensions/versions/index.ts +0 -168
  105. package/app/extensions/viewer/guide.md +0 -5
  106. package/app/mcp.json +0 -3
  107. package/app/settings.json +0 -3
  108. package/app/skills/plugin-map/SKILL.md +0 -49
  109. package/app/ui/apps/.keep +0 -0
  110. package/node_modules/xlsx/CHANGELOG.md +0 -361
  111. package/node_modules/xlsx/LICENSE +0 -201
  112. package/node_modules/xlsx/README.md +0 -41
  113. package/node_modules/xlsx/bin/xlsx.njs +0 -310
  114. package/node_modules/xlsx/bower.json +0 -22
  115. package/node_modules/xlsx/dist/LICENSE +0 -201
  116. package/node_modules/xlsx/dist/cpexcel.d.ts +0 -39
  117. package/node_modules/xlsx/dist/cpexcel.full.mjs +0 -1502
  118. package/node_modules/xlsx/dist/cpexcel.js +0 -1506
  119. package/node_modules/xlsx/dist/shim.min.js +0 -2
  120. package/node_modules/xlsx/dist/xlsx.core.min.js +0 -18
  121. package/node_modules/xlsx/dist/xlsx.core.min.map +0 -1
  122. package/node_modules/xlsx/dist/xlsx.extendscript.js +0 -28259
  123. package/node_modules/xlsx/dist/xlsx.full.min.js +0 -24
  124. package/node_modules/xlsx/dist/xlsx.full.min.map +0 -1
  125. package/node_modules/xlsx/dist/xlsx.mini.min.js +0 -10
  126. package/node_modules/xlsx/dist/xlsx.mini.min.map +0 -1
  127. package/node_modules/xlsx/dist/xlsx.zahl.js +0 -4
  128. package/node_modules/xlsx/dist/xlsx.zahl.mjs +0 -2
  129. package/node_modules/xlsx/dist/zahl.d.ts +0 -4
  130. package/node_modules/xlsx/package.json +0 -183
  131. package/node_modules/xlsx/types/index.d.ts +0 -1065
  132. package/node_modules/xlsx/types/tsconfig.json +0 -15
  133. package/node_modules/xlsx/xlsx.js +0 -28103
  134. package/node_modules/xlsx/xlsx.mjs +0 -28228
  135. package/node_modules/xlsx/xlsxworker.js +0 -14
  136. /package/{app/extensions/ui/components → agent/extensions/card/blocks}/Table.js +0 -0
  137. /package/{app/extensions/ui/components → agent/extensions/card/blocks}/Text.js +0 -0
package/README.md CHANGED
@@ -21,6 +21,9 @@ MODEL_BASE_URL=https://your-endpoint/v1 MODEL_API_KEY=... MODEL_NAME=your-model
21
21
 
22
22
  Then open http://127.0.0.1:3000.
23
23
 
24
+ To update: `npm update -g @chalky/chalky`, then restart `chalky`. The new version is merged with what your agents
25
+ changed (see `agent/` below).
26
+
24
27
  | Variable | |
25
28
  |---|---|
26
29
  | `MODEL_BASE_URL` | the endpoint, up to `/v1` |
@@ -34,13 +37,21 @@ Then open http://127.0.0.1:3000.
34
37
 
35
38
  `~/.chalky` (or `CHALKY_HOME`) holds:
36
39
 
37
- - `app/`: the agents' code, copied from the package on the first run. The agents edit it, and each change is
38
- versioned. A newer version of the package doesn't overwrite it.
39
- - `state/`: conversations, version history, what the apps keep.
40
- - `workspace/`: the agents' working folder.
40
+ - `agent/`: what the agents are: their prompt, settings and extensions (the page among them). The agents edit it.
41
+ Each new version of the package is merged into it at the next start, as a version in the history: files only the
42
+ new version changed are updated, files only your agents changed stay theirs, and both changes go in when they
43
+ touch different lines. When both changed the same lines, yours stays, the new version waits next to it as
44
+ `<file>.new`, and agent1 merges the two. Undo an update like any change, in Versions.
45
+ - `built/`: what the agents made for you: the cards on your boards, and the apps they wrote.
46
+ - `data/`: what is kept while running: conversations, what you typed into cards and apps, notes, undo history.
47
+ - `workspace/`: the agents' working folder; `workspace/drops/` holds your files.
48
+
49
+ `agent/` and `built/` share one version history: each agent turn that changes them is saved, and a rollback puts
50
+ both back. A home from before this layout (`app/`, `state/`) is moved into it on the first start; the old code
51
+ stays in `app-before-reorg/`.
41
52
 
42
53
  ## MCP servers
43
54
 
44
- Tools from MCP servers go in `~/.chalky/app/mcp.json`, in pi's format (stdio or streamable HTTP; see pi's
55
+ Tools from MCP servers go in `~/.chalky/agent/mcp.json`, in pi's format (stdio or streamable HTTP; see pi's
45
56
  `docs/mcp.md`). Type `/mcp` on the page to choose which servers and tools the agents may use. Keep secrets in
46
57
  environment variables and refer to them as `${NAME}` in `headers` or `env`.
@@ -0,0 +1,36 @@
1
+ You are an agent in Chalky. You build what the user sees: the page is a workspace where what you build (cards on
2
+ boards, each showing blocks or an app) fills the screen, and the conversation is a side panel. The user says what they want; you put a
3
+ first version on screen that is already complete and polished; then they point at things on it and leave notes, and
4
+ you change exactly that. The user marks up details, not missing basics.
5
+ - Build, don't describe. A first version they can point at beats questions you could answer by building.
6
+ - Your final reply is the only text they read in full: one or two sentences on what changed.
7
+ - The page around your work is the user's: change it only when asked. Build inside the main space.
8
+
9
+ What follows this: the skills you can load, your working folder (<cwd>: bash runs there), one short section for
10
+ each part of you that has something to say (each comes from an extension), and <paths>: your four folders, the
11
+ host and pi's docs. In README.md files and skills, {agent}/..., {built}/..., {data}/... and {workspace}/... are paths
12
+ in those folders. A section that names a README.md is the short version of its extension: read that file when the
13
+ section says, once (it stays in your conversation). Some tools are loaded only when needed: tool_search finds and
14
+ loads them (card_blocks, viewer_read, screen_see, team_reset, MCP servers' tools). Tools you already have need no
15
+ search.
16
+ Two kinds of change, two places: what the user asks you to make (a card, an app) goes in {built}; a change to how
17
+ Chalky itself works is a change to you, in {agent}.
18
+
19
+ Your tools:
20
+ - read to look at files and pictures (not cat or sed); bash for ls, grep, find and running things.
21
+ - edit for precise changes: each edits[].oldText must match the file exactly and only once; keep it as small as
22
+ that allows. Several changes in one file: one edit call with several entries, each matched against the original
23
+ file, none overlapping. An edit whose newText equals its oldText changes nothing and is refused.
24
+ - write only for new files or complete rewrites.
25
+ - Name files by their full path when you mention them.
26
+
27
+ Changing yourself: when the work needs it, you can change any part of you, the page included. You are pi
28
+ extensions, one folder each: {agent}/extensions/<name>/. In it, index.ts is `export default function (pi:
29
+ ExtensionAPI) { ... }`, which registers tools (pi.registerTool) and event handlers (pi.on); and as the extension
30
+ needs them, guide.md (its prompt section, found by itself), README.md (the whole of it), browser.js (its part of
31
+ the page, found by itself) and skills/. The page itself is the page extension; this text is {agent}/SYSTEM.md;
32
+ pi's settings (e.g. compaction) are {agent}/settings.json (pi's docs/settings.md). Extensions reach
33
+ shared state and the web server through globalThis.chalky: the host, the fixed core under them (don't edit it).
34
+ Read pi's extension docs (<paths>) before writing or changing an extension. After editing, call the reload tool:
35
+ your extensions load again when your turn ends, and your conversation is kept. A changed page or browser.js shows
36
+ when the page reloads, after your turn.
@@ -0,0 +1,72 @@
1
+ # Making an app (the app extension)
2
+ Everything a usual app needs is here (you don't need the card README). The design skill goes deeper on charts,
3
+ diagrams, game art and one look across cards: load it when the app has any of those.
4
+
5
+ ## Before you write
6
+ The first version is already complete and polished: the user marks up details, not missing basics. So first list,
7
+ in a short message, the 3-5 things that would make this app impressive for this subject: what someone who knows the
8
+ subject would expect to find, and a picture (diagram, chart, animation) wherever one explains faster than text.
9
+ Then build all of them in the first version, not a smaller first cut.
10
+
11
+ ## Write it, show it
12
+ - Write {built}/apps/<name>/index.html (other files next to it), then card_show(card=<id>, app=<name>, about="...").
13
+ It is served at {page}/built/apps/<name>/. card_show checks it on the board and reports errors and whether it
14
+ fits its card: fix those and show it again. height=<px>: its height when nothing else sizes it (default 400).
15
+ - about: one or two sentences on what it is for and any decision a later change should respect (its back side).
16
+ - Name what the user may point at: ids or data-name on elements ("habit-Read", not "row3"). A canvas: give the
17
+ page's window a partAt(x, y) returning {"name", ...} for what is at that spot.
18
+ - The user moves and resizes the card: fill the space (%, flex, grid, an SVG with a viewBox), not fixed px.
19
+ - A pinned card (the user held its label): card_show on it fails. Make a new card (made_from=[it]) or ask.
20
+ - Changing it later: edit the part (the edit tool), not a rewrite. App files show when your turn ends.
21
+
22
+ ## Keep its data
23
+ `import agents from "/app/agents.js"` (a module script). `await agents.load()` gives the saved data ({} at first);
24
+ `agents.save(data)` replaces it after every change (on the server, {data}/app/<name>.json: kept across reloads, the
25
+ same in every browser, readable by you with cat). Other cards showing the same app hear each save:
26
+ `agents.on("app_data", ({app, data}) => { if (app === agents.app) ... })` (the field is `data`). Never localStorage
27
+ (one browser only, invisible to you). To change it yourself: curl -X PUT --data @new.json {page}/app/data/<name>.
28
+
29
+ ## An app that asks an AI (a plan from a goal, a summary, an answer)
30
+ A real agent does it, never a canned sample. While building, spawn one for the card: team_spawn(role, name="planner",
31
+ app=<card id>), its role ending "Answer only with JSON like {...}. Don't use tools." In the app:
32
+ ```
33
+ const asked = Date.now() / 1000; agents.send("planner", goal);
34
+ agents.on("message", (ev) => { // events replay on connect: take only the answer to this question
35
+ if (ev.agent !== "planner" || ev.role !== "assistant" || ev.tool_calls?.length || ev.ts < asked) return;
36
+ data.plan = JSON.parse(ev.text.slice(ev.text.indexOf("{"), ev.text.lastIndexOf("}") + 1)); agents.save(data); draw();
37
+ });
38
+ ```
39
+ Show that it is thinking while it waits; agents.send is limited per minute. Agents with personalities that talk to
40
+ each other (a debate, a simulation): load the app-agents skill.
41
+
42
+ ## Apps that work together
43
+ Each card is its own page: a click in one never reaches another. One app saves what was picked
44
+ (`agents.save({...data, picked: "Goblet squat"})`); the other hears every app's saves:
45
+ `agents.on("app_data", ({app, data}) => { if (app === "fitflow") show(data.picked) })`, and can read the first one's
46
+ data at start with `fetch("/app/data/fitflow").then((r) => r.json())`.
47
+
48
+ ## Look
49
+ The page is light, glassy and quiet; match it unless the user asks for another look:
50
+ - Transparent background; the system font (`font: inherit` or system-ui); text #1d1d1f, muted rgba(60,60,67,.55),
51
+ accent #0071e3, danger #e5484d, lines rgba(0,0,0,.06), glass rgba(255,255,255,.7), radius 16px.
52
+ - Show just the thing: no frame or panel around it unless it needs one. Group with flex/grid and gap.
53
+ - Open in a working state (its real data, or a marked example), never an empty shell; `tabular-nums` for digits.
54
+ - Buttons say what happens ("Add habit"); short plain words; status colours always with a word or icon.
55
+ - Ground it in the subject: its real units, terms and conventions (a chess clock shows mm:ss; a kanban card has an
56
+ owner and a state). Real content, never lorem ipsum.
57
+ - Summary before detail; show state in form (a pill, a chip, a stripe) as well as numbers.
58
+ - Finish every state: empty, loading, error, done. Interactive things look interactive (hover, pressed, visible
59
+ keyboard focus); a small transition where something changes, never content hidden waiting for an animation.
60
+ - Avoid the generic AI look: cream and terracotta, purple gradients, emoji as markers, the same shadowed card on
61
+ everything. For a chart, a diagram, a game's art or a restyle, load the design skill.
62
+
63
+ ## Check it works
64
+ card_show covers errors and fit. Then, before telling the user what it does, use it on their page as they would:
65
+ load the test-your-app skill. Say only what you saw working.
66
+
67
+ ## More
68
+ - Calculations (totals, streaks, dates) belong in the page's code, never worked out by you and typed in.
69
+ - Voice input: import /voice/voice.js (read its header), not the browser's speech API.
70
+ - Libraries: put an ES-module build in {agent}/extensions/card/lib/<pkg>/ and import /card/lib/<pkg>/<file>.js.
71
+ - agents.action(name, context) wakes the agent that owns the card (for what truly needs you); the rest of the
72
+ bridge: the header of {agent}/extensions/app/agents.js.
@@ -1,18 +1,19 @@
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";
1
+ // Bridge from an app ({built}/apps/<name>/, shown on a card) to the agents. An app imports it:
2
+ // import agents from "/app/agents.js";
3
3
  // agents.send(to, text) a message to an agent, from "app"; its answer comes as a "message" event
4
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
5
+ // agents.action(name, context) an action for the agent that owns this app's card, like a Button
6
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
7
+ // "message" {agent, role, content, tool_calls, ts, to, text}: every message of every agent;
8
+ // to and text: its "[to: <name>]" tag ("all" without one) and the words after it;
9
+ // incl. its team_ask / team_tell calls (who talks to whom); "status" {agents}: who is busy
9
10
  // 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
+ // agents.reset() empty the memories of this app's agents (spawned with app=<this card>),
11
12
  // 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.load() this app's saved data ({} at first), kept by the app extension (on the server)
13
14
  // 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
15
+ // the same app on other cards updates (check ev.app === agents.app)
16
+ // agents.app this app's name: its folder in {built}/apps
16
17
  // It opens its own WebSocket to /ws, the page's protocol (the page extension).
17
18
  const ws = new WebSocket((location.protocol === "https:" ? "wss://" : "ws://") + location.host + "/ws");
18
19
  const open = new Promise((resolve) => ws.addEventListener("open", resolve));
@@ -22,23 +23,28 @@ let status = {};
22
23
  ws.onmessage = (e) => {
23
24
  const ev = JSON.parse(e.data);
24
25
  if (ev.type === "status") status = ev.agents;
26
+ if (ev.type === "message" && typeof ev.content === "string") { // an app agent's "[to: <name>] ..." answer, read once here
27
+ const tag = /^\s*\[to:\s*([^\]]+)\]\s*/i.exec(ev.content);
28
+ ev.to = tag ? tag[1].trim() : "all";
29
+ ev.text = tag ? ev.content.slice(tag[0].length) : ev.content;
30
+ }
25
31
  for (const fn of listeners[ev.type] || []) fn(ev);
26
32
  };
27
33
 
28
- // In the versions plugin's screenshot (/?shot) the app is only looked at: send nothing, no model calls.
34
+ // In the version extension's screenshot (/?shot) the app is only looked at: send nothing, no model calls.
29
35
  const shot = new URLSearchParams(window.top.location.search).has("shot");
30
36
  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;
37
+ const app = location.pathname.split("/")[3]; // /built/apps/<name>/...
38
+ const store = `/app/data/${app}`;
39
+ // the card this app is shown on (its frame sits inside the card on the page)
40
+ const card = () => window.frameElement?.closest("[data-card]")?.dataset.card;
35
41
 
36
42
  export default {
37
43
  send: (to, text) => post({ type: "app_message", to, text }),
38
- action: (name, context = {}) => post({ type: "ui_action", surface: surface(), component: "app", name, context }),
44
+ action: (name, context = {}) => post({ type: "card_action", card: card(), block: "app", name, context }),
39
45
  on: (type, fn) => (listeners[type] ??= []).push(fn),
40
46
  list: () => status,
41
- reset: () => post({ type: "app_reset", surface: surface() }),
47
+ reset: () => post({ type: "app_reset", card: card() }),
42
48
  app,
43
49
  load: async () => (await fetch(store, { cache: "no-store" })).json(),
44
50
  save: async (data) => { if (!shot) await fetch(store, { method: "PUT", body: JSON.stringify(data) }); },
@@ -0,0 +1,5 @@
1
+ # Apps
2
+ An app is a page you write in {built}/apps/<name>/ and show on a card (card_show with app). It keeps what the user
3
+ makes in it (notes, lists, picks) with /app/agents.js (agents.load, agents.save), on the server and shared by every
4
+ card showing it; never localStorage (one browser only, and you can't see it).
5
+ Before writing or changing an app, read README.md: it has all an app needs (showing it, its data, its look, checking it).
@@ -0,0 +1,66 @@
1
+ // Apps: pages the agents write in built/apps/<name>/ (the host serves them at /built/apps/<name>/), shown on a card
2
+ // (the card extension draws the frame). This extension gives apps what a plain page can't have:
3
+ // - their data, kept on the server: one JSON value per app in data/app/<name>.json, so it outlives the page, is
4
+ // the same in every browser, and the agents can read it. Whole value replaced each time: the last save wins
5
+ // (keep one app's data in one value). The same app on several cards shares it: every save is an "app_data" event;
6
+ // - agents.js, their bridge to the agents (load, save, send, action, reset, on: read its header);
7
+ // - the app-agents skill (apps with agents inside, e.g. a simulation's members).
8
+ // GET /app/data/<name> the app's data ({} if none yet) GET /app/agents.js the bridge
9
+ // PUT /app/data/<name> replace it (JSON, at most MAX_BYTES); event "app_data" {app, data}
10
+ // page messages app_message {to, text}: to an agent, from "app" (at most MESSAGES_PER_MINUTE, all apps together)
11
+ // app_reset {card}: empty the memories of the agents spawned for the app on that card (the team's reset)
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
+
16
+ const host = (globalThis as any).chalky;
17
+ const DIR = path.join(host.data, "app");
18
+ const MAX_BYTES = 1024 * 1024;
19
+ const MESSAGES_PER_MINUTE = 30;
20
+ const sent: number[] = host.shared.get("app-sent") ?? []; // when apps sent messages (on the host: outlives reloads)
21
+ host.shared.set("app-sent", sent);
22
+ const json = (res: any, v: unknown, status = 200) => res.writeHead(status, { "content-type": "application/json" }).end(JSON.stringify(v));
23
+
24
+ export default function (pi: ExtensionAPI) {
25
+ pi.on("resources_discover", () => ({ skillPaths: [path.join(import.meta.dirname, "skills")] }));
26
+
27
+ host.routes.set("/app/agents.js", (_req: any, res: any) => host.file(res, path.join(import.meta.dirname, "agents.js")));
28
+ host.routes.set("/app/data/", (req: any, res: any) => {
29
+ const name = decodeURIComponent(new URL(req.url, "http://x").pathname.slice("/app/data/".length));
30
+ if (!/^[\w-]+$/.test(name)) return json(res, { error: "app names are letters, digits, _ and -" }, 400);
31
+ const p = path.join(DIR, name + ".json");
32
+ if (req.method === "GET") {
33
+ if (!fs.existsSync(p)) return json(res, {});
34
+ return res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" }).end(fs.readFileSync(p));
35
+ }
36
+ if (req.method !== "PUT") return res.writeHead(405).end();
37
+ const chunks: Buffer[] = [];
38
+ let size = 0;
39
+ req.on("data", (c: Buffer) => { size += c.length; if (size <= MAX_BYTES) chunks.push(c); });
40
+ req.on("end", () => {
41
+ if (size > MAX_BYTES) return json(res, { error: `over ${MAX_BYTES / 1024} KB` }, 400);
42
+ let data;
43
+ try { data = JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch (e: any) { return json(res, { error: `not JSON: ${e.message}` }, 400); }
44
+ fs.mkdirSync(DIR, { recursive: true });
45
+ fs.writeFileSync(p + ".tmp", JSON.stringify(data));
46
+ fs.renameSync(p + ".tmp", p);
47
+ host.broadcast("app_data", { app: name, data });
48
+ json(res, { saved: true });
49
+ });
50
+ });
51
+
52
+ host.pageHandlers.set("app", async (msg: any) => {
53
+ const team = host.api.team, error = (text: string) => host.broadcast("error", { agent: "app", text });
54
+ if (msg.type === "app_reset" && team) {
55
+ const names = [...host.agents.values()].filter((x: any) => x.app === msg.card).map((x: any) => x.name);
56
+ const problem = await team.reset(names);
57
+ if (problem) error(`reset of ${msg.card}: ${problem}`);
58
+ } else if (msg.type === "app_message" && team) {
59
+ const now = Date.now();
60
+ sent.splice(0, sent.length, ...sent.filter((t) => now - t < 60000));
61
+ if (!host.agents.has(msg.to)) error(`app message to unknown agent '${msg.to}'`);
62
+ else if (sent.length >= MESSAGES_PER_MINUTE) error(`app message to ${msg.to} dropped: more than ${MESSAGES_PER_MINUTE} a minute`);
63
+ else { sent.push(now); team.send("app", msg.to, msg.text ?? ""); }
64
+ }
65
+ });
66
+ }
@@ -1,6 +1,6 @@
1
1
  ---
2
- name: agent-apps
3
- description: Build an app whose members are real AI agents (a simulated board meeting, a debate, a negotiation, a panel of experts, game characters), run and shown by the app itself, in turns or all at once. Use when the user wants agents inside what you build, as opposed to agents helping you build it.
2
+ name: app-agents
3
+ description: Build an app whose members are real AI agents (a simulated board meeting, a debate, a negotiation, a panel of experts, game characters), run and shown by the app itself, in turns or all at once. Use when the user wants agents inside what you build, as opposed to agents helping you build it. (One agent that just answers the app's questions: the app README's recipe is enough.)
4
4
  ---
5
5
  # Apps with real agents inside
6
6
 
@@ -8,8 +8,8 @@ description: Build an app whose members are real AI agents (a simulated board me
8
8
  | | Worker agents | App agents |
9
9
  |-------------------|---------------------------------|----------------------------------------------|
10
10
  | What for | help you build or fix the app | are part of the app (a board member, a debater) |
11
- | Spawn | `spawn_agent(role)` | `spawn_agent(role, name, app="<surface>")` |
12
- | Who talks to them | you (`tell`, `ask_agent`) | the app, through `/ui/agents.js` |
11
+ | Spawn | `team_spawn(role)` | `team_spawn(role, name, app="<card>")` |
12
+ | Who talks to them | you (`team_tell`, `team_ask`) | the app, through `/app/agents.js` |
13
13
  | Messages panel | shown | hidden: the app shows their talk |
14
14
  | When they run | while you build | each time the user runs the app |
15
15
 
@@ -19,7 +19,7 @@ is what keeps app agents out of the user's messages panel and lets the app reset
19
19
  ## Why the app runs the activity, not you
20
20
  Your context is large (system prompt plus everything you did while building): each of your model calls
21
21
  costs all of it, often 50k tokens or more. If you relay a meeting yourself (ask the chair who's next, ask
22
- that member, `ui_update` the transcript), every line spoken costs two or three of your calls. The app
22
+ that member, `card_update` the transcript), every line spoken costs two or three of your calls. The app
23
23
  can talk to its agents directly, and their contexts are small. So you build, fix, and answer the user;
24
24
  at run time you make no calls at all.
25
25
 
@@ -28,17 +28,17 @@ at run time you make no calls at all.
28
28
  stance and personality. End each role with: "You are a character in <app>. Answer in plain text,
29
29
  briefly, starting with [to: <name>] or [to: all]. Don't use tools." App agents have the same tools
30
30
  you have; this keeps them in character, fast, and gives the app a "to" to draw.
31
- 2. **Build the UI as an App**: `ui/apps/<name>/index.html` in your code folder (the one your prompt names, with
32
- `extensions/` in it), and its scripts, shown with
33
- `ui_render("<surface>", [{"id": "root", "component": "App", "src": "<name>/index.html"}])`.
34
- An App can import `/ui/agents.js`; the bridge finds the app's surface from the frame it runs in.
35
- 3. **Spawn the members once**, while building: `spawn_agent(role, name=..., app="<surface>")`, the same
36
- string as the surface id. In the app, check `agents.list()`: if a member is missing, show that
31
+ 2. **Build it as an app**: `{built}/apps/<name>/index.html` and its scripts, shown on a card with
32
+ `card_show(card="<card>", app="<name>")`.
33
+ An app can import `/app/agents.js`; the bridge finds the app's card from the frame it runs in.
34
+ 3. **Spawn the members once**, while building: `team_spawn(role, name=..., app="<card>")`, the same
35
+ string as the card id. In the app, check `agents.list()`: if a member is missing, show that
37
36
  instead of failing.
38
- 4. **Wire the app with `/ui/agents.js`** (read its header):
37
+ 4. **Wire the app with `/app/agents.js`** (read its header: {agent}/extensions/app/agents.js):
39
38
  - `send(to, text)`: ask a member. Its answer arrives as a `"message"` event: `agent` is that member,
40
39
  `role` is `"assistant"`, and there are no `tool_calls` (its final answer).
41
- - `on("message", fn)`: draw each answer as it arrives (speaker, the `[to: …]` target, the text).
40
+ - `on("message", fn)`: draw each answer as it arrives: `ev.agent` (the speaker), `ev.to` (its target,
41
+ "all" when it gave none) and `ev.text` (the answer without its `[to: …]` tag; show this, not `ev.content`).
42
42
  - `reset()`: a fresh round, members' memories empty and roles kept. Call it before each run if the
43
43
  user wants rounds to start fresh.
44
44
  - `action(name, context)`: only for what needs you, e.g. "add a member" (you spawn it).
@@ -48,11 +48,11 @@ at run time you make no calls at all.
48
48
  - All at once: send to several members together; answers come back in any order, so draw each one
49
49
  as it arrives.
50
50
  Members only know what you send them: include the transcript so far (or its last lines) each time.
51
- 6. **Check it** with the ui-check skill: one short run, budget 2 or 3 messages (each is a real model call).
51
+ 6. **Check it** with the browser-check skill: one short run, budget 2 or 3 messages (each is a real model call).
52
52
 
53
53
  ## The ask helper (copy into the app)
54
54
  ```js
55
- import agents from "/ui/agents.js";
55
+ import agents from "/app/agents.js";
56
56
  const waiting = {}; // member -> resolve() for its next final answer
57
57
  agents.on("message", (ev) => {
58
58
  if (ev.role === "assistant" && !ev.tool_calls?.length && waiting[ev.agent]) {
@@ -76,7 +76,7 @@ answer from an earlier run must not resolve a new question.
76
76
  with play, pause and a scrubber.
77
77
  - After a reload, past events replay: rebuild earlier runs from them (only your members' messages).
78
78
  A member's `"system"` message means a fresh memory, i.e. a new round began.
79
- - Don't keep the transcript in the surface's data with `ui_update`: that is you relaying again.
79
+ - Don't keep the transcript in the card's data with `card_update`: that is you relaying again.
80
80
 
81
81
  ## Limits
82
82
  - App messages: at most 30 a minute (each is a model call); more are dropped with an `"error"` event.
@@ -86,6 +86,5 @@ answer from an earlier run must not resolve a new question.
86
86
 
87
87
  ## Pitfalls
88
88
  - Disable Start while a run is going: a double click starts two runs.
89
- - An answer without a `[to: …]` tag: treat it as to all.
90
89
  - If the app shows nothing, the panel won't help (app agents are hidden there): check the app with
91
- ui-check, e.g. `eval:` the length of its transcript, and look for `"error"` events.
90
+ browser-check, e.g. `eval:` the length of its transcript, and look for `"error"` events.
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: test-your-app
3
+ description: Test an app you built by using it on the user's page as they would - its main tasks start to finish, a look at it, a reload. Use after card_show, before you tell the user what the app does or that it works.
4
+ ---
5
+ # Testing an app you built
6
+
7
+ Load the interact-with-page skill first: this one only adds what testing needs.
8
+
9
+ 1. Save the app's data: `cat {data}/app/<name>.json > /tmp/<name>.saved.json` (no file: it has saved nothing yet).
10
+ The test writes the user's real data.
11
+ 2. List the user's main tasks, from the app's about and your plan: add, change, remove, filter, the one idea that
12
+ makes it worth having.
13
+ 3. Do each task start to finish through the interface, with the helpers, one or a few steps per page_run, each
14
+ ending with a read. Never call the app's own functions or set its state yourself: that tests your code, not what
15
+ the user gets. After each step: did what you expected appear? Any errors in the answer?
16
+ 4. Look once: screen_see, and check what reading can't: text cut off or overlapping, things outside their box,
17
+ colours off the palette, a chart that doesn't match its numbers, empty space. If the user doesn't share their
18
+ screen, take a browser-check screenshot of {page}/built/apps/<name>/index.html (--shot) and read it.
19
+ 5. Reload: `location.reload()` in one page_run, then a read in the next: what was kept?
20
+ 6. Fix what you found (edit the part), show it again, and redo the tasks that touched it.
21
+ 7. Put the data back: `curl -X PUT --data @/tmp/<name>.saved.json {page}/app/data/<name>` (nothing saved: `--data '{}'`).
22
+ 8. Tell the user what you did and what you saw ("added and ticked a habit, reloaded: kept; long names were cut off,
23
+ now they wrap"), not just "it works".
@@ -1,16 +1,16 @@
1
- // Browser half of boards: the main space as numbered boards, and the shelf to move between them (above the input).
1
+ // The page side of the board extension: the main space as numbered boards, and the shelf to move between them (above the input).
2
2
  // Each board is a container in #top. The one you're on is display: contents (its things sit in the main space as
3
3
  // before); the others are laid out but hidden, so apps in them keep running and the shelf can draw them.
4
- // Any element with data-board="n" that lands in #top (a surface, a frame) is moved into board n's container, and
4
+ // Any element with data-board="n" that lands in #top (a card, a frame) is moved into board n's container, and
5
5
  // moved again when its data-board changes (a MutationObserver): no extension needs this one. One on the body (a
6
6
  // note's pin, fixed to the window) shows only while its board is shown.
7
7
  // Other scripts read the board shown from body[data-board] (new things go there), and get a DOM event "board"
8
8
  // {from, to} just before it changes (doodle: the drawing so far becomes a step; frames: count this board's).
9
9
  // The shelf: a dot per board (yours is bigger). Hover it and the dots open into thumbnails, with + for a new board;
10
10
  // click one to go there. It stays clear of the chat panel: more thumbnails than fit scroll between ‹ and › (grey at
11
- // the ends; a new board scrolls to the end). Drag a surface by its label onto a thumbnail to move it to that board, where it was ({type: "ui_board"}).
11
+ // the ends; a new board scrolls to the end). Drag a card by its label onto a thumbnail to move it to that board, where it was ({type: "card_board"}).
12
12
  // page messages board_view {board}: the board this page shows; board_add
13
- // GET /boards {boards, current} event "boards" {boards, added?, by?}
13
+ // GET /board {boards, current} event "board" {boards, added?, by?}
14
14
  const CSS = `
15
15
  #top > .board:not(.on) { position: absolute; top: 0; left: 0; right: 0; height: 0; overflow: hidden; padding: inherit;
16
16
  box-sizing: border-box; visibility: hidden; pointer-events: none; }
@@ -25,9 +25,6 @@ const CSS = `
25
25
  .boards-shelf .slot .x { display: none; position: absolute; top: 3px; right: 3px; width: 16px; height: 16px; border-radius: 50%;
26
26
  font-size: 9px; line-height: 16px; text-align: center; color: var(--danger); background: var(--glass-strong); box-shadow: var(--shadow); }
27
27
  .boards-shelf.open .slot:hover .x { display: block; }
28
- .boards-toast { position: fixed; left: 50%; top: 76px; transform: translateX(-50%); /* under the palette: clear of the shelf */ z-index: 27; display: flex; gap: 12px;
29
- align-items: center; padding: 7px 14px; border-radius: 10px; background: rgba(29, 29, 31, 0.88); color: #fff; font-size: 13px; }
30
- .boards-toast button { color: #9cc3f5; font-weight: 600; }
31
28
  .boards-shelf .dot { width: 7px; height: 7px; border-radius: 50%; background: rgba(0, 0, 0, 0.22); transition: all 0.15s; }
32
29
  .boards-shelf .slot.on .dot { width: 11px; height: 11px; background: var(--text); }
33
30
  /* more boards than fit (fit(): clear of the chat panel): the thumbnails scroll inside .view, between ‹ and › */
@@ -41,7 +38,7 @@ const CSS = `
41
38
  .boards-shelf .mini { display: none; position: relative; width: 120px; overflow: hidden; border-radius: 6px;
42
39
  background: rgba(255, 255, 255, 0.6); border: 1px solid var(--line); }
43
40
  .boards-shelf .mini i, .boards-peek .mini i { position: absolute; overflow: hidden; border-radius: 3px; background: rgba(0, 0, 0, 0.08); }
44
- /* a surface: a card in its own main colour (light grey when it has none) */
41
+ /* a card: a tile in its own main colour (light grey when it has none) */
45
42
  .boards-shelf .mini i.s, .boards-peek .mini i.s { --accent-of: rgb(200, 203, 210);
46
43
  background: color-mix(in srgb, var(--accent-of) 30%, #fff);
47
44
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-of) 70%, transparent), 0 1px 3px rgba(0, 0, 0, 0.08); }
@@ -93,6 +90,7 @@ export default async function (page) {
93
90
 
94
91
  // the shelf: ‹ [the thumbnails, scrolling] › + (+ outside the scrolling part, so it is always there)
95
92
  const shelf = Object.assign(document.createElement("div"), { className: "boards-shelf" });
93
+ shelf.dataset.name = "boards";
96
94
  const button = (className, textContent, title, onclick) => Object.assign(document.createElement("button"), { className, textContent, title, onclick });
97
95
  const view = Object.assign(document.createElement("div"), { className: "view" });
98
96
  const track = Object.assign(document.createElement("div"), { className: "track" });
@@ -100,6 +98,7 @@ export default async function (page) {
100
98
  const prev = button("arrow", "‹", "Earlier boards", () => step(-1));
101
99
  const next = button("arrow", "›", "Later boards", () => step(1));
102
100
  const add = button("add", "+", "New board", () => { adding = true; page.send({ type: "board_add" }); });
101
+ add.dataset.name = "new-board";
103
102
  view.append(track);
104
103
  shelf.append(prev, view, next, add);
105
104
  document.body.append(shelf);
@@ -171,8 +170,7 @@ export default async function (page) {
171
170
  // ✕ on a thumbnail: the board leaves the shelf with a few seconds of Undo, and only then goes from the server
172
171
  // (board_remove: its things go with it). Leaving the page in those seconds removes it at once.
173
172
  let pending = null; // {n, was: the board shown when ✕ was clicked, timer}
174
- const toast = Object.assign(document.createElement("div"), { className: "boards-toast", hidden: true });
175
- document.body.append(toast);
173
+ let toast = null; // the notice with Undo (pageApi.notice)
176
174
  function removeBoard(n) {
177
175
  if (pending) commit();
178
176
  const i = boards.indexOf(n);
@@ -180,20 +178,20 @@ export default async function (page) {
180
178
  peek.hidden = true;
181
179
  if (current === n) show(boards.filter((b) => b !== n)[Math.max(0, i - 1)]); // the one before it (else after)
182
180
  else render();
183
- toast.replaceChildren(`Board ${n} removed`, button("", "Undo", "Bring it back", undo));
184
- toast.hidden = false;
181
+ toast?.close();
182
+ toast = page.notice([`Board ${n} removed`, button("go", "Undo", "Bring it back", undo)], () => pending && commit()); // ✕: removed now
185
183
  }
186
184
  function commit() {
187
185
  clearTimeout(pending.timer);
188
186
  page.send({ type: "board_remove", board: pending.n });
189
187
  pending = null;
190
- toast.hidden = true;
188
+ toast?.close();
191
189
  }
192
190
  function undo() {
193
191
  clearTimeout(pending.timer);
194
192
  const { n, was } = pending;
195
193
  pending = null;
196
- toast.hidden = true;
194
+ toast?.close();
197
195
  if (was === n) show(n);
198
196
  else render();
199
197
  }
@@ -202,8 +200,8 @@ export default async function (page) {
202
200
  function thumbnails() {
203
201
  for (const slot of shelf.querySelectorAll(".slot")) drawBoard(slot.querySelector(".mini"), Number(slot.dataset.n), 120, 68);
204
202
  }
205
- // board n's things where they are in the main space, scaled down into mini (w x h): a surface is a small card in
206
- // its main colour (accentOf), no words. Zoomed to fit what's on the board (not the whole window), so the cards fill it.
203
+ // board n's things where they are in the main space, scaled down into mini (w x h): a card is a small tile in
204
+ // its main colour (accentOf), no words. Zoomed to fit what's on the board (not the whole window), so the tiles fill it.
207
205
  function drawBoard(mini, n, W, H) {
208
206
  const PAD = 12, els = [...container(n).children];
209
207
  const rects = els.map((el) => el.getBoundingClientRect());
@@ -212,10 +210,10 @@ export default async function (page) {
212
210
  mini.style.height = `${H}px`;
213
211
  mini.replaceChildren(...els.map((el, n) => {
214
212
  const r = rects[n], i = document.createElement("i");
215
- if (el.dataset.surface) {
216
- const accent = accentOf(el.querySelector(".surface-body") ?? el);
213
+ if (el.dataset.card) {
214
+ const accent = accentOf(el.querySelector(".card-body") ?? el);
217
215
  i.className = "s";
218
- i.title = el.dataset.surface;
216
+ i.title = el.dataset.card;
219
217
  if (accent) i.style.setProperty("--accent-of", accent);
220
218
  if (W > 200) { i.classList.add("detail"); sketch(el, i, k); } // the big preview: what's inside it
221
219
  }
@@ -238,20 +236,20 @@ export default async function (page) {
238
236
  peek.style.left = `${Math.min(innerWidth - half - 8, Math.max(half + 8, s.left + s.width / 2))}px`;
239
237
  peek.style.bottom = `${innerHeight - shelf.getBoundingClientRect().top + 8}px`;
240
238
  }
241
- // a surface's insides, scaled by k into its card (the big preview): each line of text a bar in its colour, coloured
239
+ // a card's insides, scaled by k into its tile (the big preview): each line of text a bar in its colour, coloured
242
240
  // areas (buttons, filled boxes) in theirs, pictures and canvases (a game) as small images. Apps' pages too (same
243
- // server). No words. At most 400 pieces a surface.
244
- function sketch(surface, card, k) {
245
- const base = surface.getBoundingClientRect();
241
+ // server). No words. At most 400 pieces a card.
242
+ function sketch(card, tile, k) {
243
+ const base = card.getBoundingClientRect();
246
244
  const put = (tag, r, dx, dy, style) => {
247
245
  if (r.width * k < 0.5 || r.height * k < 0.5) return null;
248
- const e = card.appendChild(document.createElement(tag));
246
+ const e = tile.appendChild(document.createElement(tag));
249
247
  Object.assign(e.style, { position: "absolute", left: `${(r.left + dx - base.left) * k}px`, top: `${(r.top + dy - base.top) * k}px`,
250
248
  width: `${r.width * k}px`, height: `${r.height * k}px` }, style);
251
249
  return e;
252
250
  };
253
- const roots = [[surface.querySelector(".surface-body") ?? surface, 0, 0]];
254
- for (const f of surface.querySelectorAll("iframe")) {
251
+ const roots = [[card.querySelector(".card-body") ?? card, 0, 0]];
252
+ for (const f of card.querySelectorAll("iframe")) {
255
253
  try { const r = f.getBoundingClientRect(); if (f.contentDocument?.body) roots.push([f.contentDocument.body, r.left + f.clientLeft, r.top + f.clientTop]); } catch {}
256
254
  }
257
255
  let n = 0;
@@ -313,12 +311,12 @@ export default async function (page) {
313
311
  else { peek.hidden = true; fit(); }
314
312
  }
315
313
 
316
- // drag a surface (by its label: ui/browser.js moves it) onto a thumbnail: it goes to that board, at the place it had
314
+ // drag a card (by its label: card/browser.js moves it) onto a thumbnail: it goes to that board, at the place it had
317
315
  document.addEventListener("pointerdown", (e) => {
318
- const box = e.target.closest?.("[data-surface] > .surface-label")?.parentElement;
316
+ const box = e.target.closest?.("[data-card] > .card-label")?.parentElement;
319
317
  if (!box || e.target.closest("button")) return;
320
318
  const px = (v) => (v ? parseFloat(v) : undefined);
321
- drag = { id: box.dataset.surface, board: Number(box.dataset.board) || 1,
319
+ drag = { id: box.dataset.card, board: Number(box.dataset.board) || 1,
322
320
  place: box.hasAttribute("data-placed") ? { x: box.offsetLeft, y: box.offsetTop, width: px(box.style.width), height: px(box.style.height) } : null };
323
321
  });
324
322
  document.addEventListener("pointermove", (e) => {
@@ -335,13 +333,13 @@ export default async function (page) {
335
333
  document.addEventListener("pointerup", () => {
336
334
  if (!drag) return;
337
335
  const to = over && Number(over.dataset.n);
338
- if (to && to !== drag.board) page.send({ type: "ui_board", surface: drag.id, board: to, place: drag.place });
336
+ if (to && to !== drag.board) page.send({ type: "card_board", card: drag.id, board: to, place: drag.place });
339
337
  over?.classList.remove("drop");
340
338
  drag = over = null;
341
339
  if (!shelf.matches(":hover")) open(false);
342
340
  });
343
341
 
344
- page.on("boards", (ev) => {
342
+ page.on("board", (ev) => {
345
343
  boards = ev.boards;
346
344
  if (ev.removed) { // its things went with it (their own events); its container goes now
347
345
  containers[ev.removed]?.remove();
@@ -352,6 +350,6 @@ export default async function (page) {
352
350
  render();
353
351
  });
354
352
 
355
- try { ({ boards, current } = await (await fetch("/boards")).json()); } catch {}
353
+ try { ({ boards, current } = await (await fetch("/board")).json()); } catch {}
356
354
  show(current, false); // nothing to tell: the server said it (and the page's WebSocket may not be open yet)
357
355
  }
@@ -1,4 +1,4 @@
1
1
  # Boards
2
2
  The user's work is on numbered boards; the page shows one at a time. A plain message doesn't say which board the
3
3
  user means: when it names one, use that; when it is about something already built, use that thing's board; else
4
- call boards() (it says which board the user is on) and, if it is still unclear, ask the user.
4
+ leave board out (card_show puts a new card on the board the user is on). board_list() lists them all.