very-happy-cli 0.2.100 → 0.2.102

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 (33) hide show
  1. package/dist/{AcpBackend-CGnE_dbe.mjs → AcpBackend-C1nF06ZR.mjs} +1 -1
  2. package/dist/{AcpBackend-oDDlCB78.cjs → AcpBackend-CpKkdwIL.cjs} +1 -1
  3. package/dist/{AcpSessionManager-XGnQqnaJ.mjs → AcpSessionManager-BDad8lK3.mjs} +1 -1
  4. package/dist/{AcpSessionManager-BTXK7Wn2.cjs → AcpSessionManager-DjWIwGTR.cjs} +1 -1
  5. package/dist/{config-bUqA35dk.mjs → config-BlTfR0KA.mjs} +2 -2
  6. package/dist/{config-CYGeJRgN.cjs → config-DSF29tTS.cjs} +2 -2
  7. package/dist/{index-BXhSRb9N.cjs → index-C8HuYyZq.cjs} +2 -2
  8. package/dist/{index-CPUJfdIJ.mjs → index-CF3rNFlZ.mjs} +13 -13
  9. package/dist/{index-j_bZV63O.mjs → index-CPC-a5lH.mjs} +5 -5
  10. package/dist/{index-BFQ_XDUX.cjs → index-D_2eCeep.cjs} +17 -17
  11. package/dist/{index-DlpWmQ1I.mjs → index-DjYlt3fw.mjs} +2 -2
  12. package/dist/{index-olwBiKj6.cjs → index-yLIv8JPD.cjs} +4 -4
  13. package/dist/index.cjs +2 -2
  14. package/dist/index.mjs +2 -2
  15. package/dist/{installTerminalHooks-ZNRl9Qcl.cjs → installTerminalHooks-BVMhXRZO.cjs} +2 -2
  16. package/dist/{installTerminalHooks-DmC0SrNo.mjs → installTerminalHooks-DAR3YEo3.mjs} +2 -2
  17. package/dist/lib.cjs +1 -1
  18. package/dist/lib.d.cts +6 -0
  19. package/dist/lib.d.mts +6 -0
  20. package/dist/lib.mjs +1 -1
  21. package/dist/{mcp-CryukOBj.mjs → mcp--I-Uz_wH.mjs} +2 -2
  22. package/dist/{mcp-7xQMC2Az.cjs → mcp-LCZtdhF0.cjs} +2 -2
  23. package/dist/{runGemini-12hI-_Ng.cjs → runGemini-Crna_6uM.cjs} +4 -4
  24. package/dist/{runGemini-BbTfrych.mjs → runGemini-cLR7rfj6.mjs} +4 -4
  25. package/dist/{runOpenClaw-Cr8boJJ9.mjs → runOpenClaw-C1u9xEH7.mjs} +3 -3
  26. package/dist/{runOpenClaw-CYrNNJ7P.cjs → runOpenClaw-CWjOCdWg.cjs} +3 -3
  27. package/dist/{send-DMuE8f-q.mjs → send-BtQ9qf_z.mjs} +2 -2
  28. package/dist/{send-DQjba9jZ.cjs → send-CufsYMXq.cjs} +2 -2
  29. package/dist/{spawn-d7F98wtf.cjs → spawn-DPllkoUN.cjs} +2 -2
  30. package/dist/{spawn-F6qqH4Vw.mjs → spawn-rdb0uM9R.mjs} +2 -2
  31. package/dist/{types-DTVA1Hp-.cjs → types-7-mfp-j5.cjs} +108 -42
  32. package/dist/{types-Dt0OQ0gL.mjs → types-B3Dnzv_q.mjs} +107 -41
  33. package/package.json +7 -7
@@ -51,7 +51,7 @@ var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
51
51
  var pty__namespace = /*#__PURE__*/_interopNamespaceDefault(pty);
52
52
 
53
53
  var name = "very-happy-cli";
54
- var version = "0.2.100";
54
+ var version = "0.2.102";
55
55
  var description = "Remote multi-agent workspace CLI with deep Claude integration, native terminal streaming, and self-hosting.";
56
56
  var author = "Kirill Dubovitskiy";
57
57
  var contributors = [
@@ -164,12 +164,12 @@ var dependencies = {
164
164
  zod: "^4.0.0"
165
165
  };
166
166
  var optionalDependencies = {
167
- "very-happy-tools-arm64-darwin": "0.2.100",
168
- "very-happy-tools-x64-darwin": "0.2.100",
169
- "very-happy-tools-arm64-linux": "0.2.100",
170
- "very-happy-tools-x64-linux": "0.2.100",
171
- "very-happy-tools-arm64-win32": "0.2.100",
172
- "very-happy-tools-x64-win32": "0.2.100"
167
+ "very-happy-tools-arm64-darwin": "0.2.102",
168
+ "very-happy-tools-x64-darwin": "0.2.102",
169
+ "very-happy-tools-arm64-linux": "0.2.102",
170
+ "very-happy-tools-x64-linux": "0.2.102",
171
+ "very-happy-tools-arm64-win32": "0.2.102",
172
+ "very-happy-tools-x64-win32": "0.2.102"
173
173
  };
174
174
  var devDependencies = {
175
175
  "@slopus/happy-wire": "workspace:*",
@@ -1585,7 +1585,7 @@ class RpcHandlerManager {
1585
1585
  }
1586
1586
  }
1587
1587
 
1588
- const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-DTVA1Hp-.cjs', document.baseURI).href))));
1588
+ const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-7-mfp-j5.cjs', document.baseURI).href))));
1589
1589
  function projectPath() {
1590
1590
  const path = path$1.resolve(__dirname$1, "..");
1591
1591
  return path;
@@ -4809,6 +4809,10 @@ function registerTodoHandlers(manager) {
4809
4809
  });
4810
4810
  }
4811
4811
 
4812
+ function sanitizeGeometry(cols, rows) {
4813
+ const ok = (v) => typeof v === "number" && Number.isInteger(v) && v >= 2 && v <= 1e4;
4814
+ return ok(cols) && ok(rows) ? { cols, rows } : void 0;
4815
+ }
4812
4816
  function sanitizeTagList(value) {
4813
4817
  if (!Array.isArray(value)) return void 0;
4814
4818
  const out = [];
@@ -4828,7 +4832,7 @@ function sanitizeLiveSnapshot(raw, now, ttlMs = LIVE_SNAPSHOT_TTL_MS, max = LIVE
4828
4832
  const rows = [];
4829
4833
  for (const [id, value] of Object.entries(raw)) {
4830
4834
  if (!id || !value || typeof value !== "object") continue;
4831
- const { title, cwd, seenAt, tags, manual, attachTmux } = value;
4835
+ const { title, cwd, seenAt, tags, manual, attachTmux, cols: rawCols, rows: rawRows } = value;
4832
4836
  if (typeof seenAt !== "number" || !Number.isFinite(seenAt)) continue;
4833
4837
  if (now - seenAt >= ttlMs) continue;
4834
4838
  const tagList = sanitizeTagList(tags);
@@ -4838,6 +4842,7 @@ function sanitizeLiveSnapshot(raw, now, ttlMs = LIVE_SNAPSHOT_TTL_MS, max = LIVE
4838
4842
  ...tagList !== void 0 ? { tags: tagList } : {},
4839
4843
  ...manual === true ? { manual: true } : {},
4840
4844
  ...typeof attachTmux === "string" && attachTmux ? { attachTmux } : {},
4845
+ ...sanitizeGeometry(rawCols, rawRows) ?? {},
4841
4846
  seenAt
4842
4847
  }]);
4843
4848
  }
@@ -4860,6 +4865,7 @@ function liveSnapshotChanged(prev, next) {
4860
4865
  if (!!before.manual !== !!info.manual) return true;
4861
4866
  if ((before.attachTmux ?? "") !== (info.attachTmux ?? "")) return true;
4862
4867
  if ((before.tags ?? []).join("\0") !== (info.tags ?? []).join("\0")) return true;
4868
+ if (before.cols !== info.cols || before.rows !== info.rows) return true;
4863
4869
  }
4864
4870
  return false;
4865
4871
  }
@@ -4949,6 +4955,9 @@ function attachStartupCommand(sessionId, socket) {
4949
4955
  }
4950
4956
 
4951
4957
  const TERMINAL_ID_RE$1 = /^[a-zA-Z0-9_-]{1,64}$/;
4958
+ function recordGeometry(record) {
4959
+ return record.cols !== void 0 && record.rows !== void 0 ? { cols: record.cols, rows: record.rows } : {};
4960
+ }
4952
4961
  function planTerminalRestore(record, facts) {
4953
4962
  if (facts.tmuxAlive) return { kind: "already-live" };
4954
4963
  if (record.attachTmux) {
@@ -4964,7 +4973,8 @@ function planTerminalRestore(record, facts) {
4964
4973
  manual: true,
4965
4974
  tags: record.tags,
4966
4975
  command: attachStartupCommand(matches[0].id, facts.attachSocket),
4967
- attachTmux: record.attachTmux
4976
+ attachTmux: record.attachTmux,
4977
+ ...recordGeometry(record)
4968
4978
  };
4969
4979
  }
4970
4980
  if (!record.cwd || !facts.cwdExists(record.cwd)) return { kind: "error", reason: "missing-cwd" };
@@ -4976,7 +4986,8 @@ function planTerminalRestore(record, facts) {
4976
4986
  title: record.title,
4977
4987
  manual: record.manual === true,
4978
4988
  tags: record.tags,
4979
- ...resumable ? { command: `claude --resume ${record.claudeSessionId}` } : {}
4989
+ ...resumable ? { command: `claude --resume ${record.claudeSessionId}` } : {},
4990
+ ...recordGeometry(record)
4980
4991
  };
4981
4992
  }
4982
4993
 
@@ -9537,6 +9548,7 @@ const CAPTURE_FULL_BUDGET_BYTES = 1024 * 1024;
9537
9548
  const ALT_ENTER = Buffer.from("\x1B[?1049h", "ascii");
9538
9549
  const CURSOR_HOME = Buffer.from("\x1B[H", "ascii");
9539
9550
  const CRLF = Buffer.from("\r\n", "ascii");
9551
+ const SGR_RESET = Buffer.from("\x1B[0m", "ascii");
9540
9552
  function normalizeCaptureLines(blob) {
9541
9553
  if (blob.length === 0) return blob;
9542
9554
  const lines = [];
@@ -9573,32 +9585,38 @@ function assembleRestore(responses, paneState, budget = CAPTURE_FULL_BUDGET_BYTE
9573
9585
  const screen = () => {
9574
9586
  const raw = get("visible");
9575
9587
  const lines = splitLines(raw);
9588
+ let realCount = lines.length;
9576
9589
  if (rows !== void 0 && rows > 0) {
9577
9590
  while (lines.length > rows) lines.shift();
9591
+ realCount = lines.length;
9578
9592
  while (lines.length < rows) lines.push(EMPTY);
9579
9593
  }
9580
- const body = joinCrlf(lines);
9594
+ const realBody = joinCrlf(lines.slice(0, realCount));
9595
+ const padCount = lines.length - realCount;
9596
+ const padBody = padCount > 0 ? Buffer.concat([realCount > 0 ? CRLF : EMPTY, SGR_RESET, joinCrlf(new Array(padCount).fill(EMPTY))]) : EMPTY;
9581
9597
  const cursor = paneState ? Buffer.from(`\x1B[${paneState.cursorY + 1};${paneState.cursorX + 1}H`, "ascii") : Buffer.alloc(0);
9582
- return Buffer.concat([body, tail, cursor]);
9598
+ return Buffer.concat([realBody, padBody, SGR_RESET, tail, cursor]);
9583
9599
  };
9600
+ const wrapFull = (b) => Buffer.concat([SGR_RESET, b, SGR_RESET]);
9601
+ const wrapSmall = (b) => Buffer.concat([b, SGR_RESET]);
9584
9602
  if (!alternateOn) {
9585
9603
  const history2 = trimTrailingBlankRows(normalizeCaptureLines(get("history")));
9586
9604
  const smallHistory = trimTrailingBlankRows(normalizeCaptureLines(get("smallHistory")));
9587
9605
  const withHistory = (h) => Buffer.concat(
9588
- h.length > 0 ? [truncateToBudget(h, budget), CRLF, screen()] : [screen()]
9606
+ h.length > 0 ? [truncateToBudget(h, budget), SGR_RESET, CRLF, screen()] : [screen()]
9589
9607
  );
9590
- return { full: withHistory(history2), small: withHistory(smallHistory), alternateOn };
9608
+ return { full: wrapFull(withHistory(history2)), small: wrapSmall(withHistory(smallHistory)), alternateOn };
9591
9609
  }
9592
9610
  const history = trimTrailingBlankRows(normalizeCaptureLines(get("history")));
9593
9611
  const saved = trimTrailingBlankRows(normalizeCaptureLines(get("altSaved")));
9594
9612
  const scrollback = truncateToBudget(
9595
- Buffer.concat(history.length > 0 && saved.length > 0 ? [history, CRLF, saved] : [history, saved]),
9613
+ Buffer.concat(history.length > 0 && saved.length > 0 ? [history, SGR_RESET, CRLF, saved] : [history, saved]),
9596
9614
  budget
9597
9615
  );
9598
- const altScreen = Buffer.concat([ALT_ENTER, CURSOR_HOME, screen()]);
9616
+ const altScreen = Buffer.concat([ALT_ENTER, CURSOR_HOME, SGR_RESET, screen()]);
9599
9617
  return {
9600
- full: Buffer.concat([scrollback, scrollback.length > 0 ? CRLF : Buffer.alloc(0), altScreen]),
9601
- small: altScreen,
9618
+ full: wrapFull(Buffer.concat([scrollback, scrollback.length > 0 ? CRLF : Buffer.alloc(0), altScreen])),
9619
+ small: wrapSmall(altScreen),
9602
9620
  alternateOn
9603
9621
  };
9604
9622
  }
@@ -10096,6 +10114,7 @@ function selectAutoRestore(candidates, opts) {
10096
10114
  terminalId: c.id,
10097
10115
  ...c.tags !== void 0 ? { tags: c.tags } : {},
10098
10116
  ...c.manual ? { manual: true } : {},
10117
+ ...c.cols !== void 0 && c.rows !== void 0 ? { cols: c.cols, rows: c.rows } : {},
10099
10118
  cwd: c.cwd,
10100
10119
  title: c.title,
10101
10120
  claudeSessionId: c.claudeSessionId,
@@ -10283,7 +10302,10 @@ function startupInjectionArgs(tmuxSession, command) {
10283
10302
  ];
10284
10303
  }
10285
10304
  function seenInfoOf(t) {
10286
- return { title: t.title, cwd: t.cwd, tags: t.tags, manual: t.manual, attachTmux: t.attachTmux };
10305
+ return { title: t.title, cwd: t.cwd, tags: t.tags, manual: t.manual, attachTmux: t.attachTmux, ...seenGeometry(t.paneCols, t.paneRows) };
10306
+ }
10307
+ function seenGeometry(cols, rows) {
10308
+ return cols !== void 0 && rows !== void 0 ? { cols, rows } : {};
10287
10309
  }
10288
10310
  function terminalListSignature(items) {
10289
10311
  const canon = [...items].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).map((t) => [
@@ -10328,6 +10350,12 @@ const LIST_SESSIONS_FORMAT = [
10328
10350
  // B-273: the user tmux session this terminal was opened to attach (name),
10329
10351
  // carried into close records so a restore can re-attach.
10330
10352
  "#{@vh_attach}",
10353
+ // B-287: the pane's REAL geometry, persisted with the live snapshot so a
10354
+ // cold restore (daemon boot auto-restore, archive ↻) recreates the session
10355
+ // at the size it last had — claude's one-shot welcome banner then never
10356
+ // gets reflowed by the first web open (the "half-drawn logo").
10357
+ "#{pane_width}",
10358
+ "#{pane_height}",
10331
10359
  // B-121: the control-mode client has no `pty.process`, so the agent-state
10332
10360
  // fast path lost its live `#{pane_current_command}` equivalent. Carry it in
10333
10361
  // the ONE list-sessions call the tracker already makes — the value goes
@@ -10342,9 +10370,10 @@ const LIST_SESSIONS_FORMAT = [
10342
10370
  function parseSessionListLine(line) {
10343
10371
  if (!line) return void 0;
10344
10372
  const parts = line.split(LIST_FIELD_SEP);
10345
- if (parts.length < 10) return void 0;
10346
- const [name, created, activity, cwd, vhTitle, manual, vhTags, vhAttach, paneCommand] = parts;
10373
+ if (parts.length < 12) return void 0;
10374
+ const [name, created, activity, cwd, vhTitle, manual, vhTags, vhAttach, paneW, paneH, paneCommand] = parts;
10347
10375
  if (!name) return void 0;
10376
+ const geom = parsePositiveInt(paneW) !== void 0 && parsePositiveInt(paneH) !== void 0 ? { paneCols: parsePositiveInt(paneW), paneRows: parsePositiveInt(paneH) } : {};
10348
10377
  return {
10349
10378
  name,
10350
10379
  created: created ? Number(created) * 1e3 : void 0,
@@ -10357,11 +10386,17 @@ function parseSessionListLine(line) {
10357
10386
  // Verbatim (no trim): tmux allows edge spaces in a session name and the
10358
10387
  // restore lookup / attach echo compare it exactly.
10359
10388
  attachTmux: isSafeTmuxSessionName(vhAttach) ? vhAttach : void 0,
10360
- // pane_title is last, so anything after field 9 is title content that
10389
+ ...geom,
10390
+ // pane_title is last, so anything after field 11 is title content that
10361
10391
  // contained the separator — rejoin it rather than dropping it.
10362
- paneTitle: parts.slice(9).join(LIST_FIELD_SEP) || void 0
10392
+ paneTitle: parts.slice(11).join(LIST_FIELD_SEP) || void 0
10363
10393
  };
10364
10394
  }
10395
+ function parsePositiveInt(raw) {
10396
+ if (raw === void 0 || !/^[0-9]{1,5}$/.test(raw.trim())) return void 0;
10397
+ const n = Number(raw.trim());
10398
+ return n > 0 ? n : void 0;
10399
+ }
10365
10400
  const TERMINAL_TAG_MAX_COUNT = 64;
10366
10401
  const TERMINAL_TAG_MAX_LENGTH = 24;
10367
10402
  const TERMINAL_TAGS_MAX_BYTES = 4096;
@@ -10504,6 +10539,11 @@ const VH_SESSION_OPTION_OVERRIDES = [
10504
10539
  ["window", "window-size", "latest"],
10505
10540
  ["window", "pane-border-status", "off"]
10506
10541
  ];
10542
+ const COLD_CREATE_DEFAULT_GEOMETRY = { cols: 120, rows: 30 };
10543
+ function coldCreateGeometry(plan) {
10544
+ const ok = (v) => typeof v === "number" && Number.isInteger(v) && v >= 2 && v <= 1e4;
10545
+ return ok(plan.cols) && ok(plan.rows) ? { cols: plan.cols, rows: plan.rows } : { ...COLD_CREATE_DEFAULT_GEOMETRY };
10546
+ }
10507
10547
  function tmuxNewSessionArgs(session, cols, rows, cwd, envFlags, shell) {
10508
10548
  const args = ["new-session", "-d", ...envFlags, "-s", session, "-x", String(cols), "-y", String(rows), "-c", cwd, shell];
10509
10549
  for (const [scope, option, value] of VH_SESSION_OPTION_OVERRIDES) {
@@ -10525,13 +10565,17 @@ function ptyEnv() {
10525
10565
  env.PATH = `${local}:${env.PATH || ""}`;
10526
10566
  }
10527
10567
  env.TERM = "xterm-256color";
10528
- const isUtf8 = (v) => !!v && /utf-?8/i.test(v);
10529
- if (!isUtf8(env.LC_ALL) && !isUtf8(env.LANG) && !isUtf8(env.LC_CTYPE)) {
10530
- env.LANG = "en_US.UTF-8";
10531
- env.LC_CTYPE = "en_US.UTF-8";
10532
- }
10568
+ Object.assign(env, utf8LocaleEnv(env, process.platform));
10533
10569
  return env;
10534
10570
  }
10571
+ function utf8LocaleEnv(cur, platform) {
10572
+ const isUtf8 = (v) => !!v && /utf-?8/i.test(v);
10573
+ if (isUtf8(cur.LC_ALL) || isUtf8(cur.LANG) || isUtf8(cur.LC_CTYPE)) return {};
10574
+ const locale = platform === "darwin" ? "en_US.UTF-8" : "C.UTF-8";
10575
+ const patch = { LANG: locale, LC_CTYPE: locale };
10576
+ if (cur.LC_ALL) patch.LC_ALL = locale;
10577
+ return patch;
10578
+ }
10535
10579
  class TerminalSession {
10536
10580
  id;
10537
10581
  tmuxSession;
@@ -11050,6 +11094,7 @@ class WebTerminalManager {
11050
11094
  tags: info.tags,
11051
11095
  manual: info.manual,
11052
11096
  ...info.attachTmux ? { attachTmux: info.attachTmux } : {},
11097
+ ...seenGeometry(info.cols, info.rows),
11053
11098
  // Live binding first (authoritative while it exists), persisted
11054
11099
  // metadata as the fallback once the mirror has been torn down.
11055
11100
  mirrorSessionId: this.mirrorResolver?.(id) ?? mirror?.sessionId,
@@ -11103,6 +11148,7 @@ class WebTerminalManager {
11103
11148
  tags: info.tags,
11104
11149
  manual: info.manual,
11105
11150
  ...info.attachTmux ? { attachTmux: info.attachTmux } : {},
11151
+ ...seenGeometry(info.cols, info.rows),
11106
11152
  mirrorSessionId: mirror?.sessionId,
11107
11153
  claudeSessionId: mirror?.claudeSessionId,
11108
11154
  reason: "daemon-gap",
@@ -11115,6 +11161,7 @@ class WebTerminalManager {
11115
11161
  cwd: info.cwd,
11116
11162
  tags: info.tags,
11117
11163
  manual: info.manual,
11164
+ ...seenGeometry(info.cols, info.rows),
11118
11165
  seenAt: info.seenAt,
11119
11166
  claudeSessionId: mirror?.claudeSessionId
11120
11167
  });
@@ -11256,9 +11303,10 @@ class WebTerminalManager {
11256
11303
  "-e",
11257
11304
  `VH_HAPPY_HOME_DIR=${configuration.happyHomeDir}`
11258
11305
  ] : [];
11306
+ const { cols, rows } = coldCreateGeometry(plan);
11259
11307
  const created = node_child_process.spawnSync(
11260
11308
  "tmux",
11261
- tmuxArgs(tmuxNewSessionArgs(name, 120, 30, plan.cwd, envFlags, defaultShell())),
11309
+ tmuxArgs(tmuxNewSessionArgs(name, cols, rows, plan.cwd, envFlags, defaultShell())),
11262
11310
  { stdio: "ignore", timeout: TMUX_CREATE_TIMEOUT_MS, env }
11263
11311
  );
11264
11312
  const live = created.status === 0 && node_child_process.spawnSync(
@@ -11311,7 +11359,7 @@ class WebTerminalManager {
11311
11359
  * would care about changed (see liveSnapshotChanged). */
11312
11360
  persistLiveSnapshot(live, now) {
11313
11361
  const candidate = /* @__PURE__ */ new Map();
11314
- for (const [id, info] of live) candidate.set(id, { title: info.title, cwd: info.cwd, tags: info.tags, manual: info.manual, ...info.attachTmux ? { attachTmux: info.attachTmux } : {}, seenAt: now });
11362
+ for (const [id, info] of live) candidate.set(id, { title: info.title, cwd: info.cwd, tags: info.tags, manual: info.manual, ...info.attachTmux ? { attachTmux: info.attachTmux } : {}, ...seenGeometry(info.cols, info.rows), seenAt: now });
11315
11363
  if (!liveSnapshotChanged(this.lastPersistedSnapshot, candidate)) return;
11316
11364
  this.lastPersistedSnapshot = candidate;
11317
11365
  saveLiveSnapshot(candidate);
@@ -11422,7 +11470,11 @@ class WebTerminalManager {
11422
11470
  seq: existing.seq,
11423
11471
  mode: "replay",
11424
11472
  chunks,
11425
- ...lines ? { streamMode: "lines" } : {},
11473
+ // B-287: a replay carries no restore, so without this the
11474
+ // adopting client had no authoritative width and fell back
11475
+ // to its own guess — the door through which a stale device
11476
+ // width leaked in. Announced = what we last told clients.
11477
+ ...lines ? { streamMode: "lines", paneCols: existing.announcedCols ?? existing.cols, paneRows: existing.announcedRows ?? existing.rows } : {},
11426
11478
  ...echo2
11427
11479
  };
11428
11480
  }
@@ -11504,6 +11556,13 @@ class WebTerminalManager {
11504
11556
  // the web xterm also sees the OSC, which is inert there.
11505
11557
  ["set-option", "-t", `=${tmuxSession}:`, "set-titles", "on"],
11506
11558
  ["set-option", "-t", `=${tmuxSession}:`, "set-titles-string", "#{pane_title}"],
11559
+ // B-287: re-assert on EVERY open, not just at create. A session
11560
+ // made before the create-time override (pre-0.2.96) or under a
11561
+ // user `window-size manual`/`smallest` silently refuses
11562
+ // `refresh-client -C`, so the pane freezes at whatever width the
11563
+ // first viewer set and no later device can widen it. `-w -q` so
11564
+ // an older tmux without the option does not fail the open.
11565
+ ["set-option", "-w", "-q", "-t", `=${tmuxSession}:`, "window-size", "latest"],
11507
11566
  ["set-option", "-g", "set-clipboard", "on"],
11508
11567
  ["set-option", "-ga", "terminal-features", ",xterm-256color:clipboard"]
11509
11568
  ];
@@ -11899,13 +11958,12 @@ class WebTerminalManager {
11899
11958
  this.applyResize(session, cols, rows);
11900
11959
  }
11901
11960
  /** Resize the transport AND the authoritative headless screen together, so a
11902
- * later snapshot matches the real geometry. Multiple tabs subscribed to one
11903
- * terminal all drive the same tmux window we simply take the LAST resize
11904
- * (tmux is single-size anyway); there's no per-subscriber geometry to
11905
- * reconcile. For a control client the declaration IS `refresh-client -C`:
11906
- * a client that never sends it doesn't participate in the window size at
11907
- * all (which is the door the spec leaves open for "phone mirrors without
11908
- * squeezing the desktop" — not implemented in this batch). */
11961
+ * later snapshot matches the real geometry. One tmux window = one size:
11962
+ * we take the LAST resize we are handed (B-287: the web only sends one on a
11963
+ * human-activity edge, so "last resize" == "most recently used viewer" ==
11964
+ * tmux's own `window-size latest` semantics). The control-client
11965
+ * declaration IS `refresh-client -C`, preceded by a `window-size latest`
11966
+ * re-assert so a manual/smallest session cannot swallow it. */
11909
11967
  applyResize(session, cols, rows) {
11910
11968
  const c = Math.max(2, Math.floor(cols));
11911
11969
  const r = Math.max(2, Math.floor(rows));
@@ -11916,7 +11974,10 @@ class WebTerminalManager {
11916
11974
  logger.debug(`[WEB TERMINAL] resize ${session.id} failed: ${e}`);
11917
11975
  }
11918
11976
  } else if (c !== session.cols || r !== session.rows) {
11919
- session.transport.client.sendFireAndForget([`refresh-client -C ${c}x${r}`]);
11977
+ session.transport.client.sendFireAndForget([
11978
+ `set-option -w -q -t =${session.tmuxSession}: window-size latest`,
11979
+ `refresh-client -C ${c}x${r}`
11980
+ ]);
11920
11981
  }
11921
11982
  session.resizeHeadless(c, r);
11922
11983
  }
@@ -11959,7 +12020,10 @@ class WebTerminalManager {
11959
12020
  * session (so a local `tmux attach` won't find it either). Used when the
11960
12021
  * user deletes the terminal from the sidebar. */
11961
12022
  killSession(terminalId, opts) {
11962
- const info = this.lastSeenInfo.get(terminalId) ?? this.listSessions().find((t) => t.id === terminalId);
12023
+ const info = this.lastSeenInfo.get(terminalId) ?? (() => {
12024
+ const t = this.listSessions().find((x) => x.id === terminalId);
12025
+ return t ? seenInfoOf(t) : void 0;
12026
+ })();
11963
12027
  const target = `=vh-${terminalId}:`;
11964
12028
  const killed = node_child_process.spawnSync("tmux", tmuxArgs(["kill-session", "-t", target]), { stdio: "ignore", env: ptyEnv() });
11965
12029
  const verified = node_child_process.spawnSync("tmux", tmuxArgs(["has-session", "-t", target]), { stdio: "ignore", env: ptyEnv() });
@@ -11985,6 +12049,7 @@ class WebTerminalManager {
11985
12049
  tags: info.tags,
11986
12050
  manual: info.manual,
11987
12051
  ...info.attachTmux ? { attachTmux: info.attachTmux } : {},
12052
+ ...seenGeometry(info.cols, info.rows),
11988
12053
  mirrorSessionId: this.mirrorResolver?.(terminalId) ?? mirror?.sessionId,
11989
12054
  claudeSessionId: mirror?.claudeSessionId,
11990
12055
  reason: "closed",
@@ -12074,7 +12139,8 @@ class WebTerminalManager {
12074
12139
  // the mirror reconciler's stricter "claude is really here" gate.
12075
12140
  claudeConfident: probe.claudeConfident,
12076
12141
  manual: s.manual,
12077
- ...s.attachTmux ? { attachTmux: s.attachTmux } : {}
12142
+ ...s.attachTmux ? { attachTmux: s.attachTmux } : {},
12143
+ ...s.paneCols !== void 0 && s.paneRows !== void 0 ? { paneCols: s.paneCols, paneRows: s.paneRows } : {}
12078
12144
  });
12079
12145
  }
12080
12146
  return out;