@siteboon/claude-code-ui 1.25.2 → 1.26.0

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-C08k8QbP.css +32 -0
  8. package/dist/assets/{index-DF_FFT3b.js → index-DnXcHp5q.js} +249 -242
  9. package/dist/index.html +2 -2
  10. package/dist/sw.js +59 -3
  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,5 +1,8 @@
1
1
  import { spawn } from 'child_process';
2
2
  import crossSpawn from 'cross-spawn';
3
+ import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
4
+ import { cursorAdapter } from './providers/cursor/adapter.js';
5
+ import { createNormalizedMessage } from './providers/types.js';
3
6
 
4
7
  // Use cross-spawn on Windows for better command execution
5
8
  const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
@@ -23,7 +26,7 @@ function isWorkspaceTrustPrompt(text = '') {
23
26
 
24
27
  async function spawnCursor(command, options = {}, ws) {
25
28
  return new Promise(async (resolve, reject) => {
26
- const { sessionId, projectPath, cwd, resume, toolsSettings, skipPermissions, model } = options;
29
+ const { sessionId, projectPath, cwd, resume, toolsSettings, skipPermissions, model, sessionSummary } = options;
27
30
  let capturedSessionId = sessionId; // Track session ID throughout the process
28
31
  let sessionCreatedSent = false; // Track if we've already sent session-created event
29
32
  let hasRetriedWithTrust = false;
@@ -81,6 +84,35 @@ async function spawnCursor(command, options = {}, ws) {
81
84
  const isTrustRetry = runReason === 'trust-retry';
82
85
  let runSawWorkspaceTrustPrompt = false;
83
86
  let stdoutLineBuffer = '';
87
+ let terminalNotificationSent = false;
88
+
89
+ const notifyTerminalState = ({ code = null, error = null } = {}) => {
90
+ if (terminalNotificationSent) {
91
+ return;
92
+ }
93
+
94
+ terminalNotificationSent = true;
95
+
96
+ const finalSessionId = capturedSessionId || sessionId || processKey;
97
+ if (code === 0 && !error) {
98
+ notifyRunStopped({
99
+ userId: ws?.userId || null,
100
+ provider: 'cursor',
101
+ sessionId: finalSessionId,
102
+ sessionName: sessionSummary,
103
+ stopReason: 'completed'
104
+ });
105
+ return;
106
+ }
107
+
108
+ notifyRunFailed({
109
+ userId: ws?.userId || null,
110
+ provider: 'cursor',
111
+ sessionId: finalSessionId,
112
+ sessionName: sessionSummary,
113
+ error: error || `Cursor CLI exited with code ${code}`
114
+ });
115
+ };
84
116
 
85
117
  if (isTrustRetry) {
86
118
  console.log('Retrying Cursor CLI with --trust after workspace trust prompt');
@@ -142,75 +174,42 @@ async function spawnCursor(command, options = {}, ws) {
142
174
  // Send session-created event only once for new sessions
143
175
  if (!sessionId && !sessionCreatedSent) {
144
176
  sessionCreatedSent = true;
145
- ws.send({
146
- type: 'session-created',
147
- sessionId: capturedSessionId,
148
- model: response.model,
149
- cwd: response.cwd
150
- });
177
+ ws.send(createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, model: response.model, cwd: response.cwd, sessionId: capturedSessionId, provider: 'cursor' }));
151
178
  }
152
179
  }
153
180
 
154
- // Send system info to frontend
155
- ws.send({
156
- type: 'cursor-system',
157
- data: response,
158
- sessionId: capturedSessionId || sessionId || null
159
- });
181
+ // System info no longer needed by the frontend (session-lifecycle 'created' handles nav).
160
182
  }
161
183
  break;
162
184
 
163
185
  case 'user':
164
- // Forward user message
165
- ws.send({
166
- type: 'cursor-user',
167
- data: response,
168
- sessionId: capturedSessionId || sessionId || null
169
- });
186
+ // User messages are not displayed in the UI — skip.
170
187
  break;
171
188
 
172
189
  case 'assistant':
173
190
  // Accumulate assistant message chunks
174
191
  if (response.message && response.message.content && response.message.content.length > 0) {
175
- const textContent = response.message.content[0].text;
176
-
177
- // Send as Claude-compatible format for frontend
178
- ws.send({
179
- type: 'claude-response',
180
- data: {
181
- type: 'content_block_delta',
182
- delta: {
183
- type: 'text_delta',
184
- text: textContent
185
- }
186
- },
187
- sessionId: capturedSessionId || sessionId || null
188
- });
192
+ const normalized = cursorAdapter.normalizeMessage(response, capturedSessionId || sessionId || null);
193
+ for (const msg of normalized) ws.send(msg);
189
194
  }
190
195
  break;
191
196
 
192
- case 'result':
193
- // Session complete
197
+ case 'result': {
198
+ // Session complete — send stream end + lifecycle complete with result payload
194
199
  console.log('Cursor session result:', response);
195
-
196
- // Do not emit an extra content_block_stop here.
197
- // The UI already finalizes the streaming message in cursor-result handling,
198
- // and emitting both can produce duplicate assistant messages.
199
- ws.send({
200
- type: 'cursor-result',
201
- sessionId: capturedSessionId || sessionId,
202
- data: response,
203
- success: response.subtype === 'success'
204
- });
200
+ const resultText = typeof response.result === 'string' ? response.result : '';
201
+ ws.send(createNormalizedMessage({
202
+ kind: 'complete',
203
+ exitCode: response.subtype === 'success' ? 0 : 1,
204
+ resultText,
205
+ isError: response.subtype !== 'success',
206
+ sessionId: capturedSessionId || sessionId, provider: 'cursor',
207
+ }));
205
208
  break;
209
+ }
206
210
 
207
211
  default:
208
- // Forward any other message types
209
- ws.send({
210
- type: 'cursor-response',
211
- data: response,
212
- sessionId: capturedSessionId || sessionId || null
213
- });
212
+ // Unknown message types ignore.
214
213
  }
215
214
  } catch (parseError) {
216
215
  console.log('Non-JSON response:', line);
@@ -219,12 +218,9 @@ async function spawnCursor(command, options = {}, ws) {
219
218
  return;
220
219
  }
221
220
 
222
- // If not JSON, send as raw text
223
- ws.send({
224
- type: 'cursor-output',
225
- data: line,
226
- sessionId: capturedSessionId || sessionId || null
227
- });
221
+ // If not JSON, send as stream delta via adapter
222
+ const normalized = cursorAdapter.normalizeMessage(line, capturedSessionId || sessionId || null);
223
+ for (const msg of normalized) ws.send(msg);
228
224
  }
229
225
  };
230
226
 
@@ -252,11 +248,7 @@ async function spawnCursor(command, options = {}, ws) {
252
248
  return;
253
249
  }
254
250
 
255
- ws.send({
256
- type: 'cursor-error',
257
- error: stderrText,
258
- sessionId: capturedSessionId || sessionId || null
259
- });
251
+ ws.send(createNormalizedMessage({ kind: 'error', content: stderrText, sessionId: capturedSessionId || sessionId || null, provider: 'cursor' }));
260
252
  });
261
253
 
262
254
  // Handle process completion
@@ -283,16 +275,13 @@ async function spawnCursor(command, options = {}, ws) {
283
275
  return;
284
276
  }
285
277
 
286
- ws.send({
287
- type: 'claude-complete',
288
- sessionId: finalSessionId,
289
- exitCode: code,
290
- isNewSession: !sessionId && !!command // Flag to indicate this was a new session
291
- });
278
+ ws.send(createNormalizedMessage({ kind: 'complete', exitCode: code, isNewSession: !sessionId && !!command, sessionId: finalSessionId, provider: 'cursor' }));
292
279
 
293
280
  if (code === 0) {
281
+ notifyTerminalState({ code });
294
282
  settleOnce(() => resolve());
295
283
  } else {
284
+ notifyTerminalState({ code });
296
285
  settleOnce(() => reject(new Error(`Cursor CLI exited with code ${code}`)));
297
286
  }
298
287
  });
@@ -305,11 +294,8 @@ async function spawnCursor(command, options = {}, ws) {
305
294
  const finalSessionId = capturedSessionId || sessionId || processKey;
306
295
  activeCursorProcesses.delete(finalSessionId);
307
296
 
308
- ws.send({
309
- type: 'cursor-error',
310
- error: error.message,
311
- sessionId: capturedSessionId || sessionId || null
312
- });
297
+ ws.send(createNormalizedMessage({ kind: 'error', content: error.message, sessionId: capturedSessionId || sessionId || null, provider: 'cursor' }));
298
+ notifyTerminalState({ error });
313
299
 
314
300
  settleOnce(() => reject(error));
315
301
  });
@@ -100,6 +100,35 @@ const runMigrations = () => {
100
100
  db.exec('ALTER TABLE users ADD COLUMN has_completed_onboarding BOOLEAN DEFAULT 0');
101
101
  }
102
102
 
103
+ db.exec(`
104
+ CREATE TABLE IF NOT EXISTS user_notification_preferences (
105
+ user_id INTEGER PRIMARY KEY,
106
+ preferences_json TEXT NOT NULL,
107
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
108
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
109
+ )
110
+ `);
111
+
112
+ db.exec(`
113
+ CREATE TABLE IF NOT EXISTS vapid_keys (
114
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
115
+ public_key TEXT NOT NULL,
116
+ private_key TEXT NOT NULL,
117
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
118
+ )
119
+ `);
120
+
121
+ db.exec(`
122
+ CREATE TABLE IF NOT EXISTS push_subscriptions (
123
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
124
+ user_id INTEGER NOT NULL,
125
+ endpoint TEXT NOT NULL UNIQUE,
126
+ keys_p256dh TEXT NOT NULL,
127
+ keys_auth TEXT NOT NULL,
128
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
129
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
130
+ )
131
+ `);
103
132
  // Create app_config table if it doesn't exist (for existing installations)
104
133
  db.exec(`CREATE TABLE IF NOT EXISTS app_config (
105
134
  key TEXT PRIMARY KEY,
@@ -376,6 +405,116 @@ const credentialsDb = {
376
405
  }
377
406
  };
378
407
 
408
+ const DEFAULT_NOTIFICATION_PREFERENCES = {
409
+ channels: {
410
+ inApp: false,
411
+ webPush: false
412
+ },
413
+ events: {
414
+ actionRequired: true,
415
+ stop: true,
416
+ error: true
417
+ }
418
+ };
419
+
420
+ const normalizeNotificationPreferences = (value) => {
421
+ const source = value && typeof value === 'object' ? value : {};
422
+
423
+ return {
424
+ channels: {
425
+ inApp: source.channels?.inApp === true,
426
+ webPush: source.channels?.webPush === true
427
+ },
428
+ events: {
429
+ actionRequired: source.events?.actionRequired !== false,
430
+ stop: source.events?.stop !== false,
431
+ error: source.events?.error !== false
432
+ }
433
+ };
434
+ };
435
+
436
+ const notificationPreferencesDb = {
437
+ getPreferences: (userId) => {
438
+ try {
439
+ const row = db.prepare('SELECT preferences_json FROM user_notification_preferences WHERE user_id = ?').get(userId);
440
+ if (!row) {
441
+ const defaults = normalizeNotificationPreferences(DEFAULT_NOTIFICATION_PREFERENCES);
442
+ db.prepare(
443
+ 'INSERT INTO user_notification_preferences (user_id, preferences_json, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)'
444
+ ).run(userId, JSON.stringify(defaults));
445
+ return defaults;
446
+ }
447
+
448
+ let parsed;
449
+ try {
450
+ parsed = JSON.parse(row.preferences_json);
451
+ } catch {
452
+ parsed = DEFAULT_NOTIFICATION_PREFERENCES;
453
+ }
454
+ return normalizeNotificationPreferences(parsed);
455
+ } catch (err) {
456
+ throw err;
457
+ }
458
+ },
459
+
460
+ updatePreferences: (userId, preferences) => {
461
+ try {
462
+ const normalized = normalizeNotificationPreferences(preferences);
463
+ db.prepare(
464
+ `INSERT INTO user_notification_preferences (user_id, preferences_json, updated_at)
465
+ VALUES (?, ?, CURRENT_TIMESTAMP)
466
+ ON CONFLICT(user_id) DO UPDATE SET
467
+ preferences_json = excluded.preferences_json,
468
+ updated_at = CURRENT_TIMESTAMP`
469
+ ).run(userId, JSON.stringify(normalized));
470
+ return normalized;
471
+ } catch (err) {
472
+ throw err;
473
+ }
474
+ }
475
+ };
476
+
477
+ const pushSubscriptionsDb = {
478
+ saveSubscription: (userId, endpoint, keysP256dh, keysAuth) => {
479
+ try {
480
+ db.prepare(
481
+ `INSERT INTO push_subscriptions (user_id, endpoint, keys_p256dh, keys_auth)
482
+ VALUES (?, ?, ?, ?)
483
+ ON CONFLICT(endpoint) DO UPDATE SET
484
+ user_id = excluded.user_id,
485
+ keys_p256dh = excluded.keys_p256dh,
486
+ keys_auth = excluded.keys_auth`
487
+ ).run(userId, endpoint, keysP256dh, keysAuth);
488
+ } catch (err) {
489
+ throw err;
490
+ }
491
+ },
492
+
493
+ getSubscriptions: (userId) => {
494
+ try {
495
+ return db.prepare('SELECT endpoint, keys_p256dh, keys_auth FROM push_subscriptions WHERE user_id = ?').all(userId);
496
+ } catch (err) {
497
+ throw err;
498
+ }
499
+ },
500
+
501
+ removeSubscription: (endpoint) => {
502
+ try {
503
+ db.prepare('DELETE FROM push_subscriptions WHERE endpoint = ?').run(endpoint);
504
+ } catch (err) {
505
+ throw err;
506
+ }
507
+ },
508
+
509
+ removeAllForUser: (userId) => {
510
+ try {
511
+ db.prepare('DELETE FROM push_subscriptions WHERE user_id = ?').run(userId);
512
+ } catch (err) {
513
+ throw err;
514
+ }
515
+ }
516
+ };
517
+
379
518
  // Session custom names database operations
380
519
  const sessionNamesDb = {
381
520
  // Set (insert or update) a custom session name
@@ -482,8 +621,10 @@ export {
482
621
  userDb,
483
622
  apiKeysDb,
484
623
  credentialsDb,
624
+ notificationPreferencesDb,
625
+ pushSubscriptionsDb,
485
626
  sessionNamesDb,
486
627
  applyCustomSessionNames,
487
628
  appConfigDb,
488
629
  githubTokensDb // Backward compatibility
489
- };
630
+ };
@@ -51,6 +51,33 @@ CREATE INDEX IF NOT EXISTS idx_user_credentials_user_id ON user_credentials(user
51
51
  CREATE INDEX IF NOT EXISTS idx_user_credentials_type ON user_credentials(credential_type);
52
52
  CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active);
53
53
 
54
+ -- User notification preferences (backend-owned, provider-agnostic)
55
+ CREATE TABLE IF NOT EXISTS user_notification_preferences (
56
+ user_id INTEGER PRIMARY KEY,
57
+ preferences_json TEXT NOT NULL,
58
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
59
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
60
+ );
61
+
62
+ -- VAPID key pair for Web Push notifications
63
+ CREATE TABLE IF NOT EXISTS vapid_keys (
64
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
65
+ public_key TEXT NOT NULL,
66
+ private_key TEXT NOT NULL,
67
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
68
+ );
69
+
70
+ -- Browser push subscriptions
71
+ CREATE TABLE IF NOT EXISTS push_subscriptions (
72
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
73
+ user_id INTEGER NOT NULL,
74
+ endpoint TEXT NOT NULL UNIQUE,
75
+ keys_p256dh TEXT NOT NULL,
76
+ keys_auth TEXT NOT NULL,
77
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
78
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
79
+ );
80
+
54
81
  -- Session custom names (provider-agnostic display name overrides)
55
82
  CREATE TABLE IF NOT EXISTS session_names (
56
83
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -69,4 +96,4 @@ CREATE TABLE IF NOT EXISTS app_config (
69
96
  key TEXT PRIMARY KEY,
70
97
  value TEXT NOT NULL,
71
98
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
72
- );
99
+ );
@@ -6,14 +6,15 @@ const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
6
6
  import { promises as fs } from 'fs';
7
7
  import path from 'path';
8
8
  import os from 'os';
9
- import { getSessions, getSessionMessages } from './projects.js';
10
9
  import sessionManager from './sessionManager.js';
11
10
  import GeminiResponseHandler from './gemini-response-handler.js';
11
+ import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
12
+ import { createNormalizedMessage } from './providers/types.js';
12
13
 
13
14
  let activeGeminiProcesses = new Map(); // Track active processes by session ID
14
15
 
15
16
  async function spawnGemini(command, options = {}, ws) {
16
- const { sessionId, projectPath, cwd, resume, toolsSettings, permissionMode, images } = options;
17
+ const { sessionId, projectPath, cwd, toolsSettings, permissionMode, images, sessionSummary } = options;
17
18
  let capturedSessionId = sessionId; // Track session ID throughout the process
18
19
  let sessionCreatedSent = false; // Track if we've already sent session-created event
19
20
  let assistantBlocks = []; // Accumulate the full response blocks including tools
@@ -172,6 +173,36 @@ async function spawnGemini(command, options = {}, ws) {
172
173
  stdio: ['pipe', 'pipe', 'pipe'],
173
174
  env: { ...process.env } // Inherit all environment variables
174
175
  });
176
+ let terminalNotificationSent = false;
177
+ let terminalFailureReason = null;
178
+
179
+ const notifyTerminalState = ({ code = null, error = null } = {}) => {
180
+ if (terminalNotificationSent) {
181
+ return;
182
+ }
183
+
184
+ terminalNotificationSent = true;
185
+
186
+ const finalSessionId = capturedSessionId || sessionId || processKey;
187
+ if (code === 0 && !error) {
188
+ notifyRunStopped({
189
+ userId: ws?.userId || null,
190
+ provider: 'gemini',
191
+ sessionId: finalSessionId,
192
+ sessionName: sessionSummary,
193
+ stopReason: 'completed'
194
+ });
195
+ return;
196
+ }
197
+
198
+ notifyRunFailed({
199
+ userId: ws?.userId || null,
200
+ provider: 'gemini',
201
+ sessionId: finalSessionId,
202
+ sessionName: sessionSummary,
203
+ error: error || terminalFailureReason || `Gemini CLI exited with code ${code}`
204
+ });
205
+ };
175
206
 
176
207
  // Attach temp file info to process for cleanup later
177
208
  geminiProcess.tempImagePaths = tempImagePaths;
@@ -188,7 +219,6 @@ async function spawnGemini(command, options = {}, ws) {
188
219
  geminiProcess.stdin.end();
189
220
 
190
221
  // Add timeout handler
191
- let hasReceivedOutput = false;
192
222
  const timeoutMs = 120000; // 120 seconds for slower models
193
223
  let timeout;
194
224
 
@@ -196,11 +226,8 @@ async function spawnGemini(command, options = {}, ws) {
196
226
  if (timeout) clearTimeout(timeout);
197
227
  timeout = setTimeout(() => {
198
228
  const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId || processKey);
199
- ws.send({
200
- type: 'gemini-error',
201
- sessionId: socketSessionId,
202
- error: `Gemini CLI timeout - no response received for ${timeoutMs / 1000} seconds`
203
- });
229
+ terminalFailureReason = `Gemini CLI timeout - no response received for ${timeoutMs / 1000} seconds`;
230
+ ws.send(createNormalizedMessage({ kind: 'error', content: terminalFailureReason, sessionId: socketSessionId, provider: 'gemini' }));
204
231
  try {
205
232
  geminiProcess.kill('SIGTERM');
206
233
  } catch (e) { }
@@ -262,7 +289,6 @@ async function spawnGemini(command, options = {}, ws) {
262
289
  // Handle stdout
263
290
  geminiProcess.stdout.on('data', (data) => {
264
291
  const rawOutput = data.toString();
265
- hasReceivedOutput = true;
266
292
  startTimeout(); // Re-arm the timeout
267
293
 
268
294
  // For new sessions, create a session ID FIRST
@@ -286,21 +312,7 @@ async function spawnGemini(command, options = {}, ws) {
286
312
 
287
313
  ws.setSessionId && typeof ws.setSessionId === 'function' && ws.setSessionId(capturedSessionId);
288
314
 
289
- ws.send({
290
- type: 'session-created',
291
- sessionId: capturedSessionId
292
- });
293
-
294
- // Emit fake system init so the frontend immediately navigates and saves the session
295
- ws.send({
296
- type: 'claude-response',
297
- sessionId: capturedSessionId,
298
- data: {
299
- type: 'system',
300
- subtype: 'init',
301
- session_id: capturedSessionId
302
- }
303
- });
315
+ ws.send(createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, sessionId: capturedSessionId, provider: 'gemini' }));
304
316
  }
305
317
 
306
318
  if (responseHandler) {
@@ -313,14 +325,7 @@ async function spawnGemini(command, options = {}, ws) {
313
325
  assistantBlocks.push({ type: 'text', text: rawOutput });
314
326
  }
315
327
  const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId);
316
- ws.send({
317
- type: 'gemini-response',
318
- sessionId: socketSessionId,
319
- data: {
320
- type: 'message',
321
- content: rawOutput
322
- }
323
- });
328
+ ws.send(createNormalizedMessage({ kind: 'stream_delta', content: rawOutput, sessionId: socketSessionId, provider: 'gemini' }));
324
329
  }
325
330
  });
326
331
 
@@ -337,11 +342,7 @@ async function spawnGemini(command, options = {}, ws) {
337
342
  }
338
343
 
339
344
  const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId);
340
- ws.send({
341
- type: 'gemini-error',
342
- sessionId: socketSessionId,
343
- error: errorMsg
344
- });
345
+ ws.send(createNormalizedMessage({ kind: 'error', content: errorMsg, sessionId: socketSessionId, provider: 'gemini' }));
345
346
  });
346
347
 
347
348
  // Handle process completion
@@ -363,12 +364,7 @@ async function spawnGemini(command, options = {}, ws) {
363
364
  sessionManager.addMessage(finalSessionId, 'assistant', assistantBlocks);
364
365
  }
365
366
 
366
- ws.send({
367
- type: 'claude-complete', // Use claude-complete for compatibility with UI
368
- sessionId: finalSessionId,
369
- exitCode: code,
370
- isNewSession: !sessionId && !!command // Flag to indicate this was a new session
371
- });
367
+ ws.send(createNormalizedMessage({ kind: 'complete', exitCode: code, isNewSession: !sessionId && !!command, sessionId: finalSessionId, provider: 'gemini' }));
372
368
 
373
369
  // Clean up temporary image files if any
374
370
  if (geminiProcess.tempImagePaths && geminiProcess.tempImagePaths.length > 0) {
@@ -381,8 +377,13 @@ async function spawnGemini(command, options = {}, ws) {
381
377
  }
382
378
 
383
379
  if (code === 0) {
380
+ notifyTerminalState({ code });
384
381
  resolve();
385
382
  } else {
383
+ notifyTerminalState({
384
+ code,
385
+ error: code === null ? 'Gemini CLI process was terminated or timed out' : null
386
+ });
386
387
  reject(new Error(code === null ? 'Gemini CLI process was terminated or timed out' : `Gemini CLI exited with code ${code}`));
387
388
  }
388
389
  });
@@ -394,11 +395,8 @@ async function spawnGemini(command, options = {}, ws) {
394
395
  activeGeminiProcesses.delete(finalSessionId);
395
396
 
396
397
  const errorSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId;
397
- ws.send({
398
- type: 'gemini-error',
399
- sessionId: errorSessionId,
400
- error: error.message
401
- });
398
+ ws.send(createNormalizedMessage({ kind: 'error', content: error.message, sessionId: errorSessionId, provider: 'gemini' }));
399
+ notifyTerminalState({ error });
402
400
 
403
401
  reject(error);
404
402
  });