very-happy-cli 0.2.99 → 0.2.101

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-DkvV2nhl.cjs → AcpBackend-BHst4DAF.cjs} +1 -1
  2. package/dist/{AcpBackend-C9LtzYSx.mjs → AcpBackend-mbMlyZnj.mjs} +1 -1
  3. package/dist/{AcpSessionManager-B5rJcE82.mjs → AcpSessionManager-CwdvDD_z.mjs} +1 -1
  4. package/dist/{AcpSessionManager-gsp_-PGy.cjs → AcpSessionManager-Nyxm1kCR.cjs} +1 -1
  5. package/dist/{config-BcCVR-Wx.mjs → config-C1Z39_GK.mjs} +2 -2
  6. package/dist/{config-Bidi0Yeq.cjs → config-DjtLOWRc.cjs} +2 -2
  7. package/dist/{index-B87z2DOo.mjs → index-3ssSAknz.mjs} +241 -15
  8. package/dist/{index-Wfds4scC.mjs → index-BL9LWtAR.mjs} +5 -5
  9. package/dist/index-BnVk_E3V.mjs +51 -0
  10. package/dist/{index-Dp1EYEyF.cjs → index-DemE8OgZ.cjs} +245 -18
  11. package/dist/index-DqA3-Xxl.cjs +57 -0
  12. package/dist/{index-CxdGnaov.cjs → index-tQZwyC3N.cjs} +4 -4
  13. package/dist/index.cjs +2 -2
  14. package/dist/index.mjs +2 -2
  15. package/dist/{installTerminalHooks-jaCruGfR.cjs → installTerminalHooks-BC9thC5_.cjs} +2 -2
  16. package/dist/{installTerminalHooks-DkOh088o.mjs → installTerminalHooks-BvYqvibn.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-ZQbu8tL1.cjs → mcp-ChjpeHtZ.cjs} +2 -2
  22. package/dist/{mcp-DhftF_XE.mjs → mcp-D6qSHTLO.mjs} +2 -2
  23. package/dist/{runGemini-3-xWWBRA.mjs → runGemini-D1SFLnxb.mjs} +4 -4
  24. package/dist/{runGemini-Ce7Maf-D.cjs → runGemini-Dy7X7eDh.cjs} +4 -4
  25. package/dist/{runOpenClaw-DZhGhihh.mjs → runOpenClaw-AsjRz-Q3.mjs} +3 -3
  26. package/dist/{runOpenClaw-q3inEsDr.cjs → runOpenClaw-BpyEPSp5.cjs} +3 -3
  27. package/dist/{send-BdEJeVye.cjs → send-CvArl8UY.cjs} +2 -2
  28. package/dist/{send-B3NOqi9-.mjs → send-D1arw2mU.mjs} +2 -2
  29. package/dist/{spawn-DEVUh4iW.cjs → spawn-BYby4e76.cjs} +2 -2
  30. package/dist/{spawn-CCYAeBfN.mjs → spawn-D3k-ywKX.mjs} +2 -2
  31. package/dist/{types-DhxxJFD0.cjs → types-BEwKkfnX.cjs} +99 -37
  32. package/dist/{types-CrJj-daH.mjs → types-CkxTp9iI.mjs} +98 -36
  33. package/package.json +7 -7
@@ -29,7 +29,7 @@ import { SandboxManager } from '@anthropic-ai/sandbox-runtime';
29
29
  import { Expo } from 'expo-server-sdk';
30
30
 
31
31
  var name = "very-happy-cli";
32
- var version = "0.2.99";
32
+ var version = "0.2.101";
33
33
  var description = "Remote multi-agent workspace CLI with deep Claude integration, native terminal streaming, and self-hosting.";
34
34
  var author = "Kirill Dubovitskiy";
35
35
  var contributors = [
@@ -142,12 +142,12 @@ var dependencies = {
142
142
  zod: "^4.0.0"
143
143
  };
144
144
  var optionalDependencies = {
145
- "very-happy-tools-arm64-darwin": "0.2.99",
146
- "very-happy-tools-x64-darwin": "0.2.99",
147
- "very-happy-tools-arm64-linux": "0.2.99",
148
- "very-happy-tools-x64-linux": "0.2.99",
149
- "very-happy-tools-arm64-win32": "0.2.99",
150
- "very-happy-tools-x64-win32": "0.2.99"
145
+ "very-happy-tools-arm64-darwin": "0.2.101",
146
+ "very-happy-tools-x64-darwin": "0.2.101",
147
+ "very-happy-tools-arm64-linux": "0.2.101",
148
+ "very-happy-tools-x64-linux": "0.2.101",
149
+ "very-happy-tools-arm64-win32": "0.2.101",
150
+ "very-happy-tools-x64-win32": "0.2.101"
151
151
  };
152
152
  var devDependencies = {
153
153
  "@slopus/happy-wire": "workspace:*",
@@ -4787,6 +4787,10 @@ function registerTodoHandlers(manager) {
4787
4787
  });
4788
4788
  }
4789
4789
 
4790
+ function sanitizeGeometry(cols, rows) {
4791
+ const ok = (v) => typeof v === "number" && Number.isInteger(v) && v >= 2 && v <= 1e4;
4792
+ return ok(cols) && ok(rows) ? { cols, rows } : void 0;
4793
+ }
4790
4794
  function sanitizeTagList(value) {
4791
4795
  if (!Array.isArray(value)) return void 0;
4792
4796
  const out = [];
@@ -4806,7 +4810,7 @@ function sanitizeLiveSnapshot(raw, now, ttlMs = LIVE_SNAPSHOT_TTL_MS, max = LIVE
4806
4810
  const rows = [];
4807
4811
  for (const [id, value] of Object.entries(raw)) {
4808
4812
  if (!id || !value || typeof value !== "object") continue;
4809
- const { title, cwd, seenAt, tags, manual, attachTmux } = value;
4813
+ const { title, cwd, seenAt, tags, manual, attachTmux, cols: rawCols, rows: rawRows } = value;
4810
4814
  if (typeof seenAt !== "number" || !Number.isFinite(seenAt)) continue;
4811
4815
  if (now - seenAt >= ttlMs) continue;
4812
4816
  const tagList = sanitizeTagList(tags);
@@ -4816,6 +4820,7 @@ function sanitizeLiveSnapshot(raw, now, ttlMs = LIVE_SNAPSHOT_TTL_MS, max = LIVE
4816
4820
  ...tagList !== void 0 ? { tags: tagList } : {},
4817
4821
  ...manual === true ? { manual: true } : {},
4818
4822
  ...typeof attachTmux === "string" && attachTmux ? { attachTmux } : {},
4823
+ ...sanitizeGeometry(rawCols, rawRows) ?? {},
4819
4824
  seenAt
4820
4825
  }]);
4821
4826
  }
@@ -4838,6 +4843,7 @@ function liveSnapshotChanged(prev, next) {
4838
4843
  if (!!before.manual !== !!info.manual) return true;
4839
4844
  if ((before.attachTmux ?? "") !== (info.attachTmux ?? "")) return true;
4840
4845
  if ((before.tags ?? []).join("\0") !== (info.tags ?? []).join("\0")) return true;
4846
+ if (before.cols !== info.cols || before.rows !== info.rows) return true;
4841
4847
  }
4842
4848
  return false;
4843
4849
  }
@@ -4927,6 +4933,9 @@ function attachStartupCommand(sessionId, socket) {
4927
4933
  }
4928
4934
 
4929
4935
  const TERMINAL_ID_RE$1 = /^[a-zA-Z0-9_-]{1,64}$/;
4936
+ function recordGeometry(record) {
4937
+ return record.cols !== void 0 && record.rows !== void 0 ? { cols: record.cols, rows: record.rows } : {};
4938
+ }
4930
4939
  function planTerminalRestore(record, facts) {
4931
4940
  if (facts.tmuxAlive) return { kind: "already-live" };
4932
4941
  if (record.attachTmux) {
@@ -4942,7 +4951,8 @@ function planTerminalRestore(record, facts) {
4942
4951
  manual: true,
4943
4952
  tags: record.tags,
4944
4953
  command: attachStartupCommand(matches[0].id, facts.attachSocket),
4945
- attachTmux: record.attachTmux
4954
+ attachTmux: record.attachTmux,
4955
+ ...recordGeometry(record)
4946
4956
  };
4947
4957
  }
4948
4958
  if (!record.cwd || !facts.cwdExists(record.cwd)) return { kind: "error", reason: "missing-cwd" };
@@ -4954,7 +4964,8 @@ function planTerminalRestore(record, facts) {
4954
4964
  title: record.title,
4955
4965
  manual: record.manual === true,
4956
4966
  tags: record.tags,
4957
- ...resumable ? { command: `claude --resume ${record.claudeSessionId}` } : {}
4967
+ ...resumable ? { command: `claude --resume ${record.claudeSessionId}` } : {},
4968
+ ...recordGeometry(record)
4958
4969
  };
4959
4970
  }
4960
4971
 
@@ -9515,6 +9526,7 @@ const CAPTURE_FULL_BUDGET_BYTES = 1024 * 1024;
9515
9526
  const ALT_ENTER = Buffer.from("\x1B[?1049h", "ascii");
9516
9527
  const CURSOR_HOME = Buffer.from("\x1B[H", "ascii");
9517
9528
  const CRLF = Buffer.from("\r\n", "ascii");
9529
+ const SGR_RESET = Buffer.from("\x1B[0m", "ascii");
9518
9530
  function normalizeCaptureLines(blob) {
9519
9531
  if (blob.length === 0) return blob;
9520
9532
  const lines = [];
@@ -9551,32 +9563,38 @@ function assembleRestore(responses, paneState, budget = CAPTURE_FULL_BUDGET_BYTE
9551
9563
  const screen = () => {
9552
9564
  const raw = get("visible");
9553
9565
  const lines = splitLines(raw);
9566
+ let realCount = lines.length;
9554
9567
  if (rows !== void 0 && rows > 0) {
9555
9568
  while (lines.length > rows) lines.shift();
9569
+ realCount = lines.length;
9556
9570
  while (lines.length < rows) lines.push(EMPTY);
9557
9571
  }
9558
- const body = joinCrlf(lines);
9572
+ const realBody = joinCrlf(lines.slice(0, realCount));
9573
+ const padCount = lines.length - realCount;
9574
+ const padBody = padCount > 0 ? Buffer.concat([realCount > 0 ? CRLF : EMPTY, SGR_RESET, joinCrlf(new Array(padCount).fill(EMPTY))]) : EMPTY;
9559
9575
  const cursor = paneState ? Buffer.from(`\x1B[${paneState.cursorY + 1};${paneState.cursorX + 1}H`, "ascii") : Buffer.alloc(0);
9560
- return Buffer.concat([body, tail, cursor]);
9576
+ return Buffer.concat([realBody, padBody, SGR_RESET, tail, cursor]);
9561
9577
  };
9578
+ const wrapFull = (b) => Buffer.concat([SGR_RESET, b, SGR_RESET]);
9579
+ const wrapSmall = (b) => Buffer.concat([b, SGR_RESET]);
9562
9580
  if (!alternateOn) {
9563
9581
  const history2 = trimTrailingBlankRows(normalizeCaptureLines(get("history")));
9564
9582
  const smallHistory = trimTrailingBlankRows(normalizeCaptureLines(get("smallHistory")));
9565
9583
  const withHistory = (h) => Buffer.concat(
9566
- h.length > 0 ? [truncateToBudget(h, budget), CRLF, screen()] : [screen()]
9584
+ h.length > 0 ? [truncateToBudget(h, budget), SGR_RESET, CRLF, screen()] : [screen()]
9567
9585
  );
9568
- return { full: withHistory(history2), small: withHistory(smallHistory), alternateOn };
9586
+ return { full: wrapFull(withHistory(history2)), small: wrapSmall(withHistory(smallHistory)), alternateOn };
9569
9587
  }
9570
9588
  const history = trimTrailingBlankRows(normalizeCaptureLines(get("history")));
9571
9589
  const saved = trimTrailingBlankRows(normalizeCaptureLines(get("altSaved")));
9572
9590
  const scrollback = truncateToBudget(
9573
- Buffer.concat(history.length > 0 && saved.length > 0 ? [history, CRLF, saved] : [history, saved]),
9591
+ Buffer.concat(history.length > 0 && saved.length > 0 ? [history, SGR_RESET, CRLF, saved] : [history, saved]),
9574
9592
  budget
9575
9593
  );
9576
- const altScreen = Buffer.concat([ALT_ENTER, CURSOR_HOME, screen()]);
9594
+ const altScreen = Buffer.concat([ALT_ENTER, CURSOR_HOME, SGR_RESET, screen()]);
9577
9595
  return {
9578
- full: Buffer.concat([scrollback, scrollback.length > 0 ? CRLF : Buffer.alloc(0), altScreen]),
9579
- small: altScreen,
9596
+ full: wrapFull(Buffer.concat([scrollback, scrollback.length > 0 ? CRLF : Buffer.alloc(0), altScreen])),
9597
+ small: wrapSmall(altScreen),
9580
9598
  alternateOn
9581
9599
  };
9582
9600
  }
@@ -10074,6 +10092,7 @@ function selectAutoRestore(candidates, opts) {
10074
10092
  terminalId: c.id,
10075
10093
  ...c.tags !== void 0 ? { tags: c.tags } : {},
10076
10094
  ...c.manual ? { manual: true } : {},
10095
+ ...c.cols !== void 0 && c.rows !== void 0 ? { cols: c.cols, rows: c.rows } : {},
10077
10096
  cwd: c.cwd,
10078
10097
  title: c.title,
10079
10098
  claudeSessionId: c.claudeSessionId,
@@ -10261,7 +10280,10 @@ function startupInjectionArgs(tmuxSession, command) {
10261
10280
  ];
10262
10281
  }
10263
10282
  function seenInfoOf(t) {
10264
- return { title: t.title, cwd: t.cwd, tags: t.tags, manual: t.manual, attachTmux: t.attachTmux };
10283
+ return { title: t.title, cwd: t.cwd, tags: t.tags, manual: t.manual, attachTmux: t.attachTmux, ...seenGeometry(t.paneCols, t.paneRows) };
10284
+ }
10285
+ function seenGeometry(cols, rows) {
10286
+ return cols !== void 0 && rows !== void 0 ? { cols, rows } : {};
10265
10287
  }
10266
10288
  function terminalListSignature(items) {
10267
10289
  const canon = [...items].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).map((t) => [
@@ -10306,6 +10328,12 @@ const LIST_SESSIONS_FORMAT = [
10306
10328
  // B-273: the user tmux session this terminal was opened to attach (name),
10307
10329
  // carried into close records so a restore can re-attach.
10308
10330
  "#{@vh_attach}",
10331
+ // B-287: the pane's REAL geometry, persisted with the live snapshot so a
10332
+ // cold restore (daemon boot auto-restore, archive ↻) recreates the session
10333
+ // at the size it last had — claude's one-shot welcome banner then never
10334
+ // gets reflowed by the first web open (the "half-drawn logo").
10335
+ "#{pane_width}",
10336
+ "#{pane_height}",
10309
10337
  // B-121: the control-mode client has no `pty.process`, so the agent-state
10310
10338
  // fast path lost its live `#{pane_current_command}` equivalent. Carry it in
10311
10339
  // the ONE list-sessions call the tracker already makes — the value goes
@@ -10320,9 +10348,10 @@ const LIST_SESSIONS_FORMAT = [
10320
10348
  function parseSessionListLine(line) {
10321
10349
  if (!line) return void 0;
10322
10350
  const parts = line.split(LIST_FIELD_SEP);
10323
- if (parts.length < 10) return void 0;
10324
- const [name, created, activity, cwd, vhTitle, manual, vhTags, vhAttach, paneCommand] = parts;
10351
+ if (parts.length < 12) return void 0;
10352
+ const [name, created, activity, cwd, vhTitle, manual, vhTags, vhAttach, paneW, paneH, paneCommand] = parts;
10325
10353
  if (!name) return void 0;
10354
+ const geom = parsePositiveInt(paneW) !== void 0 && parsePositiveInt(paneH) !== void 0 ? { paneCols: parsePositiveInt(paneW), paneRows: parsePositiveInt(paneH) } : {};
10326
10355
  return {
10327
10356
  name,
10328
10357
  created: created ? Number(created) * 1e3 : void 0,
@@ -10335,11 +10364,17 @@ function parseSessionListLine(line) {
10335
10364
  // Verbatim (no trim): tmux allows edge spaces in a session name and the
10336
10365
  // restore lookup / attach echo compare it exactly.
10337
10366
  attachTmux: isSafeTmuxSessionName(vhAttach) ? vhAttach : void 0,
10338
- // pane_title is last, so anything after field 9 is title content that
10367
+ ...geom,
10368
+ // pane_title is last, so anything after field 11 is title content that
10339
10369
  // contained the separator — rejoin it rather than dropping it.
10340
- paneTitle: parts.slice(9).join(LIST_FIELD_SEP) || void 0
10370
+ paneTitle: parts.slice(11).join(LIST_FIELD_SEP) || void 0
10341
10371
  };
10342
10372
  }
10373
+ function parsePositiveInt(raw) {
10374
+ if (raw === void 0 || !/^[0-9]{1,5}$/.test(raw.trim())) return void 0;
10375
+ const n = Number(raw.trim());
10376
+ return n > 0 ? n : void 0;
10377
+ }
10343
10378
  const TERMINAL_TAG_MAX_COUNT = 64;
10344
10379
  const TERMINAL_TAG_MAX_LENGTH = 24;
10345
10380
  const TERMINAL_TAGS_MAX_BYTES = 4096;
@@ -10482,6 +10517,11 @@ const VH_SESSION_OPTION_OVERRIDES = [
10482
10517
  ["window", "window-size", "latest"],
10483
10518
  ["window", "pane-border-status", "off"]
10484
10519
  ];
10520
+ const COLD_CREATE_DEFAULT_GEOMETRY = { cols: 120, rows: 30 };
10521
+ function coldCreateGeometry(plan) {
10522
+ const ok = (v) => typeof v === "number" && Number.isInteger(v) && v >= 2 && v <= 1e4;
10523
+ return ok(plan.cols) && ok(plan.rows) ? { cols: plan.cols, rows: plan.rows } : { ...COLD_CREATE_DEFAULT_GEOMETRY };
10524
+ }
10485
10525
  function tmuxNewSessionArgs(session, cols, rows, cwd, envFlags, shell) {
10486
10526
  const args = ["new-session", "-d", ...envFlags, "-s", session, "-x", String(cols), "-y", String(rows), "-c", cwd, shell];
10487
10527
  for (const [scope, option, value] of VH_SESSION_OPTION_OVERRIDES) {
@@ -11028,6 +11068,7 @@ class WebTerminalManager {
11028
11068
  tags: info.tags,
11029
11069
  manual: info.manual,
11030
11070
  ...info.attachTmux ? { attachTmux: info.attachTmux } : {},
11071
+ ...seenGeometry(info.cols, info.rows),
11031
11072
  // Live binding first (authoritative while it exists), persisted
11032
11073
  // metadata as the fallback once the mirror has been torn down.
11033
11074
  mirrorSessionId: this.mirrorResolver?.(id) ?? mirror?.sessionId,
@@ -11081,6 +11122,7 @@ class WebTerminalManager {
11081
11122
  tags: info.tags,
11082
11123
  manual: info.manual,
11083
11124
  ...info.attachTmux ? { attachTmux: info.attachTmux } : {},
11125
+ ...seenGeometry(info.cols, info.rows),
11084
11126
  mirrorSessionId: mirror?.sessionId,
11085
11127
  claudeSessionId: mirror?.claudeSessionId,
11086
11128
  reason: "daemon-gap",
@@ -11093,6 +11135,7 @@ class WebTerminalManager {
11093
11135
  cwd: info.cwd,
11094
11136
  tags: info.tags,
11095
11137
  manual: info.manual,
11138
+ ...seenGeometry(info.cols, info.rows),
11096
11139
  seenAt: info.seenAt,
11097
11140
  claudeSessionId: mirror?.claudeSessionId
11098
11141
  });
@@ -11234,9 +11277,10 @@ class WebTerminalManager {
11234
11277
  "-e",
11235
11278
  `VH_HAPPY_HOME_DIR=${configuration.happyHomeDir}`
11236
11279
  ] : [];
11280
+ const { cols, rows } = coldCreateGeometry(plan);
11237
11281
  const created = spawnSync(
11238
11282
  "tmux",
11239
- tmuxArgs(tmuxNewSessionArgs(name, 120, 30, plan.cwd, envFlags, defaultShell())),
11283
+ tmuxArgs(tmuxNewSessionArgs(name, cols, rows, plan.cwd, envFlags, defaultShell())),
11240
11284
  { stdio: "ignore", timeout: TMUX_CREATE_TIMEOUT_MS, env }
11241
11285
  );
11242
11286
  const live = created.status === 0 && spawnSync(
@@ -11289,7 +11333,7 @@ class WebTerminalManager {
11289
11333
  * would care about changed (see liveSnapshotChanged). */
11290
11334
  persistLiveSnapshot(live, now) {
11291
11335
  const candidate = /* @__PURE__ */ new Map();
11292
- 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 });
11336
+ 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 });
11293
11337
  if (!liveSnapshotChanged(this.lastPersistedSnapshot, candidate)) return;
11294
11338
  this.lastPersistedSnapshot = candidate;
11295
11339
  saveLiveSnapshot(candidate);
@@ -11400,7 +11444,11 @@ class WebTerminalManager {
11400
11444
  seq: existing.seq,
11401
11445
  mode: "replay",
11402
11446
  chunks,
11403
- ...lines ? { streamMode: "lines" } : {},
11447
+ // B-287: a replay carries no restore, so without this the
11448
+ // adopting client had no authoritative width and fell back
11449
+ // to its own guess — the door through which a stale device
11450
+ // width leaked in. Announced = what we last told clients.
11451
+ ...lines ? { streamMode: "lines", paneCols: existing.announcedCols ?? existing.cols, paneRows: existing.announcedRows ?? existing.rows } : {},
11404
11452
  ...echo2
11405
11453
  };
11406
11454
  }
@@ -11482,6 +11530,13 @@ class WebTerminalManager {
11482
11530
  // the web xterm also sees the OSC, which is inert there.
11483
11531
  ["set-option", "-t", `=${tmuxSession}:`, "set-titles", "on"],
11484
11532
  ["set-option", "-t", `=${tmuxSession}:`, "set-titles-string", "#{pane_title}"],
11533
+ // B-287: re-assert on EVERY open, not just at create. A session
11534
+ // made before the create-time override (pre-0.2.96) or under a
11535
+ // user `window-size manual`/`smallest` silently refuses
11536
+ // `refresh-client -C`, so the pane freezes at whatever width the
11537
+ // first viewer set and no later device can widen it. `-w -q` so
11538
+ // an older tmux without the option does not fail the open.
11539
+ ["set-option", "-w", "-q", "-t", `=${tmuxSession}:`, "window-size", "latest"],
11485
11540
  ["set-option", "-g", "set-clipboard", "on"],
11486
11541
  ["set-option", "-ga", "terminal-features", ",xterm-256color:clipboard"]
11487
11542
  ];
@@ -11877,13 +11932,12 @@ class WebTerminalManager {
11877
11932
  this.applyResize(session, cols, rows);
11878
11933
  }
11879
11934
  /** Resize the transport AND the authoritative headless screen together, so a
11880
- * later snapshot matches the real geometry. Multiple tabs subscribed to one
11881
- * terminal all drive the same tmux window we simply take the LAST resize
11882
- * (tmux is single-size anyway); there's no per-subscriber geometry to
11883
- * reconcile. For a control client the declaration IS `refresh-client -C`:
11884
- * a client that never sends it doesn't participate in the window size at
11885
- * all (which is the door the spec leaves open for "phone mirrors without
11886
- * squeezing the desktop" — not implemented in this batch). */
11935
+ * later snapshot matches the real geometry. One tmux window = one size:
11936
+ * we take the LAST resize we are handed (B-287: the web only sends one on a
11937
+ * human-activity edge, so "last resize" == "most recently used viewer" ==
11938
+ * tmux's own `window-size latest` semantics). The control-client
11939
+ * declaration IS `refresh-client -C`, preceded by a `window-size latest`
11940
+ * re-assert so a manual/smallest session cannot swallow it. */
11887
11941
  applyResize(session, cols, rows) {
11888
11942
  const c = Math.max(2, Math.floor(cols));
11889
11943
  const r = Math.max(2, Math.floor(rows));
@@ -11894,7 +11948,10 @@ class WebTerminalManager {
11894
11948
  logger.debug(`[WEB TERMINAL] resize ${session.id} failed: ${e}`);
11895
11949
  }
11896
11950
  } else if (c !== session.cols || r !== session.rows) {
11897
- session.transport.client.sendFireAndForget([`refresh-client -C ${c}x${r}`]);
11951
+ session.transport.client.sendFireAndForget([
11952
+ `set-option -w -q -t =${session.tmuxSession}: window-size latest`,
11953
+ `refresh-client -C ${c}x${r}`
11954
+ ]);
11898
11955
  }
11899
11956
  session.resizeHeadless(c, r);
11900
11957
  }
@@ -11937,7 +11994,10 @@ class WebTerminalManager {
11937
11994
  * session (so a local `tmux attach` won't find it either). Used when the
11938
11995
  * user deletes the terminal from the sidebar. */
11939
11996
  killSession(terminalId, opts) {
11940
- const info = this.lastSeenInfo.get(terminalId) ?? this.listSessions().find((t) => t.id === terminalId);
11997
+ const info = this.lastSeenInfo.get(terminalId) ?? (() => {
11998
+ const t = this.listSessions().find((x) => x.id === terminalId);
11999
+ return t ? seenInfoOf(t) : void 0;
12000
+ })();
11941
12001
  const target = `=vh-${terminalId}:`;
11942
12002
  const killed = spawnSync("tmux", tmuxArgs(["kill-session", "-t", target]), { stdio: "ignore", env: ptyEnv() });
11943
12003
  const verified = spawnSync("tmux", tmuxArgs(["has-session", "-t", target]), { stdio: "ignore", env: ptyEnv() });
@@ -11963,6 +12023,7 @@ class WebTerminalManager {
11963
12023
  tags: info.tags,
11964
12024
  manual: info.manual,
11965
12025
  ...info.attachTmux ? { attachTmux: info.attachTmux } : {},
12026
+ ...seenGeometry(info.cols, info.rows),
11966
12027
  mirrorSessionId: this.mirrorResolver?.(terminalId) ?? mirror?.sessionId,
11967
12028
  claudeSessionId: mirror?.claudeSessionId,
11968
12029
  reason: "closed",
@@ -12052,7 +12113,8 @@ class WebTerminalManager {
12052
12113
  // the mirror reconciler's stricter "claude is really here" gate.
12053
12114
  claudeConfident: probe.claudeConfident,
12054
12115
  manual: s.manual,
12055
- ...s.attachTmux ? { attachTmux: s.attachTmux } : {}
12116
+ ...s.attachTmux ? { attachTmux: s.attachTmux } : {},
12117
+ ...s.paneCols !== void 0 && s.paneRows !== void 0 ? { paneCols: s.paneCols, paneRows: s.paneRows } : {}
12056
12118
  });
12057
12119
  }
12058
12120
  return out;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "very-happy-cli",
3
- "version": "0.2.99",
3
+ "version": "0.2.101",
4
4
  "description": "Remote multi-agent workspace CLI with deep Claude integration, native terminal streaming, and self-hosting.",
5
5
  "author": "Kirill Dubovitskiy",
6
6
  "contributors": [
@@ -113,12 +113,12 @@
113
113
  "zod": "^4.0.0"
114
114
  },
115
115
  "optionalDependencies": {
116
- "very-happy-tools-arm64-darwin": "0.2.99",
117
- "very-happy-tools-x64-darwin": "0.2.99",
118
- "very-happy-tools-arm64-linux": "0.2.99",
119
- "very-happy-tools-x64-linux": "0.2.99",
120
- "very-happy-tools-arm64-win32": "0.2.99",
121
- "very-happy-tools-x64-win32": "0.2.99"
116
+ "very-happy-tools-arm64-darwin": "0.2.101",
117
+ "very-happy-tools-x64-darwin": "0.2.101",
118
+ "very-happy-tools-arm64-linux": "0.2.101",
119
+ "very-happy-tools-x64-linux": "0.2.101",
120
+ "very-happy-tools-arm64-win32": "0.2.101",
121
+ "very-happy-tools-x64-win32": "0.2.101"
122
122
  },
123
123
  "devDependencies": {
124
124
  "@slopus/happy-wire": "workspace:*",