claude-remote-cli 3.0.3 → 3.0.5
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/bin/claude-remote-cli.js +0 -3
- package/dist/frontend/assets/index-BgPZneAz.js +47 -0
- package/dist/frontend/assets/index-C0iHLowo.css +32 -0
- package/dist/frontend/index.html +2 -2
- package/dist/server/config.js +22 -0
- package/dist/server/git.js +193 -1
- package/dist/server/index.js +51 -292
- package/dist/server/push.js +3 -54
- package/dist/server/sessions.js +265 -180
- package/dist/server/types.js +7 -13
- package/dist/server/workspaces.js +448 -0
- package/dist/server/ws.js +31 -92
- package/dist/test/pr-state.test.js +164 -0
- package/dist/test/pull-requests.test.js +3 -3
- package/dist/test/sessions.test.js +7 -23
- package/package.json +1 -2
- package/dist/frontend/assets/index-BgOmCV-k.css +0 -32
- package/dist/frontend/assets/index-CKQHbnTN.js +0 -47
- package/dist/server/pty-handler.js +0 -214
- package/dist/server/sdk-handler.js +0 -536
|
@@ -1,536 +0,0 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { query as sdkQuery } from '@anthropic-ai/claude-agent-sdk';
|
|
6
|
-
const MAX_EVENTS = 2000;
|
|
7
|
-
const IDLE_TIMEOUT_MS = 5000;
|
|
8
|
-
// Controller for streaming user messages into an SDK query
|
|
9
|
-
class SdkInputController {
|
|
10
|
-
resolveNext = null;
|
|
11
|
-
queue = [];
|
|
12
|
-
done = false;
|
|
13
|
-
push(msg) {
|
|
14
|
-
if (this.done)
|
|
15
|
-
return;
|
|
16
|
-
if (this.resolveNext) {
|
|
17
|
-
const resolve = this.resolveNext;
|
|
18
|
-
this.resolveNext = null;
|
|
19
|
-
resolve({ value: msg, done: false });
|
|
20
|
-
}
|
|
21
|
-
else {
|
|
22
|
-
this.queue.push(msg);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
close() {
|
|
26
|
-
this.done = true;
|
|
27
|
-
if (this.resolveNext) {
|
|
28
|
-
const resolve = this.resolveNext;
|
|
29
|
-
this.resolveNext = null;
|
|
30
|
-
resolve({ value: undefined, done: true });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
[Symbol.asyncIterator]() {
|
|
34
|
-
return {
|
|
35
|
-
next: () => {
|
|
36
|
-
if (this.queue.length > 0) {
|
|
37
|
-
return Promise.resolve({ value: this.queue.shift(), done: false });
|
|
38
|
-
}
|
|
39
|
-
if (this.done) {
|
|
40
|
-
return Promise.resolve({ value: undefined, done: true });
|
|
41
|
-
}
|
|
42
|
-
return new Promise((resolve) => {
|
|
43
|
-
this.resolveNext = resolve;
|
|
44
|
-
});
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
const runtimeStates = new Map();
|
|
50
|
-
// Debug log state
|
|
51
|
-
let debugLogEnabled = false;
|
|
52
|
-
const DEBUG_DIR = path.join(os.homedir(), '.config', 'claude-remote-cli', 'debug');
|
|
53
|
-
const MAX_DEBUG_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
54
|
-
const DEBUG_FILE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
55
|
-
export function enableDebugLog(enabled) {
|
|
56
|
-
debugLogEnabled = enabled;
|
|
57
|
-
if (enabled) {
|
|
58
|
-
fs.mkdirSync(DEBUG_DIR, { recursive: true });
|
|
59
|
-
cleanupOldDebugFiles();
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
function cleanupOldDebugFiles() {
|
|
63
|
-
try {
|
|
64
|
-
const files = fs.readdirSync(DEBUG_DIR);
|
|
65
|
-
const now = Date.now();
|
|
66
|
-
for (const file of files) {
|
|
67
|
-
if (!file.endsWith('.jsonl'))
|
|
68
|
-
continue;
|
|
69
|
-
const filePath = path.join(DEBUG_DIR, file);
|
|
70
|
-
try {
|
|
71
|
-
const stat = fs.statSync(filePath);
|
|
72
|
-
if (now - stat.mtimeMs > DEBUG_FILE_MAX_AGE_MS) {
|
|
73
|
-
fs.unlinkSync(filePath);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
// ignore individual file errors
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
// ignore if directory doesn't exist yet
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
// Async write queue to avoid blocking the event loop on debug log writes
|
|
86
|
-
const debugWriteQueue = new Map();
|
|
87
|
-
let debugFlushPending = false;
|
|
88
|
-
function flushDebugWrites() {
|
|
89
|
-
if (debugFlushPending)
|
|
90
|
-
return;
|
|
91
|
-
debugFlushPending = true;
|
|
92
|
-
queueMicrotask(() => {
|
|
93
|
-
debugFlushPending = false;
|
|
94
|
-
for (const [filePath, lines] of debugWriteQueue) {
|
|
95
|
-
const data = lines.join('');
|
|
96
|
-
debugWriteQueue.delete(filePath);
|
|
97
|
-
fs.appendFile(filePath, data, 'utf-8', () => { });
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
function debugLogEvent(sessionId, event) {
|
|
102
|
-
if (!debugLogEnabled)
|
|
103
|
-
return;
|
|
104
|
-
try {
|
|
105
|
-
const filePath = path.join(DEBUG_DIR, `${sessionId}.jsonl`);
|
|
106
|
-
const line = JSON.stringify({ ...event, _logged: new Date().toISOString() }) + '\n';
|
|
107
|
-
// Async rotation check (best-effort, runs periodically)
|
|
108
|
-
fs.stat(filePath, (err, stat) => {
|
|
109
|
-
if (!err && stat.size > MAX_DEBUG_FILE_SIZE) {
|
|
110
|
-
const rotatedPath = filePath + '.1';
|
|
111
|
-
fs.unlink(rotatedPath, () => {
|
|
112
|
-
fs.rename(filePath, rotatedPath, () => { });
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
// Queue the write
|
|
117
|
-
const existing = debugWriteQueue.get(filePath);
|
|
118
|
-
if (existing) {
|
|
119
|
-
existing.push(line);
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
debugWriteQueue.set(filePath, [line]);
|
|
123
|
-
}
|
|
124
|
-
flushDebugWrites();
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
-
// debug logging should never crash the server
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
const FILE_EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
|
|
131
|
-
function extractAssistantEvents(msg, timestamp) {
|
|
132
|
-
const events = [];
|
|
133
|
-
const content = msg.message.content;
|
|
134
|
-
const textParts = [];
|
|
135
|
-
const thinkingParts = [];
|
|
136
|
-
for (const block of content) {
|
|
137
|
-
if (block.type === 'text' && block.text) {
|
|
138
|
-
textParts.push(block.text);
|
|
139
|
-
}
|
|
140
|
-
else if (block.type === 'thinking' && block.thinking) {
|
|
141
|
-
thinkingParts.push(block.thinking);
|
|
142
|
-
}
|
|
143
|
-
else if (block.type === 'tool_use' && block.name && block.id) {
|
|
144
|
-
const input = (block.input || {});
|
|
145
|
-
const isFileEdit = FILE_EDIT_TOOLS.has(block.name);
|
|
146
|
-
const filePath = input.file_path || input.path;
|
|
147
|
-
if (isFileEdit) {
|
|
148
|
-
const evt = {
|
|
149
|
-
type: 'file_change',
|
|
150
|
-
id: block.id,
|
|
151
|
-
toolName: block.name,
|
|
152
|
-
toolInput: input,
|
|
153
|
-
timestamp,
|
|
154
|
-
};
|
|
155
|
-
if (filePath)
|
|
156
|
-
evt.path = filePath;
|
|
157
|
-
events.push(evt);
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
events.push({
|
|
161
|
-
type: 'tool_call',
|
|
162
|
-
id: block.id,
|
|
163
|
-
toolName: block.name,
|
|
164
|
-
toolInput: input,
|
|
165
|
-
timestamp,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
if (thinkingParts.length > 0) {
|
|
171
|
-
// Prepend thinking before text/tool events
|
|
172
|
-
events.unshift({
|
|
173
|
-
type: 'reasoning',
|
|
174
|
-
text: thinkingParts.join('\n'),
|
|
175
|
-
timestamp,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
if (textParts.length > 0) {
|
|
179
|
-
// Insert text event after thinking but before tool events
|
|
180
|
-
const insertIdx = thinkingParts.length > 0 ? 1 : 0;
|
|
181
|
-
events.splice(insertIdx, 0, {
|
|
182
|
-
type: 'agent_message',
|
|
183
|
-
id: msg.uuid,
|
|
184
|
-
text: textParts.join(''),
|
|
185
|
-
timestamp,
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
return events;
|
|
189
|
-
}
|
|
190
|
-
function mapSdkMessageAll(msg) {
|
|
191
|
-
const timestamp = new Date().toISOString();
|
|
192
|
-
const events = [];
|
|
193
|
-
if (msg.type === 'system' && msg.subtype === 'init') {
|
|
194
|
-
events.push({
|
|
195
|
-
type: 'session_started',
|
|
196
|
-
id: msg.session_id,
|
|
197
|
-
timestamp,
|
|
198
|
-
});
|
|
199
|
-
return events;
|
|
200
|
-
}
|
|
201
|
-
if (msg.type === 'assistant') {
|
|
202
|
-
return extractAssistantEvents(msg, timestamp);
|
|
203
|
-
}
|
|
204
|
-
if (msg.type === 'result') {
|
|
205
|
-
if (msg.subtype === 'success') {
|
|
206
|
-
events.push({
|
|
207
|
-
type: 'turn_completed',
|
|
208
|
-
usage: {
|
|
209
|
-
input_tokens: msg.usage.input_tokens,
|
|
210
|
-
output_tokens: msg.usage.output_tokens,
|
|
211
|
-
},
|
|
212
|
-
timestamp,
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
else {
|
|
216
|
-
events.push({
|
|
217
|
-
type: 'error',
|
|
218
|
-
text: msg.errors.join('; '),
|
|
219
|
-
timestamp,
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
return events;
|
|
223
|
-
}
|
|
224
|
-
return events;
|
|
225
|
-
}
|
|
226
|
-
function addEvent(session, event, state) {
|
|
227
|
-
session.events.push(event);
|
|
228
|
-
// FIFO cap
|
|
229
|
-
while (session.events.length > MAX_EVENTS) {
|
|
230
|
-
session.events.shift();
|
|
231
|
-
}
|
|
232
|
-
// Notify listeners
|
|
233
|
-
for (const listener of state.eventListeners) {
|
|
234
|
-
try {
|
|
235
|
-
listener(event);
|
|
236
|
-
}
|
|
237
|
-
catch {
|
|
238
|
-
// listeners should not crash the handler
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
// Debug log
|
|
242
|
-
debugLogEvent(session.id, event);
|
|
243
|
-
}
|
|
244
|
-
function resetIdleTimer(session, state, idleChangeCallbacks) {
|
|
245
|
-
if (session.idle) {
|
|
246
|
-
session.idle = false;
|
|
247
|
-
for (const cb of idleChangeCallbacks)
|
|
248
|
-
cb(session.id, false);
|
|
249
|
-
}
|
|
250
|
-
if (state.idleTimer)
|
|
251
|
-
clearTimeout(state.idleTimer);
|
|
252
|
-
state.idleTimer = setTimeout(() => {
|
|
253
|
-
if (!session.idle) {
|
|
254
|
-
session.idle = true;
|
|
255
|
-
for (const cb of idleChangeCallbacks)
|
|
256
|
-
cb(session.id, true);
|
|
257
|
-
}
|
|
258
|
-
}, IDLE_TIMEOUT_MS);
|
|
259
|
-
}
|
|
260
|
-
export function createSdkSession(params, sessionsMap, idleChangeCallbacks) {
|
|
261
|
-
const { id, type, agent = 'claude', repoName, repoPath, cwd, root, worktreeName, branchName, displayName, prompt, } = params;
|
|
262
|
-
const createdAt = new Date().toISOString();
|
|
263
|
-
const resolvedCwd = cwd || repoPath;
|
|
264
|
-
// Strip sensitive env vars from child process
|
|
265
|
-
const env = Object.assign({}, process.env);
|
|
266
|
-
delete env.CLAUDECODE;
|
|
267
|
-
// Strip server-internal env vars
|
|
268
|
-
for (const key of Object.keys(env)) {
|
|
269
|
-
if (key.startsWith('VAPID_') || key === 'PIN_HASH') {
|
|
270
|
-
delete env[key];
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
const abortController = new AbortController();
|
|
274
|
-
const inputController = new SdkInputController();
|
|
275
|
-
// Permission queue for canUseTool callbacks
|
|
276
|
-
const permissionQueue = new Map();
|
|
277
|
-
const session = {
|
|
278
|
-
id,
|
|
279
|
-
type: type || 'worktree',
|
|
280
|
-
agent,
|
|
281
|
-
mode: 'sdk',
|
|
282
|
-
root: root || '',
|
|
283
|
-
repoName: repoName || '',
|
|
284
|
-
repoPath,
|
|
285
|
-
worktreeName: worktreeName || '',
|
|
286
|
-
branchName: branchName || worktreeName || '',
|
|
287
|
-
displayName: displayName || worktreeName || repoName || '',
|
|
288
|
-
createdAt,
|
|
289
|
-
lastActivity: createdAt,
|
|
290
|
-
idle: false,
|
|
291
|
-
cwd: resolvedCwd,
|
|
292
|
-
customCommand: null,
|
|
293
|
-
status: 'active',
|
|
294
|
-
events: [],
|
|
295
|
-
sdkSessionId: null,
|
|
296
|
-
tokenUsage: { input: 0, output: 0 },
|
|
297
|
-
estimatedCost: 0,
|
|
298
|
-
};
|
|
299
|
-
const state = {
|
|
300
|
-
query: null,
|
|
301
|
-
abortController,
|
|
302
|
-
permissionQueue,
|
|
303
|
-
eventListeners: [],
|
|
304
|
-
inputController,
|
|
305
|
-
idleTimer: null,
|
|
306
|
-
};
|
|
307
|
-
// Try to create the SDK query
|
|
308
|
-
let q;
|
|
309
|
-
try {
|
|
310
|
-
q = sdkQuery({
|
|
311
|
-
prompt: inputController,
|
|
312
|
-
options: {
|
|
313
|
-
abortController,
|
|
314
|
-
cwd: resolvedCwd,
|
|
315
|
-
env,
|
|
316
|
-
canUseTool: async (toolName, input, options) => {
|
|
317
|
-
const requestId = options.toolUseID || crypto.randomBytes(8).toString('hex');
|
|
318
|
-
// Emit a tool_call event for the permission request
|
|
319
|
-
const permEvent = {
|
|
320
|
-
type: 'tool_call',
|
|
321
|
-
id: requestId,
|
|
322
|
-
toolName,
|
|
323
|
-
toolInput: input,
|
|
324
|
-
status: 'pending',
|
|
325
|
-
text: options.title || `Claude wants to use ${toolName}`,
|
|
326
|
-
timestamp: new Date().toISOString(),
|
|
327
|
-
};
|
|
328
|
-
addEvent(session, permEvent, state);
|
|
329
|
-
return new Promise((resolve, reject) => {
|
|
330
|
-
permissionQueue.set(requestId, { resolve, reject });
|
|
331
|
-
// Clean up on abort
|
|
332
|
-
options.signal.addEventListener('abort', () => {
|
|
333
|
-
permissionQueue.delete(requestId);
|
|
334
|
-
reject(new Error('Permission request aborted'));
|
|
335
|
-
}, { once: true });
|
|
336
|
-
});
|
|
337
|
-
},
|
|
338
|
-
},
|
|
339
|
-
});
|
|
340
|
-
state.query = q;
|
|
341
|
-
}
|
|
342
|
-
catch (err) {
|
|
343
|
-
console.warn('SDK init failed, falling back to PTY:', err instanceof Error ? err.message : String(err));
|
|
344
|
-
return { fallback: true };
|
|
345
|
-
}
|
|
346
|
-
sessionsMap.set(id, session);
|
|
347
|
-
runtimeStates.set(id, state);
|
|
348
|
-
// Send initial prompt if provided
|
|
349
|
-
if (prompt) {
|
|
350
|
-
inputController.push({
|
|
351
|
-
type: 'user',
|
|
352
|
-
message: { role: 'user', content: prompt },
|
|
353
|
-
parent_tool_use_id: null,
|
|
354
|
-
session_id: id,
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
// Start consuming the event stream in the background
|
|
358
|
-
void (async () => {
|
|
359
|
-
try {
|
|
360
|
-
for await (const msg of q) {
|
|
361
|
-
session.lastActivity = new Date().toISOString();
|
|
362
|
-
resetIdleTimer(session, state, idleChangeCallbacks);
|
|
363
|
-
const events = mapSdkMessageAll(msg);
|
|
364
|
-
for (const event of events) {
|
|
365
|
-
addEvent(session, event, state);
|
|
366
|
-
// Track token usage
|
|
367
|
-
if (event.type === 'turn_completed' && event.usage) {
|
|
368
|
-
session.tokenUsage.input += event.usage.input_tokens;
|
|
369
|
-
session.tokenUsage.output += event.usage.output_tokens;
|
|
370
|
-
}
|
|
371
|
-
// Track session ID
|
|
372
|
-
if (event.type === 'session_started' && event.id) {
|
|
373
|
-
session.sdkSessionId = event.id;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
catch (err) {
|
|
379
|
-
const errorEvent = {
|
|
380
|
-
type: 'error',
|
|
381
|
-
text: err instanceof Error ? err.message : 'SDK stream error',
|
|
382
|
-
timestamp: new Date().toISOString(),
|
|
383
|
-
};
|
|
384
|
-
addEvent(session, errorEvent, state);
|
|
385
|
-
}
|
|
386
|
-
})();
|
|
387
|
-
const result = {
|
|
388
|
-
id,
|
|
389
|
-
type: session.type,
|
|
390
|
-
agent: session.agent,
|
|
391
|
-
mode: 'sdk',
|
|
392
|
-
root: session.root,
|
|
393
|
-
repoName: session.repoName,
|
|
394
|
-
repoPath,
|
|
395
|
-
worktreeName: session.worktreeName,
|
|
396
|
-
branchName: session.branchName,
|
|
397
|
-
displayName: session.displayName,
|
|
398
|
-
createdAt,
|
|
399
|
-
lastActivity: createdAt,
|
|
400
|
-
idle: false,
|
|
401
|
-
cwd: resolvedCwd,
|
|
402
|
-
customCommand: null,
|
|
403
|
-
useTmux: false,
|
|
404
|
-
tmuxSessionName: '',
|
|
405
|
-
status: 'active',
|
|
406
|
-
};
|
|
407
|
-
return { session, result };
|
|
408
|
-
}
|
|
409
|
-
export function sendMessage(sessionId, text) {
|
|
410
|
-
const state = runtimeStates.get(sessionId);
|
|
411
|
-
if (!state || !state.inputController) {
|
|
412
|
-
throw new Error(`SDK session not found or not active: ${sessionId}`);
|
|
413
|
-
}
|
|
414
|
-
state.inputController.push({
|
|
415
|
-
type: 'user',
|
|
416
|
-
message: { role: 'user', content: text },
|
|
417
|
-
parent_tool_use_id: null,
|
|
418
|
-
session_id: sessionId,
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
export function handlePermission(sessionId, requestId, approved) {
|
|
422
|
-
const state = runtimeStates.get(sessionId);
|
|
423
|
-
if (!state) {
|
|
424
|
-
throw new Error(`SDK session not found: ${sessionId}`);
|
|
425
|
-
}
|
|
426
|
-
const pending = state.permissionQueue.get(requestId);
|
|
427
|
-
if (!pending) {
|
|
428
|
-
throw new Error(`No pending permission request: ${requestId}`);
|
|
429
|
-
}
|
|
430
|
-
state.permissionQueue.delete(requestId);
|
|
431
|
-
if (approved) {
|
|
432
|
-
pending.resolve({ behavior: 'allow' });
|
|
433
|
-
}
|
|
434
|
-
else {
|
|
435
|
-
pending.resolve({ behavior: 'deny', message: 'User denied permission' });
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
export function interruptSession(sessionId) {
|
|
439
|
-
const state = runtimeStates.get(sessionId);
|
|
440
|
-
if (!state) {
|
|
441
|
-
throw new Error(`SDK session not found: ${sessionId}`);
|
|
442
|
-
}
|
|
443
|
-
if (state.query) {
|
|
444
|
-
void state.query.interrupt().catch(() => {
|
|
445
|
-
// If interrupt fails, abort
|
|
446
|
-
state.abortController.abort();
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
export function killSdkSession(sessionId) {
|
|
451
|
-
const state = runtimeStates.get(sessionId);
|
|
452
|
-
if (!state)
|
|
453
|
-
return;
|
|
454
|
-
// Reject all pending permission requests
|
|
455
|
-
for (const [, pending] of state.permissionQueue) {
|
|
456
|
-
pending.reject(new Error('Session killed'));
|
|
457
|
-
}
|
|
458
|
-
state.permissionQueue.clear();
|
|
459
|
-
// Close input stream
|
|
460
|
-
if (state.inputController) {
|
|
461
|
-
state.inputController.close();
|
|
462
|
-
}
|
|
463
|
-
// Close the query
|
|
464
|
-
if (state.query) {
|
|
465
|
-
state.query.close();
|
|
466
|
-
}
|
|
467
|
-
// Abort
|
|
468
|
-
state.abortController.abort();
|
|
469
|
-
// Clear idle timer
|
|
470
|
-
if (state.idleTimer)
|
|
471
|
-
clearTimeout(state.idleTimer);
|
|
472
|
-
// Clean up runtime state
|
|
473
|
-
runtimeStates.delete(sessionId);
|
|
474
|
-
}
|
|
475
|
-
export function onSdkEvent(sessionId, callback) {
|
|
476
|
-
const state = runtimeStates.get(sessionId);
|
|
477
|
-
if (!state) {
|
|
478
|
-
return () => { };
|
|
479
|
-
}
|
|
480
|
-
state.eventListeners.push(callback);
|
|
481
|
-
return () => {
|
|
482
|
-
const idx = state.eventListeners.indexOf(callback);
|
|
483
|
-
if (idx !== -1)
|
|
484
|
-
state.eventListeners.splice(idx, 1);
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
export function hasSdkRuntime(sessionId) {
|
|
488
|
-
return runtimeStates.has(sessionId);
|
|
489
|
-
}
|
|
490
|
-
export function serializeSdkSession(session) {
|
|
491
|
-
return {
|
|
492
|
-
id: session.id,
|
|
493
|
-
type: session.type,
|
|
494
|
-
agent: session.agent,
|
|
495
|
-
mode: 'sdk',
|
|
496
|
-
root: session.root,
|
|
497
|
-
repoName: session.repoName,
|
|
498
|
-
repoPath: session.repoPath,
|
|
499
|
-
worktreeName: session.worktreeName,
|
|
500
|
-
branchName: session.branchName,
|
|
501
|
-
displayName: session.displayName,
|
|
502
|
-
createdAt: session.createdAt,
|
|
503
|
-
lastActivity: session.lastActivity,
|
|
504
|
-
cwd: session.cwd,
|
|
505
|
-
sdkSessionId: session.sdkSessionId,
|
|
506
|
-
tokenUsage: { ...session.tokenUsage },
|
|
507
|
-
estimatedCost: session.estimatedCost,
|
|
508
|
-
events: session.events.slice(-100), // Keep last 100 events for restore
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
export function restoreSdkSession(serialized, sessionsMap) {
|
|
512
|
-
const session = {
|
|
513
|
-
id: serialized.id,
|
|
514
|
-
type: serialized.type,
|
|
515
|
-
agent: serialized.agent,
|
|
516
|
-
mode: 'sdk',
|
|
517
|
-
root: serialized.root,
|
|
518
|
-
repoName: serialized.repoName,
|
|
519
|
-
repoPath: serialized.repoPath,
|
|
520
|
-
worktreeName: serialized.worktreeName,
|
|
521
|
-
branchName: serialized.branchName,
|
|
522
|
-
displayName: serialized.displayName,
|
|
523
|
-
createdAt: serialized.createdAt,
|
|
524
|
-
lastActivity: serialized.lastActivity,
|
|
525
|
-
idle: true,
|
|
526
|
-
cwd: serialized.cwd,
|
|
527
|
-
customCommand: null,
|
|
528
|
-
status: 'disconnected',
|
|
529
|
-
events: serialized.events || [],
|
|
530
|
-
sdkSessionId: serialized.sdkSessionId,
|
|
531
|
-
tokenUsage: serialized.tokenUsage || { input: 0, output: 0 },
|
|
532
|
-
estimatedCost: serialized.estimatedCost || 0,
|
|
533
|
-
};
|
|
534
|
-
sessionsMap.set(session.id, session);
|
|
535
|
-
return session;
|
|
536
|
-
}
|