@chalky/chalky 0.1.3 → 0.1.5

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 (103) hide show
  1. package/README.md +10 -6
  2. package/{app → agent}/SYSTEM.md +16 -11
  3. package/agent/extensions/app/README.md +68 -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/board/browser.js +359 -0
  9. package/agent/extensions/board/guide.md +4 -0
  10. package/agent/extensions/board/index.ts +100 -0
  11. package/agent/extensions/card/README.md +88 -0
  12. package/agent/extensions/card/blocks/Button.js +34 -0
  13. package/{app/extensions/ui/components → agent/extensions/card/blocks}/Column.js +2 -2
  14. package/agent/extensions/card/blocks/Markdown.js +37 -0
  15. package/{app/extensions/ui/components/Card.js → agent/extensions/card/blocks/Panel.js} +3 -3
  16. package/{app/extensions/ui/components → agent/extensions/card/blocks}/Row.js +2 -2
  17. package/{app/extensions/ui/components → agent/extensions/card/blocks}/TextField.js +3 -3
  18. package/{app/extensions/ui → agent/extensions/card}/browser.js +155 -128
  19. package/agent/extensions/card/guide.md +13 -0
  20. package/agent/extensions/card/index.ts +370 -0
  21. package/agent/extensions/command/browser.js +13 -0
  22. package/agent/extensions/command/guide.md +2 -0
  23. package/agent/extensions/command/index.ts +94 -0
  24. package/agent/extensions/doodle/README.md +57 -0
  25. package/agent/extensions/doodle/browser.js +1013 -0
  26. package/agent/extensions/doodle/guide.md +22 -0
  27. package/agent/extensions/doodle/index.ts +274 -0
  28. package/agent/extensions/file/browser.js +143 -0
  29. package/agent/extensions/file/guide.md +5 -0
  30. package/{app/extensions/files → agent/extensions/file}/index.ts +34 -16
  31. package/{app/extensions/frames → agent/extensions/frame}/README.md +13 -13
  32. package/{app/extensions/frames → agent/extensions/frame}/browser.js +24 -19
  33. package/agent/extensions/frame/guide.md +3 -0
  34. package/agent/extensions/frame/index.ts +81 -0
  35. package/{app/extensions/ui_history → agent/extensions/history}/browser.js +18 -18
  36. package/{app/extensions/ui_history → agent/extensions/history}/index.ts +32 -33
  37. package/{app → agent}/extensions/mcp/browser.js +1 -1
  38. package/agent/extensions/mcp/guide.md +3 -0
  39. package/{app → agent}/extensions/mcp/index.ts +2 -4
  40. package/agent/extensions/model/browser.js +363 -0
  41. package/{app → agent}/extensions/model/index.ts +8 -4
  42. package/agent/extensions/model/login.ts +215 -0
  43. package/agent/extensions/note/README.md +35 -0
  44. package/agent/extensions/note/browser.js +356 -0
  45. package/agent/extensions/note/guide.md +8 -0
  46. package/agent/extensions/note/index.ts +192 -0
  47. package/{app → agent}/extensions/page/index.ts +2 -2
  48. package/{app → agent}/extensions/page/page.html +55 -49
  49. package/agent/extensions/prompt/index.ts +38 -0
  50. package/{app → agent}/extensions/reload/index.ts +3 -3
  51. package/{app → agent}/extensions/screen/browser.js +4 -4
  52. package/{app → agent}/extensions/screen/index.ts +11 -11
  53. package/{app/extensions/steps → agent/extensions/step}/index.ts +1 -1
  54. package/{app/extensions/swarm → agent/extensions/team}/README.md +19 -19
  55. package/{app/extensions/swarm → agent/extensions/team}/guide.md +4 -4
  56. package/{app/extensions/swarm → agent/extensions/team}/index.ts +41 -66
  57. package/{app/extensions/versions → agent/extensions/version}/browser.js +10 -10
  58. package/{app/extensions/versions → agent/extensions/version}/index.ts +39 -54
  59. package/{app → agent}/extensions/viewer/README.md +4 -4
  60. package/{app → agent}/extensions/viewer/browser.js +8 -8
  61. package/agent/extensions/viewer/guide.md +3 -0
  62. package/{app → agent}/extensions/viewer/index.ts +22 -17
  63. package/agent/extensions/voice/browser.js +72 -0
  64. package/agent/extensions/voice/index.ts +39 -0
  65. package/{app/extensions/ui → agent/extensions/voice}/voice.js +5 -5
  66. package/agent/settings.json +8 -0
  67. package/{app/extensions/ui/skills/ui-check → agent/skills/browser-check}/SKILL.md +4 -4
  68. package/{app/extensions/ui/skills/ui-check → agent/skills/browser-check}/scripts/check.mjs +1 -1
  69. package/{app/extensions/ui/skills/ui-design → agent/skills/design}/SKILL.md +14 -14
  70. package/agent/skills/extension-map/SKILL.md +56 -0
  71. package/{app → agent}/skills/write-skill/SKILL.md +1 -1
  72. package/bin/chalky.mjs +3 -3
  73. package/package.json +8 -8
  74. package/server.ts +162 -63
  75. package/app/extensions/appdata/README.md +0 -10
  76. package/app/extensions/appdata/guide.md +0 -4
  77. package/app/extensions/appdata/index.ts +0 -47
  78. package/app/extensions/doodle/README.md +0 -15
  79. package/app/extensions/doodle/browser.js +0 -388
  80. package/app/extensions/doodle/guide.md +0 -6
  81. package/app/extensions/doodle/index.ts +0 -54
  82. package/app/extensions/files/browser.js +0 -96
  83. package/app/extensions/files/guide.md +0 -7
  84. package/app/extensions/frames/guide.md +0 -7
  85. package/app/extensions/frames/index.ts +0 -76
  86. package/app/extensions/mcp/guide.md +0 -5
  87. package/app/extensions/point/README.md +0 -22
  88. package/app/extensions/point/browser.js +0 -245
  89. package/app/extensions/point/guide.md +0 -6
  90. package/app/extensions/point/index.ts +0 -97
  91. package/app/extensions/prompt/index.ts +0 -19
  92. package/app/extensions/ui/README.md +0 -69
  93. package/app/extensions/ui/components/App.js +0 -28
  94. package/app/extensions/ui/components/Button.js +0 -28
  95. package/app/extensions/ui/components/Markdown.js +0 -37
  96. package/app/extensions/ui/guide.md +0 -7
  97. package/app/extensions/ui/index.ts +0 -271
  98. package/app/extensions/viewer/guide.md +0 -5
  99. package/app/mcp.json +0 -3
  100. package/app/skills/plugin-map/SKILL.md +0 -49
  101. package/app/ui/apps/.keep +0 -0
  102. /package/{app/extensions/ui/components → agent/extensions/card/blocks}/Table.js +0 -0
  103. /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,23 +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 });
69
+ page.send({ type: "frame_send", to: document.getElementById("to")?.value, board: board() });
68
70
  };
71
+ const board = () => Number(document.body.dataset.board) || 1; // the board shown (board/): frames are drawn and sent there
72
+ document.addEventListener("board", () => setTimeout(() => render(shown))); // after the switch: count that board's
69
73
 
70
- 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)
71
75
  const m = area.getBoundingClientRect();
72
76
  return { x: Math.round(x - m.left + area.scrollLeft), y: Math.round(y - m.top + area.scrollTop) };
73
77
  };
74
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` });
75
79
 
76
- // 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).
77
81
  let start = null, ghost = null, focusNew = false;
78
82
  area.addEventListener("mousedown", (e) => {
79
83
  if (!on || e.button !== 0 || e.target.closest(".frame")) return;
@@ -83,7 +87,7 @@ export default async function (page) {
83
87
  ghost = Object.assign(document.createElement("div"), { className: "frame drawing" });
84
88
  area.append(ghost);
85
89
  }, true);
86
- 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
87
91
  window.addEventListener("mousemove", (e) => {
88
92
  if (!start) return;
89
93
  const p = inMain(e.clientX, e.clientY);
@@ -94,7 +98,7 @@ export default async function (page) {
94
98
  const f = { x: parseInt(ghost.style.left), y: parseInt(ghost.style.top), width: parseInt(ghost.style.width), height: parseInt(ghost.style.height) };
95
99
  ghost.remove();
96
100
  start = ghost = null;
97
- if (f.width >= 40 && f.height >= 30) { focusNew = true; page.send({ type: "frame_add", ...f }); } // a click or a slip is not a frame
101
+ if (f.width >= 40 && f.height >= 30) { focusNew = true; page.send({ type: "frame_add", ...f, board: board() }); } // a click or a slip is not a frame
98
102
  });
99
103
 
100
104
  const boxes = {}; // id -> its box on the page
@@ -103,7 +107,7 @@ export default async function (page) {
103
107
  document.addEventListener("erase", (e) => { // the doodle eraser at {x, y}, main px: unsent frames under it go
104
108
  const p = e.detail;
105
109
  for (const [id, f] of Object.entries(shown)) {
106
- if (f.sent || p.x < f.x || p.x > f.x + f.width || p.y < f.y || p.y > f.y + f.height) continue;
110
+ if (f.sent || (f.board ?? 1) !== board() || p.x < f.x || p.x > f.x + f.width || p.y < f.y || p.y > f.y + f.height) continue;
107
111
  delete shown[id]; // once, though the eraser keeps moving over it
108
112
  page.send({ type: "frame_remove", id });
109
113
  }
@@ -134,17 +138,18 @@ export default async function (page) {
134
138
  newest = box;
135
139
  }
136
140
  put(box, f);
141
+ box.dataset.board = f.board ?? 1; // board/ shows it on its board
137
142
  box.classList.toggle("sent", f.sent);
138
143
  const text = box.querySelector("textarea");
139
144
  text.readOnly = f.sent;
140
145
  if (document.activeElement !== text) text.value = f.text;
141
146
  }
142
147
  if (focusNew && newest) { focusNew = false; newest.querySelector("textarea").focus(); }
143
- ready = Object.values(frames).filter((f) => !f.sent && f.text.trim()).length;
148
+ ready = Object.values(frames).filter((f) => !f.sent && f.text.trim() && (f.board ?? 1) === board()).length;
144
149
  showReady();
145
150
  send.title = `Implement: send ${ready} frame${ready > 1 ? "s" : ""} to the agent to build, together`;
146
151
  }
147
- 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
148
153
  e.preventDefault();
149
154
  const corner = e.currentTarget, x = box.offsetLeft, y = box.offsetTop, w0 = box.offsetWidth, h0 = box.offsetHeight;
150
155
  corner.setPointerCapture(e.pointerId);
@@ -158,6 +163,6 @@ export default async function (page) {
158
163
  if (place) page.send({ type: "frame_place", id, ...place });
159
164
  };
160
165
  }
161
- page.on("frames", (ev) => render(ev.frames));
162
- 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 {}
163
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,81 @@
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(card="frame-1", place=<its place>);
7
+ // when that card appears (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}
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 with its id appears (after all extensions loaded: the card one may come later)
36
+ pi.on("session_start", () => {
37
+ host.api.card?.watch("frame", (sid: string) => {
38
+ if (frames[sid] && host.shared.get("card")?.cards[sid]) { delete frames[sid]; changed(); }
39
+ });
40
+ host.api.board?.onRemove("frame", (n: number) => { // a removed board (board/) takes its frames with it
41
+ const ids = Object.keys(frames).filter((id) => (frames[id].board ?? 1) === n);
42
+ const gone = ids.map((id) => ({ owner: frames[id].sent ? frames[id].to : undefined, what: `frame ${id}` }));
43
+ for (const id of ids) delete frames[id];
44
+ if (ids.length) changed();
45
+ return gone;
46
+ });
47
+ });
48
+
49
+ host.routes.set("/frame", (_req: any, res: any) => res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(frames)));
50
+
51
+ host.pageHandlers.set("frame", (msg: any) => {
52
+ const f = frames[msg.id], team = host.api.team;
53
+ if (msg.type === "frame_add") {
54
+ frames[`frame-${++saved.last}`] = { ...box(msg), text: "", sent: false, board: Number(msg.board) || 1 };
55
+ changed();
56
+ } else if (msg.type === "frame_place" && f && !f.sent) {
57
+ Object.assign(f, box(msg, f));
58
+ changed();
59
+ } else if (msg.type === "frame_text" && f && !f.sent) {
60
+ f.text = String(msg.text ?? "");
61
+ changed();
62
+ } else if (msg.type === "frame_remove" && f) {
63
+ delete frames[msg.id];
64
+ changed();
65
+ if (f.sent) team?.send("user", f.to, `[frames on the page] the user removed ${msg.id} ("${f.text}"): don't fill it.`, true);
66
+ } else if (msg.type === "frame_send" && team) {
67
+ const to = msg.to in team.status() ? msg.to : "agent1";
68
+ const board = Number(msg.board) || 1; // the board shown: its frames only (the user sees no others)
69
+ const ready = Object.entries(frames).filter(([, f]: any) => !f.sent && f.text.trim() && (f.board ?? 1) === board);
70
+ if (!ready.length) return;
71
+ for (const [, f] of ready as any) Object.assign(f, { sent: true, to });
72
+ changed();
73
+ const lines = [`[frames on the page] the user laid out ${ready.length} frame${ready.length > 1 ? "s" : ""} on board ${board} to fill:`,
74
+ ...ready.map(([id, f]: any) => ` ${id}: "${f.text}" · place=${place(f)}`),
75
+ ` Fill each with card_show(card=<its id>, place=<its place>, board=${board}, ...); its dashed box goes when it appears.`];
76
+ const waiting = Object.keys(frames).filter((id) => frames[id].sent && !ready.some(([r]) => r === id));
77
+ if (waiting.length) lines.push(` Sent before and not filled yet: ${waiting.join(", ")}.`);
78
+ team.send("user", to, lines.join("\n"));
79
+ }
80
+ });
81
+ }
@@ -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,26 @@ 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) => { if (what) host.api.history.add(by, what); });
93
92
  });
94
- const shared = (): History | undefined => host.shared.get("ui-history");
93
+ const shared = (): History | undefined => host.shared.get("history");
95
94
 
96
- host.pageHandlers.set("ui_history", (msg: any) => {
95
+ host.pageHandlers.set("history", (msg: any) => {
97
96
  const history = shared();
98
97
  if (!history) return;
99
98
  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);
99
+ if (msg.type === "history_undo" && now && now.parent !== null) history.goto(now.parent);
100
+ else if (msg.type === "history_redo") { const kids = history.children(history.at); if (kids.length) history.goto(kids.at(-1)!); }
101
+ else if (msg.type === "history_goto") history.goto(msg.step);
103
102
  });
104
- host.routes.set("/ui-api/history", (_req: any, res: any) => {
103
+ host.routes.set("/history", (_req: any, res: any) => {
105
104
  const history = shared();
106
105
  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; }) }));
106
+ line: (history?.line() ?? []).map((n) => { const { cards, parent, ...s } = history!.steps.get(n)!; return s; }) }));
108
107
  });
109
108
  }
@@ -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;