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.
- package/dist/{AcpBackend-B5Eh8G3W.mjs → AcpBackend-BXiGiFC7.mjs} +1 -1
- package/dist/{AcpBackend-BaCt4_zS.cjs → AcpBackend-DhR8ml8L.cjs} +1 -1
- package/dist/{AcpSessionManager-grr_aJEI.mjs → AcpSessionManager-CDnrxAfQ.mjs} +1 -1
- package/dist/{AcpSessionManager-CvgmPlXB.cjs → AcpSessionManager-Ckc0NFmf.cjs} +1 -1
- package/dist/{config-B69TZlJj.mjs → config-B3pA9YsN.mjs} +3 -3
- package/dist/{config-BjsUeQPA.cjs → config-GJQc_kpD.cjs} +3 -3
- package/dist/{index-DID1FV_n.cjs → index-BBm2b34F.cjs} +5 -5
- package/dist/{index-C-uJXSm3.mjs → index-Bh5Rcy0s.mjs} +6 -6
- package/dist/{index-uc-dtu-3.cjs → index-CO1gNtuP.cjs} +528 -25
- package/dist/{index-BMmbxCTq.mjs → index-CuUVSZnK.mjs} +527 -24
- package/dist/index.cjs +3 -3
- package/dist/index.mjs +3 -3
- package/dist/{installTerminalHooks-CepXxAM_.cjs → installTerminalHooks-Cp9JVxQ-.cjs} +3 -3
- package/dist/{installTerminalHooks-CBHjEQ-a.mjs → installTerminalHooks-Ru0dn_zF.mjs} +3 -3
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +77 -0
- package/dist/lib.d.mts +77 -0
- package/dist/lib.mjs +1 -1
- package/dist/{mcp-CAtbtTXF.cjs → mcp-B5HMF24K.cjs} +3 -3
- package/dist/{mcp-BdSHqi2d.mjs → mcp-CQoVN0u2.mjs} +3 -3
- package/dist/{runGemini-qStPY5fH.cjs → runGemini-BBzPVzuc.cjs} +5 -5
- package/dist/{runGemini-cMZ3_CRf.mjs → runGemini-CwZXQsgu.mjs} +5 -5
- package/dist/{runOpenClaw-B8HR6GnX.cjs → runOpenClaw-B4nC6XGL.cjs} +4 -4
- package/dist/{runOpenClaw-BJ36wEbk.mjs → runOpenClaw-C-jz4rWr.mjs} +4 -4
- package/dist/{send-6eY55o3C.cjs → send-CWgaINHC.cjs} +3 -3
- package/dist/{send-TX13v8Xe.mjs → send-DN-nVBEI.mjs} +3 -3
- package/dist/{spawn-vOZA0kOi.cjs → spawn-C0NSP84Y.cjs} +3 -3
- package/dist/{spawn-CfCE6JAv.mjs → spawn-CqcfbOnk.mjs} +3 -3
- package/dist/{types-okYCRlgl.cjs → types-BP-4phS_.cjs} +358 -72
- package/dist/{types-CLRl0ET3.mjs → types-Cl4vWuDY.mjs} +356 -72
- package/package.json +7 -7
- package/scripts/shims/keychain-off/security +51 -0
|
@@ -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.98";
|
|
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.98",
|
|
146
|
+
"very-happy-tools-x64-darwin": "0.2.98",
|
|
147
|
+
"very-happy-tools-arm64-linux": "0.2.98",
|
|
148
|
+
"very-happy-tools-x64-linux": "0.2.98",
|
|
149
|
+
"very-happy-tools-arm64-win32": "0.2.98",
|
|
150
|
+
"very-happy-tools-x64-win32": "0.2.98"
|
|
151
151
|
};
|
|
152
152
|
var devDependencies = {
|
|
153
153
|
"@slopus/happy-wire": "workspace:*",
|
|
@@ -980,6 +980,22 @@ z$1.object({
|
|
|
980
980
|
detectedAt: z$1.number()
|
|
981
981
|
}).optional()
|
|
982
982
|
});
|
|
983
|
+
const ClaudeAuthStateSchema = z$1.object({
|
|
984
|
+
probeVersion: z$1.number(),
|
|
985
|
+
daemonPid: z$1.number(),
|
|
986
|
+
status: z$1.union([z$1.enum(["ok", "not-logged-in", "unknown", "error", "claude-missing"]), z$1.string()]),
|
|
987
|
+
authMethod: z$1.string().optional(),
|
|
988
|
+
subscriptionType: z$1.string().optional(),
|
|
989
|
+
diagnosis: z$1.string().optional(),
|
|
990
|
+
detail: z$1.string().optional(),
|
|
991
|
+
repairable: z$1.string().optional(),
|
|
992
|
+
context: z$1.object({
|
|
993
|
+
platform: z$1.string(),
|
|
994
|
+
lineage: z$1.string(),
|
|
995
|
+
credentialStore: z$1.string()
|
|
996
|
+
}),
|
|
997
|
+
checkedAt: z$1.number()
|
|
998
|
+
});
|
|
983
999
|
const CliUpdateStateSchema = z$1.object({
|
|
984
1000
|
currentVersion: z$1.string(),
|
|
985
1001
|
recommendedVersion: z$1.string().nullable(),
|
|
@@ -1003,7 +1019,9 @@ const WebTerminalListItemSchema = z$1.object({
|
|
|
1003
1019
|
* conversation carried over, the processes did not. Cleared once opened. */
|
|
1004
1020
|
restoredAt: z$1.number().optional(),
|
|
1005
1021
|
/** B-265: @vh_title_manual is set (user renamed it). */
|
|
1006
|
-
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()
|
|
1007
1025
|
});
|
|
1008
1026
|
const ClosedTerminalRecordSchema = z$1.object({
|
|
1009
1027
|
id: z$1.string(),
|
|
@@ -1020,6 +1038,8 @@ const ClosedTerminalRecordSchema = z$1.object({
|
|
|
1020
1038
|
/** B-265: tags + manual-rename flag at close time (restored verbatim). */
|
|
1021
1039
|
tags: z$1.array(z$1.string()).optional(),
|
|
1022
1040
|
manual: z$1.boolean().optional(),
|
|
1041
|
+
/** B-273: attached user tmux session name — restore re-attaches. */
|
|
1042
|
+
attachTmux: z$1.string().optional(),
|
|
1023
1043
|
closedAt: z$1.number()
|
|
1024
1044
|
});
|
|
1025
1045
|
z$1.object({
|
|
@@ -1073,8 +1093,26 @@ z$1.object({
|
|
|
1073
1093
|
rpcAvailable: z$1.boolean(),
|
|
1074
1094
|
detectedAt: z$1.number()
|
|
1075
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
|
+
}).optional(),
|
|
1076
1106
|
/** Relay-owned CLI compatibility/update policy last checked by this daemon. */
|
|
1077
|
-
cliUpdate: CliUpdateStateSchema.optional()
|
|
1107
|
+
cliUpdate: CliUpdateStateSchema.optional(),
|
|
1108
|
+
/**
|
|
1109
|
+
* B-276: result of the daemon-context Claude auth preflight (spec
|
|
1110
|
+
* 2026-09-claude-auth-preflight). Trust rule for consumers:
|
|
1111
|
+
* `claudeAuth.daemonPid === daemonState.pid` — daemonState survives daemon
|
|
1112
|
+
* restarts (the server returns the existing row and connect spreads it
|
|
1113
|
+
* forward), so a downgraded daemon carries this field along unchanged.
|
|
1114
|
+
*/
|
|
1115
|
+
claudeAuth: ClaudeAuthStateSchema.optional()
|
|
1078
1116
|
});
|
|
1079
1117
|
const MessageMetaSchema = z$1.object({
|
|
1080
1118
|
sentFrom: z$1.string().optional(),
|
|
@@ -4766,7 +4804,7 @@ function sanitizeLiveSnapshot(raw, now, ttlMs = LIVE_SNAPSHOT_TTL_MS, max = LIVE
|
|
|
4766
4804
|
const rows = [];
|
|
4767
4805
|
for (const [id, value] of Object.entries(raw)) {
|
|
4768
4806
|
if (!id || !value || typeof value !== "object") continue;
|
|
4769
|
-
const { title, cwd, seenAt, tags, manual } = value;
|
|
4807
|
+
const { title, cwd, seenAt, tags, manual, attachTmux } = value;
|
|
4770
4808
|
if (typeof seenAt !== "number" || !Number.isFinite(seenAt)) continue;
|
|
4771
4809
|
if (now - seenAt >= ttlMs) continue;
|
|
4772
4810
|
const tagList = sanitizeTagList(tags);
|
|
@@ -4775,6 +4813,7 @@ function sanitizeLiveSnapshot(raw, now, ttlMs = LIVE_SNAPSHOT_TTL_MS, max = LIVE
|
|
|
4775
4813
|
cwd: typeof cwd === "string" && cwd ? cwd : void 0,
|
|
4776
4814
|
...tagList !== void 0 ? { tags: tagList } : {},
|
|
4777
4815
|
...manual === true ? { manual: true } : {},
|
|
4816
|
+
...typeof attachTmux === "string" && attachTmux ? { attachTmux } : {},
|
|
4778
4817
|
seenAt
|
|
4779
4818
|
}]);
|
|
4780
4819
|
}
|
|
@@ -4795,6 +4834,7 @@ function liveSnapshotChanged(prev, next) {
|
|
|
4795
4834
|
if (!before) return true;
|
|
4796
4835
|
if (before.title !== info.title || before.cwd !== info.cwd) return true;
|
|
4797
4836
|
if (!!before.manual !== !!info.manual) return true;
|
|
4837
|
+
if ((before.attachTmux ?? "") !== (info.attachTmux ?? "")) return true;
|
|
4798
4838
|
if ((before.tags ?? []).join("\0") !== (info.tags ?? []).join("\0")) return true;
|
|
4799
4839
|
}
|
|
4800
4840
|
return false;
|
|
@@ -4820,9 +4860,89 @@ function pickMirrorForTerminal(sessions, terminalId) {
|
|
|
4820
4860
|
return best ? { sessionId: best.id, claudeSessionId: best.claude } : null;
|
|
4821
4861
|
}
|
|
4822
4862
|
|
|
4863
|
+
function shellescape(s) {
|
|
4864
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
4865
|
+
}
|
|
4866
|
+
|
|
4867
|
+
const USER_SESSION_FIELD_SEP = "<~|~>";
|
|
4868
|
+
const USER_SESSIONS_FORMAT = [
|
|
4869
|
+
"#{session_id}",
|
|
4870
|
+
"#{session_name}",
|
|
4871
|
+
"#{session_windows}",
|
|
4872
|
+
"#{session_attached}",
|
|
4873
|
+
"#{session_activity}",
|
|
4874
|
+
"#{session_created}",
|
|
4875
|
+
"#{pane_current_command}",
|
|
4876
|
+
"#{pane_current_path}"
|
|
4877
|
+
].join(USER_SESSION_FIELD_SEP);
|
|
4878
|
+
const TMUX_SESSION_ID_RE = /^\$\d{1,9}$/;
|
|
4879
|
+
const TMUX_SESSION_NAME_MAX = 128;
|
|
4880
|
+
function isSafeTmuxSessionName(name) {
|
|
4881
|
+
if (typeof name !== "string") return false;
|
|
4882
|
+
if (name.length === 0 || name.length > TMUX_SESSION_NAME_MAX) return false;
|
|
4883
|
+
return !/[\x00-\x1f\x7f]/.test(name);
|
|
4884
|
+
}
|
|
4885
|
+
function isVhSessionName(name) {
|
|
4886
|
+
return name.startsWith("vh-");
|
|
4887
|
+
}
|
|
4888
|
+
function parseUserSessionLine(line) {
|
|
4889
|
+
if (!line) return void 0;
|
|
4890
|
+
const parts = line.split(USER_SESSION_FIELD_SEP);
|
|
4891
|
+
if (parts.length < 8) return void 0;
|
|
4892
|
+
const [id, name, windows, attached, activity, created, command] = parts;
|
|
4893
|
+
if (!TMUX_SESSION_ID_RE.test(id) || !isSafeTmuxSessionName(name)) return void 0;
|
|
4894
|
+
const cwd = parts.slice(7).join(USER_SESSION_FIELD_SEP);
|
|
4895
|
+
const num = (v) => {
|
|
4896
|
+
const n = Number(v);
|
|
4897
|
+
return Number.isFinite(n) ? n : void 0;
|
|
4898
|
+
};
|
|
4899
|
+
return {
|
|
4900
|
+
id,
|
|
4901
|
+
name,
|
|
4902
|
+
windows: Math.max(0, Math.floor(num(windows) ?? 0)),
|
|
4903
|
+
attached: (num(attached) ?? 0) > 0,
|
|
4904
|
+
activityAt: activity ? (num(activity) ?? 0) * 1e3 || void 0 : void 0,
|
|
4905
|
+
createdAt: created ? (num(created) ?? 0) * 1e3 || void 0 : void 0,
|
|
4906
|
+
command: command.trim() || void 0,
|
|
4907
|
+
cwd: cwd || void 0
|
|
4908
|
+
};
|
|
4909
|
+
}
|
|
4910
|
+
const USER_SESSIONS_MAX = 50;
|
|
4911
|
+
function parseUserSessions(stdout, max = USER_SESSIONS_MAX) {
|
|
4912
|
+
const out = [];
|
|
4913
|
+
for (const line of stdout.split("\n")) {
|
|
4914
|
+
const s = parseUserSessionLine(line);
|
|
4915
|
+
if (!s || isVhSessionName(s.name)) continue;
|
|
4916
|
+
out.push(s);
|
|
4917
|
+
}
|
|
4918
|
+
out.sort((a, b) => (b.activityAt ?? 0) - (a.activityAt ?? 0));
|
|
4919
|
+
return out.slice(0, max);
|
|
4920
|
+
}
|
|
4921
|
+
function attachStartupCommand(sessionId, socket) {
|
|
4922
|
+
if (!TMUX_SESSION_ID_RE.test(sessionId)) throw new Error(`not a tmux session id: ${sessionId}`);
|
|
4923
|
+
const sock = socket ? ` -S ${shellescape(socket)}` : "";
|
|
4924
|
+
return ` TMUX= tmux${sock} attach-session -t ${shellescape(sessionId)}`;
|
|
4925
|
+
}
|
|
4926
|
+
|
|
4823
4927
|
const TERMINAL_ID_RE$1 = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
4824
4928
|
function planTerminalRestore(record, facts) {
|
|
4825
4929
|
if (facts.tmuxAlive) return { kind: "already-live" };
|
|
4930
|
+
if (record.attachTmux) {
|
|
4931
|
+
const matches = (facts.userSessions ?? []).filter((s) => s.name === record.attachTmux);
|
|
4932
|
+
if (matches.length !== 1) return { kind: "error", reason: "tmux-session-gone" };
|
|
4933
|
+
const cwd = record.cwd && facts.cwdExists(record.cwd) ? record.cwd : facts.homeDir;
|
|
4934
|
+
if (!cwd) return { kind: "error", reason: "missing-cwd" };
|
|
4935
|
+
return {
|
|
4936
|
+
kind: "create",
|
|
4937
|
+
terminalId: record.id,
|
|
4938
|
+
cwd,
|
|
4939
|
+
title: record.title ?? record.attachTmux,
|
|
4940
|
+
manual: true,
|
|
4941
|
+
tags: record.tags,
|
|
4942
|
+
command: attachStartupCommand(matches[0].id, facts.attachSocket),
|
|
4943
|
+
attachTmux: record.attachTmux
|
|
4944
|
+
};
|
|
4945
|
+
}
|
|
4826
4946
|
if (!record.cwd || !facts.cwdExists(record.cwd)) return { kind: "error", reason: "missing-cwd" };
|
|
4827
4947
|
const resumable = isClaudeSessionId(record.claudeSessionId) && facts.conversationExists(record.cwd, record.claudeSessionId);
|
|
4828
4948
|
return {
|
|
@@ -4836,6 +4956,20 @@ function planTerminalRestore(record, facts) {
|
|
|
4836
4956
|
};
|
|
4837
4957
|
}
|
|
4838
4958
|
|
|
4959
|
+
const VH_TMUX_SOCKET_ENV = "VH_TMUX_SOCKET";
|
|
4960
|
+
function tmuxSocketFlags(env = process.env) {
|
|
4961
|
+
const socket = env[VH_TMUX_SOCKET_ENV];
|
|
4962
|
+
return socket ? ["-S", socket] : [];
|
|
4963
|
+
}
|
|
4964
|
+
function tmuxArgs(args, env = process.env) {
|
|
4965
|
+
return [...tmuxSocketFlags(env), ...args];
|
|
4966
|
+
}
|
|
4967
|
+
function scrubTmuxClientEnv(env) {
|
|
4968
|
+
delete env.TMUX;
|
|
4969
|
+
delete env.TMUX_PANE;
|
|
4970
|
+
return env;
|
|
4971
|
+
}
|
|
4972
|
+
|
|
4839
4973
|
function getProjectPath(workingDirectory) {
|
|
4840
4974
|
const projectId = resolve$1(workingDirectory).replace(/[^a-zA-Z0-9-]/g, "-");
|
|
4841
4975
|
const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
@@ -9169,20 +9303,6 @@ function previewAscii(line) {
|
|
|
9169
9303
|
return line.length > 80 ? `${out}\u2026` : out;
|
|
9170
9304
|
}
|
|
9171
9305
|
|
|
9172
|
-
const VH_TMUX_SOCKET_ENV = "VH_TMUX_SOCKET";
|
|
9173
|
-
function tmuxSocketFlags(env = process.env) {
|
|
9174
|
-
const socket = env[VH_TMUX_SOCKET_ENV];
|
|
9175
|
-
return socket ? ["-S", socket] : [];
|
|
9176
|
-
}
|
|
9177
|
-
function tmuxArgs(args, env = process.env) {
|
|
9178
|
-
return [...tmuxSocketFlags(env), ...args];
|
|
9179
|
-
}
|
|
9180
|
-
function scrubTmuxClientEnv(env) {
|
|
9181
|
-
delete env.TMUX;
|
|
9182
|
-
delete env.TMUX_PANE;
|
|
9183
|
-
return env;
|
|
9184
|
-
}
|
|
9185
|
-
|
|
9186
9306
|
const CONTROL_CLIENT_KILL_GRACE_MS = 2e3;
|
|
9187
9307
|
const CONTROL_COMMAND_TIMEOUT_MS = 1e4;
|
|
9188
9308
|
function isSafeControlCommand(command) {
|
|
@@ -10000,7 +10120,7 @@ function sanitizeClosedTerminals(raw, max = CLOSED_TERMINALS_MAX) {
|
|
|
10000
10120
|
const out = [];
|
|
10001
10121
|
for (const item of raw) {
|
|
10002
10122
|
if (!item || typeof item !== "object") continue;
|
|
10003
|
-
const { id, title, cwd, mirrorSessionId, claudeSessionId, reason, closedAt, tags, manual } = item;
|
|
10123
|
+
const { id, title, cwd, mirrorSessionId, claudeSessionId, reason, closedAt, tags, manual, attachTmux } = item;
|
|
10004
10124
|
if (typeof id !== "string" || id.length === 0 || typeof closedAt !== "number") continue;
|
|
10005
10125
|
if (seen.has(id)) continue;
|
|
10006
10126
|
seen.add(id);
|
|
@@ -10014,6 +10134,7 @@ function sanitizeClosedTerminals(raw, max = CLOSED_TERMINALS_MAX) {
|
|
|
10014
10134
|
reason: reason === "daemon-gap" || reason === "closed" ? reason : void 0,
|
|
10015
10135
|
...tagList !== void 0 ? { tags: tagList } : {},
|
|
10016
10136
|
...manual === true ? { manual: true } : {},
|
|
10137
|
+
...typeof attachTmux === "string" && attachTmux ? { attachTmux } : {},
|
|
10017
10138
|
closedAt
|
|
10018
10139
|
});
|
|
10019
10140
|
}
|
|
@@ -10138,7 +10259,7 @@ function startupInjectionArgs(tmuxSession, command) {
|
|
|
10138
10259
|
];
|
|
10139
10260
|
}
|
|
10140
10261
|
function seenInfoOf(t) {
|
|
10141
|
-
return { title: t.title, cwd: t.cwd, tags: t.tags, manual: t.manual };
|
|
10262
|
+
return { title: t.title, cwd: t.cwd, tags: t.tags, manual: t.manual, attachTmux: t.attachTmux };
|
|
10142
10263
|
}
|
|
10143
10264
|
function terminalListSignature(items) {
|
|
10144
10265
|
const canon = [...items].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).map((t) => [
|
|
@@ -10151,7 +10272,8 @@ function terminalListSignature(items) {
|
|
|
10151
10272
|
t.agentState ?? "",
|
|
10152
10273
|
// B-105: a mirror binding appearing/disappearing MUST push the
|
|
10153
10274
|
// list, or the web never learns the toggle became available.
|
|
10154
|
-
t.mirrorSessionId ?? ""
|
|
10275
|
+
t.mirrorSessionId ?? "",
|
|
10276
|
+
t.attachTmux ?? ""
|
|
10155
10277
|
]);
|
|
10156
10278
|
return JSON.stringify(canon);
|
|
10157
10279
|
}
|
|
@@ -10170,7 +10292,7 @@ function deriveAutoTitle(paneTitle, hostname) {
|
|
|
10170
10292
|
const chars = Array.from(t);
|
|
10171
10293
|
return chars.length > TITLE_MAX_CHARS ? chars.slice(0, TITLE_MAX_CHARS).join("") : t;
|
|
10172
10294
|
}
|
|
10173
|
-
const LIST_FIELD_SEP = "
|
|
10295
|
+
const LIST_FIELD_SEP = "<~|~>";
|
|
10174
10296
|
const LIST_SESSIONS_FORMAT = [
|
|
10175
10297
|
"#{session_name}",
|
|
10176
10298
|
"#{session_created}",
|
|
@@ -10179,6 +10301,9 @@ const LIST_SESSIONS_FORMAT = [
|
|
|
10179
10301
|
"#{@vh_title}",
|
|
10180
10302
|
"#{@vh_title_manual}",
|
|
10181
10303
|
"#{@vh_tags}",
|
|
10304
|
+
// B-273: the user tmux session this terminal was opened to attach (name),
|
|
10305
|
+
// carried into close records so a restore can re-attach.
|
|
10306
|
+
"#{@vh_attach}",
|
|
10182
10307
|
// B-121: the control-mode client has no `pty.process`, so the agent-state
|
|
10183
10308
|
// fast path lost its live `#{pane_current_command}` equivalent. Carry it in
|
|
10184
10309
|
// the ONE list-sessions call the tracker already makes — the value goes
|
|
@@ -10193,8 +10318,8 @@ const LIST_SESSIONS_FORMAT = [
|
|
|
10193
10318
|
function parseSessionListLine(line) {
|
|
10194
10319
|
if (!line) return void 0;
|
|
10195
10320
|
const parts = line.split(LIST_FIELD_SEP);
|
|
10196
|
-
if (parts.length <
|
|
10197
|
-
const [name, created, activity, cwd, vhTitle, manual, vhTags, paneCommand] = parts;
|
|
10321
|
+
if (parts.length < 10) return void 0;
|
|
10322
|
+
const [name, created, activity, cwd, vhTitle, manual, vhTags, vhAttach, paneCommand] = parts;
|
|
10198
10323
|
if (!name) return void 0;
|
|
10199
10324
|
return {
|
|
10200
10325
|
name,
|
|
@@ -10205,9 +10330,12 @@ function parseSessionListLine(line) {
|
|
|
10205
10330
|
manual: manual.trim().length > 0,
|
|
10206
10331
|
tags: parseTerminalTags(vhTags),
|
|
10207
10332
|
paneCurrentCommand: paneCommand.trim() || void 0,
|
|
10208
|
-
//
|
|
10333
|
+
// Verbatim (no trim): tmux allows edge spaces in a session name and the
|
|
10334
|
+
// restore lookup / attach echo compare it exactly.
|
|
10335
|
+
attachTmux: isSafeTmuxSessionName(vhAttach) ? vhAttach : void 0,
|
|
10336
|
+
// pane_title is last, so anything after field 9 is title content that
|
|
10209
10337
|
// contained the separator — rejoin it rather than dropping it.
|
|
10210
|
-
paneTitle: parts.slice(
|
|
10338
|
+
paneTitle: parts.slice(9).join(LIST_FIELD_SEP) || void 0
|
|
10211
10339
|
};
|
|
10212
10340
|
}
|
|
10213
10341
|
const TERMINAL_TAG_MAX_COUNT = 64;
|
|
@@ -10264,12 +10392,12 @@ function isClaudeConfident(tail) {
|
|
|
10264
10392
|
const last15 = lines.slice(-15).join("\n");
|
|
10265
10393
|
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");
|
|
10266
10394
|
}
|
|
10267
|
-
function planScrollAction(paneInMode, alternateOn, paneWantsMouse, lines, claudeLike = false, paneRows = 24) {
|
|
10395
|
+
function planScrollAction(paneInMode, alternateOn, paneWantsMouse, lines, claudeLike = false, paneRows = 24, nestedTmux = false) {
|
|
10268
10396
|
const count = Math.min(Math.abs(Math.trunc(lines)), 200);
|
|
10269
10397
|
if (count === 0) return { kind: "none" };
|
|
10270
10398
|
const up = lines > 0;
|
|
10271
10399
|
if (paneInMode) return { kind: "copy-scroll", dir: up ? "up" : "down", count };
|
|
10272
|
-
if (alternateOn && paneWantsMouse) return { kind: "mouse-wheel", dir: up ? "up" : "down", count };
|
|
10400
|
+
if (alternateOn && (paneWantsMouse || nestedTmux)) return { kind: "mouse-wheel", dir: up ? "up" : "down", count };
|
|
10273
10401
|
if (alternateOn && claudeLike) {
|
|
10274
10402
|
const halfPage = Math.max(1, Math.floor(paneRows / 2));
|
|
10275
10403
|
const pages = Math.max(1, Math.round(count / halfPage));
|
|
@@ -10384,6 +10512,9 @@ class TerminalSession {
|
|
|
10384
10512
|
id;
|
|
10385
10513
|
tmuxSession;
|
|
10386
10514
|
transport;
|
|
10515
|
+
/** B-273: the user tmux session this terminal was created to attach —
|
|
10516
|
+
* echoed to any later open of the same live session. */
|
|
10517
|
+
attachedTmux;
|
|
10387
10518
|
/**
|
|
10388
10519
|
* The pane this terminal follows, latched from the open capture's
|
|
10389
10520
|
* `list-panes` (first pane of the window). `%output` from any OTHER pane is
|
|
@@ -10894,6 +11025,7 @@ class WebTerminalManager {
|
|
|
10894
11025
|
cwd: info.cwd,
|
|
10895
11026
|
tags: info.tags,
|
|
10896
11027
|
manual: info.manual,
|
|
11028
|
+
...info.attachTmux ? { attachTmux: info.attachTmux } : {},
|
|
10897
11029
|
// Live binding first (authoritative while it exists), persisted
|
|
10898
11030
|
// metadata as the fallback once the mirror has been torn down.
|
|
10899
11031
|
mirrorSessionId: this.mirrorResolver?.(id) ?? mirror?.sessionId,
|
|
@@ -10946,6 +11078,7 @@ class WebTerminalManager {
|
|
|
10946
11078
|
cwd: info.cwd,
|
|
10947
11079
|
tags: info.tags,
|
|
10948
11080
|
manual: info.manual,
|
|
11081
|
+
...info.attachTmux ? { attachTmux: info.attachTmux } : {},
|
|
10949
11082
|
mirrorSessionId: mirror?.sessionId,
|
|
10950
11083
|
claudeSessionId: mirror?.claudeSessionId,
|
|
10951
11084
|
reason: "daemon-gap",
|
|
@@ -11065,12 +11198,17 @@ class WebTerminalManager {
|
|
|
11065
11198
|
return false;
|
|
11066
11199
|
}
|
|
11067
11200
|
},
|
|
11068
|
-
conversationExists: (cwd, claudeSessionId) => existsSync(join(getProjectPath(cwd), `${claudeSessionId}.jsonl`))
|
|
11201
|
+
conversationExists: (cwd, claudeSessionId) => existsSync(join(getProjectPath(cwd), `${claudeSessionId}.jsonl`)),
|
|
11202
|
+
// B-273: only consulted for records that were attach terminals.
|
|
11203
|
+
// Unbounded: the panel's 50-row cap must not hide a restore target.
|
|
11204
|
+
userSessions: record.attachTmux ? this.listUserTmuxSessions(Infinity) : [],
|
|
11205
|
+
attachSocket: process.env[VH_TMUX_SOCKET_ENV],
|
|
11206
|
+
homeDir: os$1.homedir()
|
|
11069
11207
|
});
|
|
11070
11208
|
if (plan.kind === "already-live") return { type: "success", terminalId };
|
|
11071
11209
|
if (plan.kind === "error") return { type: "error", reason: plan.reason };
|
|
11072
11210
|
if (!this.createDetachedTerminal(plan)) return { type: "error", reason: "create-failed" };
|
|
11073
|
-
logger.info(`[WEB TERMINAL] restored closed terminal ${terminalId} in ${plan.cwd}${plan.command ? " (conversation resumed)" : ""}`);
|
|
11211
|
+
logger.info(`[WEB TERMINAL] restored closed terminal ${terminalId} in ${plan.cwd}${plan.attachTmux ? ` (attached tmux session ${plan.attachTmux})` : plan.command ? " (conversation resumed)" : ""}`);
|
|
11074
11212
|
this.kickListRefresh();
|
|
11075
11213
|
return { type: "success", terminalId };
|
|
11076
11214
|
}
|
|
@@ -11130,6 +11268,13 @@ class WebTerminalManager {
|
|
|
11130
11268
|
{ stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
11131
11269
|
);
|
|
11132
11270
|
}
|
|
11271
|
+
if (plan.attachTmux) {
|
|
11272
|
+
spawnSync(
|
|
11273
|
+
"tmux",
|
|
11274
|
+
tmuxArgs(["set-option", "-t", `=${name}:`, "@vh_attach", plan.attachTmux]),
|
|
11275
|
+
{ stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
11276
|
+
);
|
|
11277
|
+
}
|
|
11133
11278
|
if (plan.command) {
|
|
11134
11279
|
for (const argv of startupInjectionArgs(name, plan.command)) {
|
|
11135
11280
|
spawnSync("tmux", tmuxArgs(argv), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env });
|
|
@@ -11142,7 +11287,7 @@ class WebTerminalManager {
|
|
|
11142
11287
|
* would care about changed (see liveSnapshotChanged). */
|
|
11143
11288
|
persistLiveSnapshot(live, now) {
|
|
11144
11289
|
const candidate = /* @__PURE__ */ new Map();
|
|
11145
|
-
for (const [id, info] of live) candidate.set(id, { title: info.title, cwd: info.cwd, tags: info.tags, manual: info.manual, seenAt: now });
|
|
11290
|
+
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 });
|
|
11146
11291
|
if (!liveSnapshotChanged(this.lastPersistedSnapshot, candidate)) return;
|
|
11147
11292
|
this.lastPersistedSnapshot = candidate;
|
|
11148
11293
|
saveLiveSnapshot(candidate);
|
|
@@ -11240,6 +11385,7 @@ class WebTerminalManager {
|
|
|
11240
11385
|
const lines = opts.streamMode === "lines";
|
|
11241
11386
|
const existing = this.terminals.get(id);
|
|
11242
11387
|
if (existing) {
|
|
11388
|
+
const echo2 = opts.attachTmux && !(opts.attachOnly || opts.resub) && existing.attachedTmux?.name === opts.attachTmux.name ? { attachedTmux: existing.attachedTmux } : {};
|
|
11243
11389
|
if (!opts.resub || existing.subscribers === 0) existing.subscribers += 1;
|
|
11244
11390
|
existing.lastTouch = Date.now();
|
|
11245
11391
|
this.applyResize(existing, cols, rows);
|
|
@@ -11252,15 +11398,16 @@ class WebTerminalManager {
|
|
|
11252
11398
|
seq: existing.seq,
|
|
11253
11399
|
mode: "replay",
|
|
11254
11400
|
chunks,
|
|
11255
|
-
...lines ? { streamMode: "lines" } : {}
|
|
11401
|
+
...lines ? { streamMode: "lines" } : {},
|
|
11402
|
+
...echo2
|
|
11256
11403
|
};
|
|
11257
11404
|
}
|
|
11258
11405
|
if (lines && existing.transport.kind === "control") {
|
|
11259
|
-
return await this.linesSnapshotResponse(id, existing);
|
|
11406
|
+
return { ...await this.linesSnapshotResponse(id, existing), ...echo2 };
|
|
11260
11407
|
}
|
|
11261
11408
|
const state = existing.subscribeState(opts.fromSeq);
|
|
11262
11409
|
logger.debug(`[WEB TERMINAL] re-subscribed ${id} (subs=${existing.subscribers}, mode=${state.mode}, seq=${state.seq})`);
|
|
11263
|
-
return { terminalId: id, tmuxSession: existing.tmuxSession, ...state };
|
|
11410
|
+
return { terminalId: id, tmuxSession: existing.tmuxSession, ...state, ...echo2 };
|
|
11264
11411
|
}
|
|
11265
11412
|
if (opts.attachOnly || opts.resub) {
|
|
11266
11413
|
const alive = isTmuxAvailable() && spawnSync(
|
|
@@ -11285,6 +11432,10 @@ class WebTerminalManager {
|
|
|
11285
11432
|
let file = defaultShell();
|
|
11286
11433
|
let args = [];
|
|
11287
11434
|
let tmuxSession;
|
|
11435
|
+
let attachedTmux;
|
|
11436
|
+
if (opts.attachTmux && !(opts.attachOnly || opts.resub) && !isTmuxAvailable()) {
|
|
11437
|
+
throw new Error("tmux-unavailable");
|
|
11438
|
+
}
|
|
11288
11439
|
if (isTmuxAvailable()) {
|
|
11289
11440
|
tmuxSession = `vh-${id}`;
|
|
11290
11441
|
let createdNew = false;
|
|
@@ -11297,6 +11448,7 @@ class WebTerminalManager {
|
|
|
11297
11448
|
`VH_HAPPY_HOME_DIR=${configuration.happyHomeDir}`
|
|
11298
11449
|
] : [];
|
|
11299
11450
|
const attachOnly = !!(opts.attachOnly || opts.resub);
|
|
11451
|
+
const attachTarget = !attachOnly && opts.attachTmux ? this.resolveAttachTarget(opts.attachTmux) : void 0;
|
|
11300
11452
|
if (!attachOnly) {
|
|
11301
11453
|
try {
|
|
11302
11454
|
const created = spawnSync(
|
|
@@ -11338,16 +11490,15 @@ class WebTerminalManager {
|
|
|
11338
11490
|
}
|
|
11339
11491
|
}
|
|
11340
11492
|
if (createdNew) {
|
|
11341
|
-
|
|
11342
|
-
|
|
11343
|
-
|
|
11344
|
-
|
|
11345
|
-
|
|
11346
|
-
|
|
11347
|
-
|
|
11348
|
-
|
|
11349
|
-
|
|
11350
|
-
}
|
|
11493
|
+
attachedTmux = this.injectIntoCreated(tmuxSession, attachTarget, opts.startupCommand, env);
|
|
11494
|
+
} else if (opts.attachTmux && !attachOnly) {
|
|
11495
|
+
const stored = spawnSync(
|
|
11496
|
+
"tmux",
|
|
11497
|
+
tmuxArgs(["show-options", "-qv", "-t", `=${tmuxSession}:`, "@vh_attach"]),
|
|
11498
|
+
{ encoding: "utf8", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
11499
|
+
);
|
|
11500
|
+
const name = stored.status === 0 && typeof stored.stdout === "string" ? stored.stdout.replace(/\r?\n$/, "") : "";
|
|
11501
|
+
if (name && name === opts.attachTmux.name) attachedTmux = { id: opts.attachTmux.id, name };
|
|
11351
11502
|
}
|
|
11352
11503
|
if (!attachOnly && !createdNew) {
|
|
11353
11504
|
const alive = spawnSync(
|
|
@@ -11362,17 +11513,7 @@ class WebTerminalManager {
|
|
|
11362
11513
|
{ stdio: "ignore", timeout: TMUX_CREATE_TIMEOUT_MS, env }
|
|
11363
11514
|
);
|
|
11364
11515
|
createdNew = retry.status === 0;
|
|
11365
|
-
if (createdNew)
|
|
11366
|
-
const startup = normalizeStartupCommand(opts.startupCommand);
|
|
11367
|
-
if (startup) {
|
|
11368
|
-
for (const a of startupInjectionArgs(tmuxSession, startup)) {
|
|
11369
|
-
try {
|
|
11370
|
-
spawnSync("tmux", tmuxArgs(a), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env });
|
|
11371
|
-
} catch {
|
|
11372
|
-
}
|
|
11373
|
-
}
|
|
11374
|
-
}
|
|
11375
|
-
}
|
|
11516
|
+
if (createdNew) attachedTmux = this.injectIntoCreated(tmuxSession, attachTarget, opts.startupCommand, env);
|
|
11376
11517
|
}
|
|
11377
11518
|
}
|
|
11378
11519
|
if (!attachOnly && !createdNew) {
|
|
@@ -11382,6 +11523,7 @@ class WebTerminalManager {
|
|
|
11382
11523
|
{ stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
11383
11524
|
).status === 0;
|
|
11384
11525
|
if (!alive) {
|
|
11526
|
+
if (attachTarget) throw new Error("tmux-unavailable");
|
|
11385
11527
|
logger.info(`[WEB TERMINAL] tmux session creation unavailable; using direct PTY for ${id}`);
|
|
11386
11528
|
tmuxSession = void 0;
|
|
11387
11529
|
}
|
|
@@ -11464,20 +11606,110 @@ class WebTerminalManager {
|
|
|
11464
11606
|
session = created;
|
|
11465
11607
|
}
|
|
11466
11608
|
session.subscribers = 1;
|
|
11609
|
+
if (attachedTmux) session.attachedTmux = attachedTmux;
|
|
11467
11610
|
this.terminals.set(id, session);
|
|
11468
11611
|
session.onTitleChange(() => this.kickListRefresh());
|
|
11469
11612
|
logger.debug(`[WEB TERMINAL] opened ${id} (${tmuxSession ? "control" : "pty"}) ${cols}x${rows} cwd=${cwd}`);
|
|
11470
11613
|
this.kickListRefresh();
|
|
11614
|
+
const echo = attachedTmux ? { attachedTmux } : {};
|
|
11471
11615
|
if (session.transport.kind !== "control") {
|
|
11472
11616
|
const state = session.subscribeState(void 0);
|
|
11473
|
-
return { terminalId: id, tmuxSession, ...state, ...lines ? { streamMode: "lines" } : {} };
|
|
11617
|
+
return { terminalId: id, tmuxSession, ...state, ...lines ? { streamMode: "lines" } : {}, ...echo };
|
|
11474
11618
|
}
|
|
11475
11619
|
const restored = await this.captureRestore(session, true);
|
|
11476
11620
|
if (!lines) {
|
|
11477
11621
|
const state = session.subscribeState(void 0);
|
|
11478
|
-
return { terminalId: id, tmuxSession, ...state };
|
|
11622
|
+
return { terminalId: id, tmuxSession, ...state, ...echo };
|
|
11623
|
+
}
|
|
11624
|
+
return { ...this.linesResponse(id, session, restored), ...echo };
|
|
11625
|
+
}
|
|
11626
|
+
/**
|
|
11627
|
+
* Post-create injection shared by BOTH creation sites in open(): an attach
|
|
11628
|
+
* request (B-273) pins the title to the user session's name (the inner
|
|
11629
|
+
* tmux's pane title is not reliable), stores `@vh_attach` for close
|
|
11630
|
+
* records / restore and types the attach line; otherwise the ordinary
|
|
11631
|
+
* startup command runs. Returns what to echo as `attachedTmux`.
|
|
11632
|
+
*/
|
|
11633
|
+
injectIntoCreated(tmuxSession, attachTarget, startupCommand, env) {
|
|
11634
|
+
if (attachTarget) {
|
|
11635
|
+
for (const a of [
|
|
11636
|
+
["set-option", "-t", `=${tmuxSession}:`, "@vh_title", attachTarget.name],
|
|
11637
|
+
["set-option", "-t", `=${tmuxSession}:`, "@vh_title_manual", "1"],
|
|
11638
|
+
["set-option", "-t", `=${tmuxSession}:`, "@vh_attach", attachTarget.name],
|
|
11639
|
+
...startupInjectionArgs(tmuxSession, attachStartupCommand(attachTarget.id, process.env[VH_TMUX_SOCKET_ENV]))
|
|
11640
|
+
]) {
|
|
11641
|
+
try {
|
|
11642
|
+
spawnSync("tmux", tmuxArgs(a), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env });
|
|
11643
|
+
} catch {
|
|
11644
|
+
}
|
|
11645
|
+
}
|
|
11646
|
+
logger.debug(`[WEB TERMINAL] ${tmuxSession} attaches user tmux session ${attachTarget.id} (${attachTarget.name})`);
|
|
11647
|
+
return { id: attachTarget.id, name: attachTarget.name };
|
|
11648
|
+
}
|
|
11649
|
+
const startup = normalizeStartupCommand(startupCommand);
|
|
11650
|
+
if (startup) {
|
|
11651
|
+
try {
|
|
11652
|
+
for (const a of startupInjectionArgs(tmuxSession, startup)) {
|
|
11653
|
+
spawnSync("tmux", tmuxArgs(a), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env });
|
|
11654
|
+
}
|
|
11655
|
+
logger.debug(`[WEB TERMINAL] injected startup command into new session ${tmuxSession}`);
|
|
11656
|
+
} catch {
|
|
11657
|
+
}
|
|
11658
|
+
}
|
|
11659
|
+
return void 0;
|
|
11660
|
+
}
|
|
11661
|
+
/**
|
|
11662
|
+
* B-273: check an attach request against the live tmux server. The id
|
|
11663
|
+
* must be a real `$N` (session ids only reset when the tmux server
|
|
11664
|
+
* restarts, so a stale id from an older list simply does not exist)
|
|
11665
|
+
* whose CURRENT name equals the name the web saw — the name cross-check
|
|
11666
|
+
* is what refuses a target that was killed and re-created in between —
|
|
11667
|
+
* and must not be a vh-* web terminal. Throws the 'tmux-session-gone'
|
|
11668
|
+
* contract string. Note `display-message -t '$N'` exits 0 with an EMPTY
|
|
11669
|
+
* line for an unknown id (its target lookup may fail silently), which is
|
|
11670
|
+
* why existence is probed with `has-session` first.
|
|
11671
|
+
*/
|
|
11672
|
+
resolveAttachTarget(req) {
|
|
11673
|
+
const id = typeof req?.id === "string" ? req.id : "";
|
|
11674
|
+
const name = typeof req?.name === "string" ? req.name : "";
|
|
11675
|
+
if (!TMUX_SESSION_ID_RE.test(id) || !isSafeTmuxSessionName(name) || isVhSessionName(name)) throw new Error("tmux-session-gone");
|
|
11676
|
+
const env = ptyEnv();
|
|
11677
|
+
const exists = spawnSync("tmux", tmuxArgs(["has-session", "-t", id]), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }).status === 0;
|
|
11678
|
+
if (!exists) throw new Error("tmux-session-gone");
|
|
11679
|
+
const r = spawnSync(
|
|
11680
|
+
"tmux",
|
|
11681
|
+
tmuxArgs(["display-message", "-p", "-t", id, "#{session_name}"]),
|
|
11682
|
+
{ encoding: "utf8", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
11683
|
+
);
|
|
11684
|
+
const current = r.status === 0 && typeof r.stdout === "string" ? r.stdout.replace(/\r?\n$/, "") : void 0;
|
|
11685
|
+
if (current !== name) throw new Error("tmux-session-gone");
|
|
11686
|
+
return { id, name };
|
|
11687
|
+
}
|
|
11688
|
+
/** B-273: the user's own tmux sessions (never vh-*), newest activity
|
|
11689
|
+
* first. Empty when tmux is missing or no server runs. */
|
|
11690
|
+
/** Last raw outcome of the list-tmux-sessions spawn (diagnostics only). */
|
|
11691
|
+
lastUserSessionsProbe;
|
|
11692
|
+
listUserTmuxSessions(max) {
|
|
11693
|
+
if (!isTmuxAvailable()) {
|
|
11694
|
+
this.lastUserSessionsProbe = { status: null, error: "tmux-unavailable", stdoutBytes: 0 };
|
|
11695
|
+
return [];
|
|
11696
|
+
}
|
|
11697
|
+
try {
|
|
11698
|
+
const r = spawnSync(
|
|
11699
|
+
"tmux",
|
|
11700
|
+
tmuxArgs(["list-sessions", "-F", USER_SESSIONS_FORMAT]),
|
|
11701
|
+
{ encoding: "utf8", timeout: TMUX_CREATE_TIMEOUT_MS, env: ptyEnv() }
|
|
11702
|
+
);
|
|
11703
|
+
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 };
|
|
11704
|
+
if (r.status !== 0 || typeof r.stdout !== "string") {
|
|
11705
|
+
logger.debug(`[WEB TERMINAL] list-tmux-sessions failed: ${JSON.stringify(this.lastUserSessionsProbe)}`);
|
|
11706
|
+
return [];
|
|
11707
|
+
}
|
|
11708
|
+
return parseUserSessions(r.stdout, max);
|
|
11709
|
+
} catch (e) {
|
|
11710
|
+
this.lastUserSessionsProbe = { status: null, error: e instanceof Error ? e.message : String(e), stdoutBytes: 0 };
|
|
11711
|
+
return [];
|
|
11479
11712
|
}
|
|
11480
|
-
return this.linesResponse(id, session, restored);
|
|
11481
11713
|
}
|
|
11482
11714
|
/**
|
|
11483
11715
|
* Run the opening/catch-up capture for a control session and prime the
|
|
@@ -11719,6 +11951,7 @@ class WebTerminalManager {
|
|
|
11719
11951
|
cwd: info.cwd,
|
|
11720
11952
|
tags: info.tags,
|
|
11721
11953
|
manual: info.manual,
|
|
11954
|
+
...info.attachTmux ? { attachTmux: info.attachTmux } : {},
|
|
11722
11955
|
mirrorSessionId: this.mirrorResolver?.(terminalId) ?? mirror?.sessionId,
|
|
11723
11956
|
claudeSessionId: mirror?.claudeSessionId,
|
|
11724
11957
|
reason: "closed",
|
|
@@ -11807,7 +12040,8 @@ class WebTerminalManager {
|
|
|
11807
12040
|
// Daemon-internal (not pushed to web / not in the signature):
|
|
11808
12041
|
// the mirror reconciler's stricter "claude is really here" gate.
|
|
11809
12042
|
claudeConfident: probe.claudeConfident,
|
|
11810
|
-
manual: s.manual
|
|
12043
|
+
manual: s.manual,
|
|
12044
|
+
...s.attachTmux ? { attachTmux: s.attachTmux } : {}
|
|
11811
12045
|
});
|
|
11812
12046
|
}
|
|
11813
12047
|
return out;
|
|
@@ -11874,18 +12108,19 @@ class WebTerminalManager {
|
|
|
11874
12108
|
try {
|
|
11875
12109
|
const probe = spawnSync(
|
|
11876
12110
|
"tmux",
|
|
11877
|
-
tmuxArgs(["display-message", "-p", "-t", name, "#{pane_in_mode}
|
|
12111
|
+
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)]),
|
|
11878
12112
|
{ encoding: "utf8", timeout: TMUX_PROBE_TIMEOUT_MS, env: ptyEnv() }
|
|
11879
12113
|
);
|
|
11880
12114
|
if (probe.status !== 0 || typeof probe.stdout !== "string") return;
|
|
11881
|
-
const [inMode, altOn, wantsMouse, paneW, paneH, paneCmd] = probe.stdout.trim().split(
|
|
12115
|
+
const [inMode, altOn, wantsMouse, paneW, paneH, paneCmd] = probe.stdout.trim().split(LIST_FIELD_SEP);
|
|
11882
12116
|
const action = planScrollAction(
|
|
11883
12117
|
inMode === "1",
|
|
11884
12118
|
altOn === "1",
|
|
11885
12119
|
wantsMouse === "1",
|
|
11886
12120
|
lines,
|
|
11887
12121
|
looksLikeClaudeCommand(paneCmd || ""),
|
|
11888
|
-
Number(paneH) || 24
|
|
12122
|
+
Number(paneH) || 24,
|
|
12123
|
+
(paneCmd || "").trim() === "tmux"
|
|
11889
12124
|
);
|
|
11890
12125
|
if (action.kind === "none") return;
|
|
11891
12126
|
if (action.kind === "mouse-wheel") {
|
|
@@ -13759,6 +13994,7 @@ class ApiMachineClient {
|
|
|
13759
13994
|
lastKnownResumeSupport = null;
|
|
13760
13995
|
cliUpdateState = null;
|
|
13761
13996
|
cliUpdatePushChain = Promise.resolve();
|
|
13997
|
+
claudeAuthState = null;
|
|
13762
13998
|
rpcHandlerManager;
|
|
13763
13999
|
resumeSessionHandler = null;
|
|
13764
14000
|
restartSessionHandler = null;
|
|
@@ -13825,6 +14061,44 @@ class ApiMachineClient {
|
|
|
13825
14061
|
if (!this.socket?.connected) return;
|
|
13826
14062
|
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));
|
|
13827
14063
|
}
|
|
14064
|
+
/**
|
|
14065
|
+
* B-276: publish the daemon-context Claude auth preflight into daemonState
|
|
14066
|
+
* (same serialized push chain as the CLI update policy). Resolves true when
|
|
14067
|
+
* the server acknowledged the write with our value in it, false otherwise
|
|
14068
|
+
* (rate-limit `result:'error'` is swallowed by updateDaemonState, so the
|
|
14069
|
+
* caller checks the echoed state and re-sends on the next probe).
|
|
14070
|
+
*/
|
|
14071
|
+
setClaudeAuthState(state) {
|
|
14072
|
+
this.claudeAuthState = state;
|
|
14073
|
+
if (!this.socket?.connected) return Promise.resolve(false);
|
|
14074
|
+
const attempt = this.cliUpdatePushChain.then(() => this.updateDaemonState((current) => ({
|
|
14075
|
+
...current ?? {},
|
|
14076
|
+
status: current?.status ?? "running",
|
|
14077
|
+
claudeAuth: state
|
|
14078
|
+
}))).then(() => this.machine.daemonState?.claudeAuth?.checkedAt === state.checkedAt && this.machine.daemonState?.claudeAuth?.daemonPid === state.daemonPid).catch((error) => {
|
|
14079
|
+
logger.debug("[API MACHINE] Failed to publish Claude auth state:", error);
|
|
14080
|
+
return false;
|
|
14081
|
+
});
|
|
14082
|
+
this.cliUpdatePushChain = attempt.then(() => void 0);
|
|
14083
|
+
return attempt;
|
|
14084
|
+
}
|
|
14085
|
+
/** B-276 machine RPCs. Responses always carry `claudeAuth` so the web can refresh in place. */
|
|
14086
|
+
setClaudeAuthHandlers(handlers) {
|
|
14087
|
+
this.rpcHandlerManager.registerHandler("claude-auth-probe", async () => {
|
|
14088
|
+
const claudeAuth = await handlers.probe();
|
|
14089
|
+
return { claudeAuth };
|
|
14090
|
+
});
|
|
14091
|
+
this.rpcHandlerManager.registerHandler("claude-auth-repair", async (params) => {
|
|
14092
|
+
const action = typeof params?.action === "string" ? params.action : "";
|
|
14093
|
+
return handlers.repair(action);
|
|
14094
|
+
});
|
|
14095
|
+
this.rpcHandlerManager.registerHandler("claude-auth-set-store", async (params) => {
|
|
14096
|
+
const store = params?.store === "file" ? "file" : params?.store === "auto" ? "auto" : null;
|
|
14097
|
+
if (!store) return { error: "invalid-store", claudeAuth: this.claudeAuthState };
|
|
14098
|
+
const claudeAuth = await handlers.setStore(store);
|
|
14099
|
+
return { claudeAuth };
|
|
14100
|
+
});
|
|
14101
|
+
}
|
|
13828
14102
|
setRPCHandlers({
|
|
13829
14103
|
spawnSession,
|
|
13830
14104
|
resumeSession,
|
|
@@ -13858,7 +14132,8 @@ class ApiMachineClient {
|
|
|
13858
14132
|
this.syncResumeSessionRpcRegistration();
|
|
13859
14133
|
this.syncRestartSessionRpcRegistration();
|
|
13860
14134
|
this.rpcHandlerManager.registerHandler("open-terminal", async (params) => {
|
|
13861
|
-
const { terminalId, cols, rows, cwd, fromSeq, encStream, startupCommand, resub, attachOnly, streamMode } = params || {};
|
|
14135
|
+
const { terminalId, cols, rows, cwd, fromSeq, encStream, startupCommand, resub, attachOnly, streamMode, attachTmux } = params || {};
|
|
14136
|
+
const attach = attachTmux && typeof attachTmux === "object" && typeof attachTmux.id === "string" && typeof attachTmux.name === "string" ? { id: attachTmux.id, name: attachTmux.name } : void 0;
|
|
13862
14137
|
const result = await this.webTerminal.open({
|
|
13863
14138
|
terminalId,
|
|
13864
14139
|
cols,
|
|
@@ -13868,7 +14143,8 @@ class ApiMachineClient {
|
|
|
13868
14143
|
startupCommand,
|
|
13869
14144
|
resub,
|
|
13870
14145
|
attachOnly,
|
|
13871
|
-
streamMode: streamMode === "lines" ? "lines" : void 0
|
|
14146
|
+
streamMode: streamMode === "lines" ? "lines" : void 0,
|
|
14147
|
+
...attach ? { attachTmux: attach } : {}
|
|
13872
14148
|
});
|
|
13873
14149
|
if (encStream) {
|
|
13874
14150
|
this.encTerminals.add(result.terminalId);
|
|
@@ -13943,6 +14219,9 @@ class ApiMachineClient {
|
|
|
13943
14219
|
this.rpcHandlerManager.registerHandler("list-terminals", async () => {
|
|
13944
14220
|
return { type: "success", terminals: this.webTerminal.buildTerminalList() };
|
|
13945
14221
|
});
|
|
14222
|
+
this.rpcHandlerManager.registerHandler("list-tmux-sessions", async () => {
|
|
14223
|
+
return { type: "success", sessions: this.webTerminal.listUserTmuxSessions() };
|
|
14224
|
+
});
|
|
13946
14225
|
this.rpcHandlerManager.registerHandler("set-terminal-title", async (params) => {
|
|
13947
14226
|
const { terminalId, title, ifAbsent } = params || {};
|
|
13948
14227
|
if (!terminalId || typeof title !== "string") {
|
|
@@ -14336,10 +14615,15 @@ class ApiMachineClient {
|
|
|
14336
14615
|
// B-265 capability flag; restamped every connect so the
|
|
14337
14616
|
// web's `detectedAt >= startedAt` trust rule holds.
|
|
14338
14617
|
terminalRestore: { rpcAvailable: true, detectedAt: now },
|
|
14618
|
+
// B-273 capability flag (same restamp discipline).
|
|
14619
|
+
tmuxSessions: { rpcAvailable: true, detectedAt: now },
|
|
14339
14620
|
// B-084: closed records survive daemon restarts (persisted
|
|
14340
14621
|
// in closed-terminals.json), so the connect snapshot ships
|
|
14341
14622
|
// them too — not just the incremental pushes.
|
|
14342
|
-
closedTerminals: this.webTerminal.getClosedTerminals()
|
|
14623
|
+
closedTerminals: this.webTerminal.getClosedTerminals(),
|
|
14624
|
+
// B-276: re-ship the last preflight so a reconnect never
|
|
14625
|
+
// leaves a stale-pid value visible longer than needed.
|
|
14626
|
+
...this.claudeAuthState ? { claudeAuth: this.claudeAuthState } : {}
|
|
14343
14627
|
};
|
|
14344
14628
|
});
|
|
14345
14629
|
this.webTerminal.startListTracking((terminals) => this.pushTerminalList(terminals));
|
|
@@ -15674,4 +15958,4 @@ const RawJSONLinesSchema = z$1.discriminatedUnion("type", [
|
|
|
15674
15958
|
}).passthrough()
|
|
15675
15959
|
]);
|
|
15676
15960
|
|
|
15677
|
-
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,
|
|
15961
|
+
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 };
|