glad-web 1.0.19 → 1.0.21
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/config.js +82 -0
- package/lib/claude/structured-session.js +707 -0
- package/lib/commands/web.js +70 -12
- package/lib/session/session-manager.js +317 -2
- package/lib/web/index.html +1016 -2
- package/package.json +2 -1
package/lib/commands/web.js
CHANGED
|
@@ -33,11 +33,16 @@ function sendCompressedJson(req, res, payload) {
|
|
|
33
33
|
res.type('application/json').send(body);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function getSessionWorkingDirectory(session) {
|
|
37
|
+
return session.workingDir || (session.ptyManager && session.ptyManager.workingDir) || process.cwd();
|
|
38
|
+
}
|
|
39
|
+
|
|
36
40
|
const { detectInstalledTools } = require('../ai-tools/detector');
|
|
37
41
|
const logger = require('../utils/logger');
|
|
38
42
|
const { JobStore } = require('../schedule/job-store');
|
|
39
43
|
const JobRunner = require('../schedule/job-runner');
|
|
40
44
|
const SchedulerService = require('../schedule/scheduler-service');
|
|
45
|
+
const { getClaudeRuntimeConfig } = require('../claude/config');
|
|
41
46
|
|
|
42
47
|
async function webCommand(options) {
|
|
43
48
|
const port = parseInt(options.port) || 3000;
|
|
@@ -85,6 +90,9 @@ async function webCommand(options) {
|
|
|
85
90
|
sessionManager.on('output', ({ sessionId, data }) => {
|
|
86
91
|
broadcastToSession(sessionId, { type: 'output', data });
|
|
87
92
|
});
|
|
93
|
+
sessionManager.on('claude-event', ({ sessionId, event }) => {
|
|
94
|
+
broadcastToSession(sessionId, { type: 'claude-event', event });
|
|
95
|
+
});
|
|
88
96
|
sessionManager.on('exit', ({ sessionId }) => {
|
|
89
97
|
broadcastToSession(sessionId, { type: 'exit' });
|
|
90
98
|
});
|
|
@@ -179,12 +187,16 @@ async function webCommand(options) {
|
|
|
179
187
|
res.json(sessionManager.list());
|
|
180
188
|
});
|
|
181
189
|
|
|
190
|
+
app.get('/api/claude-config', (req, res) => {
|
|
191
|
+
res.json({ success: true, config: getClaudeRuntimeConfig(process.env) });
|
|
192
|
+
});
|
|
193
|
+
|
|
182
194
|
// API: Create a new PTY session
|
|
183
195
|
app.post('/api/sessions', async (req, res) => {
|
|
184
196
|
logger.debug(`API: POST /api/sessions - ${JSON.stringify(req.body)}`);
|
|
185
197
|
try {
|
|
186
|
-
const { toolKey, workingDirectory } = req.body;
|
|
187
|
-
const session = sessionManager.create({ toolKey, workingDirectory });
|
|
198
|
+
const { toolKey, workingDirectory, claudeOptions } = req.body;
|
|
199
|
+
const session = sessionManager.create({ toolKey, workingDirectory, claudeOptions });
|
|
188
200
|
res.json({ id: session.id });
|
|
189
201
|
} catch (e) {
|
|
190
202
|
logger.error(`API: POST /api/sessions failed: ${e.message}`);
|
|
@@ -263,6 +275,32 @@ async function webCommand(options) {
|
|
|
263
275
|
res.json({ success: true, diagnostics });
|
|
264
276
|
});
|
|
265
277
|
|
|
278
|
+
app.get('/api/sessions/:id/claude-resume-sessions', (req, res) => {
|
|
279
|
+
const items = sessionManager.listClaudeResumeSessions(req.params.id);
|
|
280
|
+
if (!items) return res.status(404).json({ error: 'Claude session not found' });
|
|
281
|
+
res.json({ success: true, items });
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
app.patch('/api/sessions/:id/claude-settings', (req, res) => {
|
|
285
|
+
const state = sessionManager.updateClaudeSettings(req.params.id, req.body || {});
|
|
286
|
+
if (!state) return res.status(404).json({ error: 'Claude session not found' });
|
|
287
|
+
res.json({ success: true, state });
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
app.post('/api/sessions/:id/claude-abort', (req, res) => {
|
|
291
|
+
const success = sessionManager.abortClaude(req.params.id);
|
|
292
|
+
if (!success) return res.status(404).json({ error: 'Claude session not found or idle' });
|
|
293
|
+
res.json({ success: true });
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
app.post('/api/sessions/:id/claude-resume', (req, res) => {
|
|
297
|
+
const resumeSessionId = req.body && req.body.resumeSessionId;
|
|
298
|
+
if (!resumeSessionId) return res.status(400).json({ error: 'Missing resumeSessionId' });
|
|
299
|
+
const success = sessionManager.resumeClaude(req.params.id, resumeSessionId);
|
|
300
|
+
if (!success) return res.status(404).json({ error: 'Claude session not found' });
|
|
301
|
+
res.json({ success: true });
|
|
302
|
+
});
|
|
303
|
+
|
|
266
304
|
app.post('/api/debug/client-log', (req, res) => {
|
|
267
305
|
const { sessionId, event, payload } = req.body || {};
|
|
268
306
|
sessionManager.logClientDebug(sessionId, event, payload);
|
|
@@ -274,7 +312,7 @@ async function webCommand(options) {
|
|
|
274
312
|
const session = sessionManager.get(req.params.id);
|
|
275
313
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
276
314
|
const hash = req.params.hash;
|
|
277
|
-
const result = await gitService.show(session
|
|
315
|
+
const result = await gitService.show(getSessionWorkingDirectory(session), hash);
|
|
278
316
|
res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
|
|
279
317
|
});
|
|
280
318
|
|
|
@@ -283,7 +321,7 @@ async function webCommand(options) {
|
|
|
283
321
|
const session = sessionManager.get(req.params.id);
|
|
284
322
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
285
323
|
const hash = req.params.hash;
|
|
286
|
-
const result = await gitService.nameRev(session
|
|
324
|
+
const result = await gitService.nameRev(getSessionWorkingDirectory(session), hash);
|
|
287
325
|
res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
|
|
288
326
|
});
|
|
289
327
|
|
|
@@ -291,7 +329,7 @@ async function webCommand(options) {
|
|
|
291
329
|
app.get('/api/sessions/:id/git-log', async (req, res) => {
|
|
292
330
|
const session = sessionManager.get(req.params.id);
|
|
293
331
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
294
|
-
const result = await gitService.log(session
|
|
332
|
+
const result = await gitService.log(getSessionWorkingDirectory(session), req.query.maxCount);
|
|
295
333
|
if (!result.success) {
|
|
296
334
|
return res.status(500).json({ error: result.error, stderr: result.stderr });
|
|
297
335
|
}
|
|
@@ -302,7 +340,7 @@ async function webCommand(options) {
|
|
|
302
340
|
app.get('/api/sessions/:id/git-status', async (req, res) => {
|
|
303
341
|
const session = sessionManager.get(req.params.id);
|
|
304
342
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
305
|
-
const result = await gitService.status(session
|
|
343
|
+
const result = await gitService.status(getSessionWorkingDirectory(session));
|
|
306
344
|
if (!result.success) {
|
|
307
345
|
return res.status(500).json({ error: result.error, stderr: result.stderr });
|
|
308
346
|
}
|
|
@@ -314,7 +352,7 @@ async function webCommand(options) {
|
|
|
314
352
|
const session = sessionManager.get(req.params.id);
|
|
315
353
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
316
354
|
const isStaged = req.query.staged === 'true';
|
|
317
|
-
const result = await gitService.diffNumstat(session
|
|
355
|
+
const result = await gitService.diffNumstat(getSessionWorkingDirectory(session), isStaged);
|
|
318
356
|
res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
|
|
319
357
|
});
|
|
320
358
|
|
|
@@ -325,7 +363,7 @@ async function webCommand(options) {
|
|
|
325
363
|
const isStaged = req.query.staged === 'true';
|
|
326
364
|
const filePath = req.query.path;
|
|
327
365
|
if (!filePath) return res.status(400).json({ error: 'Missing file path' });
|
|
328
|
-
const result = await gitService.diffFile(session
|
|
366
|
+
const result = await gitService.diffFile(getSessionWorkingDirectory(session), filePath, isStaged);
|
|
329
367
|
res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
|
|
330
368
|
});
|
|
331
369
|
|
|
@@ -335,7 +373,7 @@ async function webCommand(options) {
|
|
|
335
373
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
336
374
|
const filePath = req.query.path || '';
|
|
337
375
|
if (!filePath) return res.status(400).json({ error: 'Missing file path' });
|
|
338
|
-
const cwd = session
|
|
376
|
+
const cwd = getSessionWorkingDirectory(session);
|
|
339
377
|
try {
|
|
340
378
|
const content = workspaceService.readFile(cwd, filePath);
|
|
341
379
|
res.json({ success: true, content });
|
|
@@ -349,7 +387,7 @@ async function webCommand(options) {
|
|
|
349
387
|
const session = sessionManager.get(req.params.id);
|
|
350
388
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
351
389
|
const dirPath = req.query.path || '';
|
|
352
|
-
const cwd = session
|
|
390
|
+
const cwd = getSessionWorkingDirectory(session);
|
|
353
391
|
try {
|
|
354
392
|
const files = await workspaceService.listDirectory(cwd, dirPath);
|
|
355
393
|
res.json({ success: true, files });
|
|
@@ -393,15 +431,20 @@ async function webCommand(options) {
|
|
|
393
431
|
session.resizeOwner = ws;
|
|
394
432
|
}
|
|
395
433
|
sessionManager.logWsConnected(sessionId, req);
|
|
434
|
+
|
|
435
|
+
if (session.kind === 'claude-structured') {
|
|
436
|
+
ws.send(JSON.stringify({ type: 'claude-snapshot', snapshot: sessionManager.getClaudeSnapshot(sessionId) }));
|
|
437
|
+
}
|
|
396
438
|
|
|
397
439
|
// Send catchup output. TUI tools may skip the raw circular buffer, so fall
|
|
398
440
|
// back to the rendered/text history snapshot instead of reconnecting blank.
|
|
399
441
|
const catchup = sessionManager.getCatchupOutput(sessionId);
|
|
400
|
-
ws.needsTuiRedraw =
|
|
442
|
+
ws.needsTuiRedraw = session.kind !== 'claude-structured'
|
|
443
|
+
&& ['antigravity', 'claude-code', 'codex'].includes(session.tool.key)
|
|
401
444
|
&& (isReconnect || (catchup && catchup.source === 'rendered-history'));
|
|
402
445
|
if (ws.needsTuiRedraw) {
|
|
403
446
|
ws.send(JSON.stringify({ type: 'reset' }));
|
|
404
|
-
} else if (catchup && catchup.data) {
|
|
447
|
+
} else if (session.kind !== 'claude-structured' && catchup && catchup.data) {
|
|
405
448
|
sessionManager.logWsCatchupOutput(sessionId, catchup);
|
|
406
449
|
ws.send(JSON.stringify({ type: 'output', data: catchup.data }));
|
|
407
450
|
}
|
|
@@ -412,6 +455,21 @@ async function webCommand(options) {
|
|
|
412
455
|
if (payload.type === 'input') {
|
|
413
456
|
session.write(payload.data);
|
|
414
457
|
}
|
|
458
|
+
if (payload.type === 'claude-input') {
|
|
459
|
+
sessionManager.sendClaudeInput(sessionId, payload.text || '');
|
|
460
|
+
}
|
|
461
|
+
if (payload.type === 'claude-permission') {
|
|
462
|
+
sessionManager.respondClaudePermission(sessionId, payload.id, Boolean(payload.approved), payload.action || null);
|
|
463
|
+
}
|
|
464
|
+
if (payload.type === 'claude-settings') {
|
|
465
|
+
sessionManager.updateClaudeSettings(sessionId, payload.settings || {});
|
|
466
|
+
}
|
|
467
|
+
if (payload.type === 'claude-abort') {
|
|
468
|
+
sessionManager.abortClaude(sessionId);
|
|
469
|
+
}
|
|
470
|
+
if (payload.type === 'claude-resume') {
|
|
471
|
+
sessionManager.resumeClaude(sessionId, payload.resumeSessionId || '');
|
|
472
|
+
}
|
|
415
473
|
if (payload.type === 'resize' && session.resizeOwner === ws) {
|
|
416
474
|
sessionManager.logWsResize(sessionId, payload.cols, payload.rows);
|
|
417
475
|
if (ws.needsTuiRedraw) {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
const { EventEmitter } = require('events');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
4
5
|
const { v4: uuidv4 } = require('uuid');
|
|
5
6
|
const PTYManager = require('./pty-manager');
|
|
6
7
|
const TextHistory = require('./text-history');
|
|
7
8
|
const RenderedHistory = require('./rendered-history');
|
|
8
9
|
const CircularBuffer = require('./buffer');
|
|
9
10
|
const { getToolByKey } = require('../ai-tools/registry');
|
|
11
|
+
const ClaudeStructuredSession = require('../claude/structured-session');
|
|
10
12
|
|
|
11
13
|
function previewText(text, maxChars = 320) {
|
|
12
14
|
if (!text) return '';
|
|
@@ -36,7 +38,8 @@ class SessionManager extends EventEmitter {
|
|
|
36
38
|
tool: session.tool.displayName,
|
|
37
39
|
startTime: session.startTime,
|
|
38
40
|
toolKey: session.tool.key,
|
|
39
|
-
workingDirectory: session
|
|
41
|
+
workingDirectory: this.getSessionWorkingDirectory(session),
|
|
42
|
+
mode: session.kind === 'claude-structured' ? 'structured' : 'terminal',
|
|
40
43
|
hasUnreadCompletion: Boolean(session.hasUnreadCompletion),
|
|
41
44
|
timedInputCount: session.timedInputs
|
|
42
45
|
? Array.from(session.timedInputs.values()).filter(item => item.sendAt > Date.now()).length
|
|
@@ -52,7 +55,7 @@ class SessionManager extends EventEmitter {
|
|
|
52
55
|
return this.sessions.has(id);
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
create({ toolKey, workingDirectory, name }) {
|
|
58
|
+
create({ toolKey, workingDirectory, name, claudeOptions }) {
|
|
56
59
|
this.logger.info(`Creating session: toolKey=${toolKey || ''}, workingDirectory=${workingDirectory || '(default)'}`);
|
|
57
60
|
const tool = getToolByKey(toolKey);
|
|
58
61
|
if (!tool) {
|
|
@@ -62,6 +65,10 @@ class SessionManager extends EventEmitter {
|
|
|
62
65
|
throw err;
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
if (tool.key === 'claude-code') {
|
|
69
|
+
return this.createClaudeStructuredSession({ tool, workingDirectory, name, claudeOptions });
|
|
70
|
+
}
|
|
71
|
+
|
|
65
72
|
const id = uuidv4();
|
|
66
73
|
const buffer = new CircularBuffer(500000);
|
|
67
74
|
const textHistory = new TextHistory({ maxBytes: 20 * 1024 * 1024, debugLabel: id });
|
|
@@ -125,16 +132,86 @@ class SessionManager extends EventEmitter {
|
|
|
125
132
|
return session;
|
|
126
133
|
}
|
|
127
134
|
|
|
135
|
+
createClaudeStructuredSession({ tool, workingDirectory, name, claudeOptions = {} }) {
|
|
136
|
+
const sessionDir = workingDirectory && String(workingDirectory).trim()
|
|
137
|
+
? path.resolve(this.baseDir, String(workingDirectory).trim())
|
|
138
|
+
: this.baseDir;
|
|
139
|
+
|
|
140
|
+
if (!fs.existsSync(sessionDir)) {
|
|
141
|
+
const err = new Error(`Directory does not exist: ${sessionDir}`);
|
|
142
|
+
err.statusCode = 400;
|
|
143
|
+
this.logger.error(`Create Claude session failed: missing directory ${sessionDir}`);
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const id = uuidv4();
|
|
148
|
+
const session = new ClaudeStructuredSession({
|
|
149
|
+
id,
|
|
150
|
+
tool,
|
|
151
|
+
workingDir: sessionDir,
|
|
152
|
+
name: name || tool.displayName,
|
|
153
|
+
logger: this.logger,
|
|
154
|
+
options: claudeOptions
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
this.sessions.set(id, session);
|
|
158
|
+
session.on('event', event => this.emit('claude-event', { sessionId: id, event, session }));
|
|
159
|
+
session.on('exit', () => this.handleExit(session));
|
|
160
|
+
this.logSessionDiagnostics('claude-session-created', session, {}, { compact: true });
|
|
161
|
+
|
|
162
|
+
return session;
|
|
163
|
+
}
|
|
164
|
+
|
|
128
165
|
write(id, data) {
|
|
129
166
|
const session = this.get(id);
|
|
130
167
|
if (!session) return false;
|
|
131
168
|
this.markSessionInput(session, data);
|
|
169
|
+
if (session.kind === 'claude-structured') return session.write(data);
|
|
132
170
|
return session.ptyManager.write(data);
|
|
133
171
|
}
|
|
134
172
|
|
|
173
|
+
sendClaudeInput(id, text) {
|
|
174
|
+
const session = this.get(id);
|
|
175
|
+
if (!session || session.kind !== 'claude-structured') return false;
|
|
176
|
+
this.markSessionInput(session, text);
|
|
177
|
+
return session.sendUserMessage(text);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
respondClaudePermission(id, permissionId, approved, action = null) {
|
|
181
|
+
const session = this.get(id);
|
|
182
|
+
if (!session || session.kind !== 'claude-structured') return false;
|
|
183
|
+
return session.respondPermission(permissionId, approved, action);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
updateClaudeSettings(id, settings) {
|
|
187
|
+
const session = this.get(id);
|
|
188
|
+
if (!session || session.kind !== 'claude-structured') return null;
|
|
189
|
+
return session.updateSettings(settings || {});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
abortClaude(id) {
|
|
193
|
+
const session = this.get(id);
|
|
194
|
+
if (!session || session.kind !== 'claude-structured') return false;
|
|
195
|
+
return session.abort('Aborted by user');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
resumeClaude(id, resumeSessionId) {
|
|
199
|
+
const session = this.get(id);
|
|
200
|
+
if (!session || session.kind !== 'claude-structured') return false;
|
|
201
|
+
const historyMessages = this.readClaudeTranscriptMessages(this.getSessionWorkingDirectory(session), resumeSessionId);
|
|
202
|
+
return session.selectResumeSession(resumeSessionId, historyMessages);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
listClaudeResumeSessions(id) {
|
|
206
|
+
const session = this.get(id);
|
|
207
|
+
if (!session || session.kind !== 'claude-structured') return null;
|
|
208
|
+
return this.scanClaudeProjectSessions(this.getSessionWorkingDirectory(session));
|
|
209
|
+
}
|
|
210
|
+
|
|
135
211
|
resize(id, cols, rows) {
|
|
136
212
|
const session = this.get(id);
|
|
137
213
|
if (!session) return false;
|
|
214
|
+
if (session.kind === 'claude-structured') return true;
|
|
138
215
|
if (session.renderedHistory) {
|
|
139
216
|
session.renderedHistory.resize(cols, rows);
|
|
140
217
|
}
|
|
@@ -145,6 +222,7 @@ class SessionManager extends EventEmitter {
|
|
|
145
222
|
redraw(id, cols, rows) {
|
|
146
223
|
const session = this.get(id);
|
|
147
224
|
if (!session) return false;
|
|
225
|
+
if (session.kind === 'claude-structured') return true;
|
|
148
226
|
if (session.renderedHistory) {
|
|
149
227
|
session.renderedHistory.resize(cols, rows);
|
|
150
228
|
}
|
|
@@ -162,6 +240,11 @@ class SessionManager extends EventEmitter {
|
|
|
162
240
|
markCompletionRead(id) {
|
|
163
241
|
const session = this.get(id);
|
|
164
242
|
if (!session) return null;
|
|
243
|
+
if (session.kind === 'claude-structured') {
|
|
244
|
+
session.markCompletionRead();
|
|
245
|
+
this.logSessionDiagnostics('completion-read', session, {}, { compact: true });
|
|
246
|
+
return session;
|
|
247
|
+
}
|
|
165
248
|
session.hasUnreadCompletion = false;
|
|
166
249
|
session.awaitingCompletion = false;
|
|
167
250
|
session.isThinking = false;
|
|
@@ -288,6 +371,10 @@ class SessionManager extends EventEmitter {
|
|
|
288
371
|
this.clearTimedInputs(session);
|
|
289
372
|
this.logSessionDiagnostics('session-deleted', session, {}, { compact: true });
|
|
290
373
|
this.disposeSessionHistory(session);
|
|
374
|
+
if (session.kind === 'claude-structured') {
|
|
375
|
+
session.ptyManager.kill();
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
291
378
|
session.ptyManager.kill();
|
|
292
379
|
this.sessions.delete(id);
|
|
293
380
|
this.emit('exit', { sessionId: id, session });
|
|
@@ -307,6 +394,7 @@ class SessionManager extends EventEmitter {
|
|
|
307
394
|
getHistory(id) {
|
|
308
395
|
const session = this.get(id);
|
|
309
396
|
if (!session) return null;
|
|
397
|
+
if (session.kind === 'claude-structured') return session.getHistory();
|
|
310
398
|
const historySource = session.renderedHistory || session.textHistory;
|
|
311
399
|
return {
|
|
312
400
|
success: true,
|
|
@@ -321,6 +409,7 @@ class SessionManager extends EventEmitter {
|
|
|
321
409
|
getCatchupOutput(id) {
|
|
322
410
|
const session = this.get(id);
|
|
323
411
|
if (!session) return null;
|
|
412
|
+
if (session.kind === 'claude-structured') return session.getCatchupOutput();
|
|
324
413
|
|
|
325
414
|
const bufferHistory = session.buffer.getAfter(0);
|
|
326
415
|
if (bufferHistory.length > 0) {
|
|
@@ -350,6 +439,201 @@ class SessionManager extends EventEmitter {
|
|
|
350
439
|
return session ? this.getSessionDiagnostics(session) : null;
|
|
351
440
|
}
|
|
352
441
|
|
|
442
|
+
getClaudeSnapshot(id) {
|
|
443
|
+
const session = this.get(id);
|
|
444
|
+
if (!session || session.kind !== 'claude-structured') return null;
|
|
445
|
+
return session.snapshot();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
scanClaudeProjectSessions(workingDirectory) {
|
|
449
|
+
const projectDir = this.getClaudeProjectDir(workingDirectory);
|
|
450
|
+
if (!projectDir || !fs.existsSync(projectDir)) return [];
|
|
451
|
+
const files = fs.readdirSync(projectDir)
|
|
452
|
+
.filter(file => /^[0-9a-f-]{36}\.jsonl$/i.test(file))
|
|
453
|
+
.map(file => {
|
|
454
|
+
const fullPath = path.join(projectDir, file);
|
|
455
|
+
const stat = fs.statSync(fullPath);
|
|
456
|
+
return {
|
|
457
|
+
id: file.replace(/\.jsonl$/i, ''),
|
|
458
|
+
path: fullPath,
|
|
459
|
+
mtimeMs: stat.mtimeMs,
|
|
460
|
+
size: stat.size
|
|
461
|
+
};
|
|
462
|
+
})
|
|
463
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
464
|
+
.slice(0, 40);
|
|
465
|
+
|
|
466
|
+
return files.map(file => this.describeClaudeSessionFile(file));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
getClaudeProjectDir(workingDirectory) {
|
|
470
|
+
const cwd = path.resolve(workingDirectory || this.baseDir);
|
|
471
|
+
const encoded = cwd.replace(/[^a-zA-Z0-9]/g, '-');
|
|
472
|
+
return path.join(os.homedir(), '.claude', 'projects', encoded);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
describeClaudeSessionFile(file) {
|
|
476
|
+
let cwd = '';
|
|
477
|
+
let firstText = '';
|
|
478
|
+
let lastText = '';
|
|
479
|
+
try {
|
|
480
|
+
const content = fs.readFileSync(file.path, 'utf8');
|
|
481
|
+
const lines = content.split('\n').filter(Boolean);
|
|
482
|
+
for (const line of lines) {
|
|
483
|
+
const parsed = this.parseClaudeJsonLine(line);
|
|
484
|
+
if (!parsed) continue;
|
|
485
|
+
if (!cwd && typeof parsed.cwd === 'string') cwd = parsed.cwd;
|
|
486
|
+
const text = this.extractClaudeTranscriptText(parsed);
|
|
487
|
+
if (!text) continue;
|
|
488
|
+
if (!firstText) firstText = text;
|
|
489
|
+
lastText = text;
|
|
490
|
+
}
|
|
491
|
+
} catch (error) {
|
|
492
|
+
this.logger.debugInfo?.(`[claude-resume] Failed to read ${file.path}: ${error.message}`);
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
id: file.id,
|
|
496
|
+
cwd,
|
|
497
|
+
updatedAt: file.mtimeMs,
|
|
498
|
+
size: file.size,
|
|
499
|
+
firstText: firstText ? previewText(firstText, 120) : '',
|
|
500
|
+
lastText: lastText ? previewText(lastText, 160) : ''
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
readClaudeTranscriptMessages(workingDirectory, resumeSessionId) {
|
|
505
|
+
const id = String(resumeSessionId || '').trim();
|
|
506
|
+
if (!/^[0-9a-f-]{36}$/i.test(id)) return [];
|
|
507
|
+
const filePath = path.join(this.getClaudeProjectDir(workingDirectory), `${id}.jsonl`);
|
|
508
|
+
if (!fs.existsSync(filePath)) return [];
|
|
509
|
+
|
|
510
|
+
const messages = [];
|
|
511
|
+
try {
|
|
512
|
+
const lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
|
|
513
|
+
for (const line of lines) {
|
|
514
|
+
const record = this.parseClaudeJsonLine(line);
|
|
515
|
+
if (!record || record.isSidechain) continue;
|
|
516
|
+
messages.push(...this.mapClaudeTranscriptRecord(record));
|
|
517
|
+
}
|
|
518
|
+
} catch (error) {
|
|
519
|
+
this.logger.debugInfo?.(`[claude-resume] Failed to backfill ${filePath}: ${error.message}`);
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const maxMessages = 1000;
|
|
524
|
+
const visible = messages.filter(Boolean).slice(-maxMessages);
|
|
525
|
+
if (messages.length > maxMessages) {
|
|
526
|
+
visible.unshift({
|
|
527
|
+
id: uuidv4(),
|
|
528
|
+
kind: 'event',
|
|
529
|
+
level: 'info',
|
|
530
|
+
text: `Showing the latest ${maxMessages} resumed transcript items.`,
|
|
531
|
+
createdAt: Date.now()
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
return visible;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
mapClaudeTranscriptRecord(record) {
|
|
538
|
+
const createdAt = Number.isFinite(Date.parse(record.timestamp)) ? Date.parse(record.timestamp) : Date.now();
|
|
539
|
+
const message = record.message || {};
|
|
540
|
+
const content = message.content;
|
|
541
|
+
if (record.type === 'user') {
|
|
542
|
+
if (typeof content === 'string') {
|
|
543
|
+
const text = content.trim();
|
|
544
|
+
return text ? [{ id: uuidv4(), kind: 'user', text, createdAt }] : [];
|
|
545
|
+
}
|
|
546
|
+
if (Array.isArray(content)) {
|
|
547
|
+
return content.flatMap(item => {
|
|
548
|
+
if (!item || typeof item !== 'object') return [];
|
|
549
|
+
if (item.type === 'tool_result') {
|
|
550
|
+
const text = this.textFromClaudeContent(item.content).trim();
|
|
551
|
+
return text ? [{
|
|
552
|
+
id: uuidv4(),
|
|
553
|
+
kind: 'tool-result',
|
|
554
|
+
toolUseId: item.tool_use_id,
|
|
555
|
+
text,
|
|
556
|
+
isError: Boolean(item.is_error),
|
|
557
|
+
createdAt
|
|
558
|
+
}] : [];
|
|
559
|
+
}
|
|
560
|
+
if (item.type === 'text' && typeof item.text === 'string' && item.text.trim()) {
|
|
561
|
+
return [{ id: uuidv4(), kind: 'user', text: item.text.trim(), createdAt }];
|
|
562
|
+
}
|
|
563
|
+
return [];
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (record.type === 'assistant' && Array.isArray(content)) {
|
|
569
|
+
const mapped = [];
|
|
570
|
+
const text = this.textFromClaudeContent(content).trim();
|
|
571
|
+
if (text) mapped.push({ id: uuidv4(), kind: 'assistant', text, createdAt });
|
|
572
|
+
for (const item of content) {
|
|
573
|
+
if (!item || item.type !== 'tool_use') continue;
|
|
574
|
+
mapped.push({
|
|
575
|
+
id: uuidv4(),
|
|
576
|
+
kind: 'tool',
|
|
577
|
+
name: item.name || 'tool',
|
|
578
|
+
summary: this.summarizeClaudeToolInput(item.input),
|
|
579
|
+
input: item.input,
|
|
580
|
+
toolUseId: item.id,
|
|
581
|
+
createdAt
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
return mapped;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (record.type === 'summary' && typeof record.summary === 'string' && record.summary.trim()) {
|
|
588
|
+
return [{ id: uuidv4(), kind: 'event', level: 'info', text: `Summary: ${record.summary.trim()}`, createdAt }];
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return [];
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
textFromClaudeContent(content) {
|
|
595
|
+
if (typeof content === 'string') return content;
|
|
596
|
+
if (!Array.isArray(content)) return '';
|
|
597
|
+
return content.map(item => {
|
|
598
|
+
if (!item || typeof item !== 'object') return '';
|
|
599
|
+
if (item.type === 'text' && typeof item.text === 'string') return item.text;
|
|
600
|
+
if (item.type === 'tool_result') return this.textFromClaudeContent(item.content);
|
|
601
|
+
return '';
|
|
602
|
+
}).filter(Boolean).join('\n');
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
summarizeClaudeToolInput(input) {
|
|
606
|
+
if (!input || typeof input !== 'object') return '';
|
|
607
|
+
if (typeof input.command === 'string') return input.command;
|
|
608
|
+
if (typeof input.file_path === 'string') return input.file_path;
|
|
609
|
+
if (typeof input.path === 'string') return input.path;
|
|
610
|
+
const serialized = JSON.stringify(input);
|
|
611
|
+
return serialized.length > 240 ? serialized.slice(0, 240) + '...' : serialized;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
parseClaudeJsonLine(line) {
|
|
615
|
+
try {
|
|
616
|
+
return JSON.parse(line);
|
|
617
|
+
} catch (_) {
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
extractClaudeTranscriptText(record) {
|
|
623
|
+
const message = record && record.message;
|
|
624
|
+
const content = message && message.content;
|
|
625
|
+
if (typeof content === 'string') return content.trim();
|
|
626
|
+
if (Array.isArray(content)) {
|
|
627
|
+
return content.map(item => {
|
|
628
|
+
if (!item || typeof item !== 'object') return '';
|
|
629
|
+
if (item.type === 'text' && typeof item.text === 'string') return item.text;
|
|
630
|
+
if (item.type === 'tool_use') return `[tool] ${item.name || 'tool'}`;
|
|
631
|
+
return '';
|
|
632
|
+
}).filter(Boolean).join('\n').trim();
|
|
633
|
+
}
|
|
634
|
+
return '';
|
|
635
|
+
}
|
|
636
|
+
|
|
353
637
|
logHistoryRequest(id, req) {
|
|
354
638
|
const session = this.get(id);
|
|
355
639
|
if (!session) return;
|
|
@@ -457,6 +741,14 @@ class SessionManager extends EventEmitter {
|
|
|
457
741
|
markSessionInput(session, data) {
|
|
458
742
|
if (typeof data !== 'string' || data.length === 0) return;
|
|
459
743
|
session.inputSeq = (session.inputSeq || 0) + 1;
|
|
744
|
+
if (session.kind === 'claude-structured') {
|
|
745
|
+
session.hasUnreadCompletion = false;
|
|
746
|
+
this.logSessionDiagnostics('session-input', session, {
|
|
747
|
+
inputSeq: session.inputSeq,
|
|
748
|
+
inputPreview: previewText(data)
|
|
749
|
+
}, { compact: true });
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
460
752
|
session.awaitingCompletion = true;
|
|
461
753
|
session.isThinking = false;
|
|
462
754
|
session.hasUnreadCompletion = false;
|
|
@@ -468,6 +760,7 @@ class SessionManager extends EventEmitter {
|
|
|
468
760
|
}
|
|
469
761
|
|
|
470
762
|
disposeSessionHistory(session) {
|
|
763
|
+
if (session.kind === 'claude-structured') return;
|
|
471
764
|
if (session.renderedHistory) {
|
|
472
765
|
session.renderedHistory.dispose();
|
|
473
766
|
session.renderedHistory = null;
|
|
@@ -483,6 +776,21 @@ class SessionManager extends EventEmitter {
|
|
|
483
776
|
}
|
|
484
777
|
|
|
485
778
|
getSessionDiagnostics(session, extra = {}) {
|
|
779
|
+
if (session.kind === 'claude-structured') {
|
|
780
|
+
return {
|
|
781
|
+
sessionId: session.id,
|
|
782
|
+
sessionName: session.name,
|
|
783
|
+
toolKey: session.tool.key,
|
|
784
|
+
kind: session.kind,
|
|
785
|
+
historyMode: 'structured',
|
|
786
|
+
status: session.status,
|
|
787
|
+
workingDirectory: this.getSessionWorkingDirectory(session),
|
|
788
|
+
messages: session.messages.length,
|
|
789
|
+
pendingPermissions: session.pendingPermissions.size,
|
|
790
|
+
timedInputCount: session.timedInputs ? session.timedInputs.size : 0,
|
|
791
|
+
...extra
|
|
792
|
+
};
|
|
793
|
+
}
|
|
486
794
|
return {
|
|
487
795
|
sessionId: session.id,
|
|
488
796
|
sessionName: session.name,
|
|
@@ -497,6 +805,9 @@ class SessionManager extends EventEmitter {
|
|
|
497
805
|
}
|
|
498
806
|
|
|
499
807
|
getCompactSessionDiagnostics(session, extra = {}) {
|
|
808
|
+
if (session.kind === 'claude-structured') {
|
|
809
|
+
return this.getSessionDiagnostics(session, extra);
|
|
810
|
+
}
|
|
500
811
|
const buffer = session.buffer.getDebugSnapshot();
|
|
501
812
|
const textHistory = session.textHistory.getDebugSnapshot();
|
|
502
813
|
const renderedHistory = session.renderedHistory ? session.renderedHistory.getDebugSnapshot() : null;
|
|
@@ -550,6 +861,10 @@ class SessionManager extends EventEmitter {
|
|
|
550
861
|
: this.getSessionDiagnostics(session, extra);
|
|
551
862
|
this.logger.debugInfo(`[history-debug] ${reason} ${JSON.stringify(payload)}`);
|
|
552
863
|
}
|
|
864
|
+
|
|
865
|
+
getSessionWorkingDirectory(session) {
|
|
866
|
+
return session.workingDir || (session.ptyManager && session.ptyManager.workingDir) || this.baseDir;
|
|
867
|
+
}
|
|
553
868
|
}
|
|
554
869
|
|
|
555
870
|
module.exports = SessionManager;
|