@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.
- package/README.de.md +239 -0
- package/README.ja.md +115 -230
- package/README.ko.md +116 -231
- package/README.md +2 -1
- package/README.ru.md +75 -54
- package/README.zh-CN.md +121 -238
- package/dist/assets/index-BenyXiE2.css +32 -0
- package/dist/assets/{index-DF_FFT3b.js → index-dyw-e9VE.js} +249 -237
- package/dist/index.html +2 -2
- package/dist/sw.js +102 -27
- package/package.json +3 -2
- package/server/claude-sdk.js +106 -62
- package/server/cli.js +10 -7
- package/server/cursor-cli.js +59 -73
- package/server/database/db.js +142 -1
- package/server/database/init.sql +28 -1
- package/server/gemini-cli.js +46 -48
- package/server/gemini-response-handler.js +12 -73
- package/server/index.js +82 -55
- package/server/middleware/auth.js +2 -2
- package/server/openai-codex.js +43 -28
- package/server/projects.js +1 -1
- package/server/providers/claude/adapter.js +278 -0
- package/server/providers/codex/adapter.js +248 -0
- package/server/providers/cursor/adapter.js +353 -0
- package/server/providers/gemini/adapter.js +186 -0
- package/server/providers/registry.js +44 -0
- package/server/providers/types.js +119 -0
- package/server/providers/utils.js +29 -0
- package/server/routes/agent.js +7 -5
- package/server/routes/cli-auth.js +38 -0
- package/server/routes/codex.js +1 -19
- package/server/routes/gemini.js +0 -30
- package/server/routes/git.js +48 -20
- package/server/routes/messages.js +61 -0
- package/server/routes/plugins.js +5 -1
- package/server/routes/settings.js +99 -1
- package/server/routes/taskmaster.js +2 -2
- package/server/services/notification-orchestrator.js +227 -0
- package/server/services/vapid-keys.js +35 -0
- package/server/utils/plugin-loader.js +53 -4
- package/shared/networkHosts.js +22 -0
- package/dist/assets/index-WNTmA_ug.css +0 -32
package/server/cursor-cli.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
sessionId: capturedSessionId || sessionId,
|
|
202
|
-
|
|
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
|
-
//
|
|
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
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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
|
});
|
package/server/database/db.js
CHANGED
|
@@ -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
|
+
};
|
package/server/database/init.sql
CHANGED
|
@@ -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
|
+
);
|
package/server/gemini-cli.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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
|
});
|