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,121 @@
|
|
|
1
|
+
import { serializePaneInput } from '../paneInput.js';
|
|
2
|
+
import { codex, codexExitSessionId } from './codex.js';
|
|
3
|
+
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
|
+
const SHELLS = new Set(['sh', 'bash', 'zsh', 'fish', 'dash', 'ksh', 'tcsh']);
|
|
5
|
+
function throwIfAborted(signal) {
|
|
6
|
+
if (signal.aborted)
|
|
7
|
+
throw signal.reason instanceof Error ? signal.reason : new Error('Activation cancelled');
|
|
8
|
+
}
|
|
9
|
+
export function createCodexConversationActivationController({ panes, process, commands, wait = pause, }) {
|
|
10
|
+
if (!panes || !process || !commands || typeof commands.sendKey !== 'function'
|
|
11
|
+
|| typeof commands.capturePlain !== 'function' || typeof commands.runPaneCommand !== 'function') {
|
|
12
|
+
throw new TypeError('Codex Conversation activation requires pane control');
|
|
13
|
+
}
|
|
14
|
+
const pane = async (paneId) => (await panes.list()).find((item) => item.paneId === paneId) ?? null;
|
|
15
|
+
const codexIdentity = async (paneId) => {
|
|
16
|
+
const current = await pane(paneId);
|
|
17
|
+
if (!current)
|
|
18
|
+
return null;
|
|
19
|
+
const identity = await process.inspectForeground(current);
|
|
20
|
+
if (!identity)
|
|
21
|
+
return null;
|
|
22
|
+
return await codex.process.verify(current, { inspectForeground: async () => identity })
|
|
23
|
+
? identity : null;
|
|
24
|
+
};
|
|
25
|
+
const shellIdentity = async (paneId) => {
|
|
26
|
+
const current = await pane(paneId);
|
|
27
|
+
if (!current)
|
|
28
|
+
return null;
|
|
29
|
+
const identity = await process.inspectForeground(current);
|
|
30
|
+
if (!identity)
|
|
31
|
+
return null;
|
|
32
|
+
const executable = identity.executable?.split('/').pop()?.toLowerCase();
|
|
33
|
+
return SHELLS.has(current.currentCommand.toLowerCase()) || (executable && SHELLS.has(executable))
|
|
34
|
+
? identity : null;
|
|
35
|
+
};
|
|
36
|
+
const sameProcess = (first, second) => (first.pid === second.pid
|
|
37
|
+
&& (first.startedAt === undefined || second.startedAt === first.startedAt)
|
|
38
|
+
&& (first.tty === undefined || second.tty === first.tty));
|
|
39
|
+
return {
|
|
40
|
+
apiVersion: 1,
|
|
41
|
+
async describe(run) {
|
|
42
|
+
if (run.ref.sessionId || run.signal.aborted || !await codexIdentity(run.ref.paneId))
|
|
43
|
+
return null;
|
|
44
|
+
return { effect: 'replace-process-preserve-session' };
|
|
45
|
+
},
|
|
46
|
+
async activate(run, signal) {
|
|
47
|
+
await serializePaneInput(run.ref.paneId, async () => {
|
|
48
|
+
const paneId = run.ref.paneId;
|
|
49
|
+
throwIfAborted(signal);
|
|
50
|
+
const original = !run.ref.sessionId && !run.signal.aborted
|
|
51
|
+
? await codexIdentity(paneId) : null;
|
|
52
|
+
throwIfAborted(signal);
|
|
53
|
+
if (!original)
|
|
54
|
+
throw new Error('The Agent run changed before Conversation activation');
|
|
55
|
+
// Authorization is bound to the verified run above. Exiting that exact process intentionally revokes
|
|
56
|
+
// its lease, so subsequent checks bind recovery to the shell that replaced it.
|
|
57
|
+
let exited = false;
|
|
58
|
+
let shell = null;
|
|
59
|
+
for (let press = 0; press < 2 && !exited; press += 1) {
|
|
60
|
+
throwIfAborted(signal);
|
|
61
|
+
const beforePress = await codexIdentity(paneId);
|
|
62
|
+
if (!beforePress || !sameProcess(original, beforePress)) {
|
|
63
|
+
throw new Error('The Agent process changed before Conversation activation');
|
|
64
|
+
}
|
|
65
|
+
throwIfAborted(signal);
|
|
66
|
+
await commands.sendKey(paneId, 'C-c');
|
|
67
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
68
|
+
await wait(500);
|
|
69
|
+
throwIfAborted(signal);
|
|
70
|
+
if (!await pane(paneId))
|
|
71
|
+
throw new Error('The pane closed during Conversation activation');
|
|
72
|
+
const currentIdentity = await codexIdentity(paneId);
|
|
73
|
+
if (!currentIdentity) {
|
|
74
|
+
shell = await shellIdentity(paneId);
|
|
75
|
+
if (!shell)
|
|
76
|
+
throw new Error('The pane did not return to the expected shell');
|
|
77
|
+
exited = true;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
if (!sameProcess(original, currentIdentity)) {
|
|
81
|
+
throw new Error('The Agent process changed during Conversation activation');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!exited || !shell)
|
|
86
|
+
throw new Error('Codex did not exit; close any open panel in the terminal and try again');
|
|
87
|
+
let sessionId = null;
|
|
88
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
89
|
+
throwIfAborted(signal);
|
|
90
|
+
const beforeCapture = await shellIdentity(paneId);
|
|
91
|
+
if (!beforeCapture || !sameProcess(shell, beforeCapture)) {
|
|
92
|
+
throw new Error('The pane shell changed during Conversation activation');
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const candidate = codexExitSessionId(await commands.capturePlain(paneId));
|
|
96
|
+
if (codex.sessions.isId(candidate)) {
|
|
97
|
+
sessionId = candidate;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
throw new Error('The pane closed during Conversation activation');
|
|
103
|
+
}
|
|
104
|
+
await wait(100);
|
|
105
|
+
}
|
|
106
|
+
if (!sessionId)
|
|
107
|
+
throw new Error('Codex did not expose a resumable session; continue in the terminal');
|
|
108
|
+
const resume = codex.sessions.managedResumeCmd?.(sessionId);
|
|
109
|
+
if (!resume)
|
|
110
|
+
throw new Error('Managed Conversation activation is unavailable');
|
|
111
|
+
throwIfAborted(signal);
|
|
112
|
+
const beforeResume = await shellIdentity(paneId);
|
|
113
|
+
if (!beforeResume || !sameProcess(shell, beforeResume)) {
|
|
114
|
+
throw new Error('The pane shell changed before Conversation activation');
|
|
115
|
+
}
|
|
116
|
+
throwIfAborted(signal);
|
|
117
|
+
await commands.runPaneCommand(paneId, resume);
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { stat as statFile } from 'node:fs/promises';
|
|
2
|
+
import { parseCodexGoal } from '../codexStreamProtocol.js';
|
|
3
|
+
import { readLatestContextUsage } from '../codexUsageSnapshot.js';
|
|
4
|
+
import { resolveCodexRollout, sessionsDir } from './codex.js';
|
|
5
|
+
const CONTEXT_RECOVERY_TTL_MS = 2_000;
|
|
6
|
+
const CONTEXT_RECOVERY_CACHE_LIMIT = 64;
|
|
7
|
+
const record = (value) => (value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null);
|
|
8
|
+
const string = (value) => (typeof value === 'string' && value.trim() ? value.trim() : undefined);
|
|
9
|
+
function target(run) {
|
|
10
|
+
const threadId = run.ref.sessionId;
|
|
11
|
+
if (!threadId)
|
|
12
|
+
throw new Error('Codex conversation control requires a managed thread');
|
|
13
|
+
return { pane: run.ref.paneId, threadId };
|
|
14
|
+
}
|
|
15
|
+
const PERMISSION_SETTINGS = {
|
|
16
|
+
default: {
|
|
17
|
+
approvalPolicy: 'on-request', approvalsReviewer: 'user',
|
|
18
|
+
sandboxPolicy: { type: 'workspaceWrite' },
|
|
19
|
+
},
|
|
20
|
+
'auto-review': {
|
|
21
|
+
approvalPolicy: 'on-request', approvalsReviewer: 'auto_review',
|
|
22
|
+
sandboxPolicy: { type: 'workspaceWrite' },
|
|
23
|
+
},
|
|
24
|
+
'full-access': {
|
|
25
|
+
approvalPolicy: 'never', approvalsReviewer: 'user',
|
|
26
|
+
sandboxPolicy: { type: 'dangerFullAccess' },
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
function permissionMode(settings) {
|
|
30
|
+
const sandbox = string(record(settings.sandboxPolicy)?.type);
|
|
31
|
+
const approval = string(settings.approvalPolicy);
|
|
32
|
+
const reviewer = string(settings.approvalsReviewer);
|
|
33
|
+
if ((sandbox === 'dangerFullAccess' || sandbox === 'danger-full-access') && approval === 'never') {
|
|
34
|
+
return 'full-access';
|
|
35
|
+
}
|
|
36
|
+
if ((sandbox === 'workspaceWrite' || sandbox === 'workspace-write') && approval === 'on-request') {
|
|
37
|
+
return reviewer === 'auto_review' || reviewer === 'guardian_subagent' ? 'auto-review' : 'default';
|
|
38
|
+
}
|
|
39
|
+
return 'custom';
|
|
40
|
+
}
|
|
41
|
+
function contextSnapshot(value) {
|
|
42
|
+
const status = record(value) ?? {};
|
|
43
|
+
const settings = record(status.settings) ?? {};
|
|
44
|
+
const usage = record(status.contextUsage);
|
|
45
|
+
const nativeStatus = record(status.status);
|
|
46
|
+
const flags = Array.isArray(nativeStatus?.activeFlags) ? nativeStatus.activeFlags : [];
|
|
47
|
+
const activity = status.activityKind === 'compacting' ? 'compacting'
|
|
48
|
+
: flags.includes('waitingOnApproval') || flags.includes('waitingOnUserInput')
|
|
49
|
+
|| (Array.isArray(status.approvals) && status.approvals.length > 0)
|
|
50
|
+
|| (Array.isArray(status.userInputs) && status.userInputs.length > 0) ? 'waiting'
|
|
51
|
+
: nativeStatus?.type === 'active' ? 'working' : 'idle';
|
|
52
|
+
const sandbox = string(record(settings.sandboxPolicy)?.type);
|
|
53
|
+
const access = sandbox === 'readOnly' || sandbox === 'read-only' ? 'read-only'
|
|
54
|
+
: sandbox === 'workspaceWrite' || sandbox === 'workspace-write' ? 'workspace-write'
|
|
55
|
+
: sandbox === 'dangerFullAccess' || sandbox === 'danger-full-access' ? 'full-access' : undefined;
|
|
56
|
+
const usedTokens = typeof usage?.usedTokens === 'number' ? usage.usedTokens : undefined;
|
|
57
|
+
const totalTokens = typeof usage?.totalTokens === 'number' ? usage.totalTokens : undefined;
|
|
58
|
+
const cwd = string(settings.cwd);
|
|
59
|
+
const branch = string(status.gitBranch);
|
|
60
|
+
return {
|
|
61
|
+
activity,
|
|
62
|
+
...(usedTokens === undefined ? {} : { usedTokens }),
|
|
63
|
+
...(totalTokens === undefined ? {} : { totalTokens }),
|
|
64
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
65
|
+
...(branch === undefined ? {} : { branch }),
|
|
66
|
+
...(access === undefined ? {} : { access }),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function permissionSnapshot(value) {
|
|
70
|
+
const status = record(value) ?? {};
|
|
71
|
+
const settings = record(status.settings) ?? {};
|
|
72
|
+
return {
|
|
73
|
+
mode: permissionMode(settings),
|
|
74
|
+
options: ['default', 'auto-review', 'full-access'],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function planSnapshot(value) {
|
|
78
|
+
const status = record(value) ?? {};
|
|
79
|
+
const plan = record(status.plan);
|
|
80
|
+
const stepsValue = Array.isArray(plan?.steps) ? plan.steps : Array.isArray(plan?.plan) ? plan.plan : [];
|
|
81
|
+
const steps = stepsValue.flatMap((candidate) => {
|
|
82
|
+
const step = record(candidate);
|
|
83
|
+
const label = string(step?.step);
|
|
84
|
+
const state = ['pending', 'inProgress', 'completed'].find((item) => item === step?.status);
|
|
85
|
+
return label && state ? [{ step: label, status: state }] : [];
|
|
86
|
+
});
|
|
87
|
+
if (!steps.length)
|
|
88
|
+
return null;
|
|
89
|
+
const explanation = string(plan?.explanation);
|
|
90
|
+
const turnId = string(plan?.turnId) ?? string(status.activeTurnId);
|
|
91
|
+
const waiting = contextSnapshot(status).activity === 'waiting';
|
|
92
|
+
return {
|
|
93
|
+
steps,
|
|
94
|
+
...(explanation === undefined ? {} : { explanation }),
|
|
95
|
+
...(turnId === undefined ? {} : { turnId }),
|
|
96
|
+
...(waiting ? { waiting: true } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export function createCodexConversationControls(app, clear, { sessionsRoot = sessionsDir(), findRollout = resolveCodexRollout, reader = readLatestContextUsage, stat = statFile, now = Date.now, } = {}) {
|
|
100
|
+
let cached;
|
|
101
|
+
const recoveredContexts = new Map();
|
|
102
|
+
const storeRecovered = (threadId, entry) => {
|
|
103
|
+
if (recoveredContexts.has(threadId))
|
|
104
|
+
recoveredContexts.delete(threadId);
|
|
105
|
+
if (recoveredContexts.size >= CONTEXT_RECOVERY_CACHE_LIMIT) {
|
|
106
|
+
const oldest = recoveredContexts.keys().next().value;
|
|
107
|
+
if (oldest !== undefined)
|
|
108
|
+
recoveredContexts.delete(oldest);
|
|
109
|
+
}
|
|
110
|
+
recoveredContexts.set(threadId, entry);
|
|
111
|
+
};
|
|
112
|
+
const recoverContext = (threadId) => {
|
|
113
|
+
const checkedAt = now();
|
|
114
|
+
const previous = recoveredContexts.get(threadId);
|
|
115
|
+
if (previous?.inFlight)
|
|
116
|
+
return previous.inFlight;
|
|
117
|
+
if (previous && previous.checkedAt + CONTEXT_RECOVERY_TTL_MS > checkedAt) {
|
|
118
|
+
return Promise.resolve(previous.value);
|
|
119
|
+
}
|
|
120
|
+
let pending;
|
|
121
|
+
pending = (async () => {
|
|
122
|
+
try {
|
|
123
|
+
const file = await findRollout(sessionsRoot, threadId);
|
|
124
|
+
if (!file) {
|
|
125
|
+
storeRecovered(threadId, { checkedAt, file: null, value: null });
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const metadata = await stat(file);
|
|
129
|
+
if (previous?.file === file && previous.size === metadata.size
|
|
130
|
+
&& previous.mtimeMs === metadata.mtimeMs) {
|
|
131
|
+
storeRecovered(threadId, {
|
|
132
|
+
checkedAt,
|
|
133
|
+
file,
|
|
134
|
+
size: metadata.size,
|
|
135
|
+
mtimeMs: metadata.mtimeMs,
|
|
136
|
+
value: previous.value,
|
|
137
|
+
});
|
|
138
|
+
return previous.value;
|
|
139
|
+
}
|
|
140
|
+
const value = await reader(file);
|
|
141
|
+
storeRecovered(threadId, {
|
|
142
|
+
checkedAt, file, size: metadata.size, mtimeMs: metadata.mtimeMs, value,
|
|
143
|
+
});
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
const fallback = previous ?? { checkedAt, file: null, value: null };
|
|
148
|
+
storeRecovered(threadId, {
|
|
149
|
+
checkedAt,
|
|
150
|
+
file: fallback.file,
|
|
151
|
+
...(fallback.size === undefined ? {} : { size: fallback.size }),
|
|
152
|
+
...(fallback.mtimeMs === undefined ? {} : { mtimeMs: fallback.mtimeMs }),
|
|
153
|
+
value: fallback.value,
|
|
154
|
+
});
|
|
155
|
+
return fallback.value;
|
|
156
|
+
}
|
|
157
|
+
})();
|
|
158
|
+
storeRecovered(threadId, {
|
|
159
|
+
checkedAt,
|
|
160
|
+
file: previous?.file ?? null,
|
|
161
|
+
...(previous?.size === undefined ? {} : { size: previous.size }),
|
|
162
|
+
...(previous?.mtimeMs === undefined ? {} : { mtimeMs: previous.mtimeMs }),
|
|
163
|
+
value: previous?.value ?? null,
|
|
164
|
+
inFlight: pending,
|
|
165
|
+
});
|
|
166
|
+
return pending;
|
|
167
|
+
};
|
|
168
|
+
const status = (pane, threadId) => {
|
|
169
|
+
const key = `${pane}\0${threadId}`;
|
|
170
|
+
if (cached?.key === key && cached.expiresAt >= now())
|
|
171
|
+
return cached.value;
|
|
172
|
+
const value = app.status(pane, threadId);
|
|
173
|
+
cached = { key, expiresAt: now() + 100, value };
|
|
174
|
+
void value.catch(() => { if (cached?.value === value)
|
|
175
|
+
cached = undefined; });
|
|
176
|
+
return value;
|
|
177
|
+
};
|
|
178
|
+
const invalidate = () => { cached = undefined; };
|
|
179
|
+
return {
|
|
180
|
+
goal: {
|
|
181
|
+
apiVersion: 1,
|
|
182
|
+
async read(run) {
|
|
183
|
+
const { pane, threadId } = target(run);
|
|
184
|
+
return parseCodexGoal(await app.getGoal(pane, threadId));
|
|
185
|
+
},
|
|
186
|
+
async start(run, objective) {
|
|
187
|
+
const { pane, threadId } = target(run);
|
|
188
|
+
const goal = parseCodexGoal(await app.startGoal(pane, threadId, objective));
|
|
189
|
+
if (!goal)
|
|
190
|
+
throw new Error('Codex returned an invalid Goal');
|
|
191
|
+
invalidate();
|
|
192
|
+
return goal;
|
|
193
|
+
},
|
|
194
|
+
async update(run, patch) {
|
|
195
|
+
const { pane, threadId } = target(run);
|
|
196
|
+
const goal = parseCodexGoal(await app.updateGoal(pane, threadId, patch));
|
|
197
|
+
if (!goal)
|
|
198
|
+
throw new Error('Codex returned an invalid Goal');
|
|
199
|
+
invalidate();
|
|
200
|
+
return goal;
|
|
201
|
+
},
|
|
202
|
+
async clear(run) {
|
|
203
|
+
const { pane, threadId } = target(run);
|
|
204
|
+
await app.clearGoal(pane, threadId);
|
|
205
|
+
invalidate();
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
plan: {
|
|
209
|
+
apiVersion: 1,
|
|
210
|
+
async read(run) {
|
|
211
|
+
const { pane, threadId } = target(run);
|
|
212
|
+
return planSnapshot(await status(pane, threadId));
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
context: {
|
|
216
|
+
apiVersion: 1,
|
|
217
|
+
async read(run) {
|
|
218
|
+
const { pane, threadId } = target(run);
|
|
219
|
+
const snapshot = contextSnapshot(await status(pane, threadId));
|
|
220
|
+
if (snapshot.usedTokens !== undefined && snapshot.totalTokens !== undefined)
|
|
221
|
+
return snapshot;
|
|
222
|
+
const recovered = await recoverContext(threadId);
|
|
223
|
+
return recovered ? {
|
|
224
|
+
...snapshot,
|
|
225
|
+
usedTokens: recovered.usedTokens,
|
|
226
|
+
totalTokens: recovered.totalTokens,
|
|
227
|
+
} : snapshot;
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
permission: {
|
|
231
|
+
apiVersion: 1,
|
|
232
|
+
async read(run) {
|
|
233
|
+
const { pane, threadId } = target(run);
|
|
234
|
+
return permissionSnapshot(await status(pane, threadId));
|
|
235
|
+
},
|
|
236
|
+
async update(run, mode) {
|
|
237
|
+
const { pane, threadId } = target(run);
|
|
238
|
+
await app.updateSettings(pane, threadId, PERMISSION_SETTINGS[mode]);
|
|
239
|
+
invalidate();
|
|
240
|
+
return permissionSnapshot(await status(pane, threadId));
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
commands: {
|
|
244
|
+
apiVersion: 1,
|
|
245
|
+
commands: ['compact', 'clear'],
|
|
246
|
+
async execute(run, command) {
|
|
247
|
+
const { pane, threadId } = target(run);
|
|
248
|
+
if (command === 'compact')
|
|
249
|
+
await app.compact(pane, threadId);
|
|
250
|
+
else
|
|
251
|
+
await clear(pane, threadId);
|
|
252
|
+
invalidate();
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
function decisionId(value) {
|
|
2
|
+
return typeof value === 'string' ? value : value.id;
|
|
3
|
+
}
|
|
4
|
+
function decisionLabel(value) {
|
|
5
|
+
if (typeof value === 'string') {
|
|
6
|
+
return {
|
|
7
|
+
accept: 'Allow once', acceptForSession: 'Allow for session', decline: 'Deny', cancel: 'Cancel',
|
|
8
|
+
}[value] ?? value;
|
|
9
|
+
}
|
|
10
|
+
if (value.type === 'networkPolicy') {
|
|
11
|
+
return `${value.action === 'deny' ? 'Deny' : 'Allow'} network${value.host ? `: ${value.host}` : ''}`;
|
|
12
|
+
}
|
|
13
|
+
return `${value.action === 'deny' ? 'Deny' : 'Allow'} command policy`;
|
|
14
|
+
}
|
|
15
|
+
function approval(value) {
|
|
16
|
+
const sourceId = `approval:${value.id}`;
|
|
17
|
+
const intent = value.type === 'command' ? 'command_approval'
|
|
18
|
+
: value.type === 'file' ? 'file_approval' : 'permission_approval';
|
|
19
|
+
const prompt = value.reason || value.command || value.type;
|
|
20
|
+
const details = [
|
|
21
|
+
...(value.reason ? [{ kind: 'reason', type: 'text', text: value.reason }] : []),
|
|
22
|
+
...(value.command ? [{ kind: 'command', type: 'code', text: value.command }] : []),
|
|
23
|
+
...(value.cwd ? [{
|
|
24
|
+
kind: 'working_directory', type: 'path', text: value.cwd,
|
|
25
|
+
}] : []),
|
|
26
|
+
];
|
|
27
|
+
const options = [...new Map(value.decisions.map((decision) => [
|
|
28
|
+
decisionId(decision), { id: decisionId(decision), label: decisionLabel(decision) },
|
|
29
|
+
])).values()];
|
|
30
|
+
if (!options.length) {
|
|
31
|
+
return {
|
|
32
|
+
sourceId,
|
|
33
|
+
requestId: value.id,
|
|
34
|
+
kind: 'local_only',
|
|
35
|
+
pending: {
|
|
36
|
+
id: sourceId, type: 'local_only', intent, prompt,
|
|
37
|
+
...(details.length ? { details } : {}),
|
|
38
|
+
...(value.correlationId || value.itemId || value.turnId
|
|
39
|
+
? { correlationId: value.correlationId || value.itemId || value.turnId } : {}),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
sourceId,
|
|
45
|
+
requestId: value.id,
|
|
46
|
+
kind: 'approval',
|
|
47
|
+
decisionIds: new Set(options.map((option) => option.id)),
|
|
48
|
+
pending: {
|
|
49
|
+
id: sourceId, type: 'approval', intent, prompt, options,
|
|
50
|
+
...(details.length ? { details } : {}),
|
|
51
|
+
...(value.correlationId || value.itemId || value.turnId
|
|
52
|
+
? { correlationId: value.correlationId || value.itemId || value.turnId } : {}),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function userInput(value) {
|
|
57
|
+
const sourceId = `input:${value.id}`;
|
|
58
|
+
const correlationId = value.correlationId || value.itemId || value.turnId;
|
|
59
|
+
if (!value.questions.length) {
|
|
60
|
+
return {
|
|
61
|
+
sourceId, requestId: value.id, kind: 'local_only',
|
|
62
|
+
pending: {
|
|
63
|
+
id: sourceId, type: 'local_only', intent: 'input_request', prompt: value.id,
|
|
64
|
+
...(correlationId ? { correlationId } : {}),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const inputFields = new Map();
|
|
69
|
+
const fields = [];
|
|
70
|
+
for (const [questionIndex, question] of value.questions.entries()) {
|
|
71
|
+
const fieldId = `field:${questionIndex}`;
|
|
72
|
+
if (question.options) {
|
|
73
|
+
const usableOptions = question.options.filter((option) => option.label.length > 0);
|
|
74
|
+
if (usableOptions.length !== question.options.length || !usableOptions.length)
|
|
75
|
+
continue;
|
|
76
|
+
const optionValues = new Map();
|
|
77
|
+
const options = usableOptions.map((option, optionIndex) => {
|
|
78
|
+
const id = `option:${questionIndex}:${optionIndex}`;
|
|
79
|
+
optionValues.set(id, option.label);
|
|
80
|
+
return {
|
|
81
|
+
id, label: option.label,
|
|
82
|
+
...(option.description ? { description: option.description } : {}),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
inputFields.set(fieldId, {
|
|
86
|
+
questionId: question.id, optionValues,
|
|
87
|
+
...(question.isOther ? { allowOther: true } : {}),
|
|
88
|
+
});
|
|
89
|
+
fields.push({
|
|
90
|
+
id: fieldId, type: 'select', prompt: question.question, options,
|
|
91
|
+
...(question.header ? { label: question.header } : {}),
|
|
92
|
+
...(question.isOther ? { allowOther: true } : {}),
|
|
93
|
+
});
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
inputFields.set(fieldId, { questionId: question.id });
|
|
97
|
+
fields.push({
|
|
98
|
+
id: fieldId, type: question.isSecret ? 'secret' : 'text',
|
|
99
|
+
prompt: question.question,
|
|
100
|
+
...(question.header ? { label: question.header } : {}),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (fields.length !== value.questions.length) {
|
|
104
|
+
return {
|
|
105
|
+
sourceId, requestId: value.id, kind: 'local_only',
|
|
106
|
+
pending: {
|
|
107
|
+
id: sourceId, type: 'local_only', intent: 'input_request',
|
|
108
|
+
prompt: value.questions.map((question) => question.question).join('\n'),
|
|
109
|
+
...(correlationId ? { correlationId } : {}),
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
sourceId, requestId: value.id, kind: 'input', inputFields,
|
|
115
|
+
pending: {
|
|
116
|
+
id: sourceId, type: 'form', intent: 'input_request',
|
|
117
|
+
prompt: value.questions[0].question, fields,
|
|
118
|
+
...(correlationId ? { correlationId } : {}),
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function normalizeSnapshot(snapshot) {
|
|
123
|
+
const values = [
|
|
124
|
+
...snapshot.approvals.map(approval),
|
|
125
|
+
...snapshot.userInputs.map(userInput),
|
|
126
|
+
];
|
|
127
|
+
return new Map(values.map((value) => [value.sourceId, value]));
|
|
128
|
+
}
|
|
129
|
+
function sourceCursor(cursor, ordinal) {
|
|
130
|
+
return `codex-interactions:${cursor}:${ordinal}`;
|
|
131
|
+
}
|
|
132
|
+
function errorReceipt(error) {
|
|
133
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
134
|
+
if (/no longer pending/i.test(message))
|
|
135
|
+
return { status: 'already_resolved' };
|
|
136
|
+
if (/session changed|not managed|stale/i.test(message))
|
|
137
|
+
return { status: 'stale_run' };
|
|
138
|
+
if (/unavailable|bad user input|unsupported|decision/i.test(message)) {
|
|
139
|
+
return { status: 'rejected', reason: 'provider_rejected' };
|
|
140
|
+
}
|
|
141
|
+
return { status: 'unknown', reason: 'temporarily_unavailable' };
|
|
142
|
+
}
|
|
143
|
+
export function createCodexInteractionAdapter(app) {
|
|
144
|
+
if (!app || typeof app.discover !== 'function' || typeof app.observeInteractions !== 'function'
|
|
145
|
+
|| typeof app.decide !== 'function' || typeof app.answerInput !== 'function') {
|
|
146
|
+
throw new TypeError('Codex Interaction adapter requires App Server interaction APIs');
|
|
147
|
+
}
|
|
148
|
+
const bindings = new Map();
|
|
149
|
+
return {
|
|
150
|
+
apiVersion: 1,
|
|
151
|
+
async observeNative(run, sink) {
|
|
152
|
+
const threadId = run.ref.sessionId;
|
|
153
|
+
if (!threadId)
|
|
154
|
+
throw new Error('Codex Interaction run has no thread');
|
|
155
|
+
const discovered = await app.discover(run.ref.paneId);
|
|
156
|
+
if (!discovered?.managed || discovered.threadId !== threadId) {
|
|
157
|
+
throw new Error('Codex Interaction run is not the pane-owned thread');
|
|
158
|
+
}
|
|
159
|
+
let phase = 'opening';
|
|
160
|
+
let current = new Map();
|
|
161
|
+
const buffered = [];
|
|
162
|
+
let tail = Promise.resolve();
|
|
163
|
+
let reset = 0;
|
|
164
|
+
const emitSnapshot = async (snapshot) => {
|
|
165
|
+
if (phase === 'closed' || !Number.isSafeInteger(snapshot.cursor) || snapshot.cursor < 0) {
|
|
166
|
+
throw new Error('Invalid Codex Interaction snapshot');
|
|
167
|
+
}
|
|
168
|
+
const next = normalizeSnapshot(snapshot);
|
|
169
|
+
const events = [];
|
|
170
|
+
for (const [id] of current) {
|
|
171
|
+
if (!next.has(id))
|
|
172
|
+
events.push(snapshot.disconnected
|
|
173
|
+
? { type: 'cancelled', interactionId: id, reason: 'stream_reset' }
|
|
174
|
+
: { type: 'resolved', interactionId: id });
|
|
175
|
+
}
|
|
176
|
+
for (const [id, item] of next) {
|
|
177
|
+
const previous = current.get(id);
|
|
178
|
+
if (!previous || JSON.stringify(previous.pending) !== JSON.stringify(item.pending)) {
|
|
179
|
+
events.push({ type: 'opened', interaction: item.pending });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
current = next;
|
|
183
|
+
binding.interactions = next;
|
|
184
|
+
for (const [index, event] of events.entries()) {
|
|
185
|
+
const cursor = snapshot.disconnected
|
|
186
|
+
? `codex-interactions:reset:${++reset}:${index}`
|
|
187
|
+
: sourceCursor(snapshot.cursor, index + 1);
|
|
188
|
+
await sink({ ...event, sourceCursor: cursor });
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const failClosed = async () => {
|
|
192
|
+
if (phase === 'closed')
|
|
193
|
+
return;
|
|
194
|
+
const pending = [...current.keys()];
|
|
195
|
+
current.clear();
|
|
196
|
+
binding.interactions.clear();
|
|
197
|
+
for (const [index, interactionId] of pending.entries()) {
|
|
198
|
+
try {
|
|
199
|
+
await sink({
|
|
200
|
+
type: 'cancelled', interactionId, reason: 'stream_reset',
|
|
201
|
+
sourceCursor: `codex-interactions:reset:${++reset}:${index}`,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
phase = 'closed';
|
|
209
|
+
observed.close();
|
|
210
|
+
};
|
|
211
|
+
const observed = await app.observeInteractions(run.ref.paneId, threadId, (snapshot) => {
|
|
212
|
+
if (phase === 'opening') {
|
|
213
|
+
buffered.push(structuredClone(snapshot));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
tail = tail.then(async () => {
|
|
217
|
+
await emitSnapshot(snapshot);
|
|
218
|
+
if (snapshot.disconnected) {
|
|
219
|
+
phase = 'closed';
|
|
220
|
+
observed.close();
|
|
221
|
+
}
|
|
222
|
+
}).catch(failClosed);
|
|
223
|
+
});
|
|
224
|
+
current = normalizeSnapshot(observed);
|
|
225
|
+
const previous = bindings.get(run.ref.runId);
|
|
226
|
+
previous?.detachAbort();
|
|
227
|
+
const binding = { run, interactions: current, detachAbort: () => { } };
|
|
228
|
+
const onAbort = () => {
|
|
229
|
+
phase = 'closed';
|
|
230
|
+
observed.close();
|
|
231
|
+
if (bindings.get(run.ref.runId) === binding)
|
|
232
|
+
bindings.delete(run.ref.runId);
|
|
233
|
+
};
|
|
234
|
+
run.signal.addEventListener('abort', onAbort, { once: true });
|
|
235
|
+
binding.detachAbort = () => run.signal.removeEventListener('abort', onAbort);
|
|
236
|
+
bindings.set(run.ref.runId, binding);
|
|
237
|
+
phase = 'live';
|
|
238
|
+
for (const snapshot of buffered.splice(0)) {
|
|
239
|
+
if (snapshot.cursor <= observed.cursor)
|
|
240
|
+
continue;
|
|
241
|
+
tail = tail.then(() => emitSnapshot(snapshot));
|
|
242
|
+
}
|
|
243
|
+
tail = tail.catch(failClosed);
|
|
244
|
+
return {
|
|
245
|
+
checkpoint: {
|
|
246
|
+
sourceCursor: sourceCursor(observed.cursor, 0),
|
|
247
|
+
pending: [...current.values()].map((item) => structuredClone(item.pending)),
|
|
248
|
+
},
|
|
249
|
+
close() {
|
|
250
|
+
if (phase === 'closed')
|
|
251
|
+
return;
|
|
252
|
+
phase = 'closed';
|
|
253
|
+
observed.close();
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
},
|
|
257
|
+
async dispatchResponse(run, request) {
|
|
258
|
+
const threadId = run.ref.sessionId;
|
|
259
|
+
const binding = bindings.get(run.ref.runId);
|
|
260
|
+
if (!threadId || !binding || binding.run !== run || run.signal.aborted) {
|
|
261
|
+
return { status: 'stale_run' };
|
|
262
|
+
}
|
|
263
|
+
const native = binding.interactions.get(request.interactionId);
|
|
264
|
+
if (!native)
|
|
265
|
+
return { status: 'already_resolved' };
|
|
266
|
+
if (native.kind === 'local_only')
|
|
267
|
+
return { status: 'rejected', reason: 'local_only' };
|
|
268
|
+
try {
|
|
269
|
+
if (native.kind === 'approval') {
|
|
270
|
+
if (request.value.type !== 'approval' || !native.decisionIds?.has(request.value.optionId)) {
|
|
271
|
+
return { status: 'rejected', reason: 'invalid_value' };
|
|
272
|
+
}
|
|
273
|
+
await app.decide(run.ref.paneId, threadId, native.requestId, request.value.optionId);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
const answers = inputAnswers(native, request.value);
|
|
277
|
+
if (answers === null)
|
|
278
|
+
return { status: 'rejected', reason: 'invalid_value' };
|
|
279
|
+
await app.answerInput(run.ref.paneId, threadId, native.requestId, answers);
|
|
280
|
+
}
|
|
281
|
+
binding.interactions.delete(request.interactionId);
|
|
282
|
+
return { status: 'accepted' };
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
return errorReceipt(error);
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function inputAnswers(native, value) {
|
|
291
|
+
if (value.type !== 'form' || native.pending.type !== 'form' || !native.inputFields)
|
|
292
|
+
return null;
|
|
293
|
+
if (Object.keys(value.answers).length !== native.inputFields.size)
|
|
294
|
+
return null;
|
|
295
|
+
const answers = {};
|
|
296
|
+
for (const [fieldId, field] of native.inputFields) {
|
|
297
|
+
const raw = value.answers[fieldId];
|
|
298
|
+
if (typeof raw !== 'string' || !raw.length)
|
|
299
|
+
return null;
|
|
300
|
+
const answer = field.optionValues?.get(raw) ?? (field.allowOther ? raw : field.optionValues ? null : raw);
|
|
301
|
+
if (answer === null)
|
|
302
|
+
return null;
|
|
303
|
+
answers[field.questionId] = [answer];
|
|
304
|
+
}
|
|
305
|
+
return answers;
|
|
306
|
+
}
|