@ai-devkit/agent-manager 0.9.0 → 0.10.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 +7 -0
- package/dist/AgentManager.d.ts.map +1 -1
- package/dist/AgentManager.js +9 -0
- package/dist/AgentManager.js.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.d.ts +2 -44
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.js +10 -285
- package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
- package/dist/terminal/TerminalFocusManager.d.ts.map +1 -1
- package/dist/terminal/TerminalFocusManager.js +38 -27
- package/dist/terminal/TerminalFocusManager.js.map +1 -1
- package/dist/utils/ClaudeSessionParser.d.ts +112 -0
- package/dist/utils/ClaudeSessionParser.d.ts.map +1 -0
- package/dist/utils/ClaudeSessionParser.js +334 -0
- package/dist/utils/ClaudeSessionParser.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/package.json +1 -1
- package/src/AgentManager.ts +11 -1
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +29 -29
- package/src/__tests__/utils/process.test.ts +27 -27
- package/src/adapters/ClaudeCodeAdapter.ts +17 -365
- package/src/terminal/TerminalFocusManager.ts +38 -26
- package/src/utils/ClaudeSessionParser.ts +383 -0
- package/src/utils/process.ts +21 -24
|
@@ -6,61 +6,33 @@ import { listAgentProcesses, enrichProcesses } from '../utils/process';
|
|
|
6
6
|
import { batchGetSessionFileBirthtimes } from '../utils/session';
|
|
7
7
|
import type { SessionFile } from '../utils/session';
|
|
8
8
|
import { matchProcessesToSessions, generateAgentName } from '../utils/matching';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*/
|
|
12
|
-
interface ContentBlock {
|
|
13
|
-
type?: string;
|
|
14
|
-
text?: string;
|
|
15
|
-
content?: string;
|
|
16
|
-
name?: string;
|
|
17
|
-
input?: Record<string, unknown>;
|
|
18
|
-
tool_use_id?: string;
|
|
19
|
-
is_error?: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface SessionEntry {
|
|
23
|
-
type?: string;
|
|
24
|
-
timestamp?: string;
|
|
25
|
-
cwd?: string;
|
|
26
|
-
message?: {
|
|
27
|
-
content?: string | ContentBlock[];
|
|
28
|
-
};
|
|
29
|
-
}
|
|
9
|
+
import { ClaudeSessionParser } from '../utils/ClaudeSessionParser';
|
|
10
|
+
import type { ClaudeSession } from '../utils/ClaudeSessionParser';
|
|
30
11
|
|
|
31
12
|
/**
|
|
32
|
-
* Entry in ~/.claude/sessions/<pid>.json written by Claude Code
|
|
13
|
+
* Entry in ~/.claude/sessions/<pid>.json written by Claude Code.
|
|
14
|
+
* Maps a running process to its session file via PID.
|
|
33
15
|
*/
|
|
34
16
|
interface PidFileEntry {
|
|
35
17
|
pid: number;
|
|
36
18
|
sessionId: string;
|
|
37
19
|
cwd: string;
|
|
38
|
-
|
|
20
|
+
/** Epoch milliseconds when the Claude Code process started */
|
|
21
|
+
startedAt: number;
|
|
39
22
|
kind: string;
|
|
40
23
|
entrypoint: string;
|
|
41
24
|
}
|
|
42
25
|
|
|
43
26
|
/**
|
|
44
|
-
* A process directly matched to a session via PID file (authoritative path)
|
|
27
|
+
* A process directly matched to a session via PID file (authoritative path).
|
|
45
28
|
*/
|
|
46
29
|
interface DirectMatch {
|
|
47
30
|
process: ProcessInfo;
|
|
48
31
|
sessionFile: SessionFile;
|
|
49
32
|
}
|
|
50
33
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
*/
|
|
54
|
-
interface ClaudeSession {
|
|
55
|
-
sessionId: string;
|
|
56
|
-
projectPath: string;
|
|
57
|
-
lastCwd?: string;
|
|
58
|
-
sessionStart: Date;
|
|
59
|
-
lastActive: Date;
|
|
60
|
-
lastEntryType?: string;
|
|
61
|
-
isInterrupted: boolean;
|
|
62
|
-
lastUserMessage?: string;
|
|
63
|
-
}
|
|
34
|
+
/** Maximum allowed delta (ms) between process start time and PID file startedAt. */
|
|
35
|
+
const PID_FILE_STALENESS_MS = 60000;
|
|
64
36
|
|
|
65
37
|
/**
|
|
66
38
|
* Claude Code Adapter
|
|
@@ -77,16 +49,15 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
77
49
|
|
|
78
50
|
private projectsDir: string;
|
|
79
51
|
private sessionsDir: string;
|
|
52
|
+
private parser: ClaudeSessionParser;
|
|
80
53
|
|
|
81
54
|
constructor() {
|
|
82
55
|
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
83
56
|
this.projectsDir = path.join(homeDir, '.claude', 'projects');
|
|
84
57
|
this.sessionsDir = path.join(homeDir, '.claude', 'sessions');
|
|
58
|
+
this.parser = new ClaudeSessionParser();
|
|
85
59
|
}
|
|
86
60
|
|
|
87
|
-
/**
|
|
88
|
-
* Check if this adapter can handle a given process
|
|
89
|
-
*/
|
|
90
61
|
canHandle(processInfo: ProcessInfo): boolean {
|
|
91
62
|
return this.isClaudeExecutable(processInfo.command);
|
|
92
63
|
}
|
|
@@ -97,9 +68,6 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
97
68
|
return base === 'claude' || base === 'claude.exe';
|
|
98
69
|
}
|
|
99
70
|
|
|
100
|
-
/**
|
|
101
|
-
* Detect running Claude Code agents
|
|
102
|
-
*/
|
|
103
71
|
async detectAgents(): Promise<AgentInfo[]> {
|
|
104
72
|
const processes = enrichProcesses(listAgentProcesses('claude'));
|
|
105
73
|
if (processes.length === 0) {
|
|
@@ -125,7 +93,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
125
93
|
|
|
126
94
|
// Build agents from direct (PID-file) matches
|
|
127
95
|
for (const { process: proc, sessionFile } of direct) {
|
|
128
|
-
const sessionData = this.readSession(sessionFile.filePath, sessionFile.resolvedCwd);
|
|
96
|
+
const sessionData = this.parser.readSession(sessionFile.filePath, sessionFile.resolvedCwd);
|
|
129
97
|
if (sessionData) {
|
|
130
98
|
agents.push(this.mapSessionToAgent(sessionData, proc, sessionFile));
|
|
131
99
|
} else {
|
|
@@ -135,7 +103,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
135
103
|
|
|
136
104
|
// Build agents from legacy matches
|
|
137
105
|
for (const match of legacyMatches) {
|
|
138
|
-
const sessionData = this.readSession(
|
|
106
|
+
const sessionData = this.parser.readSession(
|
|
139
107
|
match.session.filePath,
|
|
140
108
|
match.session.resolvedCwd,
|
|
141
109
|
);
|
|
@@ -164,7 +132,6 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
164
132
|
* via a single batched stat call across all directories.
|
|
165
133
|
*/
|
|
166
134
|
private discoverSessions(processes: ProcessInfo[]): SessionFile[] {
|
|
167
|
-
// Collect valid project dirs and map them back to their CWD
|
|
168
135
|
const dirToCwd = new Map<string, string>();
|
|
169
136
|
|
|
170
137
|
for (const proc of processes) {
|
|
@@ -184,10 +151,8 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
184
151
|
|
|
185
152
|
if (dirToCwd.size === 0) return [];
|
|
186
153
|
|
|
187
|
-
// Single batched stat call across all directories
|
|
188
154
|
const files = batchGetSessionFileBirthtimes([...dirToCwd.keys()]);
|
|
189
155
|
|
|
190
|
-
// Set resolvedCwd based on which project dir the file belongs to
|
|
191
156
|
for (const file of files) {
|
|
192
157
|
file.resolvedCwd = dirToCwd.get(file.projectDir) || '';
|
|
193
158
|
}
|
|
@@ -203,7 +168,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
203
168
|
* fallback — processes with no valid PID file (sent to legacy matching)
|
|
204
169
|
*
|
|
205
170
|
* Per-process fallback triggers on: file absent, malformed JSON,
|
|
206
|
-
* stale startedAt (>
|
|
171
|
+
* stale startedAt (>60s from proc.startTime), or missing JSONL.
|
|
207
172
|
*/
|
|
208
173
|
private tryPidFileMatching(processes: ProcessInfo[]): {
|
|
209
174
|
direct: DirectMatch[];
|
|
@@ -222,7 +187,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
222
187
|
// Stale-file guard: reject PID files from a previous process with the same PID
|
|
223
188
|
if (proc.startTime) {
|
|
224
189
|
const deltaMs = Math.abs(proc.startTime.getTime() - entry.startedAt);
|
|
225
|
-
if (deltaMs >
|
|
190
|
+
if (deltaMs > PID_FILE_STALENESS_MS) {
|
|
226
191
|
fallback.push(proc);
|
|
227
192
|
continue;
|
|
228
193
|
}
|
|
@@ -274,7 +239,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
274
239
|
return {
|
|
275
240
|
name: generateAgentName(processInfo.cwd, processInfo.pid),
|
|
276
241
|
type: this.type,
|
|
277
|
-
status: this.determineStatus(session),
|
|
242
|
+
status: this.parser.determineStatus(session),
|
|
278
243
|
summary: session.lastUserMessage || 'Session started',
|
|
279
244
|
pid: processInfo.pid,
|
|
280
245
|
projectPath: sessionFile.resolvedCwd || processInfo.cwd || '',
|
|
@@ -297,320 +262,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
297
262
|
};
|
|
298
263
|
}
|
|
299
264
|
|
|
300
|
-
/**
|
|
301
|
-
* Parse a single session file into ClaudeSession
|
|
302
|
-
*/
|
|
303
|
-
private readSession(
|
|
304
|
-
filePath: string,
|
|
305
|
-
projectPath: string,
|
|
306
|
-
): ClaudeSession | null {
|
|
307
|
-
const sessionId = path.basename(filePath, '.jsonl');
|
|
308
|
-
|
|
309
|
-
let content: string;
|
|
310
|
-
try {
|
|
311
|
-
content = fs.readFileSync(filePath, 'utf-8');
|
|
312
|
-
} catch {
|
|
313
|
-
return null;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const allLines = content.trim().split('\n');
|
|
317
|
-
if (allLines.length === 0) {
|
|
318
|
-
return null;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Parse first line for sessionStart.
|
|
322
|
-
// Claude Code may emit a "file-history-snapshot" as the first entry, which
|
|
323
|
-
// stores its timestamp inside "snapshot.timestamp" rather than at the root.
|
|
324
|
-
let sessionStart: Date | null = null;
|
|
325
|
-
try {
|
|
326
|
-
const firstEntry = JSON.parse(allLines[0]);
|
|
327
|
-
const rawTs: string | undefined =
|
|
328
|
-
firstEntry.timestamp || firstEntry.snapshot?.timestamp;
|
|
329
|
-
if (rawTs) {
|
|
330
|
-
const ts = new Date(rawTs);
|
|
331
|
-
if (!Number.isNaN(ts.getTime())) {
|
|
332
|
-
sessionStart = ts;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
} catch {
|
|
336
|
-
/* skip */
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Parse all lines for session state (file already in memory)
|
|
340
|
-
let lastEntryType: string | undefined;
|
|
341
|
-
let lastActive: Date | undefined;
|
|
342
|
-
let lastCwd: string | undefined;
|
|
343
|
-
let isInterrupted = false;
|
|
344
|
-
let lastUserMessage: string | undefined;
|
|
345
|
-
|
|
346
|
-
for (const line of allLines) {
|
|
347
|
-
try {
|
|
348
|
-
const entry: SessionEntry = JSON.parse(line);
|
|
349
|
-
|
|
350
|
-
if (entry.timestamp) {
|
|
351
|
-
const ts = new Date(entry.timestamp);
|
|
352
|
-
if (!Number.isNaN(ts.getTime())) {
|
|
353
|
-
lastActive = ts;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (typeof entry.cwd === 'string' && entry.cwd.trim().length > 0) {
|
|
358
|
-
lastCwd = entry.cwd;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (entry.type && !this.isMetadataEntryType(entry.type)) {
|
|
362
|
-
lastEntryType = entry.type;
|
|
363
|
-
|
|
364
|
-
if (entry.type === 'user') {
|
|
365
|
-
const msgContent = entry.message?.content;
|
|
366
|
-
isInterrupted =
|
|
367
|
-
Array.isArray(msgContent) &&
|
|
368
|
-
msgContent.some(
|
|
369
|
-
(c) =>
|
|
370
|
-
(c.type === 'text' &&
|
|
371
|
-
c.text?.includes('[Request interrupted')) ||
|
|
372
|
-
(c.type === 'tool_result' &&
|
|
373
|
-
c.content?.includes('[Request interrupted')),
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
// Extract user message text for summary fallback
|
|
377
|
-
const text = this.extractUserMessageText(msgContent);
|
|
378
|
-
if (text) {
|
|
379
|
-
lastUserMessage = text;
|
|
380
|
-
}
|
|
381
|
-
} else {
|
|
382
|
-
isInterrupted = false;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
} catch {
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
return {
|
|
391
|
-
sessionId,
|
|
392
|
-
projectPath: projectPath || lastCwd || '',
|
|
393
|
-
lastCwd,
|
|
394
|
-
sessionStart: sessionStart || lastActive || new Date(),
|
|
395
|
-
lastActive: lastActive || new Date(),
|
|
396
|
-
lastEntryType,
|
|
397
|
-
isInterrupted,
|
|
398
|
-
lastUserMessage,
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Determine agent status from session state
|
|
404
|
-
*/
|
|
405
|
-
private determineStatus(session: ClaudeSession): AgentStatus {
|
|
406
|
-
if (!session.lastEntryType) {
|
|
407
|
-
return AgentStatus.UNKNOWN;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// No age-based IDLE override: every agent in the list is backed by
|
|
411
|
-
// a running process (found via ps), so the entry type is the best
|
|
412
|
-
// indicator of actual state.
|
|
413
|
-
|
|
414
|
-
if (session.lastEntryType === 'user') {
|
|
415
|
-
return session.isInterrupted
|
|
416
|
-
? AgentStatus.WAITING
|
|
417
|
-
: AgentStatus.RUNNING;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (
|
|
421
|
-
session.lastEntryType === 'progress' ||
|
|
422
|
-
session.lastEntryType === 'thinking'
|
|
423
|
-
) {
|
|
424
|
-
return AgentStatus.RUNNING;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
if (session.lastEntryType === 'assistant') {
|
|
428
|
-
return AgentStatus.WAITING;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (session.lastEntryType === 'system') {
|
|
432
|
-
return AgentStatus.IDLE;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
return AgentStatus.UNKNOWN;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Extract meaningful text from a user message content.
|
|
440
|
-
* Handles string and array formats, skill command expansion, and noise filtering.
|
|
441
|
-
*/
|
|
442
|
-
private extractUserMessageText(
|
|
443
|
-
content: string | Array<{ type?: string; text?: string }> | undefined,
|
|
444
|
-
): string | undefined {
|
|
445
|
-
if (!content) {
|
|
446
|
-
return undefined;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
let raw: string | undefined;
|
|
450
|
-
|
|
451
|
-
if (typeof content === 'string') {
|
|
452
|
-
raw = content.trim();
|
|
453
|
-
} else if (Array.isArray(content)) {
|
|
454
|
-
for (const block of content) {
|
|
455
|
-
if (block.type === 'text' && block.text?.trim()) {
|
|
456
|
-
raw = block.text.trim();
|
|
457
|
-
break;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
if (!raw) {
|
|
463
|
-
return undefined;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Skill slash-command: extract /command-name and args
|
|
467
|
-
if (raw.startsWith('<command-message>')) {
|
|
468
|
-
return this.parseCommandMessage(raw);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// Expanded skill content: extract ARGUMENTS line if present, skip otherwise
|
|
472
|
-
if (raw.startsWith('Base directory for this skill:')) {
|
|
473
|
-
const argsMatch = raw.match(/\nARGUMENTS:\s*(.+)/);
|
|
474
|
-
return argsMatch?.[1]?.trim() || undefined;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Filter noise
|
|
478
|
-
if (this.isNoiseMessage(raw)) {
|
|
479
|
-
return undefined;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
return raw;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Parse a <command-message> string into "/command args" format.
|
|
487
|
-
*/
|
|
488
|
-
private parseCommandMessage(raw: string): string | undefined {
|
|
489
|
-
const nameMatch = raw.match(/<command-name>([^<]+)<\/command-name>/);
|
|
490
|
-
const argsMatch = raw.match(/<command-args>([^<]+)<\/command-args>/);
|
|
491
|
-
const name = nameMatch?.[1]?.trim();
|
|
492
|
-
if (!name) {
|
|
493
|
-
return undefined;
|
|
494
|
-
}
|
|
495
|
-
const args = argsMatch?.[1]?.trim();
|
|
496
|
-
return args ? `${name} ${args}` : name;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Check if a message is noise (not a meaningful user intent).
|
|
501
|
-
*/
|
|
502
|
-
private isNoiseMessage(text: string): boolean {
|
|
503
|
-
return (
|
|
504
|
-
text.startsWith('[Request interrupted') ||
|
|
505
|
-
text === 'Tool loaded.' ||
|
|
506
|
-
text.startsWith('This session is being continued')
|
|
507
|
-
);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Check if an entry type is metadata (not conversation state).
|
|
512
|
-
* These should not overwrite lastEntryType used for status determination.
|
|
513
|
-
*/
|
|
514
|
-
private isMetadataEntryType(type: string): boolean {
|
|
515
|
-
return type === 'last-prompt' || type === 'file-history-snapshot';
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Read the full conversation from a Claude Code session JSONL file.
|
|
520
|
-
*
|
|
521
|
-
* Default mode returns only text content from user/assistant/system messages.
|
|
522
|
-
* Verbose mode also includes tool_use and tool_result blocks.
|
|
523
|
-
*/
|
|
524
265
|
getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[] {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
let content: string;
|
|
528
|
-
try {
|
|
529
|
-
content = fs.readFileSync(sessionFilePath, 'utf-8');
|
|
530
|
-
} catch {
|
|
531
|
-
return [];
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const lines = content.trim().split('\n');
|
|
535
|
-
const messages: ConversationMessage[] = [];
|
|
536
|
-
|
|
537
|
-
for (const line of lines) {
|
|
538
|
-
let entry: SessionEntry;
|
|
539
|
-
try {
|
|
540
|
-
entry = JSON.parse(line);
|
|
541
|
-
} catch {
|
|
542
|
-
continue;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const entryType = entry.type;
|
|
546
|
-
if (!entryType || this.isMetadataEntryType(entryType)) continue;
|
|
547
|
-
if (entryType === 'progress' || entryType === 'thinking') continue;
|
|
548
|
-
|
|
549
|
-
let role: ConversationMessage['role'];
|
|
550
|
-
if (entryType === 'user') {
|
|
551
|
-
role = 'user';
|
|
552
|
-
} else if (entryType === 'assistant') {
|
|
553
|
-
role = 'assistant';
|
|
554
|
-
} else if (entryType === 'system') {
|
|
555
|
-
role = 'system';
|
|
556
|
-
} else {
|
|
557
|
-
continue;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
const text = this.extractConversationContent(entry.message?.content, role, verbose);
|
|
561
|
-
if (!text) continue;
|
|
562
|
-
|
|
563
|
-
messages.push({
|
|
564
|
-
role,
|
|
565
|
-
content: text,
|
|
566
|
-
timestamp: entry.timestamp,
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
return messages;
|
|
266
|
+
return this.parser.getConversation(sessionFilePath, options);
|
|
571
267
|
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* Extract displayable content from a message content field.
|
|
575
|
-
*/
|
|
576
|
-
private extractConversationContent(
|
|
577
|
-
content: string | ContentBlock[] | undefined,
|
|
578
|
-
role: ConversationMessage['role'],
|
|
579
|
-
verbose: boolean,
|
|
580
|
-
): string | undefined {
|
|
581
|
-
if (!content) return undefined;
|
|
582
|
-
|
|
583
|
-
if (typeof content === 'string') {
|
|
584
|
-
const trimmed = content.trim();
|
|
585
|
-
if (role === 'user' && this.isNoiseMessage(trimmed)) return undefined;
|
|
586
|
-
return trimmed || undefined;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
if (!Array.isArray(content)) return undefined;
|
|
590
|
-
|
|
591
|
-
const parts: string[] = [];
|
|
592
|
-
|
|
593
|
-
for (const block of content) {
|
|
594
|
-
if (block.type === 'text' && block.text?.trim()) {
|
|
595
|
-
if (role === 'user' && this.isNoiseMessage(block.text.trim())) continue;
|
|
596
|
-
parts.push(block.text.trim());
|
|
597
|
-
} else if (block.type === 'tool_use' && verbose) {
|
|
598
|
-
const inputSummary = block.input?.file_path || block.input?.pattern || block.input?.command || '';
|
|
599
|
-
parts.push(`[Tool: ${block.name}]${inputSummary ? ' ' + inputSummary : ''}`);
|
|
600
|
-
} else if (block.type === 'tool_result' && verbose) {
|
|
601
|
-
const truncated = this.truncateToolResult(block.content || '');
|
|
602
|
-
const prefix = block.is_error ? '[Tool Error]' : '[Tool Result]';
|
|
603
|
-
parts.push(`${prefix} ${truncated}`);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
return parts.length > 0 ? parts.join('\n') : undefined;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
private truncateToolResult(content: string, maxLength = 200): string {
|
|
611
|
-
const firstLine = content.split('\n')[0] || '';
|
|
612
|
-
if (firstLine.length <= maxLength) return firstLine;
|
|
613
|
-
return firstLine.slice(0, maxLength - 3) + '...';
|
|
614
|
-
}
|
|
615
|
-
|
|
616
268
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import { getProcessTty } from '../utils/process';
|
|
4
4
|
|
|
5
|
-
const execAsync = promisify(exec);
|
|
6
5
|
const execFileAsync = promisify(execFile);
|
|
7
6
|
|
|
8
7
|
export enum TerminalType {
|
|
@@ -18,6 +17,10 @@ export interface TerminalLocation {
|
|
|
18
17
|
tty: string; // e.g., "/dev/ttys030"
|
|
19
18
|
}
|
|
20
19
|
|
|
20
|
+
function escapeAppleScript(text: string): string {
|
|
21
|
+
return text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
22
|
+
}
|
|
23
|
+
|
|
21
24
|
export class TerminalFocusManager {
|
|
22
25
|
/**
|
|
23
26
|
* Find the terminal location (emulator info) for a given process ID
|
|
@@ -67,17 +70,16 @@ export class TerminalFocusManager {
|
|
|
67
70
|
default:
|
|
68
71
|
return false;
|
|
69
72
|
}
|
|
70
|
-
} catch
|
|
73
|
+
} catch {
|
|
71
74
|
return false;
|
|
72
75
|
}
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
private async findTmuxPane(tty: string): Promise<TerminalLocation | null> {
|
|
76
79
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const { stdout } = await execAsync("tmux list-panes -a -F '#{pane_tty}|#{session_name}:#{window_index}.#{pane_index}'");
|
|
80
|
+
const { stdout } = await execFileAsync('tmux', [
|
|
81
|
+
'list-panes', '-a', '-F', '#{pane_tty}|#{session_name}:#{window_index}.#{pane_index}'
|
|
82
|
+
]);
|
|
81
83
|
|
|
82
84
|
const lines = stdout.trim().split('\n');
|
|
83
85
|
for (const line of lines) {
|
|
@@ -91,7 +93,7 @@ export class TerminalFocusManager {
|
|
|
91
93
|
};
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
|
-
} catch
|
|
96
|
+
} catch {
|
|
95
97
|
// tmux might not be installed or running
|
|
96
98
|
}
|
|
97
99
|
return null;
|
|
@@ -100,15 +102,19 @@ export class TerminalFocusManager {
|
|
|
100
102
|
private async findITerm2Session(tty: string): Promise<TerminalLocation | null> {
|
|
101
103
|
try {
|
|
102
104
|
// Check if iTerm2 is running first to avoid launching it
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
await execFileAsync('pgrep', ['-x', 'iTerm2']);
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
105
109
|
|
|
110
|
+
try {
|
|
111
|
+
const escapedTty = escapeAppleScript(tty);
|
|
106
112
|
const script = `
|
|
107
113
|
tell application "iTerm"
|
|
108
114
|
repeat with w in windows
|
|
109
115
|
repeat with t in tabs of w
|
|
110
116
|
repeat with s in sessions of t
|
|
111
|
-
if tty of s is "${
|
|
117
|
+
if tty of s is "${escapedTty}" then
|
|
112
118
|
return "found"
|
|
113
119
|
end if
|
|
114
120
|
end repeat
|
|
@@ -117,7 +123,7 @@ export class TerminalFocusManager {
|
|
|
117
123
|
end tell
|
|
118
124
|
`;
|
|
119
125
|
|
|
120
|
-
const { stdout } = await
|
|
126
|
+
const { stdout } = await execFileAsync('osascript', ['-e', script]);
|
|
121
127
|
if (stdout.trim() === "found") {
|
|
122
128
|
return {
|
|
123
129
|
type: TerminalType.ITERM2,
|
|
@@ -125,23 +131,27 @@ export class TerminalFocusManager {
|
|
|
125
131
|
tty
|
|
126
132
|
};
|
|
127
133
|
}
|
|
128
|
-
} catch
|
|
129
|
-
// iTerm2
|
|
134
|
+
} catch {
|
|
135
|
+
// iTerm2 script failed
|
|
130
136
|
}
|
|
131
137
|
return null;
|
|
132
138
|
}
|
|
133
139
|
|
|
134
140
|
private async findTerminalAppWindow(tty: string): Promise<TerminalLocation | null> {
|
|
135
141
|
try {
|
|
136
|
-
// Check if Terminal is running
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
// Check if Terminal.app is running
|
|
143
|
+
await execFileAsync('pgrep', ['-x', 'Terminal']);
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
139
147
|
|
|
148
|
+
try {
|
|
149
|
+
const escapedTty = escapeAppleScript(tty);
|
|
140
150
|
const script = `
|
|
141
151
|
tell application "Terminal"
|
|
142
152
|
repeat with w in windows
|
|
143
153
|
repeat with t in tabs of w
|
|
144
|
-
if tty of t is "${
|
|
154
|
+
if tty of t is "${escapedTty}" then
|
|
145
155
|
return "found"
|
|
146
156
|
end if
|
|
147
157
|
end repeat
|
|
@@ -149,7 +159,7 @@ export class TerminalFocusManager {
|
|
|
149
159
|
end tell
|
|
150
160
|
`;
|
|
151
161
|
|
|
152
|
-
const { stdout } = await
|
|
162
|
+
const { stdout } = await execFileAsync('osascript', ['-e', script]);
|
|
153
163
|
if (stdout.trim() === "found") {
|
|
154
164
|
return {
|
|
155
165
|
type: TerminalType.TERMINAL_APP,
|
|
@@ -157,8 +167,8 @@ export class TerminalFocusManager {
|
|
|
157
167
|
tty
|
|
158
168
|
};
|
|
159
169
|
}
|
|
160
|
-
} catch
|
|
161
|
-
// Terminal
|
|
170
|
+
} catch {
|
|
171
|
+
// Terminal.app script failed
|
|
162
172
|
}
|
|
163
173
|
return null;
|
|
164
174
|
}
|
|
@@ -167,19 +177,20 @@ export class TerminalFocusManager {
|
|
|
167
177
|
try {
|
|
168
178
|
await execFileAsync('tmux', ['switch-client', '-t', identifier]);
|
|
169
179
|
return true;
|
|
170
|
-
} catch
|
|
180
|
+
} catch {
|
|
171
181
|
return false;
|
|
172
182
|
}
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
private async focusITerm2Session(tty: string): Promise<boolean> {
|
|
186
|
+
const escapedTty = escapeAppleScript(tty);
|
|
176
187
|
const script = `
|
|
177
188
|
tell application "iTerm"
|
|
178
189
|
activate
|
|
179
190
|
repeat with w in windows
|
|
180
191
|
repeat with t in tabs of w
|
|
181
192
|
repeat with s in sessions of t
|
|
182
|
-
if tty of s is "${
|
|
193
|
+
if tty of s is "${escapedTty}" then
|
|
183
194
|
select s
|
|
184
195
|
return "true"
|
|
185
196
|
end if
|
|
@@ -188,17 +199,18 @@ export class TerminalFocusManager {
|
|
|
188
199
|
end repeat
|
|
189
200
|
end tell
|
|
190
201
|
`;
|
|
191
|
-
const { stdout } = await
|
|
202
|
+
const { stdout } = await execFileAsync('osascript', ['-e', script]);
|
|
192
203
|
return stdout.trim() === "true";
|
|
193
204
|
}
|
|
194
205
|
|
|
195
206
|
private async focusTerminalAppWindow(tty: string): Promise<boolean> {
|
|
207
|
+
const escapedTty = escapeAppleScript(tty);
|
|
196
208
|
const script = `
|
|
197
209
|
tell application "Terminal"
|
|
198
210
|
activate
|
|
199
211
|
repeat with w in windows
|
|
200
212
|
repeat with t in tabs of w
|
|
201
|
-
if tty of t is "${
|
|
213
|
+
if tty of t is "${escapedTty}" then
|
|
202
214
|
set index of w to 1
|
|
203
215
|
set selected tab of w to t
|
|
204
216
|
return "true"
|
|
@@ -207,7 +219,7 @@ export class TerminalFocusManager {
|
|
|
207
219
|
end repeat
|
|
208
220
|
end tell
|
|
209
221
|
`;
|
|
210
|
-
const { stdout } = await
|
|
222
|
+
const { stdout } = await execFileAsync('osascript', ['-e', script]);
|
|
211
223
|
return stdout.trim() === "true";
|
|
212
224
|
}
|
|
213
225
|
}
|