glad-web 1.0.18 → 1.0.20
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/README.md +2 -3
- package/README.zh-CN.md +2 -3
- package/lib/ai-tools/registry.js +0 -9
- package/lib/claude/config.js +82 -0
- package/lib/claude/structured-session.js +516 -0
- package/lib/commands/web.js +104 -12
- package/lib/session/session-manager.js +443 -3
- package/lib/web/index.html +1114 -11
- package/package.json +2 -1
|
@@ -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,8 +38,12 @@ class SessionManager extends EventEmitter {
|
|
|
36
38
|
tool: session.tool.displayName,
|
|
37
39
|
startTime: session.startTime,
|
|
38
40
|
toolKey: session.tool.key,
|
|
39
|
-
workingDirectory: session
|
|
40
|
-
|
|
41
|
+
workingDirectory: this.getSessionWorkingDirectory(session),
|
|
42
|
+
mode: session.kind === 'claude-structured' ? 'structured' : 'terminal',
|
|
43
|
+
hasUnreadCompletion: Boolean(session.hasUnreadCompletion),
|
|
44
|
+
timedInputCount: session.timedInputs
|
|
45
|
+
? Array.from(session.timedInputs.values()).filter(item => item.sendAt > Date.now()).length
|
|
46
|
+
: 0
|
|
41
47
|
}));
|
|
42
48
|
}
|
|
43
49
|
|
|
@@ -49,7 +55,7 @@ class SessionManager extends EventEmitter {
|
|
|
49
55
|
return this.sessions.has(id);
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
create({ toolKey, workingDirectory, name }) {
|
|
58
|
+
create({ toolKey, workingDirectory, name, claudeOptions }) {
|
|
53
59
|
this.logger.info(`Creating session: toolKey=${toolKey || ''}, workingDirectory=${workingDirectory || '(default)'}`);
|
|
54
60
|
const tool = getToolByKey(toolKey);
|
|
55
61
|
if (!tool) {
|
|
@@ -59,6 +65,10 @@ class SessionManager extends EventEmitter {
|
|
|
59
65
|
throw err;
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
if (tool.key === 'claude-code') {
|
|
69
|
+
return this.createClaudeStructuredSession({ tool, workingDirectory, name, claudeOptions });
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
const id = uuidv4();
|
|
63
73
|
const buffer = new CircularBuffer(500000);
|
|
64
74
|
const textHistory = new TextHistory({ maxBytes: 20 * 1024 * 1024, debugLabel: id });
|
|
@@ -99,6 +109,7 @@ class SessionManager extends EventEmitter {
|
|
|
99
109
|
resizeOwner: null,
|
|
100
110
|
hasConnectedWebClient: false,
|
|
101
111
|
hasUnreadCompletion: false,
|
|
112
|
+
timedInputs: new Map(),
|
|
102
113
|
write: data => this.write(id, data),
|
|
103
114
|
isRunning: () => this.has(id) && ptyManager.isRunning(),
|
|
104
115
|
kill: () => this.kill(id)
|
|
@@ -121,16 +132,86 @@ class SessionManager extends EventEmitter {
|
|
|
121
132
|
return session;
|
|
122
133
|
}
|
|
123
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
|
+
|
|
124
165
|
write(id, data) {
|
|
125
166
|
const session = this.get(id);
|
|
126
167
|
if (!session) return false;
|
|
127
168
|
this.markSessionInput(session, data);
|
|
169
|
+
if (session.kind === 'claude-structured') return session.write(data);
|
|
128
170
|
return session.ptyManager.write(data);
|
|
129
171
|
}
|
|
130
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) {
|
|
181
|
+
const session = this.get(id);
|
|
182
|
+
if (!session || session.kind !== 'claude-structured') return false;
|
|
183
|
+
return session.respondPermission(permissionId, approved);
|
|
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
|
+
|
|
131
211
|
resize(id, cols, rows) {
|
|
132
212
|
const session = this.get(id);
|
|
133
213
|
if (!session) return false;
|
|
214
|
+
if (session.kind === 'claude-structured') return true;
|
|
134
215
|
if (session.renderedHistory) {
|
|
135
216
|
session.renderedHistory.resize(cols, rows);
|
|
136
217
|
}
|
|
@@ -141,6 +222,7 @@ class SessionManager extends EventEmitter {
|
|
|
141
222
|
redraw(id, cols, rows) {
|
|
142
223
|
const session = this.get(id);
|
|
143
224
|
if (!session) return false;
|
|
225
|
+
if (session.kind === 'claude-structured') return true;
|
|
144
226
|
if (session.renderedHistory) {
|
|
145
227
|
session.renderedHistory.resize(cols, rows);
|
|
146
228
|
}
|
|
@@ -158,6 +240,11 @@ class SessionManager extends EventEmitter {
|
|
|
158
240
|
markCompletionRead(id) {
|
|
159
241
|
const session = this.get(id);
|
|
160
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
|
+
}
|
|
161
248
|
session.hasUnreadCompletion = false;
|
|
162
249
|
session.awaitingCompletion = false;
|
|
163
250
|
session.isThinking = false;
|
|
@@ -167,12 +254,127 @@ class SessionManager extends EventEmitter {
|
|
|
167
254
|
return session;
|
|
168
255
|
}
|
|
169
256
|
|
|
257
|
+
listTimedInputs(id) {
|
|
258
|
+
const session = this.get(id);
|
|
259
|
+
if (!session) return null;
|
|
260
|
+
return Array.from(session.timedInputs.values()).map(item => ({
|
|
261
|
+
id: item.id,
|
|
262
|
+
text: item.text,
|
|
263
|
+
sendAt: item.sendAt,
|
|
264
|
+
createdAt: item.createdAt
|
|
265
|
+
})).sort((a, b) => a.sendAt - b.sendAt);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
scheduleTimedInput(id, input = {}) {
|
|
269
|
+
const session = this.get(id);
|
|
270
|
+
if (!session) return null;
|
|
271
|
+
|
|
272
|
+
const { text, sendAt, delay } = this.validateTimedInput(input);
|
|
273
|
+
|
|
274
|
+
const item = {
|
|
275
|
+
id: uuidv4(),
|
|
276
|
+
text,
|
|
277
|
+
sendAt,
|
|
278
|
+
createdAt: Date.now(),
|
|
279
|
+
timer: null
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
item.timer = setTimeout(() => {
|
|
283
|
+
this.executeTimedInput(session.id, item.id);
|
|
284
|
+
}, delay);
|
|
285
|
+
session.timedInputs.set(item.id, item);
|
|
286
|
+
return {
|
|
287
|
+
id: item.id,
|
|
288
|
+
text: item.text,
|
|
289
|
+
sendAt: item.sendAt,
|
|
290
|
+
createdAt: item.createdAt
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
updateTimedInput(id, inputId, input = {}) {
|
|
295
|
+
const session = this.get(id);
|
|
296
|
+
if (!session) return null;
|
|
297
|
+
const item = session.timedInputs.get(inputId);
|
|
298
|
+
if (!item) return false;
|
|
299
|
+
|
|
300
|
+
const { text, sendAt, delay } = this.validateTimedInput(input);
|
|
301
|
+
clearTimeout(item.timer);
|
|
302
|
+
item.text = text;
|
|
303
|
+
item.sendAt = sendAt;
|
|
304
|
+
item.updatedAt = Date.now();
|
|
305
|
+
item.timer = setTimeout(() => {
|
|
306
|
+
this.executeTimedInput(session.id, item.id);
|
|
307
|
+
}, delay);
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
id: item.id,
|
|
311
|
+
text: item.text,
|
|
312
|
+
sendAt: item.sendAt,
|
|
313
|
+
createdAt: item.createdAt,
|
|
314
|
+
updatedAt: item.updatedAt
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
validateTimedInput(input = {}) {
|
|
319
|
+
const text = String(input.text || '');
|
|
320
|
+
const sendAt = Number(input.sendAt);
|
|
321
|
+
if (!text.trim()) {
|
|
322
|
+
const err = new Error('Text is required');
|
|
323
|
+
err.statusCode = 400;
|
|
324
|
+
throw err;
|
|
325
|
+
}
|
|
326
|
+
if (!Number.isFinite(sendAt) || sendAt <= Date.now()) {
|
|
327
|
+
const err = new Error('Send time must be in the future');
|
|
328
|
+
err.statusCode = 400;
|
|
329
|
+
throw err;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const maxDelay = 30 * 24 * 60 * 60 * 1000;
|
|
333
|
+
const delay = sendAt - Date.now();
|
|
334
|
+
if (delay > maxDelay) {
|
|
335
|
+
const err = new Error('Send time must be within 30 days');
|
|
336
|
+
err.statusCode = 400;
|
|
337
|
+
throw err;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return { text, sendAt, delay };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
cancelTimedInput(id, inputId) {
|
|
344
|
+
const session = this.get(id);
|
|
345
|
+
if (!session) return null;
|
|
346
|
+
const item = session.timedInputs.get(inputId);
|
|
347
|
+
if (!item) return false;
|
|
348
|
+
clearTimeout(item.timer);
|
|
349
|
+
session.timedInputs.delete(inputId);
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
executeTimedInput(id, inputId) {
|
|
354
|
+
const session = this.get(id);
|
|
355
|
+
if (!session) return false;
|
|
356
|
+
const item = session.timedInputs.get(inputId);
|
|
357
|
+
if (!item) return false;
|
|
358
|
+
session.timedInputs.delete(inputId);
|
|
359
|
+
const formatted = item.text.replace(/\n/g, '\r');
|
|
360
|
+
this.write(id, formatted);
|
|
361
|
+
setTimeout(() => {
|
|
362
|
+
if (this.has(id)) this.write(id, '\r');
|
|
363
|
+
}, 1000);
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
|
|
170
367
|
kill(id) {
|
|
171
368
|
const session = this.get(id);
|
|
172
369
|
if (!session) return false;
|
|
173
370
|
clearTimeout(session.completionTimer);
|
|
371
|
+
this.clearTimedInputs(session);
|
|
174
372
|
this.logSessionDiagnostics('session-deleted', session, {}, { compact: true });
|
|
175
373
|
this.disposeSessionHistory(session);
|
|
374
|
+
if (session.kind === 'claude-structured') {
|
|
375
|
+
session.ptyManager.kill();
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
176
378
|
session.ptyManager.kill();
|
|
177
379
|
this.sessions.delete(id);
|
|
178
380
|
this.emit('exit', { sessionId: id, session });
|
|
@@ -182,6 +384,7 @@ class SessionManager extends EventEmitter {
|
|
|
182
384
|
killAll() {
|
|
183
385
|
for (const session of this.sessions.values()) {
|
|
184
386
|
clearTimeout(session.completionTimer);
|
|
387
|
+
this.clearTimedInputs(session);
|
|
185
388
|
this.disposeSessionHistory(session);
|
|
186
389
|
session.ptyManager.kill();
|
|
187
390
|
}
|
|
@@ -191,6 +394,7 @@ class SessionManager extends EventEmitter {
|
|
|
191
394
|
getHistory(id) {
|
|
192
395
|
const session = this.get(id);
|
|
193
396
|
if (!session) return null;
|
|
397
|
+
if (session.kind === 'claude-structured') return session.getHistory();
|
|
194
398
|
const historySource = session.renderedHistory || session.textHistory;
|
|
195
399
|
return {
|
|
196
400
|
success: true,
|
|
@@ -205,6 +409,7 @@ class SessionManager extends EventEmitter {
|
|
|
205
409
|
getCatchupOutput(id) {
|
|
206
410
|
const session = this.get(id);
|
|
207
411
|
if (!session) return null;
|
|
412
|
+
if (session.kind === 'claude-structured') return session.getCatchupOutput();
|
|
208
413
|
|
|
209
414
|
const bufferHistory = session.buffer.getAfter(0);
|
|
210
415
|
if (bufferHistory.length > 0) {
|
|
@@ -234,6 +439,201 @@ class SessionManager extends EventEmitter {
|
|
|
234
439
|
return session ? this.getSessionDiagnostics(session) : null;
|
|
235
440
|
}
|
|
236
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
|
+
|
|
237
637
|
logHistoryRequest(id, req) {
|
|
238
638
|
const session = this.get(id);
|
|
239
639
|
if (!session) return;
|
|
@@ -332,6 +732,7 @@ class SessionManager extends EventEmitter {
|
|
|
332
732
|
if (!this.sessions.has(session.id)) return;
|
|
333
733
|
this.logger.info(`Session ${session.id} (${session.name}) exited.`);
|
|
334
734
|
clearTimeout(session.completionTimer);
|
|
735
|
+
this.clearTimedInputs(session);
|
|
335
736
|
this.disposeSessionHistory(session);
|
|
336
737
|
this.sessions.delete(session.id);
|
|
337
738
|
this.emit('exit', { sessionId: session.id, session });
|
|
@@ -340,6 +741,14 @@ class SessionManager extends EventEmitter {
|
|
|
340
741
|
markSessionInput(session, data) {
|
|
341
742
|
if (typeof data !== 'string' || data.length === 0) return;
|
|
342
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
|
+
}
|
|
343
752
|
session.awaitingCompletion = true;
|
|
344
753
|
session.isThinking = false;
|
|
345
754
|
session.hasUnreadCompletion = false;
|
|
@@ -351,13 +760,37 @@ class SessionManager extends EventEmitter {
|
|
|
351
760
|
}
|
|
352
761
|
|
|
353
762
|
disposeSessionHistory(session) {
|
|
763
|
+
if (session.kind === 'claude-structured') return;
|
|
354
764
|
if (session.renderedHistory) {
|
|
355
765
|
session.renderedHistory.dispose();
|
|
356
766
|
session.renderedHistory = null;
|
|
357
767
|
}
|
|
358
768
|
}
|
|
359
769
|
|
|
770
|
+
clearTimedInputs(session) {
|
|
771
|
+
if (!session || !session.timedInputs) return;
|
|
772
|
+
for (const item of session.timedInputs.values()) {
|
|
773
|
+
clearTimeout(item.timer);
|
|
774
|
+
}
|
|
775
|
+
session.timedInputs.clear();
|
|
776
|
+
}
|
|
777
|
+
|
|
360
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
|
+
}
|
|
361
794
|
return {
|
|
362
795
|
sessionId: session.id,
|
|
363
796
|
sessionName: session.name,
|
|
@@ -372,6 +805,9 @@ class SessionManager extends EventEmitter {
|
|
|
372
805
|
}
|
|
373
806
|
|
|
374
807
|
getCompactSessionDiagnostics(session, extra = {}) {
|
|
808
|
+
if (session.kind === 'claude-structured') {
|
|
809
|
+
return this.getSessionDiagnostics(session, extra);
|
|
810
|
+
}
|
|
375
811
|
const buffer = session.buffer.getDebugSnapshot();
|
|
376
812
|
const textHistory = session.textHistory.getDebugSnapshot();
|
|
377
813
|
const renderedHistory = session.renderedHistory ? session.renderedHistory.getDebugSnapshot() : null;
|
|
@@ -425,6 +861,10 @@ class SessionManager extends EventEmitter {
|
|
|
425
861
|
: this.getSessionDiagnostics(session, extra);
|
|
426
862
|
this.logger.debugInfo(`[history-debug] ${reason} ${JSON.stringify(payload)}`);
|
|
427
863
|
}
|
|
864
|
+
|
|
865
|
+
getSessionWorkingDirectory(session) {
|
|
866
|
+
return session.workingDir || (session.ptyManager && session.ptyManager.workingDir) || this.baseDir;
|
|
867
|
+
}
|
|
428
868
|
}
|
|
429
869
|
|
|
430
870
|
module.exports = SessionManager;
|