@ai-devkit/agent-manager 0.9.0 → 0.11.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/AgentManager.d.ts +21 -1
- package/dist/AgentManager.d.ts.map +1 -1
- package/dist/AgentManager.js +47 -0
- package/dist/AgentManager.js.map +1 -1
- package/dist/adapters/AgentAdapter.d.ts +66 -0
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.d.ts +14 -43
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.js +60 -275
- package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts +14 -1
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +105 -6
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/dist/adapters/GeminiCliAdapter.d.ts +9 -1
- package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
- package/dist/adapters/GeminiCliAdapter.js +77 -6
- package/dist/adapters/GeminiCliAdapter.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/terminal/TerminalFocusManager.d.ts.map +1 -1
- package/dist/terminal/TerminalFocusManager.js +36 -27
- package/dist/terminal/TerminalFocusManager.js.map +1 -1
- package/dist/terminal/TtyWriter.d.ts.map +1 -1
- package/dist/terminal/TtyWriter.js +3 -12
- package/dist/terminal/TtyWriter.js.map +1 -1
- package/dist/utils/ClaudeSessionParser.d.ts +114 -0
- package/dist/utils/ClaudeSessionParser.d.ts.map +1 -0
- package/dist/utils/ClaudeSessionParser.js +377 -0
- package/dist/utils/ClaudeSessionParser.js.map +1 -0
- package/dist/utils/applescript.d.ts +6 -0
- package/dist/utils/applescript.d.ts.map +1 -0
- package/dist/utils/applescript.js +14 -0
- package/dist/utils/applescript.js.map +1 -0
- package/dist/utils/process.d.ts +3 -4
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +11 -15
- package/dist/utils/process.js.map +1 -1
- package/dist/utils/session.d.ts +34 -5
- package/dist/utils/session.d.ts.map +1 -1
- package/dist/utils/session.js +90 -44
- package/dist/utils/session.js.map +1 -1
- package/package.json +1 -1
- package/src/AgentManager.ts +66 -4
- package/src/__tests__/AgentManager.test.ts +134 -2
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +188 -32
- package/src/__tests__/adapters/CodexAdapter.test.ts +123 -3
- package/src/__tests__/adapters/GeminiCliAdapter.test.ts +133 -0
- package/src/__tests__/utils/ClaudeSessionParser.test.ts +195 -0
- package/src/__tests__/utils/process.test.ts +27 -27
- package/src/__tests__/utils/session.test.ts +79 -43
- package/src/adapters/AgentAdapter.ts +76 -0
- package/src/adapters/ClaudeCodeAdapter.ts +82 -356
- package/src/adapters/CodexAdapter.ts +126 -8
- package/src/adapters/GeminiCliAdapter.ts +102 -7
- package/src/index.ts +9 -1
- package/src/terminal/TerminalFocusManager.ts +35 -26
- package/src/terminal/TtyWriter.ts +1 -11
- package/src/utils/ClaudeSessionParser.ts +437 -0
- package/src/utils/applescript.ts +10 -0
- package/src/utils/process.ts +21 -24
- package/src/utils/session.ts +86 -45
|
@@ -12,10 +12,17 @@
|
|
|
12
12
|
|
|
13
13
|
import * as fs from 'fs';
|
|
14
14
|
import * as path from 'path';
|
|
15
|
-
import type {
|
|
15
|
+
import type {
|
|
16
|
+
AgentAdapter,
|
|
17
|
+
AgentInfo,
|
|
18
|
+
ProcessInfo,
|
|
19
|
+
ConversationMessage,
|
|
20
|
+
SessionSummary,
|
|
21
|
+
ListSessionsOptions,
|
|
22
|
+
} from './AgentAdapter';
|
|
16
23
|
import { AgentStatus } from './AgentAdapter';
|
|
17
24
|
import { listAgentProcesses, enrichProcesses } from '../utils/process';
|
|
18
|
-
import { batchGetSessionFileBirthtimes } from '../utils/session';
|
|
25
|
+
import { batchGetSessionFileBirthtimes, isDirectory, safeReadFile, safeReaddir, safeStat } from '../utils/session';
|
|
19
26
|
import type { SessionFile } from '../utils/session';
|
|
20
27
|
import { matchProcessesToSessions, generateAgentName } from '../utils/matching';
|
|
21
28
|
|
|
@@ -326,12 +333,8 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
326
333
|
getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[] {
|
|
327
334
|
const verbose = options?.verbose ?? false;
|
|
328
335
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
content = fs.readFileSync(sessionFilePath, 'utf-8');
|
|
332
|
-
} catch {
|
|
333
|
-
return [];
|
|
334
|
-
}
|
|
336
|
+
const content = safeReadFile(sessionFilePath);
|
|
337
|
+
if (content === undefined) return [];
|
|
335
338
|
|
|
336
339
|
const lines = content.trim().split('\n');
|
|
337
340
|
const messages: ConversationMessage[] = [];
|
|
@@ -372,4 +375,119 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
372
375
|
|
|
373
376
|
return messages;
|
|
374
377
|
}
|
|
378
|
+
|
|
379
|
+
async listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]> {
|
|
380
|
+
if (!isDirectory(this.codexSessionsDir)) return [];
|
|
381
|
+
|
|
382
|
+
const files = this.collectAllSessionFiles();
|
|
383
|
+
const summaries: SessionSummary[] = [];
|
|
384
|
+
|
|
385
|
+
for (const filePath of files) {
|
|
386
|
+
const summary = this.fileToSessionSummary(filePath);
|
|
387
|
+
if (!summary) continue;
|
|
388
|
+
if (opts?.cwd !== undefined && summary.cwd !== opts.cwd) continue;
|
|
389
|
+
summaries.push(summary);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return summaries;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Walk every YYYY/MM/DD directory under `codexSessionsDir` and return
|
|
397
|
+
* absolute paths of `.jsonl` files. Tolerates malformed layouts
|
|
398
|
+
* (skips entries that aren't directories at the expected depth).
|
|
399
|
+
*/
|
|
400
|
+
private collectAllSessionFiles(): string[] {
|
|
401
|
+
const out: string[] = [];
|
|
402
|
+
|
|
403
|
+
for (const yearEntry of safeReaddir(this.codexSessionsDir)) {
|
|
404
|
+
const yearDir = path.join(this.codexSessionsDir, yearEntry);
|
|
405
|
+
if (!isDirectory(yearDir)) continue;
|
|
406
|
+
|
|
407
|
+
for (const monthEntry of safeReaddir(yearDir)) {
|
|
408
|
+
const monthDir = path.join(yearDir, monthEntry);
|
|
409
|
+
if (!isDirectory(monthDir)) continue;
|
|
410
|
+
|
|
411
|
+
for (const dayEntry of safeReaddir(monthDir)) {
|
|
412
|
+
const dayDir = path.join(monthDir, dayEntry);
|
|
413
|
+
if (!isDirectory(dayDir)) continue;
|
|
414
|
+
|
|
415
|
+
for (const fileEntry of safeReaddir(dayDir)) {
|
|
416
|
+
if (!fileEntry.endsWith('.jsonl')) continue;
|
|
417
|
+
out.push(path.join(dayDir, fileEntry));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return out;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Read a Codex session JSONL file and produce a {@link SessionSummary}.
|
|
428
|
+
* Returns null when the file is unreadable, has no `session_meta`, or
|
|
429
|
+
* lacks a session id.
|
|
430
|
+
*/
|
|
431
|
+
private fileToSessionSummary(filePath: string): SessionSummary | null {
|
|
432
|
+
const content = safeReadFile(filePath);
|
|
433
|
+
if (content === undefined) return null;
|
|
434
|
+
|
|
435
|
+
const allLines = content.trim().split('\n');
|
|
436
|
+
if (!allLines[0]) return null;
|
|
437
|
+
|
|
438
|
+
let metaEntry: CodexEventEntry;
|
|
439
|
+
try {
|
|
440
|
+
metaEntry = JSON.parse(allLines[0]);
|
|
441
|
+
} catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (metaEntry.type !== 'session_meta' || !metaEntry.payload?.id) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
let firstUserMessage = '';
|
|
450
|
+
let lastTimestamp: Date | null = null;
|
|
451
|
+
|
|
452
|
+
for (let i = 1; i < allLines.length; i++) {
|
|
453
|
+
let entry: CodexEventEntry;
|
|
454
|
+
try {
|
|
455
|
+
entry = JSON.parse(allLines[i]);
|
|
456
|
+
} catch {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const ts = this.parseTimestamp(entry.timestamp);
|
|
461
|
+
if (ts) lastTimestamp = ts;
|
|
462
|
+
|
|
463
|
+
if (
|
|
464
|
+
!firstUserMessage &&
|
|
465
|
+
entry.payload?.type === 'user_message' &&
|
|
466
|
+
typeof entry.payload.message === 'string' &&
|
|
467
|
+
entry.payload.message.trim().length > 0
|
|
468
|
+
) {
|
|
469
|
+
firstUserMessage = entry.payload.message.trim();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const stat = safeStat(filePath);
|
|
474
|
+
|
|
475
|
+
const startedAt =
|
|
476
|
+
this.parseTimestamp(metaEntry.payload.timestamp) ||
|
|
477
|
+
lastTimestamp ||
|
|
478
|
+
stat?.birthtime ||
|
|
479
|
+
stat?.mtime ||
|
|
480
|
+
new Date();
|
|
481
|
+
const lastActive = lastTimestamp || startedAt;
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
type: 'codex',
|
|
485
|
+
sessionId: metaEntry.payload.id,
|
|
486
|
+
cwd: metaEntry.payload.cwd || '',
|
|
487
|
+
firstUserMessage,
|
|
488
|
+
lastActive,
|
|
489
|
+
startedAt,
|
|
490
|
+
sessionFilePath: filePath,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
375
493
|
}
|
|
@@ -13,9 +13,17 @@
|
|
|
13
13
|
import * as crypto from 'crypto';
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import * as path from 'path';
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
AgentAdapter,
|
|
18
|
+
AgentInfo,
|
|
19
|
+
ProcessInfo,
|
|
20
|
+
ConversationMessage,
|
|
21
|
+
SessionSummary,
|
|
22
|
+
ListSessionsOptions,
|
|
23
|
+
} from './AgentAdapter';
|
|
17
24
|
import { AgentStatus } from './AgentAdapter';
|
|
18
25
|
import { listAgentProcesses, enrichProcesses } from '../utils/process';
|
|
26
|
+
import { isDirectory, safeReadFile, safeReaddir, safeStat } from '../utils/session';
|
|
19
27
|
import type { SessionFile } from '../utils/session';
|
|
20
28
|
import { matchProcessesToSessions, generateAgentName } from '../utils/matching';
|
|
21
29
|
|
|
@@ -441,12 +449,8 @@ export class GeminiCliAdapter implements AgentAdapter {
|
|
|
441
449
|
getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[] {
|
|
442
450
|
const verbose = options?.verbose ?? false;
|
|
443
451
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
content = fs.readFileSync(sessionFilePath, 'utf-8');
|
|
447
|
-
} catch {
|
|
448
|
-
return [];
|
|
449
|
-
}
|
|
452
|
+
const content = safeReadFile(sessionFilePath);
|
|
453
|
+
if (content === undefined) return [];
|
|
450
454
|
|
|
451
455
|
let parsed: GeminiSessionFile;
|
|
452
456
|
try {
|
|
@@ -485,4 +489,95 @@ export class GeminiCliAdapter implements AgentAdapter {
|
|
|
485
489
|
|
|
486
490
|
return messages;
|
|
487
491
|
}
|
|
492
|
+
|
|
493
|
+
async listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]> {
|
|
494
|
+
if (!isDirectory(this.geminiTmpDir)) return [];
|
|
495
|
+
|
|
496
|
+
const summaries: SessionSummary[] = [];
|
|
497
|
+
|
|
498
|
+
for (const shortId of safeReaddir(this.geminiTmpDir)) {
|
|
499
|
+
const chatsDir = path.join(
|
|
500
|
+
this.geminiTmpDir,
|
|
501
|
+
shortId,
|
|
502
|
+
GeminiCliAdapter.CHATS_DIR_NAME,
|
|
503
|
+
);
|
|
504
|
+
if (!isDirectory(chatsDir)) continue;
|
|
505
|
+
|
|
506
|
+
for (const fileName of safeReaddir(chatsDir)) {
|
|
507
|
+
if (
|
|
508
|
+
!fileName.startsWith(GeminiCliAdapter.SESSION_FILE_PREFIX) ||
|
|
509
|
+
!fileName.endsWith('.json')
|
|
510
|
+
) {
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const filePath = path.join(chatsDir, fileName);
|
|
515
|
+
const summary = this.fileToSessionSummary(filePath);
|
|
516
|
+
if (!summary) continue;
|
|
517
|
+
if (opts?.cwd !== undefined && summary.cwd !== opts.cwd) continue;
|
|
518
|
+
summaries.push(summary);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return summaries;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Read a Gemini session JSON file and produce a {@link SessionSummary}.
|
|
527
|
+
* Returns null when the file is unreadable, the JSON doesn't parse,
|
|
528
|
+
* or the body lacks a sessionId.
|
|
529
|
+
*/
|
|
530
|
+
private fileToSessionSummary(filePath: string): SessionSummary | null {
|
|
531
|
+
const content = safeReadFile(filePath);
|
|
532
|
+
if (content === undefined) return null;
|
|
533
|
+
|
|
534
|
+
let parsed: GeminiSessionFile;
|
|
535
|
+
try {
|
|
536
|
+
parsed = JSON.parse(content);
|
|
537
|
+
} catch {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (!parsed.sessionId) return null;
|
|
542
|
+
|
|
543
|
+
const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
544
|
+
const firstUserMessage = this.extractFirstUserMessage(messages);
|
|
545
|
+
|
|
546
|
+
const cwd =
|
|
547
|
+
Array.isArray(parsed.directories) && parsed.directories.length > 0
|
|
548
|
+
? parsed.directories[0]
|
|
549
|
+
: '';
|
|
550
|
+
|
|
551
|
+
const stat = safeStat(filePath);
|
|
552
|
+
|
|
553
|
+
const lastEntryTimestamp = this.parseTimestamp(
|
|
554
|
+
messages.length > 0 ? messages[messages.length - 1]?.timestamp : undefined,
|
|
555
|
+
);
|
|
556
|
+
const lastActive =
|
|
557
|
+
this.parseTimestamp(parsed.lastUpdated) ||
|
|
558
|
+
lastEntryTimestamp ||
|
|
559
|
+
stat?.mtime ||
|
|
560
|
+
new Date();
|
|
561
|
+
const startedAt =
|
|
562
|
+
this.parseTimestamp(parsed.startTime) || stat?.birthtime || stat?.mtime || lastActive;
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
type: 'gemini_cli',
|
|
566
|
+
sessionId: parsed.sessionId,
|
|
567
|
+
cwd,
|
|
568
|
+
firstUserMessage,
|
|
569
|
+
lastActive,
|
|
570
|
+
startedAt,
|
|
571
|
+
sessionFilePath: filePath,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private extractFirstUserMessage(messages: GeminiMessageEntry[]): string {
|
|
576
|
+
for (const entry of messages) {
|
|
577
|
+
if (entry?.type !== 'user') continue;
|
|
578
|
+
const text = this.messageText(entry).trim();
|
|
579
|
+
if (text) return text;
|
|
580
|
+
}
|
|
581
|
+
return '';
|
|
582
|
+
}
|
|
488
583
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,15 @@ export { ClaudeCodeAdapter } from './adapters/ClaudeCodeAdapter';
|
|
|
4
4
|
export { CodexAdapter } from './adapters/CodexAdapter';
|
|
5
5
|
export { GeminiCliAdapter } from './adapters/GeminiCliAdapter';
|
|
6
6
|
export { AgentStatus } from './adapters/AgentAdapter';
|
|
7
|
-
export type {
|
|
7
|
+
export type {
|
|
8
|
+
AgentAdapter,
|
|
9
|
+
AgentType,
|
|
10
|
+
AgentInfo,
|
|
11
|
+
ProcessInfo,
|
|
12
|
+
ConversationMessage,
|
|
13
|
+
SessionSummary,
|
|
14
|
+
ListSessionsOptions,
|
|
15
|
+
} from './adapters/AgentAdapter';
|
|
8
16
|
|
|
9
17
|
export { TerminalFocusManager, TerminalType } from './terminal/TerminalFocusManager';
|
|
10
18
|
export type { TerminalLocation } from './terminal/TerminalFocusManager';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import { getProcessTty } from '../utils/process';
|
|
4
|
+
import { escapeAppleScript } from '../utils/applescript';
|
|
4
5
|
|
|
5
|
-
const execAsync = promisify(exec);
|
|
6
6
|
const execFileAsync = promisify(execFile);
|
|
7
7
|
|
|
8
8
|
export enum TerminalType {
|
|
@@ -67,17 +67,16 @@ export class TerminalFocusManager {
|
|
|
67
67
|
default:
|
|
68
68
|
return false;
|
|
69
69
|
}
|
|
70
|
-
} catch
|
|
70
|
+
} catch {
|
|
71
71
|
return false;
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
private async findTmuxPane(tty: string): Promise<TerminalLocation | null> {
|
|
76
76
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const { stdout } = await execAsync("tmux list-panes -a -F '#{pane_tty}|#{session_name}:#{window_index}.#{pane_index}'");
|
|
77
|
+
const { stdout } = await execFileAsync('tmux', [
|
|
78
|
+
'list-panes', '-a', '-F', '#{pane_tty}|#{session_name}:#{window_index}.#{pane_index}'
|
|
79
|
+
]);
|
|
81
80
|
|
|
82
81
|
const lines = stdout.trim().split('\n');
|
|
83
82
|
for (const line of lines) {
|
|
@@ -91,7 +90,7 @@ export class TerminalFocusManager {
|
|
|
91
90
|
};
|
|
92
91
|
}
|
|
93
92
|
}
|
|
94
|
-
} catch
|
|
93
|
+
} catch {
|
|
95
94
|
// tmux might not be installed or running
|
|
96
95
|
}
|
|
97
96
|
return null;
|
|
@@ -100,15 +99,19 @@ export class TerminalFocusManager {
|
|
|
100
99
|
private async findITerm2Session(tty: string): Promise<TerminalLocation | null> {
|
|
101
100
|
try {
|
|
102
101
|
// Check if iTerm2 is running first to avoid launching it
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
await execFileAsync('pgrep', ['-x', 'iTerm2']);
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
105
106
|
|
|
107
|
+
try {
|
|
108
|
+
const escapedTty = escapeAppleScript(tty);
|
|
106
109
|
const script = `
|
|
107
110
|
tell application "iTerm"
|
|
108
111
|
repeat with w in windows
|
|
109
112
|
repeat with t in tabs of w
|
|
110
113
|
repeat with s in sessions of t
|
|
111
|
-
if tty of s is "${
|
|
114
|
+
if tty of s is "${escapedTty}" then
|
|
112
115
|
return "found"
|
|
113
116
|
end if
|
|
114
117
|
end repeat
|
|
@@ -117,7 +120,7 @@ export class TerminalFocusManager {
|
|
|
117
120
|
end tell
|
|
118
121
|
`;
|
|
119
122
|
|
|
120
|
-
const { stdout } = await
|
|
123
|
+
const { stdout } = await execFileAsync('osascript', ['-e', script]);
|
|
121
124
|
if (stdout.trim() === "found") {
|
|
122
125
|
return {
|
|
123
126
|
type: TerminalType.ITERM2,
|
|
@@ -125,23 +128,27 @@ export class TerminalFocusManager {
|
|
|
125
128
|
tty
|
|
126
129
|
};
|
|
127
130
|
}
|
|
128
|
-
} catch
|
|
129
|
-
// iTerm2
|
|
131
|
+
} catch {
|
|
132
|
+
// iTerm2 script failed
|
|
130
133
|
}
|
|
131
134
|
return null;
|
|
132
135
|
}
|
|
133
136
|
|
|
134
137
|
private async findTerminalAppWindow(tty: string): Promise<TerminalLocation | null> {
|
|
135
138
|
try {
|
|
136
|
-
// Check if Terminal is running
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
// Check if Terminal.app is running
|
|
140
|
+
await execFileAsync('pgrep', ['-x', 'Terminal']);
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
139
144
|
|
|
145
|
+
try {
|
|
146
|
+
const escapedTty = escapeAppleScript(tty);
|
|
140
147
|
const script = `
|
|
141
148
|
tell application "Terminal"
|
|
142
149
|
repeat with w in windows
|
|
143
150
|
repeat with t in tabs of w
|
|
144
|
-
if tty of t is "${
|
|
151
|
+
if tty of t is "${escapedTty}" then
|
|
145
152
|
return "found"
|
|
146
153
|
end if
|
|
147
154
|
end repeat
|
|
@@ -149,7 +156,7 @@ export class TerminalFocusManager {
|
|
|
149
156
|
end tell
|
|
150
157
|
`;
|
|
151
158
|
|
|
152
|
-
const { stdout } = await
|
|
159
|
+
const { stdout } = await execFileAsync('osascript', ['-e', script]);
|
|
153
160
|
if (stdout.trim() === "found") {
|
|
154
161
|
return {
|
|
155
162
|
type: TerminalType.TERMINAL_APP,
|
|
@@ -157,8 +164,8 @@ export class TerminalFocusManager {
|
|
|
157
164
|
tty
|
|
158
165
|
};
|
|
159
166
|
}
|
|
160
|
-
} catch
|
|
161
|
-
// Terminal
|
|
167
|
+
} catch {
|
|
168
|
+
// Terminal.app script failed
|
|
162
169
|
}
|
|
163
170
|
return null;
|
|
164
171
|
}
|
|
@@ -167,19 +174,20 @@ export class TerminalFocusManager {
|
|
|
167
174
|
try {
|
|
168
175
|
await execFileAsync('tmux', ['switch-client', '-t', identifier]);
|
|
169
176
|
return true;
|
|
170
|
-
} catch
|
|
177
|
+
} catch {
|
|
171
178
|
return false;
|
|
172
179
|
}
|
|
173
180
|
}
|
|
174
181
|
|
|
175
182
|
private async focusITerm2Session(tty: string): Promise<boolean> {
|
|
183
|
+
const escapedTty = escapeAppleScript(tty);
|
|
176
184
|
const script = `
|
|
177
185
|
tell application "iTerm"
|
|
178
186
|
activate
|
|
179
187
|
repeat with w in windows
|
|
180
188
|
repeat with t in tabs of w
|
|
181
189
|
repeat with s in sessions of t
|
|
182
|
-
if tty of s is "${
|
|
190
|
+
if tty of s is "${escapedTty}" then
|
|
183
191
|
select s
|
|
184
192
|
return "true"
|
|
185
193
|
end if
|
|
@@ -188,17 +196,18 @@ export class TerminalFocusManager {
|
|
|
188
196
|
end repeat
|
|
189
197
|
end tell
|
|
190
198
|
`;
|
|
191
|
-
const { stdout } = await
|
|
199
|
+
const { stdout } = await execFileAsync('osascript', ['-e', script]);
|
|
192
200
|
return stdout.trim() === "true";
|
|
193
201
|
}
|
|
194
202
|
|
|
195
203
|
private async focusTerminalAppWindow(tty: string): Promise<boolean> {
|
|
204
|
+
const escapedTty = escapeAppleScript(tty);
|
|
196
205
|
const script = `
|
|
197
206
|
tell application "Terminal"
|
|
198
207
|
activate
|
|
199
208
|
repeat with w in windows
|
|
200
209
|
repeat with t in tabs of w
|
|
201
|
-
if tty of t is "${
|
|
210
|
+
if tty of t is "${escapedTty}" then
|
|
202
211
|
set index of w to 1
|
|
203
212
|
set selected tab of w to t
|
|
204
213
|
return "true"
|
|
@@ -207,7 +216,7 @@ export class TerminalFocusManager {
|
|
|
207
216
|
end repeat
|
|
208
217
|
end tell
|
|
209
218
|
`;
|
|
210
|
-
const { stdout } = await
|
|
219
|
+
const { stdout } = await execFileAsync('osascript', ['-e', script]);
|
|
211
220
|
return stdout.trim() === "true";
|
|
212
221
|
}
|
|
213
222
|
}
|
|
@@ -2,20 +2,10 @@ import { execFile } from 'child_process';
|
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import type { TerminalLocation } from './TerminalFocusManager';
|
|
4
4
|
import { TerminalType } from './TerminalFocusManager';
|
|
5
|
+
import { escapeAppleScript } from '../utils/applescript';
|
|
5
6
|
|
|
6
7
|
const execFileAsync = promisify(execFile);
|
|
7
8
|
|
|
8
|
-
/**
|
|
9
|
-
* Escape a string for safe use inside an AppleScript double-quoted string.
|
|
10
|
-
* Backslashes, double quotes, and newlines must be escaped.
|
|
11
|
-
*/
|
|
12
|
-
function escapeAppleScript(text: string): string {
|
|
13
|
-
return text
|
|
14
|
-
.replace(/\\/g, '\\\\')
|
|
15
|
-
.replace(/"/g, '\\"')
|
|
16
|
-
.replace(/\r\n|\r|\n/g, '\\n');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
9
|
export class TtyWriter {
|
|
20
10
|
/**
|
|
21
11
|
* Send a message as keyboard input to a terminal session.
|