bitpull 0.1.27 → 0.1.28

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.27",
3
+ "version": "0.1.28",
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": {
@@ -12,7 +12,7 @@ function register() {
12
12
  let current = null;
13
13
  const start = () => {
14
14
  if (term || !host || !opts || !opts.folder) return;
15
- term = mountTerminal(host, { cwd: opts.folder, token: opts.token, t: opts.t });
15
+ term = mountTerminal(host, { cwd: opts.folder, token: opts.token, t: opts.t, fontSize: opts.phone ? 12 : 13 });
16
16
  };
17
17
  ide.terminal.register({
18
18
  mount(container, o) {
@@ -30,6 +30,14 @@ function register() {
30
30
  start();
31
31
  if (term) term.newTab();
32
32
  },
33
+ input(d) {
34
+ start();
35
+ if (term) term.input(d);
36
+ },
37
+ ctrl(on, done) {
38
+ start();
39
+ if (term) term.ctrl(on, done);
40
+ },
33
41
  runVisible(command) {
34
42
  start();
35
43
  current = term ? term.runVisible(command) : null;
package/ui/ide.css CHANGED
@@ -269,3 +269,67 @@ button.sb:hover { background: var(--hover); color: var(--text); }
269
269
 
270
270
  /* Monaco draws long lines under the minimap, whose canvas is slightly transparent: a solid ground hides them. */
271
271
  .monaco-editor .minimap { background: var(--bg); }
272
+
273
+ /* ---- opened from the person's phone: one part at a time, chosen at the bottom, sized for fingers ---- */
274
+ #ide.phone { grid-template-rows: 48px minmax(0, 1fr) auto; font-size: 15px; height: 100%; }
275
+ #ide.phone #statusbar, #ide.phone #toggle-side, #ide.phone #toggle-panel, #ide.phone #toggle-agent, #ide.phone #palette-label,
276
+ #ide.phone .sash, #ide.phone #split-editor, #ide.phone #crumbs, #ide.phone #panel-max, #ide.phone #bottom-close, #ide.phone #views-more,
277
+ #ide.phone #activity button[data-side="files"], #ide.phone table.keys { display: none !important; }
278
+ #ide.phone #titlebar { gap: 4px; padding: 0 4px; }
279
+ #ide.phone button.tb { height: 42px; min-width: 42px; }
280
+ #ide.phone button.tb svg.i { width: 20px; height: 20px; }
281
+ #ide.phone button.tb.folder { flex: 1 1 auto; max-width: none; font-size: 16px; justify-content: flex-start; }
282
+ #ide.phone #titlebar .grow { display: none; }
283
+ #ide.phone button.tb.search { width: 42px; border: 0; background: transparent; justify-content: center; }
284
+ #ide.phone #activity { flex-direction: row; justify-content: flex-start; padding: 0 4px; border-inline-end: 0; border-bottom: 1px solid var(--line); }
285
+ #ide.phone #activity button { width: 56px; height: 48px; }
286
+ #ide.phone #activity button.on::before { top: auto; bottom: 0; left: 10px; right: 10px; width: auto; height: 2px; }
287
+ #ide.phone #side, #ide.phone #agent { border: 0; }
288
+ #ide.phone .view-title { height: 40px; font-size: 12px; padding-inline-start: 16px; }
289
+ #ide.phone .pane-head { height: 44px; font-size: 12px; padding-inline: 8px; }
290
+ #ide.phone .pane-acts { display: inline-flex; gap: 4px; }
291
+ #ide.phone button.ib { width: 40px; height: 40px; }
292
+ #ide.phone button.ib svg.i { width: 20px; height: 20px; }
293
+ #ide.phone ul.tree .item { height: 44px; font-size: 15px; padding-inline-end: 12px; }
294
+ #ide.phone ul.tree .item svg.f { width: 18px; height: 18px; }
295
+ #ide.phone ul.list li { height: 44px; font-size: 15px; }
296
+ #ide.phone :is(input[type=text], input[type=number], select, textarea) { font-size: 16px !important; } /* below 16px iPhone zooms in */
297
+ #ide.phone .field input[type=text] { padding: 8px 4px !important; }
298
+ #ide.phone button.primary, #ide.phone button.ghost { min-height: 42px; padding: 6px 16px; font-size: 15px; }
299
+ #ide.phone #tabs-row { height: 46px; }
300
+ #ide.phone #tabs .tab { padding: 0 4px 0 12px; font-size: 14px; }
301
+ #ide.phone #tabs .tab .x { visibility: visible; width: 36px; height: 36px; }
302
+ #ide.phone .tabs-acts button.ib { width: 44px; height: 44px; }
303
+ #ide.phone .panel-head { height: 46px; padding: 0 4px; }
304
+ #ide.phone .ptabs { overflow-x: auto; scrollbar-width: none; }
305
+ #ide.phone .ptabs button[data-bottom] { height: 42px; font-size: 12px; padding: 0 10px; flex: none; }
306
+ #ide.phone .bp-term-bar { height: 44px !important; }
307
+ #ide.phone .bp-term-bar button.ib { width: 42px !important; height: 40px !important; }
308
+ #ide.phone .bp-term-bar button { font-size: 14px !important; padding: 6px 10px !important; }
309
+ #ide.phone .bp-term-pane { padding: 2px 0 0 4px !important; }
310
+ #ide.phone .agent-top { padding: 8px 10px; }
311
+ #ide.phone ul.list.agents { max-height: 26vh; }
312
+ #ide.phone #agent-log { padding: 10px 12px; font-size: 15px; }
313
+ #ide.phone #composer { margin: 0 8px 8px; padding: 8px; border-radius: 14px; }
314
+ #ide.phone #composer textarea { min-height: 48px; }
315
+ #ide.phone .composer-bar { gap: 6px; flex-wrap: wrap; }
316
+ #ide.phone .composer-bar select { font-size: 15px !important; min-height: 40px; max-width: 40%; }
317
+ #ide.phone button.send { width: 42px; height: 42px; }
318
+ #ide.phone button.send svg.i { width: 20px; height: 20px; }
319
+ #ide.phone .welcome-box h2 { font-size: 18px; }
320
+ #ide.phone ~ .menu li, #ide.phone ~ #menu li { height: 46px; font-size: 15px; }
321
+ #ide.phone ~ #modal { padding-top: 4vh; }
322
+ #ide.phone ~ #modal .box { width: calc(100% - 16px); max-height: 88vh; font-size: 15px; }
323
+ #ide.phone ~ #modal button { min-height: 42px; }
324
+ #ide.phone ~ #tip { display: none !important; } /* no hover on a phone: the tips stay as the buttons' names */
325
+ #phone-nav { display: flex; background: var(--bar); border-top: 1px solid var(--line); padding-bottom: env(safe-area-inset-bottom); }
326
+ #phone-nav button { flex: 1 1 0; min-width: 0; height: 58px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; background: transparent; border: 0; color: var(--muted); font-size: 11px; }
327
+ #phone-nav button span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
328
+ #phone-nav button svg.i { width: 24px; height: 24px; stroke-width: 1.3; }
329
+ #phone-nav button.on { color: var(--bright); }
330
+ #phone-nav button.on svg.i { color: var(--accent); }
331
+ #ide.phone.typing #phone-nav { display: none; }
332
+ #phone-keys { display: flex; gap: 6px; overflow-x: auto; padding: 6px 8px; border-top: 1px solid var(--line); background: var(--side); flex: none; scrollbar-width: none; }
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
+ #phone-keys button.on { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
335
+ #ide.phone.typing .agent-top { display: none; }
package/ui/ide.js CHANGED
@@ -5,6 +5,9 @@
5
5
  (function () {
6
6
  const BP = window.__BITPULL__ || {};
7
7
  const token = BP.token || "";
8
+ // Opened from the person's phone (bitpull.cloud/app): one screen at a time, sized for fingers.
9
+ // The computer's own window keeps the editor layout untouched.
10
+ const PHONE = token === "phone";
8
11
  const $ = (id) => document.getElementById(id);
9
12
  // Where the page's files are: /ui/~<build>/, which the webview keeps without asking again.
10
13
  const BASE = ((document.currentScript && document.currentScript.src) || "/ui/ide.js").replace(/ide\.js(\?.*)?$/, "");
@@ -213,7 +216,7 @@
213
216
  register(impl) {
214
217
  terminal = impl;
215
218
  clear($("terminal-host"));
216
- impl.mount($("terminal-host"), { token, folder, lang, t });
219
+ impl.mount($("terminal-host"), { token, folder, lang, t, phone: PHONE });
217
220
  // Its buttons become icons, like the rest of the panel; the words stay as their name.
218
221
  const TERM_ICONS = { "term.new": "plus", "term.split": "split", "term.close": "trash" };
219
222
  const iconize = () => {
@@ -233,6 +236,7 @@
233
236
  get folder() { return folder; },
234
237
  };
235
238
  function showTerminal() {
239
+ if (PHONE) phoneTab = "terminal";
236
240
  showBottom("terminal");
237
241
  if (terminal) terminal.open();
238
242
  else clear($("terminal-host")).append(el("p", { class: "muted pad", text: t("ide.terminal.missing") }));
@@ -245,6 +249,7 @@
245
249
  );
246
250
  const narrow = () => innerWidth < 900;
247
251
  function layout() {
252
+ if (PHONE) return phoneLayout();
248
253
  const L = layoutState;
249
254
  const ws = $("workbench");
250
255
  $("ide").classList.toggle("narrow", narrow());
@@ -305,6 +310,67 @@
305
310
  store.set("layout", L);
306
311
  relayout();
307
312
  }
313
+ // ---- on a phone: one part at a time, chosen with the bar at the bottom ---------------------------------
314
+ let phoneTab = "files";
315
+ let phoneMore = "git";
316
+ const PHONE_PARTS = { files: "side", more: "side", editor: "editor-area", terminal: "panel", agent: "agent" };
317
+ function phoneLayout() {
318
+ const ws = $("workbench");
319
+ $("ide").classList.add("phone");
320
+ const more = phoneTab === "more";
321
+ ws.style.gridTemplateColumns = "minmax(0, 1fr)";
322
+ ws.style.gridTemplateAreas = more ? '"act" "main"' : '"main"';
323
+ ws.style.gridTemplateRows = more ? "auto minmax(0, 1fr)" : "minmax(0, 1fr)";
324
+ $("activity").hidden = !more;
325
+ for (const id of ["side", "editor-area", "panel", "agent"]) {
326
+ $(id).style.gridArea = "main";
327
+ $(id).hidden = PHONE_PARTS[phoneTab] !== id;
328
+ }
329
+ for (const sash of ws.querySelectorAll(".sash")) sash.hidden = true;
330
+ if ($("phone-keys")) $("phone-keys").hidden = !(phoneTab === "terminal" && currentBottom() === "terminal");
331
+ for (const b of document.querySelectorAll("#phone-nav button")) b.classList.toggle("on", b.dataset.tab === phoneTab);
332
+ for (const p of document.querySelectorAll(".pane")) p.classList.toggle("open", !!layoutState.panes[p.dataset.pane]);
333
+ relayout();
334
+ }
335
+ function phoneShow(tab) {
336
+ phoneTab = tab;
337
+ if (tab === "files") showSide("files");
338
+ else if (tab === "more") showSide(phoneMore);
339
+ else if (tab === "terminal") showTerminal();
340
+ else phoneLayout();
341
+ }
342
+ function phoneSetup() {
343
+ const nav = el("nav", { id: "phone-nav" });
344
+ for (const [tab, ic, label] of [["files", "files", "phone.files"], ["editor", "output", "phone.editor"], ["terminal", "terminal", "pj.tab.terminal"], ["agent", "logo", "phone.agent"], ["more", "more", "phone.more"]]) {
345
+ nav.append(el("button", { type: "button", "data-tab": tab, "data-tip": `tip.phone.${tab}`, "aria-label": t(label), onclick: () => phoneShow(tab) }, icon(ic), el("span", { text: t(label) })));
346
+ }
347
+ $("ide").append(nav);
348
+ // Keys a phone's keyboard lacks, for the terminal. Pressing them keeps the keyboard open.
349
+ const keys = el("div", { id: "phone-keys", hidden: true });
350
+ const KEYS = [["Esc", "\x1b"], ["Tab", "\t"], ["Ctrl", null], ["↑", "\x1b[A"], ["↓", "\x1b[B"], ["←", "\x1b[D"], ["→", "\x1b[C"], ["Ctrl+C", "\x03"], ["|", "|"], ["/", "/"], ["~", "~"], ["-", "-"]];
351
+ for (const [label, seq] of KEYS) {
352
+ const b = el("button", { type: "button", text: label, "data-tip": seq === null ? "tip.phone.ctrl" : "tip.phone.key", onpointerdown: (e) => e.preventDefault() });
353
+ b.addEventListener("click", () => {
354
+ if (!terminal) return;
355
+ if (seq === null) {
356
+ const on = !b.classList.contains("on");
357
+ b.classList.toggle("on", on);
358
+ if (terminal.ctrl) terminal.ctrl(on, () => b.classList.remove("on"));
359
+ } else if (terminal.input) terminal.input(seq);
360
+ });
361
+ keys.append(b);
362
+ }
363
+ $("panel").append(keys);
364
+ // With the keyboard open the page gets much shorter (the phone page fits it to what stays visible):
365
+ // then the bar steps aside to leave room, and the terminal's keys stay.
366
+ let tallest = innerHeight;
367
+ const keyboard = () => {
368
+ tallest = Math.max(tallest, innerHeight);
369
+ $("ide").classList.toggle("typing", innerHeight < tallest * 0.75);
370
+ };
371
+ addEventListener("resize", keyboard);
372
+ addEventListener("orientationchange", () => { tallest = 0; setTimeout(keyboard, 300); });
373
+ }
308
374
  /** Monaco and the terminal measure themselves after the grid has settled. */
309
375
  function relayout() {
310
376
  requestAnimationFrame(() => {
@@ -347,6 +413,7 @@
347
413
  $("panel-max").addEventListener("click", () => toggle("panelMax"));
348
414
 
349
415
  function showSide(name) {
416
+ if (PHONE) { if (name !== "files") phoneMore = name; phoneTab = name === "files" ? "files" : "more"; }
350
417
  layoutState.sideOpen = true;
351
418
  layoutState.sideView = name;
352
419
  for (const b of document.querySelectorAll("#activity button[data-side]")) b.classList.toggle("on", b.dataset.side === name);
@@ -449,7 +516,9 @@
449
516
  monaco.editor.defineTheme("bitpull-light", { base: "vs", inherit: true, rules: [], colors: { "editor.background": "#ffffff", "editorLineNumber.foreground": "#6e7681" } });
450
517
  }
451
518
  const theme = () => (dark() ? "bitpull-dark" : "bitpull-light");
452
- const EDITOR_OPTS = () => ({ automaticLayout: false, theme: theme(), minimap: { enabled: true, renderCharacters: false, scale: 1 }, fontSize: 13, lineHeight: 20, fontFamily: 'Menlo, Monaco, "Cascadia Code", Consolas, "Courier New", monospace', tabSize: 2, renderWhitespace: "selection", smoothScrolling: true, scrollBeyondLastLine: false, fixedOverflowWidgets: true, padding: { top: 4 }, stickyScroll: { enabled: true }, bracketPairColorization: { enabled: true }, guides: { bracketPairs: "active", indentation: true } });
519
+ const EDITOR_OPTS = () => ({ automaticLayout: false, theme: theme(), minimap: { enabled: true, renderCharacters: false, scale: 1 }, fontSize: 13, lineHeight: 20, fontFamily: 'Menlo, Monaco, "Cascadia Code", Consolas, "Courier New", monospace', tabSize: 2, renderWhitespace: "selection", smoothScrolling: true, scrollBeyondLastLine: false, fixedOverflowWidgets: true, padding: { top: 4 }, stickyScroll: { enabled: true }, bracketPairColorization: { enabled: true }, guides: { bracketPairs: "active", indentation: true },
520
+ // A phone's screen: no minimap, long lines wrap, a size readable without zooming.
521
+ ...(PHONE ? { minimap: { enabled: false }, wordWrap: "on", fontSize: 14, lineHeight: 21, stickyScroll: { enabled: false }, folding: false, glyphMargin: false, lineNumbersMinChars: 3, lineDecorationsWidth: 6, scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10 } } : {}) });
453
522
  function wireEditor(ed) {
454
523
  ed.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => saveActive());
455
524
  ed.addAction({ id: "bitpull.inline", label: t("ide.inline"), keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK], contextMenuGroupId: "1_modification", run: () => inlineEdit() });
@@ -590,6 +659,8 @@
590
659
  if (r.secret) output(t("ide.secret_open", { path }));
591
660
  }
592
661
  activate(tab);
662
+ // On a phone a file chosen in the list, a search result or a problem opens on the editor's screen.
663
+ if (PHONE && phoneTab !== "editor" && phoneTab !== "agent") { phoneTab = "editor"; phoneLayout(); }
593
664
  const ed = focused || editor;
594
665
  if (line) { ed.revealLineInCenter(line); ed.setPosition({ lineNumber: line, column: 1 }); ed.focus(); }
595
666
  }
@@ -1330,8 +1401,9 @@
1330
1401
  (async () => {
1331
1402
  drawIcons(document);
1332
1403
  await loadWords();
1404
+ if (PHONE) phoneSetup();
1333
1405
  $("ide").hidden = false;
1334
- $("back").addEventListener("click", () => { location.href = `/#token=${encodeURIComponent(token)}&lang=${encodeURIComponent(lang)}`; });
1406
+ $("back").addEventListener("click", () => { location.href = `${PHONE ? "/remote/" : "/"}#token=${encodeURIComponent(token)}&lang=${encodeURIComponent(lang)}`; });
1335
1407
  for (const b of document.querySelectorAll("#activity button[data-side]")) b.classList.toggle("on", b.dataset.side === layoutState.sideView && layoutState.sideOpen);
1336
1408
  for (const p of document.querySelectorAll("#side > section")) p.hidden = p.dataset.panel !== layoutState.sideView;
1337
1409
  layout();
@@ -1339,6 +1411,7 @@
1339
1411
  renderModeHint();
1340
1412
  renderProblemCounts();
1341
1413
  showBottom(store.get("bottom", "problems") === "terminal" ? "problems" : store.get("bottom", "problems"));
1414
+ if (PHONE) phoneShow("files");
1342
1415
  // Folders and models while the editor loads: the tree shows before Monaco is ready.
1343
1416
  const lists = Promise.all([loadModels().catch((err) => output(explain(err))), loadFolders().catch((err) => output(explain(err)))]).then(() => ($("folder").value ? setFolder($("folder").value) : null));
1344
1417
  try { monaco = await loadMonaco(); setupEditor(); } catch (err) { output(t("ide.err.editor_missing")); }
package/ui/terminal.js CHANGED
@@ -92,7 +92,7 @@ export function mountTerminalSetting(el, { token, t = (k) => k, onChange } = {})
92
92
  * Mounts the terminal in `el`. `cwd` must be a folder the person authorised; `t` translates.
93
93
  * Returns the controls the page and the Projects view use.
94
94
  */
95
- export function mountTerminal(el, { cwd, token, t = (k) => k } = {}) {
95
+ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } = {}) {
96
96
  const ended = (code) => String(t("term.exited", { code })).replace("{code}", String(code));
97
97
  addCss();
98
98
  el.replaceChildren();
@@ -117,6 +117,8 @@ export function mountTerminal(el, { cwd, token, t = (k) => k } = {}) {
117
117
  el.appendChild(root);
118
118
 
119
119
  const tabs = []; // { title, panes: [{ term, fit, id?, box, readonly }] }
120
+ let ctrlNext = false;
121
+ let ctrlDone = null;
120
122
  let active = null;
121
123
  let ws = null;
122
124
  let ready = false;
@@ -179,7 +181,7 @@ export function mountTerminal(el, { cwd, token, t = (k) => k } = {}) {
179
181
  const makePane = (readonly) => {
180
182
  const box = document.createElement("div");
181
183
  box.className = "bp-term-pane";
182
- const term = new Terminal({ cursorBlink: !readonly, disableStdin: readonly, fontSize: 13, fontFamily: "ui-monospace, Menlo, Consolas, monospace", theme: theme(), scrollback: 5000, convertEol: readonly });
184
+ const term = new Terminal({ cursorBlink: !readonly, disableStdin: readonly, fontSize, fontFamily: "ui-monospace, Menlo, Consolas, monospace", theme: theme(), scrollback: 5000, convertEol: readonly });
183
185
  const fit = new FitAddon();
184
186
  term.loadAddon(fit);
185
187
  term.open(box);
@@ -205,6 +207,13 @@ export function mountTerminal(el, { cwd, token, t = (k) => k } = {}) {
205
207
  waiting.set(pane.ref, pane);
206
208
  if (ready) pane.go();
207
209
  pane.term.onData((d) => {
210
+ // "Ctrl" pressed on a phone's extra keys: the next letter becomes Ctrl+letter.
211
+ if (ctrlNext && d.length === 1) {
212
+ const c = d.toUpperCase().charCodeAt(0);
213
+ if (c >= 64 && c <= 95) d = String.fromCharCode(c - 64);
214
+ ctrlNext = null;
215
+ if (ctrlDone) ctrlDone();
216
+ }
208
217
  if (!pane.id || !ready) return;
209
218
  // Huge pastes go in pieces: a message has a size limit.
210
219
  for (let i = 0; i < d.length; i += MAX_INPUT) ws.send(JSON.stringify({ t: "in", id: pane.id, d: d.slice(i, i + MAX_INPUT) }));
@@ -342,5 +351,20 @@ export function mountTerminal(el, { cwd, token, t = (k) => k } = {}) {
342
351
  () => message("term.failed", false),
343
352
  );
344
353
 
345
- return { newTab, split, close, runVisible, dispose, fit: fitAll };
354
+ /** Keys from outside the terminal (a phone's extra keys): typed into the shell shown now. */
355
+ function input(d) {
356
+ const p = active && !active.readonly ? active.panes[active.panes.length - 1] : null;
357
+ if (!p) return;
358
+ if (p.id && ready) ws.send(JSON.stringify({ t: "in", id: p.id, d: String(d).slice(0, MAX_INPUT) }));
359
+ p.term.focus();
360
+ }
361
+ /** Makes the next letter typed a Ctrl+letter; done() runs once it is used. */
362
+ function ctrl(on, done) {
363
+ ctrlNext = !!on;
364
+ ctrlDone = on ? done : null;
365
+ const p = active && active.panes[active.panes.length - 1];
366
+ if (p) p.term.focus();
367
+ }
368
+
369
+ return { newTab, split, close, runVisible, dispose, fit: fitAll, input, ctrl };
346
370
  }