agentgui 1.0.211 → 1.0.213
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/database.js +18 -0
- package/package.json +1 -1
- package/server.js +128 -22
- package/static/index.html +79 -0
- package/static/js/agent-auth.js +47 -30
- package/static/js/client.js +299 -44
- package/static/js/websocket-manager.js +220 -216
package/database.js
CHANGED
|
@@ -1120,6 +1120,24 @@ export const queries = {
|
|
|
1120
1120
|
});
|
|
1121
1121
|
},
|
|
1122
1122
|
|
|
1123
|
+
getChunksSinceSeq(sessionId, sinceSeq) {
|
|
1124
|
+
const stmt = prep(
|
|
1125
|
+
`SELECT id, sessionId, conversationId, sequence, type, data, created_at
|
|
1126
|
+
FROM chunks WHERE sessionId = ? AND sequence > ? ORDER BY sequence ASC`
|
|
1127
|
+
);
|
|
1128
|
+
const rows = stmt.all(sessionId, sinceSeq);
|
|
1129
|
+
return rows.map(row => {
|
|
1130
|
+
try {
|
|
1131
|
+
return {
|
|
1132
|
+
...row,
|
|
1133
|
+
data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
|
|
1134
|
+
};
|
|
1135
|
+
} catch (e) {
|
|
1136
|
+
return row;
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
},
|
|
1140
|
+
|
|
1123
1141
|
deleteSessionChunks(sessionId) {
|
|
1124
1142
|
const stmt = prep('DELETE FROM chunks WHERE sessionId = ?');
|
|
1125
1143
|
const result = stmt.run(sessionId);
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -336,6 +336,67 @@ function geminiOAuthResultPage(title, message, success) {
|
|
|
336
336
|
</div></body></html>`;
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
function encodeOAuthState(csrfToken, relayUrl) {
|
|
340
|
+
const payload = JSON.stringify({ t: csrfToken, r: relayUrl });
|
|
341
|
+
return Buffer.from(payload).toString('base64url');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function decodeOAuthState(stateStr) {
|
|
345
|
+
try {
|
|
346
|
+
const payload = JSON.parse(Buffer.from(stateStr, 'base64url').toString());
|
|
347
|
+
return { csrfToken: payload.t, relayUrl: payload.r };
|
|
348
|
+
} catch (_) {
|
|
349
|
+
return { csrfToken: stateStr, relayUrl: null };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function geminiOAuthRelayPage(code, state, error) {
|
|
354
|
+
const stateData = decodeOAuthState(state || '');
|
|
355
|
+
const relayUrl = stateData.relayUrl || '';
|
|
356
|
+
const escapedCode = (code || '').replace(/['"\\]/g, '');
|
|
357
|
+
const escapedState = (state || '').replace(/['"\\]/g, '');
|
|
358
|
+
const escapedError = (error || '').replace(/['"\\]/g, '');
|
|
359
|
+
const escapedRelay = relayUrl.replace(/['"\\]/g, '');
|
|
360
|
+
return `<!DOCTYPE html><html><head><title>Completing sign-in...</title></head>
|
|
361
|
+
<body style="margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#111827;font-family:system-ui,sans-serif;color:white;">
|
|
362
|
+
<div id="status" style="text-align:center;max-width:400px;padding:2rem;">
|
|
363
|
+
<div id="spinner" style="font-size:2rem;margin-bottom:1rem;">⌛</div>
|
|
364
|
+
<h1 id="title" style="font-size:1.5rem;margin-bottom:0.5rem;">Completing sign-in...</h1>
|
|
365
|
+
<p id="msg" style="color:#9ca3af;">Relaying authentication to server...</p>
|
|
366
|
+
</div>
|
|
367
|
+
<script>
|
|
368
|
+
(function() {
|
|
369
|
+
var code = '${escapedCode}';
|
|
370
|
+
var state = '${escapedState}';
|
|
371
|
+
var error = '${escapedError}';
|
|
372
|
+
var relayUrl = '${escapedRelay}';
|
|
373
|
+
function show(icon, title, msg, color) {
|
|
374
|
+
document.getElementById('spinner').textContent = icon;
|
|
375
|
+
document.getElementById('spinner').style.color = color;
|
|
376
|
+
document.getElementById('title').textContent = title;
|
|
377
|
+
document.getElementById('msg').textContent = msg;
|
|
378
|
+
}
|
|
379
|
+
if (error) { show('\\u2717', 'Authentication Failed', error, '#ef4444'); return; }
|
|
380
|
+
if (!code) { show('\\u2717', 'Authentication Failed', 'No authorization code received.', '#ef4444'); return; }
|
|
381
|
+
if (!relayUrl) { show('\\u2713', 'Authentication Successful', 'Credentials saved. You can close this tab.', '#10b981'); return; }
|
|
382
|
+
fetch(relayUrl, {
|
|
383
|
+
method: 'POST',
|
|
384
|
+
headers: { 'Content-Type': 'application/json' },
|
|
385
|
+
body: JSON.stringify({ code: code, state: state })
|
|
386
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
387
|
+
if (data.success) {
|
|
388
|
+
show('\\u2713', 'Authentication Successful', data.email ? 'Signed in as ' + data.email + '. You can close this tab.' : 'Credentials saved. You can close this tab.', '#10b981');
|
|
389
|
+
} else {
|
|
390
|
+
show('\\u2717', 'Authentication Failed', data.error || 'Unknown error', '#ef4444');
|
|
391
|
+
}
|
|
392
|
+
}).catch(function(e) {
|
|
393
|
+
show('\\u2717', 'Relay Failed', 'Could not reach server: ' + e.message + '. You may need to paste the URL manually.', '#ef4444');
|
|
394
|
+
});
|
|
395
|
+
})();
|
|
396
|
+
</script>
|
|
397
|
+
</body></html>`;
|
|
398
|
+
}
|
|
399
|
+
|
|
339
400
|
function isRemoteRequest(req) {
|
|
340
401
|
return !!(req && (req.headers['x-forwarded-for'] || req.headers['x-forwarded-host'] || req.headers['x-forwarded-proto']));
|
|
341
402
|
}
|
|
@@ -353,7 +414,10 @@ async function startGeminiOAuth(req) {
|
|
|
353
414
|
redirectUri = `http://localhost:${PORT}${BASE_URL}/oauth2callback`;
|
|
354
415
|
}
|
|
355
416
|
|
|
356
|
-
const
|
|
417
|
+
const csrfToken = crypto.randomBytes(32).toString('hex');
|
|
418
|
+
const relayUrl = req ? `${buildBaseUrl(req)}${BASE_URL}/api/gemini-oauth/relay` : null;
|
|
419
|
+
const state = encodeOAuthState(csrfToken, relayUrl);
|
|
420
|
+
|
|
357
421
|
const client = new OAuth2Client({
|
|
358
422
|
clientId: creds.clientId,
|
|
359
423
|
clientSecret: creds.clientSecret,
|
|
@@ -367,7 +431,7 @@ async function startGeminiOAuth(req) {
|
|
|
367
431
|
});
|
|
368
432
|
|
|
369
433
|
const mode = useCustomClient ? 'custom' : (remote ? 'cli-remote' : 'cli-local');
|
|
370
|
-
geminiOAuthPending = { client, redirectUri, state };
|
|
434
|
+
geminiOAuthPending = { client, redirectUri, state: csrfToken };
|
|
371
435
|
geminiOAuthState = { status: 'pending', error: null, email: null };
|
|
372
436
|
|
|
373
437
|
setTimeout(() => {
|
|
@@ -380,12 +444,13 @@ async function startGeminiOAuth(req) {
|
|
|
380
444
|
return { authUrl, mode };
|
|
381
445
|
}
|
|
382
446
|
|
|
383
|
-
async function exchangeGeminiOAuthCode(code,
|
|
447
|
+
async function exchangeGeminiOAuthCode(code, stateParam) {
|
|
384
448
|
if (!geminiOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
|
|
385
449
|
|
|
386
|
-
const { client, redirectUri, state:
|
|
450
|
+
const { client, redirectUri, state: expectedCsrf } = geminiOAuthPending;
|
|
451
|
+
const { csrfToken } = decodeOAuthState(stateParam);
|
|
387
452
|
|
|
388
|
-
if (
|
|
453
|
+
if (csrfToken !== expectedCsrf) {
|
|
389
454
|
geminiOAuthState = { status: 'error', error: 'State mismatch', email: null };
|
|
390
455
|
geminiOAuthPending = null;
|
|
391
456
|
throw new Error('State mismatch - possible CSRF attack.');
|
|
@@ -423,6 +488,23 @@ async function exchangeGeminiOAuthCode(code, state) {
|
|
|
423
488
|
|
|
424
489
|
async function handleGeminiOAuthCallback(req, res) {
|
|
425
490
|
const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
|
|
491
|
+
const code = reqUrl.searchParams.get('code');
|
|
492
|
+
const state = reqUrl.searchParams.get('state');
|
|
493
|
+
const error = reqUrl.searchParams.get('error');
|
|
494
|
+
const errorDesc = reqUrl.searchParams.get('error_description');
|
|
495
|
+
|
|
496
|
+
if (error) {
|
|
497
|
+
const desc = errorDesc || error;
|
|
498
|
+
geminiOAuthState = { status: 'error', error: desc, email: null };
|
|
499
|
+
geminiOAuthPending = null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const stateData = decodeOAuthState(state || '');
|
|
503
|
+
if (stateData.relayUrl) {
|
|
504
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
505
|
+
res.end(geminiOAuthRelayPage(code, state, errorDesc || error));
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
426
508
|
|
|
427
509
|
if (!geminiOAuthPending) {
|
|
428
510
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
@@ -431,20 +513,8 @@ async function handleGeminiOAuthCallback(req, res) {
|
|
|
431
513
|
}
|
|
432
514
|
|
|
433
515
|
try {
|
|
434
|
-
|
|
435
|
-
if (error) {
|
|
436
|
-
const desc = reqUrl.searchParams.get('error_description') || error;
|
|
437
|
-
geminiOAuthState = { status: 'error', error: desc, email: null };
|
|
438
|
-
geminiOAuthPending = null;
|
|
439
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
440
|
-
res.end(geminiOAuthResultPage('Authentication Failed', desc, false));
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
const code = reqUrl.searchParams.get('code');
|
|
445
|
-
const state = reqUrl.searchParams.get('state');
|
|
516
|
+
if (error) throw new Error(errorDesc || error);
|
|
446
517
|
const email = await exchangeGeminiOAuthCode(code, state);
|
|
447
|
-
|
|
448
518
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
449
519
|
res.end(geminiOAuthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Gemini CLI credentials saved.', true));
|
|
450
520
|
} catch (e) {
|
|
@@ -941,9 +1011,15 @@ const server = http.createServer(async (req, res) => {
|
|
|
941
1011
|
if (!sess) { sendJSON(req, res, 404, { error: 'Session not found' }); return; }
|
|
942
1012
|
|
|
943
1013
|
const url = new URL(req.url, 'http://localhost');
|
|
1014
|
+
const sinceSeq = parseInt(url.searchParams.get('sinceSeq') || '-1');
|
|
944
1015
|
const since = parseInt(url.searchParams.get('since') || '0');
|
|
945
1016
|
|
|
946
|
-
|
|
1017
|
+
let chunks;
|
|
1018
|
+
if (sinceSeq >= 0) {
|
|
1019
|
+
chunks = queries.getChunksSinceSeq(sessionId, sinceSeq);
|
|
1020
|
+
} else {
|
|
1021
|
+
chunks = queries.getChunksSince(sessionId, since);
|
|
1022
|
+
}
|
|
947
1023
|
sendJSON(req, res, 200, { ok: true, chunks });
|
|
948
1024
|
return;
|
|
949
1025
|
}
|
|
@@ -1159,6 +1235,24 @@ const server = http.createServer(async (req, res) => {
|
|
|
1159
1235
|
return;
|
|
1160
1236
|
}
|
|
1161
1237
|
|
|
1238
|
+
if (pathOnly === '/api/gemini-oauth/relay' && req.method === 'POST') {
|
|
1239
|
+
try {
|
|
1240
|
+
const body = await parseBody(req);
|
|
1241
|
+
const { code, state: stateParam } = body;
|
|
1242
|
+
if (!code || !stateParam) {
|
|
1243
|
+
sendJSON(req, res, 400, { error: 'Missing code or state' });
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
const email = await exchangeGeminiOAuthCode(code, stateParam);
|
|
1247
|
+
sendJSON(req, res, 200, { success: true, email });
|
|
1248
|
+
} catch (e) {
|
|
1249
|
+
geminiOAuthState = { status: 'error', error: e.message, email: null };
|
|
1250
|
+
geminiOAuthPending = null;
|
|
1251
|
+
sendJSON(req, res, 400, { error: e.message });
|
|
1252
|
+
}
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1162
1256
|
if (pathOnly === '/api/gemini-oauth/complete' && req.method === 'POST') {
|
|
1163
1257
|
try {
|
|
1164
1258
|
const body = await parseBody(req);
|
|
@@ -1711,6 +1805,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
1711
1805
|
conversationId,
|
|
1712
1806
|
block: systemBlock,
|
|
1713
1807
|
blockIndex: allBlocks.length,
|
|
1808
|
+
seq: currentSequence,
|
|
1714
1809
|
timestamp: Date.now()
|
|
1715
1810
|
});
|
|
1716
1811
|
} else if (parsed.type === 'assistant' && parsed.message?.content) {
|
|
@@ -1726,6 +1821,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
1726
1821
|
conversationId,
|
|
1727
1822
|
block,
|
|
1728
1823
|
blockIndex: allBlocks.length - 1,
|
|
1824
|
+
seq: currentSequence,
|
|
1729
1825
|
timestamp: Date.now()
|
|
1730
1826
|
});
|
|
1731
1827
|
|
|
@@ -1752,6 +1848,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
1752
1848
|
conversationId,
|
|
1753
1849
|
block: toolResultBlock,
|
|
1754
1850
|
blockIndex: allBlocks.length,
|
|
1851
|
+
seq: currentSequence,
|
|
1755
1852
|
timestamp: Date.now()
|
|
1756
1853
|
});
|
|
1757
1854
|
}
|
|
@@ -1777,6 +1874,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
1777
1874
|
block: resultBlock,
|
|
1778
1875
|
blockIndex: allBlocks.length,
|
|
1779
1876
|
isResult: true,
|
|
1877
|
+
seq: currentSequence,
|
|
1780
1878
|
timestamp: Date.now()
|
|
1781
1879
|
});
|
|
1782
1880
|
|
|
@@ -1827,6 +1925,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
1827
1925
|
sessionId,
|
|
1828
1926
|
conversationId,
|
|
1829
1927
|
eventCount,
|
|
1928
|
+
seq: currentSequence,
|
|
1830
1929
|
timestamp: Date.now()
|
|
1831
1930
|
});
|
|
1832
1931
|
|
|
@@ -2066,9 +2165,13 @@ wss.on('connection', (ws, req) => {
|
|
|
2066
2165
|
}));
|
|
2067
2166
|
} else if (data.type === 'set_voice') {
|
|
2068
2167
|
ws.ttsVoiceId = data.voiceId || 'default';
|
|
2168
|
+
} else if (data.type === 'latency_report') {
|
|
2169
|
+
ws.latencyTier = data.quality || 'good';
|
|
2170
|
+
ws.latencyAvg = data.avg || 0;
|
|
2069
2171
|
} else if (data.type === 'ping') {
|
|
2070
2172
|
ws.send(JSON.stringify({
|
|
2071
2173
|
type: 'pong',
|
|
2174
|
+
requestId: data.requestId,
|
|
2072
2175
|
timestamp: Date.now()
|
|
2073
2176
|
}));
|
|
2074
2177
|
}
|
|
@@ -2097,13 +2200,16 @@ wss.on('connection', (ws, req) => {
|
|
|
2097
2200
|
const BROADCAST_TYPES = new Set([
|
|
2098
2201
|
'message_created', 'conversation_created', 'conversation_updated',
|
|
2099
2202
|
'conversations_updated', 'conversation_deleted', 'queue_status', 'queue_updated',
|
|
2100
|
-
'streaming_start', 'streaming_complete', 'streaming_error',
|
|
2101
2203
|
'rate_limit_hit', 'rate_limit_clear',
|
|
2102
2204
|
'script_started', 'script_stopped', 'script_output'
|
|
2103
2205
|
]);
|
|
2104
2206
|
|
|
2105
2207
|
const wsBatchQueues = new Map();
|
|
2106
|
-
const
|
|
2208
|
+
const BATCH_BY_TIER = { excellent: 16, good: 32, fair: 50, poor: 100, bad: 200 };
|
|
2209
|
+
|
|
2210
|
+
function getBatchInterval(ws) {
|
|
2211
|
+
return BATCH_BY_TIER[ws.latencyTier] || 32;
|
|
2212
|
+
}
|
|
2107
2213
|
|
|
2108
2214
|
function flushWsBatch(ws) {
|
|
2109
2215
|
const queue = wsBatchQueues.get(ws);
|
|
@@ -2124,7 +2230,7 @@ function sendToClient(ws, data) {
|
|
|
2124
2230
|
if (!queue) { queue = { msgs: [], timer: null }; wsBatchQueues.set(ws, queue); }
|
|
2125
2231
|
queue.msgs.push(data);
|
|
2126
2232
|
if (!queue.timer) {
|
|
2127
|
-
queue.timer = setTimeout(() => flushWsBatch(ws),
|
|
2233
|
+
queue.timer = setTimeout(() => flushWsBatch(ws), getBatchInterval(ws));
|
|
2128
2234
|
}
|
|
2129
2235
|
}
|
|
2130
2236
|
|
package/static/index.html
CHANGED
|
@@ -2213,6 +2213,85 @@
|
|
|
2213
2213
|
}
|
|
2214
2214
|
|
|
2215
2215
|
html.dark .event-streaming-complete { background: linear-gradient(135deg, #0a1f0f, #0f2b1a); }
|
|
2216
|
+
|
|
2217
|
+
@keyframes skeleton-pulse {
|
|
2218
|
+
0%, 100% { opacity: 0.4; }
|
|
2219
|
+
50% { opacity: 0.15; }
|
|
2220
|
+
}
|
|
2221
|
+
.skeleton-pulse { animation: skeleton-pulse 1.5s ease-in-out infinite; }
|
|
2222
|
+
.skeleton-loading { padding: 1rem; }
|
|
2223
|
+
|
|
2224
|
+
.message-sending { opacity: 0.6; transition: opacity 0.3s ease; }
|
|
2225
|
+
.message-send-failed { border-left: 3px solid var(--color-error); }
|
|
2226
|
+
|
|
2227
|
+
.connection-indicator {
|
|
2228
|
+
display: inline-flex;
|
|
2229
|
+
align-items: center;
|
|
2230
|
+
gap: 0.375rem;
|
|
2231
|
+
padding: 0.25rem 0.5rem;
|
|
2232
|
+
border-radius: 1rem;
|
|
2233
|
+
font-size: 0.75rem;
|
|
2234
|
+
cursor: pointer;
|
|
2235
|
+
transition: all 0.3s ease;
|
|
2236
|
+
}
|
|
2237
|
+
.connection-dot {
|
|
2238
|
+
width: 8px;
|
|
2239
|
+
height: 8px;
|
|
2240
|
+
border-radius: 50%;
|
|
2241
|
+
transition: background-color 0.5s ease;
|
|
2242
|
+
}
|
|
2243
|
+
.connection-dot.excellent { background: #10b981; }
|
|
2244
|
+
.connection-dot.good { background: #10b981; }
|
|
2245
|
+
.connection-dot.fair { background: #f59e0b; }
|
|
2246
|
+
.connection-dot.poor { background: #f97316; }
|
|
2247
|
+
.connection-dot.bad { background: #ef4444; }
|
|
2248
|
+
.connection-dot.unknown { background: #6b7280; }
|
|
2249
|
+
.connection-dot.disconnected { background: #ef4444; animation: pulse 1.5s ease-in-out infinite; }
|
|
2250
|
+
.connection-dot.reconnecting { background: #f59e0b; animation: pulse 1s ease-in-out infinite; }
|
|
2251
|
+
|
|
2252
|
+
.connection-tooltip {
|
|
2253
|
+
position: absolute;
|
|
2254
|
+
top: 100%;
|
|
2255
|
+
right: 0;
|
|
2256
|
+
margin-top: 0.5rem;
|
|
2257
|
+
padding: 0.75rem;
|
|
2258
|
+
background: var(--color-bg-secondary);
|
|
2259
|
+
border: 1px solid var(--color-border);
|
|
2260
|
+
border-radius: 0.5rem;
|
|
2261
|
+
font-size: 0.75rem;
|
|
2262
|
+
white-space: nowrap;
|
|
2263
|
+
z-index: 100;
|
|
2264
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
.new-content-pill {
|
|
2268
|
+
position: sticky;
|
|
2269
|
+
bottom: 0.5rem;
|
|
2270
|
+
left: 50%;
|
|
2271
|
+
transform: translateX(-50%);
|
|
2272
|
+
padding: 0.375rem 1rem;
|
|
2273
|
+
background: var(--color-primary);
|
|
2274
|
+
color: white;
|
|
2275
|
+
border: none;
|
|
2276
|
+
border-radius: 1rem;
|
|
2277
|
+
font-size: 0.8rem;
|
|
2278
|
+
cursor: pointer;
|
|
2279
|
+
z-index: 50;
|
|
2280
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
2281
|
+
transition: opacity 0.2s ease;
|
|
2282
|
+
}
|
|
2283
|
+
.new-content-pill:hover { opacity: 0.9; }
|
|
2284
|
+
|
|
2285
|
+
@keyframes block-appear {
|
|
2286
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
2287
|
+
to { opacity: 1; transform: translateY(0); }
|
|
2288
|
+
}
|
|
2289
|
+
.streaming-blocks > * {
|
|
2290
|
+
animation: block-appear 0.2s ease-out both;
|
|
2291
|
+
}
|
|
2292
|
+
.streaming-blocks > details.block-tool-use {
|
|
2293
|
+
transition: max-height 0.3s ease;
|
|
2294
|
+
}
|
|
2216
2295
|
</style>
|
|
2217
2296
|
</head>
|
|
2218
2297
|
<body>
|
package/static/js/agent-auth.js
CHANGED
|
@@ -130,46 +130,61 @@
|
|
|
130
130
|
|
|
131
131
|
function closeDropdown() { dropdown.classList.remove('open'); editingProvider = null; }
|
|
132
132
|
|
|
133
|
-
var oauthPollInterval = null, oauthPollTimeout = null;
|
|
133
|
+
var oauthPollInterval = null, oauthPollTimeout = null, oauthFallbackTimer = null;
|
|
134
134
|
|
|
135
135
|
function cleanupOAuthPolling() {
|
|
136
136
|
if (oauthPollInterval) { clearInterval(oauthPollInterval); oauthPollInterval = null; }
|
|
137
137
|
if (oauthPollTimeout) { clearTimeout(oauthPollTimeout); oauthPollTimeout = null; }
|
|
138
|
+
if (oauthFallbackTimer) { clearTimeout(oauthFallbackTimer); oauthFallbackTimer = null; }
|
|
138
139
|
}
|
|
139
140
|
|
|
140
|
-
function
|
|
141
|
-
|
|
141
|
+
function showOAuthWaitingModal() {
|
|
142
|
+
removeOAuthModal();
|
|
142
143
|
var overlay = document.createElement('div');
|
|
143
|
-
overlay.id = '
|
|
144
|
+
overlay.id = 'oauthWaitingModal';
|
|
144
145
|
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:9999;';
|
|
145
|
-
var s = function(c) { return 'font-size:0.8rem;color:var(--color-text-secondary,#d1d5db);margin:0 0 ' + (c ? '0' : '0.5rem') + ';'; };
|
|
146
146
|
overlay.innerHTML = '<div style="background:var(--color-bg-secondary,#1f2937);border-radius:1rem;padding:2rem;max-width:28rem;width:calc(100% - 2rem);box-shadow:0 25px 50px rgba(0,0,0,0.5);color:var(--color-text-primary,white);font-family:system-ui,sans-serif;" onclick="event.stopPropagation()">' +
|
|
147
147
|
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">' +
|
|
148
|
-
'<h2 style="font-size:1.125rem;font-weight:700;margin:0;">
|
|
149
|
-
'<button id="
|
|
148
|
+
'<h2 style="font-size:1.125rem;font-weight:700;margin:0;">Google Sign-In</h2>' +
|
|
149
|
+
'<button id="oauthWaitingClose" style="background:none;border:none;color:var(--color-text-secondary,#9ca3af);font-size:1.5rem;cursor:pointer;padding:0;line-height:1;">\u00d7</button></div>' +
|
|
150
|
+
'<div id="oauthWaitingContent" style="text-align:center;padding:1.5rem 0;">' +
|
|
151
|
+
'<div style="font-size:2rem;margin-bottom:1rem;animation:pulse 2s infinite;">⏳</div>' +
|
|
152
|
+
'<p style="font-size:0.85rem;color:var(--color-text-secondary,#d1d5db);margin:0 0 0.5rem;">Waiting for Google sign-in to complete...</p>' +
|
|
153
|
+
'<p style="font-size:0.75rem;color:var(--color-text-secondary,#6b7280);margin:0;">Complete the sign-in in the tab that just opened.</p>' +
|
|
154
|
+
'<p style="font-size:0.75rem;color:var(--color-text-secondary,#6b7280);margin:0.25rem 0 0;">This dialog will close automatically when done.</p></div>' +
|
|
155
|
+
'<div id="oauthPasteFallback" style="display:none;">' +
|
|
150
156
|
'<div style="margin-bottom:1rem;padding:1rem;background:var(--color-bg-tertiary,rgba(255,255,255,0.05));border-radius:0.5rem;">' +
|
|
151
|
-
'<p style="
|
|
152
|
-
'<p style="
|
|
153
|
-
'<p style="' + s() + '">3. After signing in, you will be redirected to a page that <span style="color:#facc15;font-weight:600;">may not load</span> (this is expected).</p>' +
|
|
154
|
-
'<p style="' + s(1) + '">4. Copy the <span style="color:white;font-weight:600;">entire URL</span> from the address bar and paste it below.</p></div>' +
|
|
155
|
-
'<label style="display:block;font-size:0.8rem;color:var(--color-text-secondary,#d1d5db);margin-bottom:0.5rem;">Paste the redirect URL here:</label>' +
|
|
157
|
+
'<p style="font-size:0.8rem;color:var(--color-text-secondary,#d1d5db);margin:0 0 0.5rem;">The automatic relay did not complete. This can happen when accessing the server remotely.</p>' +
|
|
158
|
+
'<p style="font-size:0.8rem;color:var(--color-text-secondary,#d1d5db);margin:0;">Copy the <span style="color:white;font-weight:600;">entire URL</span> from the sign-in tab and paste it below.</p></div>' +
|
|
156
159
|
'<input type="text" id="oauthPasteInput" placeholder="http://localhost:3000/gm/oauth2callback?code=..." style="width:100%;box-sizing:border-box;padding:0.75rem 1rem;background:var(--color-bg-primary,#374151);border:1px solid var(--color-border,#4b5563);border-radius:0.5rem;color:var(--color-text-primary,white);font-size:0.8rem;font-family:monospace;outline:none;" />' +
|
|
157
|
-
'<p id="oauthPasteError" style="font-size:0.75rem;color:#ef4444;margin:0.5rem 0 0;display:none;"></p>' +
|
|
160
|
+
'<p id="oauthPasteError" style="font-size:0.75rem;color:#ef4444;margin:0.5rem 0 0;display:none;"></p></div>' +
|
|
158
161
|
'<div style="display:flex;gap:0.75rem;margin-top:1.25rem;">' +
|
|
159
|
-
'<button id="
|
|
160
|
-
'<button id="oauthPasteSubmit" style="flex:1;padding:0.625rem;border-radius:0.5rem;border:none;background:var(--color-primary,#3b82f6);color:white;font-size:0.8rem;cursor:pointer;font-weight:600;">Complete Sign-In</button></div>' +
|
|
161
|
-
'<
|
|
162
|
+
'<button id="oauthWaitingCancel" style="flex:1;padding:0.625rem;border-radius:0.5rem;border:1px solid var(--color-border,#4b5563);background:transparent;color:var(--color-text-primary,white);font-size:0.8rem;cursor:pointer;font-weight:600;">Cancel</button>' +
|
|
163
|
+
'<button id="oauthPasteSubmit" style="flex:1;padding:0.625rem;border-radius:0.5rem;border:none;background:var(--color-primary,#3b82f6);color:white;font-size:0.8rem;cursor:pointer;font-weight:600;display:none;">Complete Sign-In</button></div>' +
|
|
164
|
+
'<style>@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}</style></div>';
|
|
162
165
|
document.body.appendChild(overlay);
|
|
163
|
-
var dismiss = function() { cleanupOAuthPolling(); authRunning = false;
|
|
164
|
-
document.getElementById('
|
|
165
|
-
document.getElementById('
|
|
166
|
+
var dismiss = function() { cleanupOAuthPolling(); authRunning = false; removeOAuthModal(); };
|
|
167
|
+
document.getElementById('oauthWaitingClose').addEventListener('click', dismiss);
|
|
168
|
+
document.getElementById('oauthWaitingCancel').addEventListener('click', dismiss);
|
|
166
169
|
document.getElementById('oauthPasteSubmit').addEventListener('click', submitOAuthPasteUrl);
|
|
167
|
-
document.getElementById('oauthPasteInput').addEventListener('keydown', function(e) { if (e.key === 'Enter') submitOAuthPasteUrl(); });
|
|
168
|
-
setTimeout(function() { var i = document.getElementById('oauthPasteInput'); if (i) i.focus(); }, 100);
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
function
|
|
172
|
-
var
|
|
172
|
+
function showOAuthPasteFallback() {
|
|
173
|
+
var fallback = document.getElementById('oauthPasteFallback');
|
|
174
|
+
var waitContent = document.getElementById('oauthWaitingContent');
|
|
175
|
+
var submitBtn = document.getElementById('oauthPasteSubmit');
|
|
176
|
+
if (fallback) fallback.style.display = 'block';
|
|
177
|
+
if (waitContent) waitContent.style.display = 'none';
|
|
178
|
+
if (submitBtn) submitBtn.style.display = 'block';
|
|
179
|
+
var input = document.getElementById('oauthPasteInput');
|
|
180
|
+
if (input) {
|
|
181
|
+
input.addEventListener('keydown', function(e) { if (e.key === 'Enter') submitOAuthPasteUrl(); });
|
|
182
|
+
setTimeout(function() { input.focus(); }, 100);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function removeOAuthModal() {
|
|
187
|
+
var el = document.getElementById('oauthWaitingModal');
|
|
173
188
|
if (el) el.remove();
|
|
174
189
|
}
|
|
175
190
|
|
|
@@ -193,7 +208,7 @@
|
|
|
193
208
|
if (data.success) {
|
|
194
209
|
cleanupOAuthPolling();
|
|
195
210
|
authRunning = false;
|
|
196
|
-
|
|
211
|
+
removeOAuthModal();
|
|
197
212
|
refresh();
|
|
198
213
|
} else {
|
|
199
214
|
if (errorEl) { errorEl.textContent = data.error || 'Failed to complete authentication.'; errorEl.style.display = 'block'; }
|
|
@@ -217,26 +232,28 @@
|
|
|
217
232
|
if (data.authUrl) {
|
|
218
233
|
window.open(data.authUrl, '_blank');
|
|
219
234
|
if (agentId === 'gemini') {
|
|
220
|
-
|
|
221
|
-
if (needsPaste) showOAuthPasteModal();
|
|
235
|
+
showOAuthWaitingModal();
|
|
222
236
|
cleanupOAuthPolling();
|
|
223
237
|
oauthPollInterval = setInterval(function() {
|
|
224
238
|
fetch(BASE + '/api/gemini-oauth/status').then(function(r) { return r.json(); }).then(function(status) {
|
|
225
239
|
if (status.status === 'success') {
|
|
226
240
|
cleanupOAuthPolling();
|
|
227
241
|
authRunning = false;
|
|
228
|
-
|
|
242
|
+
removeOAuthModal();
|
|
229
243
|
refresh();
|
|
230
244
|
} else if (status.status === 'error') {
|
|
231
245
|
cleanupOAuthPolling();
|
|
232
246
|
authRunning = false;
|
|
233
|
-
|
|
247
|
+
removeOAuthModal();
|
|
234
248
|
}
|
|
235
249
|
}).catch(function() {});
|
|
236
250
|
}, 1500);
|
|
251
|
+
oauthFallbackTimer = setTimeout(function() {
|
|
252
|
+
if (authRunning) showOAuthPasteFallback();
|
|
253
|
+
}, 30000);
|
|
237
254
|
oauthPollTimeout = setTimeout(function() {
|
|
238
255
|
cleanupOAuthPolling();
|
|
239
|
-
if (authRunning) { authRunning = false;
|
|
256
|
+
if (authRunning) { authRunning = false; removeOAuthModal(); }
|
|
240
257
|
}, 5 * 60 * 1000);
|
|
241
258
|
}
|
|
242
259
|
}
|
|
@@ -257,7 +274,7 @@
|
|
|
257
274
|
if (term) term.write(data.data);
|
|
258
275
|
} else if (data.type === 'script_stopped') {
|
|
259
276
|
authRunning = false;
|
|
260
|
-
|
|
277
|
+
removeOAuthModal();
|
|
261
278
|
cleanupOAuthPolling();
|
|
262
279
|
var term = getTerminal();
|
|
263
280
|
var msg = data.error ? data.error : ('exited with code ' + (data.code || 0));
|