@siteboon/claude-code-ui 1.25.2 → 1.26.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 (43) hide show
  1. package/README.de.md +239 -0
  2. package/README.ja.md +115 -230
  3. package/README.ko.md +116 -231
  4. package/README.md +2 -1
  5. package/README.ru.md +75 -54
  6. package/README.zh-CN.md +121 -238
  7. package/dist/assets/index-BenyXiE2.css +32 -0
  8. package/dist/assets/{index-DF_FFT3b.js → index-dyw-e9VE.js} +249 -237
  9. package/dist/index.html +2 -2
  10. package/dist/sw.js +102 -27
  11. package/package.json +3 -2
  12. package/server/claude-sdk.js +106 -62
  13. package/server/cli.js +10 -7
  14. package/server/cursor-cli.js +59 -73
  15. package/server/database/db.js +142 -1
  16. package/server/database/init.sql +28 -1
  17. package/server/gemini-cli.js +46 -48
  18. package/server/gemini-response-handler.js +12 -73
  19. package/server/index.js +82 -55
  20. package/server/middleware/auth.js +2 -2
  21. package/server/openai-codex.js +43 -28
  22. package/server/projects.js +1 -1
  23. package/server/providers/claude/adapter.js +278 -0
  24. package/server/providers/codex/adapter.js +248 -0
  25. package/server/providers/cursor/adapter.js +353 -0
  26. package/server/providers/gemini/adapter.js +186 -0
  27. package/server/providers/registry.js +44 -0
  28. package/server/providers/types.js +119 -0
  29. package/server/providers/utils.js +29 -0
  30. package/server/routes/agent.js +7 -5
  31. package/server/routes/cli-auth.js +38 -0
  32. package/server/routes/codex.js +1 -19
  33. package/server/routes/gemini.js +0 -30
  34. package/server/routes/git.js +48 -20
  35. package/server/routes/messages.js +61 -0
  36. package/server/routes/plugins.js +5 -1
  37. package/server/routes/settings.js +99 -1
  38. package/server/routes/taskmaster.js +2 -2
  39. package/server/services/notification-orchestrator.js +227 -0
  40. package/server/services/vapid-keys.js +35 -0
  41. package/server/utils/plugin-loader.js +53 -4
  42. package/shared/networkHosts.js +22 -0
  43. package/dist/assets/index-WNTmA_ug.css +0 -32
@@ -1,4 +1,6 @@
1
1
  // Gemini Response Handler - JSON Stream processing
2
+ import { geminiAdapter } from './providers/gemini/adapter.js';
3
+
2
4
  class GeminiResponseHandler {
3
5
  constructor(ws, options = {}) {
4
6
  this.ws = ws;
@@ -27,13 +29,12 @@ class GeminiResponseHandler {
27
29
  this.handleEvent(event);
28
30
  } catch (err) {
29
31
  // Not a JSON line, probably debug output or CLI warnings
30
- // console.error('[Gemini Handler] Non-JSON line ignored:', line);
31
32
  }
32
33
  }
33
34
  }
34
35
 
35
36
  handleEvent(event) {
36
- const socketSessionId = typeof this.ws.getSessionId === 'function' ? this.ws.getSessionId() : null;
37
+ const sid = typeof this.ws.getSessionId === 'function' ? this.ws.getSessionId() : null;
37
38
 
38
39
  if (event.type === 'init') {
39
40
  if (this.onInit) {
@@ -42,88 +43,26 @@ class GeminiResponseHandler {
42
43
  return;
43
44
  }
44
45
 
46
+ // Invoke per-type callbacks for session tracking
45
47
  if (event.type === 'message' && event.role === 'assistant') {
46
48
  const content = event.content || '';
47
-
48
- // Notify the parent CLI handler of accumulated text
49
49
  if (this.onContentFragment && content) {
50
50
  this.onContentFragment(content);
51
51
  }
52
-
53
- let payload = {
54
- type: 'gemini-response',
55
- data: {
56
- type: 'message',
57
- content: content,
58
- isPartial: event.delta === true
59
- }
60
- };
61
- if (socketSessionId) payload.sessionId = socketSessionId;
62
- this.ws.send(payload);
63
- }
64
- else if (event.type === 'tool_use') {
65
- if (this.onToolUse) {
66
- this.onToolUse(event);
67
- }
68
- let payload = {
69
- type: 'gemini-tool-use',
70
- toolName: event.tool_name,
71
- toolId: event.tool_id,
72
- parameters: event.parameters || {}
73
- };
74
- if (socketSessionId) payload.sessionId = socketSessionId;
75
- this.ws.send(payload);
52
+ } else if (event.type === 'tool_use' && this.onToolUse) {
53
+ this.onToolUse(event);
54
+ } else if (event.type === 'tool_result' && this.onToolResult) {
55
+ this.onToolResult(event);
76
56
  }
77
- else if (event.type === 'tool_result') {
78
- if (this.onToolResult) {
79
- this.onToolResult(event);
80
- }
81
- let payload = {
82
- type: 'gemini-tool-result',
83
- toolId: event.tool_id,
84
- status: event.status,
85
- output: event.output || ''
86
- };
87
- if (socketSessionId) payload.sessionId = socketSessionId;
88
- this.ws.send(payload);
89
- }
90
- else if (event.type === 'result') {
91
- // Send a finalize message string
92
- let payload = {
93
- type: 'gemini-response',
94
- data: {
95
- type: 'message',
96
- content: '',
97
- isPartial: false
98
- }
99
- };
100
- if (socketSessionId) payload.sessionId = socketSessionId;
101
- this.ws.send(payload);
102
57
 
103
- if (event.stats && event.stats.total_tokens) {
104
- let statsPayload = {
105
- type: 'claude-status',
106
- data: {
107
- status: 'Complete',
108
- tokens: event.stats.total_tokens
109
- }
110
- };
111
- if (socketSessionId) statsPayload.sessionId = socketSessionId;
112
- this.ws.send(statsPayload);
113
- }
114
- }
115
- else if (event.type === 'error') {
116
- let payload = {
117
- type: 'gemini-error',
118
- error: event.error || event.message || 'Unknown Gemini streaming error'
119
- };
120
- if (socketSessionId) payload.sessionId = socketSessionId;
121
- this.ws.send(payload);
58
+ // Normalize via adapter and send all resulting messages
59
+ const normalized = geminiAdapter.normalizeMessage(event, sid);
60
+ for (const msg of normalized) {
61
+ this.ws.send(msg);
122
62
  }
123
63
  }
124
64
 
125
65
  forceFlush() {
126
- // If the buffer has content, try to parse it one last time
127
66
  if (this.buffer.trim()) {
128
67
  try {
129
68
  const event = JSON.parse(this.buffer);
package/server/index.js CHANGED
@@ -31,7 +31,7 @@ const c = {
31
31
  dim: (text) => `${colors.dim}${text}${colors.reset}`,
32
32
  };
33
33
 
34
- console.log('PORT from env:', process.env.PORT);
34
+ console.log('SERVER_PORT from env:', process.env.SERVER_PORT);
35
35
 
36
36
  import express from 'express';
37
37
  import { WebSocketServer, WebSocket } from 'ws';
@@ -44,7 +44,7 @@ import pty from 'node-pty';
44
44
  import fetch from 'node-fetch';
45
45
  import mime from 'mime-types';
46
46
 
47
- import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache, searchConversations } from './projects.js';
47
+ import { getProjects, getSessions, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache, searchConversations } from './projects.js';
48
48
  import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval, getPendingApprovalsForSession, reconnectSessionWriter } from './claude-sdk.js';
49
49
  import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js';
50
50
  import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js';
@@ -65,10 +65,14 @@ import userRoutes from './routes/user.js';
65
65
  import codexRoutes from './routes/codex.js';
66
66
  import geminiRoutes from './routes/gemini.js';
67
67
  import pluginsRoutes from './routes/plugins.js';
68
- import { startEnabledPluginServers, stopAllPlugins } from './utils/plugin-process-manager.js';
68
+ import messagesRoutes from './routes/messages.js';
69
+ import { createNormalizedMessage } from './providers/types.js';
70
+ import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
69
71
  import { initializeDatabase, sessionNamesDb, applyCustomSessionNames } from './database/db.js';
72
+ import { configureWebPush } from './services/vapid-keys.js';
70
73
  import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
71
74
  import { IS_PLATFORM } from './constants/config.js';
75
+ import { getConnectableHost } from '../shared/networkHosts.js';
72
76
 
73
77
  const VALID_PROVIDERS = ['claude', 'codex', 'cursor', 'gemini'];
74
78
 
@@ -394,6 +398,9 @@ app.use('/api/gemini', authenticateToken, geminiRoutes);
394
398
  // Plugins API Routes (protected)
395
399
  app.use('/api/plugins', authenticateToken, pluginsRoutes);
396
400
 
401
+ // Unified session messages route (protected)
402
+ app.use('/api/sessions', authenticateToken, messagesRoutes);
403
+
397
404
  // Agent API Routes (uses API key authentication)
398
405
  app.use('/api/agent', agentRoutes);
399
406
 
@@ -507,31 +514,6 @@ app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, re
507
514
  }
508
515
  });
509
516
 
510
- // Get messages for a specific session
511
- app.get('/api/projects/:projectName/sessions/:sessionId/messages', authenticateToken, async (req, res) => {
512
- try {
513
- const { projectName, sessionId } = req.params;
514
- const { limit, offset } = req.query;
515
-
516
- // Parse limit and offset if provided
517
- const parsedLimit = limit ? parseInt(limit, 10) : null;
518
- const parsedOffset = offset ? parseInt(offset, 10) : 0;
519
-
520
- const result = await getSessionMessages(projectName, sessionId, parsedLimit, parsedOffset);
521
-
522
- // Handle both old and new response formats
523
- if (Array.isArray(result)) {
524
- // Backward compatibility: no pagination parameters were provided
525
- res.json({ messages: result });
526
- } else {
527
- // New format with pagination info
528
- res.json(result);
529
- }
530
- } catch (error) {
531
- res.status(500).json({ error: error.message });
532
- }
533
- });
534
-
535
517
  // Rename project endpoint
536
518
  app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res) => {
537
519
  try {
@@ -956,7 +938,6 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
956
938
  }
957
939
 
958
940
  const files = await getFileTree(actualPath, 10, 0, true);
959
- const hiddenFiles = files.filter(f => f.name.startsWith('.'));
960
941
  res.json(files);
961
942
  } catch (error) {
962
943
  console.error('[ERROR] File tree error:', error.message);
@@ -1394,6 +1375,50 @@ const uploadFilesHandler = async (req, res) => {
1394
1375
 
1395
1376
  app.post('/api/projects/:projectName/files/upload', authenticateToken, uploadFilesHandler);
1396
1377
 
1378
+ /**
1379
+ * Proxy an authenticated client WebSocket to a plugin's internal WS server.
1380
+ * Auth is enforced by verifyClient before this function is reached.
1381
+ */
1382
+ function handlePluginWsProxy(clientWs, pathname) {
1383
+ const pluginName = pathname.replace('/plugin-ws/', '');
1384
+ if (!pluginName || /[^a-zA-Z0-9_-]/.test(pluginName)) {
1385
+ clientWs.close(4400, 'Invalid plugin name');
1386
+ return;
1387
+ }
1388
+
1389
+ const port = getPluginPort(pluginName);
1390
+ if (!port) {
1391
+ clientWs.close(4404, 'Plugin not running');
1392
+ return;
1393
+ }
1394
+
1395
+ const upstream = new WebSocket(`ws://127.0.0.1:${port}/ws`);
1396
+
1397
+ upstream.on('open', () => {
1398
+ console.log(`[Plugins] WS proxy connected to "${pluginName}" on port ${port}`);
1399
+ });
1400
+
1401
+ // Relay messages bidirectionally
1402
+ upstream.on('message', (data) => {
1403
+ if (clientWs.readyState === WebSocket.OPEN) clientWs.send(data);
1404
+ });
1405
+ clientWs.on('message', (data) => {
1406
+ if (upstream.readyState === WebSocket.OPEN) upstream.send(data);
1407
+ });
1408
+
1409
+ // Propagate close in both directions
1410
+ upstream.on('close', () => { if (clientWs.readyState === WebSocket.OPEN) clientWs.close(); });
1411
+ clientWs.on('close', () => { if (upstream.readyState === WebSocket.OPEN) upstream.close(); });
1412
+
1413
+ upstream.on('error', (err) => {
1414
+ console.error(`[Plugins] WS proxy error for "${pluginName}":`, err.message);
1415
+ if (clientWs.readyState === WebSocket.OPEN) clientWs.close(4502, 'Upstream error');
1416
+ });
1417
+ clientWs.on('error', () => {
1418
+ if (upstream.readyState === WebSocket.OPEN) upstream.close();
1419
+ });
1420
+ }
1421
+
1397
1422
  // WebSocket connection handler that routes based on URL path
1398
1423
  wss.on('connection', (ws, request) => {
1399
1424
  const url = request.url;
@@ -1406,7 +1431,9 @@ wss.on('connection', (ws, request) => {
1406
1431
  if (pathname === '/shell') {
1407
1432
  handleShellConnection(ws);
1408
1433
  } else if (pathname === '/ws') {
1409
- handleChatConnection(ws);
1434
+ handleChatConnection(ws, request);
1435
+ } else if (pathname.startsWith('/plugin-ws/')) {
1436
+ handlePluginWsProxy(ws, pathname);
1410
1437
  } else {
1411
1438
  console.log('[WARN] Unknown WebSocket path:', pathname);
1412
1439
  ws.close();
@@ -1415,17 +1442,21 @@ wss.on('connection', (ws, request) => {
1415
1442
 
1416
1443
  /**
1417
1444
  * WebSocket Writer - Wrapper for WebSocket to match SSEStreamWriter interface
1445
+ *
1446
+ * Provider files use `createNormalizedMessage()` from `providers/types.js` and
1447
+ * adapter `normalizeMessage()` to produce unified NormalizedMessage events.
1448
+ * The writer simply serialises and sends.
1418
1449
  */
1419
1450
  class WebSocketWriter {
1420
- constructor(ws) {
1451
+ constructor(ws, userId = null) {
1421
1452
  this.ws = ws;
1422
1453
  this.sessionId = null;
1454
+ this.userId = userId;
1423
1455
  this.isWebSocketWriter = true; // Marker for transport detection
1424
1456
  }
1425
1457
 
1426
1458
  send(data) {
1427
1459
  if (this.ws.readyState === 1) { // WebSocket.OPEN
1428
- // Providers send raw objects, we stringify for WebSocket
1429
1460
  this.ws.send(JSON.stringify(data));
1430
1461
  }
1431
1462
  }
@@ -1444,14 +1475,14 @@ class WebSocketWriter {
1444
1475
  }
1445
1476
 
1446
1477
  // Handle chat WebSocket connections
1447
- function handleChatConnection(ws) {
1478
+ function handleChatConnection(ws, request) {
1448
1479
  console.log('[INFO] Chat WebSocket connected');
1449
1480
 
1450
1481
  // Add to connected clients for project updates
1451
1482
  connectedClients.add(ws);
1452
1483
 
1453
1484
  // Wrap WebSocket with writer for consistent interface with SSEStreamWriter
1454
- const writer = new WebSocketWriter(ws);
1485
+ const writer = new WebSocketWriter(ws, request?.user?.id ?? request?.user?.userId ?? null);
1455
1486
 
1456
1487
  ws.on('message', async (message) => {
1457
1488
  try {
@@ -1506,12 +1537,7 @@ function handleChatConnection(ws) {
1506
1537
  success = await abortClaudeSDKSession(data.sessionId);
1507
1538
  }
1508
1539
 
1509
- writer.send({
1510
- type: 'session-aborted',
1511
- sessionId: data.sessionId,
1512
- provider,
1513
- success
1514
- });
1540
+ writer.send(createNormalizedMessage({ kind: 'complete', exitCode: success ? 0 : 1, aborted: true, success, sessionId: data.sessionId, provider }));
1515
1541
  } else if (data.type === 'claude-permission-response') {
1516
1542
  // Relay UI approval decisions back into the SDK control flow.
1517
1543
  // This does not persist permissions; it only resolves the in-flight request,
@@ -1527,12 +1553,7 @@ function handleChatConnection(ws) {
1527
1553
  } else if (data.type === 'cursor-abort') {
1528
1554
  console.log('[DEBUG] Abort Cursor session:', data.sessionId);
1529
1555
  const success = abortCursorSession(data.sessionId);
1530
- writer.send({
1531
- type: 'session-aborted',
1532
- sessionId: data.sessionId,
1533
- provider: 'cursor',
1534
- success
1535
- });
1556
+ writer.send(createNormalizedMessage({ kind: 'complete', exitCode: success ? 0 : 1, aborted: true, success, sessionId: data.sessionId, provider: 'cursor' }));
1536
1557
  } else if (data.type === 'check-session-status') {
1537
1558
  // Check if a specific session is currently processing
1538
1559
  const provider = data.provider || 'claude';
@@ -2401,7 +2422,8 @@ app.get('*', (req, res) => {
2401
2422
  res.sendFile(indexPath);
2402
2423
  } else {
2403
2424
  // In development, redirect to Vite dev server only if dist doesn't exist
2404
- res.redirect(`http://localhost:${process.env.VITE_PORT || 5173}`);
2425
+ const redirectHost = getConnectableHost(req.hostname);
2426
+ res.redirect(`${req.protocol}://${redirectHost}:${VITE_PORT}`);
2405
2427
  }
2406
2428
  });
2407
2429
 
@@ -2489,10 +2511,10 @@ async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden =
2489
2511
  });
2490
2512
  }
2491
2513
 
2492
- const PORT = process.env.PORT || 3001;
2514
+ const SERVER_PORT = process.env.SERVER_PORT || 3001;
2493
2515
  const HOST = process.env.HOST || '0.0.0.0';
2494
- // Show localhost in URL when binding to all interfaces (0.0.0.0 isn't a connectable address)
2495
- const DISPLAY_HOST = HOST === '0.0.0.0' ? 'localhost' : HOST;
2516
+ const DISPLAY_HOST = getConnectableHost(HOST);
2517
+ const VITE_PORT = process.env.VITE_PORT || 5173;
2496
2518
 
2497
2519
  // Initialize database and start server
2498
2520
  async function startServer() {
@@ -2500,19 +2522,24 @@ async function startServer() {
2500
2522
  // Initialize authentication database
2501
2523
  await initializeDatabase();
2502
2524
 
2525
+ // Configure Web Push (VAPID keys)
2526
+ configureWebPush();
2527
+
2503
2528
  // Check if running in production mode (dist folder exists)
2504
2529
  const distIndexPath = path.join(__dirname, '../dist/index.html');
2505
2530
  const isProduction = fs.existsSync(distIndexPath);
2506
2531
 
2507
2532
  // Log Claude implementation mode
2508
2533
  console.log(`${c.info('[INFO]')} Using Claude Agents SDK for Claude integration`);
2509
- console.log(`${c.info('[INFO]')} Running in ${c.bright(isProduction ? 'PRODUCTION' : 'DEVELOPMENT')} mode`);
2534
+ console.log('');
2510
2535
 
2511
- if (!isProduction) {
2512
- console.log(`${c.warn('[WARN]')} Note: Requests will be proxied to Vite dev server at ${c.dim('http://localhost:' + (process.env.VITE_PORT || 5173))}`);
2536
+ if (isProduction) {
2537
+ console.log(`${c.info('[INFO]')} To run in production mode, go to http://${DISPLAY_HOST}:${SERVER_PORT}`);
2513
2538
  }
2514
2539
 
2515
- server.listen(PORT, HOST, async () => {
2540
+ console.log(`${c.info('[INFO]')} To run in development mode with hot-module replacement, go to http://${DISPLAY_HOST}:${VITE_PORT}`);
2541
+
2542
+ server.listen(SERVER_PORT, HOST, async () => {
2516
2543
  const appInstallPath = path.join(__dirname, '..');
2517
2544
 
2518
2545
  console.log('');
@@ -2520,7 +2547,7 @@ async function startServer() {
2520
2547
  console.log(` ${c.bright('Claude Code UI Server - Ready')}`);
2521
2548
  console.log(c.dim('═'.repeat(63)));
2522
2549
  console.log('');
2523
- console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://' + DISPLAY_HOST + ':' + PORT)}`);
2550
+ console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://' + DISPLAY_HOST + ':' + SERVER_PORT)}`);
2524
2551
  console.log(`${c.info('[INFO]')} Installed at: ${c.dim(appInstallPath)}`);
2525
2552
  console.log(`${c.tip('[TIP]')} Run "cloudcli status" for full configuration details`);
2526
2553
  console.log('');
@@ -95,7 +95,7 @@ const authenticateWebSocket = (token) => {
95
95
  try {
96
96
  const user = userDb.getFirstUser();
97
97
  if (user) {
98
- return { userId: user.id, username: user.username };
98
+ return { id: user.id, userId: user.id, username: user.username };
99
99
  }
100
100
  return null;
101
101
  } catch (error) {
@@ -129,4 +129,4 @@ export {
129
129
  generateToken,
130
130
  authenticateWebSocket,
131
131
  JWT_SECRET
132
- };
132
+ };
@@ -14,6 +14,9 @@
14
14
  */
15
15
 
16
16
  import { Codex } from '@openai/codex-sdk';
17
+ import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
18
+ import { codexAdapter } from './providers/codex/adapter.js';
19
+ import { createNormalizedMessage } from './providers/types.js';
17
20
 
18
21
  // Track active sessions
19
22
  const activeCodexSessions = new Map();
@@ -191,6 +194,7 @@ function mapPermissionModeToCodexOptions(permissionMode) {
191
194
  export async function queryCodex(command, options = {}, ws) {
192
195
  const {
193
196
  sessionId,
197
+ sessionSummary,
194
198
  cwd,
195
199
  projectPath,
196
200
  model,
@@ -203,6 +207,7 @@ export async function queryCodex(command, options = {}, ws) {
203
207
  let codex;
204
208
  let thread;
205
209
  let currentSessionId = sessionId;
210
+ let terminalFailure = null;
206
211
  const abortController = new AbortController();
207
212
 
208
213
  try {
@@ -238,11 +243,7 @@ export async function queryCodex(command, options = {}, ws) {
238
243
  });
239
244
 
240
245
  // Send session created event
241
- sendMessage(ws, {
242
- type: 'session-created',
243
- sessionId: currentSessionId,
244
- provider: 'codex'
245
- });
246
+ sendMessage(ws, createNormalizedMessage({ kind: 'session_created', newSessionId: currentSessionId, sessionId: currentSessionId, provider: 'codex' }));
246
247
 
247
248
  // Execute with streaming
248
249
  const streamedTurn = await thread.runStreamed(command, {
@@ -262,32 +263,41 @@ export async function queryCodex(command, options = {}, ws) {
262
263
 
263
264
  const transformed = transformCodexEvent(event);
264
265
 
265
- sendMessage(ws, {
266
- type: 'codex-response',
267
- data: transformed,
268
- sessionId: currentSessionId
269
- });
266
+ // Normalize the transformed event into NormalizedMessage(s) via adapter
267
+ const normalizedMsgs = codexAdapter.normalizeMessage(transformed, currentSessionId);
268
+ for (const msg of normalizedMsgs) {
269
+ sendMessage(ws, msg);
270
+ }
271
+
272
+ if (event.type === 'turn.failed' && !terminalFailure) {
273
+ terminalFailure = event.error || new Error('Turn failed');
274
+ notifyRunFailed({
275
+ userId: ws?.userId || null,
276
+ provider: 'codex',
277
+ sessionId: currentSessionId,
278
+ sessionName: sessionSummary,
279
+ error: terminalFailure
280
+ });
281
+ }
270
282
 
271
283
  // Extract and send token usage if available (normalized to match Claude format)
272
284
  if (event.type === 'turn.completed' && event.usage) {
273
285
  const totalTokens = (event.usage.input_tokens || 0) + (event.usage.output_tokens || 0);
274
- sendMessage(ws, {
275
- type: 'token-budget',
276
- data: {
277
- used: totalTokens,
278
- total: 200000 // Default context window for Codex models
279
- },
280
- sessionId: currentSessionId
281
- });
286
+ sendMessage(ws, createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: { used: totalTokens, total: 200000 }, sessionId: currentSessionId, provider: 'codex' }));
282
287
  }
283
288
  }
284
289
 
285
290
  // Send completion event
286
- sendMessage(ws, {
287
- type: 'codex-complete',
288
- sessionId: currentSessionId,
289
- actualSessionId: thread.id
290
- });
291
+ if (!terminalFailure) {
292
+ sendMessage(ws, createNormalizedMessage({ kind: 'complete', actualSessionId: thread.id, sessionId: currentSessionId, provider: 'codex' }));
293
+ notifyRunStopped({
294
+ userId: ws?.userId || null,
295
+ provider: 'codex',
296
+ sessionId: currentSessionId,
297
+ sessionName: sessionSummary,
298
+ stopReason: 'completed'
299
+ });
300
+ }
291
301
 
292
302
  } catch (error) {
293
303
  const session = currentSessionId ? activeCodexSessions.get(currentSessionId) : null;
@@ -298,11 +308,16 @@ export async function queryCodex(command, options = {}, ws) {
298
308
 
299
309
  if (!wasAborted) {
300
310
  console.error('[Codex] Error:', error);
301
- sendMessage(ws, {
302
- type: 'codex-error',
303
- error: error.message,
304
- sessionId: currentSessionId
305
- });
311
+ sendMessage(ws, createNormalizedMessage({ kind: 'error', content: error.message, sessionId: currentSessionId, provider: 'codex' }));
312
+ if (!terminalFailure) {
313
+ notifyRunFailed({
314
+ userId: ws?.userId || null,
315
+ provider: 'codex',
316
+ sessionId: currentSessionId,
317
+ sessionName: sessionSummary,
318
+ error
319
+ });
320
+ }
306
321
  }
307
322
 
308
323
  } finally {
@@ -1014,7 +1014,7 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
1014
1014
  messages.push(entry);
1015
1015
  }
1016
1016
  } catch (parseError) {
1017
- console.warn('Error parsing line:', parseError.message);
1017
+ // Silently skip malformed JSONL lines (common with concurrent writes)
1018
1018
  }
1019
1019
  }
1020
1020
  }