@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.
- package/README.md +16 -5
- package/agent/SYSTEM.md +36 -0
- package/agent/extensions/app/README.md +72 -0
- package/{app/extensions/ui → agent/extensions/app}/agents.js +22 -16
- package/agent/extensions/app/guide.md +5 -0
- package/agent/extensions/app/index.ts +66 -0
- package/{app/extensions/ui/skills/agent-apps → agent/extensions/app/skills/app-agents}/SKILL.md +17 -18
- package/agent/extensions/app/skills/test-your-app/SKILL.md +23 -0
- package/{app/extensions/boards → agent/extensions/board}/browser.js +30 -32
- package/{app/extensions/boards → agent/extensions/board}/guide.md +1 -1
- package/{app/extensions/boards → agent/extensions/board}/index.ts +32 -30
- package/agent/extensions/card/README.md +88 -0
- package/agent/extensions/card/blocks/Button.js +34 -0
- package/{app/extensions/ui/components → agent/extensions/card/blocks}/Column.js +2 -2
- package/agent/extensions/card/blocks/Markdown.js +37 -0
- package/{app/extensions/ui/components/Card.js → agent/extensions/card/blocks/Panel.js} +3 -3
- package/{app/extensions/ui/components → agent/extensions/card/blocks}/Row.js +2 -2
- package/{app/extensions/ui/components → agent/extensions/card/blocks}/TextField.js +3 -3
- package/{app/extensions/ui → agent/extensions/card}/browser.js +155 -129
- package/agent/extensions/card/guide.md +13 -0
- package/agent/extensions/card/index.ts +370 -0
- package/{app/extensions/commands → agent/extensions/command}/browser.js +3 -3
- package/agent/extensions/command/guide.md +2 -0
- package/{app/extensions/commands → agent/extensions/command}/index.ts +14 -16
- package/agent/extensions/doodle/README.md +57 -0
- package/agent/extensions/doodle/browser.js +1013 -0
- package/agent/extensions/doodle/guide.md +22 -0
- package/agent/extensions/doodle/index.ts +274 -0
- package/agent/extensions/file/browser.js +182 -0
- package/agent/extensions/file/guide.md +5 -0
- package/{app/extensions/files → agent/extensions/file}/index.ts +37 -18
- package/{app/extensions/frames → agent/extensions/frame}/README.md +13 -13
- package/{app/extensions/frames → agent/extensions/frame}/browser.js +20 -18
- package/agent/extensions/frame/guide.md +3 -0
- package/agent/extensions/frame/index.ts +90 -0
- package/{app/extensions/ui_history → agent/extensions/history}/browser.js +18 -18
- package/{app/extensions/ui_history → agent/extensions/history}/index.ts +34 -33
- package/{app → agent}/extensions/mcp/browser.js +1 -1
- package/agent/extensions/mcp/guide.md +3 -0
- package/{app → agent}/extensions/mcp/index.ts +2 -4
- package/{app/extensions/login → agent/extensions/model}/browser.js +67 -67
- package/{app → agent}/extensions/model/index.ts +8 -4
- package/{app/extensions/login/index.ts → agent/extensions/model/login.ts} +47 -46
- package/agent/extensions/note/README.md +37 -0
- package/agent/extensions/note/browser.js +382 -0
- package/agent/extensions/note/guide.md +9 -0
- package/agent/extensions/note/index.ts +215 -0
- package/agent/extensions/page/browser.js +453 -0
- package/{app → agent}/extensions/page/index.ts +72 -3
- package/{app → agent}/extensions/page/page.html +102 -49
- package/agent/extensions/page/skills/interact-with-page/SKILL.md +59 -0
- package/agent/extensions/prompt/index.ts +38 -0
- package/{app → agent}/extensions/reload/index.ts +3 -3
- package/{app → agent}/extensions/screen/browser.js +10 -23
- package/{app → agent}/extensions/screen/index.ts +11 -11
- package/{app/extensions/steps → agent/extensions/step}/index.ts +1 -1
- package/{app/extensions/swarm → agent/extensions/team}/README.md +19 -19
- package/{app/extensions/swarm → agent/extensions/team}/guide.md +4 -4
- package/{app/extensions/swarm → agent/extensions/team}/index.ts +41 -66
- package/{app/extensions/versions → agent/extensions/version}/browser.js +19 -10
- package/agent/extensions/version/index.ts +205 -0
- package/{app → agent}/extensions/viewer/README.md +4 -4
- package/{app → agent}/extensions/viewer/browser.js +8 -8
- package/agent/extensions/viewer/guide.md +3 -0
- package/{app → agent}/extensions/viewer/index.ts +22 -17
- package/{app → agent}/extensions/voice/browser.js +4 -3
- package/{app → agent}/extensions/voice/index.ts +8 -6
- package/{app/extensions/ui → agent/extensions/voice}/voice.js +5 -5
- package/agent/settings.json +8 -0
- package/{app/extensions/ui/skills/ui-check → agent/skills/browser-check}/SKILL.md +4 -4
- package/{app/extensions/ui/skills/ui-check → agent/skills/browser-check}/scripts/check.mjs +1 -1
- package/{app/extensions/ui/skills/ui-design → agent/skills/design}/SKILL.md +15 -15
- package/agent/skills/extension-map/SKILL.md +56 -0
- package/agent/skills/show-me/SKILL.md +66 -0
- package/{app → agent}/skills/write-skill/SKILL.md +1 -1
- package/bin/chalky.mjs +2 -2
- package/package.json +9 -8
- package/server.ts +313 -58
- package/app/SYSTEM.md +0 -31
- package/app/extensions/appdata/README.md +0 -10
- package/app/extensions/appdata/guide.md +0 -4
- package/app/extensions/appdata/index.ts +0 -47
- package/app/extensions/commands/guide.md +0 -3
- package/app/extensions/doodle/README.md +0 -15
- package/app/extensions/doodle/browser.js +0 -398
- package/app/extensions/doodle/guide.md +0 -6
- package/app/extensions/doodle/index.ts +0 -54
- package/app/extensions/files/browser.js +0 -96
- package/app/extensions/files/guide.md +0 -7
- package/app/extensions/frames/guide.md +0 -7
- package/app/extensions/frames/index.ts +0 -85
- package/app/extensions/mcp/guide.md +0 -5
- package/app/extensions/point/README.md +0 -22
- package/app/extensions/point/browser.js +0 -247
- package/app/extensions/point/guide.md +0 -6
- package/app/extensions/point/index.ts +0 -112
- package/app/extensions/prompt/index.ts +0 -19
- package/app/extensions/ui/README.md +0 -69
- package/app/extensions/ui/components/App.js +0 -28
- package/app/extensions/ui/components/Button.js +0 -28
- package/app/extensions/ui/components/Markdown.js +0 -37
- package/app/extensions/ui/guide.md +0 -7
- package/app/extensions/ui/index.ts +0 -291
- package/app/extensions/versions/index.ts +0 -168
- package/app/extensions/viewer/guide.md +0 -5
- package/app/mcp.json +0 -3
- package/app/settings.json +0 -3
- package/app/skills/plugin-map/SKILL.md +0 -49
- package/app/ui/apps/.keep +0 -0
- package/node_modules/xlsx/CHANGELOG.md +0 -361
- package/node_modules/xlsx/LICENSE +0 -201
- package/node_modules/xlsx/README.md +0 -41
- package/node_modules/xlsx/bin/xlsx.njs +0 -310
- package/node_modules/xlsx/bower.json +0 -22
- package/node_modules/xlsx/dist/LICENSE +0 -201
- package/node_modules/xlsx/dist/cpexcel.d.ts +0 -39
- package/node_modules/xlsx/dist/cpexcel.full.mjs +0 -1502
- package/node_modules/xlsx/dist/cpexcel.js +0 -1506
- package/node_modules/xlsx/dist/shim.min.js +0 -2
- package/node_modules/xlsx/dist/xlsx.core.min.js +0 -18
- package/node_modules/xlsx/dist/xlsx.core.min.map +0 -1
- package/node_modules/xlsx/dist/xlsx.extendscript.js +0 -28259
- package/node_modules/xlsx/dist/xlsx.full.min.js +0 -24
- package/node_modules/xlsx/dist/xlsx.full.min.map +0 -1
- package/node_modules/xlsx/dist/xlsx.mini.min.js +0 -10
- package/node_modules/xlsx/dist/xlsx.mini.min.map +0 -1
- package/node_modules/xlsx/dist/xlsx.zahl.js +0 -4
- package/node_modules/xlsx/dist/xlsx.zahl.mjs +0 -2
- package/node_modules/xlsx/dist/zahl.d.ts +0 -4
- package/node_modules/xlsx/package.json +0 -183
- package/node_modules/xlsx/types/index.d.ts +0 -1065
- package/node_modules/xlsx/types/tsconfig.json +0 -15
- package/node_modules/xlsx/xlsx.js +0 -28103
- package/node_modules/xlsx/xlsx.mjs +0 -28228
- package/node_modules/xlsx/xlsxworker.js +0 -14
- /package/{app/extensions/ui/components → agent/extensions/card/blocks}/Table.js +0 -0
- /package/{app/extensions/ui/components → agent/extensions/card/blocks}/Text.js +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
//
|
|
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 (
|
|
5
|
-
// off, the bubble sits on the tool itself, so waiting frames still show. Until then, a frame's corner resizes it, like a
|
|
6
|
-
// Sent frames stay as they are, marked waiting, until an agent renders a
|
|
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
|
|
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.
|
|
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 = "
|
|
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: "
|
|
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 !== "
|
|
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: "
|
|
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 (
|
|
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
|
|
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
|
|
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); //
|
|
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; //
|
|
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
|
|
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("
|
|
165
|
-
try { render(await (await fetch("/
|
|
166
|
+
page.on("frame", (ev) => render(ev.frames));
|
|
167
|
+
try { render(await (await fetch("/frame")).json()); } catch {}
|
|
166
168
|
}
|
|
@@ -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
|
-
//
|
|
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 (
|
|
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
|
-
#
|
|
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
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
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
|
-
#
|
|
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="
|
|
21
|
-
const bar = Object.assign(document.createElement("div"), { id: "
|
|
22
|
-
const toastEl = Object.assign(document.createElement("div"), { id: "
|
|
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: "
|
|
39
|
-
else if ((k === "z" && e.shiftKey) || k === "y") { e.preventDefault(); page.send({ type: "
|
|
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: "
|
|
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("/
|
|
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("
|
|
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: "
|
|
95
|
+
() => page.send({ type: "history_goto", step: ev.tip }));
|
|
96
96
|
});
|
|
97
97
|
}
|
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
// Every state of the
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
|
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
|
|
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.
|
|
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,
|
|
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
|
|
33
|
-
constructor(public
|
|
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)!.
|
|
43
|
-
this.add("start", "the
|
|
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,
|
|
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("
|
|
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.
|
|
73
|
-
host.broadcast("
|
|
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
|
|
88
|
-
//
|
|
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("
|
|
91
|
-
host.api.
|
|
92
|
-
host.api.
|
|
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("
|
|
95
|
+
const shared = (): History | undefined => host.shared.get("history");
|
|
95
96
|
|
|
96
|
-
host.pageHandlers.set("
|
|
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 === "
|
|
101
|
-
else if (msg.type === "
|
|
102
|
-
else if (msg.type === "
|
|
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("/
|
|
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 {
|
|
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
|
-
//
|
|
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 = `
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// MCP picker: "/mcp" on the page ticks which servers in
|
|
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.
|
|
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;
|