glad-web 1.0.46 → 2.0.1

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,605 +0,0 @@
1
- const express = require('express');
2
- const http = require('http');
3
- const { WebSocketServer } = require('ws');
4
- const path = require('path');
5
- const fs = require('fs');
6
- const os = require('os');
7
- const zlib = require('zlib');
8
- const chalk = require('chalk');
9
- const { getAllTools } = require('../ai-tools/registry');
10
- const { GitService } = require('../git/service');
11
- const WorkspaceService = require('../workspace/service');
12
- const SessionManager = require('../session/session-manager');
13
-
14
- function sendCompressedJson(req, res, payload) {
15
- const body = Buffer.from(JSON.stringify(payload), 'utf8');
16
- const acceptEncoding = req.headers['accept-encoding'] || '';
17
-
18
- if (/\bgzip\b/.test(acceptEncoding)) {
19
- zlib.gzip(body, { level: 6 }, (error, compressed) => {
20
- if (error) {
21
- res.type('application/json').send(body);
22
- return;
23
- }
24
- res.setHeader('Content-Type', 'application/json; charset=utf-8');
25
- res.setHeader('Content-Encoding', 'gzip');
26
- res.setHeader('Vary', 'Accept-Encoding');
27
- res.setHeader('Content-Length', compressed.length);
28
- res.send(compressed);
29
- });
30
- return;
31
- }
32
-
33
- res.type('application/json').send(body);
34
- }
35
-
36
- function getSessionWorkingDirectory(session) {
37
- return session.workingDir || (session.ptyManager && session.ptyManager.workingDir) || process.cwd();
38
- }
39
-
40
- const { detectInstalledTools } = require('../ai-tools/detector');
41
- const logger = require('../utils/logger');
42
- const { JobStore } = require('../schedule/job-store');
43
- const JobRunner = require('../schedule/job-runner');
44
- const SchedulerService = require('../schedule/scheduler-service');
45
- const { getClaudeRuntimeConfig } = require('../claude/config');
46
- const registerScheduleRoutes = require('../server/routes/schedules');
47
- const registerWorkspaceRoutes = require('../server/routes/workspace');
48
- const registerProviderRoutes = require('../server/routes/providers');
49
- const registerNotificationRoutes = require('../server/routes/notifications');
50
- const registerUsageRoutes = require('../server/routes/usage');
51
- const registerSkillHubRoutes = require('../server/routes/skillhub');
52
- const { UsageService } = require('../usage/usage-service');
53
- const { ServerChanSettingsStore } = require('../notifications/serverchan-settings-store');
54
- const ServerChanClient = require('../notifications/serverchan-client');
55
- const NotificationService = require('../notifications/notification-service');
56
- const { SkillHubSettingsStore } = require('../skillhub/settings-store');
57
- const { SkillHubClient } = require('../skillhub/client');
58
- const { SkillInstaller } = require('../skillhub/skill-installer');
59
-
60
- async function webCommand(options) {
61
- const port = parseInt(options.port) || 3000;
62
- const debugHistoryEnabled = process.env.DEBUG_SESSION_HISTORY === '1';
63
- const defaultRenderedTools = getAllTools().map(tool => tool.key).join(',');
64
- const renderHistoryTools = new Set(
65
- String(process.env.HISTORY_RENDER_TOOLS || defaultRenderedTools)
66
- .split(',')
67
- .map(value => value.trim().toLowerCase())
68
- .filter(Boolean)
69
- );
70
- const app = express();
71
- app.use((req, res, next) => {
72
- res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
73
- res.setHeader('Pragma', 'no-cache');
74
- res.setHeader('Expires', '0');
75
- next();
76
- });
77
- app.use(express.json());
78
- const server = http.createServer(app);
79
- const wss = new WebSocketServer({
80
- server,
81
- perMessageDeflate: {
82
- threshold: 1024,
83
- zlibDeflateOptions: { level: 3 },
84
- zlibInflateOptions: {},
85
- clientNoContextTakeover: true,
86
- serverNoContextTakeover: true
87
- }
88
- });
89
-
90
- // Use directory from options if provided, otherwise default to current working directory
91
- const baseDir = options.directory ? path.resolve(process.cwd(), options.directory) : process.cwd();
92
-
93
- const jobStore = new JobStore();
94
- const gitService = new GitService();
95
- const workspaceService = new WorkspaceService({ gitService });
96
- const sessionManager = new SessionManager({
97
- baseDir,
98
- renderHistoryTools,
99
- debugHistoryEnabled,
100
- logger,
101
- hasConnectedSessionClient
102
- });
103
- const serverChanSettings = new ServerChanSettingsStore();
104
- const notificationService = new NotificationService({
105
- sessionManager,
106
- settingsStore: serverChanSettings,
107
- channel: new ServerChanClient(),
108
- logger
109
- });
110
- const usageService = new UsageService({ logger });
111
- const skillHubSettings = new SkillHubSettingsStore();
112
- const skillHubClient = new SkillHubClient({ settingsStore: skillHubSettings });
113
- const skillInstaller = new SkillInstaller({ client: skillHubClient });
114
- await skillInstaller.initialize();
115
- sessionManager.on('output', ({ sessionId, data }) => {
116
- broadcastToSession(sessionId, { type: 'output', data });
117
- });
118
- sessionManager.on('claude-event', ({ sessionId, event }) => {
119
- broadcastToSession(sessionId, { type: 'claude-event', event });
120
- });
121
- sessionManager.on('codex-event', ({ sessionId, event }) => {
122
- broadcastToSession(sessionId, { type: 'codex-event', event });
123
- });
124
- sessionManager.on('exit', ({ sessionId }) => {
125
- broadcastToSession(sessionId, { type: 'exit' });
126
- });
127
-
128
- const jobRunner = new JobRunner({
129
- createSession: input => sessionManager.create(input),
130
- getJob: id => jobStore.get(id),
131
- updateJob: (id, patch) => jobStore.patchRuntime(id, patch),
132
- logger
133
- });
134
- const schedulerService = new SchedulerService({ jobStore, jobRunner, logger });
135
- schedulerService.start();
136
-
137
- // API: Get all supported and installed tools
138
- app.get('/api/tools', async (req, res) => {
139
- try {
140
- const tools = await detectInstalledTools();
141
- res.json(tools);
142
- } catch (e) {
143
- res.status(500).json({ error: 'Failed to detect tools' });
144
- }
145
- });
146
-
147
- // API: Get web UI runtime configuration
148
- app.get('/api/config', (req, res) => {
149
- res.json({ defaultWorkingDirectory: baseDir });
150
- });
151
-
152
- registerScheduleRoutes(app, { jobStore, jobRunner });
153
- registerNotificationRoutes(app, {
154
- settingsStore: serverChanSettings,
155
- notificationService
156
- });
157
- registerUsageRoutes(app, { usageService, sendJson: sendCompressedJson });
158
- registerSkillHubRoutes(app, {
159
- settingsStore: skillHubSettings,
160
- client: skillHubClient,
161
- installer: skillInstaller,
162
- sessionManager
163
- });
164
-
165
- // API: List all active sessions
166
- app.get('/api/sessions', (req, res) => {
167
- logger.debug('API: GET /api/sessions');
168
- res.json(sessionManager.list());
169
- });
170
-
171
- app.get('/api/claude-config', (req, res) => {
172
- res.json({ success: true, config: getClaudeRuntimeConfig(process.env) });
173
- });
174
-
175
- // API: Create a new PTY session
176
- app.post('/api/sessions', async (req, res) => {
177
- logger.debug(`API: POST /api/sessions - ${JSON.stringify(req.body)}`);
178
- try {
179
- const { toolKey, workingDirectory, claudeOptions } = req.body;
180
- const session = sessionManager.create({ toolKey, workingDirectory, claudeOptions });
181
- res.json({ id: session.id });
182
- } catch (e) {
183
- logger.error(`API: POST /api/sessions failed: ${e.message}`);
184
- res.status(e.statusCode || 500).json({ error: e.message });
185
- }
186
- });
187
-
188
- // API: Plain text terminal history for mobile-friendly reading
189
- app.get('/api/sessions/:id/history', (req, res) => {
190
- const history = sessionManager.getHistory(req.params.id);
191
- if (!history) return res.status(404).json({ error: 'Session not found' });
192
- sessionManager.logHistoryRequest(req.params.id, req);
193
- sendCompressedJson(req, res, history);
194
- });
195
-
196
- // API: Rename session
197
- app.patch('/api/sessions/:id', (req, res) => {
198
- const session = sessionManager.rename(req.params.id, req.body.name);
199
- if (session) {
200
- res.json({ success: true, name: session.name });
201
- } else {
202
- res.status(404).json({ error: 'Session not found' });
203
- }
204
- });
205
-
206
- // API: Mark a session completion indicator as read
207
- app.post('/api/sessions/:id/completion/read', (req, res) => {
208
- const session = sessionManager.markCompletionRead(req.params.id);
209
- if (!session) return res.status(404).json({ error: 'Session not found' });
210
- res.json({ success: true });
211
- });
212
-
213
- app.get('/api/sessions/:id/timed-inputs', (req, res) => {
214
- const items = sessionManager.listTimedInputs(req.params.id);
215
- if (!items) return res.status(404).json({ error: 'Session not found' });
216
- res.json({ success: true, items });
217
- });
218
-
219
- app.post('/api/sessions/:id/timed-inputs', (req, res) => {
220
- try {
221
- const item = sessionManager.scheduleTimedInput(req.params.id, req.body || {});
222
- if (!item) return res.status(404).json({ error: 'Session not found' });
223
- res.json({ success: true, item });
224
- } catch (e) {
225
- res.status(e.statusCode || 500).json({ error: e.message });
226
- }
227
- });
228
-
229
- app.patch('/api/sessions/:id/timed-inputs/:inputId', (req, res) => {
230
- try {
231
- const item = sessionManager.updateTimedInput(req.params.id, req.params.inputId, req.body || {});
232
- if (item === null) return res.status(404).json({ error: 'Session not found' });
233
- if (!item) return res.status(404).json({ error: 'Timed input not found' });
234
- res.json({ success: true, item });
235
- } catch (e) {
236
- res.status(e.statusCode || 500).json({ error: e.message });
237
- }
238
- });
239
-
240
- app.delete('/api/sessions/:id/timed-inputs/:inputId', (req, res) => {
241
- const cancelled = sessionManager.cancelTimedInput(req.params.id, req.params.inputId);
242
- if (cancelled === null) return res.status(404).json({ error: 'Session not found' });
243
- if (!cancelled) return res.status(404).json({ error: 'Timed input not found' });
244
- res.json({ success: true });
245
- });
246
-
247
- // Browser images are stored only in a private, per-session temporary directory.
248
- // Structured providers receive either a local path or validated base64 content.
249
- app.post('/api/sessions/:id/attachments/images', express.raw({ type: () => true, limit: '50mb' }), async (req, res) => {
250
- try {
251
- const attachment = await sessionManager.storeImageAttachment(req.params.id, req.body);
252
- res.status(201).json({ success: true, attachment });
253
- } catch (e) {
254
- res.status(e.statusCode || 500).json({ error: e.message });
255
- }
256
- });
257
-
258
- // Mobile Safari can coalesce progress events for a single large request.
259
- // Small sequential chunks let the browser report progress from server receipts.
260
- app.post('/api/sessions/:id/attachments/images/chunks', express.raw({ type: () => true, limit: '1mb' }), async (req, res) => {
261
- try {
262
- const result = await sessionManager.appendImageChunk(req.params.id, {
263
- uploadId: req.get('X-Glad-Upload-Id'),
264
- chunkIndex: req.get('X-Glad-Chunk-Index'),
265
- chunkTotal: req.get('X-Glad-Chunk-Total')
266
- }, req.body);
267
- res.json({ success: true, ...result });
268
- } catch (e) {
269
- res.status(e.statusCode || 500).json({ error: e.message });
270
- }
271
- });
272
-
273
- app.delete('/api/sessions/:id/attachments/images/uploads/:uploadId', async (req, res) => {
274
- try {
275
- const removed = await sessionManager.discardImageUpload(req.params.id, req.params.uploadId);
276
- res.json({ success: true, removed });
277
- } catch (e) {
278
- res.status(e.statusCode || 500).json({ error: e.message });
279
- }
280
- });
281
-
282
- app.delete('/api/sessions/:id/attachments/images/:attachmentId', async (req, res) => {
283
- try {
284
- const removed = await sessionManager.discardImageAttachment(req.params.id, req.params.attachmentId);
285
- if (!removed) return res.status(404).json({ error: 'Image attachment not found' });
286
- res.json({ success: true });
287
- } catch (e) {
288
- res.status(e.statusCode || 500).json({ error: e.message });
289
- }
290
- });
291
-
292
- app.post('/api/sessions/:id/attachments/files/chunks', express.raw({ type: () => true, limit: '1mb' }), async (req, res) => {
293
- try {
294
- const result = await sessionManager.appendFileChunk(req.params.id, {
295
- uploadId: req.get('X-Glad-Upload-Id'),
296
- chunkIndex: req.get('X-Glad-Chunk-Index'),
297
- chunkTotal: req.get('X-Glad-Chunk-Total'),
298
- name: req.get('X-Glad-File-Name')
299
- }, req.body);
300
- res.json({ success: true, ...result });
301
- } catch (e) {
302
- res.status(e.statusCode || 500).json({ error: e.message });
303
- }
304
- });
305
-
306
- app.delete('/api/sessions/:id/attachments/files/uploads/:uploadId', async (req, res) => {
307
- try {
308
- const removed = await sessionManager.discardFileUpload(req.params.id, req.params.uploadId);
309
- res.json({ success: true, removed });
310
- } catch (e) {
311
- res.status(e.statusCode || 500).json({ error: e.message });
312
- }
313
- });
314
-
315
- app.delete('/api/sessions/:id/attachments/files/:attachmentId', async (req, res) => {
316
- try {
317
- const removed = await sessionManager.discardFileAttachment(req.params.id, req.params.attachmentId);
318
- if (!removed) return res.status(404).json({ error: 'File attachment not found' });
319
- res.json({ success: true });
320
- } catch (e) {
321
- res.status(e.statusCode || 500).json({ error: e.message });
322
- }
323
- });
324
-
325
- // API: Delete/Kill session
326
- app.delete('/api/sessions/:id', async (req, res) => {
327
- const deleted = await sessionManager.kill(req.params.id);
328
- if (!deleted) return res.status(404).json({ error: 'Session not found' });
329
- res.json({ success: true });
330
- });
331
-
332
- app.get('/api/sessions/:id/debug', (req, res) => {
333
- const diagnostics = sessionManager.getDiagnostics(req.params.id);
334
- if (!diagnostics) return res.status(404).json({ error: 'Session not found' });
335
- res.json({ success: true, diagnostics });
336
- });
337
-
338
- registerProviderRoutes(app, { sessionManager });
339
-
340
- registerWorkspaceRoutes(app, {
341
- sessionManager,
342
- gitService,
343
- workspaceService,
344
- getWorkingDirectory: getSessionWorkingDirectory
345
- });
346
-
347
-
348
- function broadcastToSession(sessionId, message) {
349
- const msgStr = JSON.stringify(message);
350
- wss.clients.forEach(client => {
351
- if (client.readyState === 1 && client.sessionId === sessionId) {
352
- client.send(msgStr);
353
- }
354
- });
355
- }
356
-
357
- function hasConnectedSessionClient(sessionId) {
358
- for (const client of wss.clients) {
359
- if (client.readyState === 1 && client.sessionId === sessionId) return true;
360
- }
361
- return false;
362
- }
363
-
364
- // WebSocket: Terminal I/O
365
- wss.on('connection', (ws, req) => {
366
- const url = new URL(req.url, 'http://' + req.headers.host);
367
- const sessionId = url.searchParams.get('sessionId');
368
-
369
- if (!sessionId || !sessionManager.has(sessionId)) {
370
- ws.close(4001, 'Invalid Session ID');
371
- return;
372
- }
373
-
374
- ws.sessionId = sessionId;
375
- const session = sessionManager.get(sessionId);
376
- const isReconnect = session.hasConnectedWebClient;
377
- session.hasConnectedWebClient = true;
378
- if (!session.resizeOwner) {
379
- session.resizeOwner = ws;
380
- }
381
- sessionManager.logWsConnected(sessionId, req);
382
-
383
- if (session.kind === 'claude-structured') {
384
- ws.send(JSON.stringify({ type: 'claude-snapshot', snapshot: sessionManager.getClaudeSnapshot(sessionId) }));
385
- }
386
- if (session.kind === 'codex-structured') {
387
- ws.send(JSON.stringify({ type: 'codex-snapshot', snapshot: sessionManager.getCodexSnapshot(sessionId) }));
388
- }
389
-
390
- // Send catchup output. TUI tools may skip the raw circular buffer, so fall
391
- // back to the rendered/text history snapshot instead of reconnecting blank.
392
- const catchup = sessionManager.getCatchupOutput(sessionId);
393
- ws.needsTuiRedraw = !['claude-structured', 'codex-structured'].includes(session.kind)
394
- && ['antigravity', 'claude-code', 'codex'].includes(session.tool.key)
395
- && (isReconnect || (catchup && catchup.source === 'rendered-history'));
396
- if (ws.needsTuiRedraw) {
397
- ws.send(JSON.stringify({ type: 'reset' }));
398
- } else if (!['claude-structured', 'codex-structured'].includes(session.kind) && catchup && catchup.data) {
399
- sessionManager.logWsCatchupOutput(sessionId, catchup);
400
- ws.send(JSON.stringify({ type: 'output', data: catchup.data }));
401
- }
402
-
403
- ws.on('message', (message) => {
404
- try {
405
- const payload = JSON.parse(message);
406
- if (payload.type === 'input') {
407
- session.write(payload.data);
408
- }
409
- if (payload.type === 'file-input') {
410
- sessionManager.sendTerminalFileInput(sessionId, payload.text || '', payload.fileAttachmentIds || []);
411
- }
412
- if (payload.type === 'claude-input') {
413
- sessionManager.sendClaudeInput(sessionId, payload.text || '', payload.attachmentIds || [], payload.fileAttachmentIds || [])
414
- .catch(error => logger.error(`Claude input error: ${error.message}`));
415
- }
416
- if (payload.type === 'claude-permission') {
417
- sessionManager.respondClaudePermission(sessionId, payload.id, Boolean(payload.approved), payload.action || null);
418
- }
419
- if (payload.type === 'claude-settings') {
420
- sessionManager.updateClaudeSettings(sessionId, payload.settings || {});
421
- }
422
- if (payload.type === 'claude-usage') {
423
- sessionManager.showClaudeUsage(sessionId).catch(error => logger.error(`Claude usage error: ${error.message}`));
424
- }
425
- if (payload.type === 'claude-context') {
426
- sessionManager.showClaudeContext(sessionId).catch(error => logger.error(`Claude context error: ${error.message}`));
427
- }
428
- if (payload.type === 'claude-abort') {
429
- sessionManager.abortClaude(sessionId);
430
- }
431
- if (payload.type === 'codex-input') {
432
- sessionManager.sendCodexInput(sessionId, payload.text || '', payload.attachmentIds || [], payload.skills || [], payload.fileAttachmentIds || [])
433
- .catch(error => logger.error(`Codex input error: ${error.message}`));
434
- }
435
- if (payload.type === 'codex-permission') {
436
- const codex = sessionManager.get(sessionId);
437
- if (codex && codex.kind === 'codex-structured') {
438
- codex.respondPermission(payload.id, payload.decision || Boolean(payload.approved));
439
- }
440
- }
441
- if (payload.type === 'codex-settings') {
442
- sessionManager.updateCodexSettings(sessionId, payload.settings || {}).catch(error => logger.error(`Codex settings error: ${error.message}`));
443
- }
444
- if (payload.type === 'codex-status') {
445
- sessionManager.showCodexStatus(sessionId).catch(error => logger.error(`Codex status error: ${error.message}`));
446
- }
447
- if (payload.type === 'codex-compact') {
448
- sessionManager.compactCodexContext(sessionId).catch(error => logger.error(`Codex compact error: ${error.message}`));
449
- }
450
- if (payload.type === 'codex-detail-request') {
451
- const codex = sessionManager.get(sessionId);
452
- if (codex && codex.kind === 'codex-structured') {
453
- ws.send(JSON.stringify({
454
- type: 'codex-detail-response',
455
- requestId: payload.requestId || null,
456
- detail: codex.getMessageDetails({ ids: payload.ids, threadId: payload.threadId })
457
- }));
458
- }
459
- }
460
- if (payload.type === 'codex-abort') {
461
- sessionManager.abortCodex(sessionId);
462
- }
463
- if (payload.type === 'claude-resume') {
464
- sessionManager.resumeClaude(sessionId, payload.resumeSessionId || '');
465
- }
466
- if (payload.type === 'resize' && session.resizeOwner === ws) {
467
- sessionManager.logWsResize(sessionId, payload.cols, payload.rows);
468
- if (ws.needsTuiRedraw) {
469
- ws.needsTuiRedraw = false;
470
- sessionManager.redraw(sessionId, payload.cols, payload.rows);
471
- } else {
472
- sessionManager.resize(sessionId, payload.cols, payload.rows);
473
- }
474
- }
475
- } catch (e) {
476
- logger.error('WS Message Error: ' + e.message);
477
- }
478
- });
479
-
480
- ws.on('close', () => {
481
- sessionManager.logWsClosed(sessionId);
482
- if (session.resizeOwner !== ws) return;
483
- session.resizeOwner = null;
484
- for (const client of wss.clients) {
485
- if (client.readyState === 1 && client.sessionId === sessionId) {
486
- session.resizeOwner = client;
487
- break;
488
- }
489
- }
490
- });
491
- });
492
-
493
- // Frontend routes
494
- const assetsDir = path.resolve(__dirname, '../../assets');
495
- const webDir = path.join(__dirname, '../web');
496
- const xtermScript = require.resolve('@xterm/xterm');
497
- const xtermStyles = path.resolve(path.dirname(xtermScript), '../css/xterm.css');
498
- const fitAddonScript = require.resolve('@xterm/addon-fit');
499
-
500
- const sendLogo = (req, res) => {
501
- res.sendFile('logo.svg', { root: assetsDir }, error => {
502
- if (error && !res.headersSent) res.status(404).send('Not found');
503
- });
504
- };
505
-
506
- app.get('/', (req, res) => {
507
- try {
508
- const htmlPath = path.join(__dirname, '../web/index.html');
509
- const html = fs.readFileSync(htmlPath, 'utf8');
510
- res.send(html);
511
- } catch (e) {
512
- res.status(500).send('UI not found');
513
- }
514
- });
515
-
516
- const sendWebAsset = assetName => (req, res) => {
517
- res.sendFile(assetName, { root: webDir }, error => {
518
- if (error && !res.headersSent) res.status(404).send('Not found');
519
- });
520
- };
521
-
522
- const sendDependencyAsset = assetPath => (req, res) => {
523
- res.sendFile(path.basename(assetPath), { root: path.dirname(assetPath) });
524
- };
525
-
526
- const webAssets = [
527
- 'bootstrap.js',
528
- 'gitgraph.js',
529
- 'styles.css',
530
- 'theme.js',
531
- 'core.js',
532
- 'layout.js',
533
- 'notifications.js',
534
- 'skillhub.js',
535
- 'claude.js',
536
- 'schedules.js',
537
- 'shell.js',
538
- 'codex.js',
539
- 'session.js',
540
- 'composer.js',
541
- 'timed-inputs.js',
542
- 'terminal-scroll.js',
543
- 'git.js',
544
- 'usage.js'
545
- ];
546
- for (const assetName of webAssets) {
547
- const escapedName = assetName.replace('.', '\\.');
548
- app.get([`/${assetName}`, new RegExp(`.*\\/${escapedName}$`)], sendWebAsset(assetName));
549
- }
550
-
551
- app.get('/vendor/xterm.js', sendDependencyAsset(xtermScript));
552
- app.get('/vendor/xterm.css', sendDependencyAsset(xtermStyles));
553
- app.get('/vendor/xterm-addon-fit.js', sendDependencyAsset(fitAddonScript));
554
-
555
- app.get(['/logo.svg', /.*\/logo\.svg$/], sendLogo);
556
-
557
- app.get(['/favicon.ico', /.*\/favicon\.ico$/], (req, res) => {
558
- res.type('image/svg+xml');
559
- sendLogo(req, res);
560
- });
561
-
562
- app.get(['/manifest.json', /.*\/manifest\.json$/], (req, res) => res.json({
563
- name: "Glad Web",
564
- short_name: "Glad",
565
- start_url: ".",
566
- display: "standalone",
567
- background_color: "#000000",
568
- theme_color: "#007aff",
569
- icons: [
570
- {
571
- src: "logo.svg",
572
- sizes: "any",
573
- type: "image/svg+xml"
574
- }
575
- ]
576
- }));
577
-
578
- server.listen(port, '0.0.0.0', () => {
579
- const interfaces = os.networkInterfaces();
580
- let networkInfo = '';
581
- for (const name of Object.keys(interfaces)) {
582
- for (const iface of interfaces[name]) {
583
- if (iface.family === 'IPv4' && !iface.internal) {
584
- networkInfo += `\n āžœ Network: http://${iface.address}:${port}`;
585
- }
586
- }
587
- }
588
- console.log(chalk.green(`\nšŸš€ Glad Web Server is running!`));
589
- console.log(chalk.cyan(` āžœ Local: http://localhost:${port}${networkInfo}\n`));
590
- console.log(chalk.gray(` āžœ Project: ${baseDir}\n`));
591
- console.log(chalk.gray(` āžœ History Render Tools: ${Array.from(renderHistoryTools).join(', ') || '(none)'}\n`));
592
- console.log(chalk.gray(`Tips: Access from your phone via the Network URL above.\n`));
593
- });
594
-
595
- const shutdown = () => {
596
- schedulerService.stop();
597
- notificationService.stop();
598
- sessionManager.killAll();
599
- process.exit(0);
600
- };
601
- process.once('SIGINT', shutdown);
602
- process.once('SIGTERM', shutdown);
603
- }
604
-
605
- module.exports = webCommand;
@@ -1,17 +0,0 @@
1
- /**
2
- * Application constants
3
- *
4
- * All magic numbers and configuration values should be defined here.
5
- */
6
-
7
- module.exports = {
8
- // Network & WebSocket
9
- HEARTBEAT_TIMEOUT: 13000, // 13s - detect network loss (server pings every ~5s)
10
- CLI_IDLE_THRESHOLD: 15000, // 15s - no PTY output = CLI is idle
11
-
12
- // Buffer
13
- DEFAULT_BUFFER_SIZE: 100000, // 100KB - circular buffer max size
14
-
15
- // Reconnection
16
- MAX_RECONNECT_ATTEMPTS: 10, // Max WebSocket reconnection attempts
17
- };