very-happy-cli 0.2.97 → 0.2.99
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-C9LtzYSx.mjs} +1 -1
- package/dist/{AcpBackend-DrwdVSeJ.cjs → AcpBackend-DkvV2nhl.cjs} +1 -1
- package/dist/{AcpSessionManager-BoBuHee3.mjs → AcpSessionManager-B5rJcE82.mjs} +1 -1
- package/dist/{AcpSessionManager-BieE187D.cjs → AcpSessionManager-gsp_-PGy.cjs} +1 -1
- package/dist/{config-DCH33LOM.mjs → config-BcCVR-Wx.mjs} +2 -2
- package/dist/{config-COq6X7U7.cjs → config-Bidi0Yeq.cjs} +2 -2
- package/dist/{index-Bcgquogx.mjs → index-B87z2DOo.mjs} +12 -15
- package/dist/{index-Dn0iOBFd.cjs → index-CxdGnaov.cjs} +4 -4
- package/dist/{index-BD2fHJ8o.cjs → index-Dp1EYEyF.cjs} +18 -21
- package/dist/{index-DkhMvofH.mjs → index-Wfds4scC.mjs} +5 -5
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{installTerminalHooks-DqNtW6a2.mjs → installTerminalHooks-DkOh088o.mjs} +2 -2
- package/dist/{installTerminalHooks-H3kpFEZS.cjs → installTerminalHooks-jaCruGfR.cjs} +2 -2
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +11 -0
- package/dist/lib.d.mts +11 -0
- package/dist/lib.mjs +1 -1
- package/dist/{mcp-BKAuzaLX.mjs → mcp-DhftF_XE.mjs} +2 -2
- package/dist/{mcp-BwNc5eRs.cjs → mcp-ZQbu8tL1.cjs} +2 -2
- package/dist/{runGemini-Brp0UmLx.mjs → runGemini-3-xWWBRA.mjs} +4 -4
- package/dist/{runGemini-w35qiFBB.cjs → runGemini-Ce7Maf-D.cjs} +4 -4
- package/dist/{runOpenClaw-BvPRFx-L.mjs → runOpenClaw-DZhGhihh.mjs} +3 -3
- package/dist/{runOpenClaw-CkHtTCpI.cjs → runOpenClaw-q3inEsDr.cjs} +3 -3
- package/dist/{send-c-wEmh5U.mjs → send-B3NOqi9-.mjs} +2 -2
- package/dist/{send-tyKG13xt.cjs → send-BdEJeVye.cjs} +2 -2
- package/dist/{spawn-yS6yY8Xi.mjs → spawn-CCYAeBfN.mjs} +2 -2
- package/dist/{spawn-xdCsG2RT.cjs → spawn-DEVUh4iW.cjs} +2 -2
- package/dist/{types-xZOYW0_G.mjs → types-CrJj-daH.mjs} +302 -73
- package/dist/{types-DoGaKVfK.cjs → types-DhxxJFD0.cjs} +303 -73
- 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.
|
|
32
|
+
var version = "0.2.99";
|
|
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.
|
|
146
|
-
"very-happy-tools-x64-darwin": "0.2.
|
|
147
|
-
"very-happy-tools-arm64-linux": "0.2.
|
|
148
|
-
"very-happy-tools-x64-linux": "0.2.
|
|
149
|
-
"very-happy-tools-arm64-win32": "0.2.
|
|
150
|
-
"very-happy-tools-x64-win32": "0.2.
|
|
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"
|
|
151
151
|
};
|
|
152
152
|
var devDependencies = {
|
|
153
153
|
"@slopus/happy-wire": "workspace:*",
|
|
@@ -1019,7 +1019,9 @@ const WebTerminalListItemSchema = z$1.object({
|
|
|
1019
1019
|
* conversation carried over, the processes did not. Cleared once opened. */
|
|
1020
1020
|
restoredAt: z$1.number().optional(),
|
|
1021
1021
|
/** B-265: @vh_title_manual is set (user renamed it). */
|
|
1022
|
-
manual: z$1.boolean().optional()
|
|
1022
|
+
manual: z$1.boolean().optional(),
|
|
1023
|
+
/** B-273: opened attached to the user's tmux session of this name. */
|
|
1024
|
+
attachTmux: z$1.string().optional()
|
|
1023
1025
|
});
|
|
1024
1026
|
const ClosedTerminalRecordSchema = z$1.object({
|
|
1025
1027
|
id: z$1.string(),
|
|
@@ -1036,6 +1038,8 @@ const ClosedTerminalRecordSchema = z$1.object({
|
|
|
1036
1038
|
/** B-265: tags + manual-rename flag at close time (restored verbatim). */
|
|
1037
1039
|
tags: z$1.array(z$1.string()).optional(),
|
|
1038
1040
|
manual: z$1.boolean().optional(),
|
|
1041
|
+
/** B-273: attached user tmux session name — restore re-attaches. */
|
|
1042
|
+
attachTmux: z$1.string().optional(),
|
|
1039
1043
|
closedAt: z$1.number()
|
|
1040
1044
|
});
|
|
1041
1045
|
z$1.object({
|
|
@@ -1089,6 +1093,18 @@ z$1.object({
|
|
|
1089
1093
|
rpcAvailable: z$1.boolean(),
|
|
1090
1094
|
detectedAt: z$1.number()
|
|
1091
1095
|
}).optional(),
|
|
1096
|
+
/**
|
|
1097
|
+
* B-273 capability flag: this daemon answers `list-tmux-sessions` and
|
|
1098
|
+
* honours `open-terminal.attachTmux`. Same trust rule (`detectedAt >=
|
|
1099
|
+
* startedAt`). Stamped unconditionally — on a machine without tmux the
|
|
1100
|
+
* RPC just answers an empty list.
|
|
1101
|
+
*/
|
|
1102
|
+
tmuxSessions: z$1.object({
|
|
1103
|
+
rpcAvailable: z$1.boolean(),
|
|
1104
|
+
detectedAt: z$1.number(),
|
|
1105
|
+
/** B-282: `kill-terminal` honours `alsoAttached`. */
|
|
1106
|
+
killAttached: z$1.boolean().optional()
|
|
1107
|
+
}).optional(),
|
|
1092
1108
|
/** Relay-owned CLI compatibility/update policy last checked by this daemon. */
|
|
1093
1109
|
cliUpdate: CliUpdateStateSchema.optional(),
|
|
1094
1110
|
/**
|
|
@@ -4790,7 +4806,7 @@ function sanitizeLiveSnapshot(raw, now, ttlMs = LIVE_SNAPSHOT_TTL_MS, max = LIVE
|
|
|
4790
4806
|
const rows = [];
|
|
4791
4807
|
for (const [id, value] of Object.entries(raw)) {
|
|
4792
4808
|
if (!id || !value || typeof value !== "object") continue;
|
|
4793
|
-
const { title, cwd, seenAt, tags, manual } = value;
|
|
4809
|
+
const { title, cwd, seenAt, tags, manual, attachTmux } = value;
|
|
4794
4810
|
if (typeof seenAt !== "number" || !Number.isFinite(seenAt)) continue;
|
|
4795
4811
|
if (now - seenAt >= ttlMs) continue;
|
|
4796
4812
|
const tagList = sanitizeTagList(tags);
|
|
@@ -4799,6 +4815,7 @@ function sanitizeLiveSnapshot(raw, now, ttlMs = LIVE_SNAPSHOT_TTL_MS, max = LIVE
|
|
|
4799
4815
|
cwd: typeof cwd === "string" && cwd ? cwd : void 0,
|
|
4800
4816
|
...tagList !== void 0 ? { tags: tagList } : {},
|
|
4801
4817
|
...manual === true ? { manual: true } : {},
|
|
4818
|
+
...typeof attachTmux === "string" && attachTmux ? { attachTmux } : {},
|
|
4802
4819
|
seenAt
|
|
4803
4820
|
}]);
|
|
4804
4821
|
}
|
|
@@ -4819,6 +4836,7 @@ function liveSnapshotChanged(prev, next) {
|
|
|
4819
4836
|
if (!before) return true;
|
|
4820
4837
|
if (before.title !== info.title || before.cwd !== info.cwd) return true;
|
|
4821
4838
|
if (!!before.manual !== !!info.manual) return true;
|
|
4839
|
+
if ((before.attachTmux ?? "") !== (info.attachTmux ?? "")) return true;
|
|
4822
4840
|
if ((before.tags ?? []).join("\0") !== (info.tags ?? []).join("\0")) return true;
|
|
4823
4841
|
}
|
|
4824
4842
|
return false;
|
|
@@ -4844,9 +4862,89 @@ function pickMirrorForTerminal(sessions, terminalId) {
|
|
|
4844
4862
|
return best ? { sessionId: best.id, claudeSessionId: best.claude } : null;
|
|
4845
4863
|
}
|
|
4846
4864
|
|
|
4865
|
+
function shellescape(s) {
|
|
4866
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
4867
|
+
}
|
|
4868
|
+
|
|
4869
|
+
const USER_SESSION_FIELD_SEP = "<~|~>";
|
|
4870
|
+
const USER_SESSIONS_FORMAT = [
|
|
4871
|
+
"#{session_id}",
|
|
4872
|
+
"#{session_name}",
|
|
4873
|
+
"#{session_windows}",
|
|
4874
|
+
"#{session_attached}",
|
|
4875
|
+
"#{session_activity}",
|
|
4876
|
+
"#{session_created}",
|
|
4877
|
+
"#{pane_current_command}",
|
|
4878
|
+
"#{pane_current_path}"
|
|
4879
|
+
].join(USER_SESSION_FIELD_SEP);
|
|
4880
|
+
const TMUX_SESSION_ID_RE = /^\$\d{1,9}$/;
|
|
4881
|
+
const TMUX_SESSION_NAME_MAX = 128;
|
|
4882
|
+
function isSafeTmuxSessionName(name) {
|
|
4883
|
+
if (typeof name !== "string") return false;
|
|
4884
|
+
if (name.length === 0 || name.length > TMUX_SESSION_NAME_MAX) return false;
|
|
4885
|
+
return !/[\x00-\x1f\x7f]/.test(name);
|
|
4886
|
+
}
|
|
4887
|
+
function isVhSessionName(name) {
|
|
4888
|
+
return name.startsWith("vh-");
|
|
4889
|
+
}
|
|
4890
|
+
function parseUserSessionLine(line) {
|
|
4891
|
+
if (!line) return void 0;
|
|
4892
|
+
const parts = line.split(USER_SESSION_FIELD_SEP);
|
|
4893
|
+
if (parts.length < 8) return void 0;
|
|
4894
|
+
const [id, name, windows, attached, activity, created, command] = parts;
|
|
4895
|
+
if (!TMUX_SESSION_ID_RE.test(id) || !isSafeTmuxSessionName(name)) return void 0;
|
|
4896
|
+
const cwd = parts.slice(7).join(USER_SESSION_FIELD_SEP);
|
|
4897
|
+
const num = (v) => {
|
|
4898
|
+
const n = Number(v);
|
|
4899
|
+
return Number.isFinite(n) ? n : void 0;
|
|
4900
|
+
};
|
|
4901
|
+
return {
|
|
4902
|
+
id,
|
|
4903
|
+
name,
|
|
4904
|
+
windows: Math.max(0, Math.floor(num(windows) ?? 0)),
|
|
4905
|
+
attached: (num(attached) ?? 0) > 0,
|
|
4906
|
+
activityAt: activity ? (num(activity) ?? 0) * 1e3 || void 0 : void 0,
|
|
4907
|
+
createdAt: created ? (num(created) ?? 0) * 1e3 || void 0 : void 0,
|
|
4908
|
+
command: command.trim() || void 0,
|
|
4909
|
+
cwd: cwd || void 0
|
|
4910
|
+
};
|
|
4911
|
+
}
|
|
4912
|
+
const USER_SESSIONS_MAX = 50;
|
|
4913
|
+
function parseUserSessions(stdout, max = USER_SESSIONS_MAX) {
|
|
4914
|
+
const out = [];
|
|
4915
|
+
for (const line of stdout.split("\n")) {
|
|
4916
|
+
const s = parseUserSessionLine(line);
|
|
4917
|
+
if (!s || isVhSessionName(s.name)) continue;
|
|
4918
|
+
out.push(s);
|
|
4919
|
+
}
|
|
4920
|
+
out.sort((a, b) => (b.activityAt ?? 0) - (a.activityAt ?? 0));
|
|
4921
|
+
return out.slice(0, max);
|
|
4922
|
+
}
|
|
4923
|
+
function attachStartupCommand(sessionId, socket) {
|
|
4924
|
+
if (!TMUX_SESSION_ID_RE.test(sessionId)) throw new Error(`not a tmux session id: ${sessionId}`);
|
|
4925
|
+
const sock = socket ? ` -S ${shellescape(socket)}` : "";
|
|
4926
|
+
return ` TMUX= tmux${sock} attach-session -t ${shellescape(sessionId)}`;
|
|
4927
|
+
}
|
|
4928
|
+
|
|
4847
4929
|
const TERMINAL_ID_RE$1 = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
4848
4930
|
function planTerminalRestore(record, facts) {
|
|
4849
4931
|
if (facts.tmuxAlive) return { kind: "already-live" };
|
|
4932
|
+
if (record.attachTmux) {
|
|
4933
|
+
const matches = (facts.userSessions ?? []).filter((s) => s.name === record.attachTmux);
|
|
4934
|
+
if (matches.length !== 1) return { kind: "error", reason: "tmux-session-gone" };
|
|
4935
|
+
const cwd = record.cwd && facts.cwdExists(record.cwd) ? record.cwd : facts.homeDir;
|
|
4936
|
+
if (!cwd) return { kind: "error", reason: "missing-cwd" };
|
|
4937
|
+
return {
|
|
4938
|
+
kind: "create",
|
|
4939
|
+
terminalId: record.id,
|
|
4940
|
+
cwd,
|
|
4941
|
+
title: record.title ?? record.attachTmux,
|
|
4942
|
+
manual: true,
|
|
4943
|
+
tags: record.tags,
|
|
4944
|
+
command: attachStartupCommand(matches[0].id, facts.attachSocket),
|
|
4945
|
+
attachTmux: record.attachTmux
|
|
4946
|
+
};
|
|
4947
|
+
}
|
|
4850
4948
|
if (!record.cwd || !facts.cwdExists(record.cwd)) return { kind: "error", reason: "missing-cwd" };
|
|
4851
4949
|
const resumable = isClaudeSessionId(record.claudeSessionId) && facts.conversationExists(record.cwd, record.claudeSessionId);
|
|
4852
4950
|
return {
|
|
@@ -4860,6 +4958,20 @@ function planTerminalRestore(record, facts) {
|
|
|
4860
4958
|
};
|
|
4861
4959
|
}
|
|
4862
4960
|
|
|
4961
|
+
const VH_TMUX_SOCKET_ENV = "VH_TMUX_SOCKET";
|
|
4962
|
+
function tmuxSocketFlags(env = process.env) {
|
|
4963
|
+
const socket = env[VH_TMUX_SOCKET_ENV];
|
|
4964
|
+
return socket ? ["-S", socket] : [];
|
|
4965
|
+
}
|
|
4966
|
+
function tmuxArgs(args, env = process.env) {
|
|
4967
|
+
return [...tmuxSocketFlags(env), ...args];
|
|
4968
|
+
}
|
|
4969
|
+
function scrubTmuxClientEnv(env) {
|
|
4970
|
+
delete env.TMUX;
|
|
4971
|
+
delete env.TMUX_PANE;
|
|
4972
|
+
return env;
|
|
4973
|
+
}
|
|
4974
|
+
|
|
4863
4975
|
function getProjectPath(workingDirectory) {
|
|
4864
4976
|
const projectId = resolve$1(workingDirectory).replace(/[^a-zA-Z0-9-]/g, "-");
|
|
4865
4977
|
const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
@@ -9193,20 +9305,6 @@ function previewAscii(line) {
|
|
|
9193
9305
|
return line.length > 80 ? `${out}\u2026` : out;
|
|
9194
9306
|
}
|
|
9195
9307
|
|
|
9196
|
-
const VH_TMUX_SOCKET_ENV = "VH_TMUX_SOCKET";
|
|
9197
|
-
function tmuxSocketFlags(env = process.env) {
|
|
9198
|
-
const socket = env[VH_TMUX_SOCKET_ENV];
|
|
9199
|
-
return socket ? ["-S", socket] : [];
|
|
9200
|
-
}
|
|
9201
|
-
function tmuxArgs(args, env = process.env) {
|
|
9202
|
-
return [...tmuxSocketFlags(env), ...args];
|
|
9203
|
-
}
|
|
9204
|
-
function scrubTmuxClientEnv(env) {
|
|
9205
|
-
delete env.TMUX;
|
|
9206
|
-
delete env.TMUX_PANE;
|
|
9207
|
-
return env;
|
|
9208
|
-
}
|
|
9209
|
-
|
|
9210
9308
|
const CONTROL_CLIENT_KILL_GRACE_MS = 2e3;
|
|
9211
9309
|
const CONTROL_COMMAND_TIMEOUT_MS = 1e4;
|
|
9212
9310
|
function isSafeControlCommand(command) {
|
|
@@ -10024,7 +10122,7 @@ function sanitizeClosedTerminals(raw, max = CLOSED_TERMINALS_MAX) {
|
|
|
10024
10122
|
const out = [];
|
|
10025
10123
|
for (const item of raw) {
|
|
10026
10124
|
if (!item || typeof item !== "object") continue;
|
|
10027
|
-
const { id, title, cwd, mirrorSessionId, claudeSessionId, reason, closedAt, tags, manual } = item;
|
|
10125
|
+
const { id, title, cwd, mirrorSessionId, claudeSessionId, reason, closedAt, tags, manual, attachTmux } = item;
|
|
10028
10126
|
if (typeof id !== "string" || id.length === 0 || typeof closedAt !== "number") continue;
|
|
10029
10127
|
if (seen.has(id)) continue;
|
|
10030
10128
|
seen.add(id);
|
|
@@ -10038,6 +10136,7 @@ function sanitizeClosedTerminals(raw, max = CLOSED_TERMINALS_MAX) {
|
|
|
10038
10136
|
reason: reason === "daemon-gap" || reason === "closed" ? reason : void 0,
|
|
10039
10137
|
...tagList !== void 0 ? { tags: tagList } : {},
|
|
10040
10138
|
...manual === true ? { manual: true } : {},
|
|
10139
|
+
...typeof attachTmux === "string" && attachTmux ? { attachTmux } : {},
|
|
10041
10140
|
closedAt
|
|
10042
10141
|
});
|
|
10043
10142
|
}
|
|
@@ -10162,7 +10261,7 @@ function startupInjectionArgs(tmuxSession, command) {
|
|
|
10162
10261
|
];
|
|
10163
10262
|
}
|
|
10164
10263
|
function seenInfoOf(t) {
|
|
10165
|
-
return { title: t.title, cwd: t.cwd, tags: t.tags, manual: t.manual };
|
|
10264
|
+
return { title: t.title, cwd: t.cwd, tags: t.tags, manual: t.manual, attachTmux: t.attachTmux };
|
|
10166
10265
|
}
|
|
10167
10266
|
function terminalListSignature(items) {
|
|
10168
10267
|
const canon = [...items].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).map((t) => [
|
|
@@ -10175,7 +10274,8 @@ function terminalListSignature(items) {
|
|
|
10175
10274
|
t.agentState ?? "",
|
|
10176
10275
|
// B-105: a mirror binding appearing/disappearing MUST push the
|
|
10177
10276
|
// list, or the web never learns the toggle became available.
|
|
10178
|
-
t.mirrorSessionId ?? ""
|
|
10277
|
+
t.mirrorSessionId ?? "",
|
|
10278
|
+
t.attachTmux ?? ""
|
|
10179
10279
|
]);
|
|
10180
10280
|
return JSON.stringify(canon);
|
|
10181
10281
|
}
|
|
@@ -10194,7 +10294,7 @@ function deriveAutoTitle(paneTitle, hostname) {
|
|
|
10194
10294
|
const chars = Array.from(t);
|
|
10195
10295
|
return chars.length > TITLE_MAX_CHARS ? chars.slice(0, TITLE_MAX_CHARS).join("") : t;
|
|
10196
10296
|
}
|
|
10197
|
-
const LIST_FIELD_SEP = "
|
|
10297
|
+
const LIST_FIELD_SEP = "<~|~>";
|
|
10198
10298
|
const LIST_SESSIONS_FORMAT = [
|
|
10199
10299
|
"#{session_name}",
|
|
10200
10300
|
"#{session_created}",
|
|
@@ -10203,6 +10303,9 @@ const LIST_SESSIONS_FORMAT = [
|
|
|
10203
10303
|
"#{@vh_title}",
|
|
10204
10304
|
"#{@vh_title_manual}",
|
|
10205
10305
|
"#{@vh_tags}",
|
|
10306
|
+
// B-273: the user tmux session this terminal was opened to attach (name),
|
|
10307
|
+
// carried into close records so a restore can re-attach.
|
|
10308
|
+
"#{@vh_attach}",
|
|
10206
10309
|
// B-121: the control-mode client has no `pty.process`, so the agent-state
|
|
10207
10310
|
// fast path lost its live `#{pane_current_command}` equivalent. Carry it in
|
|
10208
10311
|
// the ONE list-sessions call the tracker already makes — the value goes
|
|
@@ -10217,8 +10320,8 @@ const LIST_SESSIONS_FORMAT = [
|
|
|
10217
10320
|
function parseSessionListLine(line) {
|
|
10218
10321
|
if (!line) return void 0;
|
|
10219
10322
|
const parts = line.split(LIST_FIELD_SEP);
|
|
10220
|
-
if (parts.length <
|
|
10221
|
-
const [name, created, activity, cwd, vhTitle, manual, vhTags, paneCommand] = parts;
|
|
10323
|
+
if (parts.length < 10) return void 0;
|
|
10324
|
+
const [name, created, activity, cwd, vhTitle, manual, vhTags, vhAttach, paneCommand] = parts;
|
|
10222
10325
|
if (!name) return void 0;
|
|
10223
10326
|
return {
|
|
10224
10327
|
name,
|
|
@@ -10229,9 +10332,12 @@ function parseSessionListLine(line) {
|
|
|
10229
10332
|
manual: manual.trim().length > 0,
|
|
10230
10333
|
tags: parseTerminalTags(vhTags),
|
|
10231
10334
|
paneCurrentCommand: paneCommand.trim() || void 0,
|
|
10232
|
-
//
|
|
10335
|
+
// Verbatim (no trim): tmux allows edge spaces in a session name and the
|
|
10336
|
+
// restore lookup / attach echo compare it exactly.
|
|
10337
|
+
attachTmux: isSafeTmuxSessionName(vhAttach) ? vhAttach : void 0,
|
|
10338
|
+
// pane_title is last, so anything after field 9 is title content that
|
|
10233
10339
|
// contained the separator — rejoin it rather than dropping it.
|
|
10234
|
-
paneTitle: parts.slice(
|
|
10340
|
+
paneTitle: parts.slice(9).join(LIST_FIELD_SEP) || void 0
|
|
10235
10341
|
};
|
|
10236
10342
|
}
|
|
10237
10343
|
const TERMINAL_TAG_MAX_COUNT = 64;
|
|
@@ -10288,12 +10394,12 @@ function isClaudeConfident(tail) {
|
|
|
10288
10394
|
const last15 = lines.slice(-15).join("\n");
|
|
10289
10395
|
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");
|
|
10290
10396
|
}
|
|
10291
|
-
function planScrollAction(paneInMode, alternateOn, paneWantsMouse, lines, claudeLike = false, paneRows = 24) {
|
|
10397
|
+
function planScrollAction(paneInMode, alternateOn, paneWantsMouse, lines, claudeLike = false, paneRows = 24, nestedTmux = false) {
|
|
10292
10398
|
const count = Math.min(Math.abs(Math.trunc(lines)), 200);
|
|
10293
10399
|
if (count === 0) return { kind: "none" };
|
|
10294
10400
|
const up = lines > 0;
|
|
10295
10401
|
if (paneInMode) return { kind: "copy-scroll", dir: up ? "up" : "down", count };
|
|
10296
|
-
if (alternateOn && paneWantsMouse) return { kind: "mouse-wheel", dir: up ? "up" : "down", count };
|
|
10402
|
+
if (alternateOn && (paneWantsMouse || nestedTmux)) return { kind: "mouse-wheel", dir: up ? "up" : "down", count };
|
|
10297
10403
|
if (alternateOn && claudeLike) {
|
|
10298
10404
|
const halfPage = Math.max(1, Math.floor(paneRows / 2));
|
|
10299
10405
|
const pages = Math.max(1, Math.round(count / halfPage));
|
|
@@ -10408,6 +10514,9 @@ class TerminalSession {
|
|
|
10408
10514
|
id;
|
|
10409
10515
|
tmuxSession;
|
|
10410
10516
|
transport;
|
|
10517
|
+
/** B-273: the user tmux session this terminal was created to attach —
|
|
10518
|
+
* echoed to any later open of the same live session. */
|
|
10519
|
+
attachedTmux;
|
|
10411
10520
|
/**
|
|
10412
10521
|
* The pane this terminal follows, latched from the open capture's
|
|
10413
10522
|
* `list-panes` (first pane of the window). `%output` from any OTHER pane is
|
|
@@ -10918,6 +11027,7 @@ class WebTerminalManager {
|
|
|
10918
11027
|
cwd: info.cwd,
|
|
10919
11028
|
tags: info.tags,
|
|
10920
11029
|
manual: info.manual,
|
|
11030
|
+
...info.attachTmux ? { attachTmux: info.attachTmux } : {},
|
|
10921
11031
|
// Live binding first (authoritative while it exists), persisted
|
|
10922
11032
|
// metadata as the fallback once the mirror has been torn down.
|
|
10923
11033
|
mirrorSessionId: this.mirrorResolver?.(id) ?? mirror?.sessionId,
|
|
@@ -10970,6 +11080,7 @@ class WebTerminalManager {
|
|
|
10970
11080
|
cwd: info.cwd,
|
|
10971
11081
|
tags: info.tags,
|
|
10972
11082
|
manual: info.manual,
|
|
11083
|
+
...info.attachTmux ? { attachTmux: info.attachTmux } : {},
|
|
10973
11084
|
mirrorSessionId: mirror?.sessionId,
|
|
10974
11085
|
claudeSessionId: mirror?.claudeSessionId,
|
|
10975
11086
|
reason: "daemon-gap",
|
|
@@ -11089,12 +11200,17 @@ class WebTerminalManager {
|
|
|
11089
11200
|
return false;
|
|
11090
11201
|
}
|
|
11091
11202
|
},
|
|
11092
|
-
conversationExists: (cwd, claudeSessionId) => existsSync(join(getProjectPath(cwd), `${claudeSessionId}.jsonl`))
|
|
11203
|
+
conversationExists: (cwd, claudeSessionId) => existsSync(join(getProjectPath(cwd), `${claudeSessionId}.jsonl`)),
|
|
11204
|
+
// B-273: only consulted for records that were attach terminals.
|
|
11205
|
+
// Unbounded: the panel's 50-row cap must not hide a restore target.
|
|
11206
|
+
userSessions: record.attachTmux ? this.listUserTmuxSessions(Infinity) : [],
|
|
11207
|
+
attachSocket: process.env[VH_TMUX_SOCKET_ENV],
|
|
11208
|
+
homeDir: os$1.homedir()
|
|
11093
11209
|
});
|
|
11094
11210
|
if (plan.kind === "already-live") return { type: "success", terminalId };
|
|
11095
11211
|
if (plan.kind === "error") return { type: "error", reason: plan.reason };
|
|
11096
11212
|
if (!this.createDetachedTerminal(plan)) return { type: "error", reason: "create-failed" };
|
|
11097
|
-
logger.info(`[WEB TERMINAL] restored closed terminal ${terminalId} in ${plan.cwd}${plan.command ? " (conversation resumed)" : ""}`);
|
|
11213
|
+
logger.info(`[WEB TERMINAL] restored closed terminal ${terminalId} in ${plan.cwd}${plan.attachTmux ? ` (attached tmux session ${plan.attachTmux})` : plan.command ? " (conversation resumed)" : ""}`);
|
|
11098
11214
|
this.kickListRefresh();
|
|
11099
11215
|
return { type: "success", terminalId };
|
|
11100
11216
|
}
|
|
@@ -11154,6 +11270,13 @@ class WebTerminalManager {
|
|
|
11154
11270
|
{ stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
11155
11271
|
);
|
|
11156
11272
|
}
|
|
11273
|
+
if (plan.attachTmux) {
|
|
11274
|
+
spawnSync(
|
|
11275
|
+
"tmux",
|
|
11276
|
+
tmuxArgs(["set-option", "-t", `=${name}:`, "@vh_attach", plan.attachTmux]),
|
|
11277
|
+
{ stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
11278
|
+
);
|
|
11279
|
+
}
|
|
11157
11280
|
if (plan.command) {
|
|
11158
11281
|
for (const argv of startupInjectionArgs(name, plan.command)) {
|
|
11159
11282
|
spawnSync("tmux", tmuxArgs(argv), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env });
|
|
@@ -11166,7 +11289,7 @@ class WebTerminalManager {
|
|
|
11166
11289
|
* would care about changed (see liveSnapshotChanged). */
|
|
11167
11290
|
persistLiveSnapshot(live, now) {
|
|
11168
11291
|
const candidate = /* @__PURE__ */ new Map();
|
|
11169
|
-
for (const [id, info] of live) candidate.set(id, { title: info.title, cwd: info.cwd, tags: info.tags, manual: info.manual, seenAt: now });
|
|
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 });
|
|
11170
11293
|
if (!liveSnapshotChanged(this.lastPersistedSnapshot, candidate)) return;
|
|
11171
11294
|
this.lastPersistedSnapshot = candidate;
|
|
11172
11295
|
saveLiveSnapshot(candidate);
|
|
@@ -11264,6 +11387,7 @@ class WebTerminalManager {
|
|
|
11264
11387
|
const lines = opts.streamMode === "lines";
|
|
11265
11388
|
const existing = this.terminals.get(id);
|
|
11266
11389
|
if (existing) {
|
|
11390
|
+
const echo2 = opts.attachTmux && !(opts.attachOnly || opts.resub) && existing.attachedTmux?.name === opts.attachTmux.name ? { attachedTmux: existing.attachedTmux } : {};
|
|
11267
11391
|
if (!opts.resub || existing.subscribers === 0) existing.subscribers += 1;
|
|
11268
11392
|
existing.lastTouch = Date.now();
|
|
11269
11393
|
this.applyResize(existing, cols, rows);
|
|
@@ -11276,15 +11400,16 @@ class WebTerminalManager {
|
|
|
11276
11400
|
seq: existing.seq,
|
|
11277
11401
|
mode: "replay",
|
|
11278
11402
|
chunks,
|
|
11279
|
-
...lines ? { streamMode: "lines" } : {}
|
|
11403
|
+
...lines ? { streamMode: "lines" } : {},
|
|
11404
|
+
...echo2
|
|
11280
11405
|
};
|
|
11281
11406
|
}
|
|
11282
11407
|
if (lines && existing.transport.kind === "control") {
|
|
11283
|
-
return await this.linesSnapshotResponse(id, existing);
|
|
11408
|
+
return { ...await this.linesSnapshotResponse(id, existing), ...echo2 };
|
|
11284
11409
|
}
|
|
11285
11410
|
const state = existing.subscribeState(opts.fromSeq);
|
|
11286
11411
|
logger.debug(`[WEB TERMINAL] re-subscribed ${id} (subs=${existing.subscribers}, mode=${state.mode}, seq=${state.seq})`);
|
|
11287
|
-
return { terminalId: id, tmuxSession: existing.tmuxSession, ...state };
|
|
11412
|
+
return { terminalId: id, tmuxSession: existing.tmuxSession, ...state, ...echo2 };
|
|
11288
11413
|
}
|
|
11289
11414
|
if (opts.attachOnly || opts.resub) {
|
|
11290
11415
|
const alive = isTmuxAvailable() && spawnSync(
|
|
@@ -11309,6 +11434,10 @@ class WebTerminalManager {
|
|
|
11309
11434
|
let file = defaultShell();
|
|
11310
11435
|
let args = [];
|
|
11311
11436
|
let tmuxSession;
|
|
11437
|
+
let attachedTmux;
|
|
11438
|
+
if (opts.attachTmux && !(opts.attachOnly || opts.resub) && !isTmuxAvailable()) {
|
|
11439
|
+
throw new Error("tmux-unavailable");
|
|
11440
|
+
}
|
|
11312
11441
|
if (isTmuxAvailable()) {
|
|
11313
11442
|
tmuxSession = `vh-${id}`;
|
|
11314
11443
|
let createdNew = false;
|
|
@@ -11321,6 +11450,7 @@ class WebTerminalManager {
|
|
|
11321
11450
|
`VH_HAPPY_HOME_DIR=${configuration.happyHomeDir}`
|
|
11322
11451
|
] : [];
|
|
11323
11452
|
const attachOnly = !!(opts.attachOnly || opts.resub);
|
|
11453
|
+
const attachTarget = !attachOnly && opts.attachTmux ? this.resolveAttachTarget(opts.attachTmux) : void 0;
|
|
11324
11454
|
if (!attachOnly) {
|
|
11325
11455
|
try {
|
|
11326
11456
|
const created = spawnSync(
|
|
@@ -11362,16 +11492,15 @@ class WebTerminalManager {
|
|
|
11362
11492
|
}
|
|
11363
11493
|
}
|
|
11364
11494
|
if (createdNew) {
|
|
11365
|
-
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
|
|
11369
|
-
|
|
11370
|
-
|
|
11371
|
-
|
|
11372
|
-
|
|
11373
|
-
|
|
11374
|
-
}
|
|
11495
|
+
attachedTmux = this.injectIntoCreated(tmuxSession, attachTarget, opts.startupCommand, env);
|
|
11496
|
+
} else if (opts.attachTmux && !attachOnly) {
|
|
11497
|
+
const stored = spawnSync(
|
|
11498
|
+
"tmux",
|
|
11499
|
+
tmuxArgs(["show-options", "-qv", "-t", `=${tmuxSession}:`, "@vh_attach"]),
|
|
11500
|
+
{ encoding: "utf8", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
11501
|
+
);
|
|
11502
|
+
const name = stored.status === 0 && typeof stored.stdout === "string" ? stored.stdout.replace(/\r?\n$/, "") : "";
|
|
11503
|
+
if (name && name === opts.attachTmux.name) attachedTmux = { id: opts.attachTmux.id, name };
|
|
11375
11504
|
}
|
|
11376
11505
|
if (!attachOnly && !createdNew) {
|
|
11377
11506
|
const alive = spawnSync(
|
|
@@ -11386,17 +11515,7 @@ class WebTerminalManager {
|
|
|
11386
11515
|
{ stdio: "ignore", timeout: TMUX_CREATE_TIMEOUT_MS, env }
|
|
11387
11516
|
);
|
|
11388
11517
|
createdNew = retry.status === 0;
|
|
11389
|
-
if (createdNew)
|
|
11390
|
-
const startup = normalizeStartupCommand(opts.startupCommand);
|
|
11391
|
-
if (startup) {
|
|
11392
|
-
for (const a of startupInjectionArgs(tmuxSession, startup)) {
|
|
11393
|
-
try {
|
|
11394
|
-
spawnSync("tmux", tmuxArgs(a), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env });
|
|
11395
|
-
} catch {
|
|
11396
|
-
}
|
|
11397
|
-
}
|
|
11398
|
-
}
|
|
11399
|
-
}
|
|
11518
|
+
if (createdNew) attachedTmux = this.injectIntoCreated(tmuxSession, attachTarget, opts.startupCommand, env);
|
|
11400
11519
|
}
|
|
11401
11520
|
}
|
|
11402
11521
|
if (!attachOnly && !createdNew) {
|
|
@@ -11406,6 +11525,7 @@ class WebTerminalManager {
|
|
|
11406
11525
|
{ stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
11407
11526
|
).status === 0;
|
|
11408
11527
|
if (!alive) {
|
|
11528
|
+
if (attachTarget) throw new Error("tmux-unavailable");
|
|
11409
11529
|
logger.info(`[WEB TERMINAL] tmux session creation unavailable; using direct PTY for ${id}`);
|
|
11410
11530
|
tmuxSession = void 0;
|
|
11411
11531
|
}
|
|
@@ -11488,20 +11608,110 @@ class WebTerminalManager {
|
|
|
11488
11608
|
session = created;
|
|
11489
11609
|
}
|
|
11490
11610
|
session.subscribers = 1;
|
|
11611
|
+
if (attachedTmux) session.attachedTmux = attachedTmux;
|
|
11491
11612
|
this.terminals.set(id, session);
|
|
11492
11613
|
session.onTitleChange(() => this.kickListRefresh());
|
|
11493
11614
|
logger.debug(`[WEB TERMINAL] opened ${id} (${tmuxSession ? "control" : "pty"}) ${cols}x${rows} cwd=${cwd}`);
|
|
11494
11615
|
this.kickListRefresh();
|
|
11616
|
+
const echo = attachedTmux ? { attachedTmux } : {};
|
|
11495
11617
|
if (session.transport.kind !== "control") {
|
|
11496
11618
|
const state = session.subscribeState(void 0);
|
|
11497
|
-
return { terminalId: id, tmuxSession, ...state, ...lines ? { streamMode: "lines" } : {} };
|
|
11619
|
+
return { terminalId: id, tmuxSession, ...state, ...lines ? { streamMode: "lines" } : {}, ...echo };
|
|
11498
11620
|
}
|
|
11499
11621
|
const restored = await this.captureRestore(session, true);
|
|
11500
11622
|
if (!lines) {
|
|
11501
11623
|
const state = session.subscribeState(void 0);
|
|
11502
|
-
return { terminalId: id, tmuxSession, ...state };
|
|
11624
|
+
return { terminalId: id, tmuxSession, ...state, ...echo };
|
|
11625
|
+
}
|
|
11626
|
+
return { ...this.linesResponse(id, session, restored), ...echo };
|
|
11627
|
+
}
|
|
11628
|
+
/**
|
|
11629
|
+
* Post-create injection shared by BOTH creation sites in open(): an attach
|
|
11630
|
+
* request (B-273) pins the title to the user session's name (the inner
|
|
11631
|
+
* tmux's pane title is not reliable), stores `@vh_attach` for close
|
|
11632
|
+
* records / restore and types the attach line; otherwise the ordinary
|
|
11633
|
+
* startup command runs. Returns what to echo as `attachedTmux`.
|
|
11634
|
+
*/
|
|
11635
|
+
injectIntoCreated(tmuxSession, attachTarget, startupCommand, env) {
|
|
11636
|
+
if (attachTarget) {
|
|
11637
|
+
for (const a of [
|
|
11638
|
+
["set-option", "-t", `=${tmuxSession}:`, "@vh_title", attachTarget.name],
|
|
11639
|
+
["set-option", "-t", `=${tmuxSession}:`, "@vh_title_manual", "1"],
|
|
11640
|
+
["set-option", "-t", `=${tmuxSession}:`, "@vh_attach", attachTarget.name],
|
|
11641
|
+
...startupInjectionArgs(tmuxSession, attachStartupCommand(attachTarget.id, process.env[VH_TMUX_SOCKET_ENV]))
|
|
11642
|
+
]) {
|
|
11643
|
+
try {
|
|
11644
|
+
spawnSync("tmux", tmuxArgs(a), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env });
|
|
11645
|
+
} catch {
|
|
11646
|
+
}
|
|
11647
|
+
}
|
|
11648
|
+
logger.debug(`[WEB TERMINAL] ${tmuxSession} attaches user tmux session ${attachTarget.id} (${attachTarget.name})`);
|
|
11649
|
+
return { id: attachTarget.id, name: attachTarget.name };
|
|
11650
|
+
}
|
|
11651
|
+
const startup = normalizeStartupCommand(startupCommand);
|
|
11652
|
+
if (startup) {
|
|
11653
|
+
try {
|
|
11654
|
+
for (const a of startupInjectionArgs(tmuxSession, startup)) {
|
|
11655
|
+
spawnSync("tmux", tmuxArgs(a), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env });
|
|
11656
|
+
}
|
|
11657
|
+
logger.debug(`[WEB TERMINAL] injected startup command into new session ${tmuxSession}`);
|
|
11658
|
+
} catch {
|
|
11659
|
+
}
|
|
11660
|
+
}
|
|
11661
|
+
return void 0;
|
|
11662
|
+
}
|
|
11663
|
+
/**
|
|
11664
|
+
* B-273: check an attach request against the live tmux server. The id
|
|
11665
|
+
* must be a real `$N` (session ids only reset when the tmux server
|
|
11666
|
+
* restarts, so a stale id from an older list simply does not exist)
|
|
11667
|
+
* whose CURRENT name equals the name the web saw — the name cross-check
|
|
11668
|
+
* is what refuses a target that was killed and re-created in between —
|
|
11669
|
+
* and must not be a vh-* web terminal. Throws the 'tmux-session-gone'
|
|
11670
|
+
* contract string. Note `display-message -t '$N'` exits 0 with an EMPTY
|
|
11671
|
+
* line for an unknown id (its target lookup may fail silently), which is
|
|
11672
|
+
* why existence is probed with `has-session` first.
|
|
11673
|
+
*/
|
|
11674
|
+
resolveAttachTarget(req) {
|
|
11675
|
+
const id = typeof req?.id === "string" ? req.id : "";
|
|
11676
|
+
const name = typeof req?.name === "string" ? req.name : "";
|
|
11677
|
+
if (!TMUX_SESSION_ID_RE.test(id) || !isSafeTmuxSessionName(name) || isVhSessionName(name)) throw new Error("tmux-session-gone");
|
|
11678
|
+
const env = ptyEnv();
|
|
11679
|
+
const exists = spawnSync("tmux", tmuxArgs(["has-session", "-t", id]), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }).status === 0;
|
|
11680
|
+
if (!exists) throw new Error("tmux-session-gone");
|
|
11681
|
+
const r = spawnSync(
|
|
11682
|
+
"tmux",
|
|
11683
|
+
tmuxArgs(["display-message", "-p", "-t", id, "#{session_name}"]),
|
|
11684
|
+
{ encoding: "utf8", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
11685
|
+
);
|
|
11686
|
+
const current = r.status === 0 && typeof r.stdout === "string" ? r.stdout.replace(/\r?\n$/, "") : void 0;
|
|
11687
|
+
if (current !== name) throw new Error("tmux-session-gone");
|
|
11688
|
+
return { id, name };
|
|
11689
|
+
}
|
|
11690
|
+
/** B-273: the user's own tmux sessions (never vh-*), newest activity
|
|
11691
|
+
* first. Empty when tmux is missing or no server runs. */
|
|
11692
|
+
/** Last raw outcome of the list-tmux-sessions spawn (diagnostics only). */
|
|
11693
|
+
lastUserSessionsProbe;
|
|
11694
|
+
listUserTmuxSessions(max) {
|
|
11695
|
+
if (!isTmuxAvailable()) {
|
|
11696
|
+
this.lastUserSessionsProbe = { status: null, error: "tmux-unavailable", stdoutBytes: 0 };
|
|
11697
|
+
return [];
|
|
11698
|
+
}
|
|
11699
|
+
try {
|
|
11700
|
+
const r = spawnSync(
|
|
11701
|
+
"tmux",
|
|
11702
|
+
tmuxArgs(["list-sessions", "-F", USER_SESSIONS_FORMAT]),
|
|
11703
|
+
{ encoding: "utf8", timeout: TMUX_CREATE_TIMEOUT_MS, env: ptyEnv() }
|
|
11704
|
+
);
|
|
11705
|
+
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 };
|
|
11706
|
+
if (r.status !== 0 || typeof r.stdout !== "string") {
|
|
11707
|
+
logger.debug(`[WEB TERMINAL] list-tmux-sessions failed: ${JSON.stringify(this.lastUserSessionsProbe)}`);
|
|
11708
|
+
return [];
|
|
11709
|
+
}
|
|
11710
|
+
return parseUserSessions(r.stdout, max);
|
|
11711
|
+
} catch (e) {
|
|
11712
|
+
this.lastUserSessionsProbe = { status: null, error: e instanceof Error ? e.message : String(e), stdoutBytes: 0 };
|
|
11713
|
+
return [];
|
|
11503
11714
|
}
|
|
11504
|
-
return this.linesResponse(id, session, restored);
|
|
11505
11715
|
}
|
|
11506
11716
|
/**
|
|
11507
11717
|
* Run the opening/catch-up capture for a control session and prime the
|
|
@@ -11726,7 +11936,7 @@ class WebTerminalManager {
|
|
|
11726
11936
|
/** Permanently destroy the terminal: detach the pty AND kill the tmux
|
|
11727
11937
|
* session (so a local `tmux attach` won't find it either). Used when the
|
|
11728
11938
|
* user deletes the terminal from the sidebar. */
|
|
11729
|
-
killSession(terminalId) {
|
|
11939
|
+
killSession(terminalId, opts) {
|
|
11730
11940
|
const info = this.lastSeenInfo.get(terminalId) ?? this.listSessions().find((t) => t.id === terminalId);
|
|
11731
11941
|
const target = `=vh-${terminalId}:`;
|
|
11732
11942
|
const killed = spawnSync("tmux", tmuxArgs(["kill-session", "-t", target]), { stdio: "ignore", env: ptyEnv() });
|
|
@@ -11735,6 +11945,15 @@ class WebTerminalManager {
|
|
|
11735
11945
|
logger.debug(`[WEB TERMINAL] failed to verify tmux session removal ${target}`);
|
|
11736
11946
|
return false;
|
|
11737
11947
|
}
|
|
11948
|
+
if (opts?.alsoAttached && info?.attachTmux) {
|
|
11949
|
+
const matches = this.listUserTmuxSessions(Infinity).filter((s) => s.name === info.attachTmux);
|
|
11950
|
+
if (matches.length === 1) {
|
|
11951
|
+
spawnSync("tmux", tmuxArgs(["kill-session", "-t", matches[0].id]), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env: ptyEnv() });
|
|
11952
|
+
logger.info(`[WEB TERMINAL] killed attached user tmux session ${matches[0].id} (${info.attachTmux}) with ${terminalId}`);
|
|
11953
|
+
} else {
|
|
11954
|
+
logger.debug(`[WEB TERMINAL] alsoAttached: no unique session named ${info.attachTmux} (${matches.length} matches)`);
|
|
11955
|
+
}
|
|
11956
|
+
}
|
|
11738
11957
|
if (info) {
|
|
11739
11958
|
const mirror = pickMirrorForTerminal(readPersistedSessions(), terminalId);
|
|
11740
11959
|
this.recordClosed({
|
|
@@ -11743,6 +11962,7 @@ class WebTerminalManager {
|
|
|
11743
11962
|
cwd: info.cwd,
|
|
11744
11963
|
tags: info.tags,
|
|
11745
11964
|
manual: info.manual,
|
|
11965
|
+
...info.attachTmux ? { attachTmux: info.attachTmux } : {},
|
|
11746
11966
|
mirrorSessionId: this.mirrorResolver?.(terminalId) ?? mirror?.sessionId,
|
|
11747
11967
|
claudeSessionId: mirror?.claudeSessionId,
|
|
11748
11968
|
reason: "closed",
|
|
@@ -11831,7 +12051,8 @@ class WebTerminalManager {
|
|
|
11831
12051
|
// Daemon-internal (not pushed to web / not in the signature):
|
|
11832
12052
|
// the mirror reconciler's stricter "claude is really here" gate.
|
|
11833
12053
|
claudeConfident: probe.claudeConfident,
|
|
11834
|
-
manual: s.manual
|
|
12054
|
+
manual: s.manual,
|
|
12055
|
+
...s.attachTmux ? { attachTmux: s.attachTmux } : {}
|
|
11835
12056
|
});
|
|
11836
12057
|
}
|
|
11837
12058
|
return out;
|
|
@@ -11898,18 +12119,19 @@ class WebTerminalManager {
|
|
|
11898
12119
|
try {
|
|
11899
12120
|
const probe = spawnSync(
|
|
11900
12121
|
"tmux",
|
|
11901
|
-
tmuxArgs(["display-message", "-p", "-t", name, "#{pane_in_mode}
|
|
12122
|
+
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)]),
|
|
11902
12123
|
{ encoding: "utf8", timeout: TMUX_PROBE_TIMEOUT_MS, env: ptyEnv() }
|
|
11903
12124
|
);
|
|
11904
12125
|
if (probe.status !== 0 || typeof probe.stdout !== "string") return;
|
|
11905
|
-
const [inMode, altOn, wantsMouse, paneW, paneH, paneCmd] = probe.stdout.trim().split(
|
|
12126
|
+
const [inMode, altOn, wantsMouse, paneW, paneH, paneCmd] = probe.stdout.trim().split(LIST_FIELD_SEP);
|
|
11906
12127
|
const action = planScrollAction(
|
|
11907
12128
|
inMode === "1",
|
|
11908
12129
|
altOn === "1",
|
|
11909
12130
|
wantsMouse === "1",
|
|
11910
12131
|
lines,
|
|
11911
12132
|
looksLikeClaudeCommand(paneCmd || ""),
|
|
11912
|
-
Number(paneH) || 24
|
|
12133
|
+
Number(paneH) || 24,
|
|
12134
|
+
(paneCmd || "").trim() === "tmux"
|
|
11913
12135
|
);
|
|
11914
12136
|
if (action.kind === "none") return;
|
|
11915
12137
|
if (action.kind === "mouse-wheel") {
|
|
@@ -13921,7 +14143,8 @@ class ApiMachineClient {
|
|
|
13921
14143
|
this.syncResumeSessionRpcRegistration();
|
|
13922
14144
|
this.syncRestartSessionRpcRegistration();
|
|
13923
14145
|
this.rpcHandlerManager.registerHandler("open-terminal", async (params) => {
|
|
13924
|
-
const { terminalId, cols, rows, cwd, fromSeq, encStream, startupCommand, resub, attachOnly, streamMode } = params || {};
|
|
14146
|
+
const { terminalId, cols, rows, cwd, fromSeq, encStream, startupCommand, resub, attachOnly, streamMode, attachTmux } = params || {};
|
|
14147
|
+
const attach = attachTmux && typeof attachTmux === "object" && typeof attachTmux.id === "string" && typeof attachTmux.name === "string" ? { id: attachTmux.id, name: attachTmux.name } : void 0;
|
|
13925
14148
|
const result = await this.webTerminal.open({
|
|
13926
14149
|
terminalId,
|
|
13927
14150
|
cols,
|
|
@@ -13931,7 +14154,8 @@ class ApiMachineClient {
|
|
|
13931
14154
|
startupCommand,
|
|
13932
14155
|
resub,
|
|
13933
14156
|
attachOnly,
|
|
13934
|
-
streamMode: streamMode === "lines" ? "lines" : void 0
|
|
14157
|
+
streamMode: streamMode === "lines" ? "lines" : void 0,
|
|
14158
|
+
...attach ? { attachTmux: attach } : {}
|
|
13935
14159
|
});
|
|
13936
14160
|
if (encStream) {
|
|
13937
14161
|
this.encTerminals.add(result.terminalId);
|
|
@@ -13985,11 +14209,11 @@ class ApiMachineClient {
|
|
|
13985
14209
|
return { type: "success" };
|
|
13986
14210
|
});
|
|
13987
14211
|
this.rpcHandlerManager.registerHandler("kill-terminal", async (params) => {
|
|
13988
|
-
const { terminalId } = params || {};
|
|
14212
|
+
const { terminalId, alsoAttached } = params || {};
|
|
13989
14213
|
if (typeof terminalId !== "string" || !terminalId) {
|
|
13990
14214
|
return { type: "error", errorMessage: "Terminal ID is required" };
|
|
13991
14215
|
}
|
|
13992
|
-
if (!this.webTerminal.killSession(terminalId)) {
|
|
14216
|
+
if (!this.webTerminal.killSession(terminalId, { alsoAttached: alsoAttached === true })) {
|
|
13993
14217
|
return { type: "error", errorMessage: "tmux session is still running" };
|
|
13994
14218
|
}
|
|
13995
14219
|
this.encTerminals.delete(terminalId);
|
|
@@ -14006,6 +14230,9 @@ class ApiMachineClient {
|
|
|
14006
14230
|
this.rpcHandlerManager.registerHandler("list-terminals", async () => {
|
|
14007
14231
|
return { type: "success", terminals: this.webTerminal.buildTerminalList() };
|
|
14008
14232
|
});
|
|
14233
|
+
this.rpcHandlerManager.registerHandler("list-tmux-sessions", async () => {
|
|
14234
|
+
return { type: "success", sessions: this.webTerminal.listUserTmuxSessions() };
|
|
14235
|
+
});
|
|
14009
14236
|
this.rpcHandlerManager.registerHandler("set-terminal-title", async (params) => {
|
|
14010
14237
|
const { terminalId, title, ifAbsent } = params || {};
|
|
14011
14238
|
if (!terminalId || typeof title !== "string") {
|
|
@@ -14399,6 +14626,8 @@ class ApiMachineClient {
|
|
|
14399
14626
|
// B-265 capability flag; restamped every connect so the
|
|
14400
14627
|
// web's `detectedAt >= startedAt` trust rule holds.
|
|
14401
14628
|
terminalRestore: { rpcAvailable: true, detectedAt: now },
|
|
14629
|
+
// B-273 capability flag (same restamp discipline).
|
|
14630
|
+
tmuxSessions: { rpcAvailable: true, detectedAt: now, killAttached: true },
|
|
14402
14631
|
// B-084: closed records survive daemon restarts (persisted
|
|
14403
14632
|
// in closed-terminals.json), so the connect snapshot ships
|
|
14404
14633
|
// them too — not just the incremental pushes.
|
|
@@ -15740,4 +15969,4 @@ const RawJSONLinesSchema = z$1.discriminatedUnion("type", [
|
|
|
15740
15969
|
}).passthrough()
|
|
15741
15970
|
]);
|
|
15742
15971
|
|
|
15743
|
-
export { detectResumeSupport as $, ApiClient as A, deriveLocalCliUpdateSummary as B, updateSettings as C, encodeBase64Url as D, decodeBase64 as E, writeCredentialsLegacy as F, writeCredentialsDataKey as G, readCredentialsForConfiguredRelay as H, InvalidateSync as I, encrypt as J, isClaudeSessionId as K, getLocalHappyAgentCredentialPath as L, MAX_CHAT_ATTACHMENT_SOURCE_BYTES as M, readLocalHappyAgentCredentials as N, decryptWithDataKey as O, decryptLegacy as P, extractCompleteLines as Q, RawJSONLinesSchema as R, decideBackfill as S, parseTerminalHookPayload as T, tmuxArgs as U, scrubTmuxClientEnv as V, decideMirrorBinding as W, MIRROR_BACKFILL_LINES_DEFAULT as X, persistSession as Y, mirrorLineKey as Z, mirrorLocalId as _, ApiSessionClient as a, detectCLIAvailability as a0, acquireDaemonLock as a1, summarizeSpawnSessionForLog as a2, deletePersistedSessions as a3,
|
|
15972
|
+
export { detectResumeSupport as $, ApiClient as A, deriveLocalCliUpdateSummary as B, updateSettings as C, encodeBase64Url as D, decodeBase64 as E, writeCredentialsLegacy as F, writeCredentialsDataKey as G, readCredentialsForConfiguredRelay as H, InvalidateSync as I, encrypt as J, isClaudeSessionId as K, getLocalHappyAgentCredentialPath as L, MAX_CHAT_ATTACHMENT_SOURCE_BYTES as M, readLocalHappyAgentCredentials as N, decryptWithDataKey as O, decryptLegacy as P, extractCompleteLines as Q, RawJSONLinesSchema as R, decideBackfill as S, parseTerminalHookPayload as T, tmuxArgs as U, scrubTmuxClientEnv as V, decideMirrorBinding as W, MIRROR_BACKFILL_LINES_DEFAULT as X, persistSession as Y, mirrorLineKey as Z, mirrorLocalId as _, ApiSessionClient as a, detectCLIAvailability as a0, acquireDaemonLock as a1, summarizeSpawnSessionForLog as a2, deletePersistedSessions as a3, shellescape as a4, decrypt as a5, writeDaemonState as a6, writeSettings as a7, fetchCliUpdateState as a8, resolveCliUpdateCheckInterval as a9, DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS as aa, releaseDaemonLock as ab, isValidSessionId as ac, listVhTerminals as ad, isValidTerminalId as ae, readVhTerminal as af, sendToVhTerminal as ag, startOfflineReconnection as ah, clearCredentials as ai, clearMachineId as aj, SandboxConfigSchema as ak, CodexAppServerClient as al, getLatestDaemonLog as am, persistence as an, api as ao, createEnvelope as b, configuration as c, contentLogMetadata as d, errorLogMetadata as e, packageJson as f, delay as g, connectionState as h, readSettings as i, encodeBase64 as j, ensurePrivateDirectorySync as k, logger as l, hardenPrivateFileSync as m, readDaemonState as n, clearDaemonState as o, projectPath as p, getProjectPath as q, readPersistedSessions as r, initializeSandbox as s, wrapCommand as t, AsyncLock as u, ensurePrivateDirectory as v, writePrivateFileSync as w, hardenPrivateFile as x, readCredentials as y, credentialRelayProblem as z };
|