@chalky/chalky 0.1.4 → 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 -5
- package/{app → agent}/SYSTEM.md +15 -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/{app/extensions/boards → agent/extensions/board}/browser.js +25 -23
- 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 +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 +20 -18
- package/agent/extensions/frame/guide.md +3 -0
- package/{app/extensions/frames → agent/extensions/frame}/index.ts +27 -31
- 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/{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 +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/{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 +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 +2 -2
- package/package.json +8 -8
- package/server.ts +151 -58
- 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/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/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/{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,398 +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
|
-
// boards going to another board (boards/) is like a click: the drawing so far becomes a step, then "went to
|
|
17
|
-
// board N". Each step says the board it was drawn on.
|
|
18
|
-
// Things with a drag of their own keep it: a surface's label (moves it), its resize corner, lists to pick from,
|
|
19
|
-
// sliders, draggable things, frames; text boxes don't (a drag that starts on one draws). Ctrl+Z takes back
|
|
20
|
-
// the last mark; Esc turns the tools off (ink stays).
|
|
21
|
-
// Pictures: the tab is shared once (the screen extension), when the marker or Text is first turned on; without
|
|
22
|
-
// 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.
|
|
23
|
-
// Ink and words live in the main space (slot "top"), in main-space px, so they scroll with the surfaces.
|
|
24
|
-
export default async function (page) {
|
|
25
|
-
const area = page.slot("top");
|
|
26
|
-
const form = document.getElementById("f"), input = document.getElementById("text");
|
|
27
|
-
const SVG = "http://www.w3.org/2000/svg", INK = "#e5397a";
|
|
28
|
-
// The cursor: a marker (white edge, so it shows on dark too); its tip (3, 21) is where the line starts.
|
|
29
|
-
const PEN = `url("data:image/svg+xml,${encodeURIComponent(
|
|
30
|
-
`<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" ` +
|
|
31
|
-
`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`;
|
|
32
|
-
const RUB = `url("data:image/svg+xml,${encodeURIComponent( // the eraser cursor; its bottom-left edge (5, 20) rubs
|
|
33
|
-
`<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` +
|
|
34
|
-
`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"/>` +
|
|
35
|
-
`<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`;
|
|
36
|
-
const CURSORS = `
|
|
37
|
-
.doodling, .doodling * { cursor: ${PEN}; }
|
|
38
|
-
.doodling :is(button, a, select, summary, label, [onclick], [role="button"], [role="tab"], .surface-label) { cursor: pointer; }
|
|
39
|
-
.doodling :is(input, textarea, [contenteditable="true"]) { cursor: text; }
|
|
40
|
-
.doodling .surface-resize { cursor: nwse-resize; }
|
|
41
|
-
.doodle-text:not(.doodling), .doodle-text:not(.doodling) * { cursor: text !important; }
|
|
42
|
-
.doodle-erase, .doodle-erase * { cursor: ${RUB} !important; }`;
|
|
43
|
-
const style = document.createElement("style");
|
|
44
|
-
style.textContent = CURSORS + `
|
|
45
|
-
#top:is(.doodling, .doodle-text, .doodle-erase) { z-index: 11; } /* above the messages panel (10), so lines can go there too */
|
|
46
|
-
#top > svg.doodle-ink { position: absolute; left: 0; top: 0; width: 1px; height: 1px; overflow: visible; z-index: 100001; pointer-events: none; }
|
|
47
|
-
#top > svg.doodle-ink path { fill: none; stroke: ${INK}; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
|
|
48
|
-
#top > .doodle-note { position: absolute; z-index: 100001; font: 20px/1.2 "Segoe Print", "Bradley Hand", "Comic Sans MS", cursive; color: ${INK};
|
|
49
|
-
white-space: pre; outline: none; pointer-events: none; padding: 0 4px; min-width: 8px; }
|
|
50
|
-
#top > .doodle-note:empty::before { content: "type here…"; opacity: .5; }
|
|
51
|
-
#top > .doodle-note[contenteditable="true"] { pointer-events: auto; background: rgba(229, 57, 122, .1); border-radius: 6px; caret-color: ${INK}; }
|
|
52
|
-
#f .doodle-strip { display: flex; align-items: center; gap: 4px; flex: none; max-width: 45%; overflow-x: auto; }
|
|
53
|
-
#f .doodle-strip:empty { display: none; }
|
|
54
|
-
#f .doodle-chip { position: relative; flex: none; min-width: 24px; height: 24px; padding: 0 7px; box-sizing: border-box; border-radius: 7px;
|
|
55
|
-
display: grid; place-items: center; font-size: 12px; font-weight: 600; color: ${INK}; border: 1.5px solid ${INK};
|
|
56
|
-
background: rgba(229, 57, 122, .08); cursor: default; }
|
|
57
|
-
#f .doodle-chip.now { border-style: dashed; background: none; font-weight: 500; }
|
|
58
|
-
#f .doodle-chip .x { position: absolute; right: -6px; top: -6px; width: 14px; height: 14px; border-radius: 50%; background: var(--text);
|
|
59
|
-
color: #fff; font-size: 9px; line-height: 14px; text-align: center; display: none; cursor: pointer; }
|
|
60
|
-
#f .doodle-chip:hover .x { display: block; }
|
|
61
|
-
#f .doodle-click { flex: none; font-size: 12px; color: var(--muted); white-space: nowrap; max-width: 120px; overflow: hidden; text-overflow: ellipsis; }
|
|
62
|
-
.doodle-peek { position: fixed; z-index: 100; max-width: 360px; padding: 6px; border-radius: 12px; background: var(--glass-strong, #fff);
|
|
63
|
-
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; }
|
|
64
|
-
.doodle-peek img { display: block; max-width: 100%; max-height: 220px; border-radius: 6px; margin-bottom: 4px; }
|
|
65
|
-
#f .doodle-chip:not(.now) { cursor: zoom-in; }
|
|
66
|
-
.doodle-big { position: fixed; inset: 0; z-index: 200; display: grid; place-content: center; justify-items: center; gap: 10px;
|
|
67
|
-
background: rgba(0, 0, 0, .55); cursor: zoom-out; color: #fff; font-size: 13px; }
|
|
68
|
-
.doodle-big div { padding: 4px 12px; border-radius: 999px; background: rgba(0, 0, 0, .6); }
|
|
69
|
-
.doodle-big img { max-width: 92vw; max-height: 84vh; border-radius: 10px; box-shadow: 0 20px 60px rgba(0, 0, 0, .4); background: #fff; }`;
|
|
70
|
-
document.head.append(style);
|
|
71
|
-
|
|
72
|
-
const button = document.createElement("button");
|
|
73
|
-
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>`;
|
|
74
|
-
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)";
|
|
75
|
-
const textButton = document.createElement("button");
|
|
76
|
-
textButton.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7V5h14v2M12 5v14M9 19h6"/></svg>`;
|
|
77
|
-
textButton.title = "Text: click where the words go (Enter to finish); sent with the doodles, Enter in the message line (Esc to stop)";
|
|
78
|
-
const eraseButton = document.createElement("button");
|
|
79
|
-
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` +
|
|
80
|
-
`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>`;
|
|
81
|
-
eraseButton.title = "Eraser: press or drag over lines, words or unsent frames to take them away (Esc to stop)";
|
|
82
|
-
page.slot("status").append(button, textButton, eraseButton);
|
|
83
|
-
const strip = Object.assign(document.createElement("div"), { className: "doodle-strip" });
|
|
84
|
-
form.insertBefore(strip, input);
|
|
85
|
-
const peek = Object.assign(document.createElement("div"), { className: "doodle-peek", hidden: true });
|
|
86
|
-
const big = Object.assign(document.createElement("div"), { className: "doodle-big", hidden: true }); // a step's picture, big
|
|
87
|
-
big.onclick = () => { big.hidden = true; };
|
|
88
|
-
document.addEventListener("keydown", (e) => { if (e.key === "Escape") big.hidden = true; });
|
|
89
|
-
document.body.append(peek, big);
|
|
90
|
-
|
|
91
|
-
let drawing = false, texting = false, erasing = false, on = false; // which tools are on; on: any of them
|
|
92
|
-
const docs = new Set([document]); // this page and every App frame's page: they all get the cursor
|
|
93
|
-
const classes = (el) => {
|
|
94
|
-
el.classList.toggle("doodling", drawing);
|
|
95
|
-
el.classList.toggle("doodle-text", texting);
|
|
96
|
-
el.classList.toggle("doodle-erase", erasing);
|
|
97
|
-
};
|
|
98
|
-
const set = (tool, v) => { // tool: "doodle" | "text" | "erase"
|
|
99
|
-
if (tool === "doodle") drawing = v; else if (tool === "text") texting = v; else erasing = v;
|
|
100
|
-
on = drawing || texting || erasing;
|
|
101
|
-
button.classList.toggle("on", drawing);
|
|
102
|
-
textButton.classList.toggle("on", texting);
|
|
103
|
-
eraseButton.classList.toggle("on", erasing);
|
|
104
|
-
classes(area);
|
|
105
|
-
for (const d of docs) if (d !== document && d.documentElement) classes(d.documentElement);
|
|
106
|
-
if (v) {
|
|
107
|
-
if (tool !== "erase") share();
|
|
108
|
-
document.dispatchEvent(new CustomEvent("tool-on", { detail: tool }));
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
const allOff = () => { for (const t of ["doodle", "text", "erase"]) set(t, false); };
|
|
112
|
-
button.onclick = () => set("doodle", !drawing);
|
|
113
|
-
textButton.onclick = () => set("text", !texting);
|
|
114
|
-
eraseButton.onclick = () => set("erase", !erasing);
|
|
115
|
-
document.addEventListener("tool-on", (e) => { // one tool at a time, except the marker and Text, which work together
|
|
116
|
-
const t = e.detail, pair = t === "doodle" || t === "text";
|
|
117
|
-
if (drawing && t !== "doodle" && !pair) set("doodle", false);
|
|
118
|
-
if (texting && t !== "text" && !pair) set("text", false);
|
|
119
|
-
if (erasing && t !== "erase") set("erase", false);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// ---- pictures: the tab, shared once by the screen plugin (none without it: steps go as words) ----
|
|
123
|
-
let screen = null;
|
|
124
|
-
page.use("screen").then((s) => { screen = s; });
|
|
125
|
-
const share = () => screen?.share();
|
|
126
|
-
const frame = () => screen?.frame() ?? null; // the screen right now, as a canvas (null without a shared tab)
|
|
127
|
-
const picture = (full, box) => screen.picture(full, box); // crop a frame to box (window px) and upload it: {path, url}
|
|
128
|
-
|
|
129
|
-
// ---- where things are ----
|
|
130
|
-
const inMain = (x, y) => { // window px -> main-space px (as point.js)
|
|
131
|
-
const m = area.getBoundingClientRect();
|
|
132
|
-
return { x: x - m.left + area.scrollLeft, y: y - m.top + area.scrollTop };
|
|
133
|
-
};
|
|
134
|
-
const inWindow = (p) => {
|
|
135
|
-
const m = area.getBoundingClientRect();
|
|
136
|
-
return { x: p.x + m.left - area.scrollLeft, y: p.y + m.top - area.scrollTop };
|
|
137
|
-
};
|
|
138
|
-
const short = (s, n = 40) => { s = (s || "").replace(/\s+/g, " ").trim(); return s.length > n ? s.slice(0, n - 1) + "…" : s; };
|
|
139
|
-
|
|
140
|
-
// ---- marks: lines and words ----
|
|
141
|
-
const ink = document.createElementNS(SVG, "svg"); // in the main space only while it has marks (#top:empty shows the welcome)
|
|
142
|
-
ink.classList.add("doodle-ink");
|
|
143
|
-
let marks = []; // [{el, points (main px), text (words only)}], oldest first
|
|
144
|
-
const show = () => { if (marks.some((m) => m.el.localName === "path" || m.el.localName === "g")) area.append(ink); else ink.remove(); render(); };
|
|
145
|
-
const f = (n) => n.toFixed(1);
|
|
146
|
-
const smooth = (pts) => { // through the midpoints, so a quick line isn't a row of corners
|
|
147
|
-
let d = `M${f(pts[0].x)} ${f(pts[0].y)}`;
|
|
148
|
-
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)}`;
|
|
149
|
-
return d + ` L${f(pts.at(-1).x)} ${f(pts.at(-1).y)}`;
|
|
150
|
-
};
|
|
151
|
-
function note(p, first) { // words in handwriting at p (main px)
|
|
152
|
-
const n = Object.assign(document.createElement("div"), { className: "doodle-note", contentEditable: "true", textContent: first });
|
|
153
|
-
Object.assign(n.style, { left: `${p.x}px`, top: `${p.y - 14}px` });
|
|
154
|
-
area.append(n);
|
|
155
|
-
n.focus();
|
|
156
|
-
getSelection().selectAllChildren(n);
|
|
157
|
-
getSelection().collapseToEnd();
|
|
158
|
-
let done = false;
|
|
159
|
-
const finish = () => {
|
|
160
|
-
if (done) return;
|
|
161
|
-
done = true;
|
|
162
|
-
n.contentEditable = "false";
|
|
163
|
-
const t = n.textContent.trim();
|
|
164
|
-
if (!t) { n.remove(); return; }
|
|
165
|
-
marks.push({ el: n, points: [p, { x: p.x + n.offsetWidth, y: p.y + n.offsetHeight - 14 }], text: t });
|
|
166
|
-
show();
|
|
167
|
-
};
|
|
168
|
-
n.onkeydown = (e) => {
|
|
169
|
-
e.stopPropagation();
|
|
170
|
-
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); n.blur(); }
|
|
171
|
-
if (e.key === "Escape") { n.textContent = ""; n.blur(); }
|
|
172
|
-
};
|
|
173
|
-
n.onblur = finish;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ---- erasing: a mark goes whole when the eraser passes over it ----
|
|
177
|
-
function eraseAt(p) { // p: main px
|
|
178
|
-
const left = marks.filter((m) => {
|
|
179
|
-
if (m.text) { // words: their box
|
|
180
|
-
const x = m.el.offsetLeft, y = m.el.offsetTop;
|
|
181
|
-
return !(p.x >= x - 6 && p.x <= x + m.el.offsetWidth + 6 && p.y >= y - 6 && p.y <= y + m.el.offsetHeight + 6);
|
|
182
|
-
}
|
|
183
|
-
return !m.points.some((q) => Math.hypot(q.x - p.x, q.y - p.y) < 12); // a line: near any of its points
|
|
184
|
-
});
|
|
185
|
-
if (left.length !== marks.length) {
|
|
186
|
-
for (const m of marks) if (!left.includes(m)) m.el.remove();
|
|
187
|
-
marks = left;
|
|
188
|
-
show();
|
|
189
|
-
}
|
|
190
|
-
document.dispatchEvent(new CustomEvent("erase", { detail: p })); // other tools' things (frames: unsent frames)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ---- steps ----
|
|
194
|
-
let steps = [], first = []; // steps: [{marks: [text], then: [click], shot: {path, url} | null, ready}]
|
|
195
|
-
let grabbed = null; // the screen when the last button went down (a click may end a step with it)
|
|
196
|
-
function bounds(pts) { // window px, with room around the drawing, inside the window
|
|
197
|
-
const w = pts.map(inWindow), pad = 60;
|
|
198
|
-
let x0 = Math.min(...w.map((p) => p.x)) - pad, x1 = Math.max(...w.map((p) => p.x)) + pad;
|
|
199
|
-
let y0 = Math.min(...w.map((p) => p.y)) - pad, y1 = Math.max(...w.map((p) => p.y)) + pad;
|
|
200
|
-
if (x1 - x0 < 480) { const c = (x0 + x1) / 2; x0 = c - 240; x1 = c + 240; }
|
|
201
|
-
if (y1 - y0 < 300) { const c = (y0 + y1) / 2; y0 = c - 150; y1 = c + 150; }
|
|
202
|
-
x0 = Math.max(0, x0); y0 = Math.max(0, y0); x1 = Math.min(innerWidth, x1); y1 = Math.min(innerHeight, y1);
|
|
203
|
-
return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
|
|
204
|
-
}
|
|
205
|
-
function keep(full) { // the marks on the page become a step (full: the screen to crop its picture from)
|
|
206
|
-
const box = marks.length ? bounds(marks.flatMap((m) => m.points)) : { x: 0, y: 0, w: innerWidth, h: innerHeight };
|
|
207
|
-
const at = inMain(box.x, box.y); // the part of the main space the picture shows, so the agent can place things
|
|
208
|
-
const step = { drawn: marks.length > 0, words: marks.filter((m) => m.text).map((m) => m.text), then: [], shot: null,
|
|
209
|
-
board: Number(document.body.dataset.board) || 1, // the board shown (boards/)
|
|
210
|
-
area: { x: Math.round(at.x), y: Math.round(at.y), width: Math.round(box.w), height: Math.round(box.h) } };
|
|
211
|
-
step.ready = full ? picture(full, box).then((s) => { step.shot = s; }, () => {}) : Promise.resolve();
|
|
212
|
-
steps.push(step);
|
|
213
|
-
for (const m of marks) m.el.remove();
|
|
214
|
-
marks = [];
|
|
215
|
-
show();
|
|
216
|
-
}
|
|
217
|
-
// going to another board (boards/, just before it changes) is like a click: the drawing so far becomes a step
|
|
218
|
-
document.addEventListener("board", (e) => {
|
|
219
|
-
if (!marks.length && !steps.length && !first.length) return; // no doodling going on
|
|
220
|
-
if (marks.length) keep(frame());
|
|
221
|
-
(steps.length ? steps.at(-1).then : first).push(`went to board ${e.detail.to}`);
|
|
222
|
-
render();
|
|
223
|
-
});
|
|
224
|
-
function clicked(t, surface) { // a click, as words
|
|
225
|
-
const label = short(t.getAttribute("aria-label") || t.title || t.textContent || t.value || t.localName, 30);
|
|
226
|
-
const row = t.closest("tr, li");
|
|
227
|
-
const rowText = row && row !== t ? short((row.innerText || row.textContent).replace(t.innerText || t.textContent, ""), 30) : "";
|
|
228
|
-
return `clicked "${label}"${rowText ? ` on the "${rowText}" row` : ""}${surface ? ` (surface ${surface})` : ""}`;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// ---- presses: this page and every App frame's page ----
|
|
232
|
-
// Text boxes draw too (a click still focuses them, a double click still selects a word): circling a notes box
|
|
233
|
-
// is a common thing to draw.
|
|
234
|
-
const OWN_DRAG = ".surface-label, .surface-resize, select, input[type='range'], [draggable='true'], .frame, .doodle-note";
|
|
235
|
-
const CLICKABLE = "button, a, summary, select, label, [onclick], [role='button'], [role='tab'], [role='checkbox'], input[type='checkbox'], input[type='radio']";
|
|
236
|
-
let press = null, line = null, swallow = false, over = null; // over: the pointer, in window px, while it is over the page
|
|
237
|
-
function hook(win, frameEl) {
|
|
238
|
-
const doc = win.document;
|
|
239
|
-
if (frameEl) {
|
|
240
|
-
if (docs.has(doc)) return;
|
|
241
|
-
docs.add(doc);
|
|
242
|
-
const s = doc.createElement("style");
|
|
243
|
-
s.textContent = CURSORS;
|
|
244
|
-
doc.head.append(s);
|
|
245
|
-
classes(doc.documentElement);
|
|
246
|
-
}
|
|
247
|
-
const offset = () => {
|
|
248
|
-
if (!frameEl) return { x: 0, y: 0 };
|
|
249
|
-
const r = frameEl.getBoundingClientRect();
|
|
250
|
-
return { x: r.left + frameEl.clientLeft, y: r.top + frameEl.clientTop };
|
|
251
|
-
};
|
|
252
|
-
const root = frameEl ? doc : area;
|
|
253
|
-
root.addEventListener("pointerdown", (e) => {
|
|
254
|
-
if (!on || e.button !== 0) return;
|
|
255
|
-
const o = offset();
|
|
256
|
-
if (erasing) { // the press is for the eraser, not for what's under it
|
|
257
|
-
e.preventDefault();
|
|
258
|
-
press = { erase: true, win };
|
|
259
|
-
eraseAt(inMain(e.clientX + o.x, e.clientY + o.y));
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
grabbed = marks.length ? frame() : null; // before the click acts
|
|
263
|
-
if (e.target.closest?.(texting && !drawing ? ".doodle-note" : OWN_DRAG)) return;
|
|
264
|
-
if (texting) e.preventDefault(); // the press is for the words (or a line), not for what's under it
|
|
265
|
-
press = { x: e.clientX + o.x, y: e.clientY + o.y, id: e.pointerId, target: e.target, win, o, text: texting };
|
|
266
|
-
}, true);
|
|
267
|
-
win.addEventListener("pointermove", (e) => {
|
|
268
|
-
const o = offset(), x = e.clientX + o.x, y = e.clientY + o.y;
|
|
269
|
-
over = frameEl || e.target.closest?.("#top") ? { x, y } : null;
|
|
270
|
-
if (press?.erase && press.win === win) { win.getSelection().removeAllRanges(); eraseAt(inMain(x, y)); return; }
|
|
271
|
-
if (!press || press.win !== win || !drawing) return;
|
|
272
|
-
if (!line) {
|
|
273
|
-
if (Math.hypot(x - press.x, y - press.y) < 6) return; // not yet a line: may still be a click
|
|
274
|
-
line = { el: document.createElementNS(SVG, "g"), points: [inMain(press.x, press.y)] };
|
|
275
|
-
line.el.append(document.createElementNS(SVG, "path"));
|
|
276
|
-
ink.append(line.el);
|
|
277
|
-
marks.push(line);
|
|
278
|
-
show();
|
|
279
|
-
try { press.target.setPointerCapture(press.id); } catch {}
|
|
280
|
-
const active = press.win.document.activeElement; // a text box the press focused: typing now writes words, not into it
|
|
281
|
-
if (active?.matches?.("input, textarea, [contenteditable='true']")) active.blur();
|
|
282
|
-
}
|
|
283
|
-
win.getSelection().removeAllRanges(); // the first few px may have started a text selection
|
|
284
|
-
const p = inMain(x, y), last = line.points.at(-1);
|
|
285
|
-
if (Math.hypot(p.x - last.x, p.y - last.y) < 1.5) return;
|
|
286
|
-
line.points.push(p);
|
|
287
|
-
line.el.firstChild.setAttribute("d", smooth(line.points));
|
|
288
|
-
});
|
|
289
|
-
win.addEventListener("pointerup", () => {
|
|
290
|
-
if (press?.erase) { swallow = true; setTimeout(() => { swallow = false; }); } // the eraser's press isn't a click
|
|
291
|
-
if (press?.text && !line) { // Text: words where the press was (a press that drew a line was for the marker)
|
|
292
|
-
note(inMain(press.x, press.y), "");
|
|
293
|
-
swallow = true;
|
|
294
|
-
setTimeout(() => { swallow = false; });
|
|
295
|
-
}
|
|
296
|
-
if (line) {
|
|
297
|
-
if (line.points.length < 3) { marks.pop(); line.el.remove(); }
|
|
298
|
-
swallow = true; // the click that ends a line isn't a click
|
|
299
|
-
setTimeout(() => { swallow = false; });
|
|
300
|
-
show();
|
|
301
|
-
}
|
|
302
|
-
press = line = null;
|
|
303
|
-
});
|
|
304
|
-
root.addEventListener("click", (e) => {
|
|
305
|
-
if (swallow) { e.stopPropagation(); e.preventDefault(); swallow = false; return; }
|
|
306
|
-
const t = on && e.target.closest?.(CLICKABLE);
|
|
307
|
-
if (!t || t.closest(".doodle-note")) return;
|
|
308
|
-
if (marks.length) keep(grabbed); // the drawing so far, as the screen was when the button went down
|
|
309
|
-
(steps.length ? steps.at(-1).then : first).push(clicked(t, (frameEl || t).closest("[data-surface]")?.dataset.surface));
|
|
310
|
-
render();
|
|
311
|
-
}, true);
|
|
312
|
-
root.addEventListener("dragstart", (e) => { if (on) e.preventDefault(); }, true); // links and pictures don't get dragged away
|
|
313
|
-
doc.addEventListener("keydown", onKey);
|
|
314
|
-
}
|
|
315
|
-
hook(window, null);
|
|
316
|
-
const hookFrame = (fr) => {
|
|
317
|
-
const go = () => { try { if (fr.contentDocument?.readyState !== "loading") hook(fr.contentWindow, fr); } catch {} }; // other origins: not ours
|
|
318
|
-
fr.addEventListener("load", go);
|
|
319
|
-
go();
|
|
320
|
-
};
|
|
321
|
-
area.querySelectorAll("iframe").forEach(hookFrame);
|
|
322
|
-
new MutationObserver((list) => {
|
|
323
|
-
for (const r of list) for (const n of r.addedNodes) {
|
|
324
|
-
if (n.localName === "iframe") hookFrame(n);
|
|
325
|
-
else n.querySelectorAll?.("iframe").forEach(hookFrame);
|
|
326
|
-
}
|
|
327
|
-
}).observe(area, { childList: true, subtree: true });
|
|
328
|
-
|
|
329
|
-
function onKey(e) {
|
|
330
|
-
if (e.target.closest?.("input, textarea, select, [contenteditable='true']")) return;
|
|
331
|
-
if (e.key === "Escape" && on) { allOff(); return; }
|
|
332
|
-
if (!on || erasing) return;
|
|
333
|
-
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z") {
|
|
334
|
-
const m = marks.pop();
|
|
335
|
-
if (m) { m.el.remove(); show(); e.preventDefault(); }
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
if (e.key.length === 1 && e.key !== " " && !e.ctrlKey && !e.metaKey && !e.altKey && over) {
|
|
339
|
-
e.preventDefault();
|
|
340
|
-
note(inMain(over.x, over.y), e.key);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// ---- the strip in the message line ----
|
|
345
|
-
function render() {
|
|
346
|
-
strip.replaceChildren();
|
|
347
|
-
steps.forEach((s, i) => {
|
|
348
|
-
const chip = Object.assign(document.createElement("span"), { className: "doodle-chip", textContent: i + 1 });
|
|
349
|
-
const x = Object.assign(document.createElement("span"), { className: "x", textContent: "✕", title: "Drop this step" });
|
|
350
|
-
x.onclick = () => { peek.hidden = true; steps.splice(i, 1); render(); };
|
|
351
|
-
chip.append(x);
|
|
352
|
-
const words = s.words.map((w) => `"${w}"`).join(" ");
|
|
353
|
-
chip.title = s.shot ? "Click to see it big" : "No picture: the tab isn't shared";
|
|
354
|
-
chip.onmouseenter = () => { // a small preview above the chip
|
|
355
|
-
peek.replaceChildren();
|
|
356
|
-
const place = () => {
|
|
357
|
-
const r = chip.getBoundingClientRect(), p = peek.getBoundingClientRect();
|
|
358
|
-
Object.assign(peek.style, { left: `${Math.max(8, Math.min(innerWidth - p.width - 8, r.left))}px`, top: `${r.top - p.height - 10}px` });
|
|
359
|
-
};
|
|
360
|
-
if (s.shot?.url) peek.append(Object.assign(document.createElement("img"), { src: s.shot.url, onload: place }));
|
|
361
|
-
peek.append(words || (s.shot ? "" : "no picture (the tab isn't shared)"));
|
|
362
|
-
peek.hidden = false;
|
|
363
|
-
place();
|
|
364
|
-
};
|
|
365
|
-
chip.onmouseleave = () => { peek.hidden = true; };
|
|
366
|
-
chip.onclick = (e) => { // the picture big, over the page; a click or Esc closes it
|
|
367
|
-
if (e.target === x || !s.shot?.url) return;
|
|
368
|
-
peek.hidden = true;
|
|
369
|
-
big.replaceChildren(Object.assign(document.createElement("img"), { src: s.shot.url }),
|
|
370
|
-
Object.assign(document.createElement("div"), { textContent: `Step ${i + 1}${words ? ` · ${words}` : ""}` }));
|
|
371
|
-
big.hidden = false;
|
|
372
|
-
};
|
|
373
|
-
strip.append(chip);
|
|
374
|
-
for (const c of s.then) strip.append(Object.assign(document.createElement("span"), { className: "doodle-click", textContent: `→ ${c.replace(/^clicked "([^"]*)".*/, "$1")}`, title: c }));
|
|
375
|
-
});
|
|
376
|
-
if (marks.length) strip.append(Object.assign(document.createElement("span"), { className: "doodle-chip now", textContent: `${marks.length} mark${marks.length > 1 ? "s" : ""}` }));
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// ---- send: Enter in the message line takes the doodles along (chat_ui's own send is for plain words) ----
|
|
380
|
-
document.addEventListener("submit", async (e) => {
|
|
381
|
-
if (e.target !== form || (!steps.length && !marks.length)) return;
|
|
382
|
-
e.preventDefault();
|
|
383
|
-
e.stopPropagation();
|
|
384
|
-
const text = input.value;
|
|
385
|
-
input.value = "";
|
|
386
|
-
if (marks.length) keep(frame());
|
|
387
|
-
else if (steps.at(-1).then.length) keep(frame()); // clicked after the last step: how the screen looks now
|
|
388
|
-
const sent = steps;
|
|
389
|
-
steps = [];
|
|
390
|
-
const before = first;
|
|
391
|
-
first = [];
|
|
392
|
-
render();
|
|
393
|
-
await Promise.all(sent.map((s) => s.ready));
|
|
394
|
-
page.send({ type: "doodle_send", to: document.getElementById("to")?.value, text, first: before,
|
|
395
|
-
steps: sent.map((s) => ({ shot: s.shot?.path || null, area: s.area, drawn: s.drawn, words: s.words, then: s.then, board: s.board })) });
|
|
396
|
-
allOff();
|
|
397
|
-
}, true);
|
|
398
|
-
}
|
|
@@ -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}` + (st.board ? ` on board ${st.board}` : "") + (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,5 +0,0 @@
|
|
|
1
|
-
# MCP servers
|
|
2
|
-
Servers are in {code}/mcp.json (pi's built-in MCP: pi's docs/mcp.md); their tools are named mcp__<server>__<tool>.
|
|
3
|
-
The user ticks which servers and tools are on with /mcp on the page.
|
|
4
|
-
- To add a server when asked: add its entry to that file (secrets as ${ENV_VAR} in headers or env, never written
|
|
5
|
-
out), then call reload.
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# Notes on the UI (the point extension)
|
|
2
|
-
The user can click anything on the page and write a note there. You get "[note on the UI] <their words>"
|
|
3
|
-
with the target: surface and component (from ui_render), the element they clicked (inside App frames
|
|
4
|
-
too), the part (if the thing has partAt), its text, and the file it comes from. Act on that exact thing.
|
|
5
|
-
- If the text is data (a table row, a score), change the surface's data, not the component's code.
|
|
6
|
-
- "here" means the spot: to add something there, ui_render(..., place={"x", "y"}) with its position, and keep
|
|
7
|
-
it small (as wide as its content). On empty space the spot is all there is to the target.
|
|
8
|
-
- A note with an "area" came with a drawing: the user circled that box. On empty space, build what they ask
|
|
9
|
-
for inside it: ui_render(..., place={"x", "y", "width", "height"}) from the area. On a surface, the drawing
|
|
10
|
-
marks the part of it the note is about: change that part, and leave the surface's place and size alone.
|
|
11
|
-
If the thing drawn on can say what is inside the drawing (partIn), the part lists exactly what was circled
|
|
12
|
-
(cells and values, paragraphs, lines, a picture of the area): the note is about that.
|
|
13
|
-
- A note with "files": the user dropped those files there, from their computer or their file panel (all
|
|
14
|
-
in {work}/drops/). Look at them (read shows you pictures) and do what the note says with them, at
|
|
15
|
-
that spot or on that surface.
|
|
16
|
-
- "[combine on the UI]": the user dragged surface A onto surface B and said how to combine them. Change B
|
|
17
|
-
so it takes A in, as they said: ui_render(surface=B, ..., made_from=<B's made_from plus A>,
|
|
18
|
-
instruction=<their words>, about=<updated>). Read A's data with {"ref": "A", "path": "/..."} props, so it
|
|
19
|
-
stays live; copy only what has to change shape. Never change or delete A: the user deletes it when they
|
|
20
|
-
want (B then gets a copy of what it read, automatically). Keep what B already showed unless they said otherwise.
|
|
21
|
-
- Pointing is only as precise as your names: give elements you build a clear id or data-name.
|
|
22
|
-
- Each note has an id. When you have dealt with it, call note_done(id): its pin fades from the page.
|