@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.
@@ -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('pgrep -x Terminal || echo "no"');
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: System Events `keystroke` + `key code 36` (Return)
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;AAa/D,qBAAa,SAAS;IAClB;;;;;;;;;;;;;;OAcG;WACU,IAAI,CAAC,QAAQ,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;mBAgBxD,WAAW;mBAWX,aAAa;mBAsCb,kBAAkB;CA6C1C"}
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 and double quotes must be escaped.
10
+ * Backslashes, double quotes, and newlines must be escaped.
11
11
  */
12
12
  function escapeAppleScript(text) {
13
- return text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
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: System Events `keystroke` + `key code 36` (Return)
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
- static async sendViaITerm2(tty, message) {
55
- const escaped = escapeAppleScript(message);
56
- // Send text WITHOUT a trailing newline to avoid the newline being swallowed
57
- // by bracketed paste mode. Then simulate pressing Return separately so that
58
- // Claude Code (and other interactive TUIs) treat it as a real submit action.
59
- const script = `
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 write text "${escaped}" newline no
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
- const { stdout } = await execFileAsync('osascript', ['-e', script]);
86
- if (stdout.trim() !== 'ok') {
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 System Events keystroke to type into the foreground process,
93
- // NOT Terminal.app's "do script" which runs a new shell command.
94
- // First activate Terminal and select the correct tab, then type via System Events.
95
- // Send the text first, then wait for the paste/input to complete before pressing
96
- // Return separately this ensures interactive TUIs (like Claude Code) see the
97
- // Return as a real submit action, not part of a bracketed paste.
98
- const script = `
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 targetFound to false
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 selected tab of w to t
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 targetFound then exit repeat
122
+ if targetTab is not missing value then exit repeat
113
123
  end repeat
114
- if not targetFound then return "not_found"
115
- end tell
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', script]);
130
- if (stdout.trim() !== 'ok') {
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":"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"}
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;SACN,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;SACpB,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;AACvC,CAAC;AAED,MAAa,SAAS;IAClB;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,QAA0B,EAAE,OAAe;QACzD,QAAQ,QAAQ,CAAC,IAAI,EAAE,CAAC;YACpB,KAAK,mCAAY,CAAC,IAAI;gBAClB,OAAO,SAAS,CAAC,WAAW,CAAC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YAC/D,KAAK,mCAAY,CAAC,MAAM;gBACpB,OAAO,SAAS,CAAC,aAAa,CAAC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YAC1D,KAAK,mCAAY,CAAC,YAAY;gBAC1B,OAAO,SAAS,CAAC,kBAAkB,CAAC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YAC/D;gBACI,MAAM,IAAI,KAAK,CACX,iDAAiD,QAAQ,CAAC,IAAI,KAAK;oBACnE,wCAAwC,CAC3C,CAAC;QACV,CAAC;IACL,CAAC;IAEO,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,UAAkB,EAAE,OAAe;QAChE,kEAAkE;QAClE,oEAAoE;QACpE,qEAAqE;QACrE,sEAAsE;QACtE,6DAA6D;QAC7D,MAAM,aAAa,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;QAC5E,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;QACzD,MAAM,aAAa,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;IAC1E,CAAC;IAED;;;;OAIG;IACK,MAAM,CAAC,mBAAmB,CAAC,GAAW,EAAE,cAAsB;QAClE,OAAO;;;;;;0BAMW,GAAG;;;;;;;;;;0BAUH,cAAc;;YAE5B,CAAC;IACT,CAAC;IAEO,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,GAAW,EAAE,OAAe;QAC3D,MAAM,OAAO,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;QAC3C,sEAAsE;QACtE,yEAAyE;QACzE,sEAAsE;QACtE,sEAAsE;QACtE,MAAM,UAAU,GAAG,SAAS,CAAC,mBAAmB,CAAC,GAAG,EAAE,eAAe,OAAO,cAAc,CAAC,CAAC;QAE5F,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,aAAa,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC;QACpF,IAAI,UAAU,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,oCAAoC,GAAG,EAAE,CAAC,CAAC;QAC/D,CAAC;QAED,iEAAiE;QACjE,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;QAEzD,MAAM,WAAW,GAAG,SAAS,CAAC,mBAAmB,CAAC,GAAG,EAAE,2BAA2B,CAAC,CAAC;QACpF,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,aAAa,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC;QACtF,IAAI,WAAW,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,iEAAiE,GAAG,EAAE,CAAC,CAAC;QAC5F,CAAC;IACL,CAAC;IAEO,MAAM,CAAC,KAAK,CAAC,kBAAkB,CAAC,GAAW,EAAE,OAAe;QAChE,MAAM,OAAO,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;QAC3C,yEAAyE;QACzE,yEAAyE;QACzE,oEAAoE;QACpE,EAAE;QACF,0EAA0E;QAC1E,sEAAsE;QACtE,qEAAqE;QACrE,MAAM,UAAU,GAAG;;;;;;wBAMH,GAAG;;;;;;;;eAQZ,OAAO;;YAEV,CAAC;QAEL,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,aAAa,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC;QACpF,IAAI,UAAU,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,sCAAsC,GAAG,EAAE,CAAC,CAAC;QACjE,CAAC;QAED,yDAAyD;QACzD,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;QAEzD,MAAM,WAAW,GAAG;;;;;;wBAMJ,GAAG;;;;;;;;;;YAUf,CAAC;QAEL,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,aAAa,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC;QACtF,IAAI,WAAW,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,mEAAmE,GAAG,EAAE,CAAC,CAAC;QAC9F,CAAC;IACL,CAAC;CACJ;AAvJD,8BAuJC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-devkit/agent-manager",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Standalone agent detection and management utilities for AI DevKit",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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
- const scriptArg = (mockedExecFile.mock.calls[0] as unknown[])[1] as string[];
87
- const script = scriptArg[1];
88
- expect(script).toContain('key code 36');
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 System Events keystroke (not do script)', async () => {
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
- const scriptArg = (mockedExecFile.mock.calls[0] as unknown[])[1] as string[];
124
- const script = scriptArg[1];
125
- expect(script).toContain('keystroke "hello"');
126
- expect(script).toContain('key code 36');
127
- expect(script).not.toContain('do script');
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('pgrep -x Terminal || echo "no"');
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 and double quotes must be escaped.
10
+ * Backslashes, double quotes, and newlines must be escaped.
11
11
  */
12
12
  function escapeAppleScript(text: string): string {
13
- return text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
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: System Events `keystroke` + `key code 36` (Return)
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
- private static async sendViaITerm2(tty: string, message: string): Promise<void> {
60
- const escaped = escapeAppleScript(message);
61
- // Send text WITHOUT a trailing newline to avoid the newline being swallowed
62
- // by bracketed paste mode. Then simulate pressing Return separately so that
63
- // Claude Code (and other interactive TUIs) treat it as a real submit action.
64
- const script = `
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 write text "${escaped}" newline no
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', script]);
92
- if (stdout.trim() !== 'ok') {
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 System Events keystroke to type into the foreground process,
100
- // NOT Terminal.app's "do script" which runs a new shell command.
101
- // First activate Terminal and select the correct tab, then type via System Events.
102
- // Send the text first, then wait for the paste/input to complete before pressing
103
- // Return separately this ensures interactive TUIs (like Claude Code) see the
104
- // Return as a real submit action, not part of a bracketed paste.
105
- const script = `
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 targetFound to false
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 selected tab of w to t
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 targetFound then exit repeat
132
+ if targetTab is not missing value then exit repeat
120
133
  end repeat
121
- if not targetFound then return "not_found"
122
- end tell
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', script]);
138
- if (stdout.trim() !== 'ok') {
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
  }