@ai-devkit/agent-manager 0.7.0 → 0.8.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/dist/terminal/TerminalFocusManager.js +1 -1
- package/dist/terminal/TerminalFocusManager.js.map +1 -1
- package/dist/terminal/TtyWriter.d.ts +8 -2
- package/dist/terminal/TtyWriter.d.ts.map +1 -1
- package/dist/terminal/TtyWriter.js +70 -48
- package/dist/terminal/TtyWriter.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/terminal/TtyWriter.test.ts +59 -9
- package/src/terminal/TerminalFocusManager.ts +1 -1
- package/src/terminal/TtyWriter.ts +76 -48
|
@@ -124,7 +124,7 @@ class TerminalFocusManager {
|
|
|
124
124
|
async findTerminalAppWindow(tty) {
|
|
125
125
|
try {
|
|
126
126
|
// Check if Terminal is running
|
|
127
|
-
const { stdout: isRunning } = await execAsync('
|
|
127
|
+
const { stdout: isRunning } = await execAsync('ps -eo pid=,comm= | grep "Terminal.app" || echo "no"');
|
|
128
128
|
if (isRunning.trim() === "no")
|
|
129
129
|
return null;
|
|
130
130
|
const script = `
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TerminalFocusManager.js","sourceRoot":"","sources":["../../src/terminal/TerminalFocusManager.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"TerminalFocusManager.js","sourceRoot":"","sources":["../../src/terminal/TerminalFocusManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,8 +5,8 @@ export declare class TtyWriter {
|
|
|
5
5
|
*
|
|
6
6
|
* Dispatches to the correct mechanism based on terminal type:
|
|
7
7
|
* - tmux: `tmux send-keys`
|
|
8
|
-
* - iTerm2: AppleScript `write text`
|
|
9
|
-
* - Terminal.app:
|
|
8
|
+
* - iTerm2: Two separate AppleScript `write text` calls (text then newline)
|
|
9
|
+
* - Terminal.app: Two separate AppleScript `do script` calls (text then newline)
|
|
10
10
|
*
|
|
11
11
|
* All AppleScript is executed via `execFile('osascript', ['-e', script])`
|
|
12
12
|
* to avoid shell interpolation and command injection.
|
|
@@ -17,6 +17,12 @@ export declare class TtyWriter {
|
|
|
17
17
|
*/
|
|
18
18
|
static send(location: TerminalLocation, message: string): Promise<void>;
|
|
19
19
|
private static sendViaTmux;
|
|
20
|
+
/**
|
|
21
|
+
* Build an AppleScript that finds an iTerm2 session by TTY and runs a
|
|
22
|
+
* command against it. The `sessionCommand` is inserted inside a
|
|
23
|
+
* `tell targetSession` block.
|
|
24
|
+
*/
|
|
25
|
+
private static iterm2SessionScript;
|
|
20
26
|
private static sendViaITerm2;
|
|
21
27
|
private static sendViaTerminalApp;
|
|
22
28
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TtyWriter.d.ts","sourceRoot":"","sources":["../../src/terminal/TtyWriter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"TtyWriter.d.ts","sourceRoot":"","sources":["../../src/terminal/TtyWriter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAgB/D,qBAAa,SAAS;IAClB;;;;;;;;;;;;;;OAcG;WACU,IAAI,CAAC,QAAQ,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;mBAgBxD,WAAW;IAWhC;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;mBAsBb,aAAa;mBAuBb,kBAAkB;CA0D1C"}
|
|
@@ -7,10 +7,13 @@ const TerminalFocusManager_1 = require("./TerminalFocusManager");
|
|
|
7
7
|
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
8
8
|
/**
|
|
9
9
|
* Escape a string for safe use inside an AppleScript double-quoted string.
|
|
10
|
-
* Backslashes
|
|
10
|
+
* Backslashes, double quotes, and newlines must be escaped.
|
|
11
11
|
*/
|
|
12
12
|
function escapeAppleScript(text) {
|
|
13
|
-
return text
|
|
13
|
+
return text
|
|
14
|
+
.replace(/\\/g, '\\\\')
|
|
15
|
+
.replace(/"/g, '\\"')
|
|
16
|
+
.replace(/\r\n|\r|\n/g, '\\n');
|
|
14
17
|
}
|
|
15
18
|
class TtyWriter {
|
|
16
19
|
/**
|
|
@@ -18,8 +21,8 @@ class TtyWriter {
|
|
|
18
21
|
*
|
|
19
22
|
* Dispatches to the correct mechanism based on terminal type:
|
|
20
23
|
* - tmux: `tmux send-keys`
|
|
21
|
-
* - iTerm2: AppleScript `write text`
|
|
22
|
-
* - Terminal.app:
|
|
24
|
+
* - iTerm2: Two separate AppleScript `write text` calls (text then newline)
|
|
25
|
+
* - Terminal.app: Two separate AppleScript `do script` calls (text then newline)
|
|
23
26
|
*
|
|
24
27
|
* All AppleScript is executed via `execFile('osascript', ['-e', script])`
|
|
25
28
|
* to avoid shell interpolation and command injection.
|
|
@@ -51,12 +54,13 @@ class TtyWriter {
|
|
|
51
54
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
52
55
|
await execFileAsync('tmux', ['send-keys', '-t', identifier, 'Enter']);
|
|
53
56
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Build an AppleScript that finds an iTerm2 session by TTY and runs a
|
|
59
|
+
* command against it. The `sessionCommand` is inserted inside a
|
|
60
|
+
* `tell targetSession` block.
|
|
61
|
+
*/
|
|
62
|
+
static iterm2SessionScript(tty, sessionCommand) {
|
|
63
|
+
return `
|
|
60
64
|
tell application "iTerm"
|
|
61
65
|
set targetSession to missing value
|
|
62
66
|
repeat with w in windows
|
|
@@ -72,64 +76,82 @@ tell application "iTerm"
|
|
|
72
76
|
if targetSession is not missing value then exit repeat
|
|
73
77
|
end repeat
|
|
74
78
|
if targetSession is missing value then return "not_found"
|
|
75
|
-
tell targetSession to
|
|
76
|
-
end tell
|
|
77
|
-
tell application "iTerm" to activate
|
|
78
|
-
delay 0.15
|
|
79
|
-
tell application "System Events"
|
|
80
|
-
tell process "iTerm2"
|
|
81
|
-
key code 36
|
|
82
|
-
end tell
|
|
79
|
+
tell targetSession to ${sessionCommand}
|
|
83
80
|
end tell
|
|
84
81
|
return "ok"`;
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
}
|
|
83
|
+
static async sendViaITerm2(tty, message) {
|
|
84
|
+
const escaped = escapeAppleScript(message);
|
|
85
|
+
// Send text and Enter as two separate write text calls so the newline
|
|
86
|
+
// is delivered outside the bracketed paste sequence of the message body.
|
|
87
|
+
// iTerm2 appends the newline after the paste-end marker (\e[201~), so
|
|
88
|
+
// the inner TUI (Claude Code, Codex) sees it as a real submit action.
|
|
89
|
+
const textScript = TtyWriter.iterm2SessionScript(tty, `write text "${escaped}" newline no`);
|
|
90
|
+
const { stdout: textResult } = await execFileAsync('osascript', ['-e', textScript]);
|
|
91
|
+
if (textResult.trim() !== 'ok') {
|
|
87
92
|
throw new Error(`iTerm2 session not found for TTY ${tty}`);
|
|
88
93
|
}
|
|
94
|
+
// Wait for the paste to complete before sending Enter separately
|
|
95
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
96
|
+
const enterScript = TtyWriter.iterm2SessionScript(tty, 'write text "" newline yes');
|
|
97
|
+
const { stdout: enterResult } = await execFileAsync('osascript', ['-e', enterScript]);
|
|
98
|
+
if (enterResult.trim() !== 'ok') {
|
|
99
|
+
throw new Error(`iTerm2 session disappeared before Enter could be sent for TTY ${tty}`);
|
|
100
|
+
}
|
|
89
101
|
}
|
|
90
102
|
static async sendViaTerminalApp(tty, message) {
|
|
91
103
|
const escaped = escapeAppleScript(message);
|
|
92
|
-
// Use
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
104
|
+
// Use Terminal.app's `do script` to send text to the correct tab by TTY.
|
|
105
|
+
// We avoid System Events `keystroke` + `key code 36` because it requires
|
|
106
|
+
// accessibility permissions and unreliably delivers the Return key.
|
|
107
|
+
//
|
|
108
|
+
// `do script` with `in` targets a specific tab without opening a new one.
|
|
109
|
+
// We send text and Enter as two separate calls so the newline arrives
|
|
110
|
+
// outside of bracketed paste mode — same pattern as iTerm2 and tmux.
|
|
111
|
+
const textScript = `
|
|
99
112
|
tell application "Terminal"
|
|
100
|
-
set
|
|
113
|
+
set targetTab to missing value
|
|
101
114
|
repeat with w in windows
|
|
102
115
|
repeat with i from 1 to count of tabs of w
|
|
103
116
|
set t to tab i of w
|
|
104
117
|
if tty of t is "${tty}" then
|
|
105
|
-
set
|
|
106
|
-
set index of w to 1
|
|
107
|
-
activate
|
|
108
|
-
set targetFound to true
|
|
118
|
+
set targetTab to t
|
|
109
119
|
exit repeat
|
|
110
120
|
end if
|
|
111
121
|
end repeat
|
|
112
|
-
if
|
|
122
|
+
if targetTab is not missing value then exit repeat
|
|
113
123
|
end repeat
|
|
114
|
-
if
|
|
115
|
-
|
|
116
|
-
delay 0.1
|
|
117
|
-
tell application "System Events"
|
|
118
|
-
tell process "Terminal"
|
|
119
|
-
keystroke "${escaped}"
|
|
120
|
-
end tell
|
|
121
|
-
end tell
|
|
122
|
-
delay 0.15
|
|
123
|
-
tell application "System Events"
|
|
124
|
-
tell process "Terminal"
|
|
125
|
-
key code 36
|
|
126
|
-
end tell
|
|
124
|
+
if targetTab is missing value then return "not_found"
|
|
125
|
+
do script "${escaped}" in targetTab
|
|
127
126
|
end tell
|
|
128
127
|
return "ok"`;
|
|
129
|
-
const { stdout } = await execFileAsync('osascript', ['-e',
|
|
130
|
-
if (
|
|
128
|
+
const { stdout: textResult } = await execFileAsync('osascript', ['-e', textScript]);
|
|
129
|
+
if (textResult.trim() !== 'ok') {
|
|
131
130
|
throw new Error(`Terminal.app tab not found for TTY ${tty}`);
|
|
132
131
|
}
|
|
132
|
+
// Wait for the text to be delivered before sending Enter
|
|
133
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
134
|
+
const enterScript = `
|
|
135
|
+
tell application "Terminal"
|
|
136
|
+
set targetTab to missing value
|
|
137
|
+
repeat with w in windows
|
|
138
|
+
repeat with i from 1 to count of tabs of w
|
|
139
|
+
set t to tab i of w
|
|
140
|
+
if tty of t is "${tty}" then
|
|
141
|
+
set targetTab to t
|
|
142
|
+
exit repeat
|
|
143
|
+
end if
|
|
144
|
+
end repeat
|
|
145
|
+
if targetTab is not missing value then exit repeat
|
|
146
|
+
end repeat
|
|
147
|
+
if targetTab is missing value then return "not_found"
|
|
148
|
+
do script "" in targetTab
|
|
149
|
+
end tell
|
|
150
|
+
return "ok"`;
|
|
151
|
+
const { stdout: enterResult } = await execFileAsync('osascript', ['-e', enterScript]);
|
|
152
|
+
if (enterResult.trim() !== 'ok') {
|
|
153
|
+
throw new Error(`Terminal.app tab disappeared before Enter could be sent for TTY ${tty}`);
|
|
154
|
+
}
|
|
133
155
|
}
|
|
134
156
|
}
|
|
135
157
|
exports.TtyWriter = TtyWriter;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TtyWriter.js","sourceRoot":"","sources":["../../src/terminal/TtyWriter.ts"],"names":[],"mappings":";;;AAAA,iDAAyC;AACzC,+BAAiC;AAEjC,iEAAsD;AAEtD,MAAM,aAAa,GAAG,IAAA,gBAAS,EAAC,wBAAQ,CAAC,CAAC;AAE1C;;;GAGG;AACH,SAAS,iBAAiB,CAAC,IAAY;IACnC,OAAO,IAAI,
|
|
1
|
+
{"version":3,"file":"TtyWriter.js","sourceRoot":"","sources":["../../src/terminal/TtyWriter.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -78,14 +78,19 @@ describe('TtyWriter', () => {
|
|
|
78
78
|
|
|
79
79
|
await TtyWriter.send(location, 'hello');
|
|
80
80
|
|
|
81
|
+
// First call: send text without newline
|
|
81
82
|
expect(mockedExecFile).toHaveBeenCalledWith(
|
|
82
83
|
'osascript',
|
|
83
84
|
['-e', expect.stringContaining('write text "hello" newline no')],
|
|
84
85
|
expect.any(Function),
|
|
85
86
|
);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
// Second call: send Enter via separate write text with newline
|
|
88
|
+
expect(mockedExecFile).toHaveBeenCalledWith(
|
|
89
|
+
'osascript',
|
|
90
|
+
['-e', expect.stringContaining('write text "" newline yes')],
|
|
91
|
+
expect.any(Function),
|
|
92
|
+
);
|
|
93
|
+
expect(mockedExecFile).toHaveBeenCalledTimes(2);
|
|
89
94
|
});
|
|
90
95
|
|
|
91
96
|
it('escapes special characters in message', async () => {
|
|
@@ -100,12 +105,37 @@ describe('TtyWriter', () => {
|
|
|
100
105
|
);
|
|
101
106
|
});
|
|
102
107
|
|
|
108
|
+
it('escapes newlines in message', async () => {
|
|
109
|
+
mockExecFileSuccess('ok');
|
|
110
|
+
|
|
111
|
+
await TtyWriter.send(location, 'line1\nline2');
|
|
112
|
+
|
|
113
|
+
expect(mockedExecFile).toHaveBeenCalledWith(
|
|
114
|
+
'osascript',
|
|
115
|
+
['-e', expect.stringContaining('write text "line1\\nline2" newline no')],
|
|
116
|
+
expect.any(Function),
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
103
120
|
it('throws when session not found', async () => {
|
|
104
121
|
mockExecFileSuccess('not_found');
|
|
105
122
|
|
|
106
123
|
await expect(TtyWriter.send(location, 'test'))
|
|
107
124
|
.rejects.toThrow('iTerm2 session not found');
|
|
108
125
|
});
|
|
126
|
+
|
|
127
|
+
it('throws when session disappears before Enter', async () => {
|
|
128
|
+
// First call succeeds (text sent), second returns not_found
|
|
129
|
+
let callCount = 0;
|
|
130
|
+
mockedExecFile.mockImplementation((...args: unknown[]) => {
|
|
131
|
+
const cb = args[args.length - 1] as (err: Error | null, result: { stdout: string }, stderr: string) => void;
|
|
132
|
+
callCount++;
|
|
133
|
+
cb(null, { stdout: callCount === 1 ? 'ok' : 'not_found' }, '');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await expect(TtyWriter.send(location, 'test'))
|
|
137
|
+
.rejects.toThrow('iTerm2 session disappeared before Enter');
|
|
138
|
+
});
|
|
109
139
|
});
|
|
110
140
|
|
|
111
141
|
describe('Terminal.app', () => {
|
|
@@ -115,16 +145,24 @@ describe('TtyWriter', () => {
|
|
|
115
145
|
tty: '/dev/ttys030',
|
|
116
146
|
};
|
|
117
147
|
|
|
118
|
-
it('sends message via
|
|
148
|
+
it('sends message via do script (not System Events)', async () => {
|
|
119
149
|
mockExecFileSuccess('ok');
|
|
120
150
|
|
|
121
151
|
await TtyWriter.send(location, 'hello');
|
|
122
152
|
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
expect(
|
|
127
|
-
expect(
|
|
153
|
+
// First call: send text via do script
|
|
154
|
+
const firstCallArgs = (mockedExecFile.mock.calls[0] as unknown[])[1] as string[];
|
|
155
|
+
const textScript = firstCallArgs[1];
|
|
156
|
+
expect(textScript).toContain('do script "hello" in targetTab');
|
|
157
|
+
expect(textScript).not.toContain('keystroke');
|
|
158
|
+
expect(textScript).not.toContain('key code 36');
|
|
159
|
+
|
|
160
|
+
// Second call: send Enter via separate do script
|
|
161
|
+
const secondCallArgs = (mockedExecFile.mock.calls[1] as unknown[])[1] as string[];
|
|
162
|
+
const enterScript = secondCallArgs[1];
|
|
163
|
+
expect(enterScript).toContain('do script "" in targetTab');
|
|
164
|
+
|
|
165
|
+
expect(mockedExecFile).toHaveBeenCalledTimes(2);
|
|
128
166
|
});
|
|
129
167
|
|
|
130
168
|
it('throws when tab not found', async () => {
|
|
@@ -133,6 +171,18 @@ describe('TtyWriter', () => {
|
|
|
133
171
|
await expect(TtyWriter.send(location, 'test'))
|
|
134
172
|
.rejects.toThrow('Terminal.app tab not found');
|
|
135
173
|
});
|
|
174
|
+
|
|
175
|
+
it('throws when tab disappears before Enter', async () => {
|
|
176
|
+
let callCount = 0;
|
|
177
|
+
mockedExecFile.mockImplementation((...args: unknown[]) => {
|
|
178
|
+
const cb = args[args.length - 1] as (err: Error | null, result: { stdout: string }, stderr: string) => void;
|
|
179
|
+
callCount++;
|
|
180
|
+
cb(null, { stdout: callCount === 1 ? 'ok' : 'not_found' }, '');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await expect(TtyWriter.send(location, 'test'))
|
|
184
|
+
.rejects.toThrow('Terminal.app tab disappeared before Enter');
|
|
185
|
+
});
|
|
136
186
|
});
|
|
137
187
|
|
|
138
188
|
describe('unsupported terminal', () => {
|
|
@@ -134,7 +134,7 @@ export class TerminalFocusManager {
|
|
|
134
134
|
private async findTerminalAppWindow(tty: string): Promise<TerminalLocation | null> {
|
|
135
135
|
try {
|
|
136
136
|
// Check if Terminal is running
|
|
137
|
-
const { stdout: isRunning } = await execAsync('
|
|
137
|
+
const { stdout: isRunning } = await execAsync('ps -eo pid=,comm= | grep "Terminal.app" || echo "no"');
|
|
138
138
|
if (isRunning.trim() === "no") return null;
|
|
139
139
|
|
|
140
140
|
const script = `
|
|
@@ -7,10 +7,13 @@ const execFileAsync = promisify(execFile);
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Escape a string for safe use inside an AppleScript double-quoted string.
|
|
10
|
-
* Backslashes
|
|
10
|
+
* Backslashes, double quotes, and newlines must be escaped.
|
|
11
11
|
*/
|
|
12
12
|
function escapeAppleScript(text: string): string {
|
|
13
|
-
return text
|
|
13
|
+
return text
|
|
14
|
+
.replace(/\\/g, '\\\\')
|
|
15
|
+
.replace(/"/g, '\\"')
|
|
16
|
+
.replace(/\r\n|\r|\n/g, '\\n');
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export class TtyWriter {
|
|
@@ -19,8 +22,8 @@ export class TtyWriter {
|
|
|
19
22
|
*
|
|
20
23
|
* Dispatches to the correct mechanism based on terminal type:
|
|
21
24
|
* - tmux: `tmux send-keys`
|
|
22
|
-
* - iTerm2: AppleScript `write text`
|
|
23
|
-
* - Terminal.app:
|
|
25
|
+
* - iTerm2: Two separate AppleScript `write text` calls (text then newline)
|
|
26
|
+
* - Terminal.app: Two separate AppleScript `do script` calls (text then newline)
|
|
24
27
|
*
|
|
25
28
|
* All AppleScript is executed via `execFile('osascript', ['-e', script])`
|
|
26
29
|
* to avoid shell interpolation and command injection.
|
|
@@ -56,12 +59,13 @@ export class TtyWriter {
|
|
|
56
59
|
await execFileAsync('tmux', ['send-keys', '-t', identifier, 'Enter']);
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Build an AppleScript that finds an iTerm2 session by TTY and runs a
|
|
64
|
+
* command against it. The `sessionCommand` is inserted inside a
|
|
65
|
+
* `tell targetSession` block.
|
|
66
|
+
*/
|
|
67
|
+
private static iterm2SessionScript(tty: string, sessionCommand: string): string {
|
|
68
|
+
return `
|
|
65
69
|
tell application "iTerm"
|
|
66
70
|
set targetSession to missing value
|
|
67
71
|
repeat with w in windows
|
|
@@ -77,66 +81,90 @@ tell application "iTerm"
|
|
|
77
81
|
if targetSession is not missing value then exit repeat
|
|
78
82
|
end repeat
|
|
79
83
|
if targetSession is missing value then return "not_found"
|
|
80
|
-
tell targetSession to
|
|
81
|
-
end tell
|
|
82
|
-
tell application "iTerm" to activate
|
|
83
|
-
delay 0.15
|
|
84
|
-
tell application "System Events"
|
|
85
|
-
tell process "iTerm2"
|
|
86
|
-
key code 36
|
|
87
|
-
end tell
|
|
84
|
+
tell targetSession to ${sessionCommand}
|
|
88
85
|
end tell
|
|
89
86
|
return "ok"`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private static async sendViaITerm2(tty: string, message: string): Promise<void> {
|
|
90
|
+
const escaped = escapeAppleScript(message);
|
|
91
|
+
// Send text and Enter as two separate write text calls so the newline
|
|
92
|
+
// is delivered outside the bracketed paste sequence of the message body.
|
|
93
|
+
// iTerm2 appends the newline after the paste-end marker (\e[201~), so
|
|
94
|
+
// the inner TUI (Claude Code, Codex) sees it as a real submit action.
|
|
95
|
+
const textScript = TtyWriter.iterm2SessionScript(tty, `write text "${escaped}" newline no`);
|
|
90
96
|
|
|
91
|
-
const { stdout } = await execFileAsync('osascript', ['-e',
|
|
92
|
-
if (
|
|
97
|
+
const { stdout: textResult } = await execFileAsync('osascript', ['-e', textScript]);
|
|
98
|
+
if (textResult.trim() !== 'ok') {
|
|
93
99
|
throw new Error(`iTerm2 session not found for TTY ${tty}`);
|
|
94
100
|
}
|
|
101
|
+
|
|
102
|
+
// Wait for the paste to complete before sending Enter separately
|
|
103
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
104
|
+
|
|
105
|
+
const enterScript = TtyWriter.iterm2SessionScript(tty, 'write text "" newline yes');
|
|
106
|
+
const { stdout: enterResult } = await execFileAsync('osascript', ['-e', enterScript]);
|
|
107
|
+
if (enterResult.trim() !== 'ok') {
|
|
108
|
+
throw new Error(`iTerm2 session disappeared before Enter could be sent for TTY ${tty}`);
|
|
109
|
+
}
|
|
95
110
|
}
|
|
96
111
|
|
|
97
112
|
private static async sendViaTerminalApp(tty: string, message: string): Promise<void> {
|
|
98
113
|
const escaped = escapeAppleScript(message);
|
|
99
|
-
// Use
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
114
|
+
// Use Terminal.app's `do script` to send text to the correct tab by TTY.
|
|
115
|
+
// We avoid System Events `keystroke` + `key code 36` because it requires
|
|
116
|
+
// accessibility permissions and unreliably delivers the Return key.
|
|
117
|
+
//
|
|
118
|
+
// `do script` with `in` targets a specific tab without opening a new one.
|
|
119
|
+
// We send text and Enter as two separate calls so the newline arrives
|
|
120
|
+
// outside of bracketed paste mode — same pattern as iTerm2 and tmux.
|
|
121
|
+
const textScript = `
|
|
106
122
|
tell application "Terminal"
|
|
107
|
-
set
|
|
123
|
+
set targetTab to missing value
|
|
108
124
|
repeat with w in windows
|
|
109
125
|
repeat with i from 1 to count of tabs of w
|
|
110
126
|
set t to tab i of w
|
|
111
127
|
if tty of t is "${tty}" then
|
|
112
|
-
set
|
|
113
|
-
set index of w to 1
|
|
114
|
-
activate
|
|
115
|
-
set targetFound to true
|
|
128
|
+
set targetTab to t
|
|
116
129
|
exit repeat
|
|
117
130
|
end if
|
|
118
131
|
end repeat
|
|
119
|
-
if
|
|
132
|
+
if targetTab is not missing value then exit repeat
|
|
120
133
|
end repeat
|
|
121
|
-
if
|
|
122
|
-
|
|
123
|
-
delay 0.1
|
|
124
|
-
tell application "System Events"
|
|
125
|
-
tell process "Terminal"
|
|
126
|
-
keystroke "${escaped}"
|
|
127
|
-
end tell
|
|
128
|
-
end tell
|
|
129
|
-
delay 0.15
|
|
130
|
-
tell application "System Events"
|
|
131
|
-
tell process "Terminal"
|
|
132
|
-
key code 36
|
|
133
|
-
end tell
|
|
134
|
+
if targetTab is missing value then return "not_found"
|
|
135
|
+
do script "${escaped}" in targetTab
|
|
134
136
|
end tell
|
|
135
137
|
return "ok"`;
|
|
136
138
|
|
|
137
|
-
const { stdout } = await execFileAsync('osascript', ['-e',
|
|
138
|
-
if (
|
|
139
|
+
const { stdout: textResult } = await execFileAsync('osascript', ['-e', textScript]);
|
|
140
|
+
if (textResult.trim() !== 'ok') {
|
|
139
141
|
throw new Error(`Terminal.app tab not found for TTY ${tty}`);
|
|
140
142
|
}
|
|
143
|
+
|
|
144
|
+
// Wait for the text to be delivered before sending Enter
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
146
|
+
|
|
147
|
+
const enterScript = `
|
|
148
|
+
tell application "Terminal"
|
|
149
|
+
set targetTab to missing value
|
|
150
|
+
repeat with w in windows
|
|
151
|
+
repeat with i from 1 to count of tabs of w
|
|
152
|
+
set t to tab i of w
|
|
153
|
+
if tty of t is "${tty}" then
|
|
154
|
+
set targetTab to t
|
|
155
|
+
exit repeat
|
|
156
|
+
end if
|
|
157
|
+
end repeat
|
|
158
|
+
if targetTab is not missing value then exit repeat
|
|
159
|
+
end repeat
|
|
160
|
+
if targetTab is missing value then return "not_found"
|
|
161
|
+
do script "" in targetTab
|
|
162
|
+
end tell
|
|
163
|
+
return "ok"`;
|
|
164
|
+
|
|
165
|
+
const { stdout: enterResult } = await execFileAsync('osascript', ['-e', enterScript]);
|
|
166
|
+
if (enterResult.trim() !== 'ok') {
|
|
167
|
+
throw new Error(`Terminal.app tab disappeared before Enter could be sent for TTY ${tty}`);
|
|
168
|
+
}
|
|
141
169
|
}
|
|
142
170
|
}
|