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.
@@ -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.ptyManager.workingDir, hash);
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.ptyManager.workingDir, hash);
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.ptyManager.workingDir, req.query.maxCount);
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.ptyManager.workingDir);
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.ptyManager.workingDir, isStaged);
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.ptyManager.workingDir, filePath, isStaged);
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.ptyManager.workingDir || '';
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.ptyManager.workingDir || '';
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 = ['antigravity', 'claude-code', 'codex'].includes(session.tool.key)
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.ptyManager.workingDir,
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;