agentgui 1.0.198 → 1.0.200
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/package.json +1 -1
- package/server.js +276 -1
- package/static/index.html +38 -2
- package/static/js/agent-auth.js +178 -0
- package/static/js/client.js +62 -14
- package/static/js/streaming-renderer.js +5 -4
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -180,6 +180,120 @@ function discoverAgents() {
|
|
|
180
180
|
|
|
181
181
|
const discoveredAgents = discoverAgents();
|
|
182
182
|
|
|
183
|
+
const PROVIDER_CONFIGS = {
|
|
184
|
+
'anthropic': {
|
|
185
|
+
name: 'Anthropic', configPaths: [
|
|
186
|
+
path.join(os.homedir(), '.claude.json'),
|
|
187
|
+
path.join(os.homedir(), '.config', 'claude', 'settings.json'),
|
|
188
|
+
path.join(os.homedir(), '.anthropic.json')
|
|
189
|
+
],
|
|
190
|
+
configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model })
|
|
191
|
+
},
|
|
192
|
+
'openai': {
|
|
193
|
+
name: 'OpenAI', configPaths: [
|
|
194
|
+
path.join(os.homedir(), '.openai.json'),
|
|
195
|
+
path.join(os.homedir(), '.config', 'openai', 'api-key')
|
|
196
|
+
],
|
|
197
|
+
configFormat: (apiKey, model) => ({ apiKey, defaultModel: model })
|
|
198
|
+
},
|
|
199
|
+
'google': {
|
|
200
|
+
name: 'Google Gemini', configPaths: [
|
|
201
|
+
path.join(os.homedir(), '.gemini.json'),
|
|
202
|
+
path.join(os.homedir(), '.config', 'gemini', 'credentials.json')
|
|
203
|
+
],
|
|
204
|
+
configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model })
|
|
205
|
+
},
|
|
206
|
+
'openrouter': {
|
|
207
|
+
name: 'OpenRouter', configPaths: [
|
|
208
|
+
path.join(os.homedir(), '.openrouter.json'),
|
|
209
|
+
path.join(os.homedir(), '.config', 'openrouter', 'config.json')
|
|
210
|
+
],
|
|
211
|
+
configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model })
|
|
212
|
+
},
|
|
213
|
+
'github': {
|
|
214
|
+
name: 'GitHub Models', configPaths: [
|
|
215
|
+
path.join(os.homedir(), '.github.json'),
|
|
216
|
+
path.join(os.homedir(), '.config', 'github-copilot.json')
|
|
217
|
+
],
|
|
218
|
+
configFormat: (apiKey, model) => ({ github_token: apiKey, default_model: model })
|
|
219
|
+
},
|
|
220
|
+
'azure': {
|
|
221
|
+
name: 'Azure OpenAI', configPaths: [
|
|
222
|
+
path.join(os.homedir(), '.azure.json'),
|
|
223
|
+
path.join(os.homedir(), '.config', 'azure-openai', 'config.json')
|
|
224
|
+
],
|
|
225
|
+
configFormat: (apiKey, model) => ({ api_key: apiKey, endpoint: '', default_model: model })
|
|
226
|
+
},
|
|
227
|
+
'anthropic-claude-code': {
|
|
228
|
+
name: 'Claude Code Max', configPaths: [
|
|
229
|
+
path.join(os.homedir(), '.claude', 'max.json'),
|
|
230
|
+
path.join(os.homedir(), '.config', 'claude-code', 'max.json')
|
|
231
|
+
],
|
|
232
|
+
configFormat: (apiKey, model) => ({ api_key: apiKey, plan: 'max', default_model: model })
|
|
233
|
+
},
|
|
234
|
+
'opencode': {
|
|
235
|
+
name: 'OpenCode', configPaths: [
|
|
236
|
+
path.join(os.homedir(), '.opencode', 'config.json'),
|
|
237
|
+
path.join(os.homedir(), '.config', 'opencode', 'config.json')
|
|
238
|
+
],
|
|
239
|
+
configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model, providers: ['anthropic', 'openai', 'google'] })
|
|
240
|
+
},
|
|
241
|
+
'proxypilot': {
|
|
242
|
+
name: 'ProxyPilot', configPaths: [
|
|
243
|
+
path.join(os.homedir(), '.proxypilot', 'config.json'),
|
|
244
|
+
path.join(os.homedir(), '.config', 'proxypilot', 'config.json')
|
|
245
|
+
],
|
|
246
|
+
configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model })
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
function maskKey(key) {
|
|
251
|
+
if (!key || key.length < 8) return '****';
|
|
252
|
+
return '****' + key.slice(-4);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function getProviderConfigs() {
|
|
256
|
+
const configs = {};
|
|
257
|
+
for (const [providerId, config] of Object.entries(PROVIDER_CONFIGS)) {
|
|
258
|
+
for (const configPath of config.configPaths) {
|
|
259
|
+
try {
|
|
260
|
+
if (fs.existsSync(configPath)) {
|
|
261
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
262
|
+
const parsed = JSON.parse(content);
|
|
263
|
+
const rawKey = parsed.api_key || parsed.apiKey || parsed.github_token || '';
|
|
264
|
+
configs[providerId] = {
|
|
265
|
+
name: config.name,
|
|
266
|
+
apiKey: maskKey(rawKey),
|
|
267
|
+
hasKey: !!rawKey,
|
|
268
|
+
defaultModel: parsed.default_model || parsed.defaultModel || '',
|
|
269
|
+
path: configPath
|
|
270
|
+
};
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
} catch (_) {}
|
|
274
|
+
}
|
|
275
|
+
if (!configs[providerId]) {
|
|
276
|
+
configs[providerId] = { name: config.name, apiKey: '', hasKey: false, defaultModel: '', path: '' };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return configs;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function saveProviderConfig(providerId, apiKey, defaultModel) {
|
|
283
|
+
const config = PROVIDER_CONFIGS[providerId];
|
|
284
|
+
if (!config) throw new Error('Unknown provider: ' + providerId);
|
|
285
|
+
const configPath = config.configPaths[0];
|
|
286
|
+
const configDir = path.dirname(configPath);
|
|
287
|
+
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
288
|
+
let existing = {};
|
|
289
|
+
try {
|
|
290
|
+
if (fs.existsSync(configPath)) existing = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
291
|
+
} catch (_) {}
|
|
292
|
+
const merged = { ...existing, ...config.configFormat(apiKey, defaultModel) };
|
|
293
|
+
fs.writeFileSync(configPath, JSON.stringify(merged, null, 2), { mode: 0o600 });
|
|
294
|
+
return configPath;
|
|
295
|
+
}
|
|
296
|
+
|
|
183
297
|
function parseBody(req) {
|
|
184
298
|
return new Promise((resolve, reject) => {
|
|
185
299
|
let body = '';
|
|
@@ -410,6 +524,46 @@ const server = http.createServer(async (req, res) => {
|
|
|
410
524
|
return;
|
|
411
525
|
}
|
|
412
526
|
|
|
527
|
+
const queueMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/queue$/);
|
|
528
|
+
if (queueMatch && req.method === 'GET') {
|
|
529
|
+
const conversationId = queueMatch[1];
|
|
530
|
+
const conv = queries.getConversation(conversationId);
|
|
531
|
+
if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
|
|
532
|
+
const queue = messageQueues.get(conversationId) || [];
|
|
533
|
+
sendJSON(req, res, 200, { queue });
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const queueItemMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/queue\/([^/]+)$/);
|
|
538
|
+
if (queueItemMatch && req.method === 'DELETE') {
|
|
539
|
+
const conversationId = queueItemMatch[1];
|
|
540
|
+
const messageId = queueItemMatch[2];
|
|
541
|
+
const queue = messageQueues.get(conversationId);
|
|
542
|
+
if (!queue) { sendJSON(req, res, 404, { error: 'Queue not found' }); return; }
|
|
543
|
+
const index = queue.findIndex(q => q.messageId === messageId);
|
|
544
|
+
if (index === -1) { sendJSON(req, res, 404, { error: 'Queued message not found' }); return; }
|
|
545
|
+
queue.splice(index, 1);
|
|
546
|
+
if (queue.length === 0) messageQueues.delete(conversationId);
|
|
547
|
+
broadcastSync({ type: 'queue_status', conversationId, queueLength: queue?.length || 0, timestamp: Date.now() });
|
|
548
|
+
sendJSON(req, res, 200, { deleted: true });
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (queueItemMatch && req.method === 'PATCH') {
|
|
553
|
+
const conversationId = queueItemMatch[1];
|
|
554
|
+
const messageId = queueItemMatch[2];
|
|
555
|
+
const body = await parseBody(req);
|
|
556
|
+
const queue = messageQueues.get(conversationId);
|
|
557
|
+
if (!queue) { sendJSON(req, res, 404, { error: 'Queue not found' }); return; }
|
|
558
|
+
const item = queue.find(q => q.messageId === messageId);
|
|
559
|
+
if (!item) { sendJSON(req, res, 404, { error: 'Queued message not found' }); return; }
|
|
560
|
+
if (body.content !== undefined) item.content = body.content;
|
|
561
|
+
if (body.agentId !== undefined) item.agentId = body.agentId;
|
|
562
|
+
broadcastSync({ type: 'queue_updated', conversationId, messageId, content: item.content, agentId: item.agentId, timestamp: Date.now() });
|
|
563
|
+
sendJSON(req, res, 200, { updated: true, item });
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
413
567
|
const messageMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/messages\/([^/]+)$/);
|
|
414
568
|
if (messageMatch && req.method === 'GET') {
|
|
415
569
|
const msg = queries.getMessage(messageMatch[2]);
|
|
@@ -620,6 +774,127 @@ const server = http.createServer(async (req, res) => {
|
|
|
620
774
|
return;
|
|
621
775
|
}
|
|
622
776
|
|
|
777
|
+
if (pathOnly === '/api/agents/auth-status' && req.method === 'GET') {
|
|
778
|
+
const statuses = discoveredAgents.map(agent => {
|
|
779
|
+
const status = { id: agent.id, name: agent.name, authenticated: false, detail: '' };
|
|
780
|
+
try {
|
|
781
|
+
if (agent.id === 'claude-code') {
|
|
782
|
+
const credFile = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
783
|
+
if (fs.existsSync(credFile)) {
|
|
784
|
+
const creds = JSON.parse(fs.readFileSync(credFile, 'utf-8'));
|
|
785
|
+
if (creds.claudeAiOauth && creds.claudeAiOauth.expiresAt > Date.now()) {
|
|
786
|
+
status.authenticated = true;
|
|
787
|
+
status.detail = creds.claudeAiOauth.subscriptionType || 'authenticated';
|
|
788
|
+
} else {
|
|
789
|
+
status.detail = 'expired';
|
|
790
|
+
}
|
|
791
|
+
} else {
|
|
792
|
+
status.detail = 'no credentials';
|
|
793
|
+
}
|
|
794
|
+
} else if (agent.id === 'gemini') {
|
|
795
|
+
const acctFile = path.join(os.homedir(), '.gemini', 'google_accounts.json');
|
|
796
|
+
if (fs.existsSync(acctFile)) {
|
|
797
|
+
const accts = JSON.parse(fs.readFileSync(acctFile, 'utf-8'));
|
|
798
|
+
if (accts.active) {
|
|
799
|
+
status.authenticated = true;
|
|
800
|
+
status.detail = accts.active;
|
|
801
|
+
} else {
|
|
802
|
+
status.detail = 'logged out';
|
|
803
|
+
}
|
|
804
|
+
} else {
|
|
805
|
+
status.detail = 'no credentials';
|
|
806
|
+
}
|
|
807
|
+
} else if (agent.id === 'opencode') {
|
|
808
|
+
const out = execSync('opencode auth list 2>&1', { encoding: 'utf-8', timeout: 5000 });
|
|
809
|
+
const countMatch = out.match(/(\d+)\s+credentials?/);
|
|
810
|
+
if (countMatch && parseInt(countMatch[1], 10) > 0) {
|
|
811
|
+
status.authenticated = true;
|
|
812
|
+
status.detail = countMatch[1] + ' credential(s)';
|
|
813
|
+
} else {
|
|
814
|
+
status.detail = 'no credentials';
|
|
815
|
+
}
|
|
816
|
+
} else {
|
|
817
|
+
status.detail = 'unknown';
|
|
818
|
+
}
|
|
819
|
+
} catch (e) {
|
|
820
|
+
status.detail = 'check failed';
|
|
821
|
+
}
|
|
822
|
+
return status;
|
|
823
|
+
});
|
|
824
|
+
sendJSON(req, res, 200, { agents: statuses });
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const agentAuthMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/auth$/);
|
|
829
|
+
if (agentAuthMatch && req.method === 'POST') {
|
|
830
|
+
const agentId = agentAuthMatch[1];
|
|
831
|
+
const agent = discoveredAgents.find(a => a.id === agentId);
|
|
832
|
+
if (!agent) { sendJSON(req, res, 404, { error: 'Agent not found' }); return; }
|
|
833
|
+
|
|
834
|
+
const authCommands = {
|
|
835
|
+
'claude-code': { cmd: 'claude', args: ['setup-token'] },
|
|
836
|
+
'opencode': { cmd: 'opencode', args: ['auth', 'login'] },
|
|
837
|
+
'gemini': { cmd: 'gemini', args: [] }
|
|
838
|
+
};
|
|
839
|
+
const authCmd = authCommands[agentId];
|
|
840
|
+
if (!authCmd) { sendJSON(req, res, 400, { error: 'No auth command for this agent' }); return; }
|
|
841
|
+
|
|
842
|
+
const conversationId = '__agent_auth__';
|
|
843
|
+
if (activeScripts.has(conversationId)) {
|
|
844
|
+
sendJSON(req, res, 409, { error: 'Auth process already running' });
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const child = spawn(authCmd.cmd, authCmd.args, {
|
|
849
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
850
|
+
env: { ...process.env, FORCE_COLOR: '1' }
|
|
851
|
+
});
|
|
852
|
+
activeScripts.set(conversationId, { process: child, script: 'auth-' + agentId, startTime: Date.now() });
|
|
853
|
+
broadcastSync({ type: 'script_started', conversationId, script: 'auth-' + agentId, agentId, timestamp: Date.now() });
|
|
854
|
+
|
|
855
|
+
const onData = (stream) => (chunk) => {
|
|
856
|
+
broadcastSync({ type: 'script_output', conversationId, data: chunk.toString(), stream, timestamp: Date.now() });
|
|
857
|
+
};
|
|
858
|
+
child.stdout.on('data', onData('stdout'));
|
|
859
|
+
child.stderr.on('data', onData('stderr'));
|
|
860
|
+
child.on('error', (err) => {
|
|
861
|
+
activeScripts.delete(conversationId);
|
|
862
|
+
broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: err.message, timestamp: Date.now() });
|
|
863
|
+
});
|
|
864
|
+
child.on('close', (code) => {
|
|
865
|
+
activeScripts.delete(conversationId);
|
|
866
|
+
broadcastSync({ type: 'script_stopped', conversationId, code: code || 0, timestamp: Date.now() });
|
|
867
|
+
});
|
|
868
|
+
sendJSON(req, res, 200, { ok: true, agentId, pid: child.pid });
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (pathOnly === '/api/auth/configs' && req.method === 'GET') {
|
|
873
|
+
const configs = getProviderConfigs();
|
|
874
|
+
sendJSON(req, res, 200, configs);
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (pathOnly === '/api/auth/save-config' && req.method === 'POST') {
|
|
879
|
+
try {
|
|
880
|
+
const body = await parseBody(req);
|
|
881
|
+
const { providerId, apiKey, defaultModel } = body || {};
|
|
882
|
+
if (typeof providerId !== 'string' || !providerId.length || providerId.length > 100) {
|
|
883
|
+
sendJSON(req, res, 400, { error: 'Invalid providerId' }); return;
|
|
884
|
+
}
|
|
885
|
+
if (typeof apiKey !== 'string' || !apiKey.length || apiKey.length > 10000) {
|
|
886
|
+
sendJSON(req, res, 400, { error: 'Invalid apiKey' }); return;
|
|
887
|
+
}
|
|
888
|
+
if (defaultModel !== undefined && (typeof defaultModel !== 'string' || defaultModel.length > 200)) {
|
|
889
|
+
sendJSON(req, res, 400, { error: 'Invalid defaultModel' }); return;
|
|
890
|
+
}
|
|
891
|
+
const configPath = saveProviderConfig(providerId, apiKey, defaultModel || '');
|
|
892
|
+
sendJSON(req, res, 200, { success: true, path: configPath });
|
|
893
|
+
} catch (err) {
|
|
894
|
+
sendJSON(req, res, 400, { error: err.message });
|
|
895
|
+
}
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
623
898
|
|
|
624
899
|
if (pathOnly === '/api/import/claude-code' && req.method === 'GET') {
|
|
625
900
|
const result = queries.importClaudeCodeConversations();
|
|
@@ -1382,7 +1657,7 @@ wss.on('connection', (ws, req) => {
|
|
|
1382
1657
|
|
|
1383
1658
|
const BROADCAST_TYPES = new Set([
|
|
1384
1659
|
'message_created', 'conversation_created', 'conversation_updated',
|
|
1385
|
-
'conversations_updated', 'conversation_deleted', 'queue_status',
|
|
1660
|
+
'conversations_updated', 'conversation_deleted', 'queue_status', 'queue_updated',
|
|
1386
1661
|
'streaming_start', 'streaming_complete', 'streaming_error',
|
|
1387
1662
|
'rate_limit_hit', 'rate_limit_clear',
|
|
1388
1663
|
'script_started', 'script_stopped', 'script_output'
|
package/static/index.html
CHANGED
|
@@ -447,12 +447,12 @@
|
|
|
447
447
|
.script-buttons { display: flex; gap: 0.25rem; align-items: center; }
|
|
448
448
|
.header-icon-btn {
|
|
449
449
|
display: flex; align-items: center; justify-content: center;
|
|
450
|
-
width:
|
|
450
|
+
width: 36px; height: 36px; background: none; border: none;
|
|
451
451
|
border-radius: 0.375rem; cursor: pointer; color: var(--color-text-secondary);
|
|
452
452
|
transition: background-color 0.15s, color 0.15s;
|
|
453
453
|
}
|
|
454
454
|
.header-icon-btn:hover { background-color: var(--color-bg-primary); color: var(--color-text-primary); }
|
|
455
|
-
.header-icon-btn svg { width:
|
|
455
|
+
.header-icon-btn svg { width: 20px; height: 20px; }
|
|
456
456
|
#scriptStartBtn { color: var(--color-success); }
|
|
457
457
|
#scriptStartBtn:hover { background-color: rgba(16,185,129,0.1); color: var(--color-success); }
|
|
458
458
|
.script-dev-btn { color: var(--color-info); }
|
|
@@ -460,6 +460,37 @@
|
|
|
460
460
|
.script-stop-btn { color: var(--color-error); }
|
|
461
461
|
.script-stop-btn:hover { background-color: rgba(239,68,68,0.1); color: var(--color-error); }
|
|
462
462
|
|
|
463
|
+
.agent-auth-btn { color: var(--color-text-secondary); position: relative; }
|
|
464
|
+
.agent-auth-btn.auth-ok { color: var(--color-success); }
|
|
465
|
+
.agent-auth-btn.auth-warn { color: var(--color-warning); }
|
|
466
|
+
.agent-auth-btn:hover { background-color: var(--color-bg-primary); }
|
|
467
|
+
.agent-auth-dropdown {
|
|
468
|
+
position: absolute; top: 100%; right: 0; z-index: 100;
|
|
469
|
+
min-width: 260px; padding: 0.25rem 0;
|
|
470
|
+
background: var(--color-bg-secondary); border: 1px solid var(--color-border);
|
|
471
|
+
border-radius: 0.5rem; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
472
|
+
display: none;
|
|
473
|
+
}
|
|
474
|
+
.agent-auth-dropdown.open { display: block; }
|
|
475
|
+
.agent-auth-item {
|
|
476
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
477
|
+
padding: 0.5rem 0.75rem; cursor: pointer; font-size: 0.8125rem;
|
|
478
|
+
color: var(--color-text-primary); border: none; background: none; width: 100%;
|
|
479
|
+
text-align: left;
|
|
480
|
+
}
|
|
481
|
+
.agent-auth-item:hover { background: var(--color-bg-primary); }
|
|
482
|
+
.agent-auth-dot {
|
|
483
|
+
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
|
|
484
|
+
}
|
|
485
|
+
.agent-auth-dot.ok { background: var(--color-success); }
|
|
486
|
+
.agent-auth-dot.missing { background: var(--color-warning); }
|
|
487
|
+
.agent-auth-dot.unknown { background: var(--color-text-secondary); }
|
|
488
|
+
.agent-auth-section-header {
|
|
489
|
+
padding: 0.375rem 0.75rem; font-size: 0.6875rem; font-weight: 600;
|
|
490
|
+
text-transform: uppercase; letter-spacing: 0.05em;
|
|
491
|
+
color: var(--color-text-secondary); user-select: none;
|
|
492
|
+
}
|
|
493
|
+
|
|
463
494
|
.terminal-container {
|
|
464
495
|
flex: 1; display: flex; flex-direction: column; overflow: hidden; background: #1e1e1e;
|
|
465
496
|
}
|
|
@@ -2234,6 +2265,10 @@
|
|
|
2234
2265
|
<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><rect x="5" y="5" width="14" height="14" rx="1"></rect></svg>
|
|
2235
2266
|
</button>
|
|
2236
2267
|
</div>
|
|
2268
|
+
<button class="header-icon-btn agent-auth-btn" id="agentAuthBtn" title="Agent authentication" aria-label="Agent authentication" style="display:none;">
|
|
2269
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
|
|
2270
|
+
<div class="agent-auth-dropdown" id="agentAuthDropdown"></div>
|
|
2271
|
+
</button>
|
|
2237
2272
|
<div class="status-badge">
|
|
2238
2273
|
<div class="status-indicator" data-status="disconnected"></div>
|
|
2239
2274
|
<span id="connectionStatus" data-status-indicator>Disconnected</span>
|
|
@@ -2378,6 +2413,7 @@
|
|
|
2378
2413
|
<script type="module" src="/gm/js/voice.js"></script>
|
|
2379
2414
|
<script defer src="/gm/js/features.js"></script>
|
|
2380
2415
|
<script defer src="/gm/js/script-runner.js"></script>
|
|
2416
|
+
<script defer src="/gm/js/agent-auth.js"></script>
|
|
2381
2417
|
|
|
2382
2418
|
<script>
|
|
2383
2419
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
var BASE = window.__BASE_URL || '';
|
|
3
|
+
var btn = document.getElementById('agentAuthBtn');
|
|
4
|
+
var dropdown = document.getElementById('agentAuthDropdown');
|
|
5
|
+
var agents = [], providers = {}, authRunning = false, editingProvider = null;
|
|
6
|
+
var AUTH_CONV_ID = '__agent_auth__';
|
|
7
|
+
|
|
8
|
+
function init() {
|
|
9
|
+
if (!btn || !dropdown) return;
|
|
10
|
+
btn.style.display = 'flex';
|
|
11
|
+
btn.addEventListener('click', toggleDropdown);
|
|
12
|
+
document.addEventListener('click', function(e) {
|
|
13
|
+
if (!btn.contains(e.target) && !dropdown.contains(e.target)) closeDropdown();
|
|
14
|
+
});
|
|
15
|
+
window.addEventListener('conversation-selected', function() { refresh(); });
|
|
16
|
+
window.addEventListener('ws-message', onWsMessage);
|
|
17
|
+
refresh();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function refresh() { fetchAuthStatus(); fetchProviderConfigs(); }
|
|
21
|
+
|
|
22
|
+
function fetchAuthStatus() {
|
|
23
|
+
fetch(BASE + '/api/agents/auth-status').then(function(r) { return r.json(); })
|
|
24
|
+
.then(function(data) { agents = data.agents || []; updateButton(); renderDropdown(); })
|
|
25
|
+
.catch(function() {});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fetchProviderConfigs() {
|
|
29
|
+
fetch(BASE + '/api/auth/configs').then(function(r) { return r.json(); })
|
|
30
|
+
.then(function(data) { providers = data || {}; updateButton(); renderDropdown(); })
|
|
31
|
+
.catch(function() {});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function updateButton() {
|
|
35
|
+
btn.style.display = 'flex';
|
|
36
|
+
var agentOk = agents.length === 0 || agents.every(function(a) { return a.authenticated; });
|
|
37
|
+
var pkeys = Object.keys(providers);
|
|
38
|
+
var provOk = pkeys.length === 0 || pkeys.some(function(k) { return providers[k].hasKey; });
|
|
39
|
+
var anyWarn = agents.some(function(a) { return !a.authenticated; }) ||
|
|
40
|
+
pkeys.some(function(k) { return !providers[k].hasKey; });
|
|
41
|
+
btn.classList.toggle('auth-ok', agentOk && provOk && (agents.length > 0 || pkeys.length > 0));
|
|
42
|
+
btn.classList.toggle('auth-warn', anyWarn);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderDropdown() {
|
|
46
|
+
dropdown.innerHTML = '';
|
|
47
|
+
if (agents.length > 0) {
|
|
48
|
+
appendHeader('Agent CLI Auth');
|
|
49
|
+
agents.forEach(function(agent) {
|
|
50
|
+
var dotClass = agent.authenticated ? 'ok' : (agent.detail === 'unknown' ? 'unknown' : 'missing');
|
|
51
|
+
var item = makeItem(dotClass, agent.name, agent.detail);
|
|
52
|
+
item.addEventListener('click', function(e) { e.stopPropagation(); closeDropdown(); triggerAuth(agent.id); });
|
|
53
|
+
dropdown.appendChild(item);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
var pkeys = Object.keys(providers);
|
|
57
|
+
if (pkeys.length > 0) {
|
|
58
|
+
if (agents.length > 0) appendSep();
|
|
59
|
+
appendHeader('Provider Keys');
|
|
60
|
+
pkeys.forEach(function(pid) {
|
|
61
|
+
var p = providers[pid];
|
|
62
|
+
var item = makeItem(p.hasKey ? 'ok' : 'missing', p.name || pid, p.hasKey ? p.apiKey : 'not set');
|
|
63
|
+
item.style.flexWrap = 'wrap';
|
|
64
|
+
item.addEventListener('click', function(e) { e.stopPropagation(); toggleEdit(pid); });
|
|
65
|
+
dropdown.appendChild(item);
|
|
66
|
+
if (editingProvider === pid) dropdown.appendChild(makeEditForm(pid));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function appendHeader(text) {
|
|
72
|
+
var h = document.createElement('div');
|
|
73
|
+
h.className = 'agent-auth-section-header';
|
|
74
|
+
h.textContent = text;
|
|
75
|
+
dropdown.appendChild(h);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function appendSep() {
|
|
79
|
+
var s = document.createElement('div');
|
|
80
|
+
s.style.cssText = 'height:1px;background:var(--color-border);margin:0.25rem 0;';
|
|
81
|
+
dropdown.appendChild(s);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function makeItem(dotClass, name, detail) {
|
|
85
|
+
var el = document.createElement('button');
|
|
86
|
+
el.className = 'agent-auth-item';
|
|
87
|
+
el.innerHTML = '<span class="agent-auth-dot ' + dotClass + '"></span><span>' + esc(name) +
|
|
88
|
+
'</span><span style="margin-left:auto;font-size:0.7rem;color:var(--color-text-secondary)">' + esc(detail) + '</span>';
|
|
89
|
+
return el;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function makeEditForm(pid) {
|
|
93
|
+
var form = document.createElement('div');
|
|
94
|
+
form.style.cssText = 'width:100%;padding:0.375rem 0.75rem;display:flex;gap:0.375rem;';
|
|
95
|
+
var input = document.createElement('input');
|
|
96
|
+
input.type = 'password'; input.placeholder = 'API key';
|
|
97
|
+
input.style.cssText = 'flex:1;min-width:0;padding:0.25rem 0.5rem;font-size:0.75rem;border:1px solid var(--color-border);border-radius:0.25rem;background:var(--color-bg-primary);color:var(--color-text-primary);outline:none;';
|
|
98
|
+
input.addEventListener('click', function(e) { e.stopPropagation(); });
|
|
99
|
+
var saveBtn = document.createElement('button');
|
|
100
|
+
saveBtn.textContent = 'Save';
|
|
101
|
+
saveBtn.style.cssText = 'padding:0.25rem 0.5rem;font-size:0.7rem;font-weight:600;background:var(--color-primary);color:white;border:none;border-radius:0.25rem;cursor:pointer;flex-shrink:0;';
|
|
102
|
+
saveBtn.addEventListener('click', function(e) {
|
|
103
|
+
e.stopPropagation();
|
|
104
|
+
var key = input.value.trim();
|
|
105
|
+
if (!key) return;
|
|
106
|
+
saveBtn.disabled = true; saveBtn.textContent = '...';
|
|
107
|
+
saveProviderKey(pid, key);
|
|
108
|
+
});
|
|
109
|
+
form.appendChild(input); form.appendChild(saveBtn);
|
|
110
|
+
setTimeout(function() { input.focus(); }, 50);
|
|
111
|
+
return form;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function toggleEdit(pid) { editingProvider = editingProvider === pid ? null : pid; renderDropdown(); }
|
|
115
|
+
|
|
116
|
+
function saveProviderKey(providerId, apiKey) {
|
|
117
|
+
fetch(BASE + '/api/auth/save-config', {
|
|
118
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
119
|
+
body: JSON.stringify({ providerId: providerId, apiKey: apiKey, defaultModel: '' })
|
|
120
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
121
|
+
if (data.success) { editingProvider = null; fetchProviderConfigs(); }
|
|
122
|
+
}).catch(function() { editingProvider = null; renderDropdown(); });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function toggleDropdown(e) {
|
|
126
|
+
e.stopPropagation();
|
|
127
|
+
if (!dropdown.classList.contains('open')) { editingProvider = null; refresh(); }
|
|
128
|
+
dropdown.classList.toggle('open');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function closeDropdown() { dropdown.classList.remove('open'); editingProvider = null; }
|
|
132
|
+
|
|
133
|
+
function triggerAuth(agentId) {
|
|
134
|
+
if (authRunning) return;
|
|
135
|
+
fetch(BASE + '/api/agents/' + agentId + '/auth', {
|
|
136
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}'
|
|
137
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
138
|
+
if (data.ok) {
|
|
139
|
+
authRunning = true; showTerminalTab(); switchToTerminalView();
|
|
140
|
+
var term = getTerminal();
|
|
141
|
+
if (term) { term.clear(); term.writeln('\x1b[36m[authenticating ' + agentId + ']\x1b[0m\r\n'); }
|
|
142
|
+
}
|
|
143
|
+
}).catch(function() {});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function onWsMessage(e) {
|
|
147
|
+
var data = e.detail;
|
|
148
|
+
if (!data || data.conversationId !== AUTH_CONV_ID) return;
|
|
149
|
+
if (data.type === 'script_started') {
|
|
150
|
+
authRunning = true; showTerminalTab(); switchToTerminalView();
|
|
151
|
+
var term = getTerminal();
|
|
152
|
+
if (term) { term.clear(); term.writeln('\x1b[36m[authenticating ' + (data.agentId || '') + ']\x1b[0m\r\n'); }
|
|
153
|
+
} else if (data.type === 'script_output') {
|
|
154
|
+
showTerminalTab();
|
|
155
|
+
var term = getTerminal();
|
|
156
|
+
if (term) term.write(data.data);
|
|
157
|
+
} else if (data.type === 'script_stopped') {
|
|
158
|
+
authRunning = false;
|
|
159
|
+
var term = getTerminal();
|
|
160
|
+
var msg = data.error ? data.error : ('exited with code ' + (data.code || 0));
|
|
161
|
+
if (term) term.writeln('\r\n\x1b[90m[auth ' + msg + ']\x1b[0m');
|
|
162
|
+
setTimeout(refresh, 1000);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function showTerminalTab() { var t = document.getElementById('terminalTabBtn'); if (t) t.style.display = ''; }
|
|
167
|
+
function switchToTerminalView() {
|
|
168
|
+
var bar = document.getElementById('viewToggleBar');
|
|
169
|
+
if (!bar) return;
|
|
170
|
+
var t = bar.querySelector('[data-view="terminal"]'); if (t) t.click();
|
|
171
|
+
}
|
|
172
|
+
function getTerminal() { return window.scriptRunner ? window.scriptRunner.getTerminal() : null; }
|
|
173
|
+
function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
174
|
+
|
|
175
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
|
176
|
+
else init();
|
|
177
|
+
window.agentAuth = { refresh: refresh };
|
|
178
|
+
})();
|
package/static/js/client.js
CHANGED
|
@@ -372,6 +372,9 @@ class AgentGUIClient {
|
|
|
372
372
|
case 'queue_status':
|
|
373
373
|
this.handleQueueStatus(data);
|
|
374
374
|
break;
|
|
375
|
+
case 'queue_updated':
|
|
376
|
+
this.handleQueueUpdated(data);
|
|
377
|
+
break;
|
|
375
378
|
case 'rate_limit_hit':
|
|
376
379
|
this.handleRateLimitHit(data);
|
|
377
380
|
break;
|
|
@@ -478,7 +481,7 @@ class AgentGUIClient {
|
|
|
478
481
|
const bFrag = document.createDocumentFragment();
|
|
479
482
|
sList.forEach(chunk => {
|
|
480
483
|
if (!chunk.block?.type) return;
|
|
481
|
-
const el = this.renderer.renderBlock(chunk.block, chunk);
|
|
484
|
+
const el = this.renderer.renderBlock(chunk.block, chunk, bFrag);
|
|
482
485
|
if (!el) return;
|
|
483
486
|
if (chunk.block.type === 'tool_result') {
|
|
484
487
|
const lastInFrag = bFrag.lastElementChild;
|
|
@@ -699,21 +702,68 @@ class AgentGUIClient {
|
|
|
699
702
|
|
|
700
703
|
handleQueueStatus(data) {
|
|
701
704
|
if (data.conversationId !== this.state.currentConversation?.id) return;
|
|
705
|
+
this.fetchAndRenderQueue(data.conversationId);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
handleQueueUpdated(data) {
|
|
709
|
+
if (data.conversationId !== this.state.currentConversation?.id) return;
|
|
710
|
+
this.fetchAndRenderQueue(data.conversationId);
|
|
711
|
+
}
|
|
702
712
|
|
|
713
|
+
async fetchAndRenderQueue(conversationId) {
|
|
703
714
|
const outputEl = document.querySelector('.conversation-messages');
|
|
704
715
|
if (!outputEl) return;
|
|
705
716
|
|
|
706
|
-
|
|
707
|
-
|
|
717
|
+
try {
|
|
718
|
+
const response = await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/queue`);
|
|
719
|
+
const { queue } = await response.json();
|
|
720
|
+
|
|
721
|
+
let queueEl = outputEl.querySelector('.queue-indicator');
|
|
722
|
+
if (!queue || queue.length === 0) {
|
|
723
|
+
if (queueEl) queueEl.remove();
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
708
727
|
if (!queueEl) {
|
|
709
728
|
queueEl = document.createElement('div');
|
|
710
729
|
queueEl.className = 'queue-indicator';
|
|
711
|
-
queueEl.style.cssText = 'padding:0.5rem 1rem;margin:0.5rem 0;border-radius:0.375rem;background:var(--color-warning);color:#000;font-size:0.875rem;text-align:center;';
|
|
712
730
|
outputEl.appendChild(queueEl);
|
|
713
731
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
732
|
+
|
|
733
|
+
queueEl.innerHTML = queue.map((q, i) => `
|
|
734
|
+
<div class="queue-item" data-message-id="${q.messageId}" style="padding:0.5rem 1rem;margin:0.5rem 0;border-radius:0.375rem;background:var(--color-warning);color:#000;font-size:0.875rem;display:flex;align-items:center;gap:0.5rem;">
|
|
735
|
+
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${i + 1}. ${this.escapeHtml(q.content)}</span>
|
|
736
|
+
<button class="queue-edit-btn" data-index="${i}" style="padding:0.25rem 0.5rem;background:transparent;border:1px solid #000;border-radius:0.25rem;cursor:pointer;font-size:0.75rem;">Edit</button>
|
|
737
|
+
<button class="queue-delete-btn" data-index="${i}" style="padding:0.25rem 0.5rem;background:transparent;border:1px solid #000;border-radius:0.25rem;cursor:pointer;font-size:0.75rem;">Delete</button>
|
|
738
|
+
</div>
|
|
739
|
+
`).join('');
|
|
740
|
+
|
|
741
|
+
queueEl.querySelectorAll('.queue-delete-btn').forEach(btn => {
|
|
742
|
+
btn.addEventListener('click', async (e) => {
|
|
743
|
+
const index = parseInt(e.target.dataset.index);
|
|
744
|
+
const msgId = queue[index].messageId;
|
|
745
|
+
if (confirm('Delete this queued message?')) {
|
|
746
|
+
await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/queue/${msgId}`, { method: 'DELETE' });
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
queueEl.querySelectorAll('.queue-edit-btn').forEach(btn => {
|
|
752
|
+
btn.addEventListener('click', (e) => {
|
|
753
|
+
const index = parseInt(e.target.dataset.index);
|
|
754
|
+
const q = queue[index];
|
|
755
|
+
const newContent = prompt('Edit message:', q.content);
|
|
756
|
+
if (newContent !== null && newContent !== q.content) {
|
|
757
|
+
fetch(window.__BASE_URL + `/api/conversations/${conversationId}/queue/${q.messageId}`, {
|
|
758
|
+
method: 'PATCH',
|
|
759
|
+
headers: { 'Content-Type': 'application/json' },
|
|
760
|
+
body: JSON.stringify({ content: newContent })
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
} catch (err) {
|
|
766
|
+
console.error('Failed to fetch queue:', err);
|
|
717
767
|
}
|
|
718
768
|
}
|
|
719
769
|
|
|
@@ -1160,8 +1210,8 @@ class AgentGUIClient {
|
|
|
1160
1210
|
if (!streamingEl) return;
|
|
1161
1211
|
const blocksEl = streamingEl.querySelector('.streaming-blocks');
|
|
1162
1212
|
if (!blocksEl) return;
|
|
1163
|
-
const element = this.renderer.renderBlock(chunk.block, chunk);
|
|
1164
|
-
if (!element) return;
|
|
1213
|
+
const element = this.renderer.renderBlock(chunk.block, chunk, blocksEl);
|
|
1214
|
+
if (!element) { this.scrollToBottom(); return; }
|
|
1165
1215
|
if (chunk.block.type === 'tool_result') {
|
|
1166
1216
|
const matchById = chunk.block.tool_use_id && blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${chunk.block.tool_use_id}"]`);
|
|
1167
1217
|
const lastEl = blocksEl.lastElementChild;
|
|
@@ -1187,8 +1237,8 @@ class AgentGUIClient {
|
|
|
1187
1237
|
const blocksEl = streamingEl.querySelector('.streaming-blocks');
|
|
1188
1238
|
if (!blocksEl) continue;
|
|
1189
1239
|
for (const chunk of groups[sid]) {
|
|
1190
|
-
const el = this.renderer.renderBlock(chunk.block, chunk);
|
|
1191
|
-
if (!el) continue;
|
|
1240
|
+
const el = this.renderer.renderBlock(chunk.block, chunk, blocksEl);
|
|
1241
|
+
if (!el) { appended = true; continue; }
|
|
1192
1242
|
if (chunk.block.type === 'tool_result') {
|
|
1193
1243
|
const matchById = chunk.block.tool_use_id && blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${chunk.block.tool_use_id}"]`);
|
|
1194
1244
|
const lastEl = blocksEl.lastElementChild;
|
|
@@ -1271,7 +1321,6 @@ class AgentGUIClient {
|
|
|
1271
1321
|
*/
|
|
1272
1322
|
disableControls() {
|
|
1273
1323
|
if (this.ui.sendButton) this.ui.sendButton.disabled = true;
|
|
1274
|
-
if (this.ui.agentSelector) this.ui.agentSelector.disabled = true;
|
|
1275
1324
|
}
|
|
1276
1325
|
|
|
1277
1326
|
/**
|
|
@@ -1279,7 +1328,6 @@ class AgentGUIClient {
|
|
|
1279
1328
|
*/
|
|
1280
1329
|
enableControls() {
|
|
1281
1330
|
if (this.ui.sendButton) this.ui.sendButton.disabled = false;
|
|
1282
|
-
if (this.ui.agentSelector) this.ui.agentSelector.disabled = false;
|
|
1283
1331
|
}
|
|
1284
1332
|
|
|
1285
1333
|
/**
|
|
@@ -1501,7 +1549,7 @@ class AgentGUIClient {
|
|
|
1501
1549
|
const blockFrag = document.createDocumentFragment();
|
|
1502
1550
|
sessionChunkList.forEach(chunk => {
|
|
1503
1551
|
if (!chunk.block?.type) return;
|
|
1504
|
-
const element = this.renderer.renderBlock(chunk.block, chunk);
|
|
1552
|
+
const element = this.renderer.renderBlock(chunk.block, chunk, blockFrag);
|
|
1505
1553
|
if (!element) return;
|
|
1506
1554
|
if (chunk.block.type === 'tool_result') {
|
|
1507
1555
|
const lastInFrag = blockFrag.lastElementChild;
|
|
@@ -328,13 +328,13 @@ class StreamingRenderer {
|
|
|
328
328
|
/**
|
|
329
329
|
* Render Claude message blocks with beautiful styling
|
|
330
330
|
*/
|
|
331
|
-
renderBlock(block, context = {}) {
|
|
331
|
+
renderBlock(block, context = {}, targetContainer = null) {
|
|
332
332
|
if (!block || !block.type) return null;
|
|
333
333
|
|
|
334
334
|
try {
|
|
335
335
|
switch (block.type) {
|
|
336
336
|
case 'text':
|
|
337
|
-
return this.renderBlockText(block, context);
|
|
337
|
+
return this.renderBlockText(block, context, targetContainer);
|
|
338
338
|
case 'code':
|
|
339
339
|
return this.renderBlockCode(block, context);
|
|
340
340
|
case 'thinking':
|
|
@@ -363,7 +363,7 @@ class StreamingRenderer {
|
|
|
363
363
|
/**
|
|
364
364
|
* Render text block with semantic HTML
|
|
365
365
|
*/
|
|
366
|
-
renderBlockText(block, context) {
|
|
366
|
+
renderBlockText(block, context, targetContainer = null) {
|
|
367
367
|
const text = block.text || '';
|
|
368
368
|
const isHtml = this.containsHtmlTags(text);
|
|
369
369
|
const cached = this.renderCache.get(text);
|
|
@@ -373,7 +373,8 @@ class StreamingRenderer {
|
|
|
373
373
|
this.renderCache.set(text, html);
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
-
const
|
|
376
|
+
const container = targetContainer || this.outputContainer;
|
|
377
|
+
const lastChild = container && container.lastElementChild;
|
|
377
378
|
if (lastChild && lastChild.classList.contains('block-text') && !isHtml && !lastChild.classList.contains('html-content')) {
|
|
378
379
|
lastChild.innerHTML += html;
|
|
379
380
|
return null;
|