@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
@@ -1,12 +1,12 @@
1
- // Browser half of the frames extension (Python's frames.js, unchanged but for this header): a frame tool in the palette (a dashed box). While it is on, drag on the
1
+ // The page side of the frame extension: a frame tool in the palette (a dashed box). While it is on, drag on the
2
2
  // main space to lay down a frame: a dashed box with a field for what goes there (Enter finishes it). While the
3
3
  // tool is on, an Implement button (a hammer with the number of frames in a bubble, in the row under the tool)
4
- // sends every unsent frame that has words, together (frames_send, to the agent picked in the chat); while it is
5
- // off, the bubble sits on the tool itself, so waiting frames still show. Until then, a frame's corner resizes it, like a surface's (frame_place).
6
- // Sent frames stay as they are, marked waiting, until an agent renders a surface with their id (index.ts
4
+ // sends every unsent frame that has words, together (frame_send, to the agent picked in the chat); while it is
5
+ // off, the bubble sits on the tool itself, so waiting frames still show. Until then, a frame's corner resizes it, like a card's (frame_place).
6
+ // Sent frames stay as they are, marked waiting, until an agent renders a card with their id (index.ts
7
7
  // then drops them); ✕ on a frame, or the eraser passing over an unsent one ("erase" event), removes it.
8
8
  // Esc or the tool turns drawing off.
9
- // Frames live in the main space (slot "top"), in main-space px, so they scroll with the surfaces.
9
+ // Frames live in the main space (slot "top"), in main-space px, so they scroll with the cards.
10
10
  export default async function (page) {
11
11
  const area = page.slot("top");
12
12
  const style = document.createElement("style");
@@ -31,15 +31,17 @@ export default async function (page) {
31
31
  border: solid var(--muted); border-width: 0 2px 2px 0; border-radius: 0 0 4px 0; cursor: nwse-resize; opacity: 0; }
32
32
  #top .frame:hover .frame-resize { opacity: 1; }
33
33
  #top .frame.sent .frame-resize { display: none; }
34
- #status button.frames-send { color: var(--accent); }`;
34
+ #status button.frame-send { color: var(--accent); }`;
35
35
  document.head.append(style);
36
36
 
37
37
  const button = document.createElement("button");
38
38
  button.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="5" width="17" height="14" rx="2" stroke-dasharray="3 2.5"/></svg>`;
39
+ button.dataset.name = "frames"; // names an agent's pen can point at (doodle_mark on: {element: "frames"})
39
40
  button.title = "Frames: drag boxes on the page and write what goes in each, then Implement them together (Esc to stop)";
40
41
  button.append(Object.assign(document.createElement("span"), { className: "badge" })); // frames waiting, while the tool is off
41
42
  const send = document.createElement("button");
42
- send.className = "frames-send";
43
+ send.className = "frame-send";
44
+ send.dataset.name = "implement";
43
45
  send.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m15 12-8.4 8.4a2.1 2.1 0 0 1-3-3L12 9"/><path d="m18 15 4-4"/>` +
44
46
  `<path d="m21.5 11.5-1.9-1.9a2 2 0 0 1-.6-1.4V7l-2.3-2.3a6 6 0 0 0-4.2-1.7H9l.9.8A6.2 6.2 0 0 1 12 8.4V10l2 2h1.2a2 2 0 0 1 1.4.6l1.9 1.9"/></svg>` +
45
47
  `<span class="badge"></span>`; // the hammer: build these
@@ -57,25 +59,25 @@ export default async function (page) {
57
59
  button.classList.toggle("on", v);
58
60
  area.classList.toggle("framing", v);
59
61
  showReady();
60
- if (v) document.dispatchEvent(new CustomEvent("tool-on", { detail: "frames" })); // the other tools turn off
62
+ if (v) document.dispatchEvent(new CustomEvent("tool-on", { detail: "frame" })); // the other tools turn off
61
63
  };
62
64
  button.onclick = () => setOn(!on);
63
- document.addEventListener("tool-on", (e) => { if (on && e.detail !== "frames") setOn(false); }); // one tool at a time
65
+ document.addEventListener("tool-on", (e) => { if (on && e.detail !== "frame") setOn(false); }); // one tool at a time
64
66
  document.addEventListener("keydown", (e) => { if (e.key === "Escape") setOn(false); });
65
67
  send.onclick = () => {
66
68
  for (const flush of Object.values(timers)) { clearTimeout(flush.t); flush(); } // text still waiting on the typing pause goes first
67
- page.send({ type: "frames_send", to: document.getElementById("to")?.value, board: board() });
69
+ page.send({ type: "frame_send", to: document.getElementById("to")?.value, board: board() });
68
70
  };
69
- const board = () => Number(document.body.dataset.board) || 1; // the board shown (boards/): frames are drawn and sent there
71
+ const board = () => Number(document.body.dataset.board) || 1; // the board shown (board/): frames are drawn and sent there
70
72
  document.addEventListener("board", () => setTimeout(() => render(shown))); // after the switch: count that board's
71
73
 
72
- const inMain = (x, y) => { // window px -> main-space px (as point.js)
74
+ const inMain = (x, y) => { // window px -> main-space px (as note/browser.js)
73
75
  const m = area.getBoundingClientRect();
74
76
  return { x: Math.round(x - m.left + area.scrollLeft), y: Math.round(y - m.top + area.scrollTop) };
75
77
  };
76
78
  const put = (box, f) => Object.assign(box.style, { left: `${f.x}px`, top: `${f.y}px`, width: `${f.width}px`, height: `${f.height}px` });
77
79
 
78
- // Drawing: on the main space, over empty space or surfaces, but not on a frame (that is for typing).
80
+ // Drawing: on the main space, over empty space or cards, but not on a frame (that is for typing).
79
81
  let start = null, ghost = null, focusNew = false;
80
82
  area.addEventListener("mousedown", (e) => {
81
83
  if (!on || e.button !== 0 || e.target.closest(".frame")) return;
@@ -85,7 +87,7 @@ export default async function (page) {
85
87
  ghost = Object.assign(document.createElement("div"), { className: "frame drawing" });
86
88
  area.append(ghost);
87
89
  }, true);
88
- area.addEventListener("click", (e) => { if (on && !e.target.closest(".frame")) e.stopPropagation(); }, true); // surfaces don't react
90
+ area.addEventListener("click", (e) => { if (on && !e.target.closest(".frame")) e.stopPropagation(); }, true); // cards don't react
89
91
  window.addEventListener("mousemove", (e) => {
90
92
  if (!start) return;
91
93
  const p = inMain(e.clientX, e.clientY);
@@ -136,7 +138,7 @@ export default async function (page) {
136
138
  newest = box;
137
139
  }
138
140
  put(box, f);
139
- box.dataset.board = f.board ?? 1; // boards/ shows it on its board
141
+ box.dataset.board = f.board ?? 1; // board/ shows it on its board
140
142
  box.classList.toggle("sent", f.sent);
141
143
  const text = box.querySelector("textarea");
142
144
  text.readOnly = f.sent;
@@ -147,7 +149,7 @@ export default async function (page) {
147
149
  showReady();
148
150
  send.title = `Implement: send ${ready} frame${ready > 1 ? "s" : ""} to the agent to build, together`;
149
151
  }
150
- function resize(box, id, e) { // drag the corner (as ui.js does for surfaces); on release the new size is saved
152
+ function resize(box, id, e) { // drag the corner (as card/browser.js does for cards); on release the new size is saved
151
153
  e.preventDefault();
152
154
  const corner = e.currentTarget, x = box.offsetLeft, y = box.offsetTop, w0 = box.offsetWidth, h0 = box.offsetHeight;
153
155
  corner.setPointerCapture(e.pointerId);
@@ -161,6 +163,6 @@ export default async function (page) {
161
163
  if (place) page.send({ type: "frame_place", id, ...place });
162
164
  };
163
165
  }
164
- page.on("frames", (ev) => render(ev.frames));
165
- try { render(await (await fetch("/frames")).json()); } catch {}
166
+ page.on("frame", (ev) => render(ev.frames));
167
+ try { render(await (await fetch("/frame")).json()); } catch {}
166
168
  }
@@ -0,0 +1,3 @@
1
+ # Wireframes
2
+ A message "[frames on the page]" is boxes the user laid out, each saying what goes there: read README.md before you
3
+ handle one.
@@ -0,0 +1,90 @@
1
+ // Frames: wireframes the user lays out for the agents to fill. browser.js adds a tool to the palette: drag boxes on the main space, type in each what goes there,
2
+ // then press Implement. All the frames go together, as one message, to the agent picked in the chat (else agent1),
3
+ // so it sees the whole layout and decides how to split the work (alone, or with helpers: README.md, not code).
4
+ // A frame is {x, y, width, height, text, sent, to?, board} (main-space px, where card_show's place puts a card;
5
+ // board: the one it was drawn on, board/; Implement sends the frames of the board shown), with an
6
+ // id (frame-1, frame-2, ...: never reused). An agent fills it with card_show(place=<its place>) (any card id);
7
+ // when a card appears at its place (or with its id) (a card watcher, host.api.card.watch), the frame is done and its dashed box goes. The user
8
+ // can remove a frame (its ✕); if it was sent, its agent is told. Until a frame is sent, its corner resizes it.
9
+ // GET /frame {id: frame}
10
+ // page messages frame_add {x, y, width, height, board}, frame_place {id, x, y, width, height}, frame_text {id, text},
11
+ // frame_remove {id}, frame_send {to, board, by?} (by: the agent whose code pressed Implement)
12
+ // event "frame" {frames: {id: frame}} to every open page, when they changed
13
+ // Saved in data/frame.json as {last: <last id number>, frames: {id: frame}}; shared by every agent (host.shared).
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
+
18
+ const host = (globalThis as any).chalky;
19
+ const FILE = path.join(host.data, "frame.json");
20
+ if (!host.shared.has("frame"))
21
+ host.shared.set("frame", fs.existsSync(FILE) ? JSON.parse(fs.readFileSync(FILE, "utf8")) : { last: 0, frames: {} });
22
+ const saved = host.shared.get("frame"); // {last, frames}
23
+ const frames: Record<string, any> = saved.frames;
24
+
25
+ function changed() {
26
+ fs.writeFileSync(FILE + ".tmp", JSON.stringify(saved));
27
+ fs.renameSync(FILE + ".tmp", FILE);
28
+ host.broadcast("frame", { frames });
29
+ }
30
+ const box = (msg: any, f: any = {}) => Object.fromEntries(["x", "y", "width", "height"].map((k) => [k, Math.round(Number(msg[k] ?? f[k] ?? 0))]));
31
+ const place = (f: any) => JSON.stringify({ x: f.x, y: f.y, width: f.width, height: f.height });
32
+
33
+ export default function (pi: ExtensionAPI) {
34
+
35
+ // a frame is done when a card appears at its place on its board, or with its id (after all extensions loaded: the
36
+ // card one may come later). The place is what the user chose; agents often give the card a name of its own.
37
+ pi.on("session_start", () => {
38
+ host.api.card?.watch("frame", (sid: string) => {
39
+ const card = host.shared.get("card")?.cards[sid];
40
+ if (!card) return;
41
+ const at = (f: any) => card.place && (card.board ?? 1) === (f.board ?? 1)
42
+ && ["x", "y", "width", "height"].every((k) => Math.round(card.place[k]) === f[k]);
43
+ const done = Object.keys(frames).filter((id) => id === sid || at(frames[id]));
44
+ for (const id of done) delete frames[id];
45
+ if (done.length) changed();
46
+ });
47
+ host.api.board?.onRemove("frame", (n: number) => { // a removed board (board/) takes its frames with it
48
+ const ids = Object.keys(frames).filter((id) => (frames[id].board ?? 1) === n);
49
+ const gone = ids.map((id) => ({ owner: frames[id].sent ? frames[id].to : undefined, what: `frame ${id}` }));
50
+ for (const id of ids) delete frames[id];
51
+ if (ids.length) changed();
52
+ return gone;
53
+ });
54
+ });
55
+
56
+ host.routes.set("/frame", (_req: any, res: any) => res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(frames)));
57
+
58
+ host.pageHandlers.set("frame", (msg: any) => {
59
+ const f = frames[msg.id], team = host.api.team;
60
+ if (msg.type === "frame_add") {
61
+ frames[`frame-${++saved.last}`] = { ...box(msg), text: "", sent: false, board: Number(msg.board) || 1 };
62
+ changed();
63
+ } else if (msg.type === "frame_place" && f && !f.sent) {
64
+ Object.assign(f, box(msg, f));
65
+ changed();
66
+ } else if (msg.type === "frame_text" && f && !f.sent) {
67
+ f.text = String(msg.text ?? "");
68
+ changed();
69
+ } else if (msg.type === "frame_remove" && f) {
70
+ delete frames[msg.id];
71
+ changed();
72
+ if (f.sent) team?.send("user", f.to, `[frames on the page] the user removed ${msg.id} ("${f.text}"): don't fill it.`, true);
73
+ } else if (msg.type === "frame_send" && team) {
74
+ const to = msg.to in team.status() ? msg.to : "agent1";
75
+ const board = Number(msg.board) || 1; // the board shown: its frames only (the user sees no others)
76
+ const ready = Object.entries(frames).filter(([, f]: any) => !f.sent && f.text.trim() && (f.board ?? 1) === board);
77
+ if (!ready.length) return;
78
+ for (const [, f] of ready as any) Object.assign(f, { sent: true, to });
79
+ changed();
80
+ // by: an agent's code pressed Implement (page/browser.js), not the user
81
+ const who = !msg.by ? "the user laid out" : msg.by === to ? "you pressed Implement for" : `${msg.by} pressed Implement for`;
82
+ const lines = [`[frames on the page] ${who} ${ready.length} frame${ready.length > 1 ? "s" : ""} on board ${board} to fill:`,
83
+ ...ready.map(([id, f]: any) => ` ${id}: "${f.text}" · place=${place(f)}`),
84
+ ` Fill each with card_show(place=<its place>, board=${board}, ...) (any card id): its dashed box goes when a card appears there.`];
85
+ const waiting = Object.keys(frames).filter((id) => frames[id].sent && !ready.some(([r]) => r === id));
86
+ if (waiting.length) lines.push(` Sent before and not filled yet: ${waiting.join(", ")}.`);
87
+ team.send("user", to, lines.join("\n"));
88
+ }
89
+ });
90
+ }
@@ -1,25 +1,25 @@
1
- // Browser half of the ui_history plugin: undo, redo and rewind the surfaces.
1
+ // The page side of the history extension: undo, redo and rewind the cards.
2
2
  // Ctrl+Z / ⌘Z: undo. Ctrl+Shift+Z / ⌘⇧Z / Ctrl+Y: redo. (Not while typing in a field: that undoes the typing.)
3
3
  // Hold Alt (Option) and drag sideways anywhere: scrub along the history; let go to stay there.
4
4
  // Doing something new after going back starts a branch; a note offers to bring the other one back.
5
- // It only sends messages (ui_undo, ui_redo, ui_goto); the server puts the surfaces back and ui.js redraws them.
5
+ // It only sends messages (history_undo, history_redo, history_goto); the server puts the cards back and the card extension redraws them.
6
6
  const CSS = `
7
- #ui-history-bar { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 9999; display: flex;
7
+ #history-bar { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 9999; display: flex;
8
8
  flex-direction: column; align-items: center; gap: 6px; max-width: calc(100vw - 32px); padding: 8px 14px; border-radius: 12px;
9
9
  font-size: 12px; color: #fff; background: rgba(29, 29, 31, .92); pointer-events: none; }
10
- #ui-history-bar .ticks { display: flex; gap: 2px; flex-wrap: wrap; justify-content: center; }
11
- #ui-history-bar .ticks i { width: 6px; height: 14px; border-radius: 2px; background: #fff; opacity: .3; }
12
- #ui-history-bar .ticks i.now { opacity: 1; background: #f4d35e; }
13
- #ui-history-bar .ticks i.ahead { opacity: .12; }
14
- #ui-history-toast { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 9999; display: flex; gap: 10px;
10
+ #history-bar .ticks { display: flex; gap: 2px; flex-wrap: wrap; justify-content: center; }
11
+ #history-bar .ticks i { width: 6px; height: 14px; border-radius: 2px; background: #fff; opacity: .3; }
12
+ #history-bar .ticks i.now { opacity: 1; background: #f4d35e; }
13
+ #history-bar .ticks i.ahead { opacity: .12; }
14
+ #history-toast { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 9999; display: flex; gap: 10px;
15
15
  align-items: center; padding: 8px 14px; border-radius: 12px; font-size: 13px; color: #fff; background: rgba(29, 29, 31, .92); }
16
- #ui-history-toast button { font: inherit; color: inherit; background: none; border: 1px solid currentColor; border-radius: 8px;
16
+ #history-toast button { font: inherit; color: inherit; background: none; border: 1px solid currentColor; border-radius: 8px;
17
17
  padding: 2px 10px; cursor: pointer; }`;
18
18
 
19
19
  export default function (page) {
20
- document.head.insertAdjacentHTML("beforeend", `<style id="ui-history-css">${CSS}</style>`);
21
- const bar = Object.assign(document.createElement("div"), { id: "ui-history-bar", hidden: true });
22
- const toastEl = Object.assign(document.createElement("div"), { id: "ui-history-toast", hidden: true });
20
+ document.head.insertAdjacentHTML("beforeend", `<style id="history-css">${CSS}</style>`);
21
+ const bar = Object.assign(document.createElement("div"), { id: "history-bar", hidden: true });
22
+ const toastEl = Object.assign(document.createElement("div"), { id: "history-toast", hidden: true });
23
23
  document.body.append(bar, toastEl);
24
24
  let toastTimer;
25
25
  function toast(text, button, onclick) {
@@ -35,8 +35,8 @@ export default function (page) {
35
35
  document.addEventListener("keydown", (e) => {
36
36
  if (!(e.ctrlKey || e.metaKey) || typing(e)) return;
37
37
  const k = e.key.toLowerCase();
38
- if (k === "z" && !e.shiftKey) { e.preventDefault(); page.send({ type: "ui_undo" }); }
39
- else if ((k === "z" && e.shiftKey) || k === "y") { e.preventDefault(); page.send({ type: "ui_redo" }); }
38
+ if (k === "z" && !e.shiftKey) { e.preventDefault(); page.send({ type: "history_undo" }); }
39
+ else if ((k === "z" && e.shiftKey) || k === "y") { e.preventDefault(); page.send({ type: "history_redo" }); }
40
40
  });
41
41
 
42
42
  // {x0, x, line, start, i} while Alt-dragging. It starts at the press, before the history has come (line: null till
@@ -57,7 +57,7 @@ export default function (page) {
57
57
  const i = Math.max(0, Math.min(scrub.line.length - 1, scrub.start + Math.round((scrub.x - scrub.x0) / step)));
58
58
  if (i === scrub.i) return;
59
59
  scrub.i = i;
60
- page.send({ type: "ui_goto", step: scrub.line[i].n });
60
+ page.send({ type: "history_goto", step: scrub.line[i].n });
61
61
  showBar();
62
62
  }
63
63
  document.addEventListener("pointerdown", async (e) => {
@@ -65,7 +65,7 @@ export default function (page) {
65
65
  e.preventDefault(); e.stopPropagation();
66
66
  const me = scrub = { x0: e.clientX, x: e.clientX, line: null };
67
67
  let history;
68
- try { history = await (await fetch("/ui-api/history")).json(); } catch { if (scrub === me) scrub = null; return; }
68
+ try { history = await (await fetch("/history")).json(); } catch { if (scrub === me) scrub = null; return; }
69
69
  if (scrub !== me) return; // let go before the history came: nothing to do
70
70
  me.line = history.line;
71
71
  me.start = me.i = history.line.findIndex((s) => s.n === history.at);
@@ -89,9 +89,9 @@ export default function (page) {
89
89
  if (ahead) toast("Gone back. Do something new to branch from here, or Ctrl+Shift+Z to come forward.");
90
90
  }
91
91
 
92
- page.on("ui_history", (ev) => {
92
+ page.on("history", (ev) => {
93
93
  if (ev.event === "moved" && !scrub) toast(`Back to just after: ${ev.what}`);
94
94
  if (ev.event === "branched") toast("You branched from an earlier moment. The other version is kept.", "Bring it back",
95
- () => page.send({ type: "ui_goto", step: ev.tip }));
95
+ () => page.send({ type: "history_goto", step: ev.tip }));
96
96
  });
97
97
  }
@@ -1,23 +1,22 @@
1
- // Every state of the UI surfaces, kept: undo, redo and rewind for the user (port of the Python ui_history plugin; the
2
- // browser half, browser.js, is its ui_history.js). After each change (ui.ts's change() with words for it, from the user or
3
- // an agent) it saves a step: a full copy of all surfaces, design and data. Steps form a tree: each has a parent, so
4
- // undoing and then doing something new starts a branch and loses nothing; redo follows the newest branch.
5
- // Page messages: ui_undo, ui_redo, ui_goto {step}
6
- // Server -> page: {"type": "ui_history", "event": "moved", "at", "what"} after undo/redo/goto
7
- // {"type": "ui_history", "event": "branched", "tip"} a change made after undoing
8
- // GET /ui-api/history: {"at", "line": [{"n", "ts", "by", "what"}]}, the current line (root to newest tip)
9
- // Kept in state/ui-history.jsonl (one step per line), for HISTORY_KEEP_DAYS (default 7). Older steps are dropped at
1
+ // Every state of the cards, kept: undo, redo and rewind for the user (browser.js: the keys and the bar). After each
2
+ // change (the card extension's change() with words for it, from the user or an agent) it saves a step: a full copy
3
+ // of all cards, design and data. Steps form a tree: each has a parent, so undoing and then doing something new
4
+ // starts a branch and loses nothing; redo follows the newest branch.
5
+ // page messages history_undo, history_redo, history_goto {step}
6
+ // page event "history" {event: "moved", at, what} after undo/redo/goto; {event: "branched", tip} a change made after undoing
7
+ // GET /history {"at", "line": [{"n", "ts", "by", "what"}]}, the current line (root to newest tip)
8
+ // Kept in data/history.jsonl (one step per line), for HISTORY_KEEP_DAYS (default 7). Older steps are dropped at
10
9
  // start and every PRUNE_EVERY steps; the current step is always kept.
11
- // Undo puts back the surfaces only: code (components, apps) has its own history (the versions extension).
10
+ // Undo puts back the cards only: agent/ and built/ have their own history (the version extension).
12
11
  // pi has no undo for this: /tree and /fork rewind one agent's conversation, not the shared page.
13
- // It watches the ui extension's changes (host.api.ui.watch); others reach it as host.api.uiHistory. Not pi.events:
12
+ // It watches the card extension's changes (host.api.card.watch); others reach it as host.api.history. Not pi.events:
14
13
  // each agent has its own bus, and a reload keeps the old listeners on it, so every change would be recorded again.
15
14
  import fs from "node:fs";
16
15
  import path from "node:path";
17
16
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
17
 
19
18
  const host = (globalThis as any).chalky;
20
- const FILE = path.join(host.state, "ui-history.jsonl");
19
+ const FILE = path.join(host.data, "history.jsonl");
21
20
  const KEEP_DAYS = Number(process.env.HISTORY_KEEP_DAYS ?? 7);
22
21
  const PRUNE_EVERY = 200;
23
22
 
@@ -25,12 +24,12 @@ const PRUNE_EVERY = 200;
25
24
  // "data" last, one just drawn has it in the middle)
26
25
  const sorted = (v: any) => JSON.stringify(v, (_k, x) => x && typeof x === "object" && !Array.isArray(x) ? Object.fromEntries(Object.entries(x).sort()) : x);
27
26
 
28
- type Step = { n: number, parent: number | null, ts: number, by: string, what: string, surfaces: Record<string, any> };
27
+ type Step = { n: number, parent: number | null, ts: number, by: string, what: string, cards: Record<string, any> };
29
28
 
30
29
  class History {
31
30
  steps = new Map<number, Step>();
32
- at: number | null = null; // the step the surfaces are at now
33
- constructor(public ui: any) {
31
+ at: number | null = null; // the step the cards are at now
32
+ constructor(public cards: any) {
34
33
  if (fs.existsSync(FILE))
35
34
  for (const line of fs.readFileSync(FILE, "utf8").split("\n").filter(Boolean)) {
36
35
  const entry = JSON.parse(line);
@@ -39,19 +38,19 @@ class History {
39
38
  } // missed this: after a goto, a restart went back there)
40
39
  if (this.at === null || !this.steps.has(this.at)) this.at = this.steps.size ? Math.max(...this.steps.keys()) : null;
41
40
  this.prune();
42
- if (this.at === null || sorted(this.steps.get(this.at)!.surfaces) !== sorted(ui.surfaces))
43
- this.add("start", "the surfaces as loaded"); // first start, or the files changed while we were away
41
+ if (this.at === null || sorted(this.steps.get(this.at)!.cards) !== sorted(cards.cards))
42
+ this.add("start", "the cards as loaded"); // first start, or the files changed while we were away
44
43
  }
45
44
  write(entry: object) { fs.appendFileSync(FILE, JSON.stringify(entry) + "\n"); }
46
45
  add(by: string, what: string) { // a new step after the current one; tells the pages if it left a branch behind
47
46
  const left = this.children(this.at).length ? this.tip(this.at!) : null;
48
47
  const n = this.steps.size ? Math.max(...this.steps.keys()) + 1 : 0;
49
- const step = { n, parent: this.at, ts: Date.now() / 1000, by, what, surfaces: this.ui.snapshot() };
48
+ const step = { n, parent: this.at, ts: Date.now() / 1000, by, what, cards: this.cards.snapshot() };
50
49
  this.steps.set(n, step);
51
50
  this.at = n;
52
51
  this.write(step);
53
52
  if (n % PRUNE_EVERY === 0) this.prune();
54
- if (left !== null) host.broadcast("ui_history", { event: "branched", tip: left });
53
+ if (left !== null) host.broadcast("history", { event: "branched", tip: left });
55
54
  }
56
55
  children(n: number | null) { return [...this.steps.values()].filter((s) => s.parent === n).map((s) => s.n).sort((a, b) => a - b); }
57
56
  tip(n: number) { // follow the newest child down to the end of the line
@@ -69,8 +68,8 @@ class History {
69
68
  if (!this.steps.has(n) || n === this.at) return;
70
69
  this.at = n;
71
70
  this.write({ at: n });
72
- this.ui.restore(this.steps.get(n)!.surfaces);
73
- host.broadcast("ui_history", { event: "moved", at: n, what: this.steps.get(n)!.what });
71
+ this.cards.restore(this.steps.get(n)!.cards);
72
+ host.broadcast("history", { event: "moved", at: n, what: this.steps.get(n)!.what });
74
73
  }
75
74
  prune() { // drop steps older than KEEP_DAYS (never the current one), then rewrite the file
76
75
  const cutoff = Date.now() / 1000 - KEEP_DAYS * 86400;
@@ -84,26 +83,28 @@ class History {
84
83
  }
85
84
 
86
85
  export default function (pi: ExtensionAPI) {
87
- // made once all of an agent's extensions are loaded (pi loads them in folder order, not by name, so ui and its
88
- // surfaces may come after this file); the first agent's session_start makes it, at startup, before any change
86
+ // made once all of an agent's extensions are loaded (pi loads them in folder order, not by name, so the card
87
+ // extension may come after this file); the first agent's session_start makes it, at startup, before any change
89
88
  pi.on("session_start", () => {
90
- if (!host.shared.has("ui-history")) host.shared.set("ui-history", new History(host.shared.get("ui")));
91
- host.api.uiHistory = host.shared.get("ui-history");
92
- host.api.ui?.watch("ui_history", (_sid: string, by?: string, what?: string) => { if (what) host.api.uiHistory.add(by, what); });
89
+ if (!host.shared.has("history")) host.shared.set("history", new History(host.shared.get("card")));
90
+ host.api.history = host.shared.get("history");
91
+ host.api.card?.watch("history", (_id: string, by?: string, what?: string) => { // the card's words don't say who
92
+ if (what) host.api.history.add(by, `${by === "user" ? "you" : by} ${what}`);
93
+ });
93
94
  });
94
- const shared = (): History | undefined => host.shared.get("ui-history");
95
+ const shared = (): History | undefined => host.shared.get("history");
95
96
 
96
- host.pageHandlers.set("ui_history", (msg: any) => {
97
+ host.pageHandlers.set("history", (msg: any) => {
97
98
  const history = shared();
98
99
  if (!history) return;
99
100
  const now = history.at === null ? undefined : history.steps.get(history.at);
100
- if (msg.type === "ui_undo" && now && now.parent !== null) history.goto(now.parent);
101
- else if (msg.type === "ui_redo") { const kids = history.children(history.at); if (kids.length) history.goto(kids.at(-1)!); }
102
- else if (msg.type === "ui_goto") history.goto(msg.step);
101
+ if (msg.type === "history_undo" && now && now.parent !== null) history.goto(now.parent);
102
+ else if (msg.type === "history_redo") { const kids = history.children(history.at); if (kids.length) history.goto(kids.at(-1)!); }
103
+ else if (msg.type === "history_goto") history.goto(msg.step);
103
104
  });
104
- host.routes.set("/ui-api/history", (_req: any, res: any) => {
105
+ host.routes.set("/history", (_req: any, res: any) => {
105
106
  const history = shared();
106
107
  res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ at: history?.at ?? null,
107
- line: (history?.line() ?? []).map((n) => { const { surfaces, parent, ...s } = history!.steps.get(n)!; return s; }) }));
108
+ line: (history?.line() ?? []).map((n) => { const { cards, parent, ...s } = history!.steps.get(n)!; return s; }) }));
108
109
  });
109
110
  }
@@ -1,4 +1,4 @@
1
- // Browser half of the mcp extension: "/mcp" in the chat input opens a picker of the servers in app/mcp.json. A
1
+ // The page side of the mcp extension: "/mcp" in the chat input opens a picker of the servers in agent/mcp.json. A
2
2
  // server's box turns it on or off; open it to tick single tools (greyed while the server is off). Each change is
3
3
  // saved at once (mcp_set). A server's tools show once the agents have connected to it.
4
4
  const STYLE = `
@@ -0,0 +1,3 @@
1
+ # MCP servers
2
+ Servers are in {agent}/mcp.json (pi's docs/mcp.md); the user turns them on and off with /mcp. To add one when asked:
3
+ its entry there ("exposure": "deferred", secrets as ${ENV_VAR}, never written out), then call reload.
@@ -1,4 +1,4 @@
1
- // MCP picker: "/mcp" on the page ticks which servers in app/mcp.json, and which of their tools, the agents may use.
1
+ // MCP picker: "/mcp" on the page ticks which servers in agent/mcp.json, and which of their tools, the agents may use.
2
2
  // pi's built-in MCP does the rest (server.ts loads it; pi's docs/mcp.md). A tick writes the fields pi's own /mcp
3
3
  // screen writes, in that file: "enabled": false for a server, toolExposure "<tool>": "hidden" (or back to the
4
4
  // server's exposure) for a tool. Then every agent reloads, one change at a time (a busy agent after its turn).
@@ -10,7 +10,7 @@ import path from "node:path";
10
10
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
11
 
12
12
  const host = (globalThis as any).chalky;
13
- const FILE = path.join(host.app, "mcp.json");
13
+ const FILE = path.join(host.agent, "mcp.json");
14
14
 
15
15
  function state() {
16
16
  const servers = JSON.parse(fs.readFileSync(FILE, "utf8")).mcpServers ?? {};
@@ -46,8 +46,6 @@ async function reloadAll() {
46
46
  }
47
47
 
48
48
  export default function (pi: ExtensionAPI) {
49
- const guide = fs.readFileSync(path.join(import.meta.dirname, "guide.md"), "utf8");
50
- pi.on("before_agent_start", (event) => { event.systemPromptOptions.sections.mcp = guide; });
51
49
 
52
50
  host.pageHandlers.set("mcp", async (msg: any) => {
53
51
  if (msg.type !== "mcp_list" && msg.type !== "mcp_set") return;