@chalky/chalky 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +16 -5
  2. package/agent/SYSTEM.md +36 -0
  3. package/agent/extensions/app/README.md +72 -0
  4. package/{app/extensions/ui → agent/extensions/app}/agents.js +22 -16
  5. package/agent/extensions/app/guide.md +5 -0
  6. package/agent/extensions/app/index.ts +66 -0
  7. package/{app/extensions/ui/skills/agent-apps → agent/extensions/app/skills/app-agents}/SKILL.md +17 -18
  8. package/agent/extensions/app/skills/test-your-app/SKILL.md +23 -0
  9. package/{app/extensions/boards → agent/extensions/board}/browser.js +30 -32
  10. package/{app/extensions/boards → agent/extensions/board}/guide.md +1 -1
  11. package/{app/extensions/boards → agent/extensions/board}/index.ts +32 -30
  12. package/agent/extensions/card/README.md +88 -0
  13. package/agent/extensions/card/blocks/Button.js +34 -0
  14. package/{app/extensions/ui/components → agent/extensions/card/blocks}/Column.js +2 -2
  15. package/agent/extensions/card/blocks/Markdown.js +37 -0
  16. package/{app/extensions/ui/components/Card.js → agent/extensions/card/blocks/Panel.js} +3 -3
  17. package/{app/extensions/ui/components → agent/extensions/card/blocks}/Row.js +2 -2
  18. package/{app/extensions/ui/components → agent/extensions/card/blocks}/TextField.js +3 -3
  19. package/{app/extensions/ui → agent/extensions/card}/browser.js +155 -129
  20. package/agent/extensions/card/guide.md +13 -0
  21. package/agent/extensions/card/index.ts +370 -0
  22. package/{app/extensions/commands → agent/extensions/command}/browser.js +3 -3
  23. package/agent/extensions/command/guide.md +2 -0
  24. package/{app/extensions/commands → agent/extensions/command}/index.ts +14 -16
  25. package/agent/extensions/doodle/README.md +57 -0
  26. package/agent/extensions/doodle/browser.js +1013 -0
  27. package/agent/extensions/doodle/guide.md +22 -0
  28. package/agent/extensions/doodle/index.ts +274 -0
  29. package/agent/extensions/file/browser.js +182 -0
  30. package/agent/extensions/file/guide.md +5 -0
  31. package/{app/extensions/files → agent/extensions/file}/index.ts +37 -18
  32. package/{app/extensions/frames → agent/extensions/frame}/README.md +13 -13
  33. package/{app/extensions/frames → agent/extensions/frame}/browser.js +20 -18
  34. package/agent/extensions/frame/guide.md +3 -0
  35. package/agent/extensions/frame/index.ts +90 -0
  36. package/{app/extensions/ui_history → agent/extensions/history}/browser.js +18 -18
  37. package/{app/extensions/ui_history → agent/extensions/history}/index.ts +34 -33
  38. package/{app → agent}/extensions/mcp/browser.js +1 -1
  39. package/agent/extensions/mcp/guide.md +3 -0
  40. package/{app → agent}/extensions/mcp/index.ts +2 -4
  41. package/{app/extensions/login → agent/extensions/model}/browser.js +67 -67
  42. package/{app → agent}/extensions/model/index.ts +8 -4
  43. package/{app/extensions/login/index.ts → agent/extensions/model/login.ts} +47 -46
  44. package/agent/extensions/note/README.md +37 -0
  45. package/agent/extensions/note/browser.js +382 -0
  46. package/agent/extensions/note/guide.md +9 -0
  47. package/agent/extensions/note/index.ts +215 -0
  48. package/agent/extensions/page/browser.js +453 -0
  49. package/{app → agent}/extensions/page/index.ts +72 -3
  50. package/{app → agent}/extensions/page/page.html +102 -49
  51. package/agent/extensions/page/skills/interact-with-page/SKILL.md +59 -0
  52. package/agent/extensions/prompt/index.ts +38 -0
  53. package/{app → agent}/extensions/reload/index.ts +3 -3
  54. package/{app → agent}/extensions/screen/browser.js +10 -23
  55. package/{app → agent}/extensions/screen/index.ts +11 -11
  56. package/{app/extensions/steps → agent/extensions/step}/index.ts +1 -1
  57. package/{app/extensions/swarm → agent/extensions/team}/README.md +19 -19
  58. package/{app/extensions/swarm → agent/extensions/team}/guide.md +4 -4
  59. package/{app/extensions/swarm → agent/extensions/team}/index.ts +41 -66
  60. package/{app/extensions/versions → agent/extensions/version}/browser.js +19 -10
  61. package/agent/extensions/version/index.ts +205 -0
  62. package/{app → agent}/extensions/viewer/README.md +4 -4
  63. package/{app → agent}/extensions/viewer/browser.js +8 -8
  64. package/agent/extensions/viewer/guide.md +3 -0
  65. package/{app → agent}/extensions/viewer/index.ts +22 -17
  66. package/{app → agent}/extensions/voice/browser.js +4 -3
  67. package/{app → agent}/extensions/voice/index.ts +8 -6
  68. package/{app/extensions/ui → agent/extensions/voice}/voice.js +5 -5
  69. package/agent/settings.json +8 -0
  70. package/{app/extensions/ui/skills/ui-check → agent/skills/browser-check}/SKILL.md +4 -4
  71. package/{app/extensions/ui/skills/ui-check → agent/skills/browser-check}/scripts/check.mjs +1 -1
  72. package/{app/extensions/ui/skills/ui-design → agent/skills/design}/SKILL.md +15 -15
  73. package/agent/skills/extension-map/SKILL.md +56 -0
  74. package/agent/skills/show-me/SKILL.md +66 -0
  75. package/{app → agent}/skills/write-skill/SKILL.md +1 -1
  76. package/bin/chalky.mjs +2 -2
  77. package/package.json +9 -8
  78. package/server.ts +313 -58
  79. package/app/SYSTEM.md +0 -31
  80. package/app/extensions/appdata/README.md +0 -10
  81. package/app/extensions/appdata/guide.md +0 -4
  82. package/app/extensions/appdata/index.ts +0 -47
  83. package/app/extensions/commands/guide.md +0 -3
  84. package/app/extensions/doodle/README.md +0 -15
  85. package/app/extensions/doodle/browser.js +0 -398
  86. package/app/extensions/doodle/guide.md +0 -6
  87. package/app/extensions/doodle/index.ts +0 -54
  88. package/app/extensions/files/browser.js +0 -96
  89. package/app/extensions/files/guide.md +0 -7
  90. package/app/extensions/frames/guide.md +0 -7
  91. package/app/extensions/frames/index.ts +0 -85
  92. package/app/extensions/mcp/guide.md +0 -5
  93. package/app/extensions/point/README.md +0 -22
  94. package/app/extensions/point/browser.js +0 -247
  95. package/app/extensions/point/guide.md +0 -6
  96. package/app/extensions/point/index.ts +0 -112
  97. package/app/extensions/prompt/index.ts +0 -19
  98. package/app/extensions/ui/README.md +0 -69
  99. package/app/extensions/ui/components/App.js +0 -28
  100. package/app/extensions/ui/components/Button.js +0 -28
  101. package/app/extensions/ui/components/Markdown.js +0 -37
  102. package/app/extensions/ui/guide.md +0 -7
  103. package/app/extensions/ui/index.ts +0 -291
  104. package/app/extensions/versions/index.ts +0 -168
  105. package/app/extensions/viewer/guide.md +0 -5
  106. package/app/mcp.json +0 -3
  107. package/app/settings.json +0 -3
  108. package/app/skills/plugin-map/SKILL.md +0 -49
  109. package/app/ui/apps/.keep +0 -0
  110. package/node_modules/xlsx/CHANGELOG.md +0 -361
  111. package/node_modules/xlsx/LICENSE +0 -201
  112. package/node_modules/xlsx/README.md +0 -41
  113. package/node_modules/xlsx/bin/xlsx.njs +0 -310
  114. package/node_modules/xlsx/bower.json +0 -22
  115. package/node_modules/xlsx/dist/LICENSE +0 -201
  116. package/node_modules/xlsx/dist/cpexcel.d.ts +0 -39
  117. package/node_modules/xlsx/dist/cpexcel.full.mjs +0 -1502
  118. package/node_modules/xlsx/dist/cpexcel.js +0 -1506
  119. package/node_modules/xlsx/dist/shim.min.js +0 -2
  120. package/node_modules/xlsx/dist/xlsx.core.min.js +0 -18
  121. package/node_modules/xlsx/dist/xlsx.core.min.map +0 -1
  122. package/node_modules/xlsx/dist/xlsx.extendscript.js +0 -28259
  123. package/node_modules/xlsx/dist/xlsx.full.min.js +0 -24
  124. package/node_modules/xlsx/dist/xlsx.full.min.map +0 -1
  125. package/node_modules/xlsx/dist/xlsx.mini.min.js +0 -10
  126. package/node_modules/xlsx/dist/xlsx.mini.min.map +0 -1
  127. package/node_modules/xlsx/dist/xlsx.zahl.js +0 -4
  128. package/node_modules/xlsx/dist/xlsx.zahl.mjs +0 -2
  129. package/node_modules/xlsx/dist/zahl.d.ts +0 -4
  130. package/node_modules/xlsx/package.json +0 -183
  131. package/node_modules/xlsx/types/index.d.ts +0 -1065
  132. package/node_modules/xlsx/types/tsconfig.json +0 -15
  133. package/node_modules/xlsx/xlsx.js +0 -28103
  134. package/node_modules/xlsx/xlsx.mjs +0 -28228
  135. package/node_modules/xlsx/xlsxworker.js +0 -14
  136. /package/{app/extensions/ui/components → agent/extensions/card/blocks}/Table.js +0 -0
  137. /package/{app/extensions/ui/components → agent/extensions/card/blocks}/Text.js +0 -0
@@ -1,28 +0,0 @@
1
- // Button: a button that sends an action to the agent when clicked.
2
- export const about = {
3
- "version": 1,
4
- "props": {
5
- "label": {"type": "text", "default": "OK"},
6
- "action": {"type": "action", "default": "click", "doc": "the action name you will receive"},
7
- "context": {"type": "object", "doc": "sent with the action; values may be {\"path\": ...}"},
8
- "variant": {"type": "choice", "options": ["primary", "secondary"], "default": "primary"}
9
- },
10
- "children": "none",
11
- "levers": ["Says {label}.", "Looks {variant}."],
12
- "example": {"label": "Send order", "action": "send", "variant": "primary"}
13
- };
14
- export default class extends HTMLElement {
15
- update(props, ui) {
16
- if (!this.button) {
17
- this.button = document.createElement("button");
18
- this.button.onclick = () => this.ui.action(this.props.action || "click", this.props.context || {});
19
- this.append(this.button);
20
- }
21
- this.props = props;
22
- this.ui = ui;
23
- this.button.textContent = props.label || "OK";
24
- this.button.style.cssText = "border-radius:980px;padding:7px 16px;font-weight:500;" + (props.variant === "secondary"
25
- ? "background:var(--glass-strong);color:var(--accent);border:1px solid var(--line)"
26
- : "background:var(--accent);color:#fff;border:0");
27
- }
28
- }
@@ -1,37 +0,0 @@
1
- // Markdown: formatted text from a markdown string (headings, lists, tables, code, links, quotes).
2
- // HTML in the text is sanitised; links open in a new tab. The chat draws message bodies with it too.
3
- export const about = {
4
- "version": 1,
5
- "props": {
6
- "text": {"type": "text", "doc": "markdown"}
7
- },
8
- "children": "none",
9
- "levers": [],
10
- "example": {"text": "**Lunch** closes *Friday 11:00*"}
11
- };
12
- import { marked } from "marked"; // bare names: resolved by the import map in chat_ui.html
13
- import DOMPurify from "dompurify";
14
-
15
- const CSS = `
16
- ui-markdown { display: block; white-space: normal; }
17
- ui-markdown p, ui-markdown ul, ui-markdown ol, ui-markdown pre, ui-markdown table, ui-markdown blockquote { margin: 6px 0; }
18
- ui-markdown ul, ui-markdown ol { padding-left: 22px; }
19
- ui-markdown h1, ui-markdown h2, ui-markdown h3, ui-markdown h4 { font-size: 1.05em; margin: 8px 0 4px; }
20
- ui-markdown code { font: 12.5px monospace; background: rgba(0, 0, 0, .06); padding: 1px 4px; border-radius: 3px; }
21
- ui-markdown pre { background: var(--hover); padding: 8px; border-radius: 4px; overflow-x: auto; }
22
- ui-markdown pre code { background: none; padding: 0; }
23
- ui-markdown table { border-collapse: collapse; }
24
- ui-markdown th, ui-markdown td { border: 1px solid var(--line); padding: 3px 8px; }
25
- ui-markdown blockquote { border-left: 3px solid var(--line); padding-left: 8px; color: var(--soft); }`;
26
-
27
- export default class extends HTMLElement {
28
- update({ text = "" }) {
29
- if (!document.getElementById("ui-markdown-css")) {
30
- document.head.insertAdjacentHTML("beforeend", `<style id="ui-markdown-css">${CSS}</style>`);
31
- }
32
- if (this.state?.text === text) return; // unchanged: keep the DOM (and any selection)
33
- this.state = { text };
34
- this.innerHTML = DOMPurify.sanitize(marked.parse(text, { gfm: true, breaks: true }));
35
- for (const a of this.querySelectorAll("a")) { a.target = "_blank"; a.rel = "noopener"; }
36
- }
37
- }
@@ -1,7 +0,0 @@
1
- # Showing UI to the user
2
- Show what you build as UI surfaces: cards in the page's main space, described as JSON (ui_catalog, ui_render,
3
- ui_update, ui_delete), rather than by editing the page. Load the ui-design skill before building or restyling.
4
- - Name what you build (surface ids, element ids): the user points at things by name.
5
- - The user moves and resizes surfaces: build things that fill their box (%, flex, a viewBox), not fixed px.
6
- - A surface the user pinned: don't work around it (make a new one, made_from=[it], or ask).
7
- Before your first ui_render in a conversation, and before writing a component or an App, read README.md.
@@ -1,291 +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
- // a removed board (boards/) takes its surfaces with it: one step in the history (undo brings them back)
153
- pi.on("session_start", () => host.api.boards?.onRemove("ui", (n: number) => {
154
- const sids = Object.keys(ui.surfaces).filter((sid) => (ui.surfaces[sid].board ?? 1) === n);
155
- const gone = sids.map((sid) => ({ owner: ui.surfaces[sid].owner, what: `surface ${sid}` }));
156
- sids.forEach((sid, i) => {
157
- ui.copyIntoUsers(sid); // surfaces elsewhere reading from it keep its values
158
- ui.change(sid, null, "user", i === sids.length - 1 ? `you removed board ${n} (${sids.join(", ")})` : undefined);
159
- });
160
- return gone;
161
- }));
162
-
163
- pi.registerTool({
164
- name: "ui_catalog", label: "UI catalog", description: "List the UI components you can use in surfaces, with their props.",
165
- parameters: Type.Object({}),
166
- async execute() {
167
- const catalog = readCatalog();
168
- const broken = Object.entries(catalog).filter(([, m]: any) => m.problem).map(([n, m]: any) => `${n}: ${m.problem}`);
169
- return result(Object.values(catalog).map((m: any) => m.doc + (m.about ? `\nabout: ${JSON.stringify(m.about)}` : "")).join("\n\n")
170
- + (broken.length ? `\n\nProblems:\n${broken.join("\n")}` : ""));
171
- },
172
- });
173
-
174
- pi.registerTool({
175
- name: "ui_render", label: "UI render", description: "Draw or replace a UI surface (a card in the main space) from catalog components.",
176
- parameters: Type.Object({
177
- surface: Type.String(),
178
- components: Type.Optional(Type.Array(Type.Record(Type.String(), Type.Any()))),
179
- data: Type.Optional(Type.Record(Type.String(), Type.Any())),
180
- place: Place,
181
- draggable: Type.Optional(Type.Boolean()), resizable: Type.Optional(Type.Boolean()),
182
- about: Type.Optional(Type.String({ description: "what it is for and why it is built this way (its back side)" })),
183
- options: Type.Optional(Type.Array(Type.Record(Type.String(), Type.Any()), { description: 'alternatives instead of components: [{"label", "components", "data"}]' })),
184
- made_from: Type.Optional(Type.Array(Type.String())), instruction: Type.Optional(Type.String()),
185
- board: Type.Optional(Type.Union([Type.Number(), Type.Literal("new")], { description:
186
- 'a board number, or "new" for a new board; leave out: where the surface is, else the board the user is on' })),
187
- }),
188
- async execute(_id, a) {
189
- const agent = pi.getSessionName() ?? "agent";
190
- const locked = ui.locked(a.surface);
191
- if (locked) return result(locked);
192
- if (!a.components && !a.options) return result("error: give components, or options (a list of alternatives)");
193
- const options = (a.options ?? []).map((o: any, i: number) => ({ label: o.label ?? `option ${i + 1}`, components: o.components ?? [], data: o.data ?? {} }));
194
- for (const o of options.length ? options : [{ label: "", components: a.components! }]) {
195
- const errors = problems(o.components, a.made_from);
196
- if (errors.length) return result("error: " + (o.label ? `${o.label}: ` : "") + errors.join("; "));
197
- }
198
- const old = ui.surfaces[a.surface] ?? {}, catalog = readCatalog(), boards = host.api.boards;
199
- if (typeof a.board === "number" && boards && !boards.has(a.board))
200
- return result(`error: no board ${a.board}. Boards: ${boards.list().join(", ")} (or board "new")`);
201
- const first = options[0] ?? { components: a.components, data: a.data ?? {} };
202
- const record: any = { owner: agent, components: first.components, data: first.data, place: a.place ?? old.place ?? null,
203
- draggable: a.draggable ?? true, resizable: a.resizable ?? true, about: a.about ?? old.about ?? "",
204
- kinds: Object.fromEntries((options.length ? options : [first]).flatMap((o: any) => o.components)
205
- .map((c: any) => [c.component, catalog[c.component].about?.version ?? 1])) };
206
- for (const [k, v] of [["made_from", a.made_from ?? old.made_from], ["instruction", a.instruction ?? old.instruction],
207
- ["options", options.length ? options : null]] as const) if (v) record[k] = v;
208
- // its board (boards/): the one asked for, a new one, where it already is, or the one the user is on
209
- if (boards) record.board = a.board === "new" ? boards.add() : a.board ?? old.board ?? boards.current();
210
- ui.change(a.surface, record, agent, `${agent} drew ${a.surface}` + (options.length ? ` (${options.length} options)` : ""));
211
- const where = boards ? `on board ${record.board}${a.board === "new" ? " (a new board)" : ""}` : "on screen";
212
- return result(options.length ? `surface '${a.surface}' shows ${options.length} options ${where}; the user flips through them and keeps one`
213
- : `surface '${a.surface}' is ${where}`);
214
- },
215
- });
216
-
217
- pi.registerTool({
218
- name: "ui_update", label: "UI update", description: "Change one value in a surface's data (e.g. a score or a list).",
219
- parameters: Type.Object({ surface: Type.String(), path: Type.String({ description: "e.g. /score" }), value: Type.Any() }),
220
- async execute(_id, a) {
221
- if (!(a.surface in ui.surfaces)) return result(`error: no surface '${a.surface}'. Surfaces: ${Object.keys(ui.surfaces).join(", ")}`);
222
- const locked = ui.locked(a.surface);
223
- if (locked) return result(locked);
224
- const r = ui.get(a.surface);
225
- setPath(r.data, a.path, a.value);
226
- const agent = pi.getSessionName() ?? "agent";
227
- ui.change(a.surface, r, agent, `${agent} changed ${a.path} in ${a.surface}`);
228
- return result("updated");
229
- },
230
- });
231
-
232
- pi.registerTool({
233
- name: "ui_delete", label: "UI delete", description: "Remove a UI surface.",
234
- parameters: Type.Object({ surface: Type.String() }),
235
- async execute(_id, a) {
236
- const locked = ui.locked(a.surface);
237
- if (locked) return result(locked);
238
- const users = ui.copyIntoUsers(a.surface), agent = pi.getSessionName() ?? "agent";
239
- ui.change(a.surface, null, agent, `${agent} deleted ${a.surface}` + (users.length ? ` (${users.join(", ")} got a copy)` : ""));
240
- return result("deleted" + (users.length ? `; ${users.join(", ")} read from it, so they got a copy of its data` : ""));
241
- },
242
- });
243
-
244
- // The page side: registered once for all agents (keyed by name, so each agent's load replaces the last one)
245
- host.routes.set("/ui-api/catalog", (_req: any, res: any) => res.writeHead(200, { "content-type": "application/json" })
246
- .end(JSON.stringify(Object.fromEntries(Object.entries(readCatalog()).map(([n, m]: any) => [n, m.mtime])))));
247
- host.routes.set("/ui-api/about", (_req: any, res: any) => res.writeHead(200, { "content-type": "application/json" })
248
- .end(JSON.stringify(Object.fromEntries(Object.entries(readCatalog()).filter(([, m]: any) => m.about).map(([n, m]: any) => [n, m.about])))));
249
- host.routes.set("/ui/", (req: any, res: any) => {
250
- let url = "";
251
- try { url = path.posix.normalize(decodeURIComponent(new URL(req.url, "http://x").pathname)); } catch {} // bad %-escape: 404
252
- if (!url.startsWith("/ui/")) return res.writeHead(404).end(); // also a ".." out of /ui/
253
- const p = url.slice("/ui/".length);
254
- if (p.startsWith("apps/")) return host.file(res, path.join(APPS, p.slice("apps/".length)));
255
- const lib = p.slice("node_modules/".length);
256
- if (p.startsWith("node_modules/")) return LIBS.some((l) => lib.startsWith(l)) ? host.file(res, path.join(host.modules, lib)) : res.writeHead(404).end();
257
- if (!SERVED.some((s) => p === s || (s.endsWith("/") && p.startsWith(s)))) return res.writeHead(404).end();
258
- host.file(res, path.join(import.meta.dirname, p));
259
- });
260
- host.routes.set("/ui-api/surfaces", (_req: any, res: any) => res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(ui.surfaces)));
261
- host.pageHandlers.set("ui", (msg: any) => {
262
- const { type, surface: sid } = msg;
263
- const r = typeof sid === "string" ? ui.get(sid) : undefined;
264
- if (!r) return;
265
- if (type === "ui_close") {
266
- const users = ui.copyIntoUsers(sid);
267
- ui.change(sid, null, "user", `you closed ${sid}` + (users.length ? ` (${users.join(", ")} got a copy)` : ""));
268
- }
269
- else if (type === "ui_move") {
270
- if (r.pinned) return host.broadcast("ui", { op: "render", surface: sid, ...r });
271
- r.place = msg.place; ui.change(sid, r, "user", `you moved ${sid}`);
272
- } else if (type === "ui_board" && host.api.boards?.has(msg.board)) { // dropped on another board's thumbnail
273
- if (r.pinned) return host.broadcast("ui", { op: "render", surface: sid, ...r });
274
- r.board = msg.board; r.place = msg.place ?? null; ui.change(sid, r, "user", `you moved ${sid} to board ${msg.board}`);
275
- } else if (type === "ui_set") { setPath(r.data, msg.path ?? "/", msg.value); ui.change(sid, r, "user", `you changed ${msg.path} in ${sid}`); }
276
- else if (type === "ui_prop") {
277
- for (const c of r.components) if (c.id === msg.component && !["id", "component", "children", "child"].includes(msg.prop)) c[msg.prop] = msg.value;
278
- ui.change(sid, r, "user", `you set ${msg.prop} of ${msg.component} in ${sid}`);
279
- } else if (type === "ui_pin") { r.pinned = !!msg.pinned; ui.change(sid, r, "user", `you ${r.pinned ? "pinned" : "unpinned"} ${sid}`); }
280
- else if (type === "ui_keep" && r.options) {
281
- const o = r.options[Number(msg.index ?? 0)];
282
- delete r.options;
283
- if (o) { r.components = o.components; r.data = o.data; ui.change(sid, r, "user", `you kept ${o.label} of ${sid}`); }
284
- } else if (type === "ui_action") {
285
- r.data = msg.data ?? r.data;
286
- ui.change(sid, r, "user", `you used ${msg.name} in ${sid}`);
287
- host.send("user", r.owner, `[ui action] ${msg.name} on surface '${sid}' (component ${msg.component})\n`
288
- + `context: ${JSON.stringify(msg.context ?? {})}\ndata: ${JSON.stringify(r.data)}`);
289
- }
290
- });
291
- }
@@ -1,168 +0,0 @@
1
- // Version history of the agents' own code, app/ (port of the Python versions plugin; the page's Versions panel is
2
- // unchanged). The history is a git repo kept OUTSIDE app/, in state/versions.git (the idea of pi-workspace-history):
3
- // app/ has no .git, so an agent's own git commands can't rewrite it. Its first commit is app/ as it was the first
4
- // time, tagged "original". Git through isomorphic-git (pure JS; no git program needed), one operation at a time.
5
- // - When an agent's turn is over (pi's agent_settled) and app/ changed: commit, author = that agent, message =
6
- // its reload reason, else what it was asked (Python: what it first said, often just "done"), else what it said.
7
- // Event "version" to the pages ({agent, sha, message, files, restart: false}: they refresh what changed).
8
- // - Pictures for the panel: taken by the open page itself (browser.js here, with dom-to-image-more; no
9
- // headless browser), after each new version and, when a page opens, for the current version if it has none.
10
- // In state/shots/<sha>.png. A version made while no page was open and changed again before one opened: none.
11
- // - GET /versions commits, newest first ({sha, short, author, date, message, current, shot})
12
- // GET /versions/<sha>/shot that commit's picture (404 if none; HEAD too)
13
- // PUT /versions/<sha>/shot the page's picture (PNG, at most 10 MB; {saved: false} if it has one: the first page wins)
14
- // POST /versions/<sha>/rollback that commit's files as a NEW commit (history is never rewritten, so a rollback
15
- // can be rolled back). The Python agent restarted its process; here: stop every
16
- // agent, put the files back, reload every agent's extensions (pi's reload), read
17
- // the surfaces again, and tell each agent (an fyi: no turn).
18
- // - Not ported: pip freeze (Python's library list per version).
19
- // Ignored (info/exclude in the history repo): pi's auth.json, models-store.json and mcp-auth.json (logins), node_modules/.
20
- // Guard: app/ is the agents' code. If a commit includes other files, its agent is told to move them to the
21
- // workspace: a rollback would undo them.
22
- import fs from "node:fs";
23
- import path from "node:path";
24
- import git from "isomorphic-git";
25
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
26
-
27
- const host = (globalThis as any).chalky;
28
- const GIT_DIR = path.join(host.state, "versions.git");
29
- const SHOTS = path.join(host.state, "shots");
30
- const MAX_SHOT = 10 * 1024 * 1024;
31
- const CODE = ["extensions/", "skills/", "ui/", "SYSTEM.md", "mcp.json", "settings.json"];
32
- const IGNORE = ["auth.json", "models-store.json", "mcp-auth.json", "node_modules/"];
33
- const repo = { fs, dir: host.app, gitdir: GIT_DIR };
34
- const who = (name: string) => ({ name, email: `${name}@chalky` });
35
-
36
- const shared = host.shared.get("versions") ?? { queue: Promise.resolve(), turnFrom: {} as Record<string, number> };
37
- host.shared.set("versions", shared);
38
- function queued<T>(fn: () => Promise<T>): Promise<T> { // history operations one at a time (agents can finish together)
39
- const run = shared.queue.then(fn);
40
- shared.queue = run.catch(() => {});
41
- return run;
42
- }
43
-
44
- async function stage() { // git add -A: every change in app/ into the index; the changed paths
45
- const changed = [];
46
- for (const [filepath, head, workdir, index] of await git.statusMatrix(repo)) {
47
- if (head === 1 && workdir === 1 && index === 1) continue; // unchanged
48
- // new files: statusMatrix (1.44.2) checks ignores without our gitdir, so it misses info/exclude; isIgnored doesn't
49
- if (head === 0 && index === 0 && await git.isIgnored({ ...repo, filepath })) continue;
50
- if (workdir === 0) { if (index !== 0) await git.remove({ ...repo, filepath }); }
51
- else await git.add({ ...repo, filepath });
52
- changed.push(filepath);
53
- }
54
- return changed;
55
- }
56
-
57
- if (!fs.existsSync(GIT_DIR) && !shared.creating) shared.creating = queued(async () => { // the first time (once, though every agent loads this): the history starts with app/ as it is
58
- await git.init({ ...repo, defaultBranch: "main" });
59
- fs.writeFileSync(path.join(GIT_DIR, "info/exclude"), IGNORE.join("\n") + "\n");
60
- await stage();
61
- await git.commit({ ...repo, message: "original", author: who("chalky") });
62
- await git.tag({ ...repo, ref: "original" });
63
- });
64
- else if (fs.existsSync(GIT_DIR)) fs.writeFileSync(path.join(GIT_DIR, "info/exclude"), IGNORE.join("\n") + "\n"); // a history from before an IGNORE change
65
-
66
- const head = async () => { await shared.queue; return git.resolveRef({ ...repo, ref: "HEAD" }); }; // after queued work (e.g. the first commit)
67
-
68
- async function commit(author: string, message: string) { // the changed files, or [] if nothing changed
69
- const changed = await stage();
70
- if (!changed.length) return [];
71
- let sha: string;
72
- try { sha = await git.commit({ ...repo, message, author: who(author), disallowEmpty: true }); }
73
- catch (e: any) { if (e.code === "EmptyCommitError") return []; throw e; } // nothing really changed
74
- host.broadcast("version", { agent: author, sha: sha.slice(0, 7), message, files: changed, restart: false }); // pages take its picture
75
- return changed;
76
- }
77
-
78
- const textOf = (m: any) => typeof m.content === "string" ? m.content
79
- : (m.content ?? []).filter((c: any) => c.type === "text").map((c: any) => c.text).join("\n");
80
-
81
- export default function (pi: ExtensionAPI) {
82
- pi.on("agent_start", (_e, ctx) => { shared.turnFrom[pi.getSessionName()!] = ctx.sessionManager.getEntries().length; });
83
- pi.on("agent_settled", (_e, ctx) => {
84
- const agent = pi.getSessionName()!;
85
- const turn = ctx.sessionManager.getEntries().slice(shared.turnFrom[agent] ?? 0).filter((e: any) => e.type === "message").map((e: any) => e.message);
86
- const reason = host.shared.get(`reload-reason ${agent}`);
87
- host.shared.delete(`reload-reason ${agent}`);
88
- let message = "";
89
- for (const text of [reason ?? "", ...turn.filter((m: any) => m.role === "user").map(textOf), ...turn.filter((m: any) => m.role === "assistant").map(textOf)]) {
90
- const line = text.split("\n").map((l: string) => l.trim()).find((l: string) => l && !/^\[\w+ (from|to) /.test(l));
91
- if (line) { message = line; break; }
92
- }
93
- return queued(async () => {
94
- const changed = await commit(agent, message.slice(0, 100) || `changes by ${agent}`);
95
- // files outside the code folders that are there now (a deleted one is the agent moving it: no new warning)
96
- const stray = changed.filter((p) => !CODE.some((c) => p.startsWith(c)) && fs.existsSync(path.join(host.app, p)));
97
- if (stray.length && host.agents.has(agent))
98
- host.api.swarm?.send("system", agent, `Your last turn changed files in ${host.app} that aren't your code: ${stray.slice(0, 5).join(", ")}. `
99
- + `${host.app} is your own code (extensions, skills, ui, SYSTEM.md, mcp.json, settings.json): it is versioned, and a rollback would undo these files. `
100
- + `Move project work to ${host.work} and use it from there.`, true);
101
- });
102
- });
103
-
104
- host.routes.set("/versions", async (_req: any, res: any) => {
105
- const now = await head(); // waits for queued work, so the log is complete
106
- const out = (await git.log(repo)).map(({ oid, commit: c }) => ({ sha: oid, short: oid.slice(0, 7), author: c.author.name,
107
- date: new Date(c.author.timestamp * 1000).toISOString(), message: c.message.split("\n")[0], current: oid === now,
108
- shot: fs.existsSync(path.join(SHOTS, `${oid}.png`)) })); // the page takes the current one's picture if it has none
109
- res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(out));
110
- });
111
-
112
- host.routes.set("/versions/", async (req: any, res: any) => { // /versions/<sha>/shot, /versions/<sha>/rollback
113
- const [, , given, what] = new URL(req.url, "http://x").pathname.split("/");
114
- if (!/^[0-9a-f]{7,40}$/.test(given ?? "")) return res.writeHead(400).end();
115
- let sha: string;
116
- try { sha = await git.expandOid({ ...repo, oid: given }); } catch { return res.writeHead(404).end(); }
117
- const shot = path.join(SHOTS, `${sha}.png`);
118
- if (what === "shot" && (req.method === "GET" || req.method === "HEAD")) {
119
- if (!fs.existsSync(shot)) return res.writeHead(404).end();
120
- if (req.method === "HEAD") return res.writeHead(200, { "content-type": "image/png" }).end();
121
- return fs.createReadStream(shot).pipe(res.writeHead(200, { "content-type": "image/png" }));
122
- }
123
- if (what === "shot" && req.method === "PUT") { // a page's picture of this version
124
- if (fs.existsSync(shot)) // another page was first: not an error (a 409 would be logged by the browser)
125
- return res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ saved: false }));
126
- const chunks: Buffer[] = [];
127
- let size = 0;
128
- req.on("data", (c: Buffer) => { size += c.length; if (size <= MAX_SHOT) chunks.push(c); });
129
- req.on("end", () => {
130
- const png = Buffer.concat(chunks);
131
- if (size > MAX_SHOT || !png.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) return res.writeHead(400).end();
132
- fs.mkdirSync(SHOTS, { recursive: true });
133
- fs.writeFileSync(shot + ".tmp", png);
134
- fs.renameSync(shot + ".tmp", shot);
135
- res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ saved: true }));
136
- });
137
- return;
138
- }
139
- if (what !== "rollback" || req.method !== "POST") return res.writeHead(404).end();
140
- const subject = `${sha.slice(0, 7)} ${(await git.readCommit({ ...repo, oid: sha })).commit.message.split("\n")[0]}`;
141
- await host.api.swarm?.stop();
142
- const files = await queued(async () => {
143
- await commit("user", "uncommitted changes before rollback"); // nothing is ever lost
144
- // that commit's files into app/ and the index, removing files it doesn't have; the branch stays where it is
145
- await git.checkout({ ...repo, ref: sha, noUpdateHead: true, force: true });
146
- const files = (await git.statusMatrix(repo)).filter(([, h, w, i]) => !(h === 1 && w === 1 && i === 1)).map(([f]) => f);
147
- // git keeps files, not folders: a folder whose files the rollback removed stays, empty (an extension folder
148
- // without index.ts would show in the map). Remove those, up to the first one that isn't empty.
149
- for (const f of files) for (let d = path.dirname(f); d !== "."; d = path.dirname(d)) {
150
- try { fs.rmdirSync(path.join(host.app, d)); } catch { break; }
151
- }
152
- await git.commit({ ...repo, message: `rollback to ${subject}`, author: who("user") });
153
- return files;
154
- });
155
- for (const name of host.agents.keys()) await host.reloadAgent(name);
156
- if (files.includes("ui/surfaces.json")) { host.shared.get("ui")?.load(); host.api.uiHistory?.add("user", `rolled the code back to ${subject}`); }
157
- for (const name of host.agents.keys())
158
- host.api.swarm?.fyi("system", name, `The user rolled ${host.app} back to version ${subject}. Later changes are undone `
159
- + `(still in the history: GET http://127.0.0.1:${host.port}/versions). Check before relying on them.`);
160
- const now = await head();
161
- host.broadcast("version", { agent: "user", sha: now.slice(0, 7), message: `rollback to ${subject}`, files, restart: false });
162
- res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ ok: true }));
163
- });
164
-
165
- host.routes.set("/versions/dom-to-image-more.js", (_req: any, res: any) =>
166
- res.writeHead(200, { "content-type": "text/javascript" })
167
- .end(fs.readFileSync(path.join(host.modules, "dom-to-image-more/dist/dom-to-image-more.min.js"))));
168
- }
@@ -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,3 +0,0 @@
1
- {
2
- "mcpServers": {}
3
- }
package/app/settings.json DELETED
@@ -1,3 +0,0 @@
1
- {
2
- "compaction": { "keepRecentTokens": 20000 }
3
- }
@@ -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