bitpull 0.1.28 → 0.1.29

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.29",
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();
@@ -201,11 +218,16 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
201
218
  return { term, fit, box, readonly };
202
219
  };
203
220
 
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();
221
+ const startShell = (pane, existing) => {
222
+ if (existing) {
223
+ pane.id = existing;
224
+ byId.set(existing, pane);
225
+ } else {
226
+ pane.ref = nextRef++;
227
+ pane.go = () => ws.send(JSON.stringify({ t: "open", ref: pane.ref, cwd, cols: pane.term.cols, rows: pane.term.rows }));
228
+ waiting.set(pane.ref, pane);
229
+ if (ready) pane.go();
230
+ }
209
231
  pane.term.onData((d) => {
210
232
  // "Ctrl" pressed on a phone's extra keys: the next letter becomes Ctrl+letter.
211
233
  if (ctrlNext && d.length === 1) {
@@ -224,7 +246,7 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
224
246
  if (ws && ws.readyState <= 1) return;
225
247
  ready = false;
226
248
  ws = new WebSocket(`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/api/terminal`);
227
- ws.onopen = () => ws.send(JSON.stringify({ t: "auth", token }));
249
+ ws.onopen = () => ws.send(JSON.stringify({ t: "auth", token, ...(keepName ? { keep: keepName } : {}) }));
228
250
  ws.onmessage = (e) => {
229
251
  let m;
230
252
  try {
@@ -234,7 +256,14 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
234
256
  }
235
257
  if (m.t === "ready") {
236
258
  ready = true;
259
+ if (keepName) pickUp(m.kept || []);
237
260
  for (const p of waiting.values()) p.go();
261
+ if (keepName && firstReady && !tabs.length) newTab();
262
+ firstReady = false;
263
+ } else if (m.t === "attached") {
264
+ // The computer sends the screen again as it is now: it replaces what this page still showed.
265
+ byId.get(m.id)?.term.reset();
266
+ fitAll();
238
267
  } else if (m.t === "opened") {
239
268
  const p = waiting.get(m.ref);
240
269
  if (!p) return;
@@ -250,6 +279,14 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
250
279
  byId.delete(m.id);
251
280
  p.id = undefined;
252
281
  }
282
+ } else if (m.t === "error" && m.error === "no_terminal" && typeof m.id === "number") {
283
+ // Gone while the phone was away (more than an hour, or the computer restarted).
284
+ const p = byId.get(m.id);
285
+ if (p) {
286
+ byId.delete(m.id);
287
+ p.id = undefined;
288
+ p.term.write(`\r\n\x1b[2m${t("term.gone")}\x1b[0m\r\n`);
289
+ }
253
290
  } else if (m.t === "error") {
254
291
  const p = waiting.get(m.ref);
255
292
  waiting.delete(m.ref);
@@ -260,6 +297,8 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
260
297
  ws.onclose = (e) => {
261
298
  ready = false;
262
299
  if (disposed) return;
300
+ // A phone's connection comes back by itself: its shells are still there.
301
+ if (keepName && e.code !== 4409 && e.code !== 4410 && e.code !== 4401) setTimeout(() => !disposed && connect(), 2000);
263
302
  if (e.code === 4409 || e.code === 1006) {
264
303
  terminalStatus(token).then((s) => {
265
304
  if (!s.available) message("term.unavailable", false);
@@ -269,6 +308,20 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
269
308
  };
270
309
  };
271
310
 
311
+ /** Shows again the shells this phone left running: those of this page first, then the others in this folder. */
312
+ function pickUp(kept) {
313
+ for (const p of byId.values()) ws.send(JSON.stringify({ t: "attach", id: p.id, cols: p.term.cols, rows: p.term.rows }));
314
+ for (const k of kept) {
315
+ if (byId.has(k.id) || k.cwd !== cwd) continue;
316
+ const pane = makePane(false);
317
+ const tab = { title: "", panes: [pane] };
318
+ tabs.push(tab);
319
+ show(tab);
320
+ startShell(pane, k.id);
321
+ ws.send(JSON.stringify({ t: "attach", id: k.id, cols: pane.term.cols, rows: pane.term.rows }));
322
+ }
323
+ }
324
+
272
325
  function newTab() {
273
326
  const pane = makePane(false);
274
327
  const tab = { title: "", panes: [pane] };
@@ -337,7 +390,7 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
337
390
  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
391
  const resize = new ResizeObserver(() => fitAll());
339
392
  resize.observe(panesEl);
340
- // Closing the page ends its shells: the server closes them with the connection too.
393
+ // Closing the page ends its shells: the server closes them with the connection too. A phone's wait an hour.
341
394
  window.addEventListener("pagehide", dispose);
342
395
 
343
396
  terminalStatus(token).then(
@@ -346,7 +399,8 @@ export function mountTerminal(el, { cwd, token, t = (k) => k, fontSize = 13 } =
346
399
  if (!s.available) return message("term.unavailable", false);
347
400
  if (!s.enabled) return message("term.off", true);
348
401
  connect();
349
- newTab();
402
+ // On a phone, the first answer says whether shells are waiting; a new one opens only if none is.
403
+ if (!keepName) newTab();
350
404
  },
351
405
  () => message("term.failed", false),
352
406
  );