@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,271 +0,0 @@
|
|
|
1
|
-
// UI surfaces from the page's components (port of the Python ui plugin; browser.js, the page side, is its ui.js).
|
|
2
|
-
// Tools ui_catalog, ui_render, ui_update, ui_delete; guide.md, a short prompt section (README.md, copied from ui.py: the whole of it).
|
|
3
|
-
// The surfaces are shared by every agent: one Surfaces object on the host, made by whichever agent loads first.
|
|
4
|
-
// One door: every change goes through change(): it saves, tells every open page, and calls every watcher with the
|
|
5
|
-
// surface, who did it and the words for it ("you moved s1"; none: part of a bigger change). restore() puts back a
|
|
6
|
-
// saved state (not a change: no watchers). Watchers (Python: the "ui_changed" event), registered from session_start:
|
|
7
|
-
// host.api.ui.watch(name, fn(surface, by, what)) name: the extension's, so its reload replaces its own
|
|
8
|
-
// (ui_history: a step per change with words; frames: a frame is done when its surface appears).
|
|
9
|
-
// app/ui/surfaces.json design (everything but data) state/ui-data.json data (what users typed)
|
|
10
|
-
// In this folder: components/*.js (the catalog), agents.js and voice.js (what apps import), package.json and
|
|
11
|
-
// node_modules/ (browser libraries components import), skills/ (ui-design, ui-check, agent-apps: given to pi).
|
|
12
|
-
// GET /ui/apps/<name>/... an app (app/ui/apps: what the agents built)
|
|
13
|
-
// GET /ui/components/..., /ui/agents.js, /ui/voice.js from this folder; /ui/node_modules/{marked,dompurify}/... the host's
|
|
14
|
-
// GET /ui-api/catalog (name -> mtime), /ui-api/about (name -> about block), /ui-api/surfaces
|
|
15
|
-
// App messages and app_reset: swarm/.
|
|
16
|
-
import fs from "node:fs";
|
|
17
|
-
import path from "node:path";
|
|
18
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
-
import { Type } from "typebox";
|
|
20
|
-
|
|
21
|
-
const host = (globalThis as any).chalky;
|
|
22
|
-
const DESIGN = path.join(host.app, "ui/surfaces.json");
|
|
23
|
-
const APPS = path.join(host.app, "ui/apps");
|
|
24
|
-
const COMPONENTS = path.join(import.meta.dirname, "components");
|
|
25
|
-
const SERVED = ["components/", "agents.js", "voice.js"]; // what /ui/ serves from this folder
|
|
26
|
-
const LIBS = ["marked/", "dompurify/"]; // the page's browser libraries, from the host's node_modules (its package.json)
|
|
27
|
-
const DATA = path.join(host.state, "ui-data.json");
|
|
28
|
-
const read = (p: string, d: any) => (fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, "utf8")) : d);
|
|
29
|
-
const clone = (v: any) => structuredClone(v);
|
|
30
|
-
const isRef = (v: any) => v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).sort().join() === "path,ref";
|
|
31
|
-
const propsOf = (c: any) => Object.fromEntries(Object.entries(c).filter(([k]) => !["id", "component", "children", "child"].includes(k)));
|
|
32
|
-
|
|
33
|
-
function getPath(data: any, p: string) {
|
|
34
|
-
for (const k of p.split("/").filter(Boolean)) data = data?.[Array.isArray(data) ? Number(k) : k];
|
|
35
|
-
return data;
|
|
36
|
-
}
|
|
37
|
-
function setPath(data: any, p: string, value: any) {
|
|
38
|
-
const keys = p.split("/").filter(Boolean);
|
|
39
|
-
for (const k of keys.slice(0, -1)) data = data[k] ??= {};
|
|
40
|
-
data[keys.at(-1)!] = value;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// On the host, so they outlive this file's reloads; one per watching extension
|
|
44
|
-
const watchers: Map<string, (sid: string, by?: string, what?: string) => void> = host.shared.get("ui-watchers") ?? new Map();
|
|
45
|
-
host.shared.set("ui-watchers", watchers);
|
|
46
|
-
host.api.ui = { watch: (name: string, fn: (sid: string, by?: string, what?: string) => void) => watchers.set(name, fn) };
|
|
47
|
-
|
|
48
|
-
class Surfaces {
|
|
49
|
-
surfaces: Record<string, any> = read(DESIGN, {});
|
|
50
|
-
constructor() {
|
|
51
|
-
const data = read(DATA, {});
|
|
52
|
-
for (const [sid, r] of Object.entries(this.surfaces)) r.data = data[sid] ?? {};
|
|
53
|
-
}
|
|
54
|
-
save() {
|
|
55
|
-
fs.writeFileSync(DESIGN, JSON.stringify(Object.fromEntries(Object.entries(this.surfaces)
|
|
56
|
-
.map(([sid, r]) => [sid, { ...r, data: undefined }])), null, 1));
|
|
57
|
-
fs.writeFileSync(DATA, JSON.stringify(Object.fromEntries(Object.entries(this.surfaces).map(([sid, r]) => [sid, r.data ?? {}]))));
|
|
58
|
-
}
|
|
59
|
-
get(sid: string) { return clone(this.surfaces[sid]); }
|
|
60
|
-
snapshot() { return clone(this.surfaces); }
|
|
61
|
-
// record null: delete. by: "user" or an agent; what: the history's words for it (none: part of a bigger change,
|
|
62
|
-
// so no step of its own; the last change makes one)
|
|
63
|
-
change(sid: string, record: any, by?: string, what?: string) {
|
|
64
|
-
if (record === null) delete this.surfaces[sid];
|
|
65
|
-
else this.surfaces[sid] = record;
|
|
66
|
-
this.save();
|
|
67
|
-
host.broadcast("ui", record === null ? { op: "delete", surface: sid } : { op: "render", surface: sid, ...record });
|
|
68
|
-
for (const [name, fn] of watchers) {
|
|
69
|
-
try { fn(sid, by, what); }
|
|
70
|
-
catch (e) { host.broadcast("error", { agent: "server", text: `ui watcher ${name}: ${e}` }); } // one broken watcher doesn't stop the rest
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
load() { // read the files again (after a rollback of the code changed surfaces.json); true if the surfaces changed
|
|
74
|
-
const before = JSON.stringify(this.surfaces), design = read(DESIGN, {}), data = read(DATA, {});
|
|
75
|
-
for (const [sid, r] of Object.entries(design) as any) r.data = data[sid] ?? {};
|
|
76
|
-
this.surfaces = design;
|
|
77
|
-
host.broadcast("ui", { op: "reset", surfaces: this.surfaces });
|
|
78
|
-
return JSON.stringify(this.surfaces) !== before;
|
|
79
|
-
}
|
|
80
|
-
restore(surfaces: Record<string, any>) { // a whole saved state (undo, redo, rewind): not a change of its own
|
|
81
|
-
this.surfaces = clone(surfaces);
|
|
82
|
-
this.save();
|
|
83
|
-
host.broadcast("ui", { op: "reset", surfaces: this.surfaces });
|
|
84
|
-
}
|
|
85
|
-
locked(sid: string) {
|
|
86
|
-
return this.surfaces[sid]?.pinned ? `error: surface '${sid}' is pinned by the user, so agents can't change or delete it. `
|
|
87
|
-
+ `Make a new surface instead (made_from=['${sid}'] keeps the link), or ask the user to unpin it.` : null;
|
|
88
|
-
}
|
|
89
|
-
copyIntoUsers(sid: string) { // before sid goes: surfaces reading from it get its current values
|
|
90
|
-
const users = Object.keys(this.surfaces).filter((o) => o !== sid && this.surfaces[o].components
|
|
91
|
-
.some((c: any) => Object.values(propsOf(c)).some((v: any) => isRef(v) && v.ref === sid)));
|
|
92
|
-
for (const o of users) {
|
|
93
|
-
const r = this.get(o);
|
|
94
|
-
for (const c of r.components)
|
|
95
|
-
for (const [k, v] of Object.entries(propsOf(c)) as any)
|
|
96
|
-
if (isRef(v) && v.ref === sid) c[k] = clone(getPath(this.surfaces[sid].data, v.path));
|
|
97
|
-
this.change(o, r);
|
|
98
|
-
}
|
|
99
|
-
return users;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (!host.shared.has("ui")) host.shared.set("ui", new Surfaces());
|
|
104
|
-
const ui: Surfaces = host.shared.get("ui");
|
|
105
|
-
|
|
106
|
-
function problems(components: any[], madeFrom: string[] = []) {
|
|
107
|
-
const catalog = readCatalog(), ids = new Set(components.map((c) => c.id));
|
|
108
|
-
const errors = ids.has("root") ? [] : ['one component must have "id": "root"'];
|
|
109
|
-
for (const c of components) {
|
|
110
|
-
if (!(c.component in catalog)) { errors.push(`${c.id}: unknown component '${c.component}' (have: ${Object.keys(catalog).join(", ")})`); continue; }
|
|
111
|
-
for (const child of [...(c.children ?? []), ...(c.child ? [c.child] : [])])
|
|
112
|
-
if (!ids.has(child)) errors.push(`${c.id}: child '${child}' is not defined`);
|
|
113
|
-
const known = catalog[c.component].about?.props;
|
|
114
|
-
for (const [k, v] of Object.entries(propsOf(c)) as any) {
|
|
115
|
-
if (known && !(k in known)) errors.push(`${c.id}: ${c.component} has no prop '${k}' (props: ${Object.keys(known).join(", ") || "none"})`);
|
|
116
|
-
if (isRef(v) && !(v.ref in ui.surfaces)) errors.push(`${c.id}.${k}: refers to surface '${v.ref}', which does not exist`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return [...errors, ...madeFrom.filter((m) => !(m in ui.surfaces)).map((m) => `made_from: no surface '${m}'`)];
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// The components (components/*.js): {name: {doc, mtime, about, problem}}; doc = the comment lines at the top
|
|
123
|
-
function readCatalog() {
|
|
124
|
-
const out: Record<string, any> = {};
|
|
125
|
-
for (const f of fs.readdirSync(COMPONENTS).filter((f) => f.endsWith(".js")).sort()) {
|
|
126
|
-
const text = fs.readFileSync(path.join(COMPONENTS, f), "utf8");
|
|
127
|
-
const doc: string[] = [];
|
|
128
|
-
for (const line of text.split("\n")) {
|
|
129
|
-
if (!line.startsWith("//")) break;
|
|
130
|
-
doc.push(line.slice(2).trim());
|
|
131
|
-
}
|
|
132
|
-
let about = null, problem = null;
|
|
133
|
-
const i = text.indexOf("export const about = ");
|
|
134
|
-
if (i >= 0) {
|
|
135
|
-
try { about = JSON.parse(text.slice(i + 21, text.indexOf("};", i) + 1)); }
|
|
136
|
-
catch (e) { problem = `its about block is not valid JSON (${e})`; }
|
|
137
|
-
}
|
|
138
|
-
out[f.slice(0, -3)] = { doc: doc.join("\n"), mtime: Math.floor(fs.statSync(path.join(COMPONENTS, f)).mtimeMs / 1000), about, problem };
|
|
139
|
-
}
|
|
140
|
-
return out;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const result = (text: string) => ({ content: [{ type: "text" as const, text }], details: undefined });
|
|
144
|
-
const Place = Type.Optional(Type.Object({ x: Type.Number(), y: Type.Number(), width: Type.Optional(Type.Number()), height: Type.Optional(Type.Number()) },
|
|
145
|
-
{ description: "pin it at a spot (px from the main space's top-left); leave out to stack it" }));
|
|
146
|
-
|
|
147
|
-
export default function (pi: ExtensionAPI) {
|
|
148
|
-
const guide = fs.readFileSync(path.join(import.meta.dirname, "guide.md"), "utf8") // short; README.md: the whole of it, read when it says
|
|
149
|
-
.replace("README.md", path.join(import.meta.dirname, "README.md"));
|
|
150
|
-
pi.on("before_agent_start", (event) => { event.systemPromptOptions.sections.ui = guide; });
|
|
151
|
-
pi.on("resources_discover", () => ({ skillPaths: [path.join(import.meta.dirname, "skills")] }));
|
|
152
|
-
|
|
153
|
-
pi.registerTool({
|
|
154
|
-
name: "ui_catalog", label: "UI catalog", description: "List the UI components you can use in surfaces, with their props.",
|
|
155
|
-
parameters: Type.Object({}),
|
|
156
|
-
async execute() {
|
|
157
|
-
const catalog = readCatalog();
|
|
158
|
-
const broken = Object.entries(catalog).filter(([, m]: any) => m.problem).map(([n, m]: any) => `${n}: ${m.problem}`);
|
|
159
|
-
return result(Object.values(catalog).map((m: any) => m.doc + (m.about ? `\nabout: ${JSON.stringify(m.about)}` : "")).join("\n\n")
|
|
160
|
-
+ (broken.length ? `\n\nProblems:\n${broken.join("\n")}` : ""));
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
pi.registerTool({
|
|
165
|
-
name: "ui_render", label: "UI render", description: "Draw or replace a UI surface (a card in the main space) from catalog components.",
|
|
166
|
-
parameters: Type.Object({
|
|
167
|
-
surface: Type.String(),
|
|
168
|
-
components: Type.Optional(Type.Array(Type.Record(Type.String(), Type.Any()))),
|
|
169
|
-
data: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
170
|
-
place: Place,
|
|
171
|
-
draggable: Type.Optional(Type.Boolean()), resizable: Type.Optional(Type.Boolean()),
|
|
172
|
-
about: Type.Optional(Type.String({ description: "what it is for and why it is built this way (its back side)" })),
|
|
173
|
-
options: Type.Optional(Type.Array(Type.Record(Type.String(), Type.Any()), { description: 'alternatives instead of components: [{"label", "components", "data"}]' })),
|
|
174
|
-
made_from: Type.Optional(Type.Array(Type.String())), instruction: Type.Optional(Type.String()),
|
|
175
|
-
}),
|
|
176
|
-
async execute(_id, a) {
|
|
177
|
-
const agent = pi.getSessionName() ?? "agent";
|
|
178
|
-
const locked = ui.locked(a.surface);
|
|
179
|
-
if (locked) return result(locked);
|
|
180
|
-
if (!a.components && !a.options) return result("error: give components, or options (a list of alternatives)");
|
|
181
|
-
const options = (a.options ?? []).map((o: any, i: number) => ({ label: o.label ?? `option ${i + 1}`, components: o.components ?? [], data: o.data ?? {} }));
|
|
182
|
-
for (const o of options.length ? options : [{ label: "", components: a.components! }]) {
|
|
183
|
-
const errors = problems(o.components, a.made_from);
|
|
184
|
-
if (errors.length) return result("error: " + (o.label ? `${o.label}: ` : "") + errors.join("; "));
|
|
185
|
-
}
|
|
186
|
-
const old = ui.surfaces[a.surface] ?? {}, catalog = readCatalog();
|
|
187
|
-
const first = options[0] ?? { components: a.components, data: a.data ?? {} };
|
|
188
|
-
const record: any = { owner: agent, components: first.components, data: first.data, place: a.place ?? old.place ?? null,
|
|
189
|
-
draggable: a.draggable ?? true, resizable: a.resizable ?? true, about: a.about ?? old.about ?? "",
|
|
190
|
-
kinds: Object.fromEntries((options.length ? options : [first]).flatMap((o: any) => o.components)
|
|
191
|
-
.map((c: any) => [c.component, catalog[c.component].about?.version ?? 1])) };
|
|
192
|
-
for (const [k, v] of [["made_from", a.made_from ?? old.made_from], ["instruction", a.instruction ?? old.instruction],
|
|
193
|
-
["options", options.length ? options : null]] as const) if (v) record[k] = v;
|
|
194
|
-
ui.change(a.surface, record, agent, `${agent} drew ${a.surface}` + (options.length ? ` (${options.length} options)` : ""));
|
|
195
|
-
return result(options.length ? `surface '${a.surface}' shows ${options.length} options; the user flips through them and keeps one`
|
|
196
|
-
: `surface '${a.surface}' is on screen`);
|
|
197
|
-
},
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
pi.registerTool({
|
|
201
|
-
name: "ui_update", label: "UI update", description: "Change one value in a surface's data (e.g. a score or a list).",
|
|
202
|
-
parameters: Type.Object({ surface: Type.String(), path: Type.String({ description: "e.g. /score" }), value: Type.Any() }),
|
|
203
|
-
async execute(_id, a) {
|
|
204
|
-
if (!(a.surface in ui.surfaces)) return result(`error: no surface '${a.surface}'. Surfaces: ${Object.keys(ui.surfaces).join(", ")}`);
|
|
205
|
-
const locked = ui.locked(a.surface);
|
|
206
|
-
if (locked) return result(locked);
|
|
207
|
-
const r = ui.get(a.surface);
|
|
208
|
-
setPath(r.data, a.path, a.value);
|
|
209
|
-
const agent = pi.getSessionName() ?? "agent";
|
|
210
|
-
ui.change(a.surface, r, agent, `${agent} changed ${a.path} in ${a.surface}`);
|
|
211
|
-
return result("updated");
|
|
212
|
-
},
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
pi.registerTool({
|
|
216
|
-
name: "ui_delete", label: "UI delete", description: "Remove a UI surface.",
|
|
217
|
-
parameters: Type.Object({ surface: Type.String() }),
|
|
218
|
-
async execute(_id, a) {
|
|
219
|
-
const locked = ui.locked(a.surface);
|
|
220
|
-
if (locked) return result(locked);
|
|
221
|
-
const users = ui.copyIntoUsers(a.surface), agent = pi.getSessionName() ?? "agent";
|
|
222
|
-
ui.change(a.surface, null, agent, `${agent} deleted ${a.surface}` + (users.length ? ` (${users.join(", ")} got a copy)` : ""));
|
|
223
|
-
return result("deleted" + (users.length ? `; ${users.join(", ")} read from it, so they got a copy of its data` : ""));
|
|
224
|
-
},
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
// The page side: registered once for all agents (keyed by name, so each agent's load replaces the last one)
|
|
228
|
-
host.routes.set("/ui-api/catalog", (_req: any, res: any) => res.writeHead(200, { "content-type": "application/json" })
|
|
229
|
-
.end(JSON.stringify(Object.fromEntries(Object.entries(readCatalog()).map(([n, m]: any) => [n, m.mtime])))));
|
|
230
|
-
host.routes.set("/ui-api/about", (_req: any, res: any) => res.writeHead(200, { "content-type": "application/json" })
|
|
231
|
-
.end(JSON.stringify(Object.fromEntries(Object.entries(readCatalog()).filter(([, m]: any) => m.about).map(([n, m]: any) => [n, m.about])))));
|
|
232
|
-
host.routes.set("/ui/", (req: any, res: any) => {
|
|
233
|
-
let url = "";
|
|
234
|
-
try { url = path.posix.normalize(decodeURIComponent(new URL(req.url, "http://x").pathname)); } catch {} // bad %-escape: 404
|
|
235
|
-
if (!url.startsWith("/ui/")) return res.writeHead(404).end(); // also a ".." out of /ui/
|
|
236
|
-
const p = url.slice("/ui/".length);
|
|
237
|
-
if (p.startsWith("apps/")) return host.file(res, path.join(APPS, p.slice("apps/".length)));
|
|
238
|
-
const lib = p.slice("node_modules/".length);
|
|
239
|
-
if (p.startsWith("node_modules/")) return LIBS.some((l) => lib.startsWith(l)) ? host.file(res, path.join(host.modules, lib)) : res.writeHead(404).end();
|
|
240
|
-
if (!SERVED.some((s) => p === s || (s.endsWith("/") && p.startsWith(s)))) return res.writeHead(404).end();
|
|
241
|
-
host.file(res, path.join(import.meta.dirname, p));
|
|
242
|
-
});
|
|
243
|
-
host.routes.set("/ui-api/surfaces", (_req: any, res: any) => res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(ui.surfaces)));
|
|
244
|
-
host.pageHandlers.set("ui", (msg: any) => {
|
|
245
|
-
const { type, surface: sid } = msg;
|
|
246
|
-
const r = typeof sid === "string" ? ui.get(sid) : undefined;
|
|
247
|
-
if (!r) return;
|
|
248
|
-
if (type === "ui_close") {
|
|
249
|
-
const users = ui.copyIntoUsers(sid);
|
|
250
|
-
ui.change(sid, null, "user", `you closed ${sid}` + (users.length ? ` (${users.join(", ")} got a copy)` : ""));
|
|
251
|
-
}
|
|
252
|
-
else if (type === "ui_move") {
|
|
253
|
-
if (r.pinned) return host.broadcast("ui", { op: "render", surface: sid, ...r });
|
|
254
|
-
r.place = msg.place; ui.change(sid, r, "user", `you moved ${sid}`);
|
|
255
|
-
} else if (type === "ui_set") { setPath(r.data, msg.path ?? "/", msg.value); ui.change(sid, r, "user", `you changed ${msg.path} in ${sid}`); }
|
|
256
|
-
else if (type === "ui_prop") {
|
|
257
|
-
for (const c of r.components) if (c.id === msg.component && !["id", "component", "children", "child"].includes(msg.prop)) c[msg.prop] = msg.value;
|
|
258
|
-
ui.change(sid, r, "user", `you set ${msg.prop} of ${msg.component} in ${sid}`);
|
|
259
|
-
} else if (type === "ui_pin") { r.pinned = !!msg.pinned; ui.change(sid, r, "user", `you ${r.pinned ? "pinned" : "unpinned"} ${sid}`); }
|
|
260
|
-
else if (type === "ui_keep" && r.options) {
|
|
261
|
-
const o = r.options[Number(msg.index ?? 0)];
|
|
262
|
-
delete r.options;
|
|
263
|
-
if (o) { r.components = o.components; r.data = o.data; ui.change(sid, r, "user", `you kept ${o.label} of ${sid}`); }
|
|
264
|
-
} else if (type === "ui_action") {
|
|
265
|
-
r.data = msg.data ?? r.data;
|
|
266
|
-
ui.change(sid, r, "user", `you used ${msg.name} in ${sid}`);
|
|
267
|
-
host.send("user", r.owner, `[ui action] ${msg.name} on surface '${sid}' (component ${msg.component})\n`
|
|
268
|
-
+ `context: ${JSON.stringify(msg.context ?? {})}\ndata: ${JSON.stringify(r.data)}`);
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
# Previews of the user's files
|
|
2
|
-
The user can open a file from their panel in a preview and point at a spot in it: the note's part has
|
|
3
|
-
"viewing": <the file>. Such a note is about that file and that spot, not about the page.
|
|
4
|
-
read_document(path) gives a document's text (docx, pptx, xlsx, pdf, ...). Before you handle a note on a
|
|
5
|
-
preview, read README.md.
|
package/app/mcp.json
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: plugin-map
|
|
3
|
-
description: Find your way around your own code (the pi extensions in your code folder, the page among them) without reading whole files. Use before changing yourself, or before planning work for others that will.
|
|
4
|
-
---
|
|
5
|
-
# Getting a map of your own code
|
|
6
|
-
|
|
7
|
-
Your code folder is the one your prompt names (it holds `extensions/`). Every feature is one extension, and every
|
|
8
|
-
extension is one folder, `extensions/<name>/`, holding all of that feature:
|
|
9
|
-
|
|
10
|
-
| File | What it is |
|
|
11
|
-
|---------------|--------------------------------------------------------------------|
|
|
12
|
-
| `index.ts` | the extension itself (pi loads it); starts with a header comment |
|
|
13
|
-
| `guide.md` | its short prompt section (the part you are reading in your own prompt) |
|
|
14
|
-
| `README.md` | the whole of what the guide says, read when the guide says |
|
|
15
|
-
| `browser.js` | its part of the page (the page extension loads every one it finds) |
|
|
16
|
-
| `skills/` | skills only this feature needs (its index.ts gives them to pi) |
|
|
17
|
-
|
|
18
|
-
An extension can have other files too (`ui/` has its components, `page/` has `page.html`). The headers are the
|
|
19
|
-
documentation, kept with the code, so read them before the code itself.
|
|
20
|
-
|
|
21
|
-
## 1. The map: one line per extension
|
|
22
|
-
From your code folder:
|
|
23
|
-
```
|
|
24
|
-
for d in extensions/*/; do printf '%-24s %s\n' "$d" "$(grep -m1 '^//' "$d/index.ts" | sed 's|^// \?||' | cut -c1-140)"; done
|
|
25
|
-
```
|
|
26
|
-
Also there:
|
|
27
|
-
- `extensions/page/`: the page itself (`page.html`), its list of browser parts, and what serves them.
|
|
28
|
-
- `extensions/ui/components/*.js`: UI components, each starting with a comment line on what it is, then its
|
|
29
|
-
`about` block (props, levers, example; `head -20 <file>`).
|
|
30
|
-
- `skills/<name>/SKILL.md`: skills about you as a whole (this one); a feature's own skills are in its folder.
|
|
31
|
-
- `ui/`: what you built, not code: `surfaces.json` (the surfaces' design) and `apps/<name>/`.
|
|
32
|
-
- The host, `server.ts` one folder up: sessions, the web server, `globalThis.chalky`. Read its header; don't edit it.
|
|
33
|
-
|
|
34
|
-
## 2. The headers of the extensions involved
|
|
35
|
-
```
|
|
36
|
-
awk '/^import /{exit} 1' extensions/swarm/index.ts # everything above the imports
|
|
37
|
-
```
|
|
38
|
-
Read only the headers of the extensions your work touches. All of them at once is over 13,000 characters.
|
|
39
|
-
|
|
40
|
-
## 3. Then only the code you will change
|
|
41
|
-
Use `grep -n` to find the function, then `sed -n 'START,ENDp'` for that part. A whole file can be long
|
|
42
|
-
(swarm's index.ts is over 16,000 characters). If you are planning work for other agents, the headers are usually
|
|
43
|
-
enough: let each agent read the code it owns.
|
|
44
|
-
|
|
45
|
-
## Keep the headers true
|
|
46
|
-
When you change what an extension provides (a tool, a route, a page message, an event, a host service), update
|
|
47
|
-
its header in the same change. Other agents, and you after a reload, rely on it. A new extension is a new folder
|
|
48
|
-
with an `index.ts` whose header starts with a one-line summary: that line is its entry in the map. After editing,
|
|
49
|
-
call the reload tool.
|
package/app/ui/apps/.keep
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|