handmux 0.23.0 → 0.25.0
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/README.md +31 -8
- package/README.zh-CN.md +31 -8
- package/bin/handmux.js +14 -688
- package/bin/node-version.js +20 -0
- package/dist/bin/handmux.js +1015 -0
- package/dist/connectors/bridgeClient.js +385 -0
- package/dist/connectors/claude/index.js +476 -0
- package/dist/connectors/pi/bridgeClient.js +8 -0
- package/dist/connectors/pi/index.js +851 -0
- package/dist/hooks/handmux-notify.sh +64 -0
- package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
- package/dist/hooks/handmux-write.cjs +264 -0
- package/dist/package.json +127 -0
- package/dist/public/assets/index-CTrqTqtT.css +32 -0
- package/dist/public/assets/index-DmjeMSQA.js +346 -0
- package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
- package/{public → dist/public}/index.html +5 -4
- package/dist/src/agent-runtime/adapter.js +204 -0
- package/dist/src/agent-runtime/bridge.js +1006 -0
- package/dist/src/agent-runtime/bridgeStore.js +25 -0
- package/dist/src/agent-runtime/bridgeTransport.js +619 -0
- package/dist/src/agent-runtime/bridgeTypes.js +13 -0
- package/dist/src/agent-runtime/builtinRuntime.js +193 -0
- package/dist/src/agent-runtime/conversation.js +2179 -0
- package/dist/src/agent-runtime/conversationActivation.js +75 -0
- package/dist/src/agent-runtime/conversationActivity.js +69 -0
- package/dist/src/agent-runtime/conversationControls.js +206 -0
- package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
- package/dist/src/agent-runtime/conversationStore.js +352 -0
- package/dist/src/agent-runtime/conversationTypes.js +1 -0
- package/dist/src/agent-runtime/conversationValidation.js +339 -0
- package/dist/src/agent-runtime/inbox.js +973 -0
- package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
- package/dist/src/agent-runtime/inboxStore.js +25 -0
- package/dist/src/agent-runtime/inboxTypes.js +1 -0
- package/dist/src/agent-runtime/interaction.js +829 -0
- package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
- package/dist/src/agent-runtime/interactionStore.js +18 -0
- package/dist/src/agent-runtime/interactionTypes.js +1 -0
- package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
- package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
- package/dist/src/agent-runtime/resourceTypes.js +1 -0
- package/dist/src/agent-runtime/resources.js +245 -0
- package/dist/src/agent-runtime/run.js +328 -0
- package/dist/src/agent-runtime/runtime.js +793 -0
- package/dist/src/agent-runtime/sessionControl.js +164 -0
- package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
- package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
- package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
- package/dist/src/agents/claude.js +203 -0
- package/dist/src/agents/claudeConversation.js +362 -0
- package/dist/src/agents/claudeInteraction.js +134 -0
- package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
- package/dist/src/agents/codex.js +262 -0
- package/dist/src/agents/codexConversation.js +1103 -0
- package/dist/src/agents/codexConversationActivation.js +121 -0
- package/dist/src/agents/codexConversationControls.js +256 -0
- package/dist/src/agents/codexInteraction.js +306 -0
- package/dist/src/agents/codexSessionControl.js +118 -0
- package/dist/src/agents/codexSubscriptionUsage.js +313 -0
- package/dist/src/agents/codexTerminalControl.js +23 -0
- package/dist/src/agents/conversationProjectionSafety.js +416 -0
- package/dist/src/agents/index.js +39 -0
- package/dist/src/agents/nativeInbox.js +264 -0
- package/dist/src/agents/nativeInboxSources.js +115 -0
- package/dist/src/agents/pi.js +28 -0
- package/dist/src/agents/piConversation.js +523 -0
- package/dist/src/agents/piConversationHistory.js +502 -0
- package/dist/src/agents/piInboxBridge.js +368 -0
- package/dist/src/agents/piSessionControl.js +32 -0
- package/dist/src/agents/processIdentity.js +94 -0
- package/dist/src/agents/scanUtils.js +262 -0
- package/dist/src/apiAccountProviders.js +194 -0
- package/dist/src/apiAccounts.js +592 -0
- package/dist/src/apiErrors.js +86 -0
- package/{src → dist/src}/appName.js +12 -13
- package/dist/src/asr/iflyConfig.js +7 -0
- package/dist/src/asr/iflySign.js +14 -0
- package/dist/src/auth.js +27 -0
- package/dist/src/browser/bootstrap.js +72 -0
- package/dist/src/browser/cookieProfiles.js +413 -0
- package/dist/src/browser/coordinator.js +51 -0
- package/dist/src/browser/hammerheadCompat.js +104 -0
- package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
- package/dist/src/browser/manager.js +560 -0
- package/dist/src/browser/originLabel.js +15 -0
- package/dist/src/browser/profilePersistence.js +235 -0
- package/dist/src/browser/publicProxy.js +176 -0
- package/dist/src/browser/routes.js +159 -0
- package/dist/src/browser/sessionStore.js +72 -0
- package/dist/src/browser/siteVersion.js +121 -0
- package/dist/src/browser/targetPolicy.js +163 -0
- package/{src → dist/src}/browser/worker.js +14 -18
- package/dist/src/browser/workerClient.js +286 -0
- package/dist/src/browser/workerServer.js +91 -0
- package/dist/src/captureBackground.js +95 -0
- package/dist/src/claudeEvents.js +525 -0
- package/dist/src/cli/agentIntegration.js +131 -0
- package/dist/src/cli/agentLauncher.js +26 -0
- package/dist/src/cli/claudeHooks.js +290 -0
- package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
- package/dist/src/cli/cloudflared.js +106 -0
- package/dist/src/cli/codexManaged.js +80 -0
- package/{src → dist/src}/cli/cpolarUrl.js +8 -9
- package/dist/src/cli/drivers.js +98 -0
- package/dist/src/cli/hookScaffold.js +39 -0
- package/dist/src/cli/i18n/en.js +392 -0
- package/{src → dist/src}/cli/i18n/index.js +27 -25
- package/dist/src/cli/i18n/zh.js +391 -0
- package/dist/src/cli/legacyCodexHooks.js +52 -0
- package/{src → dist/src}/cli/natappUrl.js +2 -3
- package/dist/src/cli/options.js +245 -0
- package/dist/src/cli/piExtension.js +155 -0
- package/dist/src/cli/probe.js +16 -0
- package/{src → dist/src}/cli/prompt.js +4 -6
- package/dist/src/cli/pushCmd.js +102 -0
- package/{src → dist/src}/cli/qr.js +19 -19
- package/dist/src/cli/service.js +154 -0
- package/dist/src/cli/setupModel.js +194 -0
- package/dist/src/cli/setupWizard.js +647 -0
- package/dist/src/cli/shortcutEditor.js +351 -0
- package/dist/src/cli/sshTunnel.js +18 -0
- package/dist/src/cli/state.js +117 -0
- package/dist/src/cli/statusLine.js +115 -0
- package/dist/src/cli/supervisor.js +278 -0
- package/dist/src/cli/supervisorHealth.js +21 -0
- package/dist/src/cli/supervisorLaunch.js +148 -0
- package/dist/src/cli/supervisorProcesses.js +62 -0
- package/dist/src/cli/supervisorState.js +62 -0
- package/dist/src/cli/tmuxVersion.js +53 -0
- package/{src → dist/src}/cli/tunlite.js +7 -10
- package/dist/src/cli/tunnelClients.js +72 -0
- package/{src → dist/src}/cli/updateCheck.js +98 -69
- package/{src → dist/src}/cli/urlHost.js +8 -4
- package/dist/src/cli/workspaceCmd.js +347 -0
- package/dist/src/codexAppServer.js +3436 -0
- package/dist/src/codexConversationProjection.js +199 -0
- package/dist/src/codexDiff.js +118 -0
- package/dist/src/codexMessageIdentity.js +18 -0
- package/dist/src/codexPlan.js +30 -0
- package/dist/src/codexQueueProtocol.js +137 -0
- package/dist/src/codexStreamProtocol.js +207 -0
- package/dist/src/codexToolProtocol.js +68 -0
- package/dist/src/codexTranscriptParse.js +835 -0
- package/dist/src/codexUsageSnapshot.js +228 -0
- package/dist/src/config.js +8 -0
- package/{src → dist/src}/docPath.js +22 -24
- package/dist/src/docs.js +337 -0
- package/dist/src/git.js +284 -0
- package/dist/src/healthProtocol.js +97 -0
- package/dist/src/httpApi.js +96 -0
- package/dist/src/jsonStore.js +15 -0
- package/{src → dist/src}/keyNames.js +3 -3
- package/dist/src/notifications.js +111 -0
- package/dist/src/orphans.js +167 -0
- package/dist/src/paneInput.js +42 -0
- package/dist/src/pendingPrompt.js +194 -0
- package/dist/src/previewServer.js +202 -0
- package/dist/src/previews.js +179 -0
- package/dist/src/privateStateStore.js +124 -0
- package/dist/src/projectTask/backup.js +224 -0
- package/dist/src/projectTask/lock.js +108 -0
- package/dist/src/projectTask/migrations.js +90 -0
- package/dist/src/projectTask/routes.js +155 -0
- package/dist/src/projectTask/runtime.js +182 -0
- package/dist/src/projectTask/schema.js +56 -0
- package/dist/src/projectTask/store.js +457 -0
- package/dist/src/push.js +224 -0
- package/dist/src/routes/agents.js +631 -0
- package/dist/src/routes/apiAccounts.js +82 -0
- package/dist/src/routes/files.js +230 -0
- package/dist/src/routes/git.js +89 -0
- package/dist/src/routes/health.js +16 -0
- package/dist/src/routes/notifications.js +16 -0
- package/dist/src/routes/previews.js +43 -0
- package/dist/src/routes/push.js +145 -0
- package/dist/src/routes/sessions.js +244 -0
- package/dist/src/routes/system.js +220 -0
- package/dist/src/routes/terminal.js +244 -0
- package/dist/src/routes/workspace.js +267 -0
- package/dist/src/server.js +315 -0
- package/dist/src/shortcutConfig.js +83 -0
- package/{src → dist/src}/staticCache.js +5 -3
- package/{src → dist/src}/staticCompression.js +5 -6
- package/dist/src/terminalStream.js +441 -0
- package/{src → dist/src}/tmux/commands.js +185 -172
- package/dist/src/tmux/format.js +47 -0
- package/dist/src/transcriptParse.js +261 -0
- package/dist/src/transcriptReader.js +209 -0
- package/{src → dist/src}/trimCapture.js +11 -11
- package/dist/src/uploadTypes.js +31 -0
- package/dist/src/urlPolicy.js +18 -0
- package/dist/src/usagePaths.js +15 -0
- package/dist/src/workspace/agentRunner.js +301 -0
- package/dist/src/workspace/atomicJson.js +31 -0
- package/dist/src/workspace/capture.js +139 -0
- package/dist/src/workspace/checkpointer.js +228 -0
- package/dist/src/workspace/environment.js +71 -0
- package/dist/src/workspace/lock.js +137 -0
- package/dist/src/workspace/mapping.js +128 -0
- package/dist/src/workspace/operations.js +271 -0
- package/dist/src/workspace/paths.js +15 -0
- package/dist/src/workspace/planner.js +292 -0
- package/dist/src/workspace/restore.js +363 -0
- package/dist/src/workspace/runtime.js +318 -0
- package/dist/src/workspace/schema.js +156 -0
- package/dist/src/workspace/store.js +464 -0
- package/dist/src/workspace/tmuxAdapter.js +704 -0
- package/dist/src/workspaceProtocol.js +158 -0
- package/package.json +30 -17
- package/hooks/handmux-notify.sh +0 -22
- package/hooks/handmux-write.cjs +0 -78
- package/public/assets/index-C3N7OkyP.css +0 -32
- package/public/assets/index-D1DWcC5u.js +0 -337
- package/src/.gitkeep +0 -0
- package/src/agents/claude.js +0 -159
- package/src/agents/codex.js +0 -168
- package/src/agents/index.js +0 -21
- package/src/agents/processIdentity.js +0 -72
- package/src/agents/scanUtils.js +0 -225
- package/src/asr/iflyConfig.js +0 -10
- package/src/asr/iflySign.js +0 -16
- package/src/auth.js +0 -30
- package/src/browser/bootstrap.js +0 -68
- package/src/browser/cookieProfiles.js +0 -403
- package/src/browser/coordinator.js +0 -47
- package/src/browser/hammerheadCompat.js +0 -86
- package/src/browser/hammerheadRedirectCompat.js +0 -38
- package/src/browser/manager.js +0 -556
- package/src/browser/originLabel.js +0 -17
- package/src/browser/profilePersistence.js +0 -241
- package/src/browser/publicProxy.js +0 -175
- package/src/browser/routes.js +0 -154
- package/src/browser/sessionStore.js +0 -78
- package/src/browser/siteVersion.js +0 -124
- package/src/browser/targetPolicy.js +0 -137
- package/src/browser/workerClient.js +0 -253
- package/src/browser/workerServer.js +0 -98
- package/src/captureBackground.js +0 -93
- package/src/claudeEvents.js +0 -399
- package/src/cli/claudeHooks.js +0 -206
- package/src/cli/cloudflared.js +0 -99
- package/src/cli/codexManaged.js +0 -77
- package/src/cli/drivers.js +0 -97
- package/src/cli/hookScaffold.js +0 -33
- package/src/cli/i18n/en.js +0 -385
- package/src/cli/i18n/zh.js +0 -384
- package/src/cli/legacyCodexHooks.js +0 -49
- package/src/cli/options.js +0 -211
- package/src/cli/probe.js +0 -16
- package/src/cli/pushCmd.js +0 -60
- package/src/cli/service.js +0 -126
- package/src/cli/setupModel.js +0 -154
- package/src/cli/setupWizard.js +0 -506
- package/src/cli/shortcutEditor.js +0 -288
- package/src/cli/sshTunnel.js +0 -12
- package/src/cli/state.js +0 -90
- package/src/cli/statusLine.js +0 -90
- package/src/cli/supervisor.js +0 -183
- package/src/cli/supervisorProcesses.js +0 -57
- package/src/cli/tmuxVersion.js +0 -49
- package/src/cli/tunnelClients.js +0 -62
- package/src/cli/workspaceCmd.js +0 -300
- package/src/codexAppServer.js +0 -1620
- package/src/codexDiff.js +0 -108
- package/src/codexTranscriptParse.js +0 -413
- package/src/codexUsageSnapshot.cjs +0 -129
- package/src/config.js +0 -9
- package/src/docs.js +0 -228
- package/src/git.js +0 -208
- package/src/httpApi.js +0 -72
- package/src/jsonStore.js +0 -24
- package/src/notifications.js +0 -68
- package/src/orphans.js +0 -152
- package/src/pendingPrompt.js +0 -163
- package/src/previewServer.js +0 -168
- package/src/previews.js +0 -116
- package/src/push.js +0 -183
- package/src/routes/codex.js +0 -473
- package/src/routes/files.js +0 -172
- package/src/routes/git.js +0 -57
- package/src/routes/notifications.js +0 -19
- package/src/routes/previews.js +0 -35
- package/src/routes/push.js +0 -119
- package/src/routes/sessions.js +0 -168
- package/src/routes/system.js +0 -132
- package/src/routes/terminal.js +0 -210
- package/src/routes/transcript.js +0 -171
- package/src/routes/workspace.js +0 -267
- package/src/server.js +0 -186
- package/src/shortcutConfig.js +0 -75
- package/src/terminalStream.js +0 -391
- package/src/tmux/format.js +0 -45
- package/src/transcriptParse.js +0 -180
- package/src/transcriptReader.js +0 -101
- package/src/uploadTypes.js +0 -30
- package/src/urlPolicy.js +0 -14
- package/src/usage.js +0 -260
- package/src/workspace/agentRunner.js +0 -188
- package/src/workspace/atomicJson.js +0 -32
- package/src/workspace/capture.js +0 -139
- package/src/workspace/checkpointer.js +0 -187
- package/src/workspace/environment.js +0 -75
- package/src/workspace/lock.js +0 -128
- package/src/workspace/mapping.js +0 -124
- package/src/workspace/operations.js +0 -252
- package/src/workspace/paths.js +0 -16
- package/src/workspace/planner.js +0 -247
- package/src/workspace/restore.js +0 -279
- package/src/workspace/runtime.js +0 -314
- package/src/workspace/schema.js +0 -136
- package/src/workspace/store.js +0 -382
- package/src/workspace/tmuxAdapter.js +0 -554
- /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
- /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
- /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
- /package/{public → dist/public}/icons/badge-96.png +0 -0
- /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-192.png +0 -0
- /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-512.png +0 -0
- /package/{public → dist/public}/icons/logo.svg +0 -0
- /package/{public → dist/public}/manifest.webmanifest +0 -0
- /package/{public → dist/public}/offline.html +0 -0
- /package/{public → dist/public}/sw.js +0 -0
- /package/{src → dist/src}/browser/protocol.js +0 -0
- /package/{src → dist/src}/cli/cfNamed.js +0 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { createAgentRunner } from './agentRunner.js';
|
|
3
|
+
import { parseTmuxRows, tmuxFormat } from '../tmux/format.js';
|
|
4
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5
|
+
const NO_SERVER_RE = /^(?:no server running on(?: .+)?|no sessions|error connecting to .+ \(no such file or directory\))$/i;
|
|
6
|
+
const MISSING_OPTION_RE = /invalid option|unknown option|not set/i;
|
|
7
|
+
const SESSION_FORMAT = tmuxFormat(['session_id', 'session_name', 'session_last_attached', '@handmux_session_id']);
|
|
8
|
+
const WINDOW_FORMAT = tmuxFormat(['session_id', 'window_id', 'window_index', 'window_name', 'window_active', 'window_layout', '@handmux_window_id']);
|
|
9
|
+
const PANE_FORMAT = tmuxFormat(['window_id', 'pane_id', 'pane_index', 'pane_active', 'pane_current_path', '@handmux_pane_id']);
|
|
10
|
+
const ACTIVE_FORMAT = tmuxFormat(['session_id', 'window_id', 'pane_id']);
|
|
11
|
+
const recordOf = (value) => (value && typeof value === 'object' && !Array.isArray(value)
|
|
12
|
+
? value : null);
|
|
13
|
+
const text = (value) => String(recordOf(value)?.stdout ?? value ?? '');
|
|
14
|
+
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
15
|
+
const byId = (a, b) => compare(a.id ?? '', b.id ?? '');
|
|
16
|
+
const byRuntime = (a, b) => compare(a.runtimeId, b.runtimeId);
|
|
17
|
+
function errorFields(error) {
|
|
18
|
+
return error && typeof error === 'object' ? error : {};
|
|
19
|
+
}
|
|
20
|
+
function isUuid(value) { return typeof value === 'string' && UUID_RE.test(value); }
|
|
21
|
+
function isNoServer(error) {
|
|
22
|
+
const fields = errorFields(error);
|
|
23
|
+
if (fields.code === 'ENOENT')
|
|
24
|
+
return true;
|
|
25
|
+
return NO_SERVER_RE.test(String(fields.stderr || fields.message || error || '').trim());
|
|
26
|
+
}
|
|
27
|
+
function isMissingOption(error) {
|
|
28
|
+
const fields = errorFields(error);
|
|
29
|
+
return MISSING_OPTION_RE.test(String(fields.stderr || fields.message || error || ''));
|
|
30
|
+
}
|
|
31
|
+
function rows(output, columns, label) {
|
|
32
|
+
return parseTmuxRows(text(output), columns, label);
|
|
33
|
+
}
|
|
34
|
+
function field(values, offset, label) {
|
|
35
|
+
const value = values[offset];
|
|
36
|
+
if (value === undefined)
|
|
37
|
+
throw new Error(`missing ${label}`);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function index(value, label) {
|
|
41
|
+
if (!/^\d+$/.test(value))
|
|
42
|
+
throw new Error(`invalid ${label}`);
|
|
43
|
+
return Number(value);
|
|
44
|
+
}
|
|
45
|
+
function active(value, label) {
|
|
46
|
+
if (value !== '0' && value !== '1')
|
|
47
|
+
throw new Error(`invalid ${label}`);
|
|
48
|
+
return value === '1';
|
|
49
|
+
}
|
|
50
|
+
function runtime(value, prefix, label) {
|
|
51
|
+
if (!new RegExp(`^\\${prefix}\\d+$`).test(value))
|
|
52
|
+
throw new Error(`invalid ${label}`);
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
function logicalAllocator(randomUUID) {
|
|
56
|
+
const used = new Set();
|
|
57
|
+
return {
|
|
58
|
+
accept(candidate) {
|
|
59
|
+
const id = candidate;
|
|
60
|
+
if (!isUuid(id) || used.has(id))
|
|
61
|
+
return null;
|
|
62
|
+
used.add(id);
|
|
63
|
+
return id;
|
|
64
|
+
},
|
|
65
|
+
fresh() {
|
|
66
|
+
for (let tries = 0; tries < 100; tries++) {
|
|
67
|
+
const id = randomUUID();
|
|
68
|
+
if (isUuid(id) && !used.has(id)) {
|
|
69
|
+
used.add(id);
|
|
70
|
+
return id;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
throw new Error('could not allocate a unique workspace logical id');
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function requireLogicalId(value, label) {
|
|
78
|
+
if (!isUuid(value))
|
|
79
|
+
throw new Error(`${label} must be a UUID`);
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
function logicalId(item, label) {
|
|
83
|
+
return requireLogicalId(item.id, label);
|
|
84
|
+
}
|
|
85
|
+
function ephemeralLogicalId(option, runtimeId) {
|
|
86
|
+
const hash = crypto.createHash('sha256').update(`${option}\0${runtimeId}`).digest('hex');
|
|
87
|
+
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-${hash.slice(12, 16)}-${hash.slice(16, 20)}-${hash.slice(20, 32)}`;
|
|
88
|
+
}
|
|
89
|
+
function requireCreatedRuntime(value, prefix, label) {
|
|
90
|
+
return runtime(value, prefix, label);
|
|
91
|
+
}
|
|
92
|
+
function isTmuxLayout(value) {
|
|
93
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 8192)
|
|
94
|
+
return false;
|
|
95
|
+
if (!/^[0-9a-f]{4},\d+x\d+,\d+,\d+(?:,\d+|[\[{])/i.test(value))
|
|
96
|
+
return false;
|
|
97
|
+
if (!/^[0-9a-fx,{}\[\]]+$/i.test(value))
|
|
98
|
+
return false;
|
|
99
|
+
const stack = [];
|
|
100
|
+
for (const char of value) {
|
|
101
|
+
if (char === '{' || char === '[')
|
|
102
|
+
stack.push(char);
|
|
103
|
+
else if (char === '}' || char === ']') {
|
|
104
|
+
const expected = char === '}' ? '{' : '[';
|
|
105
|
+
if (stack.pop() !== expected)
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return stack.length === 0;
|
|
110
|
+
}
|
|
111
|
+
function errorText(error) {
|
|
112
|
+
return error instanceof Error ? error.message : String(error);
|
|
113
|
+
}
|
|
114
|
+
function withCleanupError(error, cleanupError) {
|
|
115
|
+
if (!cleanupError)
|
|
116
|
+
return error;
|
|
117
|
+
const combined = new Error(`${errorText(error)}; cleanup failed: ${errorText(cleanupError)}`);
|
|
118
|
+
combined.cause = error;
|
|
119
|
+
return combined;
|
|
120
|
+
}
|
|
121
|
+
export function createdTargetGuard(created) {
|
|
122
|
+
return (target) => {
|
|
123
|
+
if (!created.has(target))
|
|
124
|
+
throw new Error(`workspace target was not created by this restore: ${target}`);
|
|
125
|
+
return target;
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export function createWorkspaceTmux({ run: inputRun, randomUUID = crypto.randomUUID, agentRunner = createAgentRunner(), readOnly = false, } = {}) {
|
|
129
|
+
if (typeof inputRun !== 'function')
|
|
130
|
+
throw new Error('workspace tmux run is required');
|
|
131
|
+
const run = inputRun;
|
|
132
|
+
const created = new Set();
|
|
133
|
+
const guard = createdTargetGuard(created);
|
|
134
|
+
const sessionWindows = new Map();
|
|
135
|
+
const windowSession = new Map();
|
|
136
|
+
const windowPanes = new Map();
|
|
137
|
+
const paneWindow = new Map();
|
|
138
|
+
function ensureWritable() {
|
|
139
|
+
if (readOnly)
|
|
140
|
+
throw new Error('workspace tmux adapter is read-only');
|
|
141
|
+
}
|
|
142
|
+
function trackWindow(sessionId, windowId, paneId) {
|
|
143
|
+
const windows = sessionWindows.get(sessionId) || new Set();
|
|
144
|
+
windows.add(windowId);
|
|
145
|
+
sessionWindows.set(sessionId, windows);
|
|
146
|
+
windowSession.set(windowId, sessionId);
|
|
147
|
+
windowPanes.set(windowId, new Set([paneId]));
|
|
148
|
+
paneWindow.set(paneId, windowId);
|
|
149
|
+
}
|
|
150
|
+
function trackPane(targetPaneId, paneId) {
|
|
151
|
+
const windowId = paneWindow.get(targetPaneId);
|
|
152
|
+
if (!windowId)
|
|
153
|
+
throw new Error(`workspace pane owner is unavailable: ${targetPaneId}`);
|
|
154
|
+
windowPanes.get(windowId)?.add(paneId);
|
|
155
|
+
paneWindow.set(paneId, windowId);
|
|
156
|
+
}
|
|
157
|
+
function forgetWindow(windowId) {
|
|
158
|
+
for (const paneId of windowPanes.get(windowId) || []) {
|
|
159
|
+
created.delete(paneId);
|
|
160
|
+
paneWindow.delete(paneId);
|
|
161
|
+
}
|
|
162
|
+
windowPanes.delete(windowId);
|
|
163
|
+
const sessionId = windowSession.get(windowId);
|
|
164
|
+
if (sessionId)
|
|
165
|
+
sessionWindows.get(sessionId)?.delete(windowId);
|
|
166
|
+
windowSession.delete(windowId);
|
|
167
|
+
created.delete(windowId);
|
|
168
|
+
}
|
|
169
|
+
function forgetSession(sessionId) {
|
|
170
|
+
for (const windowId of sessionWindows.get(sessionId) || [])
|
|
171
|
+
forgetWindow(windowId);
|
|
172
|
+
sessionWindows.delete(sessionId);
|
|
173
|
+
created.delete(sessionId);
|
|
174
|
+
}
|
|
175
|
+
function revokeCreatedTargets() {
|
|
176
|
+
created.clear();
|
|
177
|
+
sessionWindows.clear();
|
|
178
|
+
windowSession.clear();
|
|
179
|
+
windowPanes.clear();
|
|
180
|
+
paneWindow.clear();
|
|
181
|
+
}
|
|
182
|
+
async function cleanupSteps(steps) {
|
|
183
|
+
const failures = [];
|
|
184
|
+
for (const step of steps) {
|
|
185
|
+
try {
|
|
186
|
+
await step();
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
failures.push(errorText(error));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (failures.length)
|
|
193
|
+
throw new Error(failures.join('; '));
|
|
194
|
+
}
|
|
195
|
+
async function cleanupSession(sessionId, windowId, paneId) {
|
|
196
|
+
let killed = false;
|
|
197
|
+
try {
|
|
198
|
+
await cleanupSteps([
|
|
199
|
+
() => run(['set-option', '-u', '-p', '-t', paneId, '@handmux_pane_id']),
|
|
200
|
+
() => run(['set-option', '-u', '-w', '-t', windowId, '@handmux_window_id']),
|
|
201
|
+
() => run(['set-option', '-u', '-t', sessionId, '@handmux_session_id']),
|
|
202
|
+
async () => { await run(['kill-session', '-t', sessionId]); killed = true; },
|
|
203
|
+
]);
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
if (killed)
|
|
207
|
+
forgetSession(sessionId);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function cleanupWindow(windowId, paneId) {
|
|
211
|
+
let killed = false;
|
|
212
|
+
try {
|
|
213
|
+
await cleanupSteps([
|
|
214
|
+
() => run(['set-option', '-u', '-p', '-t', paneId, '@handmux_pane_id']),
|
|
215
|
+
() => run(['set-option', '-u', '-w', '-t', windowId, '@handmux_window_id']),
|
|
216
|
+
async () => { await run(['kill-window', '-t', windowId]); killed = true; },
|
|
217
|
+
]);
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
if (killed)
|
|
221
|
+
forgetWindow(windowId);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function cleanupPane(paneId) {
|
|
225
|
+
let killed = false;
|
|
226
|
+
try {
|
|
227
|
+
await cleanupSteps([
|
|
228
|
+
() => run(['set-option', '-u', '-p', '-t', paneId, '@handmux_pane_id']),
|
|
229
|
+
async () => { await run(['kill-pane', '-t', paneId]); killed = true; },
|
|
230
|
+
]);
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
if (killed) {
|
|
234
|
+
created.delete(paneId);
|
|
235
|
+
const owner = paneWindow.get(paneId);
|
|
236
|
+
if (owner)
|
|
237
|
+
windowPanes.get(owner)?.delete(paneId);
|
|
238
|
+
paneWindow.delete(paneId);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async function observeEnvironment({ readOnly: observeReadOnly = readOnly, } = {}) {
|
|
243
|
+
let current;
|
|
244
|
+
try {
|
|
245
|
+
current = text(await run(['show-options', '-gv', '@handmux_server_id'])).replace(/\r?\n$/, '');
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if (isNoServer(error))
|
|
249
|
+
return { status: 'absent', tmuxServerId: null };
|
|
250
|
+
if (!isMissingOption(error))
|
|
251
|
+
return { status: 'unknown' };
|
|
252
|
+
current = '';
|
|
253
|
+
}
|
|
254
|
+
if (isUuid(current))
|
|
255
|
+
return { status: 'present', tmuxServerId: current };
|
|
256
|
+
const tmuxServerId = randomUUID();
|
|
257
|
+
if (!isUuid(tmuxServerId))
|
|
258
|
+
return { status: 'unknown' };
|
|
259
|
+
if (observeReadOnly)
|
|
260
|
+
return { status: 'present', tmuxServerId };
|
|
261
|
+
try {
|
|
262
|
+
await run(['set-option', '-g', '@handmux_server_id', tmuxServerId]);
|
|
263
|
+
return { status: 'present', tmuxServerId };
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
return isNoServer(error) ? { status: 'absent', tmuxServerId: null } : { status: 'unknown' };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function assignLogicalIds(items, option, scopeArgs, { readOnly: assignReadOnly = readOnly } = {}) {
|
|
270
|
+
const allocator = logicalAllocator(randomUUID);
|
|
271
|
+
for (const item of [...items].sort(byRuntime)) {
|
|
272
|
+
const accepted = allocator.accept(item.optionId);
|
|
273
|
+
const generated = accepted ? null : assignReadOnly
|
|
274
|
+
? allocator.accept(ephemeralLogicalId(option, item.runtimeId))
|
|
275
|
+
: allocator.fresh();
|
|
276
|
+
const allocated = accepted ?? generated;
|
|
277
|
+
if (!allocated)
|
|
278
|
+
throw new Error(`could not allocate a read-only logical id for ${item.runtimeId}`);
|
|
279
|
+
item.id = allocated;
|
|
280
|
+
if (!accepted && !assignReadOnly)
|
|
281
|
+
await run(['set-option', ...scopeArgs, '-t', item.runtimeId, option, item.id]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async function captureTopology({ readOnly: captureReadOnly = readOnly, } = {}) {
|
|
285
|
+
try {
|
|
286
|
+
const environment = await observeEnvironment({ readOnly: captureReadOnly });
|
|
287
|
+
if (environment.status === 'absent')
|
|
288
|
+
return { status: 'empty', tmuxVersion: 'unknown', active: null, sessions: [], windows: [] };
|
|
289
|
+
if (environment.status !== 'present')
|
|
290
|
+
return { status: 'unknown', error: 'tmux environment unavailable' };
|
|
291
|
+
const tmuxVersion = text(await run(['-V'])).trim().replace(/^tmux\s+/, '');
|
|
292
|
+
if (!tmuxVersion)
|
|
293
|
+
throw new Error('invalid tmux version');
|
|
294
|
+
let sessionFields;
|
|
295
|
+
try {
|
|
296
|
+
sessionFields = rows(await run(['list-sessions', '-F', SESSION_FORMAT]), 4, 'session');
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
if (isNoServer(error))
|
|
300
|
+
return { status: 'empty', tmuxVersion: 'unknown', active: null, sessions: [], windows: [] };
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
if (sessionFields.length === 0)
|
|
304
|
+
return { status: 'empty', tmuxVersion: 'unknown', active: null, sessions: [], windows: [] };
|
|
305
|
+
const sessions = sessionFields.map((values) => {
|
|
306
|
+
const runtimeId = field(values, 0, 'session runtime id');
|
|
307
|
+
const name = field(values, 1, 'session name');
|
|
308
|
+
const lastAttached = field(values, 2, 'session last attached');
|
|
309
|
+
const optionId = field(values, 3, 'session logical id option');
|
|
310
|
+
return {
|
|
311
|
+
runtimeId: runtime(runtimeId, '$', 'session runtime id'),
|
|
312
|
+
name,
|
|
313
|
+
lastAttached: index(lastAttached === '' ? '0' : lastAttached, 'session last attached'),
|
|
314
|
+
optionId,
|
|
315
|
+
windowLinks: [],
|
|
316
|
+
activeWindowId: null,
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
if (new Set(sessions.map((item) => item.runtimeId)).size !== sessions.length)
|
|
320
|
+
throw new Error('duplicate session runtime id');
|
|
321
|
+
await assignLogicalIds(sessions, '@handmux_session_id', [], { readOnly: captureReadOnly });
|
|
322
|
+
const sessionByRuntime = new Map(sessions.map((item) => [item.runtimeId, item]));
|
|
323
|
+
const windowFields = rows(await run(['list-windows', '-a', '-F', WINDOW_FORMAT]), 7, 'window');
|
|
324
|
+
const windowLinks = windowFields.map((values) => {
|
|
325
|
+
const sessionRuntimeId = field(values, 0, 'window session runtime id');
|
|
326
|
+
const runtimeId = field(values, 1, 'window runtime id');
|
|
327
|
+
const windowIndex = field(values, 2, 'window index');
|
|
328
|
+
const name = field(values, 3, 'window name');
|
|
329
|
+
const isActive = field(values, 4, 'window active');
|
|
330
|
+
const layout = field(values, 5, 'window layout');
|
|
331
|
+
const optionId = field(values, 6, 'window logical id option');
|
|
332
|
+
if (!sessionByRuntime.has(sessionRuntimeId))
|
|
333
|
+
throw new Error('window references unknown session');
|
|
334
|
+
return {
|
|
335
|
+
sessionRuntimeId, runtimeId: runtime(runtimeId, '@', 'window runtime id'), index: index(windowIndex, 'window index'),
|
|
336
|
+
name, active: active(isActive, 'window active'), layout, optionId,
|
|
337
|
+
};
|
|
338
|
+
});
|
|
339
|
+
const groupedWindows = new Map();
|
|
340
|
+
for (const link of windowLinks) {
|
|
341
|
+
const group = groupedWindows.get(link.runtimeId) || [];
|
|
342
|
+
group.push(link);
|
|
343
|
+
groupedWindows.set(link.runtimeId, group);
|
|
344
|
+
}
|
|
345
|
+
const windows = [...groupedWindows].map(([runtimeId, links]) => {
|
|
346
|
+
const optionIds = new Set(links.map((item) => item.optionId).filter(Boolean));
|
|
347
|
+
if (optionIds.size > 1)
|
|
348
|
+
throw new Error('linked window has conflicting logical ids');
|
|
349
|
+
return { runtimeId, optionId: optionIds.values().next().value || '', links };
|
|
350
|
+
});
|
|
351
|
+
await assignLogicalIds(windows, '@handmux_window_id', ['-w'], { readOnly: captureReadOnly });
|
|
352
|
+
const windowByRuntime = new Map(windows.map((item) => [item.runtimeId, item]));
|
|
353
|
+
for (const window of windows) {
|
|
354
|
+
for (const link of window.links) {
|
|
355
|
+
const session = sessionByRuntime.get(link.sessionRuntimeId);
|
|
356
|
+
if (!session)
|
|
357
|
+
throw new Error('window references unknown session');
|
|
358
|
+
const windowId = logicalId(window, 'window logical id');
|
|
359
|
+
session.windowLinks.push({ windowId, index: link.index });
|
|
360
|
+
if (link.active)
|
|
361
|
+
session.activeWindowId = windowId;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
for (const session of sessions) {
|
|
365
|
+
session.windowLinks.sort((a, b) => a.index - b.index || compare(a.windowId, b.windowId));
|
|
366
|
+
if (!session.windowLinks.length || !session.activeWindowId)
|
|
367
|
+
throw new Error('session has no active linked window');
|
|
368
|
+
}
|
|
369
|
+
const paneFields = rows(await run(['list-panes', '-a', '-F', PANE_FORMAT]), 6, 'pane');
|
|
370
|
+
const paneByRuntime = new Map();
|
|
371
|
+
for (const values of paneFields) {
|
|
372
|
+
const windowRuntimeId = field(values, 0, 'pane window runtime id');
|
|
373
|
+
const runtimeId = field(values, 1, 'pane runtime id');
|
|
374
|
+
const paneIndex = field(values, 2, 'pane index');
|
|
375
|
+
const isActive = field(values, 3, 'pane active');
|
|
376
|
+
const cwd = field(values, 4, 'pane current path');
|
|
377
|
+
const optionId = field(values, 5, 'pane logical id option');
|
|
378
|
+
if (!windowByRuntime.has(windowRuntimeId))
|
|
379
|
+
throw new Error('pane references unknown window');
|
|
380
|
+
const pane = {
|
|
381
|
+
windowRuntimeId, runtimeId: runtime(runtimeId, '%', 'pane runtime id'), index: index(paneIndex, 'pane index'),
|
|
382
|
+
active: active(isActive, 'pane active'), cwd, optionId, agent: null,
|
|
383
|
+
};
|
|
384
|
+
const existing = paneByRuntime.get(pane.runtimeId);
|
|
385
|
+
if (existing) {
|
|
386
|
+
const fields = (item) => [item.windowRuntimeId, item.index, item.active, item.cwd, item.optionId];
|
|
387
|
+
if (JSON.stringify(fields(existing)) !== JSON.stringify(fields(pane))) {
|
|
388
|
+
throw new Error('duplicate pane runtime id has conflicting fields');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
paneByRuntime.set(pane.runtimeId, pane);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const panes = [...paneByRuntime.values()];
|
|
396
|
+
await assignLogicalIds(panes, '@handmux_pane_id', ['-p'], { readOnly: captureReadOnly });
|
|
397
|
+
const canonicalSessions = sessions.sort(byId);
|
|
398
|
+
const canonicalWindows = windows.map((window) => {
|
|
399
|
+
const owner = [...window.links].sort((a, b) => compare(logicalId(sessionByRuntime.get(a.sessionRuntimeId) ?? {}, 'owner session logical id'), logicalId(sessionByRuntime.get(b.sessionRuntimeId) ?? {}, 'owner session logical id')))[0];
|
|
400
|
+
if (!owner)
|
|
401
|
+
throw new Error('window has no owner session');
|
|
402
|
+
const windowPanes = panes.filter((pane) => pane.windowRuntimeId === window.runtimeId).sort(byId);
|
|
403
|
+
const activePane = windowPanes.find((pane) => pane.active);
|
|
404
|
+
if (!activePane)
|
|
405
|
+
throw new Error('window has no active pane');
|
|
406
|
+
return {
|
|
407
|
+
id: logicalId(window, 'window logical id'), runtimeId: window.runtimeId, name: owner.name, index: owner.index, layout: owner.layout,
|
|
408
|
+
activePaneId: logicalId(activePane, 'active pane logical id'),
|
|
409
|
+
panes: windowPanes.map((pane) => ({
|
|
410
|
+
id: logicalId(pane, 'pane logical id'), runtimeId: pane.runtimeId, index: pane.index, cwd: pane.cwd, agent: pane.agent,
|
|
411
|
+
})),
|
|
412
|
+
};
|
|
413
|
+
}).sort(byId);
|
|
414
|
+
const maxAttached = Math.max(...canonicalSessions.map((session) => session.lastAttached));
|
|
415
|
+
const selected = canonicalSessions.find((session) => session.lastAttached === maxAttached);
|
|
416
|
+
if (!selected)
|
|
417
|
+
throw new Error('tmux has no selected session');
|
|
418
|
+
const activeFields = rows(await run(['display-message', '-p', '-t', selected.runtimeId, ACTIVE_FORMAT]), 3, 'active path')[0];
|
|
419
|
+
if (!activeFields)
|
|
420
|
+
throw new Error('tmux has no active path');
|
|
421
|
+
const activeSessionRuntime = field(activeFields, 0, 'active session runtime id');
|
|
422
|
+
const activeWindowRuntime = field(activeFields, 1, 'active window runtime id');
|
|
423
|
+
const activePaneRuntime = field(activeFields, 2, 'active pane runtime id');
|
|
424
|
+
const activeSession = sessionByRuntime.get(activeSessionRuntime);
|
|
425
|
+
const activeWindow = windowByRuntime.get(activeWindowRuntime);
|
|
426
|
+
const activePane = panes.find((pane) => pane.runtimeId === activePaneRuntime);
|
|
427
|
+
if (!activeSession || !activeWindow || !activePane || activePane.windowRuntimeId !== activeWindowRuntime)
|
|
428
|
+
throw new Error('invalid active path');
|
|
429
|
+
return {
|
|
430
|
+
status: 'ok', tmuxVersion,
|
|
431
|
+
active: {
|
|
432
|
+
sessionId: logicalId(activeSession, 'active session logical id'),
|
|
433
|
+
windowId: logicalId(activeWindow, 'active window logical id'),
|
|
434
|
+
paneId: logicalId(activePane, 'active pane logical id'),
|
|
435
|
+
},
|
|
436
|
+
sessions: canonicalSessions.map((session) => ({
|
|
437
|
+
id: logicalId(session, 'session logical id'), runtimeId: session.runtimeId, name: session.name,
|
|
438
|
+
windowLinks: session.windowLinks, activeWindowId: requireLogicalId(session.activeWindowId, 'active window logical id'),
|
|
439
|
+
})),
|
|
440
|
+
windows: canonicalWindows,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
return { status: 'unknown', error: errorText(error) };
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
async function createTemporarySession({ cwd, sessionLogicalId, windowLogicalId, paneLogicalId, windowName, windowIndex, }) {
|
|
448
|
+
ensureWritable();
|
|
449
|
+
requireLogicalId(sessionLogicalId, 'sessionLogicalId');
|
|
450
|
+
const hasSeed = [windowLogicalId, paneLogicalId, windowName, windowIndex].some((value) => value !== undefined);
|
|
451
|
+
if (hasSeed) {
|
|
452
|
+
requireLogicalId(windowLogicalId, 'windowLogicalId');
|
|
453
|
+
requireLogicalId(paneLogicalId, 'paneLogicalId');
|
|
454
|
+
if (typeof windowName !== 'string' || !windowName)
|
|
455
|
+
throw new Error('windowName must be a non-empty string');
|
|
456
|
+
if (typeof windowIndex !== 'number' || !Number.isInteger(windowIndex) || windowIndex < 0) {
|
|
457
|
+
throw new Error('windowIndex must be a non-negative integer');
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const name = `hm-r-${randomUUID().replaceAll('-', '').slice(0, 8)}`;
|
|
461
|
+
if (!/^hm-r-[0-9a-f]{8}$/i.test(name))
|
|
462
|
+
throw new Error('could not allocate temporary session name');
|
|
463
|
+
const args = ['new-session', '-d', '-P', '-F', tmuxFormat(['session_id', 'window_id', 'pane_id', 'window_index']), '-s', name];
|
|
464
|
+
if (hasSeed)
|
|
465
|
+
args.push('-n', windowName);
|
|
466
|
+
args.push('-c', cwd);
|
|
467
|
+
const output = await run(args);
|
|
468
|
+
let sessionId;
|
|
469
|
+
let windowId;
|
|
470
|
+
let paneId;
|
|
471
|
+
let seedIndex;
|
|
472
|
+
try {
|
|
473
|
+
const parsed = rows(output, 4, 'created session')[0];
|
|
474
|
+
if (!parsed)
|
|
475
|
+
throw new Error('tmux did not return created session ids');
|
|
476
|
+
sessionId = field(parsed, 0, 'created session id');
|
|
477
|
+
windowId = field(parsed, 1, 'created window id');
|
|
478
|
+
paneId = field(parsed, 2, 'created pane id');
|
|
479
|
+
requireCreatedRuntime(sessionId, '$', 'created session id');
|
|
480
|
+
requireCreatedRuntime(windowId, '@', 'created window id');
|
|
481
|
+
requireCreatedRuntime(paneId, '%', 'created pane id');
|
|
482
|
+
seedIndex = index(field(parsed, 3, 'created window index'), 'created window index');
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
let cleanupError;
|
|
486
|
+
try {
|
|
487
|
+
await run(['kill-session', '-t', `=${name}`]);
|
|
488
|
+
}
|
|
489
|
+
catch (failure) {
|
|
490
|
+
cleanupError = failure;
|
|
491
|
+
}
|
|
492
|
+
throw withCleanupError(error, cleanupError);
|
|
493
|
+
}
|
|
494
|
+
created.add(sessionId);
|
|
495
|
+
created.add(windowId);
|
|
496
|
+
created.add(paneId);
|
|
497
|
+
trackWindow(sessionId, windowId, paneId);
|
|
498
|
+
try {
|
|
499
|
+
const targetIndex = hasSeed ? windowIndex : 9999;
|
|
500
|
+
if (seedIndex !== targetIndex)
|
|
501
|
+
await run(['move-window', '-s', guard(windowId), '-t', `${guard(sessionId)}:${targetIndex}`]);
|
|
502
|
+
if (hasSeed) {
|
|
503
|
+
await run(['set-option', '-p', '-t', guard(paneId), '@handmux_pane_id', paneLogicalId]);
|
|
504
|
+
await run(['set-option', '-w', '-t', guard(windowId), '@handmux_window_id', windowLogicalId]);
|
|
505
|
+
}
|
|
506
|
+
// Set the session id last: after this succeeds the helper has no remaining fallible setup step.
|
|
507
|
+
await run(['set-option', '-t', guard(sessionId), '@handmux_session_id', sessionLogicalId]);
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
let cleanupError;
|
|
511
|
+
try {
|
|
512
|
+
await cleanupSession(sessionId, windowId, paneId);
|
|
513
|
+
}
|
|
514
|
+
catch (failure) {
|
|
515
|
+
cleanupError = failure;
|
|
516
|
+
}
|
|
517
|
+
throw withCleanupError(error, cleanupError);
|
|
518
|
+
}
|
|
519
|
+
return { sessionId, windowId, paneId, name };
|
|
520
|
+
}
|
|
521
|
+
async function createWindow(sessionId, { name, index: windowIndex, cwd, windowLogicalId, paneLogicalId, }) {
|
|
522
|
+
ensureWritable();
|
|
523
|
+
guard(sessionId);
|
|
524
|
+
if (!Number.isInteger(windowIndex) || windowIndex < 0)
|
|
525
|
+
throw new Error('window index must be a non-negative integer');
|
|
526
|
+
requireLogicalId(windowLogicalId, 'windowLogicalId');
|
|
527
|
+
requireLogicalId(paneLogicalId, 'paneLogicalId');
|
|
528
|
+
const parsed = rows(await run(['new-window', '-d', '-P', '-F', tmuxFormat(['window_id', 'pane_id']), '-t', `${sessionId}:${windowIndex}`, '-n', name, '-c', cwd]), 2, 'created window')[0];
|
|
529
|
+
if (!parsed)
|
|
530
|
+
throw new Error('tmux did not return created window ids');
|
|
531
|
+
const windowId = field(parsed, 0, 'created window id');
|
|
532
|
+
const paneId = field(parsed, 1, 'created pane id');
|
|
533
|
+
requireCreatedRuntime(windowId, '@', 'created window id');
|
|
534
|
+
requireCreatedRuntime(paneId, '%', 'created pane id');
|
|
535
|
+
created.add(windowId);
|
|
536
|
+
created.add(paneId);
|
|
537
|
+
trackWindow(sessionId, windowId, paneId);
|
|
538
|
+
try {
|
|
539
|
+
await run(['set-option', '-p', '-t', guard(paneId), '@handmux_pane_id', paneLogicalId]);
|
|
540
|
+
await run(['set-option', '-w', '-t', guard(windowId), '@handmux_window_id', windowLogicalId]);
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
let cleanupError;
|
|
544
|
+
try {
|
|
545
|
+
await cleanupWindow(windowId, paneId);
|
|
546
|
+
}
|
|
547
|
+
catch (failure) {
|
|
548
|
+
cleanupError = failure;
|
|
549
|
+
}
|
|
550
|
+
throw withCleanupError(error, cleanupError);
|
|
551
|
+
}
|
|
552
|
+
return { windowId, paneId };
|
|
553
|
+
}
|
|
554
|
+
async function splitPane(targetPaneId, { cwd, paneLogicalId }) {
|
|
555
|
+
ensureWritable();
|
|
556
|
+
guard(targetPaneId);
|
|
557
|
+
requireLogicalId(paneLogicalId, 'paneLogicalId');
|
|
558
|
+
const paneId = text(await run(['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', targetPaneId, '-c', cwd])).trim();
|
|
559
|
+
requireCreatedRuntime(paneId, '%', 'created pane id');
|
|
560
|
+
created.add(paneId);
|
|
561
|
+
trackPane(targetPaneId, paneId);
|
|
562
|
+
try {
|
|
563
|
+
await run(['set-option', '-p', '-t', guard(paneId), '@handmux_pane_id', paneLogicalId]);
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
let cleanupError;
|
|
567
|
+
try {
|
|
568
|
+
await cleanupPane(paneId);
|
|
569
|
+
}
|
|
570
|
+
catch (failure) {
|
|
571
|
+
cleanupError = failure;
|
|
572
|
+
}
|
|
573
|
+
throw withCleanupError(error, cleanupError);
|
|
574
|
+
}
|
|
575
|
+
return paneId;
|
|
576
|
+
}
|
|
577
|
+
async function linkWindow(windowId, sessionId, windowIndex, { existing = false } = {}) {
|
|
578
|
+
ensureWritable();
|
|
579
|
+
if (existing)
|
|
580
|
+
runtime(windowId, '@', 'existing window id');
|
|
581
|
+
else
|
|
582
|
+
guard(windowId);
|
|
583
|
+
guard(sessionId);
|
|
584
|
+
if (!Number.isInteger(windowIndex) || windowIndex < 0)
|
|
585
|
+
throw new Error('window index must be a non-negative integer');
|
|
586
|
+
await run(['link-window', '-s', windowId, '-t', `${sessionId}:${windowIndex}`]);
|
|
587
|
+
}
|
|
588
|
+
async function applyLayout(windowId, layout) {
|
|
589
|
+
ensureWritable();
|
|
590
|
+
const target = guard(windowId);
|
|
591
|
+
if (!isTmuxLayout(layout))
|
|
592
|
+
throw new Error('invalid workspace layout');
|
|
593
|
+
await run(['select-layout', '-t', target, layout]);
|
|
594
|
+
}
|
|
595
|
+
async function selectPane(paneId) { ensureWritable(); await run(['select-pane', '-t', guard(paneId)]); }
|
|
596
|
+
async function selectWindow(windowId) { ensureWritable(); await run(['select-window', '-t', guard(windowId)]); }
|
|
597
|
+
async function selectWindowInSession(sessionId, windowIndex) {
|
|
598
|
+
ensureWritable();
|
|
599
|
+
guard(sessionId);
|
|
600
|
+
if (!Number.isInteger(windowIndex) || windowIndex < 0)
|
|
601
|
+
throw new Error('window index must be a non-negative integer');
|
|
602
|
+
await run(['select-window', '-t', `${sessionId}:${windowIndex}`]);
|
|
603
|
+
}
|
|
604
|
+
async function renameCreatedSession(sessionId, name) {
|
|
605
|
+
ensureWritable();
|
|
606
|
+
await run(['rename-session', '-t', guard(sessionId), name]);
|
|
607
|
+
}
|
|
608
|
+
async function killCreatedSession(sessionId) {
|
|
609
|
+
ensureWritable();
|
|
610
|
+
guard(sessionId);
|
|
611
|
+
let killed = false;
|
|
612
|
+
try {
|
|
613
|
+
await cleanupSteps([
|
|
614
|
+
() => run(['set-option', '-u', '-t', sessionId, '@handmux_session_id']),
|
|
615
|
+
async () => { await run(['kill-session', '-t', sessionId]); killed = true; },
|
|
616
|
+
]);
|
|
617
|
+
}
|
|
618
|
+
finally {
|
|
619
|
+
if (killed)
|
|
620
|
+
forgetSession(sessionId);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
async function killCreatedWindow(windowId) {
|
|
624
|
+
ensureWritable();
|
|
625
|
+
await run(['kill-window', '-t', guard(windowId)]);
|
|
626
|
+
forgetWindow(windowId);
|
|
627
|
+
}
|
|
628
|
+
async function startAgent(paneId, cmd, args = []) {
|
|
629
|
+
ensureWritable();
|
|
630
|
+
guard(paneId);
|
|
631
|
+
const valid = cmd === 'claude'
|
|
632
|
+
? args.length === 2 && args[0] === '--resume' && isUuid(args[1])
|
|
633
|
+
: cmd === 'codex' && args.length === 2 && args[0] === 'resume' && isUuid(args[1]);
|
|
634
|
+
if (!valid)
|
|
635
|
+
throw new Error('unsafe agent command token');
|
|
636
|
+
await agentRunner.prepare({ paneId, cmd, args });
|
|
637
|
+
let foregroundStarted = false;
|
|
638
|
+
try {
|
|
639
|
+
await run(['send-keys', '-t', paneId, '-l', '--', agentRunner.command]);
|
|
640
|
+
await run(['send-keys', '-t', paneId, 'Enter']);
|
|
641
|
+
foregroundStarted = true;
|
|
642
|
+
const ready = await agentRunner.waitReady(paneId);
|
|
643
|
+
if (ready?.status !== 'ready')
|
|
644
|
+
throw new Error(ready?.error || 'agent failed before readiness');
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
const cleanupFailures = [];
|
|
648
|
+
if (foregroundStarted) {
|
|
649
|
+
try {
|
|
650
|
+
await run(['send-keys', '-t', paneId, 'C-c']);
|
|
651
|
+
}
|
|
652
|
+
catch (failure) {
|
|
653
|
+
cleanupFailures.push(failure);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (typeof agentRunner.cancel === 'function') {
|
|
657
|
+
try {
|
|
658
|
+
await agentRunner.cancel(paneId);
|
|
659
|
+
}
|
|
660
|
+
catch (failure) {
|
|
661
|
+
cleanupFailures.push(failure);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const cleanupError = cleanupFailures.length
|
|
665
|
+
? new Error(cleanupFailures.map(errorText).join('; '))
|
|
666
|
+
: null;
|
|
667
|
+
throw withCleanupError(error, cleanupError);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function waitForAgents(commands) {
|
|
671
|
+
const waits = new Map();
|
|
672
|
+
for (const command of new Set(commands)) {
|
|
673
|
+
if (command !== 'claude' && command !== 'codex')
|
|
674
|
+
throw new Error('unsafe agent command token');
|
|
675
|
+
waits.set(command, agentRunner.waitForExecutable(command));
|
|
676
|
+
}
|
|
677
|
+
return waits;
|
|
678
|
+
}
|
|
679
|
+
async function topologyFingerprint() {
|
|
680
|
+
const topology = await captureTopology();
|
|
681
|
+
if (topology.status === 'unknown')
|
|
682
|
+
return topology;
|
|
683
|
+
return crypto.createHash('sha256').update(JSON.stringify(topology)).digest('hex');
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
observeEnvironment,
|
|
687
|
+
captureTopology,
|
|
688
|
+
createTemporarySession,
|
|
689
|
+
createWindow,
|
|
690
|
+
splitPane,
|
|
691
|
+
linkWindow,
|
|
692
|
+
applyLayout,
|
|
693
|
+
selectPane,
|
|
694
|
+
selectWindow,
|
|
695
|
+
selectWindowInSession,
|
|
696
|
+
renameCreatedSession,
|
|
697
|
+
killCreatedSession,
|
|
698
|
+
killCreatedWindow,
|
|
699
|
+
startAgent,
|
|
700
|
+
waitForAgents,
|
|
701
|
+
topologyFingerprint,
|
|
702
|
+
revokeCreatedTargets,
|
|
703
|
+
};
|
|
704
|
+
}
|