glad-web 1.0.18 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -3
- package/README.zh-CN.md +2 -3
- package/lib/ai-tools/registry.js +0 -9
- package/lib/claude/config.js +82 -0
- package/lib/claude/structured-session.js +516 -0
- package/lib/commands/web.js +104 -12
- package/lib/session/session-manager.js +443 -3
- package/lib/web/index.html +1114 -11
- package/package.json +2 -1
package/lib/commands/web.js
CHANGED
|
@@ -33,11 +33,16 @@ function sendCompressedJson(req, res, payload) {
|
|
|
33
33
|
res.type('application/json').send(body);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function getSessionWorkingDirectory(session) {
|
|
37
|
+
return session.workingDir || (session.ptyManager && session.ptyManager.workingDir) || process.cwd();
|
|
38
|
+
}
|
|
39
|
+
|
|
36
40
|
const { detectInstalledTools } = require('../ai-tools/detector');
|
|
37
41
|
const logger = require('../utils/logger');
|
|
38
42
|
const { JobStore } = require('../schedule/job-store');
|
|
39
43
|
const JobRunner = require('../schedule/job-runner');
|
|
40
44
|
const SchedulerService = require('../schedule/scheduler-service');
|
|
45
|
+
const { getClaudeRuntimeConfig } = require('../claude/config');
|
|
41
46
|
|
|
42
47
|
async function webCommand(options) {
|
|
43
48
|
const port = parseInt(options.port) || 3000;
|
|
@@ -85,6 +90,9 @@ async function webCommand(options) {
|
|
|
85
90
|
sessionManager.on('output', ({ sessionId, data }) => {
|
|
86
91
|
broadcastToSession(sessionId, { type: 'output', data });
|
|
87
92
|
});
|
|
93
|
+
sessionManager.on('claude-event', ({ sessionId, event }) => {
|
|
94
|
+
broadcastToSession(sessionId, { type: 'claude-event', event });
|
|
95
|
+
});
|
|
88
96
|
sessionManager.on('exit', ({ sessionId }) => {
|
|
89
97
|
broadcastToSession(sessionId, { type: 'exit' });
|
|
90
98
|
});
|
|
@@ -179,12 +187,16 @@ async function webCommand(options) {
|
|
|
179
187
|
res.json(sessionManager.list());
|
|
180
188
|
});
|
|
181
189
|
|
|
190
|
+
app.get('/api/claude-config', (req, res) => {
|
|
191
|
+
res.json({ success: true, config: getClaudeRuntimeConfig(process.env) });
|
|
192
|
+
});
|
|
193
|
+
|
|
182
194
|
// API: Create a new PTY session
|
|
183
195
|
app.post('/api/sessions', async (req, res) => {
|
|
184
196
|
logger.debug(`API: POST /api/sessions - ${JSON.stringify(req.body)}`);
|
|
185
197
|
try {
|
|
186
|
-
const { toolKey, workingDirectory } = req.body;
|
|
187
|
-
const session = sessionManager.create({ toolKey, workingDirectory });
|
|
198
|
+
const { toolKey, workingDirectory, claudeOptions } = req.body;
|
|
199
|
+
const session = sessionManager.create({ toolKey, workingDirectory, claudeOptions });
|
|
188
200
|
res.json({ id: session.id });
|
|
189
201
|
} catch (e) {
|
|
190
202
|
logger.error(`API: POST /api/sessions failed: ${e.message}`);
|
|
@@ -217,6 +229,40 @@ async function webCommand(options) {
|
|
|
217
229
|
res.json({ success: true });
|
|
218
230
|
});
|
|
219
231
|
|
|
232
|
+
app.get('/api/sessions/:id/timed-inputs', (req, res) => {
|
|
233
|
+
const items = sessionManager.listTimedInputs(req.params.id);
|
|
234
|
+
if (!items) return res.status(404).json({ error: 'Session not found' });
|
|
235
|
+
res.json({ success: true, items });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
app.post('/api/sessions/:id/timed-inputs', (req, res) => {
|
|
239
|
+
try {
|
|
240
|
+
const item = sessionManager.scheduleTimedInput(req.params.id, req.body || {});
|
|
241
|
+
if (!item) return res.status(404).json({ error: 'Session not found' });
|
|
242
|
+
res.json({ success: true, item });
|
|
243
|
+
} catch (e) {
|
|
244
|
+
res.status(e.statusCode || 500).json({ error: e.message });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
app.patch('/api/sessions/:id/timed-inputs/:inputId', (req, res) => {
|
|
249
|
+
try {
|
|
250
|
+
const item = sessionManager.updateTimedInput(req.params.id, req.params.inputId, req.body || {});
|
|
251
|
+
if (item === null) return res.status(404).json({ error: 'Session not found' });
|
|
252
|
+
if (!item) return res.status(404).json({ error: 'Timed input not found' });
|
|
253
|
+
res.json({ success: true, item });
|
|
254
|
+
} catch (e) {
|
|
255
|
+
res.status(e.statusCode || 500).json({ error: e.message });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
app.delete('/api/sessions/:id/timed-inputs/:inputId', (req, res) => {
|
|
260
|
+
const cancelled = sessionManager.cancelTimedInput(req.params.id, req.params.inputId);
|
|
261
|
+
if (cancelled === null) return res.status(404).json({ error: 'Session not found' });
|
|
262
|
+
if (!cancelled) return res.status(404).json({ error: 'Timed input not found' });
|
|
263
|
+
res.json({ success: true });
|
|
264
|
+
});
|
|
265
|
+
|
|
220
266
|
// API: Delete/Kill session
|
|
221
267
|
app.delete('/api/sessions/:id', (req, res) => {
|
|
222
268
|
sessionManager.kill(req.params.id);
|
|
@@ -229,6 +275,32 @@ async function webCommand(options) {
|
|
|
229
275
|
res.json({ success: true, diagnostics });
|
|
230
276
|
});
|
|
231
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
|
+
|
|
232
304
|
app.post('/api/debug/client-log', (req, res) => {
|
|
233
305
|
const { sessionId, event, payload } = req.body || {};
|
|
234
306
|
sessionManager.logClientDebug(sessionId, event, payload);
|
|
@@ -240,7 +312,7 @@ async function webCommand(options) {
|
|
|
240
312
|
const session = sessionManager.get(req.params.id);
|
|
241
313
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
242
314
|
const hash = req.params.hash;
|
|
243
|
-
const result = await gitService.show(session
|
|
315
|
+
const result = await gitService.show(getSessionWorkingDirectory(session), hash);
|
|
244
316
|
res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
|
|
245
317
|
});
|
|
246
318
|
|
|
@@ -249,7 +321,7 @@ async function webCommand(options) {
|
|
|
249
321
|
const session = sessionManager.get(req.params.id);
|
|
250
322
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
251
323
|
const hash = req.params.hash;
|
|
252
|
-
const result = await gitService.nameRev(session
|
|
324
|
+
const result = await gitService.nameRev(getSessionWorkingDirectory(session), hash);
|
|
253
325
|
res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
|
|
254
326
|
});
|
|
255
327
|
|
|
@@ -257,7 +329,7 @@ async function webCommand(options) {
|
|
|
257
329
|
app.get('/api/sessions/:id/git-log', async (req, res) => {
|
|
258
330
|
const session = sessionManager.get(req.params.id);
|
|
259
331
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
260
|
-
const result = await gitService.log(session
|
|
332
|
+
const result = await gitService.log(getSessionWorkingDirectory(session), req.query.maxCount);
|
|
261
333
|
if (!result.success) {
|
|
262
334
|
return res.status(500).json({ error: result.error, stderr: result.stderr });
|
|
263
335
|
}
|
|
@@ -268,7 +340,7 @@ async function webCommand(options) {
|
|
|
268
340
|
app.get('/api/sessions/:id/git-status', async (req, res) => {
|
|
269
341
|
const session = sessionManager.get(req.params.id);
|
|
270
342
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
271
|
-
const result = await gitService.status(session
|
|
343
|
+
const result = await gitService.status(getSessionWorkingDirectory(session));
|
|
272
344
|
if (!result.success) {
|
|
273
345
|
return res.status(500).json({ error: result.error, stderr: result.stderr });
|
|
274
346
|
}
|
|
@@ -280,7 +352,7 @@ async function webCommand(options) {
|
|
|
280
352
|
const session = sessionManager.get(req.params.id);
|
|
281
353
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
282
354
|
const isStaged = req.query.staged === 'true';
|
|
283
|
-
const result = await gitService.diffNumstat(session
|
|
355
|
+
const result = await gitService.diffNumstat(getSessionWorkingDirectory(session), isStaged);
|
|
284
356
|
res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
|
|
285
357
|
});
|
|
286
358
|
|
|
@@ -291,7 +363,7 @@ async function webCommand(options) {
|
|
|
291
363
|
const isStaged = req.query.staged === 'true';
|
|
292
364
|
const filePath = req.query.path;
|
|
293
365
|
if (!filePath) return res.status(400).json({ error: 'Missing file path' });
|
|
294
|
-
const result = await gitService.diffFile(session
|
|
366
|
+
const result = await gitService.diffFile(getSessionWorkingDirectory(session), filePath, isStaged);
|
|
295
367
|
res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
|
|
296
368
|
});
|
|
297
369
|
|
|
@@ -301,7 +373,7 @@ async function webCommand(options) {
|
|
|
301
373
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
302
374
|
const filePath = req.query.path || '';
|
|
303
375
|
if (!filePath) return res.status(400).json({ error: 'Missing file path' });
|
|
304
|
-
const cwd = session
|
|
376
|
+
const cwd = getSessionWorkingDirectory(session);
|
|
305
377
|
try {
|
|
306
378
|
const content = workspaceService.readFile(cwd, filePath);
|
|
307
379
|
res.json({ success: true, content });
|
|
@@ -315,7 +387,7 @@ async function webCommand(options) {
|
|
|
315
387
|
const session = sessionManager.get(req.params.id);
|
|
316
388
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
317
389
|
const dirPath = req.query.path || '';
|
|
318
|
-
const cwd = session
|
|
390
|
+
const cwd = getSessionWorkingDirectory(session);
|
|
319
391
|
try {
|
|
320
392
|
const files = await workspaceService.listDirectory(cwd, dirPath);
|
|
321
393
|
res.json({ success: true, files });
|
|
@@ -359,15 +431,20 @@ async function webCommand(options) {
|
|
|
359
431
|
session.resizeOwner = ws;
|
|
360
432
|
}
|
|
361
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
|
+
}
|
|
362
438
|
|
|
363
439
|
// Send catchup output. TUI tools may skip the raw circular buffer, so fall
|
|
364
440
|
// back to the rendered/text history snapshot instead of reconnecting blank.
|
|
365
441
|
const catchup = sessionManager.getCatchupOutput(sessionId);
|
|
366
|
-
ws.needsTuiRedraw =
|
|
442
|
+
ws.needsTuiRedraw = session.kind !== 'claude-structured'
|
|
443
|
+
&& ['antigravity', 'claude-code', 'codex'].includes(session.tool.key)
|
|
367
444
|
&& (isReconnect || (catchup && catchup.source === 'rendered-history'));
|
|
368
445
|
if (ws.needsTuiRedraw) {
|
|
369
446
|
ws.send(JSON.stringify({ type: 'reset' }));
|
|
370
|
-
} else if (catchup && catchup.data) {
|
|
447
|
+
} else if (session.kind !== 'claude-structured' && catchup && catchup.data) {
|
|
371
448
|
sessionManager.logWsCatchupOutput(sessionId, catchup);
|
|
372
449
|
ws.send(JSON.stringify({ type: 'output', data: catchup.data }));
|
|
373
450
|
}
|
|
@@ -378,6 +455,21 @@ async function webCommand(options) {
|
|
|
378
455
|
if (payload.type === 'input') {
|
|
379
456
|
session.write(payload.data);
|
|
380
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));
|
|
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
|
+
}
|
|
381
473
|
if (payload.type === 'resize' && session.resizeOwner === ws) {
|
|
382
474
|
sessionManager.logWsResize(sessionId, payload.cols, payload.rows);
|
|
383
475
|
if (ws.needsTuiRedraw) {
|