very-happy-cli 0.2.96 → 0.2.98

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 (32) hide show
  1. package/dist/{AcpBackend-B5Eh8G3W.mjs → AcpBackend-BXiGiFC7.mjs} +1 -1
  2. package/dist/{AcpBackend-BaCt4_zS.cjs → AcpBackend-DhR8ml8L.cjs} +1 -1
  3. package/dist/{AcpSessionManager-grr_aJEI.mjs → AcpSessionManager-CDnrxAfQ.mjs} +1 -1
  4. package/dist/{AcpSessionManager-CvgmPlXB.cjs → AcpSessionManager-Ckc0NFmf.cjs} +1 -1
  5. package/dist/{config-B69TZlJj.mjs → config-B3pA9YsN.mjs} +3 -3
  6. package/dist/{config-BjsUeQPA.cjs → config-GJQc_kpD.cjs} +3 -3
  7. package/dist/{index-DID1FV_n.cjs → index-BBm2b34F.cjs} +5 -5
  8. package/dist/{index-C-uJXSm3.mjs → index-Bh5Rcy0s.mjs} +6 -6
  9. package/dist/{index-uc-dtu-3.cjs → index-CO1gNtuP.cjs} +528 -25
  10. package/dist/{index-BMmbxCTq.mjs → index-CuUVSZnK.mjs} +527 -24
  11. package/dist/index.cjs +3 -3
  12. package/dist/index.mjs +3 -3
  13. package/dist/{installTerminalHooks-CepXxAM_.cjs → installTerminalHooks-Cp9JVxQ-.cjs} +3 -3
  14. package/dist/{installTerminalHooks-CBHjEQ-a.mjs → installTerminalHooks-Ru0dn_zF.mjs} +3 -3
  15. package/dist/lib.cjs +1 -1
  16. package/dist/lib.d.cts +77 -0
  17. package/dist/lib.d.mts +77 -0
  18. package/dist/lib.mjs +1 -1
  19. package/dist/{mcp-CAtbtTXF.cjs → mcp-B5HMF24K.cjs} +3 -3
  20. package/dist/{mcp-BdSHqi2d.mjs → mcp-CQoVN0u2.mjs} +3 -3
  21. package/dist/{runGemini-qStPY5fH.cjs → runGemini-BBzPVzuc.cjs} +5 -5
  22. package/dist/{runGemini-cMZ3_CRf.mjs → runGemini-CwZXQsgu.mjs} +5 -5
  23. package/dist/{runOpenClaw-B8HR6GnX.cjs → runOpenClaw-B4nC6XGL.cjs} +4 -4
  24. package/dist/{runOpenClaw-BJ36wEbk.mjs → runOpenClaw-C-jz4rWr.mjs} +4 -4
  25. package/dist/{send-6eY55o3C.cjs → send-CWgaINHC.cjs} +3 -3
  26. package/dist/{send-TX13v8Xe.mjs → send-DN-nVBEI.mjs} +3 -3
  27. package/dist/{spawn-vOZA0kOi.cjs → spawn-C0NSP84Y.cjs} +3 -3
  28. package/dist/{spawn-CfCE6JAv.mjs → spawn-CqcfbOnk.mjs} +3 -3
  29. package/dist/{types-okYCRlgl.cjs → types-BP-4phS_.cjs} +358 -72
  30. package/dist/{types-CLRl0ET3.mjs → types-Cl4vWuDY.mjs} +356 -72
  31. package/package.json +7 -7
  32. package/scripts/shims/keychain-off/security +51 -0
@@ -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.96";
54
+ var version = "0.2.98";
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.96",
168
- "very-happy-tools-x64-darwin": "0.2.96",
169
- "very-happy-tools-arm64-linux": "0.2.96",
170
- "very-happy-tools-x64-linux": "0.2.96",
171
- "very-happy-tools-arm64-win32": "0.2.96",
172
- "very-happy-tools-x64-win32": "0.2.96"
167
+ "very-happy-tools-arm64-darwin": "0.2.98",
168
+ "very-happy-tools-x64-darwin": "0.2.98",
169
+ "very-happy-tools-arm64-linux": "0.2.98",
170
+ "very-happy-tools-x64-linux": "0.2.98",
171
+ "very-happy-tools-arm64-win32": "0.2.98",
172
+ "very-happy-tools-x64-win32": "0.2.98"
173
173
  };
174
174
  var devDependencies = {
175
175
  "@slopus/happy-wire": "workspace:*",
@@ -1002,6 +1002,22 @@ z.z.object({
1002
1002
  detectedAt: z.z.number()
1003
1003
  }).optional()
1004
1004
  });
1005
+ const ClaudeAuthStateSchema = z.z.object({
1006
+ probeVersion: z.z.number(),
1007
+ daemonPid: z.z.number(),
1008
+ status: z.z.union([z.z.enum(["ok", "not-logged-in", "unknown", "error", "claude-missing"]), z.z.string()]),
1009
+ authMethod: z.z.string().optional(),
1010
+ subscriptionType: z.z.string().optional(),
1011
+ diagnosis: z.z.string().optional(),
1012
+ detail: z.z.string().optional(),
1013
+ repairable: z.z.string().optional(),
1014
+ context: z.z.object({
1015
+ platform: z.z.string(),
1016
+ lineage: z.z.string(),
1017
+ credentialStore: z.z.string()
1018
+ }),
1019
+ checkedAt: z.z.number()
1020
+ });
1005
1021
  const CliUpdateStateSchema = z.z.object({
1006
1022
  currentVersion: z.z.string(),
1007
1023
  recommendedVersion: z.z.string().nullable(),
@@ -1025,7 +1041,9 @@ const WebTerminalListItemSchema = z.z.object({
1025
1041
  * conversation carried over, the processes did not. Cleared once opened. */
1026
1042
  restoredAt: z.z.number().optional(),
1027
1043
  /** B-265: @vh_title_manual is set (user renamed it). */
1028
- manual: z.z.boolean().optional()
1044
+ manual: z.z.boolean().optional(),
1045
+ /** B-273: opened attached to the user's tmux session of this name. */
1046
+ attachTmux: z.z.string().optional()
1029
1047
  });
1030
1048
  const ClosedTerminalRecordSchema = z.z.object({
1031
1049
  id: z.z.string(),
@@ -1042,6 +1060,8 @@ const ClosedTerminalRecordSchema = z.z.object({
1042
1060
  /** B-265: tags + manual-rename flag at close time (restored verbatim). */
1043
1061
  tags: z.z.array(z.z.string()).optional(),
1044
1062
  manual: z.z.boolean().optional(),
1063
+ /** B-273: attached user tmux session name — restore re-attaches. */
1064
+ attachTmux: z.z.string().optional(),
1045
1065
  closedAt: z.z.number()
1046
1066
  });
1047
1067
  z.z.object({
@@ -1095,8 +1115,26 @@ z.z.object({
1095
1115
  rpcAvailable: z.z.boolean(),
1096
1116
  detectedAt: z.z.number()
1097
1117
  }).optional(),
1118
+ /**
1119
+ * B-273 capability flag: this daemon answers `list-tmux-sessions` and
1120
+ * honours `open-terminal.attachTmux`. Same trust rule (`detectedAt >=
1121
+ * startedAt`). Stamped unconditionally — on a machine without tmux the
1122
+ * RPC just answers an empty list.
1123
+ */
1124
+ tmuxSessions: z.z.object({
1125
+ rpcAvailable: z.z.boolean(),
1126
+ detectedAt: z.z.number()
1127
+ }).optional(),
1098
1128
  /** Relay-owned CLI compatibility/update policy last checked by this daemon. */
1099
- cliUpdate: CliUpdateStateSchema.optional()
1129
+ cliUpdate: CliUpdateStateSchema.optional(),
1130
+ /**
1131
+ * B-276: result of the daemon-context Claude auth preflight (spec
1132
+ * 2026-09-claude-auth-preflight). Trust rule for consumers:
1133
+ * `claudeAuth.daemonPid === daemonState.pid` — daemonState survives daemon
1134
+ * restarts (the server returns the existing row and connect spreads it
1135
+ * forward), so a downgraded daemon carries this field along unchanged.
1136
+ */
1137
+ claudeAuth: ClaudeAuthStateSchema.optional()
1100
1138
  });
1101
1139
  const MessageMetaSchema = z.z.object({
1102
1140
  sentFrom: z.z.string().optional(),
@@ -1545,7 +1583,7 @@ class RpcHandlerManager {
1545
1583
  }
1546
1584
  }
1547
1585
 
1548
- 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-okYCRlgl.cjs', document.baseURI).href))));
1586
+ 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-BP-4phS_.cjs', document.baseURI).href))));
1549
1587
  function projectPath() {
1550
1588
  const path = path$1.resolve(__dirname$1, "..");
1551
1589
  return path;
@@ -4788,7 +4826,7 @@ function sanitizeLiveSnapshot(raw, now, ttlMs = LIVE_SNAPSHOT_TTL_MS, max = LIVE
4788
4826
  const rows = [];
4789
4827
  for (const [id, value] of Object.entries(raw)) {
4790
4828
  if (!id || !value || typeof value !== "object") continue;
4791
- const { title, cwd, seenAt, tags, manual } = value;
4829
+ const { title, cwd, seenAt, tags, manual, attachTmux } = value;
4792
4830
  if (typeof seenAt !== "number" || !Number.isFinite(seenAt)) continue;
4793
4831
  if (now - seenAt >= ttlMs) continue;
4794
4832
  const tagList = sanitizeTagList(tags);
@@ -4797,6 +4835,7 @@ function sanitizeLiveSnapshot(raw, now, ttlMs = LIVE_SNAPSHOT_TTL_MS, max = LIVE
4797
4835
  cwd: typeof cwd === "string" && cwd ? cwd : void 0,
4798
4836
  ...tagList !== void 0 ? { tags: tagList } : {},
4799
4837
  ...manual === true ? { manual: true } : {},
4838
+ ...typeof attachTmux === "string" && attachTmux ? { attachTmux } : {},
4800
4839
  seenAt
4801
4840
  }]);
4802
4841
  }
@@ -4817,6 +4856,7 @@ function liveSnapshotChanged(prev, next) {
4817
4856
  if (!before) return true;
4818
4857
  if (before.title !== info.title || before.cwd !== info.cwd) return true;
4819
4858
  if (!!before.manual !== !!info.manual) return true;
4859
+ if ((before.attachTmux ?? "") !== (info.attachTmux ?? "")) return true;
4820
4860
  if ((before.tags ?? []).join("\0") !== (info.tags ?? []).join("\0")) return true;
4821
4861
  }
4822
4862
  return false;
@@ -4842,9 +4882,89 @@ function pickMirrorForTerminal(sessions, terminalId) {
4842
4882
  return best ? { sessionId: best.id, claudeSessionId: best.claude } : null;
4843
4883
  }
4844
4884
 
4885
+ function shellescape(s) {
4886
+ return "'" + s.replace(/'/g, "'\\''") + "'";
4887
+ }
4888
+
4889
+ const USER_SESSION_FIELD_SEP = "<~|~>";
4890
+ const USER_SESSIONS_FORMAT = [
4891
+ "#{session_id}",
4892
+ "#{session_name}",
4893
+ "#{session_windows}",
4894
+ "#{session_attached}",
4895
+ "#{session_activity}",
4896
+ "#{session_created}",
4897
+ "#{pane_current_command}",
4898
+ "#{pane_current_path}"
4899
+ ].join(USER_SESSION_FIELD_SEP);
4900
+ const TMUX_SESSION_ID_RE = /^\$\d{1,9}$/;
4901
+ const TMUX_SESSION_NAME_MAX = 128;
4902
+ function isSafeTmuxSessionName(name) {
4903
+ if (typeof name !== "string") return false;
4904
+ if (name.length === 0 || name.length > TMUX_SESSION_NAME_MAX) return false;
4905
+ return !/[\x00-\x1f\x7f]/.test(name);
4906
+ }
4907
+ function isVhSessionName(name) {
4908
+ return name.startsWith("vh-");
4909
+ }
4910
+ function parseUserSessionLine(line) {
4911
+ if (!line) return void 0;
4912
+ const parts = line.split(USER_SESSION_FIELD_SEP);
4913
+ if (parts.length < 8) return void 0;
4914
+ const [id, name, windows, attached, activity, created, command] = parts;
4915
+ if (!TMUX_SESSION_ID_RE.test(id) || !isSafeTmuxSessionName(name)) return void 0;
4916
+ const cwd = parts.slice(7).join(USER_SESSION_FIELD_SEP);
4917
+ const num = (v) => {
4918
+ const n = Number(v);
4919
+ return Number.isFinite(n) ? n : void 0;
4920
+ };
4921
+ return {
4922
+ id,
4923
+ name,
4924
+ windows: Math.max(0, Math.floor(num(windows) ?? 0)),
4925
+ attached: (num(attached) ?? 0) > 0,
4926
+ activityAt: activity ? (num(activity) ?? 0) * 1e3 || void 0 : void 0,
4927
+ createdAt: created ? (num(created) ?? 0) * 1e3 || void 0 : void 0,
4928
+ command: command.trim() || void 0,
4929
+ cwd: cwd || void 0
4930
+ };
4931
+ }
4932
+ const USER_SESSIONS_MAX = 50;
4933
+ function parseUserSessions(stdout, max = USER_SESSIONS_MAX) {
4934
+ const out = [];
4935
+ for (const line of stdout.split("\n")) {
4936
+ const s = parseUserSessionLine(line);
4937
+ if (!s || isVhSessionName(s.name)) continue;
4938
+ out.push(s);
4939
+ }
4940
+ out.sort((a, b) => (b.activityAt ?? 0) - (a.activityAt ?? 0));
4941
+ return out.slice(0, max);
4942
+ }
4943
+ function attachStartupCommand(sessionId, socket) {
4944
+ if (!TMUX_SESSION_ID_RE.test(sessionId)) throw new Error(`not a tmux session id: ${sessionId}`);
4945
+ const sock = socket ? ` -S ${shellescape(socket)}` : "";
4946
+ return ` TMUX= tmux${sock} attach-session -t ${shellescape(sessionId)}`;
4947
+ }
4948
+
4845
4949
  const TERMINAL_ID_RE$1 = /^[a-zA-Z0-9_-]{1,64}$/;
4846
4950
  function planTerminalRestore(record, facts) {
4847
4951
  if (facts.tmuxAlive) return { kind: "already-live" };
4952
+ if (record.attachTmux) {
4953
+ const matches = (facts.userSessions ?? []).filter((s) => s.name === record.attachTmux);
4954
+ if (matches.length !== 1) return { kind: "error", reason: "tmux-session-gone" };
4955
+ const cwd = record.cwd && facts.cwdExists(record.cwd) ? record.cwd : facts.homeDir;
4956
+ if (!cwd) return { kind: "error", reason: "missing-cwd" };
4957
+ return {
4958
+ kind: "create",
4959
+ terminalId: record.id,
4960
+ cwd,
4961
+ title: record.title ?? record.attachTmux,
4962
+ manual: true,
4963
+ tags: record.tags,
4964
+ command: attachStartupCommand(matches[0].id, facts.attachSocket),
4965
+ attachTmux: record.attachTmux
4966
+ };
4967
+ }
4848
4968
  if (!record.cwd || !facts.cwdExists(record.cwd)) return { kind: "error", reason: "missing-cwd" };
4849
4969
  const resumable = isClaudeSessionId(record.claudeSessionId) && facts.conversationExists(record.cwd, record.claudeSessionId);
4850
4970
  return {
@@ -4858,6 +4978,20 @@ function planTerminalRestore(record, facts) {
4858
4978
  };
4859
4979
  }
4860
4980
 
4981
+ const VH_TMUX_SOCKET_ENV = "VH_TMUX_SOCKET";
4982
+ function tmuxSocketFlags(env = process.env) {
4983
+ const socket = env[VH_TMUX_SOCKET_ENV];
4984
+ return socket ? ["-S", socket] : [];
4985
+ }
4986
+ function tmuxArgs(args, env = process.env) {
4987
+ return [...tmuxSocketFlags(env), ...args];
4988
+ }
4989
+ function scrubTmuxClientEnv(env) {
4990
+ delete env.TMUX;
4991
+ delete env.TMUX_PANE;
4992
+ return env;
4993
+ }
4994
+
4861
4995
  function getProjectPath(workingDirectory) {
4862
4996
  const projectId = path.resolve(workingDirectory).replace(/[^a-zA-Z0-9-]/g, "-");
4863
4997
  const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
@@ -9191,20 +9325,6 @@ function previewAscii(line) {
9191
9325
  return line.length > 80 ? `${out}\u2026` : out;
9192
9326
  }
9193
9327
 
9194
- const VH_TMUX_SOCKET_ENV = "VH_TMUX_SOCKET";
9195
- function tmuxSocketFlags(env = process.env) {
9196
- const socket = env[VH_TMUX_SOCKET_ENV];
9197
- return socket ? ["-S", socket] : [];
9198
- }
9199
- function tmuxArgs(args, env = process.env) {
9200
- return [...tmuxSocketFlags(env), ...args];
9201
- }
9202
- function scrubTmuxClientEnv(env) {
9203
- delete env.TMUX;
9204
- delete env.TMUX_PANE;
9205
- return env;
9206
- }
9207
-
9208
9328
  const CONTROL_CLIENT_KILL_GRACE_MS = 2e3;
9209
9329
  const CONTROL_COMMAND_TIMEOUT_MS = 1e4;
9210
9330
  function isSafeControlCommand(command) {
@@ -10022,7 +10142,7 @@ function sanitizeClosedTerminals(raw, max = CLOSED_TERMINALS_MAX) {
10022
10142
  const out = [];
10023
10143
  for (const item of raw) {
10024
10144
  if (!item || typeof item !== "object") continue;
10025
- const { id, title, cwd, mirrorSessionId, claudeSessionId, reason, closedAt, tags, manual } = item;
10145
+ const { id, title, cwd, mirrorSessionId, claudeSessionId, reason, closedAt, tags, manual, attachTmux } = item;
10026
10146
  if (typeof id !== "string" || id.length === 0 || typeof closedAt !== "number") continue;
10027
10147
  if (seen.has(id)) continue;
10028
10148
  seen.add(id);
@@ -10036,6 +10156,7 @@ function sanitizeClosedTerminals(raw, max = CLOSED_TERMINALS_MAX) {
10036
10156
  reason: reason === "daemon-gap" || reason === "closed" ? reason : void 0,
10037
10157
  ...tagList !== void 0 ? { tags: tagList } : {},
10038
10158
  ...manual === true ? { manual: true } : {},
10159
+ ...typeof attachTmux === "string" && attachTmux ? { attachTmux } : {},
10039
10160
  closedAt
10040
10161
  });
10041
10162
  }
@@ -10160,7 +10281,7 @@ function startupInjectionArgs(tmuxSession, command) {
10160
10281
  ];
10161
10282
  }
10162
10283
  function seenInfoOf(t) {
10163
- return { title: t.title, cwd: t.cwd, tags: t.tags, manual: t.manual };
10284
+ return { title: t.title, cwd: t.cwd, tags: t.tags, manual: t.manual, attachTmux: t.attachTmux };
10164
10285
  }
10165
10286
  function terminalListSignature(items) {
10166
10287
  const canon = [...items].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).map((t) => [
@@ -10173,7 +10294,8 @@ function terminalListSignature(items) {
10173
10294
  t.agentState ?? "",
10174
10295
  // B-105: a mirror binding appearing/disappearing MUST push the
10175
10296
  // list, or the web never learns the toggle became available.
10176
- t.mirrorSessionId ?? ""
10297
+ t.mirrorSessionId ?? "",
10298
+ t.attachTmux ?? ""
10177
10299
  ]);
10178
10300
  return JSON.stringify(canon);
10179
10301
  }
@@ -10192,7 +10314,7 @@ function deriveAutoTitle(paneTitle, hostname) {
10192
10314
  const chars = Array.from(t);
10193
10315
  return chars.length > TITLE_MAX_CHARS ? chars.slice(0, TITLE_MAX_CHARS).join("") : t;
10194
10316
  }
10195
- const LIST_FIELD_SEP = "";
10317
+ const LIST_FIELD_SEP = "<~|~>";
10196
10318
  const LIST_SESSIONS_FORMAT = [
10197
10319
  "#{session_name}",
10198
10320
  "#{session_created}",
@@ -10201,6 +10323,9 @@ const LIST_SESSIONS_FORMAT = [
10201
10323
  "#{@vh_title}",
10202
10324
  "#{@vh_title_manual}",
10203
10325
  "#{@vh_tags}",
10326
+ // B-273: the user tmux session this terminal was opened to attach (name),
10327
+ // carried into close records so a restore can re-attach.
10328
+ "#{@vh_attach}",
10204
10329
  // B-121: the control-mode client has no `pty.process`, so the agent-state
10205
10330
  // fast path lost its live `#{pane_current_command}` equivalent. Carry it in
10206
10331
  // the ONE list-sessions call the tracker already makes — the value goes
@@ -10215,8 +10340,8 @@ const LIST_SESSIONS_FORMAT = [
10215
10340
  function parseSessionListLine(line) {
10216
10341
  if (!line) return void 0;
10217
10342
  const parts = line.split(LIST_FIELD_SEP);
10218
- if (parts.length < 9) return void 0;
10219
- const [name, created, activity, cwd, vhTitle, manual, vhTags, paneCommand] = parts;
10343
+ if (parts.length < 10) return void 0;
10344
+ const [name, created, activity, cwd, vhTitle, manual, vhTags, vhAttach, paneCommand] = parts;
10220
10345
  if (!name) return void 0;
10221
10346
  return {
10222
10347
  name,
@@ -10227,9 +10352,12 @@ function parseSessionListLine(line) {
10227
10352
  manual: manual.trim().length > 0,
10228
10353
  tags: parseTerminalTags(vhTags),
10229
10354
  paneCurrentCommand: paneCommand.trim() || void 0,
10230
- // pane_title is last, so anything after field 8 is title content that
10355
+ // Verbatim (no trim): tmux allows edge spaces in a session name and the
10356
+ // restore lookup / attach echo compare it exactly.
10357
+ attachTmux: isSafeTmuxSessionName(vhAttach) ? vhAttach : void 0,
10358
+ // pane_title is last, so anything after field 9 is title content that
10231
10359
  // contained the separator — rejoin it rather than dropping it.
10232
- paneTitle: parts.slice(8).join(LIST_FIELD_SEP) || void 0
10360
+ paneTitle: parts.slice(9).join(LIST_FIELD_SEP) || void 0
10233
10361
  };
10234
10362
  }
10235
10363
  const TERMINAL_TAG_MAX_COUNT = 64;
@@ -10286,12 +10414,12 @@ function isClaudeConfident(tail) {
10286
10414
  const last15 = lines.slice(-15).join("\n");
10287
10415
  return last15.includes("Do you want") || last15.includes("Would you like to proceed") || /^[\s│]*[❯>]\s*1\.\s/m.test(last15) || text.includes("esc to interrupt") || text.includes("? for shortcuts") || text.includes("bypass permissions on") || text.includes("\u23F5\u23F5");
10288
10416
  }
10289
- function planScrollAction(paneInMode, alternateOn, paneWantsMouse, lines, claudeLike = false, paneRows = 24) {
10417
+ function planScrollAction(paneInMode, alternateOn, paneWantsMouse, lines, claudeLike = false, paneRows = 24, nestedTmux = false) {
10290
10418
  const count = Math.min(Math.abs(Math.trunc(lines)), 200);
10291
10419
  if (count === 0) return { kind: "none" };
10292
10420
  const up = lines > 0;
10293
10421
  if (paneInMode) return { kind: "copy-scroll", dir: up ? "up" : "down", count };
10294
- if (alternateOn && paneWantsMouse) return { kind: "mouse-wheel", dir: up ? "up" : "down", count };
10422
+ if (alternateOn && (paneWantsMouse || nestedTmux)) return { kind: "mouse-wheel", dir: up ? "up" : "down", count };
10295
10423
  if (alternateOn && claudeLike) {
10296
10424
  const halfPage = Math.max(1, Math.floor(paneRows / 2));
10297
10425
  const pages = Math.max(1, Math.round(count / halfPage));
@@ -10406,6 +10534,9 @@ class TerminalSession {
10406
10534
  id;
10407
10535
  tmuxSession;
10408
10536
  transport;
10537
+ /** B-273: the user tmux session this terminal was created to attach —
10538
+ * echoed to any later open of the same live session. */
10539
+ attachedTmux;
10409
10540
  /**
10410
10541
  * The pane this terminal follows, latched from the open capture's
10411
10542
  * `list-panes` (first pane of the window). `%output` from any OTHER pane is
@@ -10916,6 +11047,7 @@ class WebTerminalManager {
10916
11047
  cwd: info.cwd,
10917
11048
  tags: info.tags,
10918
11049
  manual: info.manual,
11050
+ ...info.attachTmux ? { attachTmux: info.attachTmux } : {},
10919
11051
  // Live binding first (authoritative while it exists), persisted
10920
11052
  // metadata as the fallback once the mirror has been torn down.
10921
11053
  mirrorSessionId: this.mirrorResolver?.(id) ?? mirror?.sessionId,
@@ -10968,6 +11100,7 @@ class WebTerminalManager {
10968
11100
  cwd: info.cwd,
10969
11101
  tags: info.tags,
10970
11102
  manual: info.manual,
11103
+ ...info.attachTmux ? { attachTmux: info.attachTmux } : {},
10971
11104
  mirrorSessionId: mirror?.sessionId,
10972
11105
  claudeSessionId: mirror?.claudeSessionId,
10973
11106
  reason: "daemon-gap",
@@ -11087,12 +11220,17 @@ class WebTerminalManager {
11087
11220
  return false;
11088
11221
  }
11089
11222
  },
11090
- conversationExists: (cwd, claudeSessionId) => node_fs.existsSync(path.join(getProjectPath(cwd), `${claudeSessionId}.jsonl`))
11223
+ conversationExists: (cwd, claudeSessionId) => node_fs.existsSync(path.join(getProjectPath(cwd), `${claudeSessionId}.jsonl`)),
11224
+ // B-273: only consulted for records that were attach terminals.
11225
+ // Unbounded: the panel's 50-row cap must not hide a restore target.
11226
+ userSessions: record.attachTmux ? this.listUserTmuxSessions(Infinity) : [],
11227
+ attachSocket: process.env[VH_TMUX_SOCKET_ENV],
11228
+ homeDir: os.homedir()
11091
11229
  });
11092
11230
  if (plan.kind === "already-live") return { type: "success", terminalId };
11093
11231
  if (plan.kind === "error") return { type: "error", reason: plan.reason };
11094
11232
  if (!this.createDetachedTerminal(plan)) return { type: "error", reason: "create-failed" };
11095
- logger.info(`[WEB TERMINAL] restored closed terminal ${terminalId} in ${plan.cwd}${plan.command ? " (conversation resumed)" : ""}`);
11233
+ logger.info(`[WEB TERMINAL] restored closed terminal ${terminalId} in ${plan.cwd}${plan.attachTmux ? ` (attached tmux session ${plan.attachTmux})` : plan.command ? " (conversation resumed)" : ""}`);
11096
11234
  this.kickListRefresh();
11097
11235
  return { type: "success", terminalId };
11098
11236
  }
@@ -11152,6 +11290,13 @@ class WebTerminalManager {
11152
11290
  { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }
11153
11291
  );
11154
11292
  }
11293
+ if (plan.attachTmux) {
11294
+ node_child_process.spawnSync(
11295
+ "tmux",
11296
+ tmuxArgs(["set-option", "-t", `=${name}:`, "@vh_attach", plan.attachTmux]),
11297
+ { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }
11298
+ );
11299
+ }
11155
11300
  if (plan.command) {
11156
11301
  for (const argv of startupInjectionArgs(name, plan.command)) {
11157
11302
  node_child_process.spawnSync("tmux", tmuxArgs(argv), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env });
@@ -11164,7 +11309,7 @@ class WebTerminalManager {
11164
11309
  * would care about changed (see liveSnapshotChanged). */
11165
11310
  persistLiveSnapshot(live, now) {
11166
11311
  const candidate = /* @__PURE__ */ new Map();
11167
- for (const [id, info] of live) candidate.set(id, { title: info.title, cwd: info.cwd, tags: info.tags, manual: info.manual, seenAt: now });
11312
+ 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 });
11168
11313
  if (!liveSnapshotChanged(this.lastPersistedSnapshot, candidate)) return;
11169
11314
  this.lastPersistedSnapshot = candidate;
11170
11315
  saveLiveSnapshot(candidate);
@@ -11262,6 +11407,7 @@ class WebTerminalManager {
11262
11407
  const lines = opts.streamMode === "lines";
11263
11408
  const existing = this.terminals.get(id);
11264
11409
  if (existing) {
11410
+ const echo2 = opts.attachTmux && !(opts.attachOnly || opts.resub) && existing.attachedTmux?.name === opts.attachTmux.name ? { attachedTmux: existing.attachedTmux } : {};
11265
11411
  if (!opts.resub || existing.subscribers === 0) existing.subscribers += 1;
11266
11412
  existing.lastTouch = Date.now();
11267
11413
  this.applyResize(existing, cols, rows);
@@ -11274,15 +11420,16 @@ class WebTerminalManager {
11274
11420
  seq: existing.seq,
11275
11421
  mode: "replay",
11276
11422
  chunks,
11277
- ...lines ? { streamMode: "lines" } : {}
11423
+ ...lines ? { streamMode: "lines" } : {},
11424
+ ...echo2
11278
11425
  };
11279
11426
  }
11280
11427
  if (lines && existing.transport.kind === "control") {
11281
- return await this.linesSnapshotResponse(id, existing);
11428
+ return { ...await this.linesSnapshotResponse(id, existing), ...echo2 };
11282
11429
  }
11283
11430
  const state = existing.subscribeState(opts.fromSeq);
11284
11431
  logger.debug(`[WEB TERMINAL] re-subscribed ${id} (subs=${existing.subscribers}, mode=${state.mode}, seq=${state.seq})`);
11285
- return { terminalId: id, tmuxSession: existing.tmuxSession, ...state };
11432
+ return { terminalId: id, tmuxSession: existing.tmuxSession, ...state, ...echo2 };
11286
11433
  }
11287
11434
  if (opts.attachOnly || opts.resub) {
11288
11435
  const alive = isTmuxAvailable() && node_child_process.spawnSync(
@@ -11307,6 +11454,10 @@ class WebTerminalManager {
11307
11454
  let file = defaultShell();
11308
11455
  let args = [];
11309
11456
  let tmuxSession;
11457
+ let attachedTmux;
11458
+ if (opts.attachTmux && !(opts.attachOnly || opts.resub) && !isTmuxAvailable()) {
11459
+ throw new Error("tmux-unavailable");
11460
+ }
11310
11461
  if (isTmuxAvailable()) {
11311
11462
  tmuxSession = `vh-${id}`;
11312
11463
  let createdNew = false;
@@ -11319,6 +11470,7 @@ class WebTerminalManager {
11319
11470
  `VH_HAPPY_HOME_DIR=${configuration.happyHomeDir}`
11320
11471
  ] : [];
11321
11472
  const attachOnly = !!(opts.attachOnly || opts.resub);
11473
+ const attachTarget = !attachOnly && opts.attachTmux ? this.resolveAttachTarget(opts.attachTmux) : void 0;
11322
11474
  if (!attachOnly) {
11323
11475
  try {
11324
11476
  const created = node_child_process.spawnSync(
@@ -11360,16 +11512,15 @@ class WebTerminalManager {
11360
11512
  }
11361
11513
  }
11362
11514
  if (createdNew) {
11363
- const startup = normalizeStartupCommand(opts.startupCommand);
11364
- if (startup) {
11365
- try {
11366
- for (const a of startupInjectionArgs(tmuxSession, startup)) {
11367
- node_child_process.spawnSync("tmux", tmuxArgs(a), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env });
11368
- }
11369
- logger.debug(`[WEB TERMINAL] injected startup command into new session ${tmuxSession}`);
11370
- } catch {
11371
- }
11372
- }
11515
+ attachedTmux = this.injectIntoCreated(tmuxSession, attachTarget, opts.startupCommand, env);
11516
+ } else if (opts.attachTmux && !attachOnly) {
11517
+ const stored = node_child_process.spawnSync(
11518
+ "tmux",
11519
+ tmuxArgs(["show-options", "-qv", "-t", `=${tmuxSession}:`, "@vh_attach"]),
11520
+ { encoding: "utf8", timeout: TMUX_PROBE_TIMEOUT_MS, env }
11521
+ );
11522
+ const name = stored.status === 0 && typeof stored.stdout === "string" ? stored.stdout.replace(/\r?\n$/, "") : "";
11523
+ if (name && name === opts.attachTmux.name) attachedTmux = { id: opts.attachTmux.id, name };
11373
11524
  }
11374
11525
  if (!attachOnly && !createdNew) {
11375
11526
  const alive = node_child_process.spawnSync(
@@ -11384,17 +11535,7 @@ class WebTerminalManager {
11384
11535
  { stdio: "ignore", timeout: TMUX_CREATE_TIMEOUT_MS, env }
11385
11536
  );
11386
11537
  createdNew = retry.status === 0;
11387
- if (createdNew) {
11388
- const startup = normalizeStartupCommand(opts.startupCommand);
11389
- if (startup) {
11390
- for (const a of startupInjectionArgs(tmuxSession, startup)) {
11391
- try {
11392
- node_child_process.spawnSync("tmux", tmuxArgs(a), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env });
11393
- } catch {
11394
- }
11395
- }
11396
- }
11397
- }
11538
+ if (createdNew) attachedTmux = this.injectIntoCreated(tmuxSession, attachTarget, opts.startupCommand, env);
11398
11539
  }
11399
11540
  }
11400
11541
  if (!attachOnly && !createdNew) {
@@ -11404,6 +11545,7 @@ class WebTerminalManager {
11404
11545
  { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }
11405
11546
  ).status === 0;
11406
11547
  if (!alive) {
11548
+ if (attachTarget) throw new Error("tmux-unavailable");
11407
11549
  logger.info(`[WEB TERMINAL] tmux session creation unavailable; using direct PTY for ${id}`);
11408
11550
  tmuxSession = void 0;
11409
11551
  }
@@ -11486,20 +11628,110 @@ class WebTerminalManager {
11486
11628
  session = created;
11487
11629
  }
11488
11630
  session.subscribers = 1;
11631
+ if (attachedTmux) session.attachedTmux = attachedTmux;
11489
11632
  this.terminals.set(id, session);
11490
11633
  session.onTitleChange(() => this.kickListRefresh());
11491
11634
  logger.debug(`[WEB TERMINAL] opened ${id} (${tmuxSession ? "control" : "pty"}) ${cols}x${rows} cwd=${cwd}`);
11492
11635
  this.kickListRefresh();
11636
+ const echo = attachedTmux ? { attachedTmux } : {};
11493
11637
  if (session.transport.kind !== "control") {
11494
11638
  const state = session.subscribeState(void 0);
11495
- return { terminalId: id, tmuxSession, ...state, ...lines ? { streamMode: "lines" } : {} };
11639
+ return { terminalId: id, tmuxSession, ...state, ...lines ? { streamMode: "lines" } : {}, ...echo };
11496
11640
  }
11497
11641
  const restored = await this.captureRestore(session, true);
11498
11642
  if (!lines) {
11499
11643
  const state = session.subscribeState(void 0);
11500
- return { terminalId: id, tmuxSession, ...state };
11644
+ return { terminalId: id, tmuxSession, ...state, ...echo };
11645
+ }
11646
+ return { ...this.linesResponse(id, session, restored), ...echo };
11647
+ }
11648
+ /**
11649
+ * Post-create injection shared by BOTH creation sites in open(): an attach
11650
+ * request (B-273) pins the title to the user session's name (the inner
11651
+ * tmux's pane title is not reliable), stores `@vh_attach` for close
11652
+ * records / restore and types the attach line; otherwise the ordinary
11653
+ * startup command runs. Returns what to echo as `attachedTmux`.
11654
+ */
11655
+ injectIntoCreated(tmuxSession, attachTarget, startupCommand, env) {
11656
+ if (attachTarget) {
11657
+ for (const a of [
11658
+ ["set-option", "-t", `=${tmuxSession}:`, "@vh_title", attachTarget.name],
11659
+ ["set-option", "-t", `=${tmuxSession}:`, "@vh_title_manual", "1"],
11660
+ ["set-option", "-t", `=${tmuxSession}:`, "@vh_attach", attachTarget.name],
11661
+ ...startupInjectionArgs(tmuxSession, attachStartupCommand(attachTarget.id, process.env[VH_TMUX_SOCKET_ENV]))
11662
+ ]) {
11663
+ try {
11664
+ node_child_process.spawnSync("tmux", tmuxArgs(a), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env });
11665
+ } catch {
11666
+ }
11667
+ }
11668
+ logger.debug(`[WEB TERMINAL] ${tmuxSession} attaches user tmux session ${attachTarget.id} (${attachTarget.name})`);
11669
+ return { id: attachTarget.id, name: attachTarget.name };
11670
+ }
11671
+ const startup = normalizeStartupCommand(startupCommand);
11672
+ if (startup) {
11673
+ try {
11674
+ for (const a of startupInjectionArgs(tmuxSession, startup)) {
11675
+ node_child_process.spawnSync("tmux", tmuxArgs(a), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env });
11676
+ }
11677
+ logger.debug(`[WEB TERMINAL] injected startup command into new session ${tmuxSession}`);
11678
+ } catch {
11679
+ }
11680
+ }
11681
+ return void 0;
11682
+ }
11683
+ /**
11684
+ * B-273: check an attach request against the live tmux server. The id
11685
+ * must be a real `$N` (session ids only reset when the tmux server
11686
+ * restarts, so a stale id from an older list simply does not exist)
11687
+ * whose CURRENT name equals the name the web saw — the name cross-check
11688
+ * is what refuses a target that was killed and re-created in between —
11689
+ * and must not be a vh-* web terminal. Throws the 'tmux-session-gone'
11690
+ * contract string. Note `display-message -t '$N'` exits 0 with an EMPTY
11691
+ * line for an unknown id (its target lookup may fail silently), which is
11692
+ * why existence is probed with `has-session` first.
11693
+ */
11694
+ resolveAttachTarget(req) {
11695
+ const id = typeof req?.id === "string" ? req.id : "";
11696
+ const name = typeof req?.name === "string" ? req.name : "";
11697
+ if (!TMUX_SESSION_ID_RE.test(id) || !isSafeTmuxSessionName(name) || isVhSessionName(name)) throw new Error("tmux-session-gone");
11698
+ const env = ptyEnv();
11699
+ const exists = node_child_process.spawnSync("tmux", tmuxArgs(["has-session", "-t", id]), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }).status === 0;
11700
+ if (!exists) throw new Error("tmux-session-gone");
11701
+ const r = node_child_process.spawnSync(
11702
+ "tmux",
11703
+ tmuxArgs(["display-message", "-p", "-t", id, "#{session_name}"]),
11704
+ { encoding: "utf8", timeout: TMUX_PROBE_TIMEOUT_MS, env }
11705
+ );
11706
+ const current = r.status === 0 && typeof r.stdout === "string" ? r.stdout.replace(/\r?\n$/, "") : void 0;
11707
+ if (current !== name) throw new Error("tmux-session-gone");
11708
+ return { id, name };
11709
+ }
11710
+ /** B-273: the user's own tmux sessions (never vh-*), newest activity
11711
+ * first. Empty when tmux is missing or no server runs. */
11712
+ /** Last raw outcome of the list-tmux-sessions spawn (diagnostics only). */
11713
+ lastUserSessionsProbe;
11714
+ listUserTmuxSessions(max) {
11715
+ if (!isTmuxAvailable()) {
11716
+ this.lastUserSessionsProbe = { status: null, error: "tmux-unavailable", stdoutBytes: 0 };
11717
+ return [];
11718
+ }
11719
+ try {
11720
+ const r = node_child_process.spawnSync(
11721
+ "tmux",
11722
+ tmuxArgs(["list-sessions", "-F", USER_SESSIONS_FORMAT]),
11723
+ { encoding: "utf8", timeout: TMUX_CREATE_TIMEOUT_MS, env: ptyEnv() }
11724
+ );
11725
+ this.lastUserSessionsProbe = { status: r.status, error: r.error?.message, stderr: (r.stderr || "").toString().trim() || void 0, stdoutBytes: typeof r.stdout === "string" ? r.stdout.length : 0 };
11726
+ if (r.status !== 0 || typeof r.stdout !== "string") {
11727
+ logger.debug(`[WEB TERMINAL] list-tmux-sessions failed: ${JSON.stringify(this.lastUserSessionsProbe)}`);
11728
+ return [];
11729
+ }
11730
+ return parseUserSessions(r.stdout, max);
11731
+ } catch (e) {
11732
+ this.lastUserSessionsProbe = { status: null, error: e instanceof Error ? e.message : String(e), stdoutBytes: 0 };
11733
+ return [];
11501
11734
  }
11502
- return this.linesResponse(id, session, restored);
11503
11735
  }
11504
11736
  /**
11505
11737
  * Run the opening/catch-up capture for a control session and prime the
@@ -11741,6 +11973,7 @@ class WebTerminalManager {
11741
11973
  cwd: info.cwd,
11742
11974
  tags: info.tags,
11743
11975
  manual: info.manual,
11976
+ ...info.attachTmux ? { attachTmux: info.attachTmux } : {},
11744
11977
  mirrorSessionId: this.mirrorResolver?.(terminalId) ?? mirror?.sessionId,
11745
11978
  claudeSessionId: mirror?.claudeSessionId,
11746
11979
  reason: "closed",
@@ -11829,7 +12062,8 @@ class WebTerminalManager {
11829
12062
  // Daemon-internal (not pushed to web / not in the signature):
11830
12063
  // the mirror reconciler's stricter "claude is really here" gate.
11831
12064
  claudeConfident: probe.claudeConfident,
11832
- manual: s.manual
12065
+ manual: s.manual,
12066
+ ...s.attachTmux ? { attachTmux: s.attachTmux } : {}
11833
12067
  });
11834
12068
  }
11835
12069
  return out;
@@ -11896,18 +12130,19 @@ class WebTerminalManager {
11896
12130
  try {
11897
12131
  const probe = node_child_process.spawnSync(
11898
12132
  "tmux",
11899
- tmuxArgs(["display-message", "-p", "-t", name, "#{pane_in_mode} #{alternate_on} #{mouse_any_flag} #{pane_width} #{pane_height} #{pane_current_command}"]),
12133
+ tmuxArgs(["display-message", "-p", "-t", name, ["#{pane_in_mode}", "#{alternate_on}", "#{mouse_any_flag}", "#{pane_width}", "#{pane_height}", "#{pane_current_command}"].join(LIST_FIELD_SEP)]),
11900
12134
  { encoding: "utf8", timeout: TMUX_PROBE_TIMEOUT_MS, env: ptyEnv() }
11901
12135
  );
11902
12136
  if (probe.status !== 0 || typeof probe.stdout !== "string") return;
11903
- const [inMode, altOn, wantsMouse, paneW, paneH, paneCmd] = probe.stdout.trim().split(" ");
12137
+ const [inMode, altOn, wantsMouse, paneW, paneH, paneCmd] = probe.stdout.trim().split(LIST_FIELD_SEP);
11904
12138
  const action = planScrollAction(
11905
12139
  inMode === "1",
11906
12140
  altOn === "1",
11907
12141
  wantsMouse === "1",
11908
12142
  lines,
11909
12143
  looksLikeClaudeCommand(paneCmd || ""),
11910
- Number(paneH) || 24
12144
+ Number(paneH) || 24,
12145
+ (paneCmd || "").trim() === "tmux"
11911
12146
  );
11912
12147
  if (action.kind === "none") return;
11913
12148
  if (action.kind === "mouse-wheel") {
@@ -13781,6 +14016,7 @@ class ApiMachineClient {
13781
14016
  lastKnownResumeSupport = null;
13782
14017
  cliUpdateState = null;
13783
14018
  cliUpdatePushChain = Promise.resolve();
14019
+ claudeAuthState = null;
13784
14020
  rpcHandlerManager;
13785
14021
  resumeSessionHandler = null;
13786
14022
  restartSessionHandler = null;
@@ -13847,6 +14083,44 @@ class ApiMachineClient {
13847
14083
  if (!this.socket?.connected) return;
13848
14084
  this.cliUpdatePushChain = this.cliUpdatePushChain.then(() => this.updateDaemonState((current) => withCurrentCliUpdateState(current, this.cliUpdateState))).catch((error) => logger.debug("[API MACHINE] Failed to publish CLI update policy:", error));
13849
14085
  }
14086
+ /**
14087
+ * B-276: publish the daemon-context Claude auth preflight into daemonState
14088
+ * (same serialized push chain as the CLI update policy). Resolves true when
14089
+ * the server acknowledged the write with our value in it, false otherwise
14090
+ * (rate-limit `result:'error'` is swallowed by updateDaemonState, so the
14091
+ * caller checks the echoed state and re-sends on the next probe).
14092
+ */
14093
+ setClaudeAuthState(state) {
14094
+ this.claudeAuthState = state;
14095
+ if (!this.socket?.connected) return Promise.resolve(false);
14096
+ const attempt = this.cliUpdatePushChain.then(() => this.updateDaemonState((current) => ({
14097
+ ...current ?? {},
14098
+ status: current?.status ?? "running",
14099
+ claudeAuth: state
14100
+ }))).then(() => this.machine.daemonState?.claudeAuth?.checkedAt === state.checkedAt && this.machine.daemonState?.claudeAuth?.daemonPid === state.daemonPid).catch((error) => {
14101
+ logger.debug("[API MACHINE] Failed to publish Claude auth state:", error);
14102
+ return false;
14103
+ });
14104
+ this.cliUpdatePushChain = attempt.then(() => void 0);
14105
+ return attempt;
14106
+ }
14107
+ /** B-276 machine RPCs. Responses always carry `claudeAuth` so the web can refresh in place. */
14108
+ setClaudeAuthHandlers(handlers) {
14109
+ this.rpcHandlerManager.registerHandler("claude-auth-probe", async () => {
14110
+ const claudeAuth = await handlers.probe();
14111
+ return { claudeAuth };
14112
+ });
14113
+ this.rpcHandlerManager.registerHandler("claude-auth-repair", async (params) => {
14114
+ const action = typeof params?.action === "string" ? params.action : "";
14115
+ return handlers.repair(action);
14116
+ });
14117
+ this.rpcHandlerManager.registerHandler("claude-auth-set-store", async (params) => {
14118
+ const store = params?.store === "file" ? "file" : params?.store === "auto" ? "auto" : null;
14119
+ if (!store) return { error: "invalid-store", claudeAuth: this.claudeAuthState };
14120
+ const claudeAuth = await handlers.setStore(store);
14121
+ return { claudeAuth };
14122
+ });
14123
+ }
13850
14124
  setRPCHandlers({
13851
14125
  spawnSession,
13852
14126
  resumeSession,
@@ -13880,7 +14154,8 @@ class ApiMachineClient {
13880
14154
  this.syncResumeSessionRpcRegistration();
13881
14155
  this.syncRestartSessionRpcRegistration();
13882
14156
  this.rpcHandlerManager.registerHandler("open-terminal", async (params) => {
13883
- const { terminalId, cols, rows, cwd, fromSeq, encStream, startupCommand, resub, attachOnly, streamMode } = params || {};
14157
+ const { terminalId, cols, rows, cwd, fromSeq, encStream, startupCommand, resub, attachOnly, streamMode, attachTmux } = params || {};
14158
+ const attach = attachTmux && typeof attachTmux === "object" && typeof attachTmux.id === "string" && typeof attachTmux.name === "string" ? { id: attachTmux.id, name: attachTmux.name } : void 0;
13884
14159
  const result = await this.webTerminal.open({
13885
14160
  terminalId,
13886
14161
  cols,
@@ -13890,7 +14165,8 @@ class ApiMachineClient {
13890
14165
  startupCommand,
13891
14166
  resub,
13892
14167
  attachOnly,
13893
- streamMode: streamMode === "lines" ? "lines" : void 0
14168
+ streamMode: streamMode === "lines" ? "lines" : void 0,
14169
+ ...attach ? { attachTmux: attach } : {}
13894
14170
  });
13895
14171
  if (encStream) {
13896
14172
  this.encTerminals.add(result.terminalId);
@@ -13965,6 +14241,9 @@ class ApiMachineClient {
13965
14241
  this.rpcHandlerManager.registerHandler("list-terminals", async () => {
13966
14242
  return { type: "success", terminals: this.webTerminal.buildTerminalList() };
13967
14243
  });
14244
+ this.rpcHandlerManager.registerHandler("list-tmux-sessions", async () => {
14245
+ return { type: "success", sessions: this.webTerminal.listUserTmuxSessions() };
14246
+ });
13968
14247
  this.rpcHandlerManager.registerHandler("set-terminal-title", async (params) => {
13969
14248
  const { terminalId, title, ifAbsent } = params || {};
13970
14249
  if (!terminalId || typeof title !== "string") {
@@ -14358,10 +14637,15 @@ class ApiMachineClient {
14358
14637
  // B-265 capability flag; restamped every connect so the
14359
14638
  // web's `detectedAt >= startedAt` trust rule holds.
14360
14639
  terminalRestore: { rpcAvailable: true, detectedAt: now },
14640
+ // B-273 capability flag (same restamp discipline).
14641
+ tmuxSessions: { rpcAvailable: true, detectedAt: now },
14361
14642
  // B-084: closed records survive daemon restarts (persisted
14362
14643
  // in closed-terminals.json), so the connect snapshot ships
14363
14644
  // them too — not just the incremental pushes.
14364
- closedTerminals: this.webTerminal.getClosedTerminals()
14645
+ closedTerminals: this.webTerminal.getClosedTerminals(),
14646
+ // B-276: re-ship the last preflight so a reconnect never
14647
+ // leaves a stale-pid value visible longer than needed.
14648
+ ...this.claudeAuthState ? { claudeAuth: this.claudeAuthState } : {}
14365
14649
  };
14366
14650
  });
14367
14651
  this.webTerminal.startListTracking((terminals) => this.pushTerminalList(terminals));
@@ -15764,6 +16048,7 @@ exports.releaseDaemonLock = releaseDaemonLock;
15764
16048
  exports.resolveCliUpdateCheckInterval = resolveCliUpdateCheckInterval;
15765
16049
  exports.scrubTmuxClientEnv = scrubTmuxClientEnv;
15766
16050
  exports.sendToVhTerminal = sendToVhTerminal;
16051
+ exports.shellescape = shellescape;
15767
16052
  exports.startOfflineReconnection = startOfflineReconnection;
15768
16053
  exports.summarizeSpawnSessionForLog = summarizeSpawnSessionForLog;
15769
16054
  exports.tmuxArgs = tmuxArgs;
@@ -15773,3 +16058,4 @@ exports.writeCredentialsDataKey = writeCredentialsDataKey;
15773
16058
  exports.writeCredentialsLegacy = writeCredentialsLegacy;
15774
16059
  exports.writeDaemonState = writeDaemonState;
15775
16060
  exports.writePrivateFileSync = writePrivateFileSync;
16061
+ exports.writeSettings = writeSettings;