glad-web 1.0.28 → 1.0.30
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/lib/claude/cli-usage.js +95 -0
- package/lib/claude/structured-session.js +223 -51
- package/lib/claude/transcript-repository.js +216 -0
- package/lib/codex/image-store.js +175 -0
- package/lib/codex/structured-session.js +18 -4
- package/lib/commands/web.js +52 -234
- package/lib/server/routes/providers.js +103 -0
- package/lib/server/routes/schedules.js +54 -0
- package/lib/server/routes/workspace.js +77 -0
- package/lib/session/session-manager.js +111 -340
- package/lib/web/claude.js +1074 -0
- package/lib/web/codex.js +475 -0
- package/lib/web/composer.js +230 -0
- package/lib/web/core.js +327 -0
- package/lib/web/git.js +533 -0
- package/lib/web/index.html +45 -3610
- package/lib/web/schedules.js +245 -0
- package/lib/web/session.js +351 -0
- package/lib/web/shell.js +56 -0
- package/lib/web/styles.css +377 -0
- package/lib/web/terminal-scroll.js +81 -0
- package/lib/web/timed-inputs.js +223 -0
- package/lib/workspace/service.js +3 -2
- package/package.json +10 -5
- package/scripts/check-syntax.js +26 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const { EventEmitter } = require('events');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
const os = require('os');
|
|
5
4
|
const { v4: uuidv4 } = require('uuid');
|
|
6
5
|
const PTYManager = require('./pty-manager');
|
|
7
6
|
const TextHistory = require('./text-history');
|
|
@@ -10,6 +9,8 @@ const CircularBuffer = require('./buffer');
|
|
|
10
9
|
const { getToolByKey } = require('../ai-tools/registry');
|
|
11
10
|
const ClaudeStructuredSession = require('../claude/structured-session');
|
|
12
11
|
const CodexStructuredSession = require('../codex/structured-session');
|
|
12
|
+
const ClaudeTranscriptRepository = require('../claude/transcript-repository');
|
|
13
|
+
const CodexImageStore = require('../codex/image-store');
|
|
13
14
|
|
|
14
15
|
function previewText(text, maxChars = 320) {
|
|
15
16
|
if (!text) return '';
|
|
@@ -21,32 +22,15 @@ function previewText(text, maxChars = 320) {
|
|
|
21
22
|
return normalized.length > maxChars ? normalized.slice(-maxChars) : normalized;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
function detectImageExtension(buffer) {
|
|
30
|
-
if (!Buffer.isBuffer(buffer) || buffer.length < 12) return null;
|
|
31
|
-
if (buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) return 'png';
|
|
32
|
-
if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) return 'jpg';
|
|
33
|
-
if (buffer.subarray(0, 6).equals(Buffer.from('GIF87a')) || buffer.subarray(0, 6).equals(Buffer.from('GIF89a'))) return 'gif';
|
|
34
|
-
if (buffer.subarray(0, 4).equals(Buffer.from('RIFF')) && buffer.subarray(8, 12).equals(Buffer.from('WEBP'))) return 'webp';
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function attachmentError(message, statusCode = 400) {
|
|
39
|
-
const error = new Error(message);
|
|
40
|
-
error.statusCode = statusCode;
|
|
41
|
-
return error;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function isSafeUploadId(value) {
|
|
45
|
-
return typeof value === 'string' && /^[a-zA-Z0-9-]{8,100}$/.test(value);
|
|
25
|
+
function imageMediaType(name) {
|
|
26
|
+
const extension = path.extname(String(name || '')).toLowerCase();
|
|
27
|
+
return extension === '.png' ? 'image/png'
|
|
28
|
+
: extension === '.jpg' || extension === '.jpeg' ? 'image/jpeg'
|
|
29
|
+
: extension === '.gif' ? 'image/gif' : 'image/webp';
|
|
46
30
|
}
|
|
47
31
|
|
|
48
32
|
class SessionManager extends EventEmitter {
|
|
49
|
-
constructor({ baseDir, renderHistoryTools, debugHistoryEnabled = false, logger, hasConnectedSessionClient } = {}) {
|
|
33
|
+
constructor({ baseDir, renderHistoryTools, debugHistoryEnabled = false, logger, hasConnectedSessionClient, claudeTranscriptRepository, codexImageStore, claudeForkSession } = {}) {
|
|
50
34
|
super();
|
|
51
35
|
this.baseDir = baseDir || process.cwd();
|
|
52
36
|
this.renderHistoryTools = renderHistoryTools || new Set();
|
|
@@ -54,8 +38,25 @@ class SessionManager extends EventEmitter {
|
|
|
54
38
|
this.logger = logger || console;
|
|
55
39
|
this.hasConnectedSessionClient = hasConnectedSessionClient || (() => false);
|
|
56
40
|
this.sessions = new Map();
|
|
57
|
-
this.
|
|
58
|
-
|
|
41
|
+
this.codexImages = codexImageStore || new CodexImageStore({ logger: this.logger });
|
|
42
|
+
Object.defineProperties(this, {
|
|
43
|
+
codexImageRoot: {
|
|
44
|
+
get: () => this.codexImages.root,
|
|
45
|
+
set: value => { this.codexImages.root = value; }
|
|
46
|
+
},
|
|
47
|
+
codexImageUploadRoot: {
|
|
48
|
+
get: () => this.codexImages.uploadRoot,
|
|
49
|
+
set: value => { this.codexImages.uploadRoot = value; }
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
this.claudeTranscripts = claudeTranscriptRepository || new ClaudeTranscriptRepository({
|
|
53
|
+
baseDir: this.baseDir,
|
|
54
|
+
logger: this.logger
|
|
55
|
+
});
|
|
56
|
+
this.claudeForkSession = claudeForkSession || (async (sessionId, options) => {
|
|
57
|
+
const sdk = await import('@anthropic-ai/claude-agent-sdk');
|
|
58
|
+
return sdk.forkSession(sessionId, options);
|
|
59
|
+
});
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
list() {
|
|
@@ -183,6 +184,8 @@ class SessionManager extends EventEmitter {
|
|
|
183
184
|
logger: this.logger,
|
|
184
185
|
options: claudeOptions
|
|
185
186
|
});
|
|
187
|
+
session.imageAttachments = new Map();
|
|
188
|
+
session.imageUploads = new Map();
|
|
186
189
|
|
|
187
190
|
this.sessions.set(id, session);
|
|
188
191
|
session.on('event', event => this.emit('claude-event', { sessionId: id, event, session }));
|
|
@@ -224,148 +227,71 @@ class SessionManager extends EventEmitter {
|
|
|
224
227
|
return session.ptyManager.write(data);
|
|
225
228
|
}
|
|
226
229
|
|
|
227
|
-
sendClaudeInput(id, text) {
|
|
230
|
+
async sendClaudeInput(id, text, attachmentIds = []) {
|
|
228
231
|
const session = this.get(id);
|
|
229
232
|
if (!session || session.kind !== 'claude-structured') return false;
|
|
230
|
-
this.
|
|
231
|
-
|
|
233
|
+
const attachments = this.getImageAttachments(id, attachmentIds);
|
|
234
|
+
const prompt = String(text || '');
|
|
235
|
+
if (!prompt.trim() && attachments.length === 0) return false;
|
|
236
|
+
const prepared = await Promise.all(attachments.map(async attachment => ({
|
|
237
|
+
id: attachment.id,
|
|
238
|
+
name: attachment.name,
|
|
239
|
+
size: attachment.size,
|
|
240
|
+
mediaType: imageMediaType(attachment.name),
|
|
241
|
+
data: (await fs.promises.readFile(attachment.path)).toString('base64')
|
|
242
|
+
})));
|
|
243
|
+
this.markSessionInput(session, prompt || '[image attachment]');
|
|
244
|
+
const sent = session.sendUserMessage(prompt, prepared);
|
|
245
|
+
if (sent && attachments.length) this.scheduleImageCleanup(id, attachments.map(item => item.id));
|
|
246
|
+
return sent;
|
|
232
247
|
}
|
|
233
248
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (session.kind !== 'codex-structured' || session.presentation !== 'structured') {
|
|
238
|
-
throw attachmentError('Image attachments are available only in Codex chat mode');
|
|
239
|
-
}
|
|
240
|
-
if (!Buffer.isBuffer(bytes) || bytes.length === 0) throw attachmentError('Image data is required');
|
|
241
|
-
if (bytes.length > CODEX_IMAGE_MAX_BYTES) throw attachmentError('Image must be 50 MB or smaller');
|
|
242
|
-
if (session.imageAttachments.size >= CODEX_IMAGE_MAX_PER_SESSION) {
|
|
243
|
-
throw attachmentError(`You can attach at most ${CODEX_IMAGE_MAX_PER_SESSION} images at a time`);
|
|
244
|
-
}
|
|
249
|
+
storeImageAttachment(id, bytes) {
|
|
250
|
+
return this.codexImages.store(this.get(id), bytes);
|
|
251
|
+
}
|
|
245
252
|
|
|
246
|
-
|
|
247
|
-
|
|
253
|
+
appendImageChunk(id, input = {}, bytes) {
|
|
254
|
+
return this.codexImages.appendChunk(this.get(id), input, bytes);
|
|
255
|
+
}
|
|
248
256
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const attachment = {
|
|
252
|
-
id: uuidv4(),
|
|
253
|
-
name: `image.${extension}`,
|
|
254
|
-
path: path.join(directory, `${uuidv4()}.${extension}`),
|
|
255
|
-
size: bytes.length,
|
|
256
|
-
createdAt: Date.now(),
|
|
257
|
-
cleanupTimer: null
|
|
258
|
-
};
|
|
259
|
-
await fs.promises.writeFile(attachment.path, bytes, { mode: 0o600, flag: 'wx' });
|
|
260
|
-
session.imageAttachments.set(attachment.id, attachment);
|
|
261
|
-
return { id: attachment.id, name: attachment.name, size: attachment.size };
|
|
257
|
+
discardImageUpload(id, uploadId) {
|
|
258
|
+
return this.codexImages.discardUpload(this.get(id), uploadId);
|
|
262
259
|
}
|
|
263
260
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (session.kind !== 'codex-structured' || session.presentation !== 'structured') {
|
|
268
|
-
throw attachmentError('Image attachments are available only in Codex chat mode');
|
|
269
|
-
}
|
|
270
|
-
if (!Buffer.isBuffer(bytes) || bytes.length === 0) throw attachmentError('Image chunk is required');
|
|
271
|
-
const uploadId = String(input.uploadId || '');
|
|
272
|
-
const chunkIndex = Number(input.chunkIndex);
|
|
273
|
-
const chunkTotal = Number(input.chunkTotal);
|
|
274
|
-
if (!isSafeUploadId(uploadId)) throw attachmentError('Invalid image upload id');
|
|
275
|
-
if (!Number.isInteger(chunkIndex) || !Number.isInteger(chunkTotal) || chunkIndex < 0 || chunkTotal < 1 || chunkTotal > CODEX_IMAGE_MAX_CHUNKS || chunkIndex >= chunkTotal) {
|
|
276
|
-
throw attachmentError('Invalid image chunk metadata');
|
|
277
|
-
}
|
|
261
|
+
discardImageAttachment(id, attachmentId) {
|
|
262
|
+
return this.codexImages.discardAttachment(this.get(id), attachmentId);
|
|
263
|
+
}
|
|
278
264
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const directory = path.join(this.codexImageUploadRoot, session.id);
|
|
283
|
-
await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
284
|
-
upload = {
|
|
285
|
-
id: uploadId,
|
|
286
|
-
path: path.join(directory, `${uploadId}.part`),
|
|
287
|
-
chunkTotal,
|
|
288
|
-
nextChunkIndex: 0,
|
|
289
|
-
bytes: 0
|
|
290
|
-
};
|
|
291
|
-
session.imageUploads.set(uploadId, upload);
|
|
292
|
-
}
|
|
293
|
-
if (upload.chunkTotal !== chunkTotal || upload.nextChunkIndex !== chunkIndex) {
|
|
294
|
-
throw attachmentError('Image chunks arrived out of order');
|
|
295
|
-
}
|
|
296
|
-
if (upload.bytes + bytes.length > CODEX_IMAGE_MAX_BYTES) {
|
|
297
|
-
await this.discardCodexImageUpload(id, uploadId);
|
|
298
|
-
throw attachmentError('Image must be 50 MB or smaller');
|
|
299
|
-
}
|
|
265
|
+
getImageAttachments(id, attachmentIds = []) {
|
|
266
|
+
return this.codexImages.resolve(this.get(id), attachmentIds);
|
|
267
|
+
}
|
|
300
268
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
upload.nextChunkIndex += 1;
|
|
269
|
+
scheduleImageCleanup(id, attachmentIds) {
|
|
270
|
+
this.codexImages.scheduleCleanup(this.get(id), attachmentIds);
|
|
271
|
+
}
|
|
305
272
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
273
|
+
storeCodexImageAttachment(id, bytes) {
|
|
274
|
+
return this.storeImageAttachment(id, bytes);
|
|
275
|
+
}
|
|
309
276
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const image = await fs.promises.readFile(upload.path);
|
|
313
|
-
const attachment = await this.storeCodexImageAttachment(id, image);
|
|
314
|
-
return { complete: true, attachment };
|
|
315
|
-
} finally {
|
|
316
|
-
await fs.promises.rm(upload.path, { force: true });
|
|
317
|
-
}
|
|
277
|
+
appendCodexImageChunk(id, input = {}, bytes) {
|
|
278
|
+
return this.appendImageChunk(id, input, bytes);
|
|
318
279
|
}
|
|
319
280
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (!session || !session.imageUploads || !isSafeUploadId(uploadId)) return false;
|
|
323
|
-
const upload = session.imageUploads.get(uploadId);
|
|
324
|
-
if (!upload) return false;
|
|
325
|
-
session.imageUploads.delete(uploadId);
|
|
326
|
-
await fs.promises.rm(upload.path, { force: true });
|
|
327
|
-
return true;
|
|
281
|
+
discardCodexImageUpload(id, uploadId) {
|
|
282
|
+
return this.discardImageUpload(id, uploadId);
|
|
328
283
|
}
|
|
329
284
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (!session || session.kind !== 'codex-structured') return false;
|
|
333
|
-
const attachment = session.imageAttachments.get(attachmentId);
|
|
334
|
-
if (!attachment) return false;
|
|
335
|
-
clearTimeout(attachment.cleanupTimer);
|
|
336
|
-
session.imageAttachments.delete(attachmentId);
|
|
337
|
-
await fs.promises.rm(attachment.path, { force: true });
|
|
338
|
-
return true;
|
|
285
|
+
discardCodexImageAttachment(id, attachmentId) {
|
|
286
|
+
return this.discardImageAttachment(id, attachmentId);
|
|
339
287
|
}
|
|
340
288
|
|
|
341
289
|
getCodexImageAttachments(id, attachmentIds = []) {
|
|
342
|
-
|
|
343
|
-
if (!session || session.kind !== 'codex-structured') throw attachmentError('Codex session not found', 404);
|
|
344
|
-
const ids = Array.isArray(attachmentIds) ? attachmentIds : [];
|
|
345
|
-
if (ids.length > CODEX_IMAGE_MAX_PER_SESSION) throw attachmentError(`You can attach at most ${CODEX_IMAGE_MAX_PER_SESSION} images at a time`);
|
|
346
|
-
const uniqueIds = [...new Set(ids.map(value => String(value)))];
|
|
347
|
-
if (uniqueIds.length !== ids.length) throw attachmentError('Duplicate image attachment');
|
|
348
|
-
return uniqueIds.map(attachmentId => {
|
|
349
|
-
const attachment = session.imageAttachments.get(attachmentId);
|
|
350
|
-
if (!attachment) throw attachmentError('Image attachment is no longer available');
|
|
351
|
-
return attachment;
|
|
352
|
-
});
|
|
290
|
+
return this.getImageAttachments(id, attachmentIds);
|
|
353
291
|
}
|
|
354
292
|
|
|
355
293
|
scheduleCodexImageCleanup(id, attachmentIds) {
|
|
356
|
-
|
|
357
|
-
if (!session || session.kind !== 'codex-structured') return;
|
|
358
|
-
for (const attachmentId of attachmentIds) {
|
|
359
|
-
const attachment = session.imageAttachments.get(attachmentId);
|
|
360
|
-
if (!attachment) continue;
|
|
361
|
-
clearTimeout(attachment.cleanupTimer);
|
|
362
|
-
attachment.cleanupTimer = setTimeout(() => {
|
|
363
|
-
this.discardCodexImageAttachment(id, attachmentId).catch(error => {
|
|
364
|
-
this.logger.debugInfo?.(`[codex-image] cleanup failed: ${error.message}`);
|
|
365
|
-
});
|
|
366
|
-
}, CODEX_IMAGE_CLEANUP_DELAY_MS);
|
|
367
|
-
attachment.cleanupTimer.unref?.();
|
|
368
|
-
}
|
|
294
|
+
this.scheduleImageCleanup(id, attachmentIds);
|
|
369
295
|
}
|
|
370
296
|
|
|
371
297
|
async sendCodexInput(id, text, attachmentIds = []) {
|
|
@@ -392,6 +318,18 @@ class SessionManager extends EventEmitter {
|
|
|
392
318
|
return session.updateSettings(settings || {});
|
|
393
319
|
}
|
|
394
320
|
|
|
321
|
+
showClaudeUsage(id) {
|
|
322
|
+
const session = this.get(id);
|
|
323
|
+
if (!session || session.kind !== 'claude-structured') return Promise.resolve(false);
|
|
324
|
+
return session.showUsage();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
showClaudeContext(id) {
|
|
328
|
+
const session = this.get(id);
|
|
329
|
+
if (!session || session.kind !== 'claude-structured') return Promise.resolve(false);
|
|
330
|
+
return session.showContext();
|
|
331
|
+
}
|
|
332
|
+
|
|
395
333
|
abortClaude(id) {
|
|
396
334
|
const session = this.get(id);
|
|
397
335
|
if (!session || session.kind !== 'claude-structured') return false;
|
|
@@ -405,6 +343,29 @@ class SessionManager extends EventEmitter {
|
|
|
405
343
|
return session.selectResumeSession(resumeSessionId, historyMessages);
|
|
406
344
|
}
|
|
407
345
|
|
|
346
|
+
async forkClaude(id, sourceSessionId) {
|
|
347
|
+
const session = this.get(id);
|
|
348
|
+
if (!session || session.kind !== 'claude-structured') return null;
|
|
349
|
+
if (session.status === 'thinking') {
|
|
350
|
+
const error = new Error('Claude must be idle before forking a session');
|
|
351
|
+
error.statusCode = 409;
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
const sourceId = String(sourceSessionId || session.claudeSessionId || session.resumeSessionId || '').trim();
|
|
355
|
+
if (!sourceId) {
|
|
356
|
+
const error = new Error('No Claude session is available to fork');
|
|
357
|
+
error.statusCode = 400;
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
const result = await this.claudeForkSession(sourceId, { dir: this.getSessionWorkingDirectory(session) });
|
|
361
|
+
const forkedId = result?.sessionId;
|
|
362
|
+
if (!forkedId) throw new Error('Claude SDK did not return a forked session id');
|
|
363
|
+
const historyMessages = this.readClaudeTranscriptMessages(this.getSessionWorkingDirectory(session), forkedId);
|
|
364
|
+
session.selectResumeSession(forkedId, historyMessages);
|
|
365
|
+
session.appendMessage({ kind: 'event', level: 'info', text: `Forked from Claude session ${sourceId}` });
|
|
366
|
+
return { id: session.id, name: session.name, claudeSessionId: forkedId };
|
|
367
|
+
}
|
|
368
|
+
|
|
408
369
|
listClaudeResumeSessions(id) {
|
|
409
370
|
const session = this.get(id);
|
|
410
371
|
if (!session || session.kind !== 'claude-structured') return null;
|
|
@@ -716,192 +677,11 @@ class SessionManager extends EventEmitter {
|
|
|
716
677
|
}
|
|
717
678
|
|
|
718
679
|
scanClaudeProjectSessions(workingDirectory) {
|
|
719
|
-
|
|
720
|
-
if (!projectDir || !fs.existsSync(projectDir)) return [];
|
|
721
|
-
const files = fs.readdirSync(projectDir)
|
|
722
|
-
.filter(file => /^[0-9a-f-]{36}\.jsonl$/i.test(file))
|
|
723
|
-
.map(file => {
|
|
724
|
-
const fullPath = path.join(projectDir, file);
|
|
725
|
-
const stat = fs.statSync(fullPath);
|
|
726
|
-
return {
|
|
727
|
-
id: file.replace(/\.jsonl$/i, ''),
|
|
728
|
-
path: fullPath,
|
|
729
|
-
mtimeMs: stat.mtimeMs,
|
|
730
|
-
size: stat.size
|
|
731
|
-
};
|
|
732
|
-
})
|
|
733
|
-
.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
734
|
-
.slice(0, 40);
|
|
735
|
-
|
|
736
|
-
return files.map(file => this.describeClaudeSessionFile(file));
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
getClaudeProjectDir(workingDirectory) {
|
|
740
|
-
const cwd = path.resolve(workingDirectory || this.baseDir);
|
|
741
|
-
const encoded = cwd.replace(/[^a-zA-Z0-9]/g, '-');
|
|
742
|
-
return path.join(os.homedir(), '.claude', 'projects', encoded);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
describeClaudeSessionFile(file) {
|
|
746
|
-
let cwd = '';
|
|
747
|
-
let firstText = '';
|
|
748
|
-
let lastText = '';
|
|
749
|
-
try {
|
|
750
|
-
const content = fs.readFileSync(file.path, 'utf8');
|
|
751
|
-
const lines = content.split('\n').filter(Boolean);
|
|
752
|
-
for (const line of lines) {
|
|
753
|
-
const parsed = this.parseClaudeJsonLine(line);
|
|
754
|
-
if (!parsed) continue;
|
|
755
|
-
if (!cwd && typeof parsed.cwd === 'string') cwd = parsed.cwd;
|
|
756
|
-
const text = this.extractClaudeTranscriptText(parsed);
|
|
757
|
-
if (!text) continue;
|
|
758
|
-
if (!firstText) firstText = text;
|
|
759
|
-
lastText = text;
|
|
760
|
-
}
|
|
761
|
-
} catch (error) {
|
|
762
|
-
this.logger.debugInfo?.(`[claude-resume] Failed to read ${file.path}: ${error.message}`);
|
|
763
|
-
}
|
|
764
|
-
return {
|
|
765
|
-
id: file.id,
|
|
766
|
-
cwd,
|
|
767
|
-
updatedAt: file.mtimeMs,
|
|
768
|
-
size: file.size,
|
|
769
|
-
firstText: firstText ? previewText(firstText, 120) : '',
|
|
770
|
-
lastText: lastText ? previewText(lastText, 160) : ''
|
|
771
|
-
};
|
|
680
|
+
return this.claudeTranscripts.list(workingDirectory);
|
|
772
681
|
}
|
|
773
682
|
|
|
774
683
|
readClaudeTranscriptMessages(workingDirectory, resumeSessionId) {
|
|
775
|
-
|
|
776
|
-
if (!/^[0-9a-f-]{36}$/i.test(id)) return [];
|
|
777
|
-
const filePath = path.join(this.getClaudeProjectDir(workingDirectory), `${id}.jsonl`);
|
|
778
|
-
if (!fs.existsSync(filePath)) return [];
|
|
779
|
-
|
|
780
|
-
const messages = [];
|
|
781
|
-
try {
|
|
782
|
-
const lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
|
|
783
|
-
for (const line of lines) {
|
|
784
|
-
const record = this.parseClaudeJsonLine(line);
|
|
785
|
-
if (!record || record.isSidechain) continue;
|
|
786
|
-
messages.push(...this.mapClaudeTranscriptRecord(record));
|
|
787
|
-
}
|
|
788
|
-
} catch (error) {
|
|
789
|
-
this.logger.debugInfo?.(`[claude-resume] Failed to backfill ${filePath}: ${error.message}`);
|
|
790
|
-
return [];
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
const maxMessages = 1000;
|
|
794
|
-
const visible = messages.filter(Boolean).slice(-maxMessages);
|
|
795
|
-
if (messages.length > maxMessages) {
|
|
796
|
-
visible.unshift({
|
|
797
|
-
id: uuidv4(),
|
|
798
|
-
kind: 'event',
|
|
799
|
-
level: 'info',
|
|
800
|
-
text: `Showing the latest ${maxMessages} resumed transcript items.`,
|
|
801
|
-
createdAt: Date.now()
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
return visible;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
mapClaudeTranscriptRecord(record) {
|
|
808
|
-
const createdAt = Number.isFinite(Date.parse(record.timestamp)) ? Date.parse(record.timestamp) : Date.now();
|
|
809
|
-
const message = record.message || {};
|
|
810
|
-
const content = message.content;
|
|
811
|
-
if (record.type === 'user') {
|
|
812
|
-
if (typeof content === 'string') {
|
|
813
|
-
const text = content.trim();
|
|
814
|
-
return text ? [{ id: uuidv4(), kind: 'user', text, createdAt }] : [];
|
|
815
|
-
}
|
|
816
|
-
if (Array.isArray(content)) {
|
|
817
|
-
return content.flatMap(item => {
|
|
818
|
-
if (!item || typeof item !== 'object') return [];
|
|
819
|
-
if (item.type === 'tool_result') {
|
|
820
|
-
const text = this.textFromClaudeContent(item.content).trim();
|
|
821
|
-
return text ? [{
|
|
822
|
-
id: uuidv4(),
|
|
823
|
-
kind: 'tool-result',
|
|
824
|
-
toolUseId: item.tool_use_id,
|
|
825
|
-
text,
|
|
826
|
-
isError: Boolean(item.is_error),
|
|
827
|
-
createdAt
|
|
828
|
-
}] : [];
|
|
829
|
-
}
|
|
830
|
-
if (item.type === 'text' && typeof item.text === 'string' && item.text.trim()) {
|
|
831
|
-
return [{ id: uuidv4(), kind: 'user', text: item.text.trim(), createdAt }];
|
|
832
|
-
}
|
|
833
|
-
return [];
|
|
834
|
-
});
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
if (record.type === 'assistant' && Array.isArray(content)) {
|
|
839
|
-
const mapped = [];
|
|
840
|
-
const text = this.textFromClaudeContent(content).trim();
|
|
841
|
-
if (text) mapped.push({ id: uuidv4(), kind: 'assistant', text, createdAt });
|
|
842
|
-
for (const item of content) {
|
|
843
|
-
if (!item || item.type !== 'tool_use') continue;
|
|
844
|
-
mapped.push({
|
|
845
|
-
id: uuidv4(),
|
|
846
|
-
kind: 'tool',
|
|
847
|
-
name: item.name || 'tool',
|
|
848
|
-
summary: this.summarizeClaudeToolInput(item.input),
|
|
849
|
-
input: item.input,
|
|
850
|
-
toolUseId: item.id,
|
|
851
|
-
createdAt
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
return mapped;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
if (record.type === 'summary' && typeof record.summary === 'string' && record.summary.trim()) {
|
|
858
|
-
return [{ id: uuidv4(), kind: 'event', level: 'info', text: `Summary: ${record.summary.trim()}`, createdAt }];
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
return [];
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
textFromClaudeContent(content) {
|
|
865
|
-
if (typeof content === 'string') return content;
|
|
866
|
-
if (!Array.isArray(content)) return '';
|
|
867
|
-
return content.map(item => {
|
|
868
|
-
if (!item || typeof item !== 'object') return '';
|
|
869
|
-
if (item.type === 'text' && typeof item.text === 'string') return item.text;
|
|
870
|
-
if (item.type === 'tool_result') return this.textFromClaudeContent(item.content);
|
|
871
|
-
return '';
|
|
872
|
-
}).filter(Boolean).join('\n');
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
summarizeClaudeToolInput(input) {
|
|
876
|
-
if (!input || typeof input !== 'object') return '';
|
|
877
|
-
if (typeof input.command === 'string') return input.command;
|
|
878
|
-
if (typeof input.file_path === 'string') return input.file_path;
|
|
879
|
-
if (typeof input.path === 'string') return input.path;
|
|
880
|
-
const serialized = JSON.stringify(input);
|
|
881
|
-
return serialized.length > 240 ? serialized.slice(0, 240) + '...' : serialized;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
parseClaudeJsonLine(line) {
|
|
885
|
-
try {
|
|
886
|
-
return JSON.parse(line);
|
|
887
|
-
} catch (_) {
|
|
888
|
-
return null;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
extractClaudeTranscriptText(record) {
|
|
893
|
-
const message = record && record.message;
|
|
894
|
-
const content = message && message.content;
|
|
895
|
-
if (typeof content === 'string') return content.trim();
|
|
896
|
-
if (Array.isArray(content)) {
|
|
897
|
-
return content.map(item => {
|
|
898
|
-
if (!item || typeof item !== 'object') return '';
|
|
899
|
-
if (item.type === 'text' && typeof item.text === 'string') return item.text;
|
|
900
|
-
if (item.type === 'tool_use') return `[tool] ${item.name || 'tool'}`;
|
|
901
|
-
return '';
|
|
902
|
-
}).filter(Boolean).join('\n').trim();
|
|
903
|
-
}
|
|
904
|
-
return '';
|
|
684
|
+
return this.claudeTranscripts.readMessages(workingDirectory, resumeSessionId);
|
|
905
685
|
}
|
|
906
686
|
|
|
907
687
|
logHistoryRequest(id, req) {
|
|
@@ -1048,20 +828,11 @@ class SessionManager extends EventEmitter {
|
|
|
1048
828
|
}
|
|
1049
829
|
|
|
1050
830
|
clearCodexImageAttachments(session) {
|
|
1051
|
-
|
|
1052
|
-
for (const attachment of session.imageAttachments.values()) clearTimeout(attachment.cleanupTimer);
|
|
1053
|
-
session.imageAttachments.clear();
|
|
1054
|
-
fs.promises.rm(path.join(this.codexImageRoot, session.id), { recursive: true, force: true }).catch(error => {
|
|
1055
|
-
this.logger.debugInfo?.(`[codex-image] session cleanup failed: ${error.message}`);
|
|
1056
|
-
});
|
|
831
|
+
this.codexImages.clearAttachments(session);
|
|
1057
832
|
}
|
|
1058
833
|
|
|
1059
834
|
clearCodexImageUploads(session) {
|
|
1060
|
-
|
|
1061
|
-
session.imageUploads.clear();
|
|
1062
|
-
fs.promises.rm(path.join(this.codexImageUploadRoot, session.id), { recursive: true, force: true }).catch(error => {
|
|
1063
|
-
this.logger.debugInfo?.(`[codex-image] upload cleanup failed: ${error.message}`);
|
|
1064
|
-
});
|
|
835
|
+
this.codexImages.clearUploads(session);
|
|
1065
836
|
}
|
|
1066
837
|
|
|
1067
838
|
getSessionDiagnostics(session, extra = {}) {
|