glad-web 1.0.46 → 2.0.2

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.
Files changed (74) hide show
  1. package/README.md +4 -192
  2. package/THIRD_PARTY_NOTICES.md +27 -0
  3. package/bin/glad.cjs +56 -0
  4. package/package.json +19 -61
  5. package/README.zh-CN.md +0 -198
  6. package/assets/logo.svg +0 -43
  7. package/bin/cli.js +0 -65
  8. package/lib/ai-tools/demo/enhanced-demo.js +0 -625
  9. package/lib/ai-tools/demo/index.js +0 -24
  10. package/lib/ai-tools/demo/responses.js +0 -88
  11. package/lib/ai-tools/detector.js +0 -76
  12. package/lib/ai-tools/registry.js +0 -300
  13. package/lib/claude/cli-usage.js +0 -95
  14. package/lib/claude/config.js +0 -82
  15. package/lib/claude/structured-session.js +0 -884
  16. package/lib/claude/transcript-repository.js +0 -216
  17. package/lib/codex/image-store.js +0 -174
  18. package/lib/codex/structured-session.js +0 -1590
  19. package/lib/commands/config.js +0 -78
  20. package/lib/commands/tools.js +0 -128
  21. package/lib/commands/web.js +0 -605
  22. package/lib/config/constants.js +0 -17
  23. package/lib/config/manager.js +0 -108
  24. package/lib/git/service.js +0 -83
  25. package/lib/notifications/message-formatter.js +0 -94
  26. package/lib/notifications/notification-service.js +0 -143
  27. package/lib/notifications/serverchan-client.js +0 -58
  28. package/lib/notifications/serverchan-settings-store.js +0 -115
  29. package/lib/schedule/job-runner.js +0 -162
  30. package/lib/schedule/job-store.js +0 -167
  31. package/lib/schedule/key-sequences.js +0 -49
  32. package/lib/schedule/scheduler-service.js +0 -39
  33. package/lib/server/routes/notifications.js +0 -52
  34. package/lib/server/routes/providers.js +0 -114
  35. package/lib/server/routes/schedules.js +0 -54
  36. package/lib/server/routes/skillhub.js +0 -104
  37. package/lib/server/routes/usage.js +0 -23
  38. package/lib/server/routes/workspace.js +0 -77
  39. package/lib/session/buffer.js +0 -102
  40. package/lib/session/file-attachment-store.js +0 -168
  41. package/lib/session/pty-manager.js +0 -255
  42. package/lib/session/rendered-history.js +0 -225
  43. package/lib/session/session-manager.js +0 -1032
  44. package/lib/session/text-history.js +0 -274
  45. package/lib/skillhub/client.js +0 -121
  46. package/lib/skillhub/settings-store.js +0 -168
  47. package/lib/skillhub/skill-installer.js +0 -320
  48. package/lib/usage/ccusage-runner.js +0 -128
  49. package/lib/usage/source-catalog.js +0 -26
  50. package/lib/usage/usage-service.js +0 -226
  51. package/lib/utils/logger.js +0 -74
  52. package/lib/utils/pid.js +0 -67
  53. package/lib/utils/validation.js +0 -53
  54. package/lib/web/bootstrap.js +0 -34
  55. package/lib/web/claude.js +0 -1150
  56. package/lib/web/codex.js +0 -1045
  57. package/lib/web/composer.js +0 -493
  58. package/lib/web/core.js +0 -385
  59. package/lib/web/git.js +0 -535
  60. package/lib/web/gitgraph.js +0 -293
  61. package/lib/web/index.html +0 -547
  62. package/lib/web/layout.js +0 -69
  63. package/lib/web/notifications.js +0 -164
  64. package/lib/web/schedules.js +0 -245
  65. package/lib/web/session.js +0 -361
  66. package/lib/web/shell.js +0 -74
  67. package/lib/web/skillhub.js +0 -197
  68. package/lib/web/styles.css +0 -932
  69. package/lib/web/terminal-scroll.js +0 -81
  70. package/lib/web/theme.js +0 -60
  71. package/lib/web/timed-inputs.js +0 -216
  72. package/lib/web/usage.js +0 -323
  73. package/lib/workspace/service.js +0 -77
  74. package/scripts/check-syntax.js +0 -26
@@ -1,1032 +0,0 @@
1
- const { EventEmitter } = require('events');
2
- const path = require('path');
3
- const fs = require('fs');
4
- const { v4: uuidv4 } = require('uuid');
5
- const PTYManager = require('./pty-manager');
6
- const TextHistory = require('./text-history');
7
- const RenderedHistory = require('./rendered-history');
8
- const CircularBuffer = require('./buffer');
9
- const { getToolByKey } = require('../ai-tools/registry');
10
- const ClaudeStructuredSession = require('../claude/structured-session');
11
- const CodexStructuredSession = require('../codex/structured-session');
12
- const ClaudeTranscriptRepository = require('../claude/transcript-repository');
13
- const CodexImageStore = require('../codex/image-store');
14
- const FileAttachmentStore = require('./file-attachment-store');
15
-
16
- function previewText(text, maxChars = 320) {
17
- if (!text) return '';
18
- const normalized = String(text)
19
- .replace(/\r/g, '\\r')
20
- .replace(/\n/g, '\\n')
21
- .replace(/\t/g, '\\t')
22
- .replace(/\x1b/g, '\\x1b');
23
- return normalized.length > maxChars ? normalized.slice(-maxChars) : normalized;
24
- }
25
-
26
- function imageMediaType(name) {
27
- const extension = path.extname(String(name || '')).toLowerCase();
28
- return extension === '.png' ? 'image/png'
29
- : extension === '.jpg' || extension === '.jpeg' ? 'image/jpeg'
30
- : extension === '.gif' ? 'image/gif' : 'image/webp';
31
- }
32
-
33
- function promptWithFileReferences(text, attachments) {
34
- const prompt = String(text || '').trim();
35
- if (!attachments.length) return prompt;
36
- const references = attachments.map(item => `- ${item.name}: ${item.path}`).join('\n');
37
- const note = `The user attached the following local files. Read them if relevant to the request:\n${references}`;
38
- return prompt ? `${prompt}\n\n${note}` : note;
39
- }
40
-
41
- const STRUCTURED_SESSION_KINDS = new Set(['claude-structured', 'codex-structured']);
42
-
43
- function isStructuredSession(session) {
44
- return Boolean(session && STRUCTURED_SESSION_KINDS.has(session.kind));
45
- }
46
-
47
- class SessionManager extends EventEmitter {
48
- constructor({ baseDir, renderHistoryTools, debugHistoryEnabled = false, logger, hasConnectedSessionClient, claudeTranscriptRepository, codexImageStore, fileAttachmentStore, claudeForkSession } = {}) {
49
- super();
50
- this.baseDir = baseDir || process.cwd();
51
- this.renderHistoryTools = renderHistoryTools || new Set();
52
- this.debugHistoryEnabled = debugHistoryEnabled;
53
- this.logger = logger || console;
54
- this.hasConnectedSessionClient = hasConnectedSessionClient || (() => false);
55
- this.sessions = new Map();
56
- this.codexImages = codexImageStore || new CodexImageStore({ logger: this.logger });
57
- this.fileAttachmentStore = fileAttachmentStore || new FileAttachmentStore({ logger: this.logger });
58
- Object.defineProperties(this, {
59
- codexImageRoot: {
60
- get: () => this.codexImages.root,
61
- set: value => { this.codexImages.root = value; }
62
- },
63
- codexImageUploadRoot: {
64
- get: () => this.codexImages.uploadRoot,
65
- set: value => { this.codexImages.uploadRoot = value; }
66
- }
67
- });
68
- this.claudeTranscripts = claudeTranscriptRepository || new ClaudeTranscriptRepository({
69
- baseDir: this.baseDir,
70
- logger: this.logger
71
- });
72
- this.claudeForkSession = claudeForkSession || (async (sessionId, options) => {
73
- const sdk = await import('@anthropic-ai/claude-agent-sdk');
74
- return sdk.forkSession(sessionId, options);
75
- });
76
- }
77
-
78
- list() {
79
- return Array.from(this.sessions.entries()).map(([id, session]) => ({
80
- id,
81
- name: session.name,
82
- tool: session.tool.displayName,
83
- startTime: session.startTime,
84
- toolKey: session.tool.key,
85
- workingDirectory: this.getSessionWorkingDirectory(session),
86
- mode: isStructuredSession(session) ? 'structured' : 'terminal',
87
- hasUnreadCompletion: Boolean(session.hasUnreadCompletion),
88
- serverChanNotificationEnabled: Boolean(session.serverChanNotificationEnabled),
89
- timedInputCount: session.timedInputs
90
- ? Array.from(session.timedInputs.values()).filter(item => item.sendAt > Date.now()).length
91
- : 0
92
- }));
93
- }
94
-
95
- get(id) {
96
- return this.sessions.get(id) || null;
97
- }
98
-
99
- has(id) {
100
- return this.sessions.has(id);
101
- }
102
-
103
- create({ id: requestedId, toolKey, workingDirectory, name, claudeOptions, codexOptions, disposeResources }) {
104
- this.logger.info(`Creating session: toolKey=${toolKey || ''}, workingDirectory=${workingDirectory || '(default)'}`);
105
- const tool = getToolByKey(toolKey);
106
- if (!tool) {
107
- const err = new Error('Invalid tool');
108
- err.statusCode = 400;
109
- this.logger.error(`Create session failed: invalid toolKey=${toolKey || ''}`);
110
- throw err;
111
- }
112
-
113
- if (tool.key === 'claude-code') {
114
- return this.createClaudeStructuredSession({ tool, workingDirectory, name, claudeOptions });
115
- }
116
- if (tool.key === 'codex') {
117
- return this.createCodexStructuredSession({
118
- id: requestedId,
119
- tool,
120
- workingDirectory,
121
- name,
122
- codexOptions,
123
- disposeResources
124
- });
125
- }
126
-
127
- const id = uuidv4();
128
- const buffer = new CircularBuffer(500000);
129
- const textHistory = new TextHistory({ maxBytes: 20 * 1024 * 1024, debugLabel: id });
130
- const historyMode = this.getHistoryModeForTool(tool.key);
131
- const renderedHistory = historyMode === 'rendered'
132
- ? new RenderedHistory({ maxBytes: 20 * 1024 * 1024, debugLabel: id, cols: 80, rows: 24 })
133
- : null;
134
-
135
- const sessionDir = workingDirectory && String(workingDirectory).trim()
136
- ? path.resolve(this.baseDir, String(workingDirectory).trim())
137
- : this.baseDir;
138
-
139
- if (!fs.existsSync(sessionDir)) {
140
- const err = new Error(`Directory does not exist: ${sessionDir}`);
141
- err.statusCode = 400;
142
- this.logger.error(`Create session failed: missing directory ${sessionDir}`);
143
- throw err;
144
- }
145
-
146
- this.logger.info(`Resolved session directory: ${sessionDir}`);
147
-
148
- const ptyManager = new PTYManager(tool, sessionDir, buffer, { silent: true });
149
- const session = {
150
- id,
151
- name: name || tool.displayName,
152
- ptyManager,
153
- buffer,
154
- textHistory,
155
- renderedHistory,
156
- historyMode,
157
- tool,
158
- startTime: Date.now(),
159
- isThinking: false,
160
- completionTimer: null,
161
- awaitingCompletion: false,
162
- inputSeq: 0,
163
- completionReadInputSeq: 0,
164
- resizeOwner: null,
165
- hasConnectedWebClient: false,
166
- hasUnreadCompletion: false,
167
- timedInputs: new Map(),
168
- fileAttachments: new Map(),
169
- fileUploads: new Map(),
170
- write: data => this.write(id, data),
171
- isRunning: () => this.has(id) && ptyManager.isRunning(),
172
- kill: () => this.kill(id)
173
- };
174
-
175
- this.sessions.set(id, session);
176
- this.logSessionDiagnostics('session-created', session, {}, { compact: true });
177
-
178
- ptyManager.onData((data) => this.handleOutput(session, data));
179
- ptyManager.onExit(() => this.handleExit(session));
180
-
181
- const started = ptyManager.start([]);
182
- if (!started) {
183
- const err = new Error(`Failed to start ${tool.displayName}`);
184
- err.statusCode = 500;
185
- this.sessions.delete(id);
186
- throw err;
187
- }
188
-
189
- return session;
190
- }
191
-
192
- createClaudeStructuredSession({ tool, workingDirectory, name, claudeOptions = {} }) {
193
- const sessionDir = workingDirectory && String(workingDirectory).trim()
194
- ? path.resolve(this.baseDir, String(workingDirectory).trim())
195
- : this.baseDir;
196
-
197
- if (!fs.existsSync(sessionDir)) {
198
- const err = new Error(`Directory does not exist: ${sessionDir}`);
199
- err.statusCode = 400;
200
- this.logger.error(`Create Claude session failed: missing directory ${sessionDir}`);
201
- throw err;
202
- }
203
-
204
- const id = uuidv4();
205
- const session = new ClaudeStructuredSession({
206
- id,
207
- tool,
208
- workingDir: sessionDir,
209
- name: name || tool.displayName,
210
- logger: this.logger,
211
- options: claudeOptions
212
- });
213
- session.imageAttachments = new Map();
214
- session.imageUploads = new Map();
215
- session.fileAttachments = new Map();
216
- session.fileUploads = new Map();
217
-
218
- this.sessions.set(id, session);
219
- session.on('event', event => this.emit('claude-event', { sessionId: id, event, session }));
220
- session.on('exit', () => this.handleExit(session));
221
- this.logSessionDiagnostics('claude-session-created', session, {}, { compact: true });
222
-
223
- return session;
224
- }
225
-
226
- createCodexStructuredSession({ id: requestedId, tool, workingDirectory, name, codexOptions = {}, disposeResources = null }) {
227
- const sessionDir = workingDirectory && String(workingDirectory).trim()
228
- ? path.resolve(this.baseDir, String(workingDirectory).trim())
229
- : this.baseDir;
230
- if (!fs.existsSync(sessionDir)) {
231
- const err = new Error(`Directory does not exist: ${sessionDir}`);
232
- err.statusCode = 400;
233
- throw err;
234
- }
235
- const id = requestedId || uuidv4();
236
- const session = new CodexStructuredSession({ id, tool, workingDir: sessionDir, name: name || tool.displayName, logger: this.logger, options: codexOptions });
237
- session.imageAttachments = new Map();
238
- session.imageUploads = new Map();
239
- session.fileAttachments = new Map();
240
- session.fileUploads = new Map();
241
- session.disposeResources = typeof disposeResources === 'function' ? disposeResources : null;
242
- this.sessions.set(id, session);
243
- session.on('event', event => this.emit('codex-event', { sessionId: id, event, session }));
244
- session.on('exit', () => this.handleExit(session));
245
- session.ensureProcess().catch(error => {
246
- session.append({ kind: 'event', level: 'error', text: `Unable to start Codex app-server: ${error.message}` });
247
- });
248
- this.logSessionDiagnostics('codex-session-created', session, {}, { compact: true });
249
- return session;
250
- }
251
-
252
- write(id, data) {
253
- const session = this.get(id);
254
- if (!session) return false;
255
- this.markSessionInput(session, data);
256
- if (isStructuredSession(session)) return session.write(data);
257
- return session.ptyManager.write(data);
258
- }
259
-
260
- sendTerminalFileInput(id, text, fileAttachmentIds = []) {
261
- const session = this.get(id);
262
- if (!session || isStructuredSession(session)) return false;
263
- const files = this.getFileAttachments(id, fileAttachmentIds);
264
- const prompt = promptWithFileReferences(text, files);
265
- if (!prompt) return false;
266
- this.write(id, prompt.replace(/\n/g, '\r'));
267
- const enterTimer = setTimeout(() => this.write(id, '\r'), 1000);
268
- enterTimer.unref?.();
269
- if (files.length) this.scheduleFileCleanup(id, files.map(item => item.id));
270
- return true;
271
- }
272
-
273
- async sendClaudeInput(id, text, attachmentIds = [], fileAttachmentIds = []) {
274
- const session = this.get(id);
275
- if (!session || session.kind !== 'claude-structured') return false;
276
- const attachments = this.getImageAttachments(id, attachmentIds);
277
- const files = this.getFileAttachments(id, fileAttachmentIds);
278
- const prompt = String(text || '');
279
- if (!prompt.trim() && attachments.length === 0 && files.length === 0) return false;
280
- const prepared = await Promise.all(attachments.map(async attachment => ({
281
- id: attachment.id,
282
- name: attachment.name,
283
- size: attachment.size,
284
- mediaType: imageMediaType(attachment.name),
285
- data: (await fs.promises.readFile(attachment.path)).toString('base64')
286
- })));
287
- const agentPrompt = promptWithFileReferences(prompt, files);
288
- this.markSessionInput(session, prompt || (files.length ? '[file attachment]' : '[image attachment]'));
289
- const sent = session.sendUserMessage(prompt, prepared, {
290
- agentText: agentPrompt,
291
- displayAttachments: files.map(item => ({ id: item.id, name: item.name, size: item.size, kind: 'file' }))
292
- });
293
- if (sent && attachments.length) this.scheduleImageCleanup(id, attachments.map(item => item.id));
294
- if (sent && files.length) this.scheduleFileCleanup(id, files.map(item => item.id));
295
- return sent;
296
- }
297
-
298
- appendFileChunk(id, input = {}, bytes) {
299
- return this.fileAttachmentStore.appendChunk(this.get(id), input, bytes);
300
- }
301
-
302
- discardFileUpload(id, uploadId) {
303
- return this.fileAttachmentStore.discardUpload(this.get(id), uploadId);
304
- }
305
-
306
- discardFileAttachment(id, attachmentId) {
307
- return this.fileAttachmentStore.discardAttachment(this.get(id), attachmentId);
308
- }
309
-
310
- getFileAttachments(id, attachmentIds = []) {
311
- return this.fileAttachmentStore.resolve(this.get(id), attachmentIds);
312
- }
313
-
314
- scheduleFileCleanup(id, attachmentIds) {
315
- this.fileAttachmentStore.scheduleCleanup(this.get(id), attachmentIds);
316
- }
317
-
318
- storeImageAttachment(id, bytes) {
319
- return this.codexImages.store(this.get(id), bytes);
320
- }
321
-
322
- appendImageChunk(id, input = {}, bytes) {
323
- return this.codexImages.appendChunk(this.get(id), input, bytes);
324
- }
325
-
326
- discardImageUpload(id, uploadId) {
327
- return this.codexImages.discardUpload(this.get(id), uploadId);
328
- }
329
-
330
- discardImageAttachment(id, attachmentId) {
331
- return this.codexImages.discardAttachment(this.get(id), attachmentId);
332
- }
333
-
334
- getImageAttachments(id, attachmentIds = []) {
335
- return this.codexImages.resolve(this.get(id), attachmentIds);
336
- }
337
-
338
- scheduleImageCleanup(id, attachmentIds) {
339
- this.codexImages.scheduleCleanup(this.get(id), attachmentIds);
340
- }
341
-
342
- storeCodexImageAttachment(id, bytes) {
343
- return this.storeImageAttachment(id, bytes);
344
- }
345
-
346
- appendCodexImageChunk(id, input = {}, bytes) {
347
- return this.appendImageChunk(id, input, bytes);
348
- }
349
-
350
- discardCodexImageUpload(id, uploadId) {
351
- return this.discardImageUpload(id, uploadId);
352
- }
353
-
354
- discardCodexImageAttachment(id, attachmentId) {
355
- return this.discardImageAttachment(id, attachmentId);
356
- }
357
-
358
- getCodexImageAttachments(id, attachmentIds = []) {
359
- return this.getImageAttachments(id, attachmentIds);
360
- }
361
-
362
- scheduleCodexImageCleanup(id, attachmentIds) {
363
- this.scheduleImageCleanup(id, attachmentIds);
364
- }
365
-
366
- async sendCodexInput(id, text, attachmentIds = [], skills = [], fileAttachmentIds = []) {
367
- const session = this.get(id);
368
- if (!session || session.kind !== 'codex-structured') return false;
369
- const attachments = this.getCodexImageAttachments(id, attachmentIds);
370
- const files = this.getFileAttachments(id, fileAttachmentIds);
371
- const prompt = String(text || '');
372
- if (!prompt.trim() && attachments.length === 0 && files.length === 0) return false;
373
- const agentPrompt = promptWithFileReferences(prompt, files);
374
- this.markSessionInput(session, prompt || (files.length ? '[file attachment]' : '[image attachment]'));
375
- const sent = await session.sendUserMessage(prompt, attachments, skills, {
376
- agentText: agentPrompt,
377
- displayAttachments: files.map(item => ({ id: item.id, name: item.name, size: item.size, kind: 'file' }))
378
- });
379
- if (sent && attachments.length) this.scheduleCodexImageCleanup(id, attachments.map(item => item.id));
380
- if (sent && files.length) this.scheduleFileCleanup(id, files.map(item => item.id));
381
- return sent;
382
- }
383
-
384
- respondClaudePermission(id, permissionId, approved, action = null) {
385
- const session = this.get(id);
386
- if (!session || session.kind !== 'claude-structured') return false;
387
- return session.respondPermission(permissionId, approved, action);
388
- }
389
-
390
- updateClaudeSettings(id, settings) {
391
- const session = this.get(id);
392
- if (!session || session.kind !== 'claude-structured') return null;
393
- return session.updateSettings(settings || {});
394
- }
395
-
396
- showClaudeUsage(id) {
397
- const session = this.get(id);
398
- if (!session || session.kind !== 'claude-structured') return Promise.resolve(false);
399
- return session.showUsage();
400
- }
401
-
402
- showClaudeContext(id) {
403
- const session = this.get(id);
404
- if (!session || session.kind !== 'claude-structured') return Promise.resolve(false);
405
- return session.showContext();
406
- }
407
-
408
- abortClaude(id) {
409
- const session = this.get(id);
410
- if (!session || session.kind !== 'claude-structured') return false;
411
- return session.abort('Aborted by user');
412
- }
413
-
414
- resumeClaude(id, resumeSessionId) {
415
- const session = this.get(id);
416
- if (!session || session.kind !== 'claude-structured') return false;
417
- const historyMessages = this.readClaudeTranscriptMessages(this.getSessionWorkingDirectory(session), resumeSessionId);
418
- return session.selectResumeSession(resumeSessionId, historyMessages);
419
- }
420
-
421
- async forkClaude(id, sourceSessionId) {
422
- const session = this.get(id);
423
- if (!session || session.kind !== 'claude-structured') return null;
424
- if (session.status === 'thinking') {
425
- const error = new Error('Claude must be idle before forking a session');
426
- error.statusCode = 409;
427
- throw error;
428
- }
429
- const sourceId = String(sourceSessionId || session.claudeSessionId || session.resumeSessionId || '').trim();
430
- if (!sourceId) {
431
- const error = new Error('No Claude session is available to fork');
432
- error.statusCode = 400;
433
- throw error;
434
- }
435
- const result = await this.claudeForkSession(sourceId, { dir: this.getSessionWorkingDirectory(session) });
436
- const forkedId = result?.sessionId;
437
- if (!forkedId) throw new Error('Claude SDK did not return a forked session id');
438
- const historyMessages = this.readClaudeTranscriptMessages(this.getSessionWorkingDirectory(session), forkedId);
439
- session.selectResumeSession(forkedId, historyMessages);
440
- session.appendMessage({ kind: 'event', level: 'info', text: `Forked from Claude session ${sourceId}` });
441
- return { id: session.id, name: session.name, claudeSessionId: forkedId };
442
- }
443
-
444
- listClaudeResumeSessions(id) {
445
- const session = this.get(id);
446
- if (!session || session.kind !== 'claude-structured') return null;
447
- return this.scanClaudeProjectSessions(this.getSessionWorkingDirectory(session));
448
- }
449
-
450
- getCodexSnapshot(id) {
451
- const session = this.get(id);
452
- return session && session.kind === 'codex-structured' ? session.snapshot() : null;
453
- }
454
-
455
- updateCodexSettings(id, settings) {
456
- const session = this.get(id);
457
- if (!session || session.kind !== 'codex-structured') return null;
458
- return session.updateSettings(settings || {});
459
- }
460
-
461
- showCodexStatus(id) {
462
- const session = this.get(id);
463
- if (!session || session.kind !== 'codex-structured') return Promise.resolve(false);
464
- return session.showStatus();
465
- }
466
-
467
- compactCodexContext(id) {
468
- const session = this.get(id);
469
- if (!session || session.kind !== 'codex-structured') return Promise.resolve(false);
470
- return session.compactContext();
471
- }
472
-
473
- abortCodex(id) {
474
- const session = this.get(id);
475
- return session && session.kind === 'codex-structured' ? session.abort('Aborted by user') : false;
476
- }
477
-
478
- resumeCodex(id, threadId) {
479
- const session = this.get(id);
480
- return session && session.kind === 'codex-structured' ? session.resume(threadId) : false;
481
- }
482
-
483
- async forkCodex(id, threadId) {
484
- const session = this.get(id);
485
- if (!session || session.kind !== 'codex-structured') return null;
486
- if (session.status !== 'idle' || session.resuming) {
487
- const error = new Error('Codex must be idle before forking');
488
- error.statusCode = 409;
489
- throw error;
490
- }
491
- const sourceThreadId = String(threadId || session.threadId || '').trim();
492
- if (!sourceThreadId) {
493
- const error = new Error('Choose a Codex thread to fork');
494
- error.statusCode = 400;
495
- throw error;
496
- }
497
- const result = await session.forkFrom(sourceThreadId);
498
- if (!result) throw new Error('Unable to fork the selected Codex thread');
499
- session.forkedFromThreadId = sourceThreadId;
500
- return session;
501
- }
502
-
503
- listCodexResumeThreads(id) {
504
- const session = this.get(id);
505
- if (!session || session.kind !== 'codex-structured') return null;
506
- return session.listResumeThreads();
507
- }
508
-
509
- listCodexPrompts(id, options) {
510
- const session = this.get(id);
511
- if (!session || session.kind !== 'codex-structured') return null;
512
- return session.listPromptHistory(options);
513
- }
514
-
515
- listCodexSkills(id, forceReload = false) {
516
- const session = this.get(id);
517
- if (!session || session.kind !== 'codex-structured') return null;
518
- return session.listSkills(forceReload);
519
- }
520
-
521
- resize(id, cols, rows) {
522
- const session = this.get(id);
523
- if (!session) return false;
524
- if (session.kind === 'claude-structured') return true;
525
- if (session.kind === 'codex-structured') return true;
526
- if (session.renderedHistory) {
527
- session.renderedHistory.resize(cols, rows);
528
- }
529
- session.ptyManager.resize(cols, rows);
530
- return true;
531
- }
532
-
533
- redraw(id, cols, rows) {
534
- const session = this.get(id);
535
- if (!session) return false;
536
- if (isStructuredSession(session)) return true;
537
- if (session.renderedHistory) {
538
- session.renderedHistory.resize(cols, rows);
539
- }
540
- return session.ptyManager.redraw(cols, rows);
541
- }
542
-
543
- rename(id, name) {
544
- const session = this.get(id);
545
- if (!session || !name) return null;
546
- session.name = name;
547
- this.logSessionDiagnostics('session-renamed', session, {}, { compact: true });
548
- return session;
549
- }
550
-
551
- markCompletionRead(id) {
552
- const session = this.get(id);
553
- if (!session) return null;
554
- if (isStructuredSession(session)) {
555
- session.markCompletionRead();
556
- this.logSessionDiagnostics('completion-read', session, {}, { compact: true });
557
- return session;
558
- }
559
- session.hasUnreadCompletion = false;
560
- session.awaitingCompletion = false;
561
- session.isThinking = false;
562
- session.completionReadInputSeq = session.inputSeq || 0;
563
- clearTimeout(session.completionTimer);
564
- this.logSessionDiagnostics('completion-read', session, {}, { compact: true });
565
- return session;
566
- }
567
-
568
- listTimedInputs(id) {
569
- const session = this.get(id);
570
- if (!session) return null;
571
- return Array.from(session.timedInputs.values()).map(item => ({
572
- id: item.id,
573
- text: item.text,
574
- sendAt: item.sendAt,
575
- createdAt: item.createdAt
576
- })).sort((a, b) => a.sendAt - b.sendAt);
577
- }
578
-
579
- scheduleTimedInput(id, input = {}) {
580
- const session = this.get(id);
581
- if (!session) return null;
582
-
583
- const { text, sendAt, delay } = this.validateTimedInput(input);
584
-
585
- const item = {
586
- id: uuidv4(),
587
- text,
588
- sendAt,
589
- createdAt: Date.now(),
590
- timer: null
591
- };
592
-
593
- item.timer = setTimeout(() => {
594
- this.executeTimedInput(session.id, item.id);
595
- }, delay);
596
- session.timedInputs.set(item.id, item);
597
- return {
598
- id: item.id,
599
- text: item.text,
600
- sendAt: item.sendAt,
601
- createdAt: item.createdAt
602
- };
603
- }
604
-
605
- updateTimedInput(id, inputId, input = {}) {
606
- const session = this.get(id);
607
- if (!session) return null;
608
- const item = session.timedInputs.get(inputId);
609
- if (!item) return false;
610
-
611
- const { text, sendAt, delay } = this.validateTimedInput(input);
612
- clearTimeout(item.timer);
613
- item.text = text;
614
- item.sendAt = sendAt;
615
- item.updatedAt = Date.now();
616
- item.timer = setTimeout(() => {
617
- this.executeTimedInput(session.id, item.id);
618
- }, delay);
619
-
620
- return {
621
- id: item.id,
622
- text: item.text,
623
- sendAt: item.sendAt,
624
- createdAt: item.createdAt,
625
- updatedAt: item.updatedAt
626
- };
627
- }
628
-
629
- validateTimedInput(input = {}) {
630
- const text = String(input.text || '');
631
- const sendAt = Number(input.sendAt);
632
- if (!text.trim()) {
633
- const err = new Error('Text is required');
634
- err.statusCode = 400;
635
- throw err;
636
- }
637
- if (!Number.isFinite(sendAt) || sendAt <= Date.now()) {
638
- const err = new Error('Send time must be in the future');
639
- err.statusCode = 400;
640
- throw err;
641
- }
642
-
643
- const maxDelay = 30 * 24 * 60 * 60 * 1000;
644
- const delay = sendAt - Date.now();
645
- if (delay > maxDelay) {
646
- const err = new Error('Send time must be within 30 days');
647
- err.statusCode = 400;
648
- throw err;
649
- }
650
-
651
- return { text, sendAt, delay };
652
- }
653
-
654
- cancelTimedInput(id, inputId) {
655
- const session = this.get(id);
656
- if (!session) return null;
657
- const item = session.timedInputs.get(inputId);
658
- if (!item) return false;
659
- clearTimeout(item.timer);
660
- session.timedInputs.delete(inputId);
661
- return true;
662
- }
663
-
664
- executeTimedInput(id, inputId) {
665
- const session = this.get(id);
666
- if (!session) return false;
667
- const item = session.timedInputs.get(inputId);
668
- if (!item) return false;
669
- session.timedInputs.delete(inputId);
670
- const formatted = item.text.replace(/\n/g, '\r');
671
- this.write(id, formatted);
672
- setTimeout(() => {
673
- if (this.has(id)) this.write(id, '\r');
674
- }, 1000);
675
- return true;
676
- }
677
-
678
- quiesceSession(session) {
679
- clearTimeout(session.completionTimer);
680
- this.clearTimedInputs(session);
681
- }
682
-
683
- disposeSessionResources(session) {
684
- this.quiesceSession(session);
685
- this.clearCodexImageUploads(session);
686
- this.clearCodexImageAttachments(session);
687
- this.fileAttachmentStore.clear(session);
688
- if (typeof session.disposeResources === 'function') {
689
- try { session.disposeResources(); } catch (error) {
690
- this.logger.error?.(`Failed to clean session resources ${session.id}: ${error.message}`);
691
- }
692
- session.disposeResources = null;
693
- }
694
- this.disposeSessionHistory(session);
695
- }
696
-
697
- async stopSessionRuntime(session) {
698
- if (isStructuredSession(session)) {
699
- await session.kill();
700
- return;
701
- }
702
- session.ptyManager.kill();
703
- }
704
-
705
- removeSession(session) {
706
- if (this.sessions.get(session.id) !== session) return false;
707
- this.disposeSessionResources(session);
708
- this.sessions.delete(session.id);
709
- this.emit('exit', { sessionId: session.id, session });
710
- return true;
711
- }
712
-
713
- async kill(id) {
714
- const session = this.get(id);
715
- if (!session) return false;
716
- this.logSessionDiagnostics('session-deleted', session, {}, { compact: true });
717
- this.quiesceSession(session);
718
- await this.stopSessionRuntime(session);
719
- this.removeSession(session);
720
- return true;
721
- }
722
-
723
- killAll() {
724
- for (const session of Array.from(this.sessions.values())) {
725
- this.disposeSessionResources(session);
726
- void this.stopSessionRuntime(session).catch(error => {
727
- this.logger.error?.(`Failed to stop session ${session.id}: ${error.message}`);
728
- });
729
- }
730
- this.sessions.clear();
731
- }
732
-
733
- getHistory(id) {
734
- const session = this.get(id);
735
- if (!session) return null;
736
- if (isStructuredSession(session)) return session.getHistory();
737
- const historySource = session.renderedHistory || session.textHistory;
738
- return {
739
- success: true,
740
- sessionId: session.id,
741
- sessionName: session.name,
742
- tool: session.tool.displayName,
743
- historyMode: session.historyMode,
744
- ...historySource.toJSON()
745
- };
746
- }
747
-
748
- getCatchupOutput(id) {
749
- const session = this.get(id);
750
- if (!session) return null;
751
- if (isStructuredSession(session)) return session.getCatchupOutput();
752
-
753
- const bufferHistory = session.buffer.getAfter(0);
754
- if (bufferHistory.length > 0) {
755
- return {
756
- source: 'buffer',
757
- items: bufferHistory.length,
758
- data: bufferHistory.map(message => message.data).join('')
759
- };
760
- }
761
-
762
- const historySource = session.renderedHistory || session.textHistory;
763
- if (!historySource || typeof historySource.toJSON !== 'function') {
764
- return { source: 'none', items: 0, data: '' };
765
- }
766
-
767
- const snapshot = historySource.toJSON();
768
- const text = String(snapshot.text || '');
769
- return {
770
- source: session.renderedHistory ? 'rendered-history' : 'text-history',
771
- items: snapshot.lines || 0,
772
- data: text ? text + (text.endsWith('\n') ? '' : '\r\n') : ''
773
- };
774
- }
775
-
776
- getDiagnostics(id) {
777
- const session = this.get(id);
778
- return session ? this.getSessionDiagnostics(session) : null;
779
- }
780
-
781
- getClaudeSnapshot(id) {
782
- const session = this.get(id);
783
- if (!session || session.kind !== 'claude-structured') return null;
784
- return session.snapshot();
785
- }
786
-
787
- scanClaudeProjectSessions(workingDirectory) {
788
- return this.claudeTranscripts.list(workingDirectory);
789
- }
790
-
791
- readClaudeTranscriptMessages(workingDirectory, resumeSessionId) {
792
- return this.claudeTranscripts.readMessages(workingDirectory, resumeSessionId);
793
- }
794
-
795
- logHistoryRequest(id, req) {
796
- const session = this.get(id);
797
- if (!session) return;
798
- this.logSessionDiagnostics('history-request', session, {
799
- userAgent: req.headers['user-agent'] || '',
800
- acceptEncoding: req.headers['accept-encoding'] || ''
801
- }, { compact: true });
802
- }
803
-
804
- logClientDebug(sessionId, event, payload) {
805
- const session = sessionId ? this.get(sessionId) : null;
806
- this.logger.debugInfo(`[client-debug] ${JSON.stringify({
807
- sessionId: sessionId || null,
808
- event: event || 'unknown',
809
- payload: payload || null,
810
- serverSide: session ? this.getSessionDiagnostics(session) : null
811
- })}`);
812
- }
813
-
814
- logWsConnected(id, req) {
815
- const session = this.get(id);
816
- if (!session) return;
817
- this.logSessionDiagnostics('ws-connected', session, {
818
- remoteAddress: req.socket.remoteAddress || null,
819
- userAgent: req.headers['user-agent'] || ''
820
- }, { compact: true });
821
- }
822
-
823
- logWsCatchup(id, history) {
824
- const session = this.get(id);
825
- if (!session) return;
826
- this.logSessionDiagnostics('ws-catchup', session, {
827
- catchupItems: history.length,
828
- catchupPreview: previewText(history.map(message => message.data).join(''))
829
- }, { compact: true });
830
- }
831
-
832
- logWsCatchupOutput(id, catchup) {
833
- const session = this.get(id);
834
- if (!session) return;
835
- this.logSessionDiagnostics('ws-catchup', session, {
836
- catchupSource: catchup.source,
837
- catchupItems: catchup.items,
838
- catchupBytes: Buffer.byteLength(String(catchup.data || ''), 'utf8'),
839
- catchupPreview: previewText(catchup.data)
840
- }, { compact: true });
841
- }
842
-
843
- logWsResize(id, cols, rows) {
844
- const session = this.get(id);
845
- if (!session) return;
846
- this.logSessionDiagnostics('ws-resize', session, { cols, rows }, { compact: true });
847
- }
848
-
849
- logWsClosed(id) {
850
- const session = this.get(id);
851
- if (!session) return;
852
- this.logSessionDiagnostics('ws-closed', session, {}, { compact: true });
853
- }
854
-
855
- getHistoryModeForTool(toolKey) {
856
- return this.renderHistoryTools.has(String(toolKey || '').toLowerCase()) ? 'rendered' : 'transcript';
857
- }
858
-
859
- handleOutput(session, data) {
860
- session.textHistory.write(data);
861
- if (session.renderedHistory) {
862
- session.renderedHistory.write(data);
863
- }
864
- this.logSessionDiagnostics('pty-output', session, {
865
- chunkBytes: Buffer.byteLength(String(data), 'utf8'),
866
- chunkPreview: previewText(data),
867
- containsClear: /\x1b\[[0-9;?]*J/.test(data),
868
- containsCursorMove: /\x1b\[[0-9;?]*(?:[ABCDGHf])/.test(data)
869
- }, { compact: true });
870
-
871
- if (session.awaitingCompletion && !session.isThinking && data.trim().length > 0) session.isThinking = true;
872
- if (session.awaitingCompletion && session.isThinking) {
873
- clearTimeout(session.completionTimer);
874
- const watchedInputSeq = session.inputSeq || 0;
875
- session.completionTimer = setTimeout(() => {
876
- const hasUnreadInput = watchedInputSeq > (session.completionReadInputSeq || 0);
877
- const isCurrentInput = watchedInputSeq === (session.inputSeq || 0);
878
- if (session.awaitingCompletion && hasUnreadInput && isCurrentInput && !this.hasConnectedSessionClient(session.id)) {
879
- session.hasUnreadCompletion = true;
880
- }
881
- session.awaitingCompletion = false;
882
- session.isThinking = false;
883
- }, 10000);
884
- }
885
-
886
- this.emit('output', { sessionId: session.id, data, session });
887
- }
888
-
889
- handleExit(session) {
890
- if (this.sessions.get(session.id) !== session) return;
891
- this.logger.info(`Session ${session.id} (${session.name}) exited.`);
892
- this.removeSession(session);
893
- }
894
-
895
- markSessionInput(session, data) {
896
- if (typeof data !== 'string' || data.length === 0) return;
897
- session.inputSeq = (session.inputSeq || 0) + 1;
898
- if (isStructuredSession(session)) {
899
- session.hasUnreadCompletion = false;
900
- this.logSessionDiagnostics('session-input', session, {
901
- inputSeq: session.inputSeq,
902
- inputPreview: previewText(data)
903
- }, { compact: true });
904
- return;
905
- }
906
- session.awaitingCompletion = true;
907
- session.isThinking = false;
908
- session.hasUnreadCompletion = false;
909
- clearTimeout(session.completionTimer);
910
- this.logSessionDiagnostics('session-input', session, {
911
- inputSeq: session.inputSeq,
912
- inputPreview: previewText(data)
913
- }, { compact: true });
914
- }
915
-
916
- disposeSessionHistory(session) {
917
- if (isStructuredSession(session)) return;
918
- if (session.renderedHistory) {
919
- session.renderedHistory.dispose();
920
- session.renderedHistory = null;
921
- }
922
- }
923
-
924
- clearTimedInputs(session) {
925
- if (!session || !session.timedInputs) return;
926
- for (const item of session.timedInputs.values()) {
927
- clearTimeout(item.timer);
928
- }
929
- session.timedInputs.clear();
930
- }
931
-
932
- clearCodexImageAttachments(session) {
933
- this.codexImages.clearAttachments(session);
934
- }
935
-
936
- clearCodexImageUploads(session) {
937
- this.codexImages.clearUploads(session);
938
- }
939
-
940
- getSessionDiagnostics(session, extra = {}) {
941
- if (isStructuredSession(session)) {
942
- return {
943
- sessionId: session.id,
944
- sessionName: session.name,
945
- toolKey: session.tool.key,
946
- kind: session.kind,
947
- historyMode: 'structured',
948
- status: session.status,
949
- workingDirectory: this.getSessionWorkingDirectory(session),
950
- messages: session.messages.length,
951
- pendingPermissions: session.pendingPermissions.size,
952
- timedInputCount: session.timedInputs ? session.timedInputs.size : 0,
953
- ...extra
954
- };
955
- }
956
- return {
957
- sessionId: session.id,
958
- sessionName: session.name,
959
- toolKey: session.tool.key,
960
- historyMode: session.historyMode,
961
- workingDirectory: session.ptyManager.workingDir,
962
- buffer: session.buffer.getDebugSnapshot(),
963
- textHistory: session.textHistory.getDebugSnapshot(),
964
- renderedHistory: session.renderedHistory ? session.renderedHistory.getDebugSnapshot() : null,
965
- ...extra
966
- };
967
- }
968
-
969
- getCompactSessionDiagnostics(session, extra = {}) {
970
- if (isStructuredSession(session)) {
971
- return this.getSessionDiagnostics(session, extra);
972
- }
973
- const buffer = session.buffer.getDebugSnapshot();
974
- const textHistory = session.textHistory.getDebugSnapshot();
975
- const renderedHistory = session.renderedHistory ? session.renderedHistory.getDebugSnapshot() : null;
976
- return {
977
- sessionId: session.id,
978
- sessionName: session.name,
979
- toolKey: session.tool.key,
980
- historyMode: session.historyMode,
981
- workingDirectory: session.ptyManager.workingDir,
982
- buffer: {
983
- items: buffer.items,
984
- totalSize: buffer.totalSize,
985
- currentSeq: buffer.currentSeq,
986
- oldestSeq: buffer.oldestSeq,
987
- newestSeq: buffer.newestSeq,
988
- combinedTailPreview: buffer.combinedTailPreview
989
- },
990
- textHistory: {
991
- lines: textHistory.lines,
992
- bytes: textHistory.bytes,
993
- totalWrites: textHistory.totalWrites,
994
- totalBytes: textHistory.totalBytes,
995
- escapeCount: textHistory.escapeCount,
996
- clearEvents: textHistory.clearEvents,
997
- eraseLineEvents: textHistory.eraseLineEvents,
998
- cursorMoveEvents: textHistory.cursorMoveEvents,
999
- trimEvents: textHistory.trimEvents,
1000
- tailPreview: textHistory.tailPreview
1001
- },
1002
- renderedHistory: renderedHistory ? {
1003
- cols: renderedHistory.cols,
1004
- rows: renderedHistory.rows,
1005
- totalWrites: renderedHistory.totalWrites,
1006
- totalBytes: renderedHistory.totalBytes,
1007
- pendingWrites: renderedHistory.pendingWrites,
1008
- resizeEvents: renderedHistory.resizeEvents,
1009
- bufferLines: renderedHistory.bufferLines,
1010
- baseY: renderedHistory.baseY,
1011
- cursorY: renderedHistory.cursorY,
1012
- cursorX: renderedHistory.cursorX,
1013
- tailPreview: renderedHistory.tailPreview
1014
- } : null,
1015
- ...extra
1016
- };
1017
- }
1018
-
1019
- logSessionDiagnostics(reason, session, extra = {}, options = {}) {
1020
- if (!this.debugHistoryEnabled || !session) return;
1021
- const payload = options.compact
1022
- ? this.getCompactSessionDiagnostics(session, extra)
1023
- : this.getSessionDiagnostics(session, extra);
1024
- this.logger.debugInfo(`[history-debug] ${reason} ${JSON.stringify(payload)}`);
1025
- }
1026
-
1027
- getSessionWorkingDirectory(session) {
1028
- return session.workingDir || (session.ptyManager && session.ptyManager.workingDir) || this.baseDir;
1029
- }
1030
- }
1031
-
1032
- module.exports = SessionManager;