@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,7 +1,7 @@
|
|
|
1
|
-
//
|
|
1
|
+
// The page side of the model extension (sign-ins and the picker): /login and /logout open a dialog in the middle of the page, as /mcp does
|
|
2
2
|
// (a click on the backdrop, Esc or ✕ closes it; closing it during a sign-in cancels the sign-in).
|
|
3
3
|
// list Sign in (pi's providers) and custom endpoints, each "connected" / "sign in again" (GET /login)
|
|
4
|
-
// sign-in what pi asks for, as it asks (event "
|
|
4
|
+
// sign-in what pi asks for, as it asks (event "model"): a link to open, a code to type, a choice, a box to paste
|
|
5
5
|
// into; ✕ cancels
|
|
6
6
|
// custom name, API type, base URL, key (optional), model names as chips (Enter or "," after each; a paste is
|
|
7
7
|
// split); "Ask the endpoint" lists its models to pick from
|
|
@@ -16,77 +16,77 @@ export default function (page) {
|
|
|
16
16
|
|
|
17
17
|
const style = document.createElement("style");
|
|
18
18
|
style.textContent = `
|
|
19
|
-
#
|
|
19
|
+
#model-sheet { width: min(560px, calc(100vw - 32px)); max-height: 75vh; padding: 0; box-sizing: border-box; color: var(--text);
|
|
20
20
|
background: var(--glass-strong); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
|
|
21
21
|
border: 1px solid var(--edge); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; font-size: 13px; }
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
22
|
+
#model-sheet[open] { display: flex; flex-direction: column; }
|
|
23
|
+
#model-sheet::backdrop { background: rgba(0, 0, 0, .08); }
|
|
24
|
+
#model-sheet:focus { outline: none; }
|
|
25
|
+
#model-sheet .body > .btns:last-child { position: sticky; bottom: -16px; margin: 0 -14px -16px; padding: 10px 14px;
|
|
26
26
|
background: var(--glass-strong); border-top: 1px solid var(--line); } /* Use / Save stay in view */
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
27
|
+
#model-sheet .head { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--line); font-weight: 600; }
|
|
28
|
+
#model-sheet .head button { border: 0; background: none; padding: 0; color: var(--accent); font: inherit; font-weight: 400; cursor: pointer; }
|
|
29
|
+
#model-sheet .head .x { margin-left: auto; color: var(--muted); font-size: 15px; }
|
|
30
|
+
#model-sheet .body { flex: 1; min-height: 0; overflow-y: auto; padding: 12px 14px 16px; display: grid; gap: 12px; align-content: start;
|
|
31
31
|
grid-auto-rows: max-content; } /* rows keep their height: the body scrolls, not each list */
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
32
|
+
#model-sheet .group { font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); margin: 4px 0 -6px; }
|
|
33
|
+
#model-sheet .rows { display: grid; border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
|
|
34
|
+
#model-sheet .row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; cursor: pointer; }
|
|
35
|
+
#model-sheet .row + .row { border-top: 1px solid var(--line); }
|
|
36
|
+
#model-sheet .row:hover { background: var(--hover); }
|
|
37
|
+
#model-sheet .row .how { margin-left: auto; font-size: 11px; color: var(--muted); white-space: nowrap; }
|
|
38
|
+
#model-sheet .dot { width: 22px; height: 22px; border-radius: 6px; display: grid; place-items: center; flex: none;
|
|
39
39
|
background: var(--hover); color: var(--soft); font-weight: 600; font-size: 11px; }
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
40
|
+
#model-sheet .chip { font-size: 10.5px; padding: 1px 7px; border-radius: 99px; white-space: nowrap; margin-left: auto; }
|
|
41
|
+
#model-sheet .chip.ok { background: rgba(52, 199, 89, 0.14); color: #1f8a3e; }
|
|
42
|
+
#model-sheet .chip.expired { background: rgba(255, 159, 10, 0.16); color: #b25e00; }
|
|
43
|
+
#model-sheet label { display: grid; gap: 5px; font-size: 12px; color: var(--soft); }
|
|
44
|
+
#model-sheet input, #model-sheet select { font: inherit; color: var(--text); background: rgba(255, 255, 255, 0.9); border: 1px solid var(--line);
|
|
45
45
|
border-radius: 9px; padding: 7px 10px; outline: none; min-width: 0; }
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
46
|
+
#model-sheet input:focus, #model-sheet select:focus, #model-sheet .tags:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.12); }
|
|
47
|
+
#model-sheet .mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 12.5px; }
|
|
48
|
+
#model-sheet .two { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
49
|
+
#model-sheet .btns { display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap; }
|
|
50
|
+
#model-sheet .btn { border: 0; border-radius: 9px; padding: 7px 14px; font: 500 13px inherit; font-family: inherit; cursor: pointer;
|
|
51
51
|
background: var(--hover); color: var(--text); text-decoration: none; }
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
52
|
+
#model-sheet .btn.primary { background: var(--accent); color: #fff; }
|
|
53
|
+
#model-sheet .btn.small { padding: 3px 10px; font-size: 12px; }
|
|
54
|
+
#model-sheet .btn:disabled { opacity: 0.5; cursor: default; }
|
|
55
|
+
#model-sheet .hint { font-size: 11.5px; color: var(--muted); }
|
|
56
|
+
#model-sheet .row .hint { white-space: nowrap; }
|
|
57
|
+
#model-sheet .error { color: var(--danger); font-size: 12.5px; }
|
|
58
|
+
#model-sheet .wait { display: flex; gap: 10px; align-items: center; color: var(--soft); }
|
|
59
|
+
#model-sheet .spin { width: 14px; height: 14px; border-radius: 50%; border: 2px solid rgba(0, 113, 227, 0.15); border-top-color: var(--accent);
|
|
60
60
|
animation: login-spin 1s linear infinite; flex: none; }
|
|
61
61
|
@keyframes login-spin { to { transform: rotate(360deg) } }
|
|
62
|
-
#
|
|
62
|
+
#model-sheet .code { font: 600 26px ui-monospace, "SF Mono", Menlo, monospace; letter-spacing: 0.12em; text-align: center; padding: 14px;
|
|
63
63
|
border-radius: 10px; background: rgba(255, 255, 255, 0.9); border: 1px dashed var(--line); user-select: all; }
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
64
|
+
#model-sheet details summary { cursor: pointer; color: var(--accent); font-size: 12.5px; }
|
|
65
|
+
#model-sheet details[open] summary { margin-bottom: 8px; }
|
|
66
|
+
#model-sheet .tags { display: flex; flex-wrap: wrap; gap: 6px; background: rgba(255, 255, 255, 0.9); border: 1px solid var(--line); border-radius: 9px; padding: 5px; }
|
|
67
|
+
#model-sheet .tags input { border: 0; box-shadow: none; padding: 3px 4px; flex: 1; min-width: 120px; background: none; }
|
|
68
|
+
#model-sheet .tag { font: 12px ui-monospace, "SF Mono", Menlo, monospace; background: rgba(0, 113, 227, 0.1); border-radius: 6px;
|
|
69
69
|
padding: 3px 4px 3px 8px; display: inline-flex; gap: 2px; align-items: center; }
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
70
|
+
#model-sheet .tag button { border: 0; background: none; color: var(--muted); cursor: pointer; padding: 0 4px; font-size: 11px; }
|
|
71
|
+
#model-sheet .found { max-height: 150px; overflow-y: auto; display: flex; flex-wrap: wrap; gap: 4px; }
|
|
72
|
+
#model-sheet .found button { border: 1px solid var(--line); background: none; border-radius: 6px; padding: 2px 7px; cursor: pointer;
|
|
73
73
|
font: 11.5px ui-monospace, "SF Mono", Menlo, monospace; color: var(--soft); }
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
74
|
+
#model-sheet .found button:hover { background: var(--hover); color: var(--text); }
|
|
75
|
+
#model-sheet .for { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; color: var(--soft); }
|
|
76
|
+
#model-sheet .seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
|
|
77
|
+
#model-sheet .seg button { border: 0; background: none; padding: 3px 10px; font: inherit; font-size: 12px; cursor: pointer; color: var(--text); }
|
|
78
|
+
#model-sheet .seg button.on { background: var(--accent); color: #fff; }
|
|
79
|
+
#model-sheet .radio { width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid var(--muted); flex: none; box-sizing: border-box; }
|
|
80
|
+
#model-sheet .row.on .radio { border: 4.5px solid var(--accent); }
|
|
81
|
+
#model-sheet .model-id { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 12.5px; overflow-wrap: anywhere; }`;
|
|
82
82
|
document.head.append(style);
|
|
83
83
|
|
|
84
84
|
const sheet = document.createElement("dialog");
|
|
85
|
-
sheet.id = "
|
|
85
|
+
sheet.id = "model-sheet";
|
|
86
86
|
document.body.append(sheet);
|
|
87
87
|
sheet.onclick = (e) => { if (e.target === sheet) sheet.close(); }; // a click on the backdrop closes it
|
|
88
88
|
sheet.onclose = () => { // ✕, Esc, the backdrop, Done
|
|
89
|
-
if (view === "signin") page.send({ type: "
|
|
89
|
+
if (view === "signin") page.send({ type: "model_cancel" });
|
|
90
90
|
view = signing = form = null;
|
|
91
91
|
};
|
|
92
92
|
|
|
@@ -109,7 +109,7 @@ export default function (page) {
|
|
|
109
109
|
}
|
|
110
110
|
const chip = (status) => status === "connected" ? `<span class="chip ok">connected</span>`
|
|
111
111
|
: status === "expired" ? `<span class="chip expired">sign in again</span>` : "";
|
|
112
|
-
const fetchList = () => fetch("/
|
|
112
|
+
const fetchList = () => fetch("/model/logins").then((r) => r.ok ? r.json() : Promise.reject(new Error(`/model/logins: ${r.status}`)));
|
|
113
113
|
|
|
114
114
|
// ---- the list
|
|
115
115
|
async function showList() {
|
|
@@ -145,9 +145,9 @@ export default function (page) {
|
|
|
145
145
|
view = "signin";
|
|
146
146
|
signing = { id, name };
|
|
147
147
|
const body = draw(name, `<div class="link"></div><div class="wait"><span class="spin"></span><span class="msg">Starting…</span></div>
|
|
148
|
-
<div class="ask"></div><div class="error"></div>`, () => { page.send({ type: "
|
|
148
|
+
<div class="ask"></div><div class="error"></div>`, () => { page.send({ type: "model_cancel" }); showList(); });
|
|
149
149
|
body.dataset.provider = id;
|
|
150
|
-
page.send({ type: "
|
|
150
|
+
page.send({ type: "model_login", provider: id });
|
|
151
151
|
}
|
|
152
152
|
function onSignIn(ev) {
|
|
153
153
|
const body = sheet.querySelector(".body");
|
|
@@ -173,7 +173,7 @@ export default function (page) {
|
|
|
173
173
|
if (p.type === "select") {
|
|
174
174
|
ask.innerHTML = `<div class="hint">${esc(p.message)}</div><div class="rows">${p.options.map((o) =>
|
|
175
175
|
`<div class="row" data-id="${esc(o.id)}">${esc(o.label)}</div>`).join("")}</div>`;
|
|
176
|
-
for (const r of ask.querySelectorAll(".row")) r.onclick = () => { ask.innerHTML = ""; page.send({ type: "
|
|
176
|
+
for (const r of ask.querySelectorAll(".row")) r.onclick = () => { ask.innerHTML = ""; page.send({ type: "model_answer", text: r.dataset.id }); };
|
|
177
177
|
msg.textContent = "Choose one:";
|
|
178
178
|
return;
|
|
179
179
|
}
|
|
@@ -181,7 +181,7 @@ export default function (page) {
|
|
|
181
181
|
const field = `<label>${esc(p.message)}<input class="mono" type="${p.type === "secret" ? "password" : "text"}"
|
|
182
182
|
placeholder="${esc(p.placeholder ?? "")}"></label><div class="btns"><button class="btn primary">Continue</button></div>`;
|
|
183
183
|
ask.innerHTML = p.type === "manual_code" ? `<details><summary>The page after signing in didn't load?</summary>${field}</details>` : field;
|
|
184
|
-
const input = ask.querySelector("input"), go = () => { page.send({ type: "
|
|
184
|
+
const input = ask.querySelector("input"), go = () => { page.send({ type: "model_answer", text: input.value }); ask.innerHTML = ""; msg.textContent = "Checking…"; };
|
|
185
185
|
ask.querySelector(".btn").onclick = go;
|
|
186
186
|
input.onkeydown = (e) => { if (e.key === "Enter") go(); };
|
|
187
187
|
if (p.type !== "manual_code") input.focus();
|
|
@@ -238,13 +238,13 @@ export default function (page) {
|
|
|
238
238
|
body.querySelector(".ask").onclick = () => {
|
|
239
239
|
body.querySelector(".error").textContent = "";
|
|
240
240
|
found.innerHTML = `<span class="hint">Asking…</span>`;
|
|
241
|
-
page.send({ type: "
|
|
241
|
+
page.send({ type: "model_probe", id: c?.id, api: value(".api"), baseUrl: value(".url"), key: value(".key") });
|
|
242
242
|
};
|
|
243
243
|
body.querySelector(".save").onclick = () => {
|
|
244
244
|
if (add.value.trim()) addNames(add.value.split(",")); // names typed but not yet added
|
|
245
245
|
body.querySelector(".error").textContent = "";
|
|
246
246
|
body.querySelector(".save").disabled = true;
|
|
247
|
-
page.send({ type: "
|
|
247
|
+
page.send({ type: "model_custom", id: value(".id"), api: value(".api"), baseUrl: value(".url"), key: value(".key"), models: form.models });
|
|
248
248
|
};
|
|
249
249
|
(c ? add : body.querySelector(".id")).focus();
|
|
250
250
|
}
|
|
@@ -260,11 +260,11 @@ export default function (page) {
|
|
|
260
260
|
...data.custom.map((c) => ({ id: c.id, name: `${c.id} (custom)`, how: c.status ? "key" : "no key", act: "Remove" }))];
|
|
261
261
|
body.innerHTML = rows.length ? `<div class="rows">${rows.map((r) => `<div class="row" data-id="${esc(r.id)}"><span class="dot">${esc(r.name[0].toUpperCase())}</span>
|
|
262
262
|
${esc(r.name)}<span class="how">${r.how}</span><button class="btn small">${r.act}</button></div>`).join("")}</div>
|
|
263
|
-
<div class="hint">Log out removes the login from this machine (
|
|
263
|
+
<div class="hint">Log out removes the login from this machine (agent/auth.json). Remove also deletes the endpoint.</div>`
|
|
264
264
|
: `<div class="hint">No saved logins. Type /login to add one.</div>`;
|
|
265
265
|
for (const b of body.querySelectorAll(".row .btn")) b.onclick = () => {
|
|
266
266
|
b.disabled = true;
|
|
267
|
-
page.send({ type: "
|
|
267
|
+
page.send({ type: "model_logout", provider: b.closest(".row").dataset.id });
|
|
268
268
|
};
|
|
269
269
|
}
|
|
270
270
|
|
|
@@ -273,7 +273,7 @@ export default function (page) {
|
|
|
273
273
|
view = "model";
|
|
274
274
|
const body = draw(title, `<div class="wait"><span class="spin"></span>Loading models…</div>`);
|
|
275
275
|
let data;
|
|
276
|
-
try { data = await fetch("/
|
|
276
|
+
try { data = await fetch("/model/choices").then((r) => r.ok ? r.json() : Promise.reject(new Error(`/model/choices: ${r.status}`))); }
|
|
277
277
|
catch (e) { body.innerHTML = `<div class="error">${esc(e.message)}</div>`; return; }
|
|
278
278
|
if (view !== "model") return;
|
|
279
279
|
const key = (m) => m ? `${m.provider}/${m.id}` : "same";
|
|
@@ -326,7 +326,7 @@ export default function (page) {
|
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
// ---- what the server says
|
|
329
|
-
page.on("
|
|
329
|
+
page.on("model", (ev) => {
|
|
330
330
|
if (!sheet.open) return;
|
|
331
331
|
const err = sheet.querySelector(".error");
|
|
332
332
|
if (ev.step === "error") {
|
|
@@ -354,7 +354,7 @@ export default function (page) {
|
|
|
354
354
|
onSignIn(ev);
|
|
355
355
|
});
|
|
356
356
|
|
|
357
|
-
fetch("/
|
|
357
|
+
fetch("/model/on").then((r) => { // CHALKY_LOGIN=off: the server has no sign-ins, and the page no commands
|
|
358
358
|
if (!r.ok) return;
|
|
359
359
|
page.command("login", () => open(showList), "Connect a model: sign in with a subscription, or a custom endpoint");
|
|
360
360
|
page.command("logout", () => open(showLogout), "Remove a saved login");
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
// The model: one
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// The model the agents use: the one picked on the page (login.ts: sign-ins, custom endpoints, the picker; browser.js:
|
|
2
|
+
// /login, /logout, /model), else the endpoint set in the environment (here): one OpenAI-compatible endpoint (in
|
|
3
|
+
// Chalky: keyproxy -> LiteLLM). MODEL_BASE_URL (up to /v1), MODEL_API_KEY, MODEL_NAME (a LiteLLM route, "default").
|
|
4
|
+
// pi does the calls, streaming, retries, thinking and compaction; this only tells it where. The host selects provider
|
|
5
|
+
// "chalky" when nothing was picked (server.ts). MODEL_CONTEXT_WINDOW: the model's context size; pi compacts when a
|
|
6
|
+
// request would leave less than 16384 tokens of it.
|
|
5
7
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import login from "./login.ts";
|
|
6
9
|
|
|
7
10
|
export default function (pi: ExtensionAPI) {
|
|
11
|
+
login(pi); // CHALKY_LOGIN=off (a cloud deployment): no sign-ins or picker, only this endpoint
|
|
8
12
|
if (!process.env.MODEL_BASE_URL) return; // pi then says no model is configured on each message
|
|
9
13
|
pi.registerProvider("chalky", {
|
|
10
14
|
baseUrl: process.env.MODEL_BASE_URL,
|
|
@@ -1,38 +1,39 @@
|
|
|
1
|
-
//
|
|
1
|
+
// The model extension's sign-ins and model picker (index.ts loads this; browser.js: the page's /login, /logout, /model):
|
|
2
|
+
// connect models from the page and pick one. pi's own login does the work, the call its
|
|
2
3
|
// terminal /login makes (modelRuntime.login): a sign-in (OAuth: pi opens the provider's page and waits on a callback
|
|
3
4
|
// server, or asks for the address to be pasted), or a custom endpoint (any OpenAI-, Anthropic- or Google-compatible
|
|
4
5
|
// URL, its model names, an optional key). pi renews sign-ins itself; an expired one shows as pi's own error.
|
|
5
|
-
// Where things go: tokens and keys in
|
|
6
|
-
// (pi's; no secrets), this install's device id (OpenAI's sign-in asks for one) and the model choices in
|
|
6
|
+
// Where things go: tokens and keys in agent/auth.json (pi's; never versioned), custom endpoints in agent/models.json
|
|
7
|
+
// (pi's; no secrets), this install's device id (OpenAI's sign-in asks for one) and the model choices in data/model.json:
|
|
7
8
|
// {deviceId, default: {provider, id} (all agents), agents: {name: {provider, id}} (one agent's own; it wins)}.
|
|
8
|
-
// server.ts asks host.api.
|
|
9
|
-
// one, else nothing (then the MODEL_BASE_URL endpoint,
|
|
9
|
+
// server.ts asks host.api.model.modelFor(name, session) when it starts an agent: its own choice, else the all-agents
|
|
10
|
+
// one, else nothing (then the MODEL_BASE_URL endpoint, index.ts); a choice whose login is gone is skipped.
|
|
10
11
|
// CHALKY_LOGIN=off (a cloud deployment, where the backend fixes the endpoint): none of this loads, and server.ts doesn't ask.
|
|
11
|
-
// GET /
|
|
12
|
-
// GET /
|
|
13
|
-
// GET /
|
|
12
|
+
// GET /model/on 204 (so the page knows sign-ins are on)
|
|
13
|
+
// GET /model/choices {groups: [{provider, name, models: [{id, name, context}]}] (usable now), default, agents: [{name, model, own}]}
|
|
14
|
+
// GET /model/logins {providers: [{id, name, subscription, status}], custom: [{id, baseUrl, api, models, status}]}
|
|
14
15
|
// status: "connected" | "expired" | undefined; each call checks the stored sign-ins (pi renews one that is due)
|
|
15
|
-
// page messages
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// event "
|
|
19
|
-
// (pi asks: the page answers with
|
|
16
|
+
// page messages model_login {provider}, model_answer {text}, model_cancel,
|
|
17
|
+
// model_custom {id, api, baseUrl, key?, models: [names]}, model_probe {baseUrl, api, key?, id?},
|
|
18
|
+
// model_logout {provider}, model_set {for: "all" | <agent>, provider?, id?} (for an agent, no provider: back to all agents')
|
|
19
|
+
// event "model" {step: "notify", event} (pi: open this link / enter this code / waiting) | {step: "prompt", prompt}
|
|
20
|
+
// (pi asks: the page answers with model_answer) | {step: "prompt_done"} (pi stopped asking: the
|
|
20
21
|
// callback came first) | {step: "done", provider} | {step: "cancelled"} | {step: "error", text} | {step: "models", models}
|
|
21
|
-
// (
|
|
22
|
+
// (model_probe's answer) | {step: "changed"} (a login, endpoint or model changed: agents are synced)
|
|
22
23
|
import fs from "node:fs";
|
|
23
24
|
import path from "node:path";
|
|
24
25
|
import { randomUUID } from "node:crypto";
|
|
25
26
|
import { ModelRuntime, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
26
27
|
|
|
27
28
|
const host = (globalThis as any).chalky;
|
|
28
|
-
const MODELS = path.join(host.
|
|
29
|
-
const STATE = path.join(host.
|
|
29
|
+
const MODELS = path.join(host.agent, "models.json");
|
|
30
|
+
const STATE = path.join(host.data, "model.json");
|
|
30
31
|
const APIS = ["openai-completions", "openai-responses", "anthropic-messages", "google-generative-ai"];
|
|
31
32
|
|
|
32
33
|
// one runtime for logins, on the same files as the agents' (each agent's session has its own: synced after a change)
|
|
33
|
-
if (!host.shared.has("
|
|
34
|
-
host.shared.set("
|
|
35
|
-
const runtime = (): Promise<ModelRuntime> => host.shared.get("
|
|
34
|
+
if (!host.shared.has("model-runtime"))
|
|
35
|
+
host.shared.set("model-runtime", ModelRuntime.create({ authPath: path.join(host.agent, "auth.json"), modelsPath: MODELS }));
|
|
36
|
+
const runtime = (): Promise<ModelRuntime> => host.shared.get("model-runtime");
|
|
36
37
|
|
|
37
38
|
const readModels = () => (fs.existsSync(MODELS) ? JSON.parse(fs.readFileSync(MODELS, "utf8")) : { providers: {} });
|
|
38
39
|
const readState = () => (fs.existsSync(STATE) ? JSON.parse(fs.readFileSync(STATE, "utf8")) : {});
|
|
@@ -50,13 +51,13 @@ function modelFor(name: string, session: any) {
|
|
|
50
51
|
if (c && rt.hasConfiguredAuth(c.provider)) { const m = rt.getModel(c.provider, c.id); if (m) return m; }
|
|
51
52
|
return undefined;
|
|
52
53
|
}
|
|
53
|
-
const fallback = (session: any) => session.modelRuntime.getModel("chalky", process.env.MODEL_NAME || "default"); //
|
|
54
|
+
const fallback = (session: any) => session.modelRuntime.getModel("chalky", process.env.MODEL_NAME || "default"); // index.ts
|
|
54
55
|
async function apply(a: any) { // put an agent on the model it should use now (after a choice, a login or a logout)
|
|
55
56
|
const m = modelFor(a.name, a.session) ?? fallback(a.session);
|
|
56
57
|
const cur = a.session.model;
|
|
57
58
|
if (m && !(cur && cur.provider === m.provider && cur.id === m.id)) await a.session.setModel(m);
|
|
58
59
|
}
|
|
59
|
-
host.api.
|
|
60
|
+
host.api.model = { modelFor };
|
|
60
61
|
|
|
61
62
|
async function status(rt: ModelRuntime, id: string) {
|
|
62
63
|
if (!rt.getProviderAuthStatus(id).configured) return undefined;
|
|
@@ -66,25 +67,25 @@ async function status(rt: ModelRuntime, id: string) {
|
|
|
66
67
|
async function synced() { // a login or endpoint changed: every runtime reads the files again (offline)
|
|
67
68
|
await (await runtime()).refresh();
|
|
68
69
|
for (const a of host.agents.values()) { await a.session.modelRuntime.refresh(); await apply(a); }
|
|
69
|
-
host.broadcast("
|
|
70
|
+
host.broadcast("model", { step: "changed" });
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
const fail = (e: any) => host.broadcast("
|
|
73
|
+
const fail = (e: any) => host.broadcast("model", { step: "error", text: e?.message ?? String(e) });
|
|
73
74
|
|
|
74
75
|
export default function (pi: ExtensionAPI) {
|
|
75
76
|
if (process.env.CHALKY_LOGIN === "off") return;
|
|
76
77
|
|
|
77
78
|
// at start, once: renew the sign-ins that are due, and say which failed (server log)
|
|
78
79
|
pi.on("session_start", async () => {
|
|
79
|
-
if (host.shared.has("
|
|
80
|
-
host.shared.set("
|
|
80
|
+
if (host.shared.has("model-checked")) return;
|
|
81
|
+
host.shared.set("model-checked", true);
|
|
81
82
|
const rt = await runtime();
|
|
82
83
|
for (const c of await rt.listCredentials())
|
|
83
84
|
if (c.type === "oauth" && (await status(rt, c.providerId)) === "expired") console.error(`login: the ${c.providerId} sign-in has expired (/login)`);
|
|
84
85
|
});
|
|
85
86
|
|
|
86
|
-
host.routes.set("/
|
|
87
|
-
host.routes.set("/
|
|
87
|
+
host.routes.set("/model/on", (_req: any, res: any) => res.writeHead(204).end()); // browser.js: add the commands only when this answers
|
|
88
|
+
host.routes.set("/model/choices", (_req: any, res: any) => models(res).catch((e) =>
|
|
88
89
|
res.writeHead(500, { "content-type": "text/plain" }).end(String(e?.message ?? e))));
|
|
89
90
|
async function models(res: any) { // read through an agent's runtime: it also has the MODEL_BASE_URL endpoint ("chalky")
|
|
90
91
|
const agents = [...host.agents.values()], s = readState();
|
|
@@ -101,7 +102,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
101
102
|
own: !!s.agents?.[a.name] })) }));
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
host.routes.set("/
|
|
105
|
+
host.routes.set("/model/logins", (_req: any, res: any) => list(res).catch((e) =>
|
|
105
106
|
res.writeHead(500, { "content-type": "text/plain" }).end(String(e?.message ?? e))));
|
|
106
107
|
async function list(res: any) {
|
|
107
108
|
const rt = await runtime();
|
|
@@ -115,35 +116,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
115
116
|
.end(JSON.stringify({ providers: providers.sort((a, b) => a.name.localeCompare(b.name)), custom: list }));
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
host.pageHandlers.set("
|
|
119
|
-
const flow = host.shared.get("
|
|
120
|
-
if (msg.type === "
|
|
121
|
-
if (msg.type === "
|
|
119
|
+
host.pageHandlers.set("model", async (msg: any) => {
|
|
120
|
+
const flow = host.shared.get("model-flow"); // the sign-in under way: {abort, answer?}
|
|
121
|
+
if (msg.type === "model_answer") return flow?.answer?.(String(msg.text ?? ""));
|
|
122
|
+
if (msg.type === "model_cancel") return flow?.abort.abort();
|
|
122
123
|
|
|
123
|
-
if (msg.type === "
|
|
124
|
+
if (msg.type === "model_login") {
|
|
124
125
|
flow?.abort.abort(); // one at a time
|
|
125
126
|
const abort = new AbortController(), me: any = { abort };
|
|
126
|
-
host.shared.set("
|
|
127
|
+
host.shared.set("model-flow", me);
|
|
127
128
|
try {
|
|
128
129
|
await (await runtime()).login(String(msg.provider), "oauth", {
|
|
129
130
|
signal: abort.signal,
|
|
130
|
-
notify: (event) => host.broadcast("
|
|
131
|
+
notify: (event) => host.broadcast("model", { step: "notify", event }),
|
|
131
132
|
prompt: ({ signal, ...prompt }) => new Promise((resolve, reject) => {
|
|
132
133
|
const signals = [signal, abort.signal].filter((s) => s) as AbortSignal[];
|
|
133
134
|
const end = () => { me.answer = undefined; for (const s of signals) s.removeEventListener("abort", stop); };
|
|
134
135
|
me.answer = (text: string) => { end(); resolve(text); };
|
|
135
|
-
const stop = () => { end(); host.broadcast("
|
|
136
|
+
const stop = () => { end(); host.broadcast("model", { step: "prompt_done" }); reject(new Error("Login cancelled")); };
|
|
136
137
|
for (const s of signals) s.addEventListener("abort", stop, { once: true });
|
|
137
|
-
host.broadcast("
|
|
138
|
+
host.broadcast("model", { step: "prompt", prompt });
|
|
138
139
|
}),
|
|
139
140
|
}, { getDeviceId: deviceId });
|
|
140
141
|
await synced();
|
|
141
|
-
host.broadcast("
|
|
142
|
-
} catch (e) { if (abort.signal.aborted) host.broadcast("
|
|
143
|
-
finally { if (host.shared.get("
|
|
142
|
+
host.broadcast("model", { step: "done", provider: msg.provider });
|
|
143
|
+
} catch (e) { if (abort.signal.aborted) host.broadcast("model", { step: "cancelled" }); else fail(e); } // ✕: not an error
|
|
144
|
+
finally { if (host.shared.get("model-flow") === me) host.shared.delete("model-flow"); }
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
if (msg.type === "
|
|
147
|
+
if (msg.type === "model_custom") {
|
|
147
148
|
const id = String(msg.id ?? "").trim(), baseUrl = String(msg.baseUrl ?? "").trim().replace(/\/+$/, "");
|
|
148
149
|
const models = [...new Set<string>((msg.models ?? []).map((m: any) => String(m).trim()).filter(Boolean))];
|
|
149
150
|
const rt = await runtime(), saved = readModels();
|
|
@@ -161,11 +162,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
161
162
|
await rt.refresh();
|
|
162
163
|
if (key) await rt.login(id, "api_key", { prompt: async () => key, notify() {} }); // pi saves it in auth.json
|
|
163
164
|
await synced();
|
|
164
|
-
host.broadcast("
|
|
165
|
+
host.broadcast("model", { step: "done", provider: id });
|
|
165
166
|
} catch (e) { fail(e); }
|
|
166
167
|
}
|
|
167
168
|
|
|
168
|
-
if (msg.type === "
|
|
169
|
+
if (msg.type === "model_probe") { // the endpoint's own list (GET <baseUrl>/models); the key never goes to the page
|
|
169
170
|
try {
|
|
170
171
|
const rt = await runtime(), baseUrl = String(msg.baseUrl ?? "").trim().replace(/\/+$/, "");
|
|
171
172
|
const key = String(msg.key ?? "").trim() || (msg.id && rt.getProviderAuthStatus(msg.id).configured ? (await rt.getAuth(msg.id))?.auth.apiKey : "");
|
|
@@ -175,7 +176,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
175
176
|
if (!r.ok) return fail(`${baseUrl}/models answered ${r.status} ${r.statusText}`);
|
|
176
177
|
const body = await r.json();
|
|
177
178
|
const models = (body.data ?? body.models ?? []).map((m: any) => m.id ?? m.name?.replace(/^models\//, "")).filter(Boolean);
|
|
178
|
-
host.broadcast("
|
|
179
|
+
host.broadcast("model", { step: "models", models });
|
|
179
180
|
} catch (e) { fail(e); }
|
|
180
181
|
}
|
|
181
182
|
|
|
@@ -195,11 +196,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
195
196
|
writeState(s);
|
|
196
197
|
try {
|
|
197
198
|
for (const a of host.agents.values()) await apply(a);
|
|
198
|
-
host.broadcast("
|
|
199
|
+
host.broadcast("model", { step: "changed" });
|
|
199
200
|
} catch (e) { fail(e); }
|
|
200
201
|
}
|
|
201
202
|
|
|
202
|
-
if (msg.type === "
|
|
203
|
+
if (msg.type === "model_logout") {
|
|
203
204
|
const id = String(msg.provider ?? ""), rt = await runtime(), saved = readModels();
|
|
204
205
|
try {
|
|
205
206
|
if ((await rt.listCredentials()).some((c) => c.providerId === id)) await rt.logout(id);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Notes on the page (the note extension)
|
|
2
|
+
A note is a thread pinned on a thing on the page: a numbered pin, and a bubble with who wrote what, a reply box and
|
|
3
|
+
✓ Done. You and the user can both start one, and both write in it.
|
|
4
|
+
|
|
5
|
+
## The user's notes: "[note on the page] <their words>"
|
|
6
|
+
They clicked something (or drew around it, or dropped files there) and wrote. You get the target: the card and the
|
|
7
|
+
block (the chain of block ids, outermost first), the element they clicked (inside app frames too), the part (if the
|
|
8
|
+
thing has partAt), its text, and the file it comes from. Act on that exact thing.
|
|
9
|
+
- If the text is data (a table row, a score), change the card's data, not the block or the app's code.
|
|
10
|
+
- "here" means the spot: to add something there, card_show(..., place={"x", "y"}) with its position, and keep
|
|
11
|
+
it small (as wide as its content). On empty space the spot is all there is to the target.
|
|
12
|
+
- A note with an "area" came with a drawing: the user circled that box. On empty space, build what they ask for
|
|
13
|
+
inside it: card_show(..., place={"x", "y", "width", "height"}) from the area. On a card, the drawing marks the
|
|
14
|
+
part of it the note is about: change that part, and leave the card's place and size alone.
|
|
15
|
+
If the thing drawn on can say what is inside the drawing (partIn), the part lists exactly what was circled
|
|
16
|
+
(cells and values, paragraphs, lines, a picture of the area): the note is about that.
|
|
17
|
+
- A note with "files": the user dropped those files there, from their computer or their file panel (all in
|
|
18
|
+
{workspace}/drops/). Look at them (read shows you pictures) and do what the note says with them, at that spot
|
|
19
|
+
or on that card.
|
|
20
|
+
- "[combine on the page]": the user dragged card A onto card B and said how to combine them. Change B so it takes
|
|
21
|
+
A in, as they said: card_show(card=B, ..., made_from=<B's made_from plus A>, instruction=<their words>,
|
|
22
|
+
about=<updated>). Read A's data with {"ref": "A", "path": "/..."} props, so it stays live; copy only what has to
|
|
23
|
+
change shape. Never change or remove A: the user closes it when they want (B then gets a copy of what it read,
|
|
24
|
+
automatically). Keep what B already showed unless they said otherwise.
|
|
25
|
+
- Pointing is only as precise as your names: give the cards, blocks and elements you build a clear id or data-name.
|
|
26
|
+
|
|
27
|
+
## Threads
|
|
28
|
+
- Answer a note in its thread: note_reply(id, text), any length. Don't repeat it in chat; your chat can be empty.
|
|
29
|
+
- Their reply in a thread comes as "[note reply on the page] <words>", with the note's id and the thread so far.
|
|
30
|
+
- Start a thread yourself with note_add(on, text) when there is something to explain or ask about one thing
|
|
31
|
+
("this total leaves out tax: add it?"). on is as doodle_mark's: {card, block, element, text, mark}. For a short
|
|
32
|
+
pointer ("this one") use the pen (doodle_mark) instead.
|
|
33
|
+
- When you did what a note asked, note_done(id): its pin turns into a faded ✓, which the user can reopen or delete. If your
|
|
34
|
+
answer leaves anything to them (you suggest something, ask, or offer a choice), leave the note open: their reply
|
|
35
|
+
comes in the same thread, and you close it once that is settled. The user can close one too (✓ Done); you get an fyi.
|