bitpull 0.1.30 → 0.1.32

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitpull",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "BitPull (bitpull.cloud): share signed, AI-readable code, computing power and open AI models with AI tools through MCP. CLI and MCP server.",
5
5
  "mcpName": "io.github.SamuealPK/bitpull",
6
6
  "bin": {
package/ui/drop.js ADDED
@@ -0,0 +1,155 @@
1
+ // Dropping things on the app's window. The desktop app tells the agent what was dropped (real paths,
2
+ // which a page can never see) and calls window.bitpullDropped with an id and what it is. A folder
3
+ // opens a window asking to authorise it and open it in Projects; files go to the chat.
4
+ (() => {
5
+ const BP = window.__BITPULL__ || {};
6
+ const token = BP.token || "";
7
+ const lang = BP.lang || document.documentElement.lang || "en";
8
+ let dict = {};
9
+ let fallback = {};
10
+ const ready = Promise.all([
11
+ fetch("/i18n/en.json").then((r) => r.json()).then((d) => (fallback = d), () => {}),
12
+ lang === "en" ? null : fetch(`/i18n/${lang}.json`).then((r) => (r.ok ? r.json() : {})).then((d) => (dict = d), () => {}),
13
+ ]);
14
+ const t = (k, vars = {}) => String(dict[k] ?? fallback[k] ?? k).replace(/\{(\w+)\}/g, (m, v) => (v in vars ? vars[v] : m));
15
+ const api = (path, body) =>
16
+ fetch(path, { method: "POST", headers: { "content-type": "application/json", "x-bitpull-token": token }, body: JSON.stringify(body) }).then(async (r) => {
17
+ const data = await r.json().catch(() => ({}));
18
+ if (!r.ok) throw new Error((data.error && (data.error.message || data.error)) || String(r.status));
19
+ return data;
20
+ });
21
+
22
+ const css = document.createElement("style");
23
+ css.textContent = `
24
+ .bp-drop-veil { position:fixed; inset:0; z-index:9998; display:none; align-items:center; justify-content:center; padding:24px;
25
+ background:rgba(20,22,28,.55); backdrop-filter:blur(2px); pointer-events:none; }
26
+ .bp-drop-veil.on { display:flex; }
27
+ .bp-drop-veil div { border:2px dashed #8ea2ff; border-radius:18px; padding:28px 32px; max-width:520px; text-align:center;
28
+ color:#fff; font:600 17px/1.45 system-ui, sans-serif; background:rgba(30,33,44,.85); }
29
+ .bp-drop-dialog { position:fixed; inset:0; z-index:9999; display:flex; align-items:center; justify-content:center; padding:16px; background:rgba(0,0,0,.45); }
30
+ .bp-drop-card { width:min(480px,100%); border-radius:16px; padding:22px; background:var(--surface,#1f2128); color:var(--text,#ececec);
31
+ border:1px solid var(--line,#33363f); box-shadow:0 18px 50px rgba(0,0,0,.4); font:15px/1.5 system-ui, sans-serif; }
32
+ .bp-drop-card h2 { margin:0 0 6px; font-size:19px; }
33
+ .bp-drop-card .bp-drop-path { margin:10px 0 12px; padding:10px 12px; border-radius:10px; background:var(--bg,#16181d); font:13px/1.4 ui-monospace, Menlo, Consolas, monospace; overflow-wrap:anywhere; }
34
+ .bp-drop-card p { margin:0 0 10px; color:var(--muted,#a2a19b); font-size:14px; }
35
+ .bp-drop-card .bp-drop-row { display:flex; gap:10px; justify-content:flex-end; flex-wrap:wrap; margin-top:16px; }
36
+ .bp-drop-card button { min-height:40px; padding:8px 16px; border-radius:10px; border:1px solid var(--line,#33363f); font:600 14px system-ui, sans-serif; cursor:pointer;
37
+ background:transparent; color:var(--text,#ececec); }
38
+ .bp-drop-card button.bp-yes { background:var(--accent,#7f95ff); border-color:var(--accent,#7f95ff); color:#0d0f14; }
39
+ .bp-drop-card .bp-drop-msg { color:#ff8a8a; font-size:13px; min-height:1em; }`;
40
+ document.head.appendChild(css);
41
+
42
+ const veil = document.createElement("div");
43
+ veil.className = "bp-drop-veil";
44
+ veil.setAttribute("aria-hidden", "true");
45
+ veil.appendChild(document.createElement("div"));
46
+ document.addEventListener("DOMContentLoaded", () => document.body.appendChild(veil));
47
+ if (document.body) document.body.appendChild(veil);
48
+
49
+ window.bitpullDragging = async (on) => {
50
+ await ready;
51
+ veil.firstChild.textContent = t("drop.here");
52
+ veil.classList.toggle("on", !!on);
53
+ };
54
+
55
+ /** A small window of its own, so it looks the same on every page. */
56
+ function dialog({ title, path, text, yes, onYes }) {
57
+ const wrap = document.createElement("div");
58
+ wrap.className = "bp-drop-dialog";
59
+ wrap.setAttribute("role", "dialog");
60
+ wrap.setAttribute("aria-modal", "true");
61
+ const card = document.createElement("div");
62
+ card.className = "bp-drop-card";
63
+ const h = document.createElement("h2");
64
+ h.textContent = title;
65
+ card.append(h);
66
+ if (path) {
67
+ const p = document.createElement("div");
68
+ p.className = "bp-drop-path";
69
+ p.textContent = path;
70
+ card.append(p);
71
+ }
72
+ for (const line of [].concat(text || [])) {
73
+ const p = document.createElement("p");
74
+ p.textContent = line;
75
+ card.append(p);
76
+ }
77
+ const msg = document.createElement("div");
78
+ msg.className = "bp-drop-msg";
79
+ const row = document.createElement("div");
80
+ row.className = "bp-drop-row";
81
+ const close = () => wrap.remove();
82
+ const no = document.createElement("button");
83
+ no.textContent = onYes ? t("drop.no") : t("drop.ok");
84
+ no.dataset.tip = "tip.drop.no";
85
+ no.title = t("tip.drop.no");
86
+ no.addEventListener("click", close);
87
+ row.append(no);
88
+ if (onYes) {
89
+ const ok = document.createElement("button");
90
+ ok.className = "bp-yes";
91
+ ok.textContent = yes;
92
+ ok.dataset.tip = "tip.drop.yes";
93
+ ok.title = t("tip.drop.yes");
94
+ ok.addEventListener("click", async () => {
95
+ ok.disabled = no.disabled = true;
96
+ try {
97
+ await onYes();
98
+ close();
99
+ } catch (err) {
100
+ msg.textContent = err.message === "not a folder" ? t("drop.failed") : `${t("drop.failed")} (${err.message})`;
101
+ ok.disabled = no.disabled = false;
102
+ }
103
+ });
104
+ row.append(ok);
105
+ setTimeout(() => ok.focus(), 0);
106
+ }
107
+ card.append(msg, row);
108
+ wrap.append(card);
109
+ wrap.addEventListener("click", (e) => e.target === wrap && close());
110
+ wrap.addEventListener("keydown", (e) => e.key === "Escape" && close());
111
+ document.body.appendChild(wrap);
112
+ }
113
+
114
+ const openProjects = (folder) => {
115
+ const q = new URLSearchParams({ token, lang, folder });
116
+ if (location.pathname === "/ide") {
117
+ location.hash = q.toString();
118
+ location.reload();
119
+ } else location.href = `/ide#${q}`;
120
+ };
121
+
122
+ window.bitpullDropped = async (drop, x, y) => {
123
+ await ready;
124
+ if (!drop || !Array.isArray(drop.items) || !drop.items.length) {
125
+ return dialog({ title: t("drop.failed.title"), text: t("drop.failed") });
126
+ }
127
+ const folders = drop.items.filter((i) => i.kind === "folder");
128
+ const files = drop.items.filter((i) => i.kind === "file");
129
+ if (folders.length) {
130
+ const f = folders[0];
131
+ return dialog({
132
+ title: t("drop.title"),
133
+ path: f.path,
134
+ text: [t("drop.text"), folders.length > 1 ? t("drop.more", { n: folders.length - 1 }) : ""].filter(Boolean),
135
+ yes: t("drop.yes"),
136
+ onYes: async () => {
137
+ const r = await api("/api/drop/folder", { id: drop.id, i: f.i });
138
+ openProjects(r.folder);
139
+ },
140
+ });
141
+ }
142
+ // Files: into the chat, where a page knows how to attach them.
143
+ if (files.length && typeof window.bitpullAttach === "function") {
144
+ const list = [];
145
+ for (const f of files) {
146
+ const r = await fetch(`/api/drop/file?id=${encodeURIComponent(drop.id)}&i=${f.i}`, { headers: { "x-bitpull-token": token } });
147
+ if (r.status === 413) return dialog({ title: t("drop.failed.title"), text: t("drop.big", { name: f.name }) });
148
+ if (!r.ok) continue;
149
+ list.push(new File([await r.blob()], f.name));
150
+ }
151
+ if (list.length) return window.bitpullAttach(list, x, y);
152
+ }
153
+ return dialog({ title: t("drop.files.title"), text: t("drop.files") });
154
+ };
155
+ })();
@@ -10,8 +10,24 @@ function register() {
10
10
  let opts = null;
11
11
  let term = null;
12
12
  let current = null;
13
+ let note = null;
13
14
  const start = () => {
14
- if (term || !host || !opts || !opts.folder) return;
15
+ if (term || !host || !opts) return;
16
+ // No folder: say what is missing instead of an empty box.
17
+ if (!opts.folder) {
18
+ if (!note) {
19
+ note = document.createElement("div");
20
+ note.className = "bp-term-msg";
21
+ note.style.padding = "14px";
22
+ note.textContent = opts.t ? opts.t("term.nofolder") : "term.nofolder";
23
+ host.appendChild(note);
24
+ }
25
+ return;
26
+ }
27
+ if (note) {
28
+ note.remove();
29
+ note = null;
30
+ }
15
31
  term = mountTerminal(host, { cwd: opts.folder, token: opts.token, t: opts.t, fontSize: opts.phone ? 12 : 13, keep: !!opts.phone });
16
32
  };
17
33
  ide.terminal.register({
@@ -54,11 +70,13 @@ function register() {
54
70
  if (!opts) return;
55
71
  opts.folder = folder;
56
72
  // A new folder is a new place to work: the old shells close, new ones open there.
73
+ // Only where the terminal was already shown (open, or saying a folder was missing).
74
+ const shown = !!term || !!note;
57
75
  if (term) {
58
76
  term.dispose();
59
77
  term = null;
60
- start();
61
78
  }
79
+ if (shown) start();
62
80
  },
63
81
  });
64
82
  }
package/ui/ide.css CHANGED
@@ -333,3 +333,13 @@ button.sb:hover { background: var(--hover); color: var(--text); }
333
333
  #phone-keys button { flex: none; min-width: 46px; height: 40px; padding: 0 10px; border: 1px solid var(--line2); border-radius: 8px; background: var(--raised); color: var(--bright); font: 14px ui-monospace, Menlo, Consolas, monospace; }
334
334
  #phone-keys button.on { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
335
335
  #ide.phone.typing .agent-top { display: none; }
336
+
337
+ /* Projects without a folder: plainly what to do. */
338
+ #welcome-nofolder .nofolder-ways { text-align:start; margin:10px auto 14px; max-width:460px; line-height:1.6; }
339
+ #welcome-nofolder button { margin-top:4px; }
340
+
341
+ /* Quick commands: the line under the box, and texts shown by a command (/btw, /context, /status). */
342
+ #qc-line { min-height: 0; padding: 2px 4px 0; white-space: pre-wrap; }
343
+ #qc-line:empty { display: none; }
344
+ #qc-line.warn { color: var(--warn, #e0a64b); }
345
+ .qc-pre { white-space: pre-wrap; max-height: 60vh; overflow: auto; margin: 0; font: 13px/1.5 ui-monospace, Menlo, Consolas, monospace; }
package/ui/ide.html CHANGED
@@ -27,6 +27,9 @@
27
27
  term.type = "module";
28
28
  term.src = "/ui/ide-terminal.js";
29
29
  document.body.appendChild(term);
30
+ var drop = document.createElement("script");
31
+ drop.src = "/ui/drop.js";
32
+ document.body.appendChild(drop);
30
33
  });
31
34
  })();
32
35
  </script>
@@ -114,7 +117,7 @@
114
117
  <div id="editor" dir="ltr"></div>
115
118
  <div id="editor2" dir="ltr" hidden></div>
116
119
  <div id="diff" dir="ltr" hidden></div>
117
- <div id="welcome"><div class="welcome-box"><div class="logo" data-icon="logo"></div><h2 data-i18n="ide.welcome"></h2><p class="muted" data-i18n="ide.welcome.text"></p><table id="keys" class="keys"></table></div></div>
120
+ <div id="welcome"><div class="welcome-box"><div class="logo" data-icon="logo"></div><div id="welcome-nofolder" hidden><h2 data-i18n="ide.nofolder.title"></h2><p class="muted" data-i18n="ide.nofolder.text"></p><ol class="muted nofolder-ways"><li data-i18n="ide.nofolder.drag"></li><li data-i18n="ide.nofolder.pick"></li><li data-i18n="ide.nofolder.panel"></li></ol><button id="nofolder-panel" data-i18n="ide.nofolder.open" data-tip="tip.ide.nofolder.open"></button></div><div id="welcome-folder"><h2 data-i18n="ide.welcome"></h2><p class="muted" data-i18n="ide.welcome.text"></p></div><table id="keys" class="keys"></table></div></div>
118
121
  </div>
119
122
  </main>
120
123
  <div class="sash" data-split="panel" data-tip="tip.ide.resize"></div>
@@ -156,6 +159,7 @@
156
159
  <button class="ib danger" id="agent-stop" data-tip="tip.pj.stop" data-icon="stop" hidden></button>
157
160
  <button class="send" id="agent-send" data-tip="tip.pj.send" data-icon="send"></button>
158
161
  </div>
162
+ <div id="qc-line" class="muted small" aria-live="polite"></div>
159
163
  <div id="mode-hint" class="muted small"></div>
160
164
  </div>
161
165
  </aside>
package/ui/ide.js CHANGED
@@ -106,6 +106,8 @@
106
106
  document.title = `${t("ide.title")} · BitPull`;
107
107
  for (const n of document.querySelectorAll("[data-i18n]")) n.textContent = t(n.dataset.i18n);
108
108
  for (const n of document.querySelectorAll("[data-i18n-placeholder]")) n.placeholder = t(n.dataset.i18nPlaceholder);
109
+ // The empty box says how to find the commands and the shortcuts.
110
+ $("prompt").placeholder = `${t("ide.prompt.placeholder")}\n${t("qc.hint")}`;
109
111
  for (const n of document.querySelectorAll("[data-tip]")) n.setAttribute("aria-label", t(n.dataset.tip));
110
112
  }
111
113
  /** A known error's words, or the error as it came. */
@@ -1091,6 +1093,9 @@
1091
1093
  const sel = clear($("folder"));
1092
1094
  for (const f of r.folders) sel.append(el("option", { value: f, text: f }));
1093
1095
  if (!r.folders.length) sel.append(el("option", { value: "", text: t("ide.folder.none") }));
1096
+ // No folder at all: say so in the middle of the page too, not only in the list at the top.
1097
+ $("welcome-nofolder").hidden = r.folders.length > 0 && !!sel.value;
1098
+ $("welcome-folder").hidden = !$("welcome-nofolder").hidden;
1094
1099
  const want = BP.folder || store.get("folder", "");
1095
1100
  sel.value = r.folders.includes(want) ? want : r.folders[0] || "";
1096
1101
  return r;
@@ -1101,6 +1106,9 @@
1101
1106
  folder = f;
1102
1107
  store.set("folder", f);
1103
1108
  $("folder-name").textContent = f ? folderLabel(f) : t("ide.folder.none");
1109
+ // No folder yet: the page says plainly to choose the one to work on, and how.
1110
+ $("welcome-nofolder").hidden = !!f;
1111
+ $("welcome-folder").hidden = !f;
1104
1112
  $("tree-title").textContent = f ? folderLabel(f) : "";
1105
1113
  $("palette-label").textContent = f ? `${folderLabel(f)} — ${t("ide.palette.hint", { keys: `${MOD}⇧P` })}` : t("ide.palette");
1106
1114
  session = null;
@@ -1112,6 +1120,7 @@
1112
1120
  await refreshGit();
1113
1121
  refreshTree();
1114
1122
  if (terminal && terminal.folderChanged) terminal.folderChanged(f);
1123
+ if (quick) quick.refreshSkills();
1115
1124
  await refreshAgents();
1116
1125
  }
1117
1126
  $("folder").addEventListener("change", () => setFolder($("folder").value));
@@ -1226,6 +1235,9 @@
1226
1235
  }
1227
1236
  log.scrollTop = log.scrollHeight;
1228
1237
  $("agent-status").textContent = `${t(`pj.status.${v.status}`)} · ${t("pj.used", { steps: v.used.steps, minutes: v.used.minutes, credits: Math.round(v.used.credits * 100) / 100 })} · ${t("ide.limits.short", { steps: v.limits.steps, minutes: v.limits.minutes, credits: v.limits.credits || "—" })}`;
1238
+ const nowBusy = STATUS_BUSY.has(v.status);
1239
+ if (quick && renderAgent.wasBusy && !nowBusy) setTimeout(() => quick.idle(), 0);
1240
+ renderAgent.wasBusy = nowBusy;
1229
1241
  $("agent-stop").hidden = !STATUS_BUSY.has(v.status);
1230
1242
  $("agent-send").hidden = STATUS_BUSY.has(v.status);
1231
1243
  $("agent-undo").hidden = !v.canUndo;
@@ -1306,11 +1318,13 @@
1306
1318
  if (e.key === "ArrowDown") { mentionSel = Math.min(mentionItems.length - 1, mentionSel + 1); drawMentions(); e.preventDefault(); return; }
1307
1319
  if (e.key === "ArrowUp") { mentionSel = Math.max(0, mentionSel - 1); drawMentions(); e.preventDefault(); return; }
1308
1320
  if (e.key === "Enter" || e.key === "Tab") { e.preventDefault(); insertMention(mentionItems[mentionSel]); return; }
1309
- if (e.key === "Escape") { mentionItems = []; drawMentions(); return; }
1321
+ if (e.key === "Escape") { e.preventDefault(); mentionItems = []; drawMentions(); return; }
1310
1322
  }
1311
- if (e.key === "Enter" && !e.shiftKey && !e.isComposing) { e.preventDefault(); send(); }
1323
+ // Enter, Esc and the other keys of the box: the quick commands (quick.js). Without them, Enter sends.
1324
+ if (!quick && e.key === "Enter" && !e.shiftKey && !e.isComposing) { e.preventDefault(); send(); }
1312
1325
  });
1313
1326
  async function send() {
1327
+ if (quick) return quick.submit();
1314
1328
  const text = $("prompt").value.trim();
1315
1329
  if (!text) return;
1316
1330
  if (!session && !(await newAgent())) return;
@@ -1327,6 +1341,224 @@
1327
1341
  if (!(await confirmBox(t("pj.undo"), t("ide.undo.confirm"), t("pj.undo"), true))) return;
1328
1342
  try { renderAgent(await api("/api/projects/undo", { id: session.id })); for (const tab of tabs) if (!isDirty(tab)) reloadTab(tab).catch(() => {}); refreshTree(); } catch (err) { output(explain(err)); }
1329
1343
  });
1344
+ // ---- quick commands: "/", "!", ":", "?" and the keys of the composer (see quick.js) ---------------------
1345
+ const quickReady = new Promise((resolve) => {
1346
+ if (window.BitPullQuick) return resolve(window.BitPullQuick);
1347
+ const s = document.createElement("script");
1348
+ s.src = `${BASE}quick.js`;
1349
+ s.onload = () => resolve(window.BitPullQuick);
1350
+ s.onerror = () => resolve(null);
1351
+ document.head.appendChild(s);
1352
+ });
1353
+ let quick = null;
1354
+ const agentBusy = () => !!(session && STATUS_BUSY.has(session.status));
1355
+ const say = (text, kind) => { const line = $("qc-line"); line.textContent = text || ""; line.className = `small ${kind === "warn" ? "warn" : "muted"}`; };
1356
+ /** Where a command of the panel lives: the panel opens on that part. */
1357
+ const toPanel = (section) => { location.href = `${PHONE ? "/remote/" : "/"}#token=${encodeURIComponent(token)}&lang=${encodeURIComponent(lang)}&view=panel${section ? `&section=${encodeURIComponent(section)}` : ""}`; };
1358
+ const ensureAgent = async () => session || (await newAgent());
1359
+ /** A message to the agent, as if typed (used by /review, /init and the others that ask for work). */
1360
+ async function ask(text) {
1361
+ if (!(await ensureAgent())) return;
1362
+ try { renderAgent(await api("/api/projects/send", { id: session.id, text })); } catch (err) { output(explain(err)); say(explain(err), "warn"); }
1363
+ }
1364
+ const logText = () => [...$("agent-log").children].map((n) => (n.classList.contains("user") ? `> ${n.textContent}` : n.textContent)).join("\n\n");
1365
+ const answers = () => [...$("agent-log").querySelectorAll(".ev:not(.user):not(.action):not(.output):not(.notice):not(.error)")].map((n) => n.textContent);
1366
+ const copyOut = async (text) => { try { await navigator.clipboard.writeText(text); say(t("qc.copied"), "info"); } catch { await modal(t("qc.copy.title"), el("textarea", { rows: 12, readonly: true, text }), [{ label: t("ide.ok"), value: null }]); } };
1367
+ const showText = (title, text) => modal(title, el("pre", { class: "qc-pre", text }), [{ label: t("ide.ok"), value: null }]);
1368
+ const shellReply = () => store.get("qcShellReply", true) !== false;
1369
+ let loopTimer = null;
1370
+
1371
+ const PROMPTS = {
1372
+ review: "Review the changes in this project that are not committed yet (git diff), or the files I name. List real problems first: bugs, security, missing error handling, then smaller things. Do not change files.",
1373
+ "security-review": "Do a security review of the changes not committed yet (git diff): injection, secrets in code, unsafe input handling, permissions, dependencies. Explain each problem and how to fix it. Do not change files.",
1374
+ simplify: "Look at the recently changed code for duplication, needless complexity and dead code, and simplify it without changing what it does. Then run the tests.",
1375
+ verify: "Check that the work done so far really works: build it, run the tests and, where possible, run the program. Report what passed and what failed.",
1376
+ debug: "Help me find the cause of a problem. Reproduce it first, read the relevant code and logs, form a hypothesis, check it, then fix it.",
1377
+ init: `Create (or improve, if it exists) an instruction file for this project named ${"BITPULL.md"} in the project's root: what the project is, how to install, build, test and run it, the layout of the folders, and the rules for the code style that you can see. Keep it short and exact.`,
1378
+ recap: "In a few lines: what have we done so far, and what is left to do?",
1379
+ };
1380
+
1381
+ const H = {
1382
+ clear: async (name) => { if (await newAgent() && name) renderAgent(await api("/api/projects/rename", { id: session.id, title: name })); },
1383
+ compact: async (instructions) => {
1384
+ if (!session) return say(t("qc.noagent"), "warn");
1385
+ say(t("qc.compacting"), "info");
1386
+ try { renderAgent(await api("/api/projects/compact", { id: session.id, instructions })); say(t("qc.compacted"), "info"); } catch (err) { say(explain(err), "warn"); }
1387
+ },
1388
+ resume: async (name) => {
1389
+ await refreshAgents();
1390
+ const q = name.toLowerCase();
1391
+ const hit = q && agentList.find((a) => (a.title || "").toLowerCase().includes(q));
1392
+ if (hit) return selectAgent(hit.id);
1393
+ if (!agentList.length) return say(t("qc.noagents"), "warn");
1394
+ menu($("prompt"), agentList.slice(0, 30).map((a) => ({ label: a.title || t("ide.agent.untitled"), keys: t(`pj.status.${a.status}`).replace(/[.…]$/, ""), check: session && a.id === session.id, run: () => selectAgent(a.id) })));
1395
+ },
1396
+ rename: async (name) => {
1397
+ if (!session) return say(t("qc.noagent"), "warn");
1398
+ const title = name || (await askText(t("qc.rename.title"), t("qc.rename.text"), session.title || ""));
1399
+ if (title) { renderAgent(await api("/api/projects/rename", { id: session.id, title })); refreshAgents(); }
1400
+ },
1401
+ export: (file) => window.BitPullQuick.download(file || `${(session && session.title) || "bitpull-agent"}.txt`.replace(/[\\/:*?"<>|]/g, "_"), logText()),
1402
+ copy: (n) => { const list = answers(); const i = Math.max(1, Number(n) || 1); const text = list[list.length - i]; if (!text) return say(t("qc.copy.none"), "warn"); return copyOut(text); },
1403
+ recap: () => H.btw(PROMPTS.recap),
1404
+ btw: async (question) => {
1405
+ if (!question) return say(t("qc.btw.empty"), "warn");
1406
+ if (!session) return say(t("qc.noagent"), "warn");
1407
+ say(t("qc.thinking"), "info");
1408
+ try { const r = await api("/api/projects/aside", { id: session.id, question }); say("", "info"); await showText(t("qc.btw.title"), r.answer); } catch (err) { say(explain(err), "warn"); }
1409
+ },
1410
+ rewind: () => { if (!session || $("agent-undo").hidden) return say(t("qc.rewind.none"), "warn"); $("agent-undo").click(); },
1411
+ stop: () => { if (agentBusy()) $("agent-stop").click(); },
1412
+ exit: () => $("back").click(),
1413
+ "add-dir": async (path) => {
1414
+ if (!path) return addFolder();
1415
+ try { await api("/api/folders", { path }); await loadFolders(); say(t("qc.folder.added"), "info"); } catch (err) { say(explain(err), "warn"); }
1416
+ },
1417
+ cd: (path) => {
1418
+ const options = [...$("folder").options].map((o) => o.value).filter(Boolean);
1419
+ const q = path.toLowerCase();
1420
+ const hit = q && (options.find((f) => f.toLowerCase() === q) || options.find((f) => folderLabel(f).toLowerCase() === q) || options.find((f) => f.toLowerCase().includes(q)));
1421
+ if (hit) { $("folder").value = hit; return setFolder(hit); }
1422
+ $("folder-btn").click();
1423
+ },
1424
+ init: () => ask(PROMPTS.init),
1425
+ memory: async () => {
1426
+ if (!folder) return say(t("pj.err.project_folder_not_authorised"), "warn");
1427
+ const tree = await api("/api/ws/tree", { folder, path: "." }).catch(() => ({ entries: [] }));
1428
+ const names = new Set((tree.entries || tree.items || []).map((e) => e.name || e.path));
1429
+ const found = ["BITPULL.md", "AGENTS.md", "CLAUDE.md"].find((n) => names.has(n));
1430
+ if (found) return openFile(found);
1431
+ if (!(await confirmBox(t("qc.memory.title"), t("qc.memory.none"), t("qc.memory.create")))) return;
1432
+ try { await api("/api/ws/write", { folder, path: "BITPULL.md", content: "# Instructions for the agent\n\n- \n" }); refreshTree(); openFile("BITPULL.md"); } catch (err) { say(explain(err), "warn"); }
1433
+ },
1434
+ context: async () => {
1435
+ if (!session) return say(t("qc.noagent"), "warn");
1436
+ const c = await api("/api/projects/context", { id: session.id });
1437
+ const pct = (n) => `${Math.round((n / c.limit) * 100)}%`;
1438
+ await showText(t("qc.context.title"), [
1439
+ t("qc.context.used", { used: pct(c.used), chars: c.used.toLocaleString(), limit: c.limit.toLocaleString() }),
1440
+ "",
1441
+ `${t("qc.context.system")}: ${pct(c.parts.system)}`,
1442
+ `${t("qc.context.person")}: ${pct(c.parts.person)}`,
1443
+ `${t("qc.context.agent")}: ${pct(c.parts.agent)}`,
1444
+ `${t("qc.context.results")}: ${pct(c.parts.results)}`,
1445
+ "",
1446
+ t("qc.context.hint"),
1447
+ ].join("\n"));
1448
+ },
1449
+ model: (name) => {
1450
+ const sel = $("model");
1451
+ const q = name.toLowerCase();
1452
+ const opt = q && [...sel.options].find((o) => !o.disabled && o.textContent.toLowerCase().includes(q));
1453
+ if (opt) { sel.value = opt.value; sel.dispatchEvent(new Event("change")); return say(t("qc.model.set", { name: opt.textContent }), "info"); }
1454
+ sel.focus();
1455
+ try { sel.showPicker(); } catch {}
1456
+ },
1457
+ plan: (desc) => { setMode("ask"); if (desc) return ask(`Make a plan, step by step, before changing anything: ${desc}`); say(t("qc.plan.on"), "info"); },
1458
+ permissions: (mode) => { if (MODES.includes(mode)) return setMode(mode); $("mode").focus(); try { $("mode").showPicker(); } catch {} },
1459
+ diff: () => showSide("git"),
1460
+ review: (focus) => ask(`${PROMPTS.review}${focus ? ` Focus on: ${focus}` : ""}`),
1461
+ "security-review": () => ask(PROMPTS["security-review"]),
1462
+ simplify: (target) => ask(`${PROMPTS.simplify}${target ? ` Look at: ${target}` : ""}`),
1463
+ verify: () => ask(PROMPTS.verify),
1464
+ debug: (desc) => ask(`${PROMPTS.debug}${desc ? ` The problem: ${desc}` : ""}`),
1465
+ tasks: () => showTerminal(),
1466
+ loop: (args) => {
1467
+ if (/^stop$/i.test(args) || (!args && loopTimer)) { clearInterval(loopTimer); loopTimer = null; return say(t("qc.loop.stopped"), "info"); }
1468
+ const m = /^(\d+)\s*([smh])\b\s*([\s\S]*)$/i.exec(args);
1469
+ const every = m ? Number(m[1]) * { s: 1, m: 60, h: 3600 }[m[2].toLowerCase()] : 600;
1470
+ const prompt = (m ? m[3] : args).trim();
1471
+ if (!prompt) return say(t("qc.loop.empty"), "warn");
1472
+ clearInterval(loopTimer);
1473
+ const seconds = Math.max(60, every);
1474
+ loopTimer = setInterval(() => { if (!agentBusy()) quick.submitText(prompt); }, seconds * 1000);
1475
+ quick.submitText(prompt);
1476
+ say(t("qc.loop.on", { minutes: Math.round(seconds / 60) }), "info");
1477
+ },
1478
+ skills: async () => {
1479
+ const r = await api("/api/quick/commands", { folder }).catch(() => ({ skills: [] }));
1480
+ const lines = r.skills.length ? r.skills.map((s) => `/${s.name}${s.argumentHint ? ` ${s.argumentHint}` : ""} — ${s.description || ""}\n ${s.file}`) : [t("qc.skills.none")];
1481
+ await showText(t("qc.skills.title"), `${lines.join("\n")}\n\n${t("qc.skills.how")}`);
1482
+ },
1483
+ "reload-skills": async () => { await quick.refreshSkills(); say(t("qc.skills.reloaded"), "info"); },
1484
+ config: () => $("settings-btn").click(),
1485
+ theme: () => $("settings-btn").click(),
1486
+ keybindings: () => showText(t("qc.keys.title"), `${t("qc.keys.file", { file: quick.keymapFile() || "~/.bitpull/keybindings.json" })}\n\n${JSON.stringify(quick.keys(), null, 2)}`),
1487
+ "shell-reply": (v) => { const on = v ? /^(on|1|yes|si|sì|true)$/i.test(v) : !shellReply(); store.set("qcShellReply", on); say(t(on ? "qc.shellreply.on" : "qc.shellreply.off"), "info"); },
1488
+ "privacy-settings": () => toPanel("rules.title"),
1489
+ help: () => { quick.set("/"); quick.update(); },
1490
+ status: () => showText(t("qc.status.title"), [
1491
+ `${t("qc.status.folder")}: ${folder || "—"}`,
1492
+ `${t("qc.status.model")}: ${(chosenModel() || {}).label || "—"}`,
1493
+ `${t("qc.status.mode")}: ${t(`pj.mode.${$("mode").value}`)}`,
1494
+ `${t("qc.status.agent")}: ${session ? `${session.title || t("ide.agent.untitled")} · ${t(`pj.status.${session.status}`)}` : "—"}`,
1495
+ session ? t("pj.used", { steps: session.used.steps, minutes: session.used.minutes, credits: Math.round(session.used.credits * 100) / 100 }) : "",
1496
+ ].filter(Boolean).join("\n")),
1497
+ usage: () => toPanel("credits.title"),
1498
+ doctor: () => toPanel("setup.title"),
1499
+ "release-notes": () => toPanel("update.title"),
1500
+ version: () => toPanel("update.title"),
1501
+ bug: () => api("/api/open", { url: "https://bitpull.cloud/help" }).catch(() => window.open("https://bitpull.cloud/help")),
1502
+ feedback: () => api("/api/open", { url: "https://bitpull.cloud/help" }).catch(() => window.open("https://bitpull.cloud/help")),
1503
+ login: () => toPanel("credits.title"),
1504
+ logout: () => toPanel("credits.title"),
1505
+ plans: () => toPanel("credits.title"),
1506
+ topup: () => toPanel("credits.title"),
1507
+ phone: () => toPanel("remote.title"),
1508
+ lend: () => toPanel("lend.title"),
1509
+ pause: () => toPanel("lend.title"),
1510
+ network: () => toPanel("mynet.title"),
1511
+ groups: () => toPanel("team.title"),
1512
+ models: () => toPanel("models.title"),
1513
+ terminal: () => showTerminal(),
1514
+ chat: () => { location.href = `${PHONE ? "/remote/" : "/"}#token=${encodeURIComponent(token)}&lang=${encodeURIComponent(lang)}&view=chat`; },
1515
+ };
1516
+
1517
+ quickReady.then((Q) => {
1518
+ if (!Q) return;
1519
+ const registry = Q.createRegistry();
1520
+ Q.builtins(registry, H);
1521
+ quick = Q.attach($("prompt"), {
1522
+ t,
1523
+ surface: "projects",
1524
+ scope: () => folder,
1525
+ registry,
1526
+ busy: agentBusy,
1527
+ say,
1528
+ send: async (text) => {
1529
+ if (!(await ensureAgent())) { $("prompt").value = text; return; }
1530
+ try { renderAgent(await api("/api/projects/send", { id: session.id, text })); } catch (err) { $("prompt").value = text; output(explain(err)); say(explain(err), "warn"); }
1531
+ },
1532
+ shell: async (command) => {
1533
+ if (!(await ensureAgent())) return;
1534
+ // The command runs while the page follows its output: the answer of this request comes at the end.
1535
+ api("/api/projects/shell", { id: session.id, command, reply: shellReply() }).then(renderAgent, (err) => say(explain(err), "warn"));
1536
+ setTimeout(() => api("/api/projects/state", { id: session.id, after: cursor }).then(renderAgent, () => {}), 300);
1537
+ },
1538
+ loadSkills: async () => (await api("/api/quick/commands", { folder })).skills,
1539
+ runSkill: async (name, args) => {
1540
+ try {
1541
+ let r = await api("/api/quick/expand", { folder, name, args, session: session && session.id });
1542
+ // A command that came with the project runs commands: the person sees them and says yes once.
1543
+ if (r.needsTrust) {
1544
+ const ok = await modal(t("qc.trust.title", { name: `/${r.name}` }), [el("p", { text: t("qc.trust.text") }), el("pre", { class: "qc-pre", text: r.commands.join("\n") })], [{ label: t("pj.no"), ghost: true, value: false }, { label: t("qc.trust.yes"), value: true }]);
1545
+ if (!ok) return;
1546
+ await api("/api/quick/trust", { folder, name: r.name });
1547
+ r = await api("/api/quick/expand", { folder, name, args, session: session && session.id });
1548
+ }
1549
+ await quick.sendNow(r.prompt);
1550
+ } catch (err) {
1551
+ // "skill_command_failed: npm test": the words, then which command.
1552
+ const m = String(err.message || err), k = `pj.err.${m.split(":")[0]}`;
1553
+ say(t(k) !== k ? `${t(k)}${m.includes(":") ? ` (${m.slice(m.indexOf(":") + 1).trim()})` : ""}` : explain(err), "warn");
1554
+ }
1555
+ },
1556
+ interrupt: () => $("agent-stop").click(),
1557
+ pickModel: () => H.model(""),
1558
+ cycleMode,
1559
+ });
1560
+ });
1561
+
1330
1562
  // The app works only while a page asks for news: this is also how it knows someone is watching.
1331
1563
  // Each request waits on the app until something happens, so a command's output shows while it is written.
1332
1564
  (async function follow() {
@@ -1380,7 +1612,8 @@
1380
1612
  document.addEventListener("keydown", (e) => {
1381
1613
  const mod = isMac ? e.metaKey : e.ctrlKey;
1382
1614
  const inEditor = e.target && e.target.closest && e.target.closest(".monaco-editor");
1383
- if (e.key === "Tab" && e.shiftKey && !inEditor && !mentionItems.length) { e.preventDefault(); cycleMode(); return; }
1615
+ const inPrompt = quick && e.target === $("prompt");
1616
+ if (e.key === "Tab" && e.shiftKey && !inEditor && !inPrompt && !mentionItems.length) { e.preventDefault(); cycleMode(); return; }
1384
1617
  if (e.ctrlKey && e.key === "`") { e.preventDefault(); showTerminal(); return; }
1385
1618
  if (!mod) return;
1386
1619
  const k = e.key.toLowerCase();
@@ -1392,7 +1625,7 @@
1392
1625
  else if (k === "b" && !e.shiftKey) { e.preventDefault(); toggle("sideOpen"); }
1393
1626
  else if (k === "j") { e.preventDefault(); if (layoutState.panelOpen && currentBottom() === "terminal") { layoutState.panelOpen = false; layout(); if (terminal) terminal.close(); } else showTerminal(); }
1394
1627
  else if (k === "l") { e.preventDefault(); newAgent(); }
1395
- else if (k === "s" && !inEditor) { e.preventDefault(); saveActive(); }
1628
+ else if (k === "s" && !inEditor && !(inPrompt && !isMac)) { e.preventDefault(); saveActive(); }
1396
1629
  else if (k === "\\") { e.preventDefault(); if (active) toggleSplit(); }
1397
1630
  }, true);
1398
1631
  addEventListener("beforeunload", (e) => { if (tabs.some(isDirty)) { e.preventDefault(); e.returnValue = ""; } });
@@ -1404,6 +1637,7 @@
1404
1637
  if (PHONE) phoneSetup();
1405
1638
  $("ide").hidden = false;
1406
1639
  $("back").addEventListener("click", () => { location.href = `${PHONE ? "/remote/" : "/"}#token=${encodeURIComponent(token)}&lang=${encodeURIComponent(lang)}`; });
1640
+ $("nofolder-panel").addEventListener("click", () => { location.href = `${PHONE ? "/remote/" : "/"}#token=${encodeURIComponent(token)}&lang=${encodeURIComponent(lang)}&view=panel`; });
1407
1641
  for (const b of document.querySelectorAll("#activity button[data-side]")) b.classList.toggle("on", b.dataset.side === layoutState.sideView && layoutState.sideOpen);
1408
1642
  for (const p of document.querySelectorAll("#side > section")) p.hidden = p.dataset.panel !== layoutState.sideView;
1409
1643
  layout();