very-happy-cli 0.2.97 → 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.
- package/dist/{AcpBackend-CTUM4dvG.mjs → AcpBackend-BXiGiFC7.mjs} +1 -1
- package/dist/{AcpBackend-DrwdVSeJ.cjs → AcpBackend-DhR8ml8L.cjs} +1 -1
- package/dist/{AcpSessionManager-BoBuHee3.mjs → AcpSessionManager-CDnrxAfQ.mjs} +1 -1
- package/dist/{AcpSessionManager-BieE187D.cjs → AcpSessionManager-Ckc0NFmf.cjs} +1 -1
- package/dist/{config-DCH33LOM.mjs → config-B3pA9YsN.mjs} +2 -2
- package/dist/{config-COq6X7U7.cjs → config-GJQc_kpD.cjs} +2 -2
- package/dist/{index-Dn0iOBFd.cjs → index-BBm2b34F.cjs} +4 -4
- package/dist/{index-DkhMvofH.mjs → index-Bh5Rcy0s.mjs} +5 -5
- package/dist/{index-BD2fHJ8o.cjs → index-CO1gNtuP.cjs} +18 -21
- package/dist/{index-Bcgquogx.mjs → index-CuUVSZnK.mjs} +12 -15
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{installTerminalHooks-H3kpFEZS.cjs → installTerminalHooks-Cp9JVxQ-.cjs} +2 -2
- package/dist/{installTerminalHooks-DqNtW6a2.mjs → installTerminalHooks-Ru0dn_zF.mjs} +2 -2
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +10 -0
- package/dist/lib.d.mts +10 -0
- package/dist/lib.mjs +1 -1
- package/dist/{mcp-BwNc5eRs.cjs → mcp-B5HMF24K.cjs} +2 -2
- package/dist/{mcp-BKAuzaLX.mjs → mcp-CQoVN0u2.mjs} +2 -2
- package/dist/{runGemini-w35qiFBB.cjs → runGemini-BBzPVzuc.cjs} +4 -4
- package/dist/{runGemini-Brp0UmLx.mjs → runGemini-CwZXQsgu.mjs} +4 -4
- package/dist/{runOpenClaw-CkHtTCpI.cjs → runOpenClaw-B4nC6XGL.cjs} +3 -3
- package/dist/{runOpenClaw-BvPRFx-L.mjs → runOpenClaw-C-jz4rWr.mjs} +3 -3
- package/dist/{send-tyKG13xt.cjs → send-CWgaINHC.cjs} +2 -2
- package/dist/{send-c-wEmh5U.mjs → send-DN-nVBEI.mjs} +2 -2
- package/dist/{spawn-xdCsG2RT.cjs → spawn-C0NSP84Y.cjs} +2 -2
- package/dist/{spawn-yS6yY8Xi.mjs → spawn-CqcfbOnk.mjs} +2 -2
- package/dist/{types-DoGaKVfK.cjs → types-BP-4phS_.cjs} +289 -70
- package/dist/{types-xZOYW0_G.mjs → types-Cl4vWuDY.mjs} +288 -70
- 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.
|
|
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.
|
|
168
|
-
"very-happy-tools-x64-darwin": "0.2.
|
|
169
|
-
"very-happy-tools-arm64-linux": "0.2.
|
|
170
|
-
"very-happy-tools-x64-linux": "0.2.
|
|
171
|
-
"very-happy-tools-arm64-win32": "0.2.
|
|
172
|
-
"very-happy-tools-x64-win32": "0.2.
|
|
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:*",
|
|
@@ -1041,7 +1041,9 @@ const WebTerminalListItemSchema = z.z.object({
|
|
|
1041
1041
|
* conversation carried over, the processes did not. Cleared once opened. */
|
|
1042
1042
|
restoredAt: z.z.number().optional(),
|
|
1043
1043
|
/** B-265: @vh_title_manual is set (user renamed it). */
|
|
1044
|
-
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()
|
|
1045
1047
|
});
|
|
1046
1048
|
const ClosedTerminalRecordSchema = z.z.object({
|
|
1047
1049
|
id: z.z.string(),
|
|
@@ -1058,6 +1060,8 @@ const ClosedTerminalRecordSchema = z.z.object({
|
|
|
1058
1060
|
/** B-265: tags + manual-rename flag at close time (restored verbatim). */
|
|
1059
1061
|
tags: z.z.array(z.z.string()).optional(),
|
|
1060
1062
|
manual: z.z.boolean().optional(),
|
|
1063
|
+
/** B-273: attached user tmux session name — restore re-attaches. */
|
|
1064
|
+
attachTmux: z.z.string().optional(),
|
|
1061
1065
|
closedAt: z.z.number()
|
|
1062
1066
|
});
|
|
1063
1067
|
z.z.object({
|
|
@@ -1111,6 +1115,16 @@ z.z.object({
|
|
|
1111
1115
|
rpcAvailable: z.z.boolean(),
|
|
1112
1116
|
detectedAt: z.z.number()
|
|
1113
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(),
|
|
1114
1128
|
/** Relay-owned CLI compatibility/update policy last checked by this daemon. */
|
|
1115
1129
|
cliUpdate: CliUpdateStateSchema.optional(),
|
|
1116
1130
|
/**
|
|
@@ -1569,7 +1583,7 @@ class RpcHandlerManager {
|
|
|
1569
1583
|
}
|
|
1570
1584
|
}
|
|
1571
1585
|
|
|
1572
|
-
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-
|
|
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))));
|
|
1573
1587
|
function projectPath() {
|
|
1574
1588
|
const path = path$1.resolve(__dirname$1, "..");
|
|
1575
1589
|
return path;
|
|
@@ -4812,7 +4826,7 @@ function sanitizeLiveSnapshot(raw, now, ttlMs = LIVE_SNAPSHOT_TTL_MS, max = LIVE
|
|
|
4812
4826
|
const rows = [];
|
|
4813
4827
|
for (const [id, value] of Object.entries(raw)) {
|
|
4814
4828
|
if (!id || !value || typeof value !== "object") continue;
|
|
4815
|
-
const { title, cwd, seenAt, tags, manual } = value;
|
|
4829
|
+
const { title, cwd, seenAt, tags, manual, attachTmux } = value;
|
|
4816
4830
|
if (typeof seenAt !== "number" || !Number.isFinite(seenAt)) continue;
|
|
4817
4831
|
if (now - seenAt >= ttlMs) continue;
|
|
4818
4832
|
const tagList = sanitizeTagList(tags);
|
|
@@ -4821,6 +4835,7 @@ function sanitizeLiveSnapshot(raw, now, ttlMs = LIVE_SNAPSHOT_TTL_MS, max = LIVE
|
|
|
4821
4835
|
cwd: typeof cwd === "string" && cwd ? cwd : void 0,
|
|
4822
4836
|
...tagList !== void 0 ? { tags: tagList } : {},
|
|
4823
4837
|
...manual === true ? { manual: true } : {},
|
|
4838
|
+
...typeof attachTmux === "string" && attachTmux ? { attachTmux } : {},
|
|
4824
4839
|
seenAt
|
|
4825
4840
|
}]);
|
|
4826
4841
|
}
|
|
@@ -4841,6 +4856,7 @@ function liveSnapshotChanged(prev, next) {
|
|
|
4841
4856
|
if (!before) return true;
|
|
4842
4857
|
if (before.title !== info.title || before.cwd !== info.cwd) return true;
|
|
4843
4858
|
if (!!before.manual !== !!info.manual) return true;
|
|
4859
|
+
if ((before.attachTmux ?? "") !== (info.attachTmux ?? "")) return true;
|
|
4844
4860
|
if ((before.tags ?? []).join("\0") !== (info.tags ?? []).join("\0")) return true;
|
|
4845
4861
|
}
|
|
4846
4862
|
return false;
|
|
@@ -4866,9 +4882,89 @@ function pickMirrorForTerminal(sessions, terminalId) {
|
|
|
4866
4882
|
return best ? { sessionId: best.id, claudeSessionId: best.claude } : null;
|
|
4867
4883
|
}
|
|
4868
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
|
+
|
|
4869
4949
|
const TERMINAL_ID_RE$1 = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
4870
4950
|
function planTerminalRestore(record, facts) {
|
|
4871
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
|
+
}
|
|
4872
4968
|
if (!record.cwd || !facts.cwdExists(record.cwd)) return { kind: "error", reason: "missing-cwd" };
|
|
4873
4969
|
const resumable = isClaudeSessionId(record.claudeSessionId) && facts.conversationExists(record.cwd, record.claudeSessionId);
|
|
4874
4970
|
return {
|
|
@@ -4882,6 +4978,20 @@ function planTerminalRestore(record, facts) {
|
|
|
4882
4978
|
};
|
|
4883
4979
|
}
|
|
4884
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
|
+
|
|
4885
4995
|
function getProjectPath(workingDirectory) {
|
|
4886
4996
|
const projectId = path.resolve(workingDirectory).replace(/[^a-zA-Z0-9-]/g, "-");
|
|
4887
4997
|
const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
|
|
@@ -9215,20 +9325,6 @@ function previewAscii(line) {
|
|
|
9215
9325
|
return line.length > 80 ? `${out}\u2026` : out;
|
|
9216
9326
|
}
|
|
9217
9327
|
|
|
9218
|
-
const VH_TMUX_SOCKET_ENV = "VH_TMUX_SOCKET";
|
|
9219
|
-
function tmuxSocketFlags(env = process.env) {
|
|
9220
|
-
const socket = env[VH_TMUX_SOCKET_ENV];
|
|
9221
|
-
return socket ? ["-S", socket] : [];
|
|
9222
|
-
}
|
|
9223
|
-
function tmuxArgs(args, env = process.env) {
|
|
9224
|
-
return [...tmuxSocketFlags(env), ...args];
|
|
9225
|
-
}
|
|
9226
|
-
function scrubTmuxClientEnv(env) {
|
|
9227
|
-
delete env.TMUX;
|
|
9228
|
-
delete env.TMUX_PANE;
|
|
9229
|
-
return env;
|
|
9230
|
-
}
|
|
9231
|
-
|
|
9232
9328
|
const CONTROL_CLIENT_KILL_GRACE_MS = 2e3;
|
|
9233
9329
|
const CONTROL_COMMAND_TIMEOUT_MS = 1e4;
|
|
9234
9330
|
function isSafeControlCommand(command) {
|
|
@@ -10046,7 +10142,7 @@ function sanitizeClosedTerminals(raw, max = CLOSED_TERMINALS_MAX) {
|
|
|
10046
10142
|
const out = [];
|
|
10047
10143
|
for (const item of raw) {
|
|
10048
10144
|
if (!item || typeof item !== "object") continue;
|
|
10049
|
-
const { id, title, cwd, mirrorSessionId, claudeSessionId, reason, closedAt, tags, manual } = item;
|
|
10145
|
+
const { id, title, cwd, mirrorSessionId, claudeSessionId, reason, closedAt, tags, manual, attachTmux } = item;
|
|
10050
10146
|
if (typeof id !== "string" || id.length === 0 || typeof closedAt !== "number") continue;
|
|
10051
10147
|
if (seen.has(id)) continue;
|
|
10052
10148
|
seen.add(id);
|
|
@@ -10060,6 +10156,7 @@ function sanitizeClosedTerminals(raw, max = CLOSED_TERMINALS_MAX) {
|
|
|
10060
10156
|
reason: reason === "daemon-gap" || reason === "closed" ? reason : void 0,
|
|
10061
10157
|
...tagList !== void 0 ? { tags: tagList } : {},
|
|
10062
10158
|
...manual === true ? { manual: true } : {},
|
|
10159
|
+
...typeof attachTmux === "string" && attachTmux ? { attachTmux } : {},
|
|
10063
10160
|
closedAt
|
|
10064
10161
|
});
|
|
10065
10162
|
}
|
|
@@ -10184,7 +10281,7 @@ function startupInjectionArgs(tmuxSession, command) {
|
|
|
10184
10281
|
];
|
|
10185
10282
|
}
|
|
10186
10283
|
function seenInfoOf(t) {
|
|
10187
|
-
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 };
|
|
10188
10285
|
}
|
|
10189
10286
|
function terminalListSignature(items) {
|
|
10190
10287
|
const canon = [...items].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).map((t) => [
|
|
@@ -10197,7 +10294,8 @@ function terminalListSignature(items) {
|
|
|
10197
10294
|
t.agentState ?? "",
|
|
10198
10295
|
// B-105: a mirror binding appearing/disappearing MUST push the
|
|
10199
10296
|
// list, or the web never learns the toggle became available.
|
|
10200
|
-
t.mirrorSessionId ?? ""
|
|
10297
|
+
t.mirrorSessionId ?? "",
|
|
10298
|
+
t.attachTmux ?? ""
|
|
10201
10299
|
]);
|
|
10202
10300
|
return JSON.stringify(canon);
|
|
10203
10301
|
}
|
|
@@ -10216,7 +10314,7 @@ function deriveAutoTitle(paneTitle, hostname) {
|
|
|
10216
10314
|
const chars = Array.from(t);
|
|
10217
10315
|
return chars.length > TITLE_MAX_CHARS ? chars.slice(0, TITLE_MAX_CHARS).join("") : t;
|
|
10218
10316
|
}
|
|
10219
|
-
const LIST_FIELD_SEP = "
|
|
10317
|
+
const LIST_FIELD_SEP = "<~|~>";
|
|
10220
10318
|
const LIST_SESSIONS_FORMAT = [
|
|
10221
10319
|
"#{session_name}",
|
|
10222
10320
|
"#{session_created}",
|
|
@@ -10225,6 +10323,9 @@ const LIST_SESSIONS_FORMAT = [
|
|
|
10225
10323
|
"#{@vh_title}",
|
|
10226
10324
|
"#{@vh_title_manual}",
|
|
10227
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}",
|
|
10228
10329
|
// B-121: the control-mode client has no `pty.process`, so the agent-state
|
|
10229
10330
|
// fast path lost its live `#{pane_current_command}` equivalent. Carry it in
|
|
10230
10331
|
// the ONE list-sessions call the tracker already makes — the value goes
|
|
@@ -10239,8 +10340,8 @@ const LIST_SESSIONS_FORMAT = [
|
|
|
10239
10340
|
function parseSessionListLine(line) {
|
|
10240
10341
|
if (!line) return void 0;
|
|
10241
10342
|
const parts = line.split(LIST_FIELD_SEP);
|
|
10242
|
-
if (parts.length <
|
|
10243
|
-
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;
|
|
10244
10345
|
if (!name) return void 0;
|
|
10245
10346
|
return {
|
|
10246
10347
|
name,
|
|
@@ -10251,9 +10352,12 @@ function parseSessionListLine(line) {
|
|
|
10251
10352
|
manual: manual.trim().length > 0,
|
|
10252
10353
|
tags: parseTerminalTags(vhTags),
|
|
10253
10354
|
paneCurrentCommand: paneCommand.trim() || void 0,
|
|
10254
|
-
//
|
|
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
|
|
10255
10359
|
// contained the separator — rejoin it rather than dropping it.
|
|
10256
|
-
paneTitle: parts.slice(
|
|
10360
|
+
paneTitle: parts.slice(9).join(LIST_FIELD_SEP) || void 0
|
|
10257
10361
|
};
|
|
10258
10362
|
}
|
|
10259
10363
|
const TERMINAL_TAG_MAX_COUNT = 64;
|
|
@@ -10310,12 +10414,12 @@ function isClaudeConfident(tail) {
|
|
|
10310
10414
|
const last15 = lines.slice(-15).join("\n");
|
|
10311
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");
|
|
10312
10416
|
}
|
|
10313
|
-
function planScrollAction(paneInMode, alternateOn, paneWantsMouse, lines, claudeLike = false, paneRows = 24) {
|
|
10417
|
+
function planScrollAction(paneInMode, alternateOn, paneWantsMouse, lines, claudeLike = false, paneRows = 24, nestedTmux = false) {
|
|
10314
10418
|
const count = Math.min(Math.abs(Math.trunc(lines)), 200);
|
|
10315
10419
|
if (count === 0) return { kind: "none" };
|
|
10316
10420
|
const up = lines > 0;
|
|
10317
10421
|
if (paneInMode) return { kind: "copy-scroll", dir: up ? "up" : "down", count };
|
|
10318
|
-
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 };
|
|
10319
10423
|
if (alternateOn && claudeLike) {
|
|
10320
10424
|
const halfPage = Math.max(1, Math.floor(paneRows / 2));
|
|
10321
10425
|
const pages = Math.max(1, Math.round(count / halfPage));
|
|
@@ -10430,6 +10534,9 @@ class TerminalSession {
|
|
|
10430
10534
|
id;
|
|
10431
10535
|
tmuxSession;
|
|
10432
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;
|
|
10433
10540
|
/**
|
|
10434
10541
|
* The pane this terminal follows, latched from the open capture's
|
|
10435
10542
|
* `list-panes` (first pane of the window). `%output` from any OTHER pane is
|
|
@@ -10940,6 +11047,7 @@ class WebTerminalManager {
|
|
|
10940
11047
|
cwd: info.cwd,
|
|
10941
11048
|
tags: info.tags,
|
|
10942
11049
|
manual: info.manual,
|
|
11050
|
+
...info.attachTmux ? { attachTmux: info.attachTmux } : {},
|
|
10943
11051
|
// Live binding first (authoritative while it exists), persisted
|
|
10944
11052
|
// metadata as the fallback once the mirror has been torn down.
|
|
10945
11053
|
mirrorSessionId: this.mirrorResolver?.(id) ?? mirror?.sessionId,
|
|
@@ -10992,6 +11100,7 @@ class WebTerminalManager {
|
|
|
10992
11100
|
cwd: info.cwd,
|
|
10993
11101
|
tags: info.tags,
|
|
10994
11102
|
manual: info.manual,
|
|
11103
|
+
...info.attachTmux ? { attachTmux: info.attachTmux } : {},
|
|
10995
11104
|
mirrorSessionId: mirror?.sessionId,
|
|
10996
11105
|
claudeSessionId: mirror?.claudeSessionId,
|
|
10997
11106
|
reason: "daemon-gap",
|
|
@@ -11111,12 +11220,17 @@ class WebTerminalManager {
|
|
|
11111
11220
|
return false;
|
|
11112
11221
|
}
|
|
11113
11222
|
},
|
|
11114
|
-
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()
|
|
11115
11229
|
});
|
|
11116
11230
|
if (plan.kind === "already-live") return { type: "success", terminalId };
|
|
11117
11231
|
if (plan.kind === "error") return { type: "error", reason: plan.reason };
|
|
11118
11232
|
if (!this.createDetachedTerminal(plan)) return { type: "error", reason: "create-failed" };
|
|
11119
|
-
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)" : ""}`);
|
|
11120
11234
|
this.kickListRefresh();
|
|
11121
11235
|
return { type: "success", terminalId };
|
|
11122
11236
|
}
|
|
@@ -11176,6 +11290,13 @@ class WebTerminalManager {
|
|
|
11176
11290
|
{ stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
11177
11291
|
);
|
|
11178
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
|
+
}
|
|
11179
11300
|
if (plan.command) {
|
|
11180
11301
|
for (const argv of startupInjectionArgs(name, plan.command)) {
|
|
11181
11302
|
node_child_process.spawnSync("tmux", tmuxArgs(argv), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env });
|
|
@@ -11188,7 +11309,7 @@ class WebTerminalManager {
|
|
|
11188
11309
|
* would care about changed (see liveSnapshotChanged). */
|
|
11189
11310
|
persistLiveSnapshot(live, now) {
|
|
11190
11311
|
const candidate = /* @__PURE__ */ new Map();
|
|
11191
|
-
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 });
|
|
11192
11313
|
if (!liveSnapshotChanged(this.lastPersistedSnapshot, candidate)) return;
|
|
11193
11314
|
this.lastPersistedSnapshot = candidate;
|
|
11194
11315
|
saveLiveSnapshot(candidate);
|
|
@@ -11286,6 +11407,7 @@ class WebTerminalManager {
|
|
|
11286
11407
|
const lines = opts.streamMode === "lines";
|
|
11287
11408
|
const existing = this.terminals.get(id);
|
|
11288
11409
|
if (existing) {
|
|
11410
|
+
const echo2 = opts.attachTmux && !(opts.attachOnly || opts.resub) && existing.attachedTmux?.name === opts.attachTmux.name ? { attachedTmux: existing.attachedTmux } : {};
|
|
11289
11411
|
if (!opts.resub || existing.subscribers === 0) existing.subscribers += 1;
|
|
11290
11412
|
existing.lastTouch = Date.now();
|
|
11291
11413
|
this.applyResize(existing, cols, rows);
|
|
@@ -11298,15 +11420,16 @@ class WebTerminalManager {
|
|
|
11298
11420
|
seq: existing.seq,
|
|
11299
11421
|
mode: "replay",
|
|
11300
11422
|
chunks,
|
|
11301
|
-
...lines ? { streamMode: "lines" } : {}
|
|
11423
|
+
...lines ? { streamMode: "lines" } : {},
|
|
11424
|
+
...echo2
|
|
11302
11425
|
};
|
|
11303
11426
|
}
|
|
11304
11427
|
if (lines && existing.transport.kind === "control") {
|
|
11305
|
-
return await this.linesSnapshotResponse(id, existing);
|
|
11428
|
+
return { ...await this.linesSnapshotResponse(id, existing), ...echo2 };
|
|
11306
11429
|
}
|
|
11307
11430
|
const state = existing.subscribeState(opts.fromSeq);
|
|
11308
11431
|
logger.debug(`[WEB TERMINAL] re-subscribed ${id} (subs=${existing.subscribers}, mode=${state.mode}, seq=${state.seq})`);
|
|
11309
|
-
return { terminalId: id, tmuxSession: existing.tmuxSession, ...state };
|
|
11432
|
+
return { terminalId: id, tmuxSession: existing.tmuxSession, ...state, ...echo2 };
|
|
11310
11433
|
}
|
|
11311
11434
|
if (opts.attachOnly || opts.resub) {
|
|
11312
11435
|
const alive = isTmuxAvailable() && node_child_process.spawnSync(
|
|
@@ -11331,6 +11454,10 @@ class WebTerminalManager {
|
|
|
11331
11454
|
let file = defaultShell();
|
|
11332
11455
|
let args = [];
|
|
11333
11456
|
let tmuxSession;
|
|
11457
|
+
let attachedTmux;
|
|
11458
|
+
if (opts.attachTmux && !(opts.attachOnly || opts.resub) && !isTmuxAvailable()) {
|
|
11459
|
+
throw new Error("tmux-unavailable");
|
|
11460
|
+
}
|
|
11334
11461
|
if (isTmuxAvailable()) {
|
|
11335
11462
|
tmuxSession = `vh-${id}`;
|
|
11336
11463
|
let createdNew = false;
|
|
@@ -11343,6 +11470,7 @@ class WebTerminalManager {
|
|
|
11343
11470
|
`VH_HAPPY_HOME_DIR=${configuration.happyHomeDir}`
|
|
11344
11471
|
] : [];
|
|
11345
11472
|
const attachOnly = !!(opts.attachOnly || opts.resub);
|
|
11473
|
+
const attachTarget = !attachOnly && opts.attachTmux ? this.resolveAttachTarget(opts.attachTmux) : void 0;
|
|
11346
11474
|
if (!attachOnly) {
|
|
11347
11475
|
try {
|
|
11348
11476
|
const created = node_child_process.spawnSync(
|
|
@@ -11384,16 +11512,15 @@ class WebTerminalManager {
|
|
|
11384
11512
|
}
|
|
11385
11513
|
}
|
|
11386
11514
|
if (createdNew) {
|
|
11387
|
-
|
|
11388
|
-
|
|
11389
|
-
|
|
11390
|
-
|
|
11391
|
-
|
|
11392
|
-
|
|
11393
|
-
|
|
11394
|
-
|
|
11395
|
-
|
|
11396
|
-
}
|
|
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 };
|
|
11397
11524
|
}
|
|
11398
11525
|
if (!attachOnly && !createdNew) {
|
|
11399
11526
|
const alive = node_child_process.spawnSync(
|
|
@@ -11408,17 +11535,7 @@ class WebTerminalManager {
|
|
|
11408
11535
|
{ stdio: "ignore", timeout: TMUX_CREATE_TIMEOUT_MS, env }
|
|
11409
11536
|
);
|
|
11410
11537
|
createdNew = retry.status === 0;
|
|
11411
|
-
if (createdNew)
|
|
11412
|
-
const startup = normalizeStartupCommand(opts.startupCommand);
|
|
11413
|
-
if (startup) {
|
|
11414
|
-
for (const a of startupInjectionArgs(tmuxSession, startup)) {
|
|
11415
|
-
try {
|
|
11416
|
-
node_child_process.spawnSync("tmux", tmuxArgs(a), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env });
|
|
11417
|
-
} catch {
|
|
11418
|
-
}
|
|
11419
|
-
}
|
|
11420
|
-
}
|
|
11421
|
-
}
|
|
11538
|
+
if (createdNew) attachedTmux = this.injectIntoCreated(tmuxSession, attachTarget, opts.startupCommand, env);
|
|
11422
11539
|
}
|
|
11423
11540
|
}
|
|
11424
11541
|
if (!attachOnly && !createdNew) {
|
|
@@ -11428,6 +11545,7 @@ class WebTerminalManager {
|
|
|
11428
11545
|
{ stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
11429
11546
|
).status === 0;
|
|
11430
11547
|
if (!alive) {
|
|
11548
|
+
if (attachTarget) throw new Error("tmux-unavailable");
|
|
11431
11549
|
logger.info(`[WEB TERMINAL] tmux session creation unavailable; using direct PTY for ${id}`);
|
|
11432
11550
|
tmuxSession = void 0;
|
|
11433
11551
|
}
|
|
@@ -11510,20 +11628,110 @@ class WebTerminalManager {
|
|
|
11510
11628
|
session = created;
|
|
11511
11629
|
}
|
|
11512
11630
|
session.subscribers = 1;
|
|
11631
|
+
if (attachedTmux) session.attachedTmux = attachedTmux;
|
|
11513
11632
|
this.terminals.set(id, session);
|
|
11514
11633
|
session.onTitleChange(() => this.kickListRefresh());
|
|
11515
11634
|
logger.debug(`[WEB TERMINAL] opened ${id} (${tmuxSession ? "control" : "pty"}) ${cols}x${rows} cwd=${cwd}`);
|
|
11516
11635
|
this.kickListRefresh();
|
|
11636
|
+
const echo = attachedTmux ? { attachedTmux } : {};
|
|
11517
11637
|
if (session.transport.kind !== "control") {
|
|
11518
11638
|
const state = session.subscribeState(void 0);
|
|
11519
|
-
return { terminalId: id, tmuxSession, ...state, ...lines ? { streamMode: "lines" } : {} };
|
|
11639
|
+
return { terminalId: id, tmuxSession, ...state, ...lines ? { streamMode: "lines" } : {}, ...echo };
|
|
11520
11640
|
}
|
|
11521
11641
|
const restored = await this.captureRestore(session, true);
|
|
11522
11642
|
if (!lines) {
|
|
11523
11643
|
const state = session.subscribeState(void 0);
|
|
11524
|
-
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 [];
|
|
11525
11734
|
}
|
|
11526
|
-
return this.linesResponse(id, session, restored);
|
|
11527
11735
|
}
|
|
11528
11736
|
/**
|
|
11529
11737
|
* Run the opening/catch-up capture for a control session and prime the
|
|
@@ -11765,6 +11973,7 @@ class WebTerminalManager {
|
|
|
11765
11973
|
cwd: info.cwd,
|
|
11766
11974
|
tags: info.tags,
|
|
11767
11975
|
manual: info.manual,
|
|
11976
|
+
...info.attachTmux ? { attachTmux: info.attachTmux } : {},
|
|
11768
11977
|
mirrorSessionId: this.mirrorResolver?.(terminalId) ?? mirror?.sessionId,
|
|
11769
11978
|
claudeSessionId: mirror?.claudeSessionId,
|
|
11770
11979
|
reason: "closed",
|
|
@@ -11853,7 +12062,8 @@ class WebTerminalManager {
|
|
|
11853
12062
|
// Daemon-internal (not pushed to web / not in the signature):
|
|
11854
12063
|
// the mirror reconciler's stricter "claude is really here" gate.
|
|
11855
12064
|
claudeConfident: probe.claudeConfident,
|
|
11856
|
-
manual: s.manual
|
|
12065
|
+
manual: s.manual,
|
|
12066
|
+
...s.attachTmux ? { attachTmux: s.attachTmux } : {}
|
|
11857
12067
|
});
|
|
11858
12068
|
}
|
|
11859
12069
|
return out;
|
|
@@ -11920,18 +12130,19 @@ class WebTerminalManager {
|
|
|
11920
12130
|
try {
|
|
11921
12131
|
const probe = node_child_process.spawnSync(
|
|
11922
12132
|
"tmux",
|
|
11923
|
-
tmuxArgs(["display-message", "-p", "-t", name, "#{pane_in_mode}
|
|
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)]),
|
|
11924
12134
|
{ encoding: "utf8", timeout: TMUX_PROBE_TIMEOUT_MS, env: ptyEnv() }
|
|
11925
12135
|
);
|
|
11926
12136
|
if (probe.status !== 0 || typeof probe.stdout !== "string") return;
|
|
11927
|
-
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);
|
|
11928
12138
|
const action = planScrollAction(
|
|
11929
12139
|
inMode === "1",
|
|
11930
12140
|
altOn === "1",
|
|
11931
12141
|
wantsMouse === "1",
|
|
11932
12142
|
lines,
|
|
11933
12143
|
looksLikeClaudeCommand(paneCmd || ""),
|
|
11934
|
-
Number(paneH) || 24
|
|
12144
|
+
Number(paneH) || 24,
|
|
12145
|
+
(paneCmd || "").trim() === "tmux"
|
|
11935
12146
|
);
|
|
11936
12147
|
if (action.kind === "none") return;
|
|
11937
12148
|
if (action.kind === "mouse-wheel") {
|
|
@@ -13943,7 +14154,8 @@ class ApiMachineClient {
|
|
|
13943
14154
|
this.syncResumeSessionRpcRegistration();
|
|
13944
14155
|
this.syncRestartSessionRpcRegistration();
|
|
13945
14156
|
this.rpcHandlerManager.registerHandler("open-terminal", async (params) => {
|
|
13946
|
-
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;
|
|
13947
14159
|
const result = await this.webTerminal.open({
|
|
13948
14160
|
terminalId,
|
|
13949
14161
|
cols,
|
|
@@ -13953,7 +14165,8 @@ class ApiMachineClient {
|
|
|
13953
14165
|
startupCommand,
|
|
13954
14166
|
resub,
|
|
13955
14167
|
attachOnly,
|
|
13956
|
-
streamMode: streamMode === "lines" ? "lines" : void 0
|
|
14168
|
+
streamMode: streamMode === "lines" ? "lines" : void 0,
|
|
14169
|
+
...attach ? { attachTmux: attach } : {}
|
|
13957
14170
|
});
|
|
13958
14171
|
if (encStream) {
|
|
13959
14172
|
this.encTerminals.add(result.terminalId);
|
|
@@ -14028,6 +14241,9 @@ class ApiMachineClient {
|
|
|
14028
14241
|
this.rpcHandlerManager.registerHandler("list-terminals", async () => {
|
|
14029
14242
|
return { type: "success", terminals: this.webTerminal.buildTerminalList() };
|
|
14030
14243
|
});
|
|
14244
|
+
this.rpcHandlerManager.registerHandler("list-tmux-sessions", async () => {
|
|
14245
|
+
return { type: "success", sessions: this.webTerminal.listUserTmuxSessions() };
|
|
14246
|
+
});
|
|
14031
14247
|
this.rpcHandlerManager.registerHandler("set-terminal-title", async (params) => {
|
|
14032
14248
|
const { terminalId, title, ifAbsent } = params || {};
|
|
14033
14249
|
if (!terminalId || typeof title !== "string") {
|
|
@@ -14421,6 +14637,8 @@ class ApiMachineClient {
|
|
|
14421
14637
|
// B-265 capability flag; restamped every connect so the
|
|
14422
14638
|
// web's `detectedAt >= startedAt` trust rule holds.
|
|
14423
14639
|
terminalRestore: { rpcAvailable: true, detectedAt: now },
|
|
14640
|
+
// B-273 capability flag (same restamp discipline).
|
|
14641
|
+
tmuxSessions: { rpcAvailable: true, detectedAt: now },
|
|
14424
14642
|
// B-084: closed records survive daemon restarts (persisted
|
|
14425
14643
|
// in closed-terminals.json), so the connect snapshot ships
|
|
14426
14644
|
// them too — not just the incremental pushes.
|
|
@@ -15830,6 +16048,7 @@ exports.releaseDaemonLock = releaseDaemonLock;
|
|
|
15830
16048
|
exports.resolveCliUpdateCheckInterval = resolveCliUpdateCheckInterval;
|
|
15831
16049
|
exports.scrubTmuxClientEnv = scrubTmuxClientEnv;
|
|
15832
16050
|
exports.sendToVhTerminal = sendToVhTerminal;
|
|
16051
|
+
exports.shellescape = shellescape;
|
|
15833
16052
|
exports.startOfflineReconnection = startOfflineReconnection;
|
|
15834
16053
|
exports.summarizeSpawnSessionForLog = summarizeSpawnSessionForLog;
|
|
15835
16054
|
exports.tmuxArgs = tmuxArgs;
|