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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.198",
3
+ "version": "1.0.200",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
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: 32px; height: 32px; background: none; border: none;
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: 16px; height: 16px; }
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
+ })();
@@ -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
- let queueEl = outputEl.querySelector('.queue-indicator');
707
- if (data.queueLength > 0) {
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
- queueEl.textContent = `${data.queueLength} message${data.queueLength > 1 ? 's' : ''} queued`;
715
- } else if (queueEl) {
716
- queueEl.remove();
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 lastChild = this.outputContainer && this.outputContainer.lastElementChild;
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;