bitpull 0.1.28 → 0.1.30

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.28",
3
+ "version": "0.1.30",
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, fontSize: opts.phone ? 12 : 13 });
15
+ term = mountTerminal(host, { cwd: opts.folder, token: opts.token, t: opts.t, fontSize: opts.phone ? 12 : 13, keep: !!opts.phone });
16
16
  };
17
17
  ide.terminal.register({
18
18
  mount(container, o) {
package/ui/terminal.js CHANGED
@@ -88,11 +88,25 @@ export function mountTerminalSetting(el, { token, t = (k) => k, onChange } = {})
88
88
  terminalStatus(token).then(render, () => render({ enabled: false, available: false }));
89
89
  }
90
90
 
91
+ /** This phone's name for its shells on the computer: the same every time the page opens. */
92
+ function keptName() {
93
+ try {
94
+ let n = localStorage.getItem("bitpull_term_keep");
95
+ if (!n || !/^[A-Za-z0-9_-]{16,64}$/.test(n)) {
96
+ n = Array.from(crypto.getRandomValues(new Uint8Array(18)), (b) => b.toString(16).padStart(2, "0")).join("");
97
+ localStorage.setItem("bitpull_term_keep", n);
98
+ }
99
+ return n;
100
+ } catch {
101
+ return Array.from(crypto.getRandomValues(new Uint8Array(18)), (b) => b.toString(16).padStart(2, "0")).join("");
102
+ }
103
+ }
104
+
91
105
  /**
92
106
  * Mounts the terminal in `el`. `cwd` must be a folder the person authorised; `t` translates.
93
107
  * Returns the controls the page and the Projects view use.
94
108
  */
95
- export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } = {}) {
109
+ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13, keep = false } = {}) {
96
110
  const ended = (code) => String(t("term.exited", { code })).replace("{code}", String(code));
97
111
  addCss();
98
112
  el.replaceChildren();
@@ -126,6 +140,9 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
126
140
  const waiting = new Map();
127
141
  let nextRef = 1;
128
142
  const byId = new Map();
143
+ // On a phone the shells wait on the computer for an hour when the phone goes away, under this name.
144
+ const keepName = keep ? keptName() : null;
145
+ let firstReady = true;
129
146
 
130
147
  const message = (key, withEnable) => {
131
148
  panesEl.replaceChildren();
@@ -186,7 +203,9 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
186
203
  term.loadAddon(fit);
187
204
  term.open(box);
188
205
  const fast = () => {
189
- if (!webgl) return;
206
+ // On Windows the app's window (WebView2) can draw nothing at all with WebGL on some graphics cards,
207
+ // without saying so: the ordinary drawing always shows the text.
208
+ if (!webgl || /Windows/i.test(navigator.userAgent)) return;
190
209
  try {
191
210
  const gl = new webgl();
192
211
  // The graphics card can take the context back (sleep, driver update): then the ordinary drawing resumes.
@@ -201,11 +220,16 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
201
220
  return { term, fit, box, readonly };
202
221
  };
203
222
 
204
- const startShell = (pane) => {
205
- pane.ref = nextRef++;
206
- pane.go = () => ws.send(JSON.stringify({ t: "open", ref: pane.ref, cwd, cols: pane.term.cols, rows: pane.term.rows }));
207
- waiting.set(pane.ref, pane);
208
- if (ready) pane.go();
223
+ const startShell = (pane, existing) => {
224
+ if (existing) {
225
+ pane.id = existing;
226
+ byId.set(existing, pane);
227
+ } else {
228
+ pane.ref = nextRef++;
229
+ pane.go = () => ws.send(JSON.stringify({ t: "open", ref: pane.ref, cwd, cols: pane.term.cols, rows: pane.term.rows }));
230
+ waiting.set(pane.ref, pane);
231
+ if (ready) pane.go();
232
+ }
209
233
  pane.term.onData((d) => {
210
234
  // "Ctrl" pressed on a phone's extra keys: the next letter becomes Ctrl+letter.
211
235
  if (ctrlNext && d.length === 1) {
@@ -224,7 +248,7 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
224
248
  if (ws && ws.readyState <= 1) return;
225
249
  ready = false;
226
250
  ws = new WebSocket(`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/api/terminal`);
227
- ws.onopen = () => ws.send(JSON.stringify({ t: "auth", token }));
251
+ ws.onopen = () => ws.send(JSON.stringify({ t: "auth", token, ...(keepName ? { keep: keepName } : {}) }));
228
252
  ws.onmessage = (e) => {
229
253
  let m;
230
254
  try {
@@ -234,7 +258,14 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
234
258
  }
235
259
  if (m.t === "ready") {
236
260
  ready = true;
261
+ if (keepName) pickUp(m.kept || []);
237
262
  for (const p of waiting.values()) p.go();
263
+ if (keepName && firstReady && !tabs.length) newTab();
264
+ firstReady = false;
265
+ } else if (m.t === "attached") {
266
+ // The computer sends the screen again as it is now: it replaces what this page still showed.
267
+ byId.get(m.id)?.term.reset();
268
+ fitAll();
238
269
  } else if (m.t === "opened") {
239
270
  const p = waiting.get(m.ref);
240
271
  if (!p) return;
@@ -250,6 +281,14 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
250
281
  byId.delete(m.id);
251
282
  p.id = undefined;
252
283
  }
284
+ } else if (m.t === "error" && m.error === "no_terminal" && typeof m.id === "number") {
285
+ // Gone while the phone was away (more than an hour, or the computer restarted).
286
+ const p = byId.get(m.id);
287
+ if (p) {
288
+ byId.delete(m.id);
289
+ p.id = undefined;
290
+ p.term.write(`\r\n\x1b[2m${t("term.gone")}\x1b[0m\r\n`);
291
+ }
253
292
  } else if (m.t === "error") {
254
293
  const p = waiting.get(m.ref);
255
294
  waiting.delete(m.ref);
@@ -260,6 +299,8 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
260
299
  ws.onclose = (e) => {
261
300
  ready = false;
262
301
  if (disposed) return;
302
+ // A phone's connection comes back by itself: its shells are still there.
303
+ if (keepName && e.code !== 4409 && e.code !== 4410 && e.code !== 4401) setTimeout(() => !disposed && connect(), 2000);
263
304
  if (e.code === 4409 || e.code === 1006) {
264
305
  terminalStatus(token).then((s) => {
265
306
  if (!s.available) message("term.unavailable", false);
@@ -269,6 +310,20 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
269
310
  };
270
311
  };
271
312
 
313
+ /** Shows again the shells this phone left running: those of this page first, then the others in this folder. */
314
+ function pickUp(kept) {
315
+ for (const p of byId.values()) ws.send(JSON.stringify({ t: "attach", id: p.id, cols: p.term.cols, rows: p.term.rows }));
316
+ for (const k of kept) {
317
+ if (byId.has(k.id) || k.cwd !== cwd) continue;
318
+ const pane = makePane(false);
319
+ const tab = { title: "", panes: [pane] };
320
+ tabs.push(tab);
321
+ show(tab);
322
+ startShell(pane, k.id);
323
+ ws.send(JSON.stringify({ t: "attach", id: k.id, cols: pane.term.cols, rows: pane.term.rows }));
324
+ }
325
+ }
326
+
272
327
  function newTab() {
273
328
  const pane = makePane(false);
274
329
  const tab = { title: "", panes: [pane] };
@@ -337,7 +392,7 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
337
392
  bar.append(tabsEl, button("term.new", "tip.term.new", () => newTab()), button("term.split", "tip.term.split", () => split()), button("term.close", "tip.term.close", () => close()));
338
393
  const resize = new ResizeObserver(() => fitAll());
339
394
  resize.observe(panesEl);
340
- // Closing the page ends its shells: the server closes them with the connection too.
395
+ // Closing the page ends its shells: the server closes them with the connection too. A phone's wait an hour.
341
396
  window.addEventListener("pagehide", dispose);
342
397
 
343
398
  terminalStatus(token).then(
@@ -346,7 +401,8 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
346
401
  if (!s.available) return message("term.unavailable", false);
347
402
  if (!s.enabled) return message("term.off", true);
348
403
  connect();
349
- newTab();
404
+ // On a phone, the first answer says whether shells are waiting; a new one opens only if none is.
405
+ if (!keepName) newTab();
350
406
  },
351
407
  () => message("term.failed", false),
352
408
  );