@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.
Files changed (104) hide show
  1. package/README.md +10 -5
  2. package/{app → agent}/SYSTEM.md +15 -11
  3. package/agent/extensions/app/README.md +68 -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/{app/extensions/boards → agent/extensions/board}/browser.js +25 -23
  9. package/{app/extensions/boards → agent/extensions/board}/guide.md +1 -1
  10. package/{app/extensions/boards → agent/extensions/board}/index.ts +32 -30
  11. package/agent/extensions/card/README.md +88 -0
  12. package/agent/extensions/card/blocks/Button.js +34 -0
  13. package/{app/extensions/ui/components → agent/extensions/card/blocks}/Column.js +2 -2
  14. package/agent/extensions/card/blocks/Markdown.js +37 -0
  15. package/{app/extensions/ui/components/Card.js → agent/extensions/card/blocks/Panel.js} +3 -3
  16. package/{app/extensions/ui/components → agent/extensions/card/blocks}/Row.js +2 -2
  17. package/{app/extensions/ui/components → agent/extensions/card/blocks}/TextField.js +3 -3
  18. package/{app/extensions/ui → agent/extensions/card}/browser.js +155 -129
  19. package/agent/extensions/card/guide.md +13 -0
  20. package/agent/extensions/card/index.ts +370 -0
  21. package/{app/extensions/commands → agent/extensions/command}/browser.js +3 -3
  22. package/agent/extensions/command/guide.md +2 -0
  23. package/{app/extensions/commands → agent/extensions/command}/index.ts +14 -16
  24. package/agent/extensions/doodle/README.md +57 -0
  25. package/agent/extensions/doodle/browser.js +1013 -0
  26. package/agent/extensions/doodle/guide.md +22 -0
  27. package/agent/extensions/doodle/index.ts +274 -0
  28. package/agent/extensions/file/browser.js +143 -0
  29. package/agent/extensions/file/guide.md +5 -0
  30. package/{app/extensions/files → agent/extensions/file}/index.ts +34 -16
  31. package/{app/extensions/frames → agent/extensions/frame}/README.md +13 -13
  32. package/{app/extensions/frames → agent/extensions/frame}/browser.js +20 -18
  33. package/agent/extensions/frame/guide.md +3 -0
  34. package/{app/extensions/frames → agent/extensions/frame}/index.ts +27 -31
  35. package/{app/extensions/ui_history → agent/extensions/history}/browser.js +18 -18
  36. package/{app/extensions/ui_history → agent/extensions/history}/index.ts +32 -33
  37. package/{app → agent}/extensions/mcp/browser.js +1 -1
  38. package/agent/extensions/mcp/guide.md +3 -0
  39. package/{app → agent}/extensions/mcp/index.ts +2 -4
  40. package/{app/extensions/login → agent/extensions/model}/browser.js +67 -67
  41. package/{app → agent}/extensions/model/index.ts +8 -4
  42. package/{app/extensions/login/index.ts → agent/extensions/model/login.ts} +47 -46
  43. package/agent/extensions/note/README.md +35 -0
  44. package/agent/extensions/note/browser.js +356 -0
  45. package/agent/extensions/note/guide.md +8 -0
  46. package/agent/extensions/note/index.ts +192 -0
  47. package/{app → agent}/extensions/page/index.ts +2 -2
  48. package/{app → agent}/extensions/page/page.html +55 -49
  49. package/agent/extensions/prompt/index.ts +38 -0
  50. package/{app → agent}/extensions/reload/index.ts +3 -3
  51. package/{app → agent}/extensions/screen/browser.js +4 -4
  52. package/{app → agent}/extensions/screen/index.ts +11 -11
  53. package/{app/extensions/steps → agent/extensions/step}/index.ts +1 -1
  54. package/{app/extensions/swarm → agent/extensions/team}/README.md +19 -19
  55. package/{app/extensions/swarm → agent/extensions/team}/guide.md +4 -4
  56. package/{app/extensions/swarm → agent/extensions/team}/index.ts +41 -66
  57. package/{app/extensions/versions → agent/extensions/version}/browser.js +10 -10
  58. package/{app/extensions/versions → agent/extensions/version}/index.ts +39 -54
  59. package/{app → agent}/extensions/viewer/README.md +4 -4
  60. package/{app → agent}/extensions/viewer/browser.js +8 -8
  61. package/agent/extensions/viewer/guide.md +3 -0
  62. package/{app → agent}/extensions/viewer/index.ts +22 -17
  63. package/{app → agent}/extensions/voice/browser.js +4 -3
  64. package/{app → agent}/extensions/voice/index.ts +8 -6
  65. package/{app/extensions/ui → agent/extensions/voice}/voice.js +5 -5
  66. package/agent/settings.json +8 -0
  67. package/{app/extensions/ui/skills/ui-check → agent/skills/browser-check}/SKILL.md +4 -4
  68. package/{app/extensions/ui/skills/ui-check → agent/skills/browser-check}/scripts/check.mjs +1 -1
  69. package/{app/extensions/ui/skills/ui-design → agent/skills/design}/SKILL.md +14 -14
  70. package/agent/skills/extension-map/SKILL.md +56 -0
  71. package/{app → agent}/skills/write-skill/SKILL.md +1 -1
  72. package/bin/chalky.mjs +2 -2
  73. package/package.json +8 -8
  74. package/server.ts +151 -58
  75. package/app/extensions/appdata/README.md +0 -10
  76. package/app/extensions/appdata/guide.md +0 -4
  77. package/app/extensions/appdata/index.ts +0 -47
  78. package/app/extensions/commands/guide.md +0 -3
  79. package/app/extensions/doodle/README.md +0 -15
  80. package/app/extensions/doodle/browser.js +0 -398
  81. package/app/extensions/doodle/guide.md +0 -6
  82. package/app/extensions/doodle/index.ts +0 -54
  83. package/app/extensions/files/browser.js +0 -96
  84. package/app/extensions/files/guide.md +0 -7
  85. package/app/extensions/frames/guide.md +0 -7
  86. package/app/extensions/mcp/guide.md +0 -5
  87. package/app/extensions/point/README.md +0 -22
  88. package/app/extensions/point/browser.js +0 -247
  89. package/app/extensions/point/guide.md +0 -6
  90. package/app/extensions/point/index.ts +0 -112
  91. package/app/extensions/prompt/index.ts +0 -19
  92. package/app/extensions/ui/README.md +0 -69
  93. package/app/extensions/ui/components/App.js +0 -28
  94. package/app/extensions/ui/components/Button.js +0 -28
  95. package/app/extensions/ui/components/Markdown.js +0 -37
  96. package/app/extensions/ui/guide.md +0 -7
  97. package/app/extensions/ui/index.ts +0 -291
  98. package/app/extensions/viewer/guide.md +0 -5
  99. package/app/mcp.json +0 -3
  100. package/app/settings.json +0 -3
  101. package/app/skills/plugin-map/SKILL.md +0 -49
  102. package/app/ui/apps/.keep +0 -0
  103. /package/{app/extensions/ui/components → agent/extensions/card/blocks}/Table.js +0 -0
  104. /package/{app/extensions/ui/components → agent/extensions/card/blocks}/Text.js +0 -0
@@ -1,7 +1,7 @@
1
- // Browser half of the login extension: /login and /logout open a dialog in the middle of the page, as /mcp does
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 "login"): a link to open, a code to type, a choice, a box to paste
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
- #login { width: min(560px, calc(100vw - 32px)); max-height: 75vh; padding: 0; box-sizing: border-box; color: var(--text);
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
- #login[open] { display: flex; flex-direction: column; }
23
- #login::backdrop { background: rgba(0, 0, 0, .08); }
24
- #login:focus { outline: none; }
25
- #login .body > .btns:last-child { position: sticky; bottom: -16px; margin: 0 -14px -16px; padding: 10px 14px;
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
- #login .head { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--line); font-weight: 600; }
28
- #login .head button { border: 0; background: none; padding: 0; color: var(--accent); font: inherit; font-weight: 400; cursor: pointer; }
29
- #login .head .x { margin-left: auto; color: var(--muted); font-size: 15px; }
30
- #login .body { flex: 1; min-height: 0; overflow-y: auto; padding: 12px 14px 16px; display: grid; gap: 12px; align-content: start;
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
- #login .group { font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); margin: 4px 0 -6px; }
33
- #login .rows { display: grid; border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
34
- #login .row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; cursor: pointer; }
35
- #login .row + .row { border-top: 1px solid var(--line); }
36
- #login .row:hover { background: var(--hover); }
37
- #login .row .how { margin-left: auto; font-size: 11px; color: var(--muted); white-space: nowrap; }
38
- #login .dot { width: 22px; height: 22px; border-radius: 6px; display: grid; place-items: center; flex: none;
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
- #login .chip { font-size: 10.5px; padding: 1px 7px; border-radius: 99px; white-space: nowrap; margin-left: auto; }
41
- #login .chip.ok { background: rgba(52, 199, 89, 0.14); color: #1f8a3e; }
42
- #login .chip.expired { background: rgba(255, 159, 10, 0.16); color: #b25e00; }
43
- #login label { display: grid; gap: 5px; font-size: 12px; color: var(--soft); }
44
- #login input, #login select { font: inherit; color: var(--text); background: rgba(255, 255, 255, 0.9); border: 1px solid var(--line);
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
- #login input:focus, #login select:focus, #login .tags:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.12); }
47
- #login .mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 12.5px; }
48
- #login .two { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
49
- #login .btns { display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap; }
50
- #login .btn { border: 0; border-radius: 9px; padding: 7px 14px; font: 500 13px inherit; font-family: inherit; cursor: pointer;
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
- #login .btn.primary { background: var(--accent); color: #fff; }
53
- #login .btn.small { padding: 3px 10px; font-size: 12px; }
54
- #login .btn:disabled { opacity: 0.5; cursor: default; }
55
- #login .hint { font-size: 11.5px; color: var(--muted); }
56
- #login .row .hint { white-space: nowrap; }
57
- #login .error { color: var(--danger); font-size: 12.5px; }
58
- #login .wait { display: flex; gap: 10px; align-items: center; color: var(--soft); }
59
- #login .spin { width: 14px; height: 14px; border-radius: 50%; border: 2px solid rgba(0, 113, 227, 0.15); border-top-color: var(--accent);
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
- #login .code { font: 600 26px ui-monospace, "SF Mono", Menlo, monospace; letter-spacing: 0.12em; text-align: center; padding: 14px;
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
- #login details summary { cursor: pointer; color: var(--accent); font-size: 12.5px; }
65
- #login details[open] summary { margin-bottom: 8px; }
66
- #login .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
- #login .tags input { border: 0; box-shadow: none; padding: 3px 4px; flex: 1; min-width: 120px; background: none; }
68
- #login .tag { font: 12px ui-monospace, "SF Mono", Menlo, monospace; background: rgba(0, 113, 227, 0.1); border-radius: 6px;
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
- #login .tag button { border: 0; background: none; color: var(--muted); cursor: pointer; padding: 0 4px; font-size: 11px; }
71
- #login .found { max-height: 150px; overflow-y: auto; display: flex; flex-wrap: wrap; gap: 4px; }
72
- #login .found button { border: 1px solid var(--line); background: none; border-radius: 6px; padding: 2px 7px; cursor: pointer;
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
- #login .found button:hover { background: var(--hover); color: var(--text); }
75
- #login .for { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; color: var(--soft); }
76
- #login .seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
77
- #login .seg button { border: 0; background: none; padding: 3px 10px; font: inherit; font-size: 12px; cursor: pointer; color: var(--text); }
78
- #login .seg button.on { background: var(--accent); color: #fff; }
79
- #login .radio { width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid var(--muted); flex: none; box-sizing: border-box; }
80
- #login .row.on .radio { border: 4.5px solid var(--accent); }
81
- #login .model-id { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 12.5px; overflow-wrap: anywhere; }`;
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 = "login";
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: "login_cancel" });
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("/login").then((r) => r.ok ? r.json() : Promise.reject(new Error(`/login: ${r.status}`)));
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: "login_cancel" }); showList(); });
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: "login_start", provider: id });
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: "login_answer", text: r.dataset.id }); };
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: "login_answer", text: input.value }); ask.innerHTML = ""; msg.textContent = "Checking…"; };
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: "login_list_models", id: c?.id, api: value(".api"), baseUrl: value(".url"), key: value(".key") });
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: "login_custom", id: value(".id"), api: value(".api"), baseUrl: value(".url"), key: value(".key"), models: form.models });
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 (app/auth.json). Remove also deletes the endpoint.</div>`
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: "logout", provider: b.closest(".row").dataset.id });
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("/login/models").then((r) => r.ok ? r.json() : Promise.reject(new Error(`/login/models: ${r.status}`))); }
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("login", (ev) => {
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("/login/on").then((r) => { // CHALKY_LOGIN=off: the server has no /login, and the page no commands
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 OpenAI-compatible endpoint (in Chalky: keyproxy -> LiteLLM), as the Python agent's llm plugin.
2
- // MODEL_BASE_URL (up to /v1), MODEL_API_KEY, MODEL_NAME (a LiteLLM route, "default"). pi does the calls,
3
- // streaming, retries, thinking and compaction; this only tells it where. The host selects provider "chalky".
4
- // MODEL_CONTEXT_WINDOW: the model's context size; pi compacts when a request would leave less than 16384 tokens of it.
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
- // Login: connect models from the page and pick one (browser.js: /login, /logout, /model). pi's own login does the work, the call its
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 app/auth.json (pi's; never versioned), custom endpoints in app/models.json
6
- // (pi's; no secrets), this install's device id (OpenAI's sign-in asks for one) and the model choices in state/login.json:
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.login.modelFor(name, session) when it starts an agent: its own choice, else the all-agents
9
- // one, else nothing (then the MODEL_BASE_URL endpoint, extensions/model); a choice whose login is gone is skipped.
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 /login/on 204 (so the page knows login is on)
12
- // GET /login/models {groups: [{provider, name, models: [{id, name, context}]}] (usable now), default, agents: [{name, model, own}]}
13
- // GET /login {providers: [{id, name, subscription, status}], custom: [{id, baseUrl, api, models, status}]}
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 login_start {provider}, login_answer {text}, login_cancel,
16
- // login_custom {id, api, baseUrl, key?, models: [names]}, login_list_models {baseUrl, api, key?, id?},
17
- // logout {provider}, model_set {for: "all" | <agent>, provider?, id?} (for an agent, no provider: back to all agents')
18
- // event "login" {step: "notify", event} (pi: open this link / enter this code / waiting) | {step: "prompt", prompt}
19
- // (pi asks: the page answers with login_answer) | {step: "prompt_done"} (pi stopped asking: the
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
- // (login_list_models' answer) | {step: "changed"} (a login, endpoint or model changed: agents are synced)
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.app, "models.json");
29
- const STATE = path.join(host.state, "login.json");
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("login-runtime"))
34
- host.shared.set("login-runtime", ModelRuntime.create({ authPath: path.join(host.app, "auth.json"), modelsPath: MODELS }));
35
- const runtime = (): Promise<ModelRuntime> => host.shared.get("login-runtime");
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"); // extensions/model
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.login = { modelFor };
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("login", { step: "changed" });
70
+ host.broadcast("model", { step: "changed" });
70
71
  }
71
72
 
72
- const fail = (e: any) => host.broadcast("login", { step: "error", text: e?.message ?? String(e) });
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("login-checked")) return;
80
- host.shared.set("login-checked", true);
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("/login/on", (_req: any, res: any) => res.writeHead(204).end()); // browser.js: add the commands only when this answers
87
- host.routes.set("/login/models", (_req: any, res: any) => models(res).catch((e) =>
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("/login", (_req: any, res: any) => list(res).catch((e) =>
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("login", async (msg: any) => {
119
- const flow = host.shared.get("login-flow"); // the sign-in under way: {abort, answer?}
120
- if (msg.type === "login_answer") return flow?.answer?.(String(msg.text ?? ""));
121
- if (msg.type === "login_cancel") return flow?.abort.abort();
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 === "login_start") {
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("login-flow", me);
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("login", { step: "notify", event }),
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("login", { step: "prompt_done" }); reject(new Error("Login cancelled")); };
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("login", { step: "prompt", prompt });
138
+ host.broadcast("model", { step: "prompt", prompt });
138
139
  }),
139
140
  }, { getDeviceId: deviceId });
140
141
  await synced();
141
- host.broadcast("login", { step: "done", provider: msg.provider });
142
- } catch (e) { if (abort.signal.aborted) host.broadcast("login", { step: "cancelled" }); else fail(e); } // ✕: not an error
143
- finally { if (host.shared.get("login-flow") === me) host.shared.delete("login-flow"); }
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 === "login_custom") {
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("login", { step: "done", provider: id });
165
+ host.broadcast("model", { step: "done", provider: id });
165
166
  } catch (e) { fail(e); }
166
167
  }
167
168
 
168
- if (msg.type === "login_list_models") { // the endpoint's own list (GET <baseUrl>/models); the key never goes to the page
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("login", { step: "models", models });
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("login", { step: "changed" });
199
+ host.broadcast("model", { step: "changed" });
199
200
  } catch (e) { fail(e); }
200
201
  }
201
202
 
202
- if (msg.type === "logout") {
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.