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.
- package/README.md +4 -192
- package/THIRD_PARTY_NOTICES.md +27 -0
- package/bin/glad.cjs +56 -0
- package/package.json +19 -61
- package/README.zh-CN.md +0 -198
- package/assets/logo.svg +0 -43
- package/bin/cli.js +0 -65
- package/lib/ai-tools/demo/enhanced-demo.js +0 -625
- package/lib/ai-tools/demo/index.js +0 -24
- package/lib/ai-tools/demo/responses.js +0 -88
- package/lib/ai-tools/detector.js +0 -76
- package/lib/ai-tools/registry.js +0 -300
- package/lib/claude/cli-usage.js +0 -95
- package/lib/claude/config.js +0 -82
- package/lib/claude/structured-session.js +0 -884
- package/lib/claude/transcript-repository.js +0 -216
- package/lib/codex/image-store.js +0 -174
- package/lib/codex/structured-session.js +0 -1590
- package/lib/commands/config.js +0 -78
- package/lib/commands/tools.js +0 -128
- package/lib/commands/web.js +0 -605
- package/lib/config/constants.js +0 -17
- package/lib/config/manager.js +0 -108
- package/lib/git/service.js +0 -83
- package/lib/notifications/message-formatter.js +0 -94
- package/lib/notifications/notification-service.js +0 -143
- package/lib/notifications/serverchan-client.js +0 -58
- package/lib/notifications/serverchan-settings-store.js +0 -115
- package/lib/schedule/job-runner.js +0 -162
- package/lib/schedule/job-store.js +0 -167
- package/lib/schedule/key-sequences.js +0 -49
- package/lib/schedule/scheduler-service.js +0 -39
- package/lib/server/routes/notifications.js +0 -52
- package/lib/server/routes/providers.js +0 -114
- package/lib/server/routes/schedules.js +0 -54
- package/lib/server/routes/skillhub.js +0 -104
- package/lib/server/routes/usage.js +0 -23
- package/lib/server/routes/workspace.js +0 -77
- package/lib/session/buffer.js +0 -102
- package/lib/session/file-attachment-store.js +0 -168
- package/lib/session/pty-manager.js +0 -255
- package/lib/session/rendered-history.js +0 -225
- package/lib/session/session-manager.js +0 -1032
- package/lib/session/text-history.js +0 -274
- package/lib/skillhub/client.js +0 -121
- package/lib/skillhub/settings-store.js +0 -168
- package/lib/skillhub/skill-installer.js +0 -320
- package/lib/usage/ccusage-runner.js +0 -128
- package/lib/usage/source-catalog.js +0 -26
- package/lib/usage/usage-service.js +0 -226
- package/lib/utils/logger.js +0 -74
- package/lib/utils/pid.js +0 -67
- package/lib/utils/validation.js +0 -53
- package/lib/web/bootstrap.js +0 -34
- package/lib/web/claude.js +0 -1150
- package/lib/web/codex.js +0 -1045
- package/lib/web/composer.js +0 -493
- package/lib/web/core.js +0 -385
- package/lib/web/git.js +0 -535
- package/lib/web/gitgraph.js +0 -293
- package/lib/web/index.html +0 -547
- package/lib/web/layout.js +0 -69
- package/lib/web/notifications.js +0 -164
- package/lib/web/schedules.js +0 -245
- package/lib/web/session.js +0 -361
- package/lib/web/shell.js +0 -74
- package/lib/web/skillhub.js +0 -197
- package/lib/web/styles.css +0 -932
- package/lib/web/terminal-scroll.js +0 -81
- package/lib/web/theme.js +0 -60
- package/lib/web/timed-inputs.js +0 -216
- package/lib/web/usage.js +0 -323
- package/lib/workspace/service.js +0 -77
- 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;
|