@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.
- package/README.md +10 -6
- package/{app → agent}/SYSTEM.md +16 -11
- package/agent/extensions/app/README.md +68 -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/board/browser.js +359 -0
- package/agent/extensions/board/guide.md +4 -0
- package/agent/extensions/board/index.ts +100 -0
- 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 -128
- package/agent/extensions/card/guide.md +13 -0
- package/agent/extensions/card/index.ts +370 -0
- package/agent/extensions/command/browser.js +13 -0
- package/agent/extensions/command/guide.md +2 -0
- package/agent/extensions/command/index.ts +94 -0
- 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 +143 -0
- package/agent/extensions/file/guide.md +5 -0
- package/{app/extensions/files → agent/extensions/file}/index.ts +34 -16
- package/{app/extensions/frames → agent/extensions/frame}/README.md +13 -13
- package/{app/extensions/frames → agent/extensions/frame}/browser.js +24 -19
- package/agent/extensions/frame/guide.md +3 -0
- package/agent/extensions/frame/index.ts +81 -0
- package/{app/extensions/ui_history → agent/extensions/history}/browser.js +18 -18
- package/{app/extensions/ui_history → agent/extensions/history}/index.ts +32 -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/agent/extensions/model/browser.js +363 -0
- package/{app → agent}/extensions/model/index.ts +8 -4
- package/agent/extensions/model/login.ts +215 -0
- package/agent/extensions/note/README.md +35 -0
- package/agent/extensions/note/browser.js +356 -0
- package/agent/extensions/note/guide.md +8 -0
- package/agent/extensions/note/index.ts +192 -0
- package/{app → agent}/extensions/page/index.ts +2 -2
- package/{app → agent}/extensions/page/page.html +55 -49
- package/agent/extensions/prompt/index.ts +38 -0
- package/{app → agent}/extensions/reload/index.ts +3 -3
- package/{app → agent}/extensions/screen/browser.js +4 -4
- 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 +10 -10
- package/{app/extensions/versions → agent/extensions/version}/index.ts +39 -54
- 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/agent/extensions/voice/browser.js +72 -0
- package/agent/extensions/voice/index.ts +39 -0
- 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 +14 -14
- package/agent/skills/extension-map/SKILL.md +56 -0
- package/{app → agent}/skills/write-skill/SKILL.md +1 -1
- package/bin/chalky.mjs +3 -3
- package/package.json +8 -8
- package/server.ts +162 -63
- 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/doodle/README.md +0 -15
- package/app/extensions/doodle/browser.js +0 -388
- 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 -76
- package/app/extensions/mcp/guide.md +0 -5
- package/app/extensions/point/README.md +0 -22
- package/app/extensions/point/browser.js +0 -245
- package/app/extensions/point/guide.md +0 -6
- package/app/extensions/point/index.ts +0 -97
- 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 -271
- package/app/extensions/viewer/guide.md +0 -5
- package/app/mcp.json +0 -3
- package/app/skills/plugin-map/SKILL.md +0 -49
- package/app/ui/apps/.keep +0 -0
- /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,388 +0,0 @@
|
|
|
1
|
-
// Browser half of the doodle extension (Python's doodle.js, unchanged but for this header): three tools in the palette, a marker, Text and an eraser. The marker and
|
|
2
|
-
// Text can be on together; the eraser (like any other tool) turns them off.
|
|
3
|
-
// marker drag draws on the page, over surfaces and inside App frames (same-origin). A press that doesn't move
|
|
4
|
-
// is an ordinary click, so double and triple clicks still select text. Typing with the pointer over the
|
|
5
|
-
// page and no text box focused puts words in handwriting there.
|
|
6
|
-
// Text (T) every click puts words in handwriting there; Enter or a click elsewhere ends them. With the
|
|
7
|
-
// marker on too, a drag draws and a click writes.
|
|
8
|
-
// eraser press or drag over a line or words to take them away whole; it also says "erase" (a page event,
|
|
9
|
-
// {x, y} in main-space px) so other tools take theirs away (frames: an unsent frame).
|
|
10
|
-
// click with the marker (Text off) works the page as usual. If there is drawing, it first becomes a step: a picture of the screen
|
|
11
|
-
// as it was when the button went down (before the click acts), cropped to the drawing, and the
|
|
12
|
-
// ink clears. The drawing goes only as the picture; typed words also go as text, exactly. Clicks
|
|
13
|
-
// are written down as words, between the steps.
|
|
14
|
-
// Enter in the message line (also with the marker off) sends the steps, the drawing still on the page and
|
|
15
|
-
// the words typed there, together: doodle_send, to the agent picked in the chat.
|
|
16
|
-
// Things with a drag of their own keep it: a surface's label (moves it), its resize corner, lists to pick from,
|
|
17
|
-
// sliders, draggable things, frames; text boxes don't (a drag that starts on one draws). Ctrl+Z takes back
|
|
18
|
-
// the last mark; Esc turns the tools off (ink stays).
|
|
19
|
-
// Pictures: the tab is shared once (the screen extension), when the marker or Text is first turned on; without
|
|
20
|
-
// it, steps go as words only. The steps so far show in the message line ("1 → ☆ Week 2 · 2 marks"): hover one for its picture, click it to see it big, ✕ drops it.
|
|
21
|
-
// Ink and words live in the main space (slot "top"), in main-space px, so they scroll with the surfaces.
|
|
22
|
-
export default async function (page) {
|
|
23
|
-
const area = page.slot("top");
|
|
24
|
-
const form = document.getElementById("f"), input = document.getElementById("text");
|
|
25
|
-
const SVG = "http://www.w3.org/2000/svg", INK = "#e5397a";
|
|
26
|
-
// The cursor: a marker (white edge, so it shows on dark too); its tip (3, 21) is where the line starts.
|
|
27
|
-
const PEN = `url("data:image/svg+xml,${encodeURIComponent(
|
|
28
|
-
`<svg xmlns="${SVG}" width="24" height="24" viewBox="0 0 24 24"><path d="M3 21l1.6-5.2L16.2 4.2a2.2 2.2 0 013.1 3.1L7.7 18.9z" ` +
|
|
29
|
-
`fill="#fff" stroke="#1d1d1f" stroke-width="1.4" stroke-linejoin="round"/><path d="M3 21l1.6-5.2 3.1 3.1z" fill="${INK}"/></svg>`)}") 3 21, crosshair`;
|
|
30
|
-
const RUB = `url("data:image/svg+xml,${encodeURIComponent( // the eraser cursor; its bottom-left edge (5, 20) rubs
|
|
31
|
-
`<svg xmlns="${SVG}" width="24" height="24" viewBox="0 0 24 24"><path d="m7 21-4.3-4.3a2.4 2.4 0 0 1 0-3.4l9.6-9.6a2.4 2.4 0 0 1 3.4 0` +
|
|
32
|
-
`l5.6 5.6a2.4 2.4 0 0 1 0 3.4L13 21z" fill="#fff" stroke="#1d1d1f" stroke-width="1.4" stroke-linejoin="round"/>` +
|
|
33
|
-
`<path d="m7 21-4.3-4.3a2.4 2.4 0 0 1 0-3.4L7.5 8.5l9 9L13 21z" fill="${INK}"/></svg>`)}") 5 20, crosshair`;
|
|
34
|
-
const CURSORS = `
|
|
35
|
-
.doodling, .doodling * { cursor: ${PEN}; }
|
|
36
|
-
.doodling :is(button, a, select, summary, label, [onclick], [role="button"], [role="tab"], .surface-label) { cursor: pointer; }
|
|
37
|
-
.doodling :is(input, textarea, [contenteditable="true"]) { cursor: text; }
|
|
38
|
-
.doodling .surface-resize { cursor: nwse-resize; }
|
|
39
|
-
.doodle-text:not(.doodling), .doodle-text:not(.doodling) * { cursor: text !important; }
|
|
40
|
-
.doodle-erase, .doodle-erase * { cursor: ${RUB} !important; }`;
|
|
41
|
-
const style = document.createElement("style");
|
|
42
|
-
style.textContent = CURSORS + `
|
|
43
|
-
#top:is(.doodling, .doodle-text, .doodle-erase) { z-index: 11; } /* above the messages panel (10), so lines can go there too */
|
|
44
|
-
#top > svg.doodle-ink { position: absolute; left: 0; top: 0; width: 1px; height: 1px; overflow: visible; z-index: 100001; pointer-events: none; }
|
|
45
|
-
#top > svg.doodle-ink path { fill: none; stroke: ${INK}; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
|
|
46
|
-
#top > .doodle-note { position: absolute; z-index: 100001; font: 20px/1.2 "Segoe Print", "Bradley Hand", "Comic Sans MS", cursive; color: ${INK};
|
|
47
|
-
white-space: pre; outline: none; pointer-events: none; padding: 0 4px; min-width: 8px; }
|
|
48
|
-
#top > .doodle-note:empty::before { content: "type here…"; opacity: .5; }
|
|
49
|
-
#top > .doodle-note[contenteditable="true"] { pointer-events: auto; background: rgba(229, 57, 122, .1); border-radius: 6px; caret-color: ${INK}; }
|
|
50
|
-
#f .doodle-strip { display: flex; align-items: center; gap: 4px; flex: none; max-width: 45%; overflow-x: auto; }
|
|
51
|
-
#f .doodle-strip:empty { display: none; }
|
|
52
|
-
#f .doodle-chip { position: relative; flex: none; min-width: 24px; height: 24px; padding: 0 7px; box-sizing: border-box; border-radius: 7px;
|
|
53
|
-
display: grid; place-items: center; font-size: 12px; font-weight: 600; color: ${INK}; border: 1.5px solid ${INK};
|
|
54
|
-
background: rgba(229, 57, 122, .08); cursor: default; }
|
|
55
|
-
#f .doodle-chip.now { border-style: dashed; background: none; font-weight: 500; }
|
|
56
|
-
#f .doodle-chip .x { position: absolute; right: -6px; top: -6px; width: 14px; height: 14px; border-radius: 50%; background: var(--text);
|
|
57
|
-
color: #fff; font-size: 9px; line-height: 14px; text-align: center; display: none; cursor: pointer; }
|
|
58
|
-
#f .doodle-chip:hover .x { display: block; }
|
|
59
|
-
#f .doodle-click { flex: none; font-size: 12px; color: var(--muted); white-space: nowrap; max-width: 120px; overflow: hidden; text-overflow: ellipsis; }
|
|
60
|
-
.doodle-peek { position: fixed; z-index: 100; max-width: 360px; padding: 6px; border-radius: 12px; background: var(--glass-strong, #fff);
|
|
61
|
-
border: 1px solid var(--edge, #eee); box-shadow: 0 8px 30px rgba(0, 0, 0, .12); font-size: 12px; color: var(--muted); pointer-events: none; }
|
|
62
|
-
.doodle-peek img { display: block; max-width: 100%; max-height: 220px; border-radius: 6px; margin-bottom: 4px; }
|
|
63
|
-
#f .doodle-chip:not(.now) { cursor: zoom-in; }
|
|
64
|
-
.doodle-big { position: fixed; inset: 0; z-index: 200; display: grid; place-content: center; justify-items: center; gap: 10px;
|
|
65
|
-
background: rgba(0, 0, 0, .55); cursor: zoom-out; color: #fff; font-size: 13px; }
|
|
66
|
-
.doodle-big div { padding: 4px 12px; border-radius: 999px; background: rgba(0, 0, 0, .6); }
|
|
67
|
-
.doodle-big img { max-width: 92vw; max-height: 84vh; border-radius: 10px; box-shadow: 0 20px 60px rgba(0, 0, 0, .4); background: #fff; }`;
|
|
68
|
-
document.head.append(style);
|
|
69
|
-
|
|
70
|
-
const button = document.createElement("button");
|
|
71
|
-
button.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 17c3-6 5-6 7-2s4 4 7-3 3-5 4-4"/></svg>`;
|
|
72
|
-
button.title = "Doodle: drag to draw, type to write, click to use the page; then Enter in the message line sends it all (Esc to stop)";
|
|
73
|
-
const textButton = document.createElement("button");
|
|
74
|
-
textButton.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7V5h14v2M12 5v14M9 19h6"/></svg>`;
|
|
75
|
-
textButton.title = "Text: click where the words go (Enter to finish); sent with the doodles, Enter in the message line (Esc to stop)";
|
|
76
|
-
const eraseButton = document.createElement("button");
|
|
77
|
-
eraseButton.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m7 21-4.3-4.3a2.4 2.4 0 0 1 0-3.4l9.6-9.6a2.4 2.4 0 0 1 3.4 0` +
|
|
78
|
-
`l5.6 5.6a2.4 2.4 0 0 1 0 3.4L13 21"/><path d="M22 21H7"/><path d="m5 11 9 9"/></svg>`;
|
|
79
|
-
eraseButton.title = "Eraser: press or drag over lines, words or unsent frames to take them away (Esc to stop)";
|
|
80
|
-
page.slot("status").append(button, textButton, eraseButton);
|
|
81
|
-
const strip = Object.assign(document.createElement("div"), { className: "doodle-strip" });
|
|
82
|
-
form.insertBefore(strip, input);
|
|
83
|
-
const peek = Object.assign(document.createElement("div"), { className: "doodle-peek", hidden: true });
|
|
84
|
-
const big = Object.assign(document.createElement("div"), { className: "doodle-big", hidden: true }); // a step's picture, big
|
|
85
|
-
big.onclick = () => { big.hidden = true; };
|
|
86
|
-
document.addEventListener("keydown", (e) => { if (e.key === "Escape") big.hidden = true; });
|
|
87
|
-
document.body.append(peek, big);
|
|
88
|
-
|
|
89
|
-
let drawing = false, texting = false, erasing = false, on = false; // which tools are on; on: any of them
|
|
90
|
-
const docs = new Set([document]); // this page and every App frame's page: they all get the cursor
|
|
91
|
-
const classes = (el) => {
|
|
92
|
-
el.classList.toggle("doodling", drawing);
|
|
93
|
-
el.classList.toggle("doodle-text", texting);
|
|
94
|
-
el.classList.toggle("doodle-erase", erasing);
|
|
95
|
-
};
|
|
96
|
-
const set = (tool, v) => { // tool: "doodle" | "text" | "erase"
|
|
97
|
-
if (tool === "doodle") drawing = v; else if (tool === "text") texting = v; else erasing = v;
|
|
98
|
-
on = drawing || texting || erasing;
|
|
99
|
-
button.classList.toggle("on", drawing);
|
|
100
|
-
textButton.classList.toggle("on", texting);
|
|
101
|
-
eraseButton.classList.toggle("on", erasing);
|
|
102
|
-
classes(area);
|
|
103
|
-
for (const d of docs) if (d !== document && d.documentElement) classes(d.documentElement);
|
|
104
|
-
if (v) {
|
|
105
|
-
if (tool !== "erase") share();
|
|
106
|
-
document.dispatchEvent(new CustomEvent("tool-on", { detail: tool }));
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
const allOff = () => { for (const t of ["doodle", "text", "erase"]) set(t, false); };
|
|
110
|
-
button.onclick = () => set("doodle", !drawing);
|
|
111
|
-
textButton.onclick = () => set("text", !texting);
|
|
112
|
-
eraseButton.onclick = () => set("erase", !erasing);
|
|
113
|
-
document.addEventListener("tool-on", (e) => { // one tool at a time, except the marker and Text, which work together
|
|
114
|
-
const t = e.detail, pair = t === "doodle" || t === "text";
|
|
115
|
-
if (drawing && t !== "doodle" && !pair) set("doodle", false);
|
|
116
|
-
if (texting && t !== "text" && !pair) set("text", false);
|
|
117
|
-
if (erasing && t !== "erase") set("erase", false);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// ---- pictures: the tab, shared once by the screen plugin (none without it: steps go as words) ----
|
|
121
|
-
let screen = null;
|
|
122
|
-
page.use("screen").then((s) => { screen = s; });
|
|
123
|
-
const share = () => screen?.share();
|
|
124
|
-
const frame = () => screen?.frame() ?? null; // the screen right now, as a canvas (null without a shared tab)
|
|
125
|
-
const picture = (full, box) => screen.picture(full, box); // crop a frame to box (window px) and upload it: {path, url}
|
|
126
|
-
|
|
127
|
-
// ---- where things are ----
|
|
128
|
-
const inMain = (x, y) => { // window px -> main-space px (as point.js)
|
|
129
|
-
const m = area.getBoundingClientRect();
|
|
130
|
-
return { x: x - m.left + area.scrollLeft, y: y - m.top + area.scrollTop };
|
|
131
|
-
};
|
|
132
|
-
const inWindow = (p) => {
|
|
133
|
-
const m = area.getBoundingClientRect();
|
|
134
|
-
return { x: p.x + m.left - area.scrollLeft, y: p.y + m.top - area.scrollTop };
|
|
135
|
-
};
|
|
136
|
-
const short = (s, n = 40) => { s = (s || "").replace(/\s+/g, " ").trim(); return s.length > n ? s.slice(0, n - 1) + "…" : s; };
|
|
137
|
-
|
|
138
|
-
// ---- marks: lines and words ----
|
|
139
|
-
const ink = document.createElementNS(SVG, "svg"); // in the main space only while it has marks (#top:empty shows the welcome)
|
|
140
|
-
ink.classList.add("doodle-ink");
|
|
141
|
-
let marks = []; // [{el, points (main px), text (words only)}], oldest first
|
|
142
|
-
const show = () => { if (marks.some((m) => m.el.localName === "path" || m.el.localName === "g")) area.append(ink); else ink.remove(); render(); };
|
|
143
|
-
const f = (n) => n.toFixed(1);
|
|
144
|
-
const smooth = (pts) => { // through the midpoints, so a quick line isn't a row of corners
|
|
145
|
-
let d = `M${f(pts[0].x)} ${f(pts[0].y)}`;
|
|
146
|
-
for (let i = 1; i < pts.length - 1; i++) d += ` Q${f(pts[i].x)} ${f(pts[i].y)} ${f((pts[i].x + pts[i + 1].x) / 2)} ${f((pts[i].y + pts[i + 1].y) / 2)}`;
|
|
147
|
-
return d + ` L${f(pts.at(-1).x)} ${f(pts.at(-1).y)}`;
|
|
148
|
-
};
|
|
149
|
-
function note(p, first) { // words in handwriting at p (main px)
|
|
150
|
-
const n = Object.assign(document.createElement("div"), { className: "doodle-note", contentEditable: "true", textContent: first });
|
|
151
|
-
Object.assign(n.style, { left: `${p.x}px`, top: `${p.y - 14}px` });
|
|
152
|
-
area.append(n);
|
|
153
|
-
n.focus();
|
|
154
|
-
getSelection().selectAllChildren(n);
|
|
155
|
-
getSelection().collapseToEnd();
|
|
156
|
-
let done = false;
|
|
157
|
-
const finish = () => {
|
|
158
|
-
if (done) return;
|
|
159
|
-
done = true;
|
|
160
|
-
n.contentEditable = "false";
|
|
161
|
-
const t = n.textContent.trim();
|
|
162
|
-
if (!t) { n.remove(); return; }
|
|
163
|
-
marks.push({ el: n, points: [p, { x: p.x + n.offsetWidth, y: p.y + n.offsetHeight - 14 }], text: t });
|
|
164
|
-
show();
|
|
165
|
-
};
|
|
166
|
-
n.onkeydown = (e) => {
|
|
167
|
-
e.stopPropagation();
|
|
168
|
-
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); n.blur(); }
|
|
169
|
-
if (e.key === "Escape") { n.textContent = ""; n.blur(); }
|
|
170
|
-
};
|
|
171
|
-
n.onblur = finish;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// ---- erasing: a mark goes whole when the eraser passes over it ----
|
|
175
|
-
function eraseAt(p) { // p: main px
|
|
176
|
-
const left = marks.filter((m) => {
|
|
177
|
-
if (m.text) { // words: their box
|
|
178
|
-
const x = m.el.offsetLeft, y = m.el.offsetTop;
|
|
179
|
-
return !(p.x >= x - 6 && p.x <= x + m.el.offsetWidth + 6 && p.y >= y - 6 && p.y <= y + m.el.offsetHeight + 6);
|
|
180
|
-
}
|
|
181
|
-
return !m.points.some((q) => Math.hypot(q.x - p.x, q.y - p.y) < 12); // a line: near any of its points
|
|
182
|
-
});
|
|
183
|
-
if (left.length !== marks.length) {
|
|
184
|
-
for (const m of marks) if (!left.includes(m)) m.el.remove();
|
|
185
|
-
marks = left;
|
|
186
|
-
show();
|
|
187
|
-
}
|
|
188
|
-
document.dispatchEvent(new CustomEvent("erase", { detail: p })); // other tools' things (frames: unsent frames)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// ---- steps ----
|
|
192
|
-
let steps = [], first = []; // steps: [{marks: [text], then: [click], shot: {path, url} | null, ready}]
|
|
193
|
-
let grabbed = null; // the screen when the last button went down (a click may end a step with it)
|
|
194
|
-
function bounds(pts) { // window px, with room around the drawing, inside the window
|
|
195
|
-
const w = pts.map(inWindow), pad = 60;
|
|
196
|
-
let x0 = Math.min(...w.map((p) => p.x)) - pad, x1 = Math.max(...w.map((p) => p.x)) + pad;
|
|
197
|
-
let y0 = Math.min(...w.map((p) => p.y)) - pad, y1 = Math.max(...w.map((p) => p.y)) + pad;
|
|
198
|
-
if (x1 - x0 < 480) { const c = (x0 + x1) / 2; x0 = c - 240; x1 = c + 240; }
|
|
199
|
-
if (y1 - y0 < 300) { const c = (y0 + y1) / 2; y0 = c - 150; y1 = c + 150; }
|
|
200
|
-
x0 = Math.max(0, x0); y0 = Math.max(0, y0); x1 = Math.min(innerWidth, x1); y1 = Math.min(innerHeight, y1);
|
|
201
|
-
return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
|
|
202
|
-
}
|
|
203
|
-
function keep(full) { // the marks on the page become a step (full: the screen to crop its picture from)
|
|
204
|
-
const box = marks.length ? bounds(marks.flatMap((m) => m.points)) : { x: 0, y: 0, w: innerWidth, h: innerHeight };
|
|
205
|
-
const at = inMain(box.x, box.y); // the part of the main space the picture shows, so the agent can place things
|
|
206
|
-
const step = { drawn: marks.length > 0, words: marks.filter((m) => m.text).map((m) => m.text), then: [], shot: null,
|
|
207
|
-
area: { x: Math.round(at.x), y: Math.round(at.y), width: Math.round(box.w), height: Math.round(box.h) } };
|
|
208
|
-
step.ready = full ? picture(full, box).then((s) => { step.shot = s; }, () => {}) : Promise.resolve();
|
|
209
|
-
steps.push(step);
|
|
210
|
-
for (const m of marks) m.el.remove();
|
|
211
|
-
marks = [];
|
|
212
|
-
show();
|
|
213
|
-
}
|
|
214
|
-
function clicked(t, surface) { // a click, as words
|
|
215
|
-
const label = short(t.getAttribute("aria-label") || t.title || t.textContent || t.value || t.localName, 30);
|
|
216
|
-
const row = t.closest("tr, li");
|
|
217
|
-
const rowText = row && row !== t ? short((row.innerText || row.textContent).replace(t.innerText || t.textContent, ""), 30) : "";
|
|
218
|
-
return `clicked "${label}"${rowText ? ` on the "${rowText}" row` : ""}${surface ? ` (surface ${surface})` : ""}`;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// ---- presses: this page and every App frame's page ----
|
|
222
|
-
// Text boxes draw too (a click still focuses them, a double click still selects a word): circling a notes box
|
|
223
|
-
// is a common thing to draw.
|
|
224
|
-
const OWN_DRAG = ".surface-label, .surface-resize, select, input[type='range'], [draggable='true'], .frame, .doodle-note";
|
|
225
|
-
const CLICKABLE = "button, a, summary, select, label, [onclick], [role='button'], [role='tab'], [role='checkbox'], input[type='checkbox'], input[type='radio']";
|
|
226
|
-
let press = null, line = null, swallow = false, over = null; // over: the pointer, in window px, while it is over the page
|
|
227
|
-
function hook(win, frameEl) {
|
|
228
|
-
const doc = win.document;
|
|
229
|
-
if (frameEl) {
|
|
230
|
-
if (docs.has(doc)) return;
|
|
231
|
-
docs.add(doc);
|
|
232
|
-
const s = doc.createElement("style");
|
|
233
|
-
s.textContent = CURSORS;
|
|
234
|
-
doc.head.append(s);
|
|
235
|
-
classes(doc.documentElement);
|
|
236
|
-
}
|
|
237
|
-
const offset = () => {
|
|
238
|
-
if (!frameEl) return { x: 0, y: 0 };
|
|
239
|
-
const r = frameEl.getBoundingClientRect();
|
|
240
|
-
return { x: r.left + frameEl.clientLeft, y: r.top + frameEl.clientTop };
|
|
241
|
-
};
|
|
242
|
-
const root = frameEl ? doc : area;
|
|
243
|
-
root.addEventListener("pointerdown", (e) => {
|
|
244
|
-
if (!on || e.button !== 0) return;
|
|
245
|
-
const o = offset();
|
|
246
|
-
if (erasing) { // the press is for the eraser, not for what's under it
|
|
247
|
-
e.preventDefault();
|
|
248
|
-
press = { erase: true, win };
|
|
249
|
-
eraseAt(inMain(e.clientX + o.x, e.clientY + o.y));
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
grabbed = marks.length ? frame() : null; // before the click acts
|
|
253
|
-
if (e.target.closest?.(texting && !drawing ? ".doodle-note" : OWN_DRAG)) return;
|
|
254
|
-
if (texting) e.preventDefault(); // the press is for the words (or a line), not for what's under it
|
|
255
|
-
press = { x: e.clientX + o.x, y: e.clientY + o.y, id: e.pointerId, target: e.target, win, o, text: texting };
|
|
256
|
-
}, true);
|
|
257
|
-
win.addEventListener("pointermove", (e) => {
|
|
258
|
-
const o = offset(), x = e.clientX + o.x, y = e.clientY + o.y;
|
|
259
|
-
over = frameEl || e.target.closest?.("#top") ? { x, y } : null;
|
|
260
|
-
if (press?.erase && press.win === win) { win.getSelection().removeAllRanges(); eraseAt(inMain(x, y)); return; }
|
|
261
|
-
if (!press || press.win !== win || !drawing) return;
|
|
262
|
-
if (!line) {
|
|
263
|
-
if (Math.hypot(x - press.x, y - press.y) < 6) return; // not yet a line: may still be a click
|
|
264
|
-
line = { el: document.createElementNS(SVG, "g"), points: [inMain(press.x, press.y)] };
|
|
265
|
-
line.el.append(document.createElementNS(SVG, "path"));
|
|
266
|
-
ink.append(line.el);
|
|
267
|
-
marks.push(line);
|
|
268
|
-
show();
|
|
269
|
-
try { press.target.setPointerCapture(press.id); } catch {}
|
|
270
|
-
const active = press.win.document.activeElement; // a text box the press focused: typing now writes words, not into it
|
|
271
|
-
if (active?.matches?.("input, textarea, [contenteditable='true']")) active.blur();
|
|
272
|
-
}
|
|
273
|
-
win.getSelection().removeAllRanges(); // the first few px may have started a text selection
|
|
274
|
-
const p = inMain(x, y), last = line.points.at(-1);
|
|
275
|
-
if (Math.hypot(p.x - last.x, p.y - last.y) < 1.5) return;
|
|
276
|
-
line.points.push(p);
|
|
277
|
-
line.el.firstChild.setAttribute("d", smooth(line.points));
|
|
278
|
-
});
|
|
279
|
-
win.addEventListener("pointerup", () => {
|
|
280
|
-
if (press?.erase) { swallow = true; setTimeout(() => { swallow = false; }); } // the eraser's press isn't a click
|
|
281
|
-
if (press?.text && !line) { // Text: words where the press was (a press that drew a line was for the marker)
|
|
282
|
-
note(inMain(press.x, press.y), "");
|
|
283
|
-
swallow = true;
|
|
284
|
-
setTimeout(() => { swallow = false; });
|
|
285
|
-
}
|
|
286
|
-
if (line) {
|
|
287
|
-
if (line.points.length < 3) { marks.pop(); line.el.remove(); }
|
|
288
|
-
swallow = true; // the click that ends a line isn't a click
|
|
289
|
-
setTimeout(() => { swallow = false; });
|
|
290
|
-
show();
|
|
291
|
-
}
|
|
292
|
-
press = line = null;
|
|
293
|
-
});
|
|
294
|
-
root.addEventListener("click", (e) => {
|
|
295
|
-
if (swallow) { e.stopPropagation(); e.preventDefault(); swallow = false; return; }
|
|
296
|
-
const t = on && e.target.closest?.(CLICKABLE);
|
|
297
|
-
if (!t || t.closest(".doodle-note")) return;
|
|
298
|
-
if (marks.length) keep(grabbed); // the drawing so far, as the screen was when the button went down
|
|
299
|
-
(steps.length ? steps.at(-1).then : first).push(clicked(t, (frameEl || t).closest("[data-surface]")?.dataset.surface));
|
|
300
|
-
render();
|
|
301
|
-
}, true);
|
|
302
|
-
root.addEventListener("dragstart", (e) => { if (on) e.preventDefault(); }, true); // links and pictures don't get dragged away
|
|
303
|
-
doc.addEventListener("keydown", onKey);
|
|
304
|
-
}
|
|
305
|
-
hook(window, null);
|
|
306
|
-
const hookFrame = (fr) => {
|
|
307
|
-
const go = () => { try { if (fr.contentDocument?.readyState !== "loading") hook(fr.contentWindow, fr); } catch {} }; // other origins: not ours
|
|
308
|
-
fr.addEventListener("load", go);
|
|
309
|
-
go();
|
|
310
|
-
};
|
|
311
|
-
area.querySelectorAll("iframe").forEach(hookFrame);
|
|
312
|
-
new MutationObserver((list) => {
|
|
313
|
-
for (const r of list) for (const n of r.addedNodes) {
|
|
314
|
-
if (n.localName === "iframe") hookFrame(n);
|
|
315
|
-
else n.querySelectorAll?.("iframe").forEach(hookFrame);
|
|
316
|
-
}
|
|
317
|
-
}).observe(area, { childList: true, subtree: true });
|
|
318
|
-
|
|
319
|
-
function onKey(e) {
|
|
320
|
-
if (e.target.closest?.("input, textarea, select, [contenteditable='true']")) return;
|
|
321
|
-
if (e.key === "Escape" && on) { allOff(); return; }
|
|
322
|
-
if (!on || erasing) return;
|
|
323
|
-
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z") {
|
|
324
|
-
const m = marks.pop();
|
|
325
|
-
if (m) { m.el.remove(); show(); e.preventDefault(); }
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
if (e.key.length === 1 && e.key !== " " && !e.ctrlKey && !e.metaKey && !e.altKey && over) {
|
|
329
|
-
e.preventDefault();
|
|
330
|
-
note(inMain(over.x, over.y), e.key);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// ---- the strip in the message line ----
|
|
335
|
-
function render() {
|
|
336
|
-
strip.replaceChildren();
|
|
337
|
-
steps.forEach((s, i) => {
|
|
338
|
-
const chip = Object.assign(document.createElement("span"), { className: "doodle-chip", textContent: i + 1 });
|
|
339
|
-
const x = Object.assign(document.createElement("span"), { className: "x", textContent: "✕", title: "Drop this step" });
|
|
340
|
-
x.onclick = () => { peek.hidden = true; steps.splice(i, 1); render(); };
|
|
341
|
-
chip.append(x);
|
|
342
|
-
const words = s.words.map((w) => `"${w}"`).join(" ");
|
|
343
|
-
chip.title = s.shot ? "Click to see it big" : "No picture: the tab isn't shared";
|
|
344
|
-
chip.onmouseenter = () => { // a small preview above the chip
|
|
345
|
-
peek.replaceChildren();
|
|
346
|
-
const place = () => {
|
|
347
|
-
const r = chip.getBoundingClientRect(), p = peek.getBoundingClientRect();
|
|
348
|
-
Object.assign(peek.style, { left: `${Math.max(8, Math.min(innerWidth - p.width - 8, r.left))}px`, top: `${r.top - p.height - 10}px` });
|
|
349
|
-
};
|
|
350
|
-
if (s.shot?.url) peek.append(Object.assign(document.createElement("img"), { src: s.shot.url, onload: place }));
|
|
351
|
-
peek.append(words || (s.shot ? "" : "no picture (the tab isn't shared)"));
|
|
352
|
-
peek.hidden = false;
|
|
353
|
-
place();
|
|
354
|
-
};
|
|
355
|
-
chip.onmouseleave = () => { peek.hidden = true; };
|
|
356
|
-
chip.onclick = (e) => { // the picture big, over the page; a click or Esc closes it
|
|
357
|
-
if (e.target === x || !s.shot?.url) return;
|
|
358
|
-
peek.hidden = true;
|
|
359
|
-
big.replaceChildren(Object.assign(document.createElement("img"), { src: s.shot.url }),
|
|
360
|
-
Object.assign(document.createElement("div"), { textContent: `Step ${i + 1}${words ? ` · ${words}` : ""}` }));
|
|
361
|
-
big.hidden = false;
|
|
362
|
-
};
|
|
363
|
-
strip.append(chip);
|
|
364
|
-
for (const c of s.then) strip.append(Object.assign(document.createElement("span"), { className: "doodle-click", textContent: `→ ${c.replace(/^clicked "([^"]*)".*/, "$1")}`, title: c }));
|
|
365
|
-
});
|
|
366
|
-
if (marks.length) strip.append(Object.assign(document.createElement("span"), { className: "doodle-chip now", textContent: `${marks.length} mark${marks.length > 1 ? "s" : ""}` }));
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// ---- send: Enter in the message line takes the doodles along (chat_ui's own send is for plain words) ----
|
|
370
|
-
document.addEventListener("submit", async (e) => {
|
|
371
|
-
if (e.target !== form || (!steps.length && !marks.length)) return;
|
|
372
|
-
e.preventDefault();
|
|
373
|
-
e.stopPropagation();
|
|
374
|
-
const text = input.value;
|
|
375
|
-
input.value = "";
|
|
376
|
-
if (marks.length) keep(frame());
|
|
377
|
-
else if (steps.at(-1).then.length) keep(frame()); // clicked after the last step: how the screen looks now
|
|
378
|
-
const sent = steps;
|
|
379
|
-
steps = [];
|
|
380
|
-
const before = first;
|
|
381
|
-
first = [];
|
|
382
|
-
render();
|
|
383
|
-
await Promise.all(sent.map((s) => s.ready));
|
|
384
|
-
page.send({ type: "doodle_send", to: document.getElementById("to")?.value, text, first: before,
|
|
385
|
-
steps: sent.map((s) => ({ shot: s.shot?.path || null, area: s.area, drawn: s.drawn, words: s.words, then: s.then })) });
|
|
386
|
-
allOff();
|
|
387
|
-
}, true);
|
|
388
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
# Doodles
|
|
2
|
-
The user draws on the page and clicks through it, then sends it with a few words: "[doodles on the UI]", with a
|
|
3
|
-
picture for each step.
|
|
4
|
-
- Read the steps in order, as a story; look closely at every picture: the drawing is the message.
|
|
5
|
-
- If it could mean two quite different things, propose one in a line and ask; otherwise do it.
|
|
6
|
-
Before you handle a doodle, read README.md.
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
// Doodle: draw on the page, click through it, and send the whole sequence to an agent (port of the Python doodle
|
|
2
|
-
// plugin; browser.js is its doodle.js). browser.js adds a marker, Text and an eraser to the palette: while the marker
|
|
3
|
-
// is on, a drag draws, typing writes words where the pointer is, and a click works the page as usual. A click after
|
|
4
|
-
// drawing makes the drawing a step: a picture of the screen as the user saw it (from the tab, shared once: the
|
|
5
|
-
// screen extension), cropped to the drawing. Enter in the message line sends the steps and the words typed there,
|
|
6
|
-
// together, to the agent picked in the chat (else agent1), with the pictures attached to the message itself
|
|
7
|
-
// (swarm send's images), in step order.
|
|
8
|
-
// page message doodle_send {to, text, first: [click], steps: [{shot, area, drawn, words: [str], then: [click]}]}
|
|
9
|
-
// (shot: a path from /screen/shot, or null when the tab wasn't shared; area: the part of the main
|
|
10
|
-
// space the picture shows, main px; drawn: anything drawn on it; words: what the user typed on it,
|
|
11
|
-
// exactly; clicks are words browser.js wrote)
|
|
12
|
-
// The drawing itself goes only as the picture: lines aren't put into words (a guess at what a stroke means can be
|
|
13
|
-
// wrong, and the picture shows it).
|
|
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 s = (n: number) => (n === 1 ? "" : "s");
|
|
20
|
-
|
|
21
|
-
function message(msg: any) {
|
|
22
|
-
const steps: any[] = msg.steps ?? [];
|
|
23
|
-
const shots: string[] = steps.map((st) => st.shot).filter((p) => host.api.screen?.isShot(p));
|
|
24
|
-
const words = String(msg.text ?? "").trim();
|
|
25
|
-
const lines = [`[doodles on the UI] ${words || "(no words: the drawing says it)"}`,
|
|
26
|
-
`${steps.length} step${s(steps.length)}, ${shots.length} picture${s(shots.length)} attached in this order`
|
|
27
|
-
+ (shots.length === steps.length ? "" : " (the tab wasn't shared for the others: words only)") + ":"];
|
|
28
|
-
if (msg.first?.length) lines.push(` first: ${msg.first.join("; ")}`);
|
|
29
|
-
let n = 0;
|
|
30
|
-
steps.forEach((st, i) => {
|
|
31
|
-
const has = shots.includes(st.shot), a = st.area;
|
|
32
|
-
if (has) n++;
|
|
33
|
-
lines.push(`Step ${i + 1}` + (has ? ` (picture ${n})` : " (no picture)")
|
|
34
|
-
+ (has && a ? `, showing x=${a.x} y=${a.y} w=${a.width} h=${a.height} of the page` : "")
|
|
35
|
-
+ (st.drawn ? "" : ": nothing drawn, how the screen looked after those clicks"));
|
|
36
|
-
for (const w of st.words ?? []) lines.push(` words: "${w}"`);
|
|
37
|
-
if (st.then?.length) lines.push(` then: ${st.then.join("; ")}`);
|
|
38
|
-
});
|
|
39
|
-
return { text: lines.join("\n"), shots };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export default function (pi: ExtensionAPI) {
|
|
43
|
-
const guide = fs.readFileSync(path.join(import.meta.dirname, "guide.md"), "utf8") // short; README.md: the whole of it, read when it says
|
|
44
|
-
.replace("README.md", path.join(import.meta.dirname, "README.md"));
|
|
45
|
-
pi.on("before_agent_start", (event) => { event.systemPromptOptions.sections.doodle = guide; });
|
|
46
|
-
|
|
47
|
-
host.pageHandlers.set("doodle", (msg: any) => {
|
|
48
|
-
const swarm = host.api.swarm;
|
|
49
|
-
if (msg.type !== "doodle_send" || !swarm) return;
|
|
50
|
-
const to = msg.to in swarm.status() ? msg.to : "agent1";
|
|
51
|
-
const { text, shots } = message(msg);
|
|
52
|
-
swarm.send("user", to, text, false, shots);
|
|
53
|
-
});
|
|
54
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
// Browser half of the files plugin: the file panel on the right edge (slot "side"), one name per row,
|
|
2
|
-
// collapsed and opened by its folder button (remembered in this browser). Hidden while there are no files.
|
|
3
|
-
// Files dragged from the computer onto the page: uploaded (POST /files/upload), then a "files-dropped"
|
|
4
|
-
// DOM event {x, y, files: [{name, path, size}]} at the drop spot (the point plugin opens its note box
|
|
5
|
-
// there). Dropped onto the panel itself: only stored.
|
|
6
|
-
// A row dragged out of the panel onto the page: the same event, with that file.
|
|
7
|
-
// A click on a row, or files dropped from the computer (the first of them): a "file-open" DOM event
|
|
8
|
-
// {file, row} (the viewer plugin opens a preview window attached to the row; rows carry data-path).
|
|
9
|
-
// The list comes from GET /files, then from "files" events (after an upload, and after each agent turn).
|
|
10
|
-
export default async function (page) {
|
|
11
|
-
const side = page.slot("side");
|
|
12
|
-
const style = document.createElement("style");
|
|
13
|
-
style.textContent = `
|
|
14
|
-
#side .files-toggle { width: 38px; height: 38px; display: grid; place-items: center; border-radius: 12px; color: var(--muted);
|
|
15
|
-
align-self: flex-end; margin: 16px 9px 0 0; transition: background 0.15s, color 0.15s; }
|
|
16
|
-
#side .files-toggle:hover { background: var(--hover); color: var(--text); }
|
|
17
|
-
#side[data-open] .files-toggle { background: var(--glass-strong); color: var(--text);
|
|
18
|
-
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.06), inset 0 0 0 1px var(--edge); }
|
|
19
|
-
#side .files { display: none; width: 220px; max-height: calc(100vh - 140px); overflow-y: auto; margin: 8px 9px 0 0;
|
|
20
|
-
padding: 8px; box-sizing: border-box; border-radius: var(--radius); font-size: 13px;
|
|
21
|
-
background: var(--glass-strong); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
|
|
22
|
-
border: 1px solid var(--edge); box-shadow: var(--shadow); }
|
|
23
|
-
#side[data-open] .files { display: block; }
|
|
24
|
-
#side[data-over] .files { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
25
|
-
#side .files-row { padding: 5px 8px; border-radius: 8px; color: var(--soft); cursor: grab;
|
|
26
|
-
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
27
|
-
#side .files-row:hover, #side .files-row.viewing { background: var(--hover); color: var(--text); }`;
|
|
28
|
-
document.head.append(style);
|
|
29
|
-
|
|
30
|
-
const toggle = document.createElement("button");
|
|
31
|
-
toggle.className = "files-toggle";
|
|
32
|
-
toggle.title = "Files: drop files anywhere on the page; drag a name onto the page to use it there";
|
|
33
|
-
toggle.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>`;
|
|
34
|
-
const list = document.createElement("div");
|
|
35
|
-
list.className = "files";
|
|
36
|
-
side.append(toggle, list);
|
|
37
|
-
|
|
38
|
-
const setOpen = (open) => {
|
|
39
|
-
side.toggleAttribute("data-open", open);
|
|
40
|
-
try { localStorage.setItem("files-open", open ? "1" : ""); } catch {}
|
|
41
|
-
};
|
|
42
|
-
let open = true;
|
|
43
|
-
try { open = localStorage.getItem("files-open") !== ""; } catch {}
|
|
44
|
-
setOpen(open);
|
|
45
|
-
toggle.onclick = () => setOpen(!side.hasAttribute("data-open"));
|
|
46
|
-
|
|
47
|
-
const ROW = "application/x-chalky-file"; // a row being dragged: its file as JSON
|
|
48
|
-
function render(files) {
|
|
49
|
-
list.replaceChildren();
|
|
50
|
-
side.hidden = !files.length; // no files yet: no panel (a drop from the computer still works anywhere)
|
|
51
|
-
for (const f of files) {
|
|
52
|
-
const row = Object.assign(document.createElement("div"), { className: "files-row", textContent: f.name, draggable: true });
|
|
53
|
-
row.title = `${f.path} · ${Math.ceil(f.size / 1024)} KB · click to preview`;
|
|
54
|
-
row.dataset.path = f.path;
|
|
55
|
-
row.onclick = () => opened(f); // a drag never ends in a click, so dragging a row still works
|
|
56
|
-
row.ondragstart = (e) => { e.dataTransfer.setData(ROW, JSON.stringify(f)); e.dataTransfer.effectAllowed = "copy"; };
|
|
57
|
-
list.append(row);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
page.on("files", ({ files }) => render(files));
|
|
61
|
-
try { render(await (await fetch("/files")).json()); } catch { render([]); }
|
|
62
|
-
|
|
63
|
-
const kind = (e) => {
|
|
64
|
-
const types = [...(e.dataTransfer?.types || [])];
|
|
65
|
-
return types.includes(ROW) ? "row" : types.includes("Files") ? "files" : null;
|
|
66
|
-
};
|
|
67
|
-
const onPanel = (e) => side.contains(e.target);
|
|
68
|
-
const opened = (file) => document.dispatchEvent(new CustomEvent("file-open",
|
|
69
|
-
{ detail: { file, row: list.querySelector(`[data-path="${CSS.escape(file.path)}"]`) } }));
|
|
70
|
-
const dropped = (x, y, files) => document.dispatchEvent(new CustomEvent("files-dropped", { detail: { x, y, files } }));
|
|
71
|
-
|
|
72
|
-
document.addEventListener("dragover", (e) => {
|
|
73
|
-
if (!kind(e)) return;
|
|
74
|
-
e.preventDefault();
|
|
75
|
-
side.toggleAttribute("data-over", kind(e) === "files" && onPanel(e));
|
|
76
|
-
});
|
|
77
|
-
document.addEventListener("dragleave", (e) => { if (!e.relatedTarget) side.removeAttribute("data-over"); });
|
|
78
|
-
document.addEventListener("drop", async (e) => {
|
|
79
|
-
const k = kind(e);
|
|
80
|
-
if (!k) return;
|
|
81
|
-
e.preventDefault();
|
|
82
|
-
side.removeAttribute("data-over");
|
|
83
|
-
const { clientX: x, clientY: y } = e;
|
|
84
|
-
if (k === "row") return onPanel(e) || dropped(x, y, [JSON.parse(e.dataTransfer.getData(ROW))]);
|
|
85
|
-
const stored = [];
|
|
86
|
-
for (const f of [...e.dataTransfer.files].slice(0, 10)) {
|
|
87
|
-
try {
|
|
88
|
-
const out = await (await fetch(`/files/upload?name=${encodeURIComponent(f.name)}`, { method: "POST", body: f })).json();
|
|
89
|
-
if (out.path) stored.push({ name: out.path.split("/").pop(), path: out.path, size: out.size });
|
|
90
|
-
else page.note(`Could not upload ${f.name}: ${out.error}`);
|
|
91
|
-
} catch (err) { page.note(`Could not upload ${f.name}: ${err}`); }
|
|
92
|
-
}
|
|
93
|
-
if (stored.length) opened(stored[0]); // the list has its row by now (the "files" event comes before the reply)
|
|
94
|
-
if (stored.length && !onPanel(e)) dropped(x, y, stored);
|
|
95
|
-
});
|
|
96
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# The user's files
|
|
2
|
-
{work}/drops/ holds the user's files: the ones they dropped on the page, and any you save there. The
|
|
3
|
-
user sees all of them by name in the file panel on the page's right edge.
|
|
4
|
-
- A file matters only when the user points at it (a note with "files": dropped from their computer or
|
|
5
|
-
dragged from their panel) or names it. Then read it from there (read shows you pictures).
|
|
6
|
-
- To hand the user a file (an export, a generated image, a dataset), save it in {work}/drops/: it shows
|
|
7
|
-
in their panel when your turn ends. Other work files stay in {work}.
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# Wireframes
|
|
2
|
-
The user lays out boxes (frames) on the page, writes in each what goes there, and sends them together:
|
|
3
|
-
"[frames on the UI]".
|
|
4
|
-
- Fill each frame with ui_render(surface=<the frame's id>, place=<its place>): its dashed box goes when it appears.
|
|
5
|
-
- Build a working version of what the words name, as a good one would work (an App when it keeps or calculates
|
|
6
|
-
things), never a mock with sample data. Frames sent together are one design.
|
|
7
|
-
Before you handle a frames message, read README.md.
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
// Frames: wireframes the user lays out for the agents to fill (port of the Python frames plugin; browser.js is its
|
|
2
|
-
// frames.js). browser.js adds a tool to the palette: drag boxes on the main space, type in each what goes there,
|
|
3
|
-
// then press Implement. All the frames go together, as one message, to the agent picked in the chat (else agent1),
|
|
4
|
-
// so it sees the whole layout and decides how to split the work (alone, or with helpers: README.md, not code).
|
|
5
|
-
// A frame is {x, y, width, height, text, sent, to?} (main-space px, where ui_render's place puts a surface), with an
|
|
6
|
-
// id (frame-1, frame-2, ...: never reused). An agent fills it with ui_render(surface="frame-1", place=<its place>);
|
|
7
|
-
// when that surface appears (a ui watcher, host.api.ui.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 /frames {id: frame}
|
|
10
|
-
// page messages frame_add {x, y, width, height}, frame_place {id, x, y, width, height}, frame_text {id, text},
|
|
11
|
-
// frame_remove {id}, frames_send {to}
|
|
12
|
-
// event "frames" {frames: {id: frame}} to every open page, when they changed
|
|
13
|
-
// Saved in state/frames.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.state, "frames.json");
|
|
20
|
-
if (!host.shared.has("frames"))
|
|
21
|
-
host.shared.set("frames", fs.existsSync(FILE) ? JSON.parse(fs.readFileSync(FILE, "utf8")) : { last: 0, frames: {} });
|
|
22
|
-
const saved = host.shared.get("frames"); // {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("frames", { 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
|
-
const guide = fs.readFileSync(path.join(import.meta.dirname, "guide.md"), "utf8") // short; README.md: the whole of it, read when it says
|
|
35
|
-
.replace("README.md", path.join(import.meta.dirname, "README.md"));
|
|
36
|
-
pi.on("before_agent_start", (event) => { event.systemPromptOptions.sections.frames = guide; });
|
|
37
|
-
|
|
38
|
-
// a frame is done when a surface with its id appears (after all extensions loaded: the ui one may come later)
|
|
39
|
-
pi.on("session_start", () => {
|
|
40
|
-
host.api.ui?.watch("frames", (sid: string) => {
|
|
41
|
-
if (frames[sid] && host.shared.get("ui")?.surfaces[sid]) { delete frames[sid]; changed(); }
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
host.routes.set("/frames", (_req: any, res: any) => res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(frames)));
|
|
46
|
-
|
|
47
|
-
host.pageHandlers.set("frames", (msg: any) => {
|
|
48
|
-
const f = frames[msg.id], swarm = host.api.swarm;
|
|
49
|
-
if (msg.type === "frame_add") {
|
|
50
|
-
frames[`frame-${++saved.last}`] = { ...box(msg), text: "", sent: false };
|
|
51
|
-
changed();
|
|
52
|
-
} else if (msg.type === "frame_place" && f && !f.sent) {
|
|
53
|
-
Object.assign(f, box(msg, f));
|
|
54
|
-
changed();
|
|
55
|
-
} else if (msg.type === "frame_text" && f && !f.sent) {
|
|
56
|
-
f.text = String(msg.text ?? "");
|
|
57
|
-
changed();
|
|
58
|
-
} else if (msg.type === "frame_remove" && f) {
|
|
59
|
-
delete frames[msg.id];
|
|
60
|
-
changed();
|
|
61
|
-
if (f.sent) swarm?.send("user", f.to, `[frames on the UI] the user removed ${msg.id} ("${f.text}"): don't fill it.`, true);
|
|
62
|
-
} else if (msg.type === "frames_send" && swarm) {
|
|
63
|
-
const to = msg.to in swarm.status() ? msg.to : "agent1";
|
|
64
|
-
const ready = Object.entries(frames).filter(([, f]: any) => !f.sent && f.text.trim());
|
|
65
|
-
if (!ready.length) return;
|
|
66
|
-
for (const [, f] of ready as any) Object.assign(f, { sent: true, to });
|
|
67
|
-
changed();
|
|
68
|
-
const lines = [`[frames on the UI] the user laid out ${ready.length} frame${ready.length > 1 ? "s" : ""} to fill:`,
|
|
69
|
-
...ready.map(([id, f]: any) => ` ${id}: "${f.text}" · place=${place(f)}`),
|
|
70
|
-
" Fill each with ui_render(surface=<its id>, place=<its place>, ...); its dashed box goes when it appears."];
|
|
71
|
-
const waiting = Object.keys(frames).filter((id) => frames[id].sent && !ready.some(([r]) => r === id));
|
|
72
|
-
if (waiting.length) lines.push(` Sent before and not filled yet: ${waiting.join(", ")}.`);
|
|
73
|
-
swarm.send("user", to, lines.join("\n"));
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
}
|