@provos/ironcurtain 0.5.0 → 0.6.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 +66 -66
- package/dist/cli.js +9 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/types.d.ts +6 -0
- package/dist/docker/adapters/claude-code.js +44 -19
- package/dist/docker/adapters/claude-code.js.map +1 -1
- package/dist/docker/agent-adapter.d.ts +6 -0
- package/dist/docker/agent-adapter.js +6 -1
- package/dist/docker/agent-adapter.js.map +1 -1
- package/dist/docker/docker-agent-session.d.ts +1 -1
- package/dist/docker/docker-agent-session.js +3 -2
- package/dist/docker/docker-agent-session.js.map +1 -1
- package/dist/docker/docker-infrastructure.js +10 -3
- package/dist/docker/docker-infrastructure.js.map +1 -1
- package/dist/docker/mitm-proxy.d.ts +5 -2
- package/dist/docker/mitm-proxy.js +169 -46
- package/dist/docker/mitm-proxy.js.map +1 -1
- package/dist/docker/oauth-credentials.d.ts +15 -2
- package/dist/docker/oauth-credentials.js +92 -8
- package/dist/docker/oauth-credentials.js.map +1 -1
- package/dist/docker/oauth-token-manager.d.ts +80 -0
- package/dist/docker/oauth-token-manager.js +133 -0
- package/dist/docker/oauth-token-manager.js.map +1 -0
- package/dist/docker/orientation.d.ts +1 -1
- package/dist/docker/orientation.js +2 -1
- package/dist/docker/orientation.js.map +1 -1
- package/dist/docker/pty-session.d.ts +2 -0
- package/dist/docker/pty-session.js +105 -9
- package/dist/docker/pty-session.js.map +1 -1
- package/dist/escalation/escalation-watcher.js +2 -0
- package/dist/escalation/escalation-watcher.js.map +1 -1
- package/dist/escalation/listener-command.js +9 -58
- package/dist/escalation/listener-command.js.map +1 -1
- package/dist/escalation/listener-lock.d.ts +20 -0
- package/dist/escalation/listener-lock.js +69 -0
- package/dist/escalation/listener-lock.js.map +1 -0
- package/dist/index.js +21 -1
- package/dist/index.js.map +1 -1
- package/dist/mux/mux-app.d.ts +24 -0
- package/dist/mux/mux-app.js +445 -0
- package/dist/mux/mux-app.js.map +1 -0
- package/dist/mux/mux-command.d.ts +7 -0
- package/dist/mux/mux-command.js +93 -0
- package/dist/mux/mux-command.js.map +1 -0
- package/dist/mux/mux-escalation-manager.d.ts +48 -0
- package/dist/mux/mux-escalation-manager.js +162 -0
- package/dist/mux/mux-escalation-manager.js.map +1 -0
- package/dist/mux/mux-input-handler.d.ts +49 -0
- package/dist/mux/mux-input-handler.js +457 -0
- package/dist/mux/mux-input-handler.js.map +1 -0
- package/dist/mux/mux-renderer.d.ts +86 -0
- package/dist/mux/mux-renderer.js +758 -0
- package/dist/mux/mux-renderer.js.map +1 -0
- package/dist/mux/mux-splash.d.ts +17 -0
- package/dist/mux/mux-splash.js +322 -0
- package/dist/mux/mux-splash.js.map +1 -0
- package/dist/mux/pty-bridge.d.ts +79 -0
- package/dist/mux/pty-bridge.js +151 -0
- package/dist/mux/pty-bridge.js.map +1 -0
- package/dist/mux/trusted-input.d.ts +27 -0
- package/dist/mux/trusted-input.js +27 -0
- package/dist/mux/trusted-input.js.map +1 -0
- package/dist/mux/types.d.ts +92 -0
- package/dist/mux/types.js +45 -0
- package/dist/mux/types.js.map +1 -0
- package/dist/sandbox/index.js +6 -0
- package/dist/sandbox/index.js.map +1 -1
- package/dist/session/agent-session.js +3 -0
- package/dist/session/agent-session.js.map +1 -1
- package/dist/session/index.js +14 -7
- package/dist/session/index.js.map +1 -1
- package/dist/session/types.d.ts +6 -0
- package/dist/session/workspace-validation.d.ts +17 -0
- package/dist/session/workspace-validation.js +65 -0
- package/dist/session/workspace-validation.js.map +1 -0
- package/dist/trusted-process/auto-approver.d.ts +13 -4
- package/dist/trusted-process/auto-approver.js +12 -5
- package/dist/trusted-process/auto-approver.js.map +1 -1
- package/dist/trusted-process/escalation.js +5 -0
- package/dist/trusted-process/escalation.js.map +1 -1
- package/dist/trusted-process/index.js +23 -0
- package/dist/trusted-process/index.js.map +1 -1
- package/dist/trusted-process/mcp-proxy-server.d.ts +0 -2
- package/dist/trusted-process/mcp-proxy-server.js +82 -26
- package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
- package/dist/trusted-process/path-utils.d.ts +15 -0
- package/dist/trusted-process/path-utils.js +38 -0
- package/dist/trusted-process/path-utils.js.map +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MuxEscalationManager -- manages escalation watchers for all PTY sessions.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the existing EscalationWatcher and ListenerState modules for the
|
|
5
|
+
* mux context. Creates watchers directly when sessions are spawned, and
|
|
6
|
+
* optionally polls the PTY registry for externally-spawned sessions.
|
|
7
|
+
*/
|
|
8
|
+
import { mkdirSync } from 'node:fs';
|
|
9
|
+
import { getPtyRegistryDir } from '../config/paths.js';
|
|
10
|
+
import { readActiveRegistrations } from '../escalation/session-registry.js';
|
|
11
|
+
import { createEscalationWatcher } from '../escalation/escalation-watcher.js';
|
|
12
|
+
import { createInitialState, addSession, removeSession, addEscalation, resolveEscalation, expireEscalation, } from '../escalation/listener-state.js';
|
|
13
|
+
/** Poll interval for the session registry directory (ms). */
|
|
14
|
+
const REGISTRY_POLL_INTERVAL_MS = 1000;
|
|
15
|
+
/**
|
|
16
|
+
* Creates a new MuxEscalationManager.
|
|
17
|
+
*/
|
|
18
|
+
export function createMuxEscalationManager() {
|
|
19
|
+
let state = createInitialState();
|
|
20
|
+
const changeCallbacks = [];
|
|
21
|
+
const discoveryCallbacks = [];
|
|
22
|
+
let registryPollInterval = null;
|
|
23
|
+
// Track session IDs that we manage (spawned by mux)
|
|
24
|
+
const managedSessionIds = new Set();
|
|
25
|
+
// Tombstones: removed sessions that may still linger in the registry briefly
|
|
26
|
+
const removedSessionIds = new Set();
|
|
27
|
+
function notifyChange() {
|
|
28
|
+
for (const cb of changeCallbacks)
|
|
29
|
+
cb();
|
|
30
|
+
}
|
|
31
|
+
function createWatcherForSession(sessionId, escalationDir) {
|
|
32
|
+
return createEscalationWatcher(escalationDir, {
|
|
33
|
+
onEscalation: (request) => {
|
|
34
|
+
state = addEscalation(state, sessionId, request);
|
|
35
|
+
notifyChange();
|
|
36
|
+
// BEL to alert user
|
|
37
|
+
process.stderr.write('\x07');
|
|
38
|
+
},
|
|
39
|
+
onEscalationExpired: (escalationId) => {
|
|
40
|
+
state = expireEscalation(state, sessionId, escalationId);
|
|
41
|
+
notifyChange();
|
|
42
|
+
},
|
|
43
|
+
}, { pollIntervalMs: 300 });
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
get state() {
|
|
47
|
+
return state;
|
|
48
|
+
},
|
|
49
|
+
get pendingCount() {
|
|
50
|
+
return state.pendingEscalations.size;
|
|
51
|
+
},
|
|
52
|
+
addSession(registration) {
|
|
53
|
+
managedSessionIds.add(registration.sessionId);
|
|
54
|
+
const watcher = createWatcherForSession(registration.sessionId, registration.escalationDir);
|
|
55
|
+
state = addSession(state, registration, watcher);
|
|
56
|
+
watcher.start();
|
|
57
|
+
notifyChange();
|
|
58
|
+
},
|
|
59
|
+
removeSession(sessionId) {
|
|
60
|
+
const session = state.sessions.get(sessionId);
|
|
61
|
+
session?.watcher.stop();
|
|
62
|
+
removedSessionIds.add(sessionId);
|
|
63
|
+
state = removeSession(state, sessionId);
|
|
64
|
+
notifyChange();
|
|
65
|
+
},
|
|
66
|
+
resolve(displayNumber, decision) {
|
|
67
|
+
const escalation = state.pendingEscalations.get(displayNumber);
|
|
68
|
+
if (!escalation) {
|
|
69
|
+
return `No pending escalation #${displayNumber}`;
|
|
70
|
+
}
|
|
71
|
+
const session = state.sessions.get(escalation.sessionId);
|
|
72
|
+
if (!session) {
|
|
73
|
+
return `Session for escalation #${displayNumber} no longer active`;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const accepted = session.watcher.resolve(escalation.request.escalationId, decision);
|
|
77
|
+
state = resolveEscalation(state, displayNumber, decision);
|
|
78
|
+
notifyChange();
|
|
79
|
+
if (!accepted) {
|
|
80
|
+
return `Escalation #${displayNumber} expired (response was too late)`;
|
|
81
|
+
}
|
|
82
|
+
const label = decision === 'approved' ? 'APPROVED' : 'DENIED';
|
|
83
|
+
return `Escalation #${displayNumber} ${label}`;
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
resolveAll(decision) {
|
|
90
|
+
if (state.pendingEscalations.size === 0) {
|
|
91
|
+
return 'No pending escalations';
|
|
92
|
+
}
|
|
93
|
+
const messages = [];
|
|
94
|
+
const nums = [...state.pendingEscalations.keys()].sort((a, b) => a - b);
|
|
95
|
+
for (const num of nums) {
|
|
96
|
+
messages.push(this.resolve(num, decision));
|
|
97
|
+
}
|
|
98
|
+
return messages.join('\n');
|
|
99
|
+
},
|
|
100
|
+
startRegistryPolling() {
|
|
101
|
+
if (registryPollInterval)
|
|
102
|
+
return;
|
|
103
|
+
const registryDir = getPtyRegistryDir();
|
|
104
|
+
mkdirSync(registryDir, { recursive: true, mode: 0o700 });
|
|
105
|
+
registryPollInterval = setInterval(() => {
|
|
106
|
+
const registrations = readActiveRegistrations(registryDir);
|
|
107
|
+
const currentIds = new Set(registrations.map((r) => r.sessionId));
|
|
108
|
+
const stateIds = new Set(state.sessions.keys());
|
|
109
|
+
let changed = false;
|
|
110
|
+
// Add externally-spawned sessions (skip managed, tombstoned)
|
|
111
|
+
for (const reg of registrations) {
|
|
112
|
+
if (!stateIds.has(reg.sessionId) &&
|
|
113
|
+
!managedSessionIds.has(reg.sessionId) &&
|
|
114
|
+
!removedSessionIds.has(reg.sessionId)) {
|
|
115
|
+
const watcher = createWatcherForSession(reg.sessionId, reg.escalationDir);
|
|
116
|
+
state = addSession(state, reg, watcher);
|
|
117
|
+
watcher.start();
|
|
118
|
+
changed = true;
|
|
119
|
+
for (const cb of discoveryCallbacks)
|
|
120
|
+
cb(reg);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Clear tombstones once the registry entry disappears
|
|
124
|
+
for (const id of removedSessionIds) {
|
|
125
|
+
if (!currentIds.has(id)) {
|
|
126
|
+
removedSessionIds.delete(id);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Remove gone external sessions
|
|
130
|
+
for (const id of stateIds) {
|
|
131
|
+
if (!currentIds.has(id) && !managedSessionIds.has(id)) {
|
|
132
|
+
const session = state.sessions.get(id);
|
|
133
|
+
session?.watcher.stop();
|
|
134
|
+
state = removeSession(state, id);
|
|
135
|
+
changed = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (changed)
|
|
139
|
+
notifyChange();
|
|
140
|
+
}, REGISTRY_POLL_INTERVAL_MS);
|
|
141
|
+
},
|
|
142
|
+
stop() {
|
|
143
|
+
if (registryPollInterval) {
|
|
144
|
+
clearInterval(registryPollInterval);
|
|
145
|
+
registryPollInterval = null;
|
|
146
|
+
}
|
|
147
|
+
for (const session of state.sessions.values()) {
|
|
148
|
+
session.watcher.stop();
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
onChange(callback) {
|
|
152
|
+
changeCallbacks.push(callback);
|
|
153
|
+
},
|
|
154
|
+
onSessionDiscovered(callback) {
|
|
155
|
+
discoveryCallbacks.push(callback);
|
|
156
|
+
},
|
|
157
|
+
claimSession(sessionId) {
|
|
158
|
+
managedSessionIds.add(sessionId);
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=mux-escalation-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mux-escalation-manager.js","sourceRoot":"","sources":["../../src/mux/mux-escalation-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MuxInputHandler -- key routing state machine for the terminal multiplexer.
|
|
3
|
+
*
|
|
4
|
+
* Two input modes:
|
|
5
|
+
* - PTY mode: all keystrokes go to the active PtyBridge except Ctrl-A
|
|
6
|
+
* - Command mode: keystrokes collected in a line buffer, dispatched on Enter
|
|
7
|
+
*
|
|
8
|
+
* Ctrl-A toggles between modes (following screen/tmux convention).
|
|
9
|
+
* Pressing Ctrl-A in PTY mode enters command mode; pressing it again
|
|
10
|
+
* returns to PTY mode.
|
|
11
|
+
*/
|
|
12
|
+
import type { InputMode, MuxAction } from './types.js';
|
|
13
|
+
export type PickerPhase = 'menu' | 'browse';
|
|
14
|
+
export interface PickerState {
|
|
15
|
+
phase: PickerPhase;
|
|
16
|
+
menuSelection: number;
|
|
17
|
+
inputPath: string;
|
|
18
|
+
cursorPos: number;
|
|
19
|
+
entries: string[];
|
|
20
|
+
selectedIndex: number;
|
|
21
|
+
scrollOffset: number;
|
|
22
|
+
inList: boolean;
|
|
23
|
+
error: string | null;
|
|
24
|
+
}
|
|
25
|
+
export interface MuxInputHandler {
|
|
26
|
+
/** Current input mode. */
|
|
27
|
+
readonly mode: InputMode;
|
|
28
|
+
/** Current command-mode input buffer (for display). */
|
|
29
|
+
readonly inputBuffer: string;
|
|
30
|
+
/** Cursor position within the input buffer. */
|
|
31
|
+
readonly cursorPos: number;
|
|
32
|
+
/** Current picker state (null when not in picker mode). */
|
|
33
|
+
readonly pickerState: PickerState | null;
|
|
34
|
+
/** Enter picker mode (called by /new command handler). */
|
|
35
|
+
enterPickerMode(): void;
|
|
36
|
+
/** Re-enter picker browse phase with a validation error. */
|
|
37
|
+
enterBrowseWithError(path: string, error: string): void;
|
|
38
|
+
/** Exit picker mode and return to PTY mode (no side effects). */
|
|
39
|
+
exitPickerMode(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Processes a key event from terminal-kit.
|
|
42
|
+
* Returns a MuxAction describing what the orchestrator should do.
|
|
43
|
+
*/
|
|
44
|
+
handleKey(key: string): MuxAction;
|
|
45
|
+
}
|
|
46
|
+
export interface MuxInputHandlerOptions {
|
|
47
|
+
initialMode?: InputMode;
|
|
48
|
+
}
|
|
49
|
+
export declare function createMuxInputHandler(options?: MuxInputHandlerOptions): MuxInputHandler;
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MuxInputHandler -- key routing state machine for the terminal multiplexer.
|
|
3
|
+
*
|
|
4
|
+
* Two input modes:
|
|
5
|
+
* - PTY mode: all keystrokes go to the active PtyBridge except Ctrl-A
|
|
6
|
+
* - Command mode: keystrokes collected in a line buffer, dispatched on Enter
|
|
7
|
+
*
|
|
8
|
+
* Ctrl-A toggles between modes (following screen/tmux convention).
|
|
9
|
+
* Pressing Ctrl-A in PTY mode enters command mode; pressing it again
|
|
10
|
+
* returns to PTY mode.
|
|
11
|
+
*/
|
|
12
|
+
import { readdirSync } from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { expandTilde } from '../types/argument-roles.js';
|
|
15
|
+
/** terminal-kit key names for special keys. */
|
|
16
|
+
const CTRL_A = 'CTRL_A';
|
|
17
|
+
const ENTER = 'ENTER';
|
|
18
|
+
const ESCAPE = 'ESCAPE';
|
|
19
|
+
const CTRL_C = 'CTRL_C';
|
|
20
|
+
const BACKSPACE = 'BACKSPACE';
|
|
21
|
+
const DELETE = 'DELETE';
|
|
22
|
+
const LEFT = 'LEFT';
|
|
23
|
+
const RIGHT = 'RIGHT';
|
|
24
|
+
const UP = 'UP';
|
|
25
|
+
const DOWN = 'DOWN';
|
|
26
|
+
const TAB = 'TAB';
|
|
27
|
+
/**
|
|
28
|
+
* Maps terminal-kit key names to raw escape sequences for the PTY.
|
|
29
|
+
* terminal-kit emits human-readable names (e.g. 'ENTER'), but the
|
|
30
|
+
* child PTY expects raw bytes (e.g. '\r').
|
|
31
|
+
*/
|
|
32
|
+
const KEY_TO_SEQUENCE = {
|
|
33
|
+
ENTER: '\r',
|
|
34
|
+
BACKSPACE: '\x7f',
|
|
35
|
+
ESCAPE: '\x1b',
|
|
36
|
+
DELETE: '\x1b[3~',
|
|
37
|
+
TAB: '\t',
|
|
38
|
+
UP: '\x1b[A',
|
|
39
|
+
DOWN: '\x1b[B',
|
|
40
|
+
RIGHT: '\x1b[C',
|
|
41
|
+
LEFT: '\x1b[D',
|
|
42
|
+
HOME: '\x1b[H',
|
|
43
|
+
END: '\x1b[F',
|
|
44
|
+
PAGE_UP: '\x1b[5~',
|
|
45
|
+
PAGE_DOWN: '\x1b[6~',
|
|
46
|
+
INSERT: '\x1b[2~',
|
|
47
|
+
// Ctrl keys -> raw bytes
|
|
48
|
+
...Object.fromEntries(Array.from({ length: 26 }, (_, i) => [`CTRL_${String.fromCharCode(65 + i)}`, String.fromCharCode(i + 1)])),
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Splits a path input into the directory portion and the filename prefix.
|
|
52
|
+
*/
|
|
53
|
+
function splitDirPrefix(inputPath) {
|
|
54
|
+
const lastSlash = inputPath.lastIndexOf('/');
|
|
55
|
+
if (lastSlash === -1) {
|
|
56
|
+
return { dir: homedir(), prefix: inputPath };
|
|
57
|
+
}
|
|
58
|
+
return { dir: inputPath.slice(0, lastSlash) || '/', prefix: inputPath.slice(lastSlash + 1) };
|
|
59
|
+
}
|
|
60
|
+
// Cache for directory listings to avoid re-reading on every keystroke
|
|
61
|
+
// when only the prefix (filename portion) changes.
|
|
62
|
+
let _cachedDir = null;
|
|
63
|
+
let _cachedDirents = [];
|
|
64
|
+
/**
|
|
65
|
+
* Reads directory entries for the picker's browse phase.
|
|
66
|
+
* Returns entries matching the prefix, with trailing `/` for directories.
|
|
67
|
+
* Directories are sorted first, then alphabetical.
|
|
68
|
+
* Caches the raw directory listing; only re-reads when the directory changes.
|
|
69
|
+
*/
|
|
70
|
+
function refreshEntries(inputPath) {
|
|
71
|
+
const { dir: rawDir, prefix } = splitDirPrefix(inputPath);
|
|
72
|
+
const dir = expandTilde(rawDir);
|
|
73
|
+
try {
|
|
74
|
+
if (dir !== _cachedDir) {
|
|
75
|
+
_cachedDirents = readdirSync(dir, { withFileTypes: true });
|
|
76
|
+
_cachedDir = dir;
|
|
77
|
+
}
|
|
78
|
+
const entries = [];
|
|
79
|
+
for (const ent of _cachedDirents) {
|
|
80
|
+
if (!ent.name.startsWith(prefix))
|
|
81
|
+
continue;
|
|
82
|
+
if (ent.name.startsWith('.') && !prefix.startsWith('.'))
|
|
83
|
+
continue;
|
|
84
|
+
entries.push({ name: ent.name, isDir: ent.isDirectory() });
|
|
85
|
+
}
|
|
86
|
+
entries.sort((a, b) => {
|
|
87
|
+
if (a.isDir !== b.isDir)
|
|
88
|
+
return a.isDir ? -1 : 1;
|
|
89
|
+
return a.name.localeCompare(b.name);
|
|
90
|
+
});
|
|
91
|
+
return entries.map((e) => (e.isDir ? e.name + '/' : e.name));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Finds the longest common prefix among a list of strings.
|
|
99
|
+
*/
|
|
100
|
+
function longestCommonPrefix(strings) {
|
|
101
|
+
if (strings.length === 0)
|
|
102
|
+
return '';
|
|
103
|
+
let prefix = strings[0];
|
|
104
|
+
for (let i = 1; i < strings.length; i++) {
|
|
105
|
+
while (!strings[i].startsWith(prefix)) {
|
|
106
|
+
prefix = prefix.slice(0, -1);
|
|
107
|
+
if (prefix.length === 0)
|
|
108
|
+
return '';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return prefix;
|
|
112
|
+
}
|
|
113
|
+
export function createMuxInputHandler(options) {
|
|
114
|
+
let _mode = options?.initialMode ?? 'pty';
|
|
115
|
+
let _inputBuffer = '';
|
|
116
|
+
let _cursorPos = 0;
|
|
117
|
+
let _pickerState = null;
|
|
118
|
+
function enterPickerMode() {
|
|
119
|
+
_mode = 'picker';
|
|
120
|
+
_pickerState = {
|
|
121
|
+
phase: 'menu',
|
|
122
|
+
menuSelection: 0,
|
|
123
|
+
inputPath: '',
|
|
124
|
+
cursorPos: 0,
|
|
125
|
+
entries: [],
|
|
126
|
+
selectedIndex: 0,
|
|
127
|
+
scrollOffset: 0,
|
|
128
|
+
inList: false,
|
|
129
|
+
error: null,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** Resets entry selection and scroll after refreshing entries. */
|
|
133
|
+
function resetEntrySelection(ps) {
|
|
134
|
+
ps.entries = refreshEntries(ps.inputPath);
|
|
135
|
+
ps.selectedIndex = 0;
|
|
136
|
+
ps.scrollOffset = 0;
|
|
137
|
+
}
|
|
138
|
+
function initBrowsePhase(path) {
|
|
139
|
+
if (!_pickerState)
|
|
140
|
+
return;
|
|
141
|
+
const browsePath = path ?? homedir() + '/';
|
|
142
|
+
_pickerState.phase = 'browse';
|
|
143
|
+
_pickerState.inputPath = browsePath;
|
|
144
|
+
_pickerState.cursorPos = browsePath.length;
|
|
145
|
+
_pickerState.inList = false;
|
|
146
|
+
_pickerState.error = null;
|
|
147
|
+
resetEntrySelection(_pickerState);
|
|
148
|
+
}
|
|
149
|
+
function enterBrowseWithError(path, error) {
|
|
150
|
+
_mode = 'picker';
|
|
151
|
+
_pickerState = {
|
|
152
|
+
phase: 'browse',
|
|
153
|
+
menuSelection: 0,
|
|
154
|
+
inputPath: path,
|
|
155
|
+
cursorPos: path.length,
|
|
156
|
+
entries: [],
|
|
157
|
+
selectedIndex: 0,
|
|
158
|
+
scrollOffset: 0,
|
|
159
|
+
inList: false,
|
|
160
|
+
error,
|
|
161
|
+
};
|
|
162
|
+
resetEntrySelection(_pickerState);
|
|
163
|
+
}
|
|
164
|
+
function exitPickerMode() {
|
|
165
|
+
_mode = 'pty';
|
|
166
|
+
_pickerState = null;
|
|
167
|
+
}
|
|
168
|
+
function executeMenuSelection(selection) {
|
|
169
|
+
if (selection === 0) {
|
|
170
|
+
_mode = 'pty';
|
|
171
|
+
_pickerState = null;
|
|
172
|
+
return { kind: 'picker-spawn' };
|
|
173
|
+
}
|
|
174
|
+
initBrowsePhase();
|
|
175
|
+
return { kind: 'redraw-picker' };
|
|
176
|
+
}
|
|
177
|
+
function handleMenuKey(key) {
|
|
178
|
+
const ps = _pickerState;
|
|
179
|
+
if (!ps)
|
|
180
|
+
return { kind: 'none' };
|
|
181
|
+
if (key === '1')
|
|
182
|
+
return executeMenuSelection(0);
|
|
183
|
+
if (key === '2')
|
|
184
|
+
return executeMenuSelection(1);
|
|
185
|
+
if (key === ENTER)
|
|
186
|
+
return executeMenuSelection(ps.menuSelection);
|
|
187
|
+
if (key === UP || key === DOWN) {
|
|
188
|
+
ps.menuSelection = ps.menuSelection === 0 ? 1 : 0;
|
|
189
|
+
return { kind: 'redraw-picker' };
|
|
190
|
+
}
|
|
191
|
+
if (key === ESCAPE || key === CTRL_C) {
|
|
192
|
+
_mode = 'command';
|
|
193
|
+
_pickerState = null;
|
|
194
|
+
return { kind: 'picker-cancel' };
|
|
195
|
+
}
|
|
196
|
+
return { kind: 'none' };
|
|
197
|
+
}
|
|
198
|
+
function handleBrowseKey(key) {
|
|
199
|
+
const ps = _pickerState;
|
|
200
|
+
if (!ps)
|
|
201
|
+
return { kind: 'none' };
|
|
202
|
+
ps.error = null;
|
|
203
|
+
if (key === ESCAPE || key === CTRL_C) {
|
|
204
|
+
if (ps.inList) {
|
|
205
|
+
// Return from list to input field
|
|
206
|
+
ps.inList = false;
|
|
207
|
+
return { kind: 'redraw-picker' };
|
|
208
|
+
}
|
|
209
|
+
// Return to menu phase
|
|
210
|
+
ps.phase = 'menu';
|
|
211
|
+
ps.menuSelection = 0;
|
|
212
|
+
return { kind: 'redraw-picker' };
|
|
213
|
+
}
|
|
214
|
+
// Tab completion (only when in input field)
|
|
215
|
+
if (key === TAB && !ps.inList) {
|
|
216
|
+
if (ps.entries.length === 0) {
|
|
217
|
+
return { kind: 'none' };
|
|
218
|
+
}
|
|
219
|
+
const { dir: dirPart, prefix } = splitDirPrefix(ps.inputPath);
|
|
220
|
+
const base = dirPart === homedir() && !ps.inputPath.includes('/') ? '' : dirPart + '/';
|
|
221
|
+
if (ps.entries.length === 1) {
|
|
222
|
+
ps.inputPath = base + ps.entries[0];
|
|
223
|
+
ps.cursorPos = ps.inputPath.length;
|
|
224
|
+
resetEntrySelection(ps);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
const names = ps.entries.map((e) => e.replace(/\/$/, ''));
|
|
228
|
+
const lcp = longestCommonPrefix(names);
|
|
229
|
+
if (lcp.length > prefix.length) {
|
|
230
|
+
ps.inputPath = base + lcp;
|
|
231
|
+
ps.cursorPos = ps.inputPath.length;
|
|
232
|
+
resetEntrySelection(ps);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { kind: 'redraw-picker' };
|
|
236
|
+
}
|
|
237
|
+
// Up/Down navigation
|
|
238
|
+
if (key === DOWN) {
|
|
239
|
+
if (!ps.inList) {
|
|
240
|
+
// Move focus from input field into the entry list
|
|
241
|
+
if (ps.entries.length > 0) {
|
|
242
|
+
ps.inList = true;
|
|
243
|
+
ps.selectedIndex = 0;
|
|
244
|
+
ps.scrollOffset = 0;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else if (ps.selectedIndex < ps.entries.length - 1) {
|
|
248
|
+
ps.selectedIndex++;
|
|
249
|
+
}
|
|
250
|
+
return { kind: 'redraw-picker' };
|
|
251
|
+
}
|
|
252
|
+
if (key === UP) {
|
|
253
|
+
if (ps.inList) {
|
|
254
|
+
if (ps.selectedIndex > 0) {
|
|
255
|
+
ps.selectedIndex--;
|
|
256
|
+
if (ps.selectedIndex < ps.scrollOffset) {
|
|
257
|
+
ps.scrollOffset = ps.selectedIndex;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
// At top of list — return focus to input field
|
|
262
|
+
ps.inList = false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return { kind: 'redraw-picker' };
|
|
266
|
+
}
|
|
267
|
+
// When in list mode, Enter picks the entry into the input field
|
|
268
|
+
if (key === ENTER && ps.inList) {
|
|
269
|
+
if (ps.selectedIndex < ps.entries.length) {
|
|
270
|
+
const selected = ps.entries[ps.selectedIndex];
|
|
271
|
+
const { dir: dirPart } = splitDirPrefix(ps.inputPath);
|
|
272
|
+
ps.inputPath = dirPart + '/' + selected;
|
|
273
|
+
ps.cursorPos = ps.inputPath.length;
|
|
274
|
+
ps.inList = false;
|
|
275
|
+
resetEntrySelection(ps);
|
|
276
|
+
}
|
|
277
|
+
return { kind: 'redraw-picker' };
|
|
278
|
+
}
|
|
279
|
+
// When in input field, Enter submits the current path
|
|
280
|
+
if (key === ENTER && !ps.inList) {
|
|
281
|
+
if (ps.inputPath.trim()) {
|
|
282
|
+
const workspacePath = ps.inputPath.replace(/\/+$/, '');
|
|
283
|
+
_mode = 'pty';
|
|
284
|
+
_pickerState = null;
|
|
285
|
+
return { kind: 'picker-spawn', workspacePath };
|
|
286
|
+
}
|
|
287
|
+
return { kind: 'redraw-picker' };
|
|
288
|
+
}
|
|
289
|
+
// Keys below only apply when focus is on the input field
|
|
290
|
+
if (ps.inList) {
|
|
291
|
+
return { kind: 'none' };
|
|
292
|
+
}
|
|
293
|
+
// Cursor movement within input path
|
|
294
|
+
if (key === LEFT) {
|
|
295
|
+
if (ps.cursorPos > 0)
|
|
296
|
+
ps.cursorPos--;
|
|
297
|
+
return { kind: 'redraw-picker' };
|
|
298
|
+
}
|
|
299
|
+
if (key === RIGHT) {
|
|
300
|
+
if (ps.cursorPos < ps.inputPath.length)
|
|
301
|
+
ps.cursorPos++;
|
|
302
|
+
return { kind: 'redraw-picker' };
|
|
303
|
+
}
|
|
304
|
+
// Backspace
|
|
305
|
+
if (key === BACKSPACE) {
|
|
306
|
+
if (ps.cursorPos > 0) {
|
|
307
|
+
ps.inputPath = ps.inputPath.slice(0, ps.cursorPos - 1) + ps.inputPath.slice(ps.cursorPos);
|
|
308
|
+
ps.cursorPos--;
|
|
309
|
+
resetEntrySelection(ps);
|
|
310
|
+
}
|
|
311
|
+
return { kind: 'redraw-picker' };
|
|
312
|
+
}
|
|
313
|
+
// Delete
|
|
314
|
+
if (key === DELETE) {
|
|
315
|
+
if (ps.cursorPos < ps.inputPath.length) {
|
|
316
|
+
ps.inputPath = ps.inputPath.slice(0, ps.cursorPos) + ps.inputPath.slice(ps.cursorPos + 1);
|
|
317
|
+
resetEntrySelection(ps);
|
|
318
|
+
}
|
|
319
|
+
return { kind: 'redraw-picker' };
|
|
320
|
+
}
|
|
321
|
+
// Printable character
|
|
322
|
+
if (key.length === 1 && key.charCodeAt(0) >= 32) {
|
|
323
|
+
ps.inputPath = ps.inputPath.slice(0, ps.cursorPos) + key + ps.inputPath.slice(ps.cursorPos);
|
|
324
|
+
ps.cursorPos++;
|
|
325
|
+
resetEntrySelection(ps);
|
|
326
|
+
return { kind: 'redraw-picker' };
|
|
327
|
+
}
|
|
328
|
+
return { kind: 'none' };
|
|
329
|
+
}
|
|
330
|
+
function handlePickerKey(key) {
|
|
331
|
+
if (!_pickerState)
|
|
332
|
+
return { kind: 'none' };
|
|
333
|
+
if (_pickerState.phase === 'menu')
|
|
334
|
+
return handleMenuKey(key);
|
|
335
|
+
return handleBrowseKey(key);
|
|
336
|
+
}
|
|
337
|
+
function handlePtyKey(key) {
|
|
338
|
+
if (key === CTRL_A) {
|
|
339
|
+
_mode = 'command';
|
|
340
|
+
_inputBuffer = '';
|
|
341
|
+
_cursorPos = 0;
|
|
342
|
+
return { kind: 'enter-command-mode' };
|
|
343
|
+
}
|
|
344
|
+
return { kind: 'write-pty', data: KEY_TO_SEQUENCE[key] ?? key };
|
|
345
|
+
}
|
|
346
|
+
function handleCommandKey(key) {
|
|
347
|
+
// Ctrl-A in command mode returns to PTY mode
|
|
348
|
+
if (key === CTRL_A) {
|
|
349
|
+
_mode = 'pty';
|
|
350
|
+
_inputBuffer = '';
|
|
351
|
+
_cursorPos = 0;
|
|
352
|
+
return { kind: 'enter-pty-mode' };
|
|
353
|
+
}
|
|
354
|
+
// Escape: discard buffer, return to PTY mode
|
|
355
|
+
if (key === ESCAPE) {
|
|
356
|
+
_mode = 'pty';
|
|
357
|
+
_inputBuffer = '';
|
|
358
|
+
_cursorPos = 0;
|
|
359
|
+
return { kind: 'enter-pty-mode' };
|
|
360
|
+
}
|
|
361
|
+
// Ctrl-C: clear input buffer
|
|
362
|
+
if (key === CTRL_C) {
|
|
363
|
+
_inputBuffer = '';
|
|
364
|
+
_cursorPos = 0;
|
|
365
|
+
return { kind: 'redraw-input' };
|
|
366
|
+
}
|
|
367
|
+
// Enter: dispatch command or trusted input
|
|
368
|
+
if (key === ENTER) {
|
|
369
|
+
const text = _inputBuffer.trim();
|
|
370
|
+
_inputBuffer = '';
|
|
371
|
+
_cursorPos = 0;
|
|
372
|
+
if (!text) {
|
|
373
|
+
return { kind: 'redraw-input' };
|
|
374
|
+
}
|
|
375
|
+
// Commands start with /
|
|
376
|
+
if (text.startsWith('/')) {
|
|
377
|
+
const parts = text.slice(1).split(/\s+/);
|
|
378
|
+
const command = parts[0].toLowerCase();
|
|
379
|
+
const args = parts.slice(1);
|
|
380
|
+
return { kind: 'command', command, args };
|
|
381
|
+
}
|
|
382
|
+
// Non-command text -> trusted input
|
|
383
|
+
return { kind: 'trusted-input', text };
|
|
384
|
+
}
|
|
385
|
+
// Backspace
|
|
386
|
+
if (key === BACKSPACE) {
|
|
387
|
+
if (_cursorPos > 0) {
|
|
388
|
+
_inputBuffer = _inputBuffer.slice(0, _cursorPos - 1) + _inputBuffer.slice(_cursorPos);
|
|
389
|
+
_cursorPos--;
|
|
390
|
+
return { kind: 'redraw-input' };
|
|
391
|
+
}
|
|
392
|
+
return { kind: 'none' };
|
|
393
|
+
}
|
|
394
|
+
// Delete
|
|
395
|
+
if (key === DELETE) {
|
|
396
|
+
if (_cursorPos < _inputBuffer.length) {
|
|
397
|
+
_inputBuffer = _inputBuffer.slice(0, _cursorPos) + _inputBuffer.slice(_cursorPos + 1);
|
|
398
|
+
return { kind: 'redraw-input' };
|
|
399
|
+
}
|
|
400
|
+
return { kind: 'none' };
|
|
401
|
+
}
|
|
402
|
+
// Arrow keys
|
|
403
|
+
if (key === LEFT) {
|
|
404
|
+
if (_cursorPos > 0) {
|
|
405
|
+
_cursorPos--;
|
|
406
|
+
return { kind: 'redraw-input' };
|
|
407
|
+
}
|
|
408
|
+
return { kind: 'none' };
|
|
409
|
+
}
|
|
410
|
+
if (key === RIGHT) {
|
|
411
|
+
if (_cursorPos < _inputBuffer.length) {
|
|
412
|
+
_cursorPos++;
|
|
413
|
+
return { kind: 'redraw-input' };
|
|
414
|
+
}
|
|
415
|
+
return { kind: 'none' };
|
|
416
|
+
}
|
|
417
|
+
// Alt-1 through Alt-9: quick tab switching
|
|
418
|
+
const altMatch = /^ALT_(\d)$/.exec(key);
|
|
419
|
+
if (altMatch) {
|
|
420
|
+
return { kind: 'command', command: 'tab', args: [altMatch[1]] };
|
|
421
|
+
}
|
|
422
|
+
// Regular printable character
|
|
423
|
+
if (key.length === 1 && key.charCodeAt(0) >= 32) {
|
|
424
|
+
_inputBuffer = _inputBuffer.slice(0, _cursorPos) + key + _inputBuffer.slice(_cursorPos);
|
|
425
|
+
_cursorPos++;
|
|
426
|
+
return { kind: 'redraw-input' };
|
|
427
|
+
}
|
|
428
|
+
return { kind: 'none' };
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
get mode() {
|
|
432
|
+
return _mode;
|
|
433
|
+
},
|
|
434
|
+
get inputBuffer() {
|
|
435
|
+
return _inputBuffer;
|
|
436
|
+
},
|
|
437
|
+
get cursorPos() {
|
|
438
|
+
return _cursorPos;
|
|
439
|
+
},
|
|
440
|
+
get pickerState() {
|
|
441
|
+
return _pickerState;
|
|
442
|
+
},
|
|
443
|
+
enterPickerMode,
|
|
444
|
+
enterBrowseWithError,
|
|
445
|
+
exitPickerMode,
|
|
446
|
+
handleKey(key) {
|
|
447
|
+
if (_mode === 'pty') {
|
|
448
|
+
return handlePtyKey(key);
|
|
449
|
+
}
|
|
450
|
+
if (_mode === 'picker') {
|
|
451
|
+
return handlePickerKey(key);
|
|
452
|
+
}
|
|
453
|
+
return handleCommandKey(key);
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
//# sourceMappingURL=mux-input-handler.js.map
|