@ai-devkit/agent-manager 0.10.0 → 0.12.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 +14 -1
- package/dist/AgentManager.d.ts.map +1 -1
- package/dist/AgentManager.js +38 -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 +22 -1
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.js +108 -4
- 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 +5 -7
- 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 +2 -0
- package/dist/utils/ClaudeSessionParser.d.ts.map +1 -1
- package/dist/utils/ClaudeSessionParser.js +48 -5
- package/dist/utils/ClaudeSessionParser.js.map +1 -1
- 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/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 +55 -3
- package/src/__tests__/AgentManager.test.ts +134 -2
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +229 -3
- 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/session.test.ts +79 -43
- package/src/adapters/AgentAdapter.ts +76 -0
- package/src/adapters/ClaudeCodeAdapter.ts +136 -6
- 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 +1 -4
- package/src/terminal/TtyWriter.ts +1 -11
- package/src/utils/ClaudeSessionParser.ts +59 -5
- package/src/utils/applescript.ts +10 -0
- package/src/utils/session.ts +86 -45
package/dist/utils/session.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Session File Utilities
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* Uses
|
|
5
|
+
* Utilities for discovering session files and their birth times.
|
|
6
|
+
* Uses Node.js fs APIs to get birth timestamps without reading file contents.
|
|
7
7
|
*/
|
|
8
8
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
9
|
if (k2 === undefined) k2 = k;
|
|
@@ -39,62 +39,108 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
39
39
|
};
|
|
40
40
|
})();
|
|
41
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.isDirectory = isDirectory;
|
|
43
|
+
exports.safeStat = safeStat;
|
|
44
|
+
exports.safeReadFile = safeReadFile;
|
|
45
|
+
exports.safeReaddir = safeReaddir;
|
|
46
|
+
exports.listJsonl = listJsonl;
|
|
42
47
|
exports.batchGetSessionFileBirthtimes = batchGetSessionFileBirthtimes;
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
43
49
|
const path = __importStar(require("path"));
|
|
44
|
-
const child_process_1 = require("child_process");
|
|
45
50
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* Combines all directory globs into one `stat` command to avoid per-directory exec overhead.
|
|
49
|
-
* Returns empty array if no directories have .jsonl files or command fails.
|
|
50
|
-
* resolvedCwd is left empty — the adapter must set it.
|
|
51
|
+
* Check whether a path exists and is a directory.
|
|
52
|
+
* Returns false on any error (missing path, permission denied, broken symlink, etc.).
|
|
51
53
|
*/
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
function isDirectory(p) {
|
|
55
|
+
return safeStat(p)?.isDirectory() ?? false;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* `fs.statSync` that swallows errors and returns `undefined` on failure.
|
|
59
|
+
* Callers can pull whichever fields they need (mtime, birthtime, ...).
|
|
60
|
+
*/
|
|
61
|
+
function safeStat(filePath) {
|
|
55
62
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
return fs.statSync(filePath);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* `fs.readFileSync` (utf-8) that swallows errors and returns `undefined`
|
|
71
|
+
* on failure. Use when an unreadable file should be skipped rather than
|
|
72
|
+
* raised.
|
|
73
|
+
*/
|
|
74
|
+
function safeReadFile(filePath) {
|
|
75
|
+
try {
|
|
76
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* `fs.readdirSync` that swallows errors and returns `[]` on failure.
|
|
84
|
+
* Useful when walking optional/transient directories where missing or
|
|
85
|
+
* unreadable entries should be skipped silently.
|
|
86
|
+
*/
|
|
87
|
+
function safeReaddir(dir) {
|
|
88
|
+
try {
|
|
89
|
+
return fs.readdirSync(dir);
|
|
64
90
|
}
|
|
65
91
|
catch {
|
|
66
92
|
return [];
|
|
67
93
|
}
|
|
68
94
|
}
|
|
69
95
|
/**
|
|
70
|
-
*
|
|
96
|
+
* List entries in a directory that end with `.jsonl`. Returns `[]` on
|
|
97
|
+
* read errors. The result preserves directory order (no sorting).
|
|
98
|
+
*/
|
|
99
|
+
function listJsonl(dir) {
|
|
100
|
+
return safeReaddir(dir).filter((name) => name.endsWith('.jsonl'));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Get birth times for .jsonl session files across multiple directories.
|
|
104
|
+
*
|
|
105
|
+
* Enumerates each directory with readdirSync and stats each .jsonl file
|
|
106
|
+
* to get its birth time. No shell commands are used.
|
|
107
|
+
* Returns empty array if no directories have .jsonl files or reads fail.
|
|
108
|
+
* resolvedCwd is left empty — the adapter must set it.
|
|
71
109
|
*/
|
|
72
|
-
function
|
|
110
|
+
function batchGetSessionFileBirthtimes(dirs) {
|
|
111
|
+
if (dirs.length === 0)
|
|
112
|
+
return [];
|
|
73
113
|
const results = [];
|
|
74
|
-
for (const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (spaceIdx === -1)
|
|
81
|
-
continue;
|
|
82
|
-
const epochStr = line.slice(0, spaceIdx);
|
|
83
|
-
const filePath = line.slice(spaceIdx + 1).trim();
|
|
84
|
-
const epochSeconds = parseInt(epochStr, 10);
|
|
85
|
-
if (!Number.isFinite(epochSeconds) || epochSeconds <= 0)
|
|
86
|
-
continue;
|
|
87
|
-
const fileName = path.basename(filePath);
|
|
88
|
-
if (!fileName.endsWith('.jsonl'))
|
|
114
|
+
for (const dir of dirs) {
|
|
115
|
+
let entries;
|
|
116
|
+
try {
|
|
117
|
+
entries = fs.readdirSync(dir);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
89
120
|
continue;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
birthtimeMs
|
|
96
|
-
|
|
97
|
-
|
|
121
|
+
}
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
if (!entry.endsWith('.jsonl'))
|
|
124
|
+
continue;
|
|
125
|
+
const filePath = path.join(dir, entry);
|
|
126
|
+
let birthtimeMs;
|
|
127
|
+
try {
|
|
128
|
+
birthtimeMs = fs.statSync(filePath).birthtimeMs;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (!Number.isFinite(birthtimeMs) || birthtimeMs <= 0)
|
|
134
|
+
continue;
|
|
135
|
+
const sessionId = entry.replace(/\.jsonl$/, '');
|
|
136
|
+
results.push({
|
|
137
|
+
sessionId,
|
|
138
|
+
filePath,
|
|
139
|
+
projectDir: dir,
|
|
140
|
+
birthtimeMs,
|
|
141
|
+
resolvedCwd: '',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
98
144
|
}
|
|
99
145
|
return results;
|
|
100
146
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/utils/session.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/utils/session.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BH,kCAEC;AAMD,4BAMC;AAOD,oCAMC;AAOD,kCAMC;AAMD,8BAEC;AAUD,sEAwCC;AA7HD,uCAAyB;AACzB,2CAA6B;AAsB7B;;;GAGG;AACH,SAAgB,WAAW,CAAC,CAAS;IACjC,OAAO,QAAQ,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,KAAK,CAAC;AAC/C,CAAC;AAED;;;GAGG;AACH,SAAgB,QAAQ,CAAC,QAAgB;IACrC,IAAI,CAAC;QACD,OAAO,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,SAAS,CAAC;IACrB,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,SAAgB,YAAY,CAAC,QAAgB;IACzC,IAAI,CAAC;QACD,OAAO,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,SAAS,CAAC;IACrB,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,SAAgB,WAAW,CAAC,GAAW;IACnC,IAAI,CAAC;QACD,OAAO,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,EAAE,CAAC;IACd,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,SAAgB,SAAS,CAAC,GAAW;IACjC,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;AACtE,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,6BAA6B,CAAC,IAAc;IACxD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEjC,MAAM,OAAO,GAAkB,EAAE,CAAC;IAElC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,OAAiB,CAAC;QACtB,IAAI,CAAC;YACD,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QAClC,CAAC;QAAC,MAAM,CAAC;YACL,SAAS;QACb,CAAC;QAED,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC1B,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAExC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YAEvC,IAAI,WAAmB,CAAC;YACxB,IAAI,CAAC;gBACD,WAAW,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,WAAW,CAAC;YACpD,CAAC;YAAC,MAAM,CAAC;gBACL,SAAS;YACb,CAAC;YAED,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,WAAW,IAAI,CAAC;gBAAE,SAAS;YAEhE,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;YAEhD,OAAO,CAAC,IAAI,CAAC;gBACT,SAAS;gBACT,QAAQ;gBACR,UAAU,EAAE,GAAG;gBACf,WAAW;gBACX,WAAW,EAAE,EAAE;aAClB,CAAC,CAAC;QACP,CAAC;IACL,CAAC;IAED,OAAO,OAAO,CAAC;AACnB,CAAC"}
|
package/package.json
CHANGED
package/src/AgentManager.ts
CHANGED
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
* Manages adapter registration and aggregates results from all adapters.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
AgentAdapter,
|
|
10
|
+
AgentInfo,
|
|
11
|
+
SessionSummary,
|
|
12
|
+
ListSessionsOptions,
|
|
13
|
+
} from './adapters/AgentAdapter';
|
|
9
14
|
import { AgentStatus } from './adapters/AgentAdapter';
|
|
10
15
|
|
|
11
16
|
/**
|
|
@@ -141,12 +146,59 @@ export class AgentManager {
|
|
|
141
146
|
return this.sortAgentsByStatus(allAgents);
|
|
142
147
|
}
|
|
143
148
|
|
|
149
|
+
/**
|
|
150
|
+
* List historical sessions across every registered adapter.
|
|
151
|
+
*
|
|
152
|
+
* When `opts.type` is set, adapters whose `type` doesn't match are
|
|
153
|
+
* skipped without being called. The remaining adapters' results are
|
|
154
|
+
* merged and sorted by `lastActive` descending. Adapter failures are
|
|
155
|
+
* caught (one-line stderr warning) so one broken adapter doesn't hide
|
|
156
|
+
* the others.
|
|
157
|
+
*
|
|
158
|
+
* @param opts Filter options computed by the CLI; the manager passes
|
|
159
|
+
* them through to each adapter unchanged.
|
|
160
|
+
*/
|
|
161
|
+
async listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]> {
|
|
162
|
+
const targetAdapters = Array.from(this.adapters.values()).filter(
|
|
163
|
+
(adapter) => opts?.type === undefined || adapter.type === opts.type,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const errors: Array<{ type: string; error: Error }> = [];
|
|
167
|
+
|
|
168
|
+
const results = await Promise.all(
|
|
169
|
+
targetAdapters.map(async (adapter) => {
|
|
170
|
+
try {
|
|
171
|
+
return await adapter.listSessions(opts);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
174
|
+
errors.push({ type: adapter.type, error: err });
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
if (errors.length > 0) {
|
|
181
|
+
console.error(`Warning: ${errors.length} adapter(s) failed to list sessions:`);
|
|
182
|
+
for (const { type, error } of errors) {
|
|
183
|
+
console.error(` - ${type}: ${error.message}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const merged: SessionSummary[] = [];
|
|
188
|
+
for (const list of results) {
|
|
189
|
+
merged.push(...list);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
merged.sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime());
|
|
193
|
+
return merged;
|
|
194
|
+
}
|
|
195
|
+
|
|
144
196
|
/**
|
|
145
197
|
* Sort agents by status priority
|
|
146
|
-
*
|
|
198
|
+
*
|
|
147
199
|
* Priority order: waiting > running > idle > unknown
|
|
148
200
|
* This ensures agents that need attention appear first.
|
|
149
|
-
*
|
|
201
|
+
*
|
|
150
202
|
* @param agents Array of agents to sort
|
|
151
203
|
* @returns Sorted array of agents
|
|
152
204
|
*/
|
|
@@ -4,15 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
import { describe, it, expect, beforeEach } from '@jest/globals';
|
|
6
6
|
import { AgentManager } from '../AgentManager';
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
AgentAdapter,
|
|
9
|
+
AgentInfo,
|
|
10
|
+
AgentType,
|
|
11
|
+
ConversationMessage,
|
|
12
|
+
SessionSummary,
|
|
13
|
+
} from '../adapters/AgentAdapter';
|
|
8
14
|
import { AgentStatus } from '../adapters/AgentAdapter';
|
|
9
15
|
|
|
10
16
|
// Mock adapter for testing
|
|
11
17
|
class MockAdapter implements AgentAdapter {
|
|
18
|
+
public lastListSessionsOpts: unknown = undefined;
|
|
19
|
+
|
|
12
20
|
constructor(
|
|
13
21
|
public readonly type: AgentType,
|
|
14
22
|
private mockAgents: AgentInfo[] = [],
|
|
15
|
-
private shouldFail: boolean = false
|
|
23
|
+
private shouldFail: boolean = false,
|
|
24
|
+
private mockSessions: SessionSummary[] = [],
|
|
25
|
+
private shouldFailListSessions: boolean = false,
|
|
16
26
|
) { }
|
|
17
27
|
|
|
18
28
|
async detectAgents(): Promise<AgentInfo[]> {
|
|
@@ -30,6 +40,14 @@ class MockAdapter implements AgentAdapter {
|
|
|
30
40
|
return [];
|
|
31
41
|
}
|
|
32
42
|
|
|
43
|
+
async listSessions(opts?: unknown): Promise<SessionSummary[]> {
|
|
44
|
+
this.lastListSessionsOpts = opts;
|
|
45
|
+
if (this.shouldFailListSessions) {
|
|
46
|
+
throw new Error(`Mock adapter ${this.type} listSessions failed`);
|
|
47
|
+
}
|
|
48
|
+
return this.mockSessions;
|
|
49
|
+
}
|
|
50
|
+
|
|
33
51
|
setAgents(agents: AgentInfo[]): void {
|
|
34
52
|
this.mockAgents = agents;
|
|
35
53
|
}
|
|
@@ -37,6 +55,14 @@ class MockAdapter implements AgentAdapter {
|
|
|
37
55
|
setFail(shouldFail: boolean): void {
|
|
38
56
|
this.shouldFail = shouldFail;
|
|
39
57
|
}
|
|
58
|
+
|
|
59
|
+
setSessions(sessions: SessionSummary[]): void {
|
|
60
|
+
this.mockSessions = sessions;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setFailListSessions(shouldFail: boolean): void {
|
|
64
|
+
this.shouldFailListSessions = shouldFail;
|
|
65
|
+
}
|
|
40
66
|
}
|
|
41
67
|
|
|
42
68
|
// Helper to create mock agent
|
|
@@ -226,6 +252,112 @@ describe('AgentManager', () => {
|
|
|
226
252
|
});
|
|
227
253
|
});
|
|
228
254
|
|
|
255
|
+
describe('listSessions', () => {
|
|
256
|
+
function createMockSession(overrides: Partial<SessionSummary> = {}): SessionSummary {
|
|
257
|
+
return {
|
|
258
|
+
type: 'claude',
|
|
259
|
+
sessionId: 'session-1',
|
|
260
|
+
cwd: '/repo',
|
|
261
|
+
firstUserMessage: 'hello',
|
|
262
|
+
lastActive: new Date('2025-01-01T00:00:00Z'),
|
|
263
|
+
startedAt: new Date('2025-01-01T00:00:00Z'),
|
|
264
|
+
sessionFilePath: '/tmp/session-1.jsonl',
|
|
265
|
+
...overrides,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
it('returns empty array when no adapters are registered', async () => {
|
|
270
|
+
const result = await manager.listSessions();
|
|
271
|
+
expect(result).toEqual([]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('merges sessions from every registered adapter', async () => {
|
|
275
|
+
const claudeSession = createMockSession({ type: 'claude', sessionId: 'c1' });
|
|
276
|
+
const codexSession = createMockSession({ type: 'codex', sessionId: 'cx1' });
|
|
277
|
+
manager.registerAdapter(new MockAdapter('claude', [], false, [claudeSession]));
|
|
278
|
+
manager.registerAdapter(new MockAdapter('codex', [], false, [codexSession]));
|
|
279
|
+
|
|
280
|
+
const result = await manager.listSessions();
|
|
281
|
+
|
|
282
|
+
expect(result).toHaveLength(2);
|
|
283
|
+
expect(result.map((s) => s.sessionId).sort()).toEqual(['c1', 'cx1']);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('sorts merged sessions by lastActive descending', async () => {
|
|
287
|
+
const older = createMockSession({
|
|
288
|
+
sessionId: 'older',
|
|
289
|
+
lastActive: new Date('2025-01-01T00:00:00Z'),
|
|
290
|
+
});
|
|
291
|
+
const newer = createMockSession({
|
|
292
|
+
type: 'codex',
|
|
293
|
+
sessionId: 'newer',
|
|
294
|
+
lastActive: new Date('2025-06-01T00:00:00Z'),
|
|
295
|
+
});
|
|
296
|
+
manager.registerAdapter(new MockAdapter('claude', [], false, [older]));
|
|
297
|
+
manager.registerAdapter(new MockAdapter('codex', [], false, [newer]));
|
|
298
|
+
|
|
299
|
+
const result = await manager.listSessions();
|
|
300
|
+
|
|
301
|
+
expect(result.map((s) => s.sessionId)).toEqual(['newer', 'older']);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('skips adapters whose type does not match opts.type', async () => {
|
|
305
|
+
const claudeAdapter = new MockAdapter(
|
|
306
|
+
'claude',
|
|
307
|
+
[],
|
|
308
|
+
false,
|
|
309
|
+
[createMockSession({ type: 'claude', sessionId: 'c1' })],
|
|
310
|
+
);
|
|
311
|
+
const codexAdapter = new MockAdapter(
|
|
312
|
+
'codex',
|
|
313
|
+
[],
|
|
314
|
+
false,
|
|
315
|
+
[createMockSession({ type: 'codex', sessionId: 'cx1' })],
|
|
316
|
+
);
|
|
317
|
+
manager.registerAdapter(claudeAdapter);
|
|
318
|
+
manager.registerAdapter(codexAdapter);
|
|
319
|
+
|
|
320
|
+
const result = await manager.listSessions({ type: 'claude' });
|
|
321
|
+
|
|
322
|
+
expect(result).toHaveLength(1);
|
|
323
|
+
expect(result[0].sessionId).toBe('c1');
|
|
324
|
+
// Codex adapter must not have been called
|
|
325
|
+
expect(codexAdapter.lastListSessionsOpts).toBeUndefined();
|
|
326
|
+
expect(claudeAdapter.lastListSessionsOpts).toEqual({ type: 'claude' });
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('tolerates an adapter that throws and still returns the others', async () => {
|
|
330
|
+
const goodSession = createMockSession({ sessionId: 'good' });
|
|
331
|
+
manager.registerAdapter(new MockAdapter('claude', [], false, [goodSession]));
|
|
332
|
+
manager.registerAdapter(
|
|
333
|
+
new MockAdapter('codex', [], false, [], true /* failListSessions */),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
const result = await manager.listSessions();
|
|
340
|
+
expect(result).toHaveLength(1);
|
|
341
|
+
expect(result[0].sessionId).toBe('good');
|
|
342
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
343
|
+
} finally {
|
|
344
|
+
consoleErrorSpy.mockRestore();
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('passes the same opts to every called adapter', async () => {
|
|
349
|
+
const a = new MockAdapter('claude', [], false, []);
|
|
350
|
+
const b = new MockAdapter('codex', [], false, []);
|
|
351
|
+
manager.registerAdapter(a);
|
|
352
|
+
manager.registerAdapter(b);
|
|
353
|
+
|
|
354
|
+
await manager.listSessions({ cwd: '/Users/test/proj' });
|
|
355
|
+
|
|
356
|
+
expect(a.lastListSessionsOpts).toEqual({ cwd: '/Users/test/proj' });
|
|
357
|
+
expect(b.lastListSessionsOpts).toEqual({ cwd: '/Users/test/proj' });
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
229
361
|
describe('resolveAgent', () => {
|
|
230
362
|
it('should return null for empty input or empty agents list', () => {
|
|
231
363
|
const agent = createMockAgent({ name: 'test-agent' });
|