@madarco/agentbox 0.8.0 → 0.10.0

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 (51) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +161 -0
  3. package/dist/{_cloud-attach-T727ZPRV.js → _cloud-attach-O6NYTLES.js} +4 -4
  4. package/dist/{chunk-67N47KUS.js → chunk-2GPORKYF.js} +349 -182
  5. package/dist/chunk-2GPORKYF.js.map +1 -0
  6. package/dist/{chunk-6OZDFNBF.js → chunk-7UIAO7PC.js} +401 -82
  7. package/dist/chunk-7UIAO7PC.js.map +1 -0
  8. package/dist/{chunk-BGK32PZE.js → chunk-KL36BRN4.js} +2 -2
  9. package/dist/chunk-KL36BRN4.js.map +1 -0
  10. package/dist/chunk-MTVI44DW.js +662 -0
  11. package/dist/chunk-MTVI44DW.js.map +1 -0
  12. package/dist/{chunk-FODMEHD3.js → chunk-R4O5WPHW.js} +705 -77
  13. package/dist/chunk-R4O5WPHW.js.map +1 -0
  14. package/dist/{dist-ZODPD2I6.js → dist-5FQGYRW5.js} +20 -10
  15. package/dist/dist-5FQGYRW5.js.map +1 -0
  16. package/dist/{dist-LOZBWMBF.js → dist-BQNX7RQE.js} +19 -3
  17. package/dist/dist-PZW3GWWU.js +874 -0
  18. package/dist/dist-PZW3GWWU.js.map +1 -0
  19. package/dist/{dist-L4LCG5SJ.js → dist-TMHSUVTP.js} +4 -4
  20. package/dist/index.js +2385 -842
  21. package/dist/index.js.map +1 -1
  22. package/dist/{prepared-state-CL4CWXQA-ME4HSKDE.js → prepared-state-CL4CWXQA-H5THETIM.js} +2 -2
  23. package/package.json +11 -7
  24. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +9 -8
  25. package/runtime/docker/packages/ctl/dist/bin.cjs +129 -31
  26. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-vnc-start +15 -1
  27. package/runtime/hetzner/agentbox-setup-skill.md +9 -8
  28. package/runtime/hetzner/agentbox-vnc-start +15 -1
  29. package/runtime/hetzner/ctl.cjs +129 -31
  30. package/runtime/relay/bin.cjs +260 -39
  31. package/runtime/vercel/agentbox-checkpoint-cleanup +52 -0
  32. package/runtime/vercel/agentbox-codex-hooks.json +68 -0
  33. package/runtime/vercel/agentbox-open +28 -0
  34. package/runtime/vercel/agentbox-setup-skill.md +197 -0
  35. package/runtime/vercel/agentbox-vnc-start +91 -0
  36. package/runtime/vercel/claude-managed-settings.json +115 -0
  37. package/runtime/vercel/ctl.cjs +23495 -0
  38. package/runtime/vercel/custom-system-CLAUDE.md +47 -0
  39. package/runtime/vercel/gh-shim +263 -0
  40. package/runtime/vercel/git-shim +131 -0
  41. package/runtime/vercel/scripts/provision.sh +314 -0
  42. package/share/agentbox-setup/SKILL.md +9 -8
  43. package/dist/chunk-67N47KUS.js.map +0 -1
  44. package/dist/chunk-6OZDFNBF.js.map +0 -1
  45. package/dist/chunk-BGK32PZE.js.map +0 -1
  46. package/dist/chunk-FODMEHD3.js.map +0 -1
  47. package/dist/dist-ZODPD2I6.js.map +0 -1
  48. /package/dist/{_cloud-attach-T727ZPRV.js.map → _cloud-attach-O6NYTLES.js.map} +0 -0
  49. /package/dist/{dist-LOZBWMBF.js.map → dist-BQNX7RQE.js.map} +0 -0
  50. /package/dist/{dist-L4LCG5SJ.js.map → dist-TMHSUVTP.js.map} +0 -0
  51. /package/dist/{prepared-state-CL4CWXQA-ME4HSKDE.js.map → prepared-state-CL4CWXQA-H5THETIM.js.map} +0 -0
@@ -2,32 +2,41 @@
2
2
  import {
3
3
  DEFAULT_RELAY_PORT,
4
4
  readBoxStatus
5
- } from "./chunk-6OZDFNBF.js";
5
+ } from "./chunk-7UIAO7PC.js";
6
6
 
7
7
  // src/commands/_cloud-attach.ts
8
8
  import { spawn as spawn3 } from "child_process";
9
+ import { appendFileSync } from "fs";
10
+ import { homedir } from "os";
11
+ import { join as join2 } from "path";
12
+ import { spinner } from "@clack/prompts";
9
13
 
10
14
  // src/provider/registry.ts
11
- var KNOWN = ["docker", "daytona", "hetzner"];
15
+ var KNOWN = ["docker", "daytona", "hetzner", "vercel"];
12
16
  function isKnownProvider(name) {
13
17
  return KNOWN.includes(name);
14
18
  }
15
19
  async function getProvider(name) {
16
20
  switch (name) {
17
21
  case "docker": {
18
- const mod = await import("./dist-LOZBWMBF.js");
22
+ const mod = await import("./dist-BQNX7RQE.js");
19
23
  return mod.dockerProvider;
20
24
  }
21
25
  case "daytona": {
22
- const mod = await import("./dist-L4LCG5SJ.js");
26
+ const mod = await import("./dist-TMHSUVTP.js");
23
27
  await mod.ensureDaytonaCredentials();
24
28
  return mod.daytonaProvider;
25
29
  }
26
30
  case "hetzner": {
27
- const mod = await import("./dist-ZODPD2I6.js");
31
+ const mod = await import("./dist-5FQGYRW5.js");
28
32
  await mod.ensureHetznerCredentials();
29
33
  return mod.hetznerProvider;
30
34
  }
35
+ case "vercel": {
36
+ const mod = await import("./dist-PZW3GWWU.js");
37
+ await mod.ensureVercelCredentials();
38
+ return mod.vercelProvider;
39
+ }
31
40
  default:
32
41
  throw new Error(`unknown sandbox provider: ${String(name)}`);
33
42
  }
@@ -179,6 +188,25 @@ function runQuiet(cmd, argv) {
179
188
  });
180
189
  }
181
190
 
191
+ // src/terminal/title.ts
192
+ var ESC = "\x1B";
193
+ var BEL = "\x07";
194
+ function sanitize(title) {
195
+ return title.replace(/[\x00-\x1f\x7f]/g, " ").trim();
196
+ }
197
+ function setTerminalTitle(title, stream = process.stdout) {
198
+ if (!stream.isTTY) return;
199
+ stream.write(`${ESC}]0;${sanitize(title)}${BEL}`);
200
+ }
201
+ function pushTerminalTitle(stream = process.stdout) {
202
+ if (!stream.isTTY) return;
203
+ stream.write(`${ESC}[22;2t`);
204
+ }
205
+ function popTerminalTitle(stream = process.stdout) {
206
+ if (!stream.isTTY) return;
207
+ stream.write(`${ESC}[23;2t`);
208
+ }
209
+
182
210
  // src/wrapped-pty/input-router.ts
183
211
  var KEY_ENTER = 13;
184
212
  var KEY_LF = 10;
@@ -189,12 +217,58 @@ var KEY_Y_UP = 89;
189
217
  var KEY_N_LOW = 110;
190
218
  var KEY_N_UP = 78;
191
219
  var KEY_LEADER = 1;
220
+ var KEY_CTRL_V = 22;
221
+ var KEY_A_LOW = 97;
222
+ function parseCsiKey(buf, i) {
223
+ if (buf[i] !== KEY_ESC || buf[i + 1] !== 91) return null;
224
+ const params = [];
225
+ let val = -1;
226
+ for (let j = i + 2; j < buf.length; j++) {
227
+ const b = buf[j];
228
+ if (b !== void 0 && b >= 48 && b <= 57) {
229
+ val = (val < 0 ? 0 : val) * 10 + (b - 48);
230
+ continue;
231
+ }
232
+ if (b === 59) {
233
+ params.push(val);
234
+ val = -1;
235
+ continue;
236
+ }
237
+ if (b === 58) {
238
+ params.push(val);
239
+ val = -1;
240
+ while (j + 1 < buf.length && buf[j + 1] !== 59 && buf[j + 1] !== 117 && buf[j + 1] !== 126) {
241
+ j++;
242
+ }
243
+ continue;
244
+ }
245
+ if (b === 117 || b === 126) {
246
+ if (val >= 0) params.push(val);
247
+ const len = j - i + 1;
248
+ const modsToCtrl = (m) => (m - 1 & 4) !== 0;
249
+ if (b === 117) {
250
+ const code2 = params[0];
251
+ if (code2 === void 0 || code2 < 0) return null;
252
+ return { len, code: code2, ctrl: modsToCtrl(params[1] ?? 1) };
253
+ }
254
+ if (params[0] !== 27) return null;
255
+ const code = params[2];
256
+ if (code === void 0 || code < 0) return null;
257
+ return { len, code, ctrl: modsToCtrl(params[1] ?? 1) };
258
+ }
259
+ return null;
260
+ }
261
+ return null;
262
+ }
192
263
  var DEFAULT_LEADER_TIMEOUT_MS = 2e3;
193
264
  function createInputRouter(opts) {
194
265
  let active = null;
195
266
  let disposed = false;
196
267
  const leaderChords = opts.leaderChords ?? {};
197
268
  const leaderEnabled = Object.keys(leaderChords).length > 0;
269
+ const onPasteImage = opts.onPasteImage;
270
+ const pasteEnabled = typeof onPasteImage === "function";
271
+ let pasteInFlight = false;
198
272
  const leaderTimeoutMs = opts.leaderTimeoutMs ?? DEFAULT_LEADER_TIMEOUT_MS;
199
273
  const setTimer = opts.setTimer ?? ((ms, fn) => setTimeout(fn, ms));
200
274
  const clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h));
@@ -272,25 +346,74 @@ function createInputRouter(opts) {
272
346
  return;
273
347
  }
274
348
  };
349
+ const triggerPaste = () => {
350
+ if (pasteInFlight) return;
351
+ pasteInFlight = true;
352
+ const done = () => {
353
+ pasteInFlight = false;
354
+ if (!disposed) opts.onForward(Buffer.from([KEY_CTRL_V]));
355
+ };
356
+ void Promise.resolve().then(() => onPasteImage?.()).then(done, done);
357
+ };
275
358
  const feedSteady = (buf) => {
276
359
  let chunkStart = 0;
277
360
  const flushChunk = (end) => {
278
361
  if (end > chunkStart) opts.onForward(buf.subarray(chunkStart, end));
279
362
  chunkStart = end;
280
363
  };
281
- for (let i = 0; i < buf.length; i++) {
364
+ let i = 0;
365
+ while (i < buf.length) {
282
366
  const byte = buf[i];
283
- if (byte === void 0) continue;
367
+ if (byte === void 0) {
368
+ i++;
369
+ continue;
370
+ }
284
371
  if (leader) {
372
+ const k = parseCsiKey(buf, i);
373
+ if (k) {
374
+ if (k.ctrl && k.code === KEY_A_LOW) {
375
+ exitLeader();
376
+ opts.onForward(Buffer.from([KEY_LEADER]));
377
+ } else {
378
+ const action = leaderChords[String.fromCharCode(k.code).toLowerCase()];
379
+ exitLeader();
380
+ if (action) opts.onAction?.(action);
381
+ else opts.onForward(buf.subarray(i, i + k.len));
382
+ }
383
+ i += k.len;
384
+ chunkStart = i;
385
+ continue;
386
+ }
285
387
  resolveLeaderByte(byte);
286
- chunkStart = i + 1;
388
+ i += 1;
389
+ chunkStart = i;
287
390
  continue;
288
391
  }
289
- if (byte === KEY_LEADER) {
392
+ if (leaderEnabled && byte === KEY_LEADER) {
290
393
  flushChunk(i);
291
- chunkStart = i + 1;
292
394
  enterLeader();
395
+ i += 1;
396
+ chunkStart = i;
397
+ continue;
398
+ }
399
+ if (leaderEnabled && byte === KEY_ESC) {
400
+ const k = parseCsiKey(buf, i);
401
+ if (k && k.ctrl && k.code === KEY_A_LOW) {
402
+ flushChunk(i);
403
+ enterLeader();
404
+ i += k.len;
405
+ chunkStart = i;
406
+ continue;
407
+ }
293
408
  }
409
+ if (pasteEnabled && byte === KEY_CTRL_V) {
410
+ flushChunk(i);
411
+ i += 1;
412
+ chunkStart = i;
413
+ triggerPaste();
414
+ continue;
415
+ }
416
+ i += 1;
294
417
  }
295
418
  flushChunk(buf.length);
296
419
  };
@@ -314,7 +437,7 @@ function createInputRouter(opts) {
314
437
  }
315
438
  return;
316
439
  }
317
- if (!leaderEnabled) {
440
+ if (!leaderEnabled && !pasteEnabled) {
318
441
  opts.onForward(buf);
319
442
  return;
320
443
  }
@@ -569,9 +692,13 @@ function statusLine(box, w, stateLabel, groups = HINT_GROUPS, fallbackGroups) {
569
692
  // src/wrapped-pty/footer.ts
570
693
  var SPINNER_FRAMES = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"];
571
694
  var URGENT = "\x1B[38;5;220m\x1B[1m";
695
+ var TITLE = "\x1B[1m\x1B[38;5;253m";
572
696
  var TXT = "\x1B[38;5;250m";
573
697
  var SUBTLE = "\x1B[38;5;245m";
574
698
  var RESET = "\x1B[0m";
699
+ var UNDERLINE = "\x1B[4m";
700
+ var NO_UNDERLINE = "\x1B[24m";
701
+ var QUESTION_ACCENT = "\x1B[38;5;51m\x1B[1m";
575
702
  var NOTICE_BG = "\x1B[48;5;220m";
576
703
  var NOTICE_FG = "\x1B[38;5;16m\x1B[1m";
577
704
  var FLASH_FG = "\x1B[38;5;150m\x1B[1m";
@@ -604,6 +731,17 @@ function padTo(visible, width) {
604
731
  }
605
732
  return visible + " ".repeat(width - visible.length);
606
733
  }
734
+ function answerChip(defaultAnswer) {
735
+ const yesKey = "y Yes";
736
+ const noKey = "n No";
737
+ const sep = " \xB7 ";
738
+ const yesIsDefault = defaultAnswer === "y";
739
+ const yes = yesIsDefault ? `${UNDERLINE}${yesKey}${NO_UNDERLINE}` : yesKey;
740
+ const no = yesIsDefault ? noKey : `${UNDERLINE}${noKey}${NO_UNDERLINE}`;
741
+ const ansi = `${NOTICE_BG}${NOTICE_FG} ${yes}${sep}${no} ${RESET}`;
742
+ const width = ` ${yesKey}${sep}${noKey} `.length;
743
+ return { ansi, width };
744
+ }
607
745
  function renderFooter(state, cols) {
608
746
  if (cols <= 0) return "";
609
747
  if (state.kind === "idle") {
@@ -634,18 +772,16 @@ function renderFooter(state, cols) {
634
772
  return `${BAR_BG}${FLASH_FG}${prefix}${TXT}${message2}${RESET}`;
635
773
  }
636
774
  if (state.kind === "notice") {
637
- const spinner = SPINNER_FRAMES[state.frame % SPINNER_FRAMES.length];
638
- const prefix = ` ${spinner} `;
775
+ const spinner2 = SPINNER_FRAMES[state.frame % SPINNER_FRAMES.length];
776
+ const prefix = ` ${spinner2} `;
639
777
  const inner2 = Math.max(0, cols - prefix.length);
640
778
  const message2 = padTo(state.message, inner2);
641
779
  return `${NOTICE_BG}${NOTICE_FG}${prefix}${message2}${RESET}`;
642
780
  }
643
- const def = state.prompt.defaultAnswer ?? "n";
644
- const yn = def === "y" ? "[Y/n]" : "[y/N]";
781
+ const chip = answerChip(state.prompt.defaultAnswer);
645
782
  const tag = " [!] ";
646
783
  const sep = " ";
647
- const hintW = ` ${yn} `.length;
648
- const inner = Math.max(0, cols - tag.length - hintW);
784
+ const inner = Math.max(0, cols - tag.length - chip.width);
649
785
  const detailRaw = state.prompt.detail ?? "";
650
786
  let message = state.prompt.message;
651
787
  let detail = detailRaw;
@@ -660,7 +796,7 @@ function renderFooter(state, cols) {
660
796
  }
661
797
  const middlePlain = detail.length > 0 ? `${message}${sep}${detail}` : message;
662
798
  const padded = padTo(middlePlain, inner);
663
- return `${BAR_BG}${URGENT}${tag}${TXT}${padded}${SUBTLE} ${yn} ${RESET}`;
799
+ return `${BAR_BG}${URGENT}${tag}${TXT}${padded}${RESET}${chip.ansi}`;
664
800
  }
665
801
  function cursorMoveTo(row, col) {
666
802
  return `\x1B[${String(row)};${String(col)}H`;
@@ -669,6 +805,73 @@ var CURSOR_SAVE = "\x1B7";
669
805
  var CURSOR_RESTORE = "\x1B8";
670
806
  var SYNC_BEGIN = "\x1B[?2026h";
671
807
  var SYNC_END = "\x1B[?2026l";
808
+ var ALERT_BAND_ROWS = 3;
809
+ function blankBar(cols, bg) {
810
+ return `${bg}${" ".repeat(Math.max(0, cols))}${RESET}`;
811
+ }
812
+ function renderPromptBand(prompt, cols, rows) {
813
+ const tag = " [!] ";
814
+ const indent = " ".repeat(tag.length);
815
+ const contW = Math.max(0, cols - indent.length);
816
+ const chip = answerChip(prompt.defaultAnswer);
817
+ const title = (prompt.context?.command ?? "confirm").toUpperCase();
818
+ const titleW = Math.max(0, cols - tag.length - chip.width);
819
+ const titlePadded = padTo(title, titleW);
820
+ const line1 = `${BAR_BG}${URGENT}${tag}${TITLE}${titlePadded}${RESET}${chip.ansi}`;
821
+ const line2 = `${BAR_BG}${TXT}${indent}${padTo(prompt.message, contW)}${RESET}`;
822
+ const line3 = `${BAR_BG}${SUBTLE}${indent}${padTo(prompt.detail ?? "", contW)}${RESET}`;
823
+ return [line1, line2, line3].slice(0, rows);
824
+ }
825
+ function renderNoticeBand(message, frame, cols, rows) {
826
+ const spinner2 = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
827
+ const prefix = ` ${spinner2} `;
828
+ const indent = " ".repeat(prefix.length);
829
+ const firstW = Math.max(0, cols - prefix.length);
830
+ const contW = Math.max(0, cols - indent.length);
831
+ const out = [];
832
+ let i = 0;
833
+ for (let r = 0; r < rows; r++) {
834
+ const isLast = r === rows - 1;
835
+ const w = r === 0 ? firstW : contW;
836
+ let cell;
837
+ if (i >= message.length) {
838
+ cell = " ".repeat(w);
839
+ } else if (isLast) {
840
+ cell = padTo(message.slice(i), w);
841
+ i = message.length;
842
+ } else {
843
+ cell = message.slice(i, i + w).padEnd(w);
844
+ i += w;
845
+ }
846
+ const lead = r === 0 ? prefix : indent;
847
+ out.push(`${NOTICE_BG}${NOTICE_FG}${lead}${cell}${RESET}`);
848
+ }
849
+ return out;
850
+ }
851
+ function renderQuestionBand(payload, cols, rows) {
852
+ const q = payload.questions[0];
853
+ if (!q) return Array.from({ length: rows }, () => blankBar(cols, BAR_BG));
854
+ const tag = " [?] ";
855
+ const indent = " ".repeat(tag.length);
856
+ const innerW = Math.max(0, cols - tag.length);
857
+ const contW = Math.max(0, cols - indent.length);
858
+ const header = q.header && q.header.trim().length > 0 ? q.header : "Question";
859
+ const headerPadded = padTo(header, innerW);
860
+ const line1 = `${BAR_BG}${QUESTION_ACCENT}${tag}${TXT}${headerPadded}${RESET}`;
861
+ const questionText = padTo(q.question, contW);
862
+ const line2 = `${BAR_BG}${TXT}${indent}${questionText}${RESET}`;
863
+ const optLabels = q.options.map((o) => o.label).join(" \xB7 ");
864
+ const optsLine = optLabels.length > 0 ? `options: ${optLabels}` : "";
865
+ const optsPadded = padTo(optsLine, contW);
866
+ const line3 = `${BAR_BG}${SUBTLE}${indent}${optsPadded}${RESET}`;
867
+ return [line1, line2, line3].slice(0, rows);
868
+ }
869
+ function renderAlertBand(state, cols, rows = ALERT_BAND_ROWS) {
870
+ if (cols <= 0 || rows <= 0) return Array.from({ length: Math.max(0, rows) }, () => "");
871
+ if (state.kind === "prompt") return renderPromptBand(state.prompt, cols, rows);
872
+ if (state.kind === "notice") return renderNoticeBand(state.message, state.frame, cols, rows);
873
+ return renderQuestionBand(state.question, cols, rows);
874
+ }
672
875
 
673
876
  // src/wrapped-pty/prompt-client.ts
674
877
  import { request as httpRequest } from "http";
@@ -837,9 +1040,13 @@ function postAnswer(opts) {
837
1040
 
838
1041
  // src/wrapped-pty/run.ts
839
1042
  var FOOTER_ROWS = 1;
1043
+ var MIN_INNER_ROWS = 5;
840
1044
  var STATUS_POLL_INTERVAL_MS = 3e3;
841
1045
  var SPINNER_INTERVAL_MS = 120;
842
1046
  var FLASH_DURATION_MS = 2e3;
1047
+ var RAPID_RECONNECT_MS = 8e3;
1048
+ var MAX_RAPID_RECONNECTS = 3;
1049
+ var CHECKPOINT_DROP_GRACE_MS = 4e3;
843
1050
  var ACTION_FLASH = {
844
1051
  screen: "Opening noVNC viewer\u2026",
845
1052
  code: "Launching VS Code / Cursor\u2026",
@@ -880,24 +1087,34 @@ async function runWrappedAttach(opts) {
880
1087
  }
881
1088
  }
882
1089
  if (!process.stdout.isTTY || !process.stdin.isTTY) {
883
- return runFallback(command, opts.dockerArgv);
1090
+ return runFallback(command, opts.dockerArgv, opts.env);
884
1091
  }
885
1092
  const backend = await loadPtyBackend();
886
1093
  if (!backend) {
887
1094
  process.stderr.write(
888
1095
  "agentbox: permission prompts disabled (node-pty backend unavailable)\n"
889
1096
  );
890
- return runFallback(command, opts.dockerArgv);
1097
+ return runFallback(command, opts.dockerArgv, opts.env);
891
1098
  }
892
1099
  const cols = process.stdout.columns ?? 80;
893
1100
  const rows = process.stdout.rows ?? 24;
894
1101
  const innerRows = Math.max(1, rows - FOOTER_ROWS);
895
- const pty = backend.ptySpawn(command, opts.dockerArgv, {
1102
+ let pty = backend.ptySpawn(command, opts.dockerArgv, {
896
1103
  name: "xterm-256color",
897
1104
  cols,
898
1105
  rows: innerRows,
899
- env: process.env
1106
+ env: opts.env ? { ...process.env, ...opts.env } : process.env
900
1107
  });
1108
+ let lastSpawnAt = Date.now();
1109
+ const resizePty = (c, r) => {
1110
+ try {
1111
+ pty.resize(c, r);
1112
+ } catch {
1113
+ }
1114
+ };
1115
+ pushTerminalTitle();
1116
+ let lastEmittedTitle = opts.boxName;
1117
+ setTerminalTitle(lastEmittedTitle);
901
1118
  const detachable = opts.detachable ?? opts.mode === "claude";
902
1119
  let leaderActive = false;
903
1120
  const buildIdle = (sessionTitle, claudeActivity) => ({
@@ -914,21 +1131,46 @@ async function runWrappedAttach(opts) {
914
1131
  let lastActivity;
915
1132
  let capturingPrompt = null;
916
1133
  let activeNotice = null;
1134
+ let reconnectBanner = null;
917
1135
  let noticeFrame = 0;
1136
+ let questionPayload = null;
1137
+ let bandState = null;
1138
+ let bandReservedRows = 0;
918
1139
  let spinnerTimer = null;
919
1140
  let flashMessage = null;
920
1141
  let flashTimer = null;
921
- const redrawFooter = () => {
1142
+ let reconnecting = false;
1143
+ let reconnectAbort = null;
1144
+ let userDetached = false;
1145
+ let checkpointNoticeAt = 0;
1146
+ let checkpointNoticeClearedAt = 0;
1147
+ const reservedRows = () => FOOTER_ROWS + bandReservedRows;
1148
+ const bandFits = () => {
1149
+ const rs = process.stdout.rows ?? rows;
1150
+ return rs - FOOTER_ROWS - ALERT_BAND_ROWS >= MIN_INNER_ROWS;
1151
+ };
1152
+ const redrawChrome = () => {
922
1153
  const cs = process.stdout.columns ?? cols;
923
1154
  const rs = process.stdout.rows ?? rows;
924
- const line = renderFooter(footerState, cs);
925
- const payload = SYNC_BEGIN + CURSOR_SAVE + cursorMoveTo(rs, 1) + line + CURSOR_RESTORE + SYNC_END;
1155
+ const footerLine = renderFooter(footerState, cs);
1156
+ let payload = SYNC_BEGIN + CURSOR_SAVE;
1157
+ if (bandReservedRows > 0 && bandState) {
1158
+ const bandLines = renderAlertBand(bandState, cs, bandReservedRows);
1159
+ for (let i = 0; i < bandLines.length; i++) {
1160
+ const row = rs - FOOTER_ROWS - (bandLines.length - i);
1161
+ payload += cursorMoveTo(row + 1, 1) + bandLines[i];
1162
+ }
1163
+ }
1164
+ payload += cursorMoveTo(rs, 1) + footerLine + CURSOR_RESTORE + SYNC_END;
926
1165
  process.stdout.write(payload);
927
1166
  };
928
1167
  const recomputeFooter = () => {
929
- if (capturingPrompt) {
1168
+ const collapsed = bandState !== null && bandReservedRows === 0;
1169
+ if (collapsed && reconnectBanner) {
1170
+ footerState = { kind: "notice", message: reconnectBanner, frame: noticeFrame };
1171
+ } else if (collapsed && capturingPrompt) {
930
1172
  footerState = { kind: "prompt", prompt: capturingPrompt };
931
- } else if (activeNotice) {
1173
+ } else if (collapsed && activeNotice) {
932
1174
  footerState = { kind: "notice", message: activeNotice.message, frame: noticeFrame };
933
1175
  } else if (flashMessage) {
934
1176
  footerState = { kind: "flash", message: flashMessage };
@@ -936,13 +1178,48 @@ async function runWrappedAttach(opts) {
936
1178
  footerState = buildIdle(lastSessionTitle, lastActivity);
937
1179
  }
938
1180
  };
1181
+ const recomputeBand = () => {
1182
+ if (reconnectBanner) {
1183
+ bandState = { kind: "notice", message: reconnectBanner, frame: noticeFrame };
1184
+ } else if (capturingPrompt) {
1185
+ bandState = { kind: "prompt", prompt: capturingPrompt };
1186
+ } else if (activeNotice) {
1187
+ bandState = { kind: "notice", message: activeNotice.message, frame: noticeFrame };
1188
+ } else if (questionPayload) {
1189
+ bandState = { kind: "question", question: questionPayload };
1190
+ } else {
1191
+ bandState = null;
1192
+ }
1193
+ };
1194
+ const relayoutForBand = () => {
1195
+ const cs = process.stdout.columns ?? cols;
1196
+ const rs = process.stdout.rows ?? rows;
1197
+ const inner = Math.max(1, rs - reservedRows());
1198
+ resizePty(cs, inner);
1199
+ process.stdout.write(`\x1B[1;${String(inner)}r`);
1200
+ let clear = SYNC_BEGIN + CURSOR_SAVE;
1201
+ for (let r = inner + 1; r <= rs; r++) clear += cursorMoveTo(r, 1) + "\x1B[2K";
1202
+ clear += CURSOR_RESTORE + SYNC_END;
1203
+ process.stdout.write(clear);
1204
+ };
1205
+ const applyBandChange = () => {
1206
+ recomputeBand();
1207
+ const wantRows = bandState && bandFits() ? ALERT_BAND_ROWS : 0;
1208
+ if (wantRows !== bandReservedRows) {
1209
+ bandReservedRows = wantRows;
1210
+ relayoutForBand();
1211
+ }
1212
+ recomputeFooter();
1213
+ redrawChrome();
1214
+ };
939
1215
  const startSpinner = () => {
940
1216
  if (spinnerTimer) return;
941
1217
  spinnerTimer = setInterval(() => {
942
1218
  noticeFrame++;
943
- if (footerState.kind === "notice") {
944
- recomputeFooter();
945
- redrawFooter();
1219
+ if (bandState?.kind === "notice") {
1220
+ bandState = { kind: "notice", message: bandState.message, frame: noticeFrame };
1221
+ if (bandReservedRows === 0) recomputeFooter();
1222
+ redrawChrome();
946
1223
  }
947
1224
  }, SPINNER_INTERVAL_MS);
948
1225
  if (typeof spinnerTimer.unref === "function") spinnerTimer.unref();
@@ -953,14 +1230,20 @@ async function runWrappedAttach(opts) {
953
1230
  spinnerTimer = null;
954
1231
  }
955
1232
  };
956
- pty.onData((d) => {
957
- process.stdout.write(d);
958
- redrawFooter();
959
- });
1233
+ const wireOutput = () => {
1234
+ pty.onData((d) => {
1235
+ process.stdout.write(d);
1236
+ redrawChrome();
1237
+ });
1238
+ };
1239
+ wireOutput();
960
1240
  const leaderChords = detachable ? { c: "code", s: "screen", u: "url", d: "detach" } : { c: "code", s: "screen", u: "url" };
961
1241
  const runAction = (name) => {
962
1242
  if (name === "detach") {
963
- pty.write("d");
1243
+ if (!reconnecting) {
1244
+ userDetached = true;
1245
+ pty.write("d");
1246
+ }
964
1247
  return;
965
1248
  }
966
1249
  const cliEntry = process.argv[1];
@@ -982,31 +1265,61 @@ async function runWrappedAttach(opts) {
982
1265
  flashTimer = null;
983
1266
  flashMessage = null;
984
1267
  recomputeFooter();
985
- redrawFooter();
1268
+ redrawChrome();
986
1269
  }, FLASH_DURATION_MS);
987
1270
  if (typeof flashTimer.unref === "function") flashTimer.unref();
988
1271
  recomputeFooter();
989
- redrawFooter();
1272
+ redrawChrome();
1273
+ };
1274
+ const handlePasteImage = async () => {
1275
+ if (!opts.onPasteImage) return;
1276
+ if (flashTimer) {
1277
+ clearTimeout(flashTimer);
1278
+ flashTimer = null;
1279
+ }
1280
+ flashMessage = "Pasting image\u2026";
1281
+ recomputeFooter();
1282
+ redrawChrome();
1283
+ let result = "error";
1284
+ try {
1285
+ result = await opts.onPasteImage();
1286
+ } catch (e) {
1287
+ logErr(`paste-image failed: ${e.message}`);
1288
+ }
1289
+ flashMessage = result === "pasted" ? "Image pasted" : result === "no-image" ? "No image in clipboard" : "Image paste failed";
1290
+ flashTimer = setTimeout(() => {
1291
+ flashTimer = null;
1292
+ flashMessage = null;
1293
+ recomputeFooter();
1294
+ redrawChrome();
1295
+ }, FLASH_DURATION_MS);
1296
+ if (typeof flashTimer.unref === "function") flashTimer.unref();
1297
+ recomputeFooter();
1298
+ redrawChrome();
990
1299
  };
991
1300
  const router = createInputRouter({
992
1301
  onForward: (b) => {
1302
+ if (reconnecting) {
1303
+ if (b.length === 1 && b[0] === 3) reconnectAbort?.abort();
1304
+ return;
1305
+ }
993
1306
  pty.write(b.toString("utf8"));
994
1307
  },
995
1308
  onAnswer: (body) => {
996
1309
  void postAnswer({ relayBaseUrl: opts.relayBaseUrl, body });
997
1310
  capturingPrompt = null;
998
- recomputeFooter();
999
- redrawFooter();
1311
+ applyBandChange();
1000
1312
  },
1001
1313
  leaderChords,
1002
1314
  onLeaderChange: (open) => {
1003
1315
  leaderActive = open;
1004
1316
  recomputeFooter();
1005
- redrawFooter();
1317
+ redrawChrome();
1006
1318
  },
1007
1319
  onAction: (name) => {
1008
1320
  runAction(name);
1009
- }
1321
+ },
1322
+ onPasteImage: opts.onPasteImage ? handlePasteImage : void 0
1010
1323
  });
1011
1324
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
1012
1325
  process.stdin.resume();
@@ -1017,10 +1330,12 @@ async function runWrappedAttach(opts) {
1017
1330
  const onResize = () => {
1018
1331
  const cs = process.stdout.columns ?? cols;
1019
1332
  const rs = process.stdout.rows ?? rows;
1020
- const inner = Math.max(1, rs - FOOTER_ROWS);
1021
- pty.resize(cs, inner);
1333
+ bandReservedRows = bandState && bandFits() ? ALERT_BAND_ROWS : 0;
1334
+ const inner = Math.max(1, rs - reservedRows());
1335
+ resizePty(cs, inner);
1022
1336
  process.stdout.write(`\x1B[1;${String(inner)}r`);
1023
- redrawFooter();
1337
+ recomputeFooter();
1338
+ redrawChrome();
1024
1339
  };
1025
1340
  process.stdout.on("resize", onResize);
1026
1341
  const stream = subscribePrompts({
@@ -1028,8 +1343,7 @@ async function runWrappedAttach(opts) {
1028
1343
  boxId: opts.boxId,
1029
1344
  onPrompt: (ev) => {
1030
1345
  capturingPrompt = ev;
1031
- recomputeFooter();
1032
- redrawFooter();
1346
+ applyBandChange();
1033
1347
  router.capture(ev).catch((e) => {
1034
1348
  const msg = e instanceof Error ? e.message : String(e);
1035
1349
  if (msg !== "resolved-elsewhere") {
@@ -1041,22 +1355,21 @@ async function runWrappedAttach(opts) {
1041
1355
  if (capturingPrompt && capturingPrompt.id === id) {
1042
1356
  capturingPrompt = null;
1043
1357
  router.abort("resolved-elsewhere");
1044
- recomputeFooter();
1045
- redrawFooter();
1358
+ applyBandChange();
1046
1359
  }
1047
1360
  },
1048
1361
  onNotice: (ev) => {
1362
+ if (ev.kind === "checkpoint") checkpointNoticeAt = Date.now();
1049
1363
  activeNotice = ev;
1050
1364
  startSpinner();
1051
- recomputeFooter();
1052
- redrawFooter();
1365
+ applyBandChange();
1053
1366
  },
1054
1367
  onNoticeCleared: (id) => {
1055
1368
  if (activeNotice && activeNotice.id === id) {
1369
+ if (activeNotice.kind === "checkpoint") checkpointNoticeClearedAt = Date.now();
1056
1370
  activeNotice = null;
1057
1371
  stopSpinner();
1058
- recomputeFooter();
1059
- redrawFooter();
1372
+ applyBandChange();
1060
1373
  }
1061
1374
  }
1062
1375
  });
@@ -1067,14 +1380,26 @@ async function runWrappedAttach(opts) {
1067
1380
  name: opts.boxName,
1068
1381
  projectIndex: opts.projectIndex
1069
1382
  });
1070
- const nextTitle = status?.claude?.sessionTitle?.trim() || void 0;
1071
- const nextActivity = status?.claude?.state || void 0;
1383
+ const body = opts.mode === "codex" ? status?.codex : opts.mode === "opencode" ? status?.opencode : opts.mode === "shell" ? void 0 : status?.claude;
1384
+ const nextTitle = body?.sessionTitle?.trim() || void 0;
1385
+ const nextActivity = body?.state || void 0;
1386
+ const desiredTitle = nextTitle ?? opts.boxName;
1387
+ if (desiredTitle !== lastEmittedTitle) {
1388
+ lastEmittedTitle = desiredTitle;
1389
+ setTerminalTitle(desiredTitle);
1390
+ }
1391
+ const nextQuestion = opts.mode === "claude" && status?.claude.state === "question" ? status.claude.question ?? null : null;
1392
+ const questionChanged = (nextQuestion?.capturedAt ?? null) !== (questionPayload?.capturedAt ?? null);
1393
+ if (questionChanged) {
1394
+ questionPayload = nextQuestion;
1395
+ applyBandChange();
1396
+ }
1072
1397
  if (nextTitle === lastSessionTitle && nextActivity === lastActivity) return;
1073
1398
  lastSessionTitle = nextTitle;
1074
1399
  lastActivity = nextActivity;
1075
1400
  if (footerState.kind === "idle") {
1076
1401
  recomputeFooter();
1077
- redrawFooter();
1402
+ redrawChrome();
1078
1403
  }
1079
1404
  } catch (e) {
1080
1405
  logErr(`status poll failed: ${e.message}`);
@@ -1089,10 +1414,82 @@ async function runWrappedAttach(opts) {
1089
1414
  if (opts.mode === "shell" && !detachable) {
1090
1415
  process.stdout.write("\x1B[H\x1B[2J");
1091
1416
  }
1092
- redrawFooter();
1093
- const exitCode = await new Promise((resolve) => {
1094
- pty.onExit(({ exitCode: exitCode2 }) => resolve(exitCode2));
1095
- });
1417
+ redrawChrome();
1418
+ const reconnectFlow = async (code) => {
1419
+ const controller = new AbortController();
1420
+ reconnecting = true;
1421
+ reconnectAbort = controller;
1422
+ reconnectBanner = "box rebooting \u2014 reconnecting\u2026";
1423
+ startSpinner();
1424
+ applyBandChange();
1425
+ let spec = null;
1426
+ try {
1427
+ spec = await opts.reconnect?.(controller.signal, code) ?? null;
1428
+ } catch (e) {
1429
+ logErr(`reconnect failed: ${e.message}`);
1430
+ } finally {
1431
+ reconnecting = false;
1432
+ reconnectAbort = null;
1433
+ reconnectBanner = null;
1434
+ if (!activeNotice) stopSpinner();
1435
+ applyBandChange();
1436
+ }
1437
+ if (spec) {
1438
+ flashMessage = "reconnected";
1439
+ if (flashTimer) clearTimeout(flashTimer);
1440
+ flashTimer = setTimeout(() => {
1441
+ flashTimer = null;
1442
+ flashMessage = null;
1443
+ recomputeFooter();
1444
+ redrawChrome();
1445
+ }, FLASH_DURATION_MS);
1446
+ if (typeof flashTimer.unref === "function") flashTimer.unref();
1447
+ recomputeFooter();
1448
+ redrawChrome();
1449
+ }
1450
+ return spec;
1451
+ };
1452
+ let exitCode = 0;
1453
+ let rapidFails = 0;
1454
+ for (; ; ) {
1455
+ const code = await new Promise((resolve) => {
1456
+ pty.onExit(({ exitCode: exitCode2 }) => resolve(exitCode2));
1457
+ });
1458
+ if (userDetached || !opts.reconnect) {
1459
+ exitCode = code;
1460
+ break;
1461
+ }
1462
+ const checkpointing = checkpointNoticeAt > checkpointNoticeClearedAt || Date.now() - checkpointNoticeClearedAt < CHECKPOINT_DROP_GRACE_MS;
1463
+ if (!checkpointing && code === 0) {
1464
+ exitCode = code;
1465
+ break;
1466
+ }
1467
+ rapidFails = Date.now() - lastSpawnAt < RAPID_RECONNECT_MS ? rapidFails + 1 : 0;
1468
+ if (rapidFails >= MAX_RAPID_RECONNECTS) {
1469
+ logErr("giving up reconnect after repeated rapid failures");
1470
+ exitCode = code;
1471
+ break;
1472
+ }
1473
+ const next = await reconnectFlow(code);
1474
+ if (!next) {
1475
+ exitCode = code;
1476
+ break;
1477
+ }
1478
+ const rsNow = process.stdout.rows ?? rows;
1479
+ const innerNow = Math.max(1, rsNow - reservedRows());
1480
+ pty = backend.ptySpawn(next.command, next.argv, {
1481
+ name: "xterm-256color",
1482
+ cols: process.stdout.columns ?? cols,
1483
+ rows: innerNow,
1484
+ env: next.env ? { ...process.env, ...next.env } : process.env
1485
+ });
1486
+ wireOutput();
1487
+ lastSpawnAt = Date.now();
1488
+ checkpointNoticeAt = 0;
1489
+ checkpointNoticeClearedAt = 0;
1490
+ process.stdout.write(`\x1B[1;${String(innerNow)}r`);
1491
+ redrawChrome();
1492
+ }
1096
1493
  process.stdin.off("data", onStdinData);
1097
1494
  process.stdout.off("resize", onResize);
1098
1495
  clearInterval(statusTimer);
@@ -1104,72 +1501,295 @@ async function runWrappedAttach(opts) {
1104
1501
  router.dispose();
1105
1502
  const rsFinal = process.stdout.rows ?? rows;
1106
1503
  const csFinal = process.stdout.columns ?? cols;
1107
- process.stdout.write(
1108
- "\x1B[r" + cursorMoveTo(rsFinal, 1) + `\x1B[2K` + cursorMoveTo(rsFinal, csFinal)
1109
- );
1504
+ let teardownPaint = "\x1B[r";
1505
+ for (let r = rsFinal - bandReservedRows; r <= rsFinal; r++) {
1506
+ if (r >= 1) teardownPaint += cursorMoveTo(r, 1) + "\x1B[2K";
1507
+ }
1508
+ teardownPaint += cursorMoveTo(rsFinal, csFinal);
1509
+ process.stdout.write(teardownPaint);
1510
+ popTerminalTitle();
1110
1511
  if (exitCode === 0 && opts.detachNotice) {
1111
1512
  process.stdout.write("\x1B[1A\x1B[2K\r" + opts.detachNotice + "\n");
1112
1513
  }
1113
1514
  return exitCode;
1114
1515
  }
1115
- function runFallback(command, argv) {
1116
- const child = spawnSync(command, argv, { stdio: "inherit" });
1516
+ function runFallback(command, argv, env) {
1517
+ const child = spawnSync(command, argv, {
1518
+ stdio: "inherit",
1519
+ env: env ? { ...process.env, ...env } : process.env
1520
+ });
1117
1521
  return child.status ?? 0;
1118
1522
  }
1119
1523
 
1524
+ // src/lib/paste-image.ts
1525
+ import { rm as rm2 } from "fs/promises";
1526
+ import { dirname } from "path";
1527
+
1528
+ // src/lib/host-clipboard.ts
1529
+ import { mkdtemp, rm, stat, writeFile } from "fs/promises";
1530
+ import { tmpdir } from "os";
1531
+ import { join } from "path";
1532
+ import { execa } from "execa";
1533
+ async function captureClipboardImage() {
1534
+ if (process.platform !== "darwin" && process.platform !== "linux") return null;
1535
+ const dir = await mkdtemp(join(tmpdir(), "agentbox-clip-"));
1536
+ const pngPath = join(dir, "clip.png");
1537
+ const ok = process.platform === "darwin" ? await captureDarwin(dir, pngPath) : await captureLinux(pngPath);
1538
+ if (ok) return pngPath;
1539
+ await rm(dir, { recursive: true, force: true }).catch(() => {
1540
+ });
1541
+ return null;
1542
+ }
1543
+ async function clipboardCaptureAvailable() {
1544
+ if (process.platform === "darwin") return true;
1545
+ if (process.platform === "linux") return await linuxClipboardTool() !== null;
1546
+ return false;
1547
+ }
1548
+ function captureScriptArgs(pngPath, tiffPath) {
1549
+ return [
1550
+ "try",
1551
+ " set theData to (the clipboard as \xABclass PNGf\xBB)",
1552
+ ` set fh to open for access (POSIX file ${JSON.stringify(pngPath)}) with write permission`,
1553
+ " set eof fh to 0",
1554
+ " write theData to fh",
1555
+ " close access fh",
1556
+ ' return "PNG"',
1557
+ "on error",
1558
+ " try",
1559
+ " set theData to (the clipboard as \xABclass TIFF\xBB)",
1560
+ ` set fh to open for access (POSIX file ${JSON.stringify(tiffPath)}) with write permission`,
1561
+ " set eof fh to 0",
1562
+ " write theData to fh",
1563
+ " close access fh",
1564
+ ' return "TIFF"',
1565
+ " on error",
1566
+ ' return "NONE"',
1567
+ " end try",
1568
+ "end try"
1569
+ ].map((line) => ["-e", line]).flat();
1570
+ }
1571
+ async function captureDarwin(dir, pngPath) {
1572
+ const tiffPath = join(dir, "clip.tiff");
1573
+ const res = await execa("osascript", captureScriptArgs(pngPath, tiffPath), {
1574
+ reject: false
1575
+ });
1576
+ const kind = res.stdout.trim();
1577
+ if (kind === "PNG") return fileHasBytes(pngPath);
1578
+ if (kind === "TIFF" && await fileHasBytes(tiffPath)) {
1579
+ const conv = await execa(
1580
+ "sips",
1581
+ ["-s", "format", "png", tiffPath, "--out", pngPath],
1582
+ { reject: false }
1583
+ );
1584
+ if (conv.exitCode === 0) return fileHasBytes(pngPath);
1585
+ }
1586
+ return false;
1587
+ }
1588
+ async function linuxClipboardTool() {
1589
+ if (process.env["WAYLAND_DISPLAY"] && await hasCmd("wl-paste")) return "wayland";
1590
+ if (process.env["DISPLAY"] && await hasCmd("xclip")) return "x11";
1591
+ return null;
1592
+ }
1593
+ async function captureLinux(pngPath) {
1594
+ const tool = await linuxClipboardTool();
1595
+ if (!tool) return false;
1596
+ let buf = null;
1597
+ if (tool === "wayland") {
1598
+ const types = await execa("wl-paste", ["--list-types"], { reject: false });
1599
+ if (types.exitCode !== 0 || !/image\/png/i.test(types.stdout)) return false;
1600
+ const r = await execa("wl-paste", ["--type", "image/png"], {
1601
+ encoding: "buffer",
1602
+ reject: false
1603
+ });
1604
+ if (r.exitCode === 0) buf = asBuffer(r.stdout);
1605
+ } else {
1606
+ const sel = ["-selection", "clipboard"];
1607
+ const targets = await execa("xclip", [...sel, "-t", "TARGETS", "-o"], {
1608
+ reject: false
1609
+ });
1610
+ if (targets.exitCode !== 0 || !/image\/png/i.test(targets.stdout)) return false;
1611
+ const r = await execa("xclip", [...sel, "-t", "image/png", "-o"], {
1612
+ encoding: "buffer",
1613
+ reject: false
1614
+ });
1615
+ if (r.exitCode === 0) buf = asBuffer(r.stdout);
1616
+ }
1617
+ if (!buf || !isPng(buf)) return false;
1618
+ await writeFile(pngPath, buf);
1619
+ return true;
1620
+ }
1621
+ async function hasCmd(cmd) {
1622
+ const r = await execa("sh", ["-c", `command -v ${cmd}`], { reject: false });
1623
+ return r.exitCode === 0;
1624
+ }
1625
+ function asBuffer(out) {
1626
+ if (Buffer.isBuffer(out)) return out;
1627
+ if (out instanceof Uint8Array) return Buffer.from(out);
1628
+ return null;
1629
+ }
1630
+ function isPng(buf) {
1631
+ return buf.length >= 8 && buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71;
1632
+ }
1633
+ async function fileHasBytes(path) {
1634
+ try {
1635
+ const s = await stat(path);
1636
+ return s.isFile() && s.size > 0;
1637
+ } catch {
1638
+ return false;
1639
+ }
1640
+ }
1641
+
1642
+ // src/lib/paste-image.ts
1643
+ function loadClipboardScript(boxPngPath) {
1644
+ return [
1645
+ "pgrep -x Xvnc >/dev/null 2>&1 || /usr/local/bin/agentbox-vnc-start >/dev/null 2>&1 || true",
1646
+ "for _ in $(seq 1 30); do [ -S /tmp/.X11-unix/X1 ] && break; sleep 0.2; done",
1647
+ `setsid sh -c 'DISPLAY=:1 xclip -selection clipboard -t image/png -i ${boxPngPath}' </dev/null >/dev/null 2>&1 &`
1648
+ ].join("; ");
1649
+ }
1650
+ async function pasteHostClipboardImage(provider, box) {
1651
+ if (typeof provider.uploadPath !== "function") return "error";
1652
+ const hostPng = await captureClipboardImage();
1653
+ if (!hostPng) return "no-image";
1654
+ const boxPng = `/tmp/agentbox-clip-${String(Date.now())}.png`;
1655
+ try {
1656
+ await provider.uploadPath(box, hostPng, boxPng);
1657
+ await provider.exec(box, ["sh", "-lc", loadClipboardScript(boxPng)], {
1658
+ user: "vscode"
1659
+ });
1660
+ return "pasted";
1661
+ } catch {
1662
+ return "error";
1663
+ } finally {
1664
+ await rm2(dirname(hostPng), { recursive: true, force: true }).catch(() => {
1665
+ });
1666
+ }
1667
+ }
1668
+
1120
1669
  // src/commands/_cloud-attach.ts
1121
1670
  var RELAY_HOST_URL = `http://127.0.0.1:${String(DEFAULT_RELAY_PORT)}`;
1671
+ var RECONNECT_TIMEOUT_MS = 5 * 6e4;
1672
+ function abortableSleep(ms, signal) {
1673
+ return new Promise((resolve) => {
1674
+ if (signal.aborted) {
1675
+ resolve();
1676
+ return;
1677
+ }
1678
+ const t = setTimeout(resolve, ms);
1679
+ signal.addEventListener(
1680
+ "abort",
1681
+ () => {
1682
+ clearTimeout(t);
1683
+ resolve();
1684
+ },
1685
+ { once: true }
1686
+ );
1687
+ });
1688
+ }
1122
1689
  function buildCloudAttachInnerCommand(binary, extraArgs) {
1123
1690
  if (!extraArgs || extraArgs.length === 0) {
1124
1691
  return `bash -lc exec\\ ${binary}`;
1125
1692
  }
1126
1693
  const blob = Buffer.from(extraArgs.join("\n"), "utf8").toString("base64");
1127
- return `bash -lc 'mapfile -t A < <(echo ${blob} | base64 -d); exec ${binary} "\${A[@]}"'`;
1694
+ return `bash -lc 'mapfile -t A <<< "$(echo ${blob} | base64 -d)"; exec ${binary} "\${A[@]}"'`;
1128
1695
  }
1129
1696
  async function cloudAgentAttach(args) {
1130
1697
  const provider = await providerForBox(args.box);
1131
1698
  if (!provider.buildAttach) {
1132
1699
  throw new Error(`provider '${provider.name}' does not support interactive attach`);
1133
1700
  }
1701
+ const buildAttach = provider.buildAttach.bind(provider);
1702
+ let box = args.box;
1703
+ const state = await provider.probeState(box);
1704
+ if (state === "missing") {
1705
+ throw new Error(`cloud sandbox for ${box.name} is missing; was it destroyed?`);
1706
+ }
1707
+ if (state !== "running") {
1708
+ const s = spinner();
1709
+ s.start(state === "paused" ? "resuming box" : "starting box");
1710
+ box = await provider.start(box);
1711
+ s.stop("box running");
1712
+ }
1134
1713
  const command = buildCloudAttachInnerCommand(args.binary, args.extraArgs);
1135
- const safeOpenIn = args.box.provider === "daytona" ? "same" : args.openIn;
1714
+ const safeOpenIn = box.provider === "daytona" ? "same" : args.openIn;
1136
1715
  if (safeOpenIn && safeOpenIn !== "same" && args.extraArgs && args.extraArgs.length > 0) {
1137
- const pre = await provider.buildAttach(args.box, "agent", {
1716
+ const pre = await provider.buildAttach(box, "agent", {
1138
1717
  sessionName: args.sessionName,
1139
1718
  command,
1140
1719
  detached: true
1141
1720
  });
1142
1721
  try {
1143
- await runDetached(pre.argv);
1722
+ await runDetached(pre.argv, pre.env);
1144
1723
  } finally {
1145
1724
  if (pre.cleanup) await pre.cleanup();
1146
1725
  }
1147
1726
  }
1148
- const spec = await provider.buildAttach(args.box, "agent", {
1727
+ let spec = await provider.buildAttach(box, "agent", {
1149
1728
  sessionName: args.sessionName,
1150
1729
  command
1151
1730
  });
1731
+ const canPaste = args.mode === "claude" && await clipboardCaptureAvailable();
1732
+ const reconnect = async (signal) => {
1733
+ const deadline = Date.now() + RECONNECT_TIMEOUT_MS;
1734
+ let backoff = 500;
1735
+ for (; ; ) {
1736
+ if (signal.aborted || Date.now() > deadline) return null;
1737
+ try {
1738
+ box = await provider.start(box);
1739
+ break;
1740
+ } catch {
1741
+ await abortableSleep(backoff, signal);
1742
+ backoff = Math.min(backoff * 2, 5e3);
1743
+ }
1744
+ }
1745
+ if (signal.aborted) return null;
1746
+ const prev = spec;
1747
+ spec = await buildAttach(box, "agent", { sessionName: args.sessionName, command });
1748
+ if (prev.cleanup) {
1749
+ try {
1750
+ await prev.cleanup();
1751
+ } catch {
1752
+ }
1753
+ }
1754
+ return { command: spec.argv[0], argv: spec.argv.slice(1), env: spec.env };
1755
+ };
1152
1756
  try {
1153
1757
  const code = await runWrappedAttach({
1154
- container: args.box.name,
1758
+ container: box.name,
1155
1759
  command: spec.argv[0],
1156
1760
  dockerArgv: spec.argv.slice(1),
1761
+ env: spec.env,
1157
1762
  relayBaseUrl: RELAY_HOST_URL,
1158
- boxId: args.box.id,
1159
- boxName: args.box.name,
1160
- projectIndex: args.box.projectIndex,
1763
+ boxId: box.id,
1764
+ boxName: box.name,
1765
+ projectIndex: box.projectIndex,
1161
1766
  mode: args.mode,
1162
1767
  detachable: true,
1163
- openIn: safeOpenIn
1768
+ openIn: safeOpenIn,
1769
+ reconnect,
1770
+ onError: (msg) => {
1771
+ try {
1772
+ appendFileSync(
1773
+ join2(homedir(), ".agentbox", "logs", "attach.log"),
1774
+ `${(/* @__PURE__ */ new Date()).toISOString()} [${box.name}] ${msg}
1775
+ `
1776
+ );
1777
+ } catch {
1778
+ }
1779
+ },
1780
+ onPasteImage: canPaste ? () => pasteHostClipboardImage(provider, box) : void 0
1164
1781
  });
1165
1782
  process.exit(code);
1166
1783
  } finally {
1167
1784
  if (spec.cleanup) await spec.cleanup();
1168
1785
  }
1169
1786
  }
1170
- function runDetached(argv) {
1787
+ function runDetached(argv, env) {
1171
1788
  return new Promise((resolve) => {
1172
- const child = spawn3(argv[0], argv.slice(1), { stdio: "ignore" });
1789
+ const child = spawn3(argv[0], argv.slice(1), {
1790
+ stdio: "ignore",
1791
+ env: env ? { ...process.env, ...env } : process.env
1792
+ });
1173
1793
  child.on("error", () => resolve());
1174
1794
  child.on("exit", () => resolve());
1175
1795
  });
@@ -1182,8 +1802,12 @@ export {
1182
1802
  providerForCreate,
1183
1803
  loadPtyBackend,
1184
1804
  detectHostTerminal,
1805
+ setTerminalTitle,
1806
+ pushTerminalTitle,
1807
+ popTerminalTitle,
1185
1808
  NEW_BOX_ID,
1186
1809
  NEW_BOX_LABEL,
1810
+ stripTitleGlyph,
1187
1811
  sidebarLines,
1188
1812
  menuLines,
1189
1813
  lifecycleMenuLines,
@@ -1191,10 +1815,14 @@ export {
1191
1815
  ADVANCED_HINT_GROUPS,
1192
1816
  statusLine,
1193
1817
  renderFooter,
1818
+ ALERT_BAND_ROWS,
1819
+ renderAlertBand,
1194
1820
  subscribePrompts,
1195
1821
  postAnswer,
1196
1822
  runWrappedAttach,
1823
+ clipboardCaptureAvailable,
1824
+ pasteHostClipboardImage,
1197
1825
  buildCloudAttachInnerCommand,
1198
1826
  cloudAgentAttach
1199
1827
  };
1200
- //# sourceMappingURL=chunk-FODMEHD3.js.map
1828
+ //# sourceMappingURL=chunk-R4O5WPHW.js.map