agentgui 1.0.413 → 1.0.415

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.
@@ -97,7 +97,20 @@ export function register(router, deps) {
97
97
  return data;
98
98
  });
99
99
 
100
- router.handle('agent.ls', () => ({ agents: discoveredAgents }));
100
+ router.handle('agent.ls', () => {
101
+ // Get local agents only (avoid external HTTP calls in WebSocket to prevent recursion)
102
+ const localAgents = discoveredAgents.map(agent => ({
103
+ id: agent.id,
104
+ name: agent.name,
105
+ icon: agent.icon,
106
+ path: agent.path,
107
+ protocol: agent.protocol || 'unknown',
108
+ description: agent.description || '',
109
+ status: 'available'
110
+ }));
111
+
112
+ return { agents: localAgents };
113
+ });
101
114
 
102
115
  router.handle('agent.get', (p) => {
103
116
  const a = discoveredAgents.find(x => x.id === p.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.413",
3
+ "version": "1.0.415",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -342,31 +342,96 @@ function findCommand(cmd) {
342
342
  }
343
343
  }
344
344
 
345
+ async function queryACPServerAgents(baseUrl) {
346
+ try {
347
+ // Ensure correct endpoint format (handle both /api and /api/ cases)
348
+ const endpoint = baseUrl.endsWith('/') ? `${baseUrl}agents/search` : `${baseUrl}/agents/search`;
349
+ const response = await fetch(endpoint, {
350
+ method: 'POST',
351
+ headers: {
352
+ 'Content-Type': 'application/json',
353
+ },
354
+ body: JSON.stringify({}),
355
+ });
356
+
357
+ if (!response.ok) {
358
+ console.error(`Failed to query ACP agents from ${baseUrl}: ${response.status}`);
359
+ return [];
360
+ }
361
+
362
+ const data = await response.json();
363
+ if (!data.agents || !Array.isArray(data.agents)) {
364
+ console.error(`Invalid agents response from ${baseUrl}`);
365
+ return [];
366
+ }
367
+
368
+ // Convert ACP agent format to our internal format
369
+ return data.agents.map(agent => ({
370
+ id: agent.agent_id || agent.id,
371
+ name: agent.metadata?.ref?.name || agent.name || 'Unknown Agent',
372
+ icon: agent.metadata?.ref?.name?.charAt(0) || 'A',
373
+ path: baseUrl,
374
+ protocol: 'acp',
375
+ description: agent.metadata?.description || '',
376
+ }));
377
+ } catch (error) {
378
+ console.error(`Error querying ACP server ${baseUrl}:`, error.message);
379
+ return [];
380
+ }
381
+ }
382
+
345
383
  function discoverAgents() {
346
384
  const agents = [];
347
385
  const binaries = [
348
- { cmd: 'claude', id: 'claude-code', name: 'Claude Code', icon: 'C' },
349
- { cmd: 'opencode', id: 'opencode', name: 'OpenCode', icon: 'O' },
350
- { cmd: 'gemini', id: 'gemini', name: 'Gemini CLI', icon: 'G' },
351
- { cmd: 'kilo', id: 'kilo', name: 'Kilo Code', icon: 'K' },
352
- { cmd: 'goose', id: 'goose', name: 'Goose', icon: 'g' },
353
- { cmd: 'openhands', id: 'openhands', name: 'OpenHands', icon: 'H' },
354
- { cmd: 'augment', id: 'augment', name: 'Augment Code', icon: 'A' },
355
- { cmd: 'cline', id: 'cline', name: 'Cline', icon: 'c' },
356
- { cmd: 'kimi', id: 'kimi', name: 'Kimi CLI', icon: 'K' },
357
- { cmd: 'qwen-code', id: 'qwen', name: 'Qwen Code', icon: 'Q' },
358
- { cmd: 'codex', id: 'codex', name: 'Codex CLI', icon: 'X' },
359
- { cmd: 'mistral-vibe', id: 'mistral', name: 'Mistral Vibe', icon: 'M' },
360
- { cmd: 'kiro', id: 'kiro', name: 'Kiro CLI', icon: 'k' },
361
- { cmd: 'fast-agent', id: 'fast-agent', name: 'fast-agent', icon: 'F' },
386
+ { cmd: 'claude', id: 'claude-code', name: 'Claude Code', icon: 'C', protocol: 'cli' },
387
+ { cmd: 'opencode', id: 'opencode', name: 'OpenCode', icon: 'O', protocol: 'acp' },
388
+ { cmd: 'gemini', id: 'gemini', name: 'Gemini CLI', icon: 'G', protocol: 'acp' },
389
+ { cmd: 'kilo', id: 'kilo', name: 'Kilo Code', icon: 'K', protocol: 'acp' },
390
+ { cmd: 'goose', id: 'goose', name: 'Goose', icon: 'g', protocol: 'acp' },
391
+ { cmd: 'openhands', id: 'openhands', name: 'OpenHands', icon: 'H', protocol: 'acp' },
392
+ { cmd: 'augment', id: 'augment', name: 'Augment Code', icon: 'A', protocol: 'acp' },
393
+ { cmd: 'cline', id: 'cline', name: 'Cline', icon: 'c', protocol: 'acp' },
394
+ { cmd: 'kimi', id: 'kimi', name: 'Kimi CLI', icon: 'K', protocol: 'acp' },
395
+ { cmd: 'qwen-code', id: 'qwen', name: 'Qwen Code', icon: 'Q', protocol: 'acp' },
396
+ { cmd: 'codex', id: 'codex', name: 'Codex CLI', icon: 'X', protocol: 'acp' },
397
+ { cmd: 'mistral-vibe', id: 'mistral', name: 'Mistral Vibe', icon: 'M', protocol: 'acp' },
398
+ { cmd: 'kiro', id: 'kiro', name: 'Kiro CLI', icon: 'k', protocol: 'acp' },
399
+ { cmd: 'fast-agent', id: 'fast-agent', name: 'fast-agent', icon: 'F', protocol: 'acp' },
362
400
  ];
363
401
  for (const bin of binaries) {
364
402
  const result = findCommand(bin.cmd);
365
- if (result) agents.push({ id: bin.id, name: bin.name, icon: bin.icon, path: result });
403
+ if (result) agents.push({
404
+ id: bin.id,
405
+ name: bin.name,
406
+ icon: bin.icon,
407
+ path: result,
408
+ protocol: bin.protocol
409
+ });
366
410
  }
367
411
  return agents;
368
412
  }
369
413
 
414
+ // Function to discover agents from external ACP servers
415
+ async function discoverExternalACPServers() {
416
+ // Default ACP servers to query (excluding local server to prevent recursion)
417
+ const acpServers = [
418
+ 'http://localhost:8080', // Common default ACP port
419
+ ];
420
+
421
+ const externalAgents = [];
422
+ for (const serverUrl of acpServers) {
423
+ try {
424
+ console.log(`Querying ACP agents from: ${serverUrl}`);
425
+ const agents = await queryACPServerAgents(serverUrl);
426
+ externalAgents.push(...agents);
427
+ } catch (error) {
428
+ console.error(`Failed to query ${serverUrl}:`, error.message);
429
+ }
430
+ }
431
+
432
+ return externalAgents;
433
+ }
434
+
370
435
  const discoveredAgents = discoverAgents();
371
436
  initializeDescriptors(discoveredAgents);
372
437
 
@@ -569,6 +634,11 @@ async function getModelsForAgent(agentId) {
569
634
 
570
635
  // Fallback default models for agents that fail to retrieve models
571
636
  const fallbackModels = {
637
+ 'claude-code': [
638
+ { id: 'haiku', label: 'Haiku (Default)' },
639
+ { id: 'sonnet', label: 'Sonnet' },
640
+ { id: 'opus', label: 'Opus' }
641
+ ],
572
642
  'gemini': [
573
643
  { id: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro' },
574
644
  { id: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash' },
@@ -585,15 +655,65 @@ async function getModelsForAgent(agentId) {
585
655
  { id: 'claude-sonnet-4', label: 'Claude Sonnet 4' },
586
656
  { id: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' },
587
657
  { id: 'gpt-4', label: 'GPT-4' }
658
+ ],
659
+ 'goose': [
660
+ { id: 'default', label: 'Default Model' },
661
+ { id: 'goose-pro', label: 'Goose Pro' }
662
+ ],
663
+ 'openhands': [
664
+ { id: 'default', label: 'Default Model' },
665
+ { id: 'openhands-1', label: 'OpenHands 1' }
666
+ ],
667
+ 'augment': [
668
+ { id: 'default', label: 'Default Model' },
669
+ { id: 'augment-pro', label: 'Augment Pro' }
670
+ ],
671
+ 'cline': [
672
+ { id: 'default', label: 'Default Model' },
673
+ { id: 'cline-1', label: 'Cline 1' }
674
+ ],
675
+ 'kimi': [
676
+ { id: 'default', label: 'Default Model' },
677
+ { id: 'kimi-pro', label: 'Kimi Pro' }
678
+ ],
679
+ 'qwen': [
680
+ { id: 'default', label: 'Default Model' },
681
+ { id: 'qwen-7b', label: 'Qwen 7B' },
682
+ { id: 'qwen-14b', label: 'Qwen 14B' }
683
+ ],
684
+ 'codex': [
685
+ { id: 'default', label: 'Default Model' },
686
+ { id: 'code-davinci-002', label: 'Code Davinci 002' },
687
+ { id: 'code-cushman-001', label: 'Code Cushman 001' }
688
+ ],
689
+ 'mistral': [
690
+ { id: 'default', label: 'Default Model' },
691
+ { id: 'mistral-small', label: 'Mistral Small' },
692
+ { id: 'mistral-medium', label: 'Mistral Medium' },
693
+ { id: 'mistral-large', label: 'Mistral Large' }
694
+ ],
695
+ 'kiro': [
696
+ { id: 'default', label: 'Default Model' },
697
+ { id: 'kiro-1', label: 'Kiro 1' }
698
+ ],
699
+ 'fast-agent': [
700
+ { id: 'default', label: 'Default Model' },
701
+ { id: 'fast-agent-1', label: 'Fast Agent 1' }
588
702
  ]
589
703
  };
590
704
 
591
705
  if (fallbackModels[agentId]) {
592
- modelCache.set(agentId, { models: fallbackModels[agentId], timestamp: Date.now() });
593
- return fallbackModels[agentId];
706
+ const models = fallbackModels[agentId];
707
+ modelCache.set(agentId, { models, timestamp: Date.now() });
708
+ return models;
594
709
  }
595
710
 
596
- return [];
711
+ // Default fallback for any other agent
712
+ const defaultFallback = [
713
+ { id: 'default', label: 'Default Model' }
714
+ ];
715
+ modelCache.set(agentId, { models: defaultFallback, timestamp: Date.now() });
716
+ return defaultFallback;
597
717
  }
598
718
 
599
719
  const GEMINI_SCOPES = [
@@ -1892,8 +2012,31 @@ const server = http.createServer(async (req, res) => {
1892
2012
 
1893
2013
  if (pathOnly === '/api/agents/search' && req.method === 'POST') {
1894
2014
  const body = await parseBody(req);
1895
- const result = queries.searchAgents(discoveredAgents, body);
1896
- sendJSON(req, res, 200, result);
2015
+ try {
2016
+ // Get local agents
2017
+ const localResult = queries.searchAgents(discoveredAgents, body);
2018
+
2019
+ // Get external agents from ACP servers
2020
+ const externalAgents = await discoverExternalACPServers();
2021
+ const externalResult = queries.searchAgents(externalAgents, body);
2022
+
2023
+ // Combine results
2024
+ const combinedAgents = [...localResult.agents, ...externalResult.agents];
2025
+ const total = localResult.total + externalResult.total;
2026
+ const hasMore = localResult.hasMore || externalResult.hasMore;
2027
+
2028
+ sendJSON(req, res, 200, {
2029
+ agents: combinedAgents,
2030
+ total,
2031
+ limit: body.limit || 50,
2032
+ offset: body.offset || 0,
2033
+ hasMore,
2034
+ });
2035
+ } catch (error) {
2036
+ console.error('Error searching agents:', error);
2037
+ const result = queries.searchAgents(discoveredAgents, body);
2038
+ sendJSON(req, res, 200, result);
2039
+ }
1897
2040
  return;
1898
2041
  }
1899
2042
 
package/static/index.html CHANGED
@@ -804,25 +804,35 @@
804
804
  align-items: flex-end;
805
805
  }
806
806
 
807
- .agent-selector {
808
- padding: 0.5rem;
809
- border: none;
810
- border-radius: 0.375rem;
811
- background-color: var(--color-bg-secondary);
812
- color: var(--color-text-primary);
813
- font-size: 0.8rem;
814
- cursor: pointer;
815
- flex-shrink: 0;
816
- width: auto;
817
- min-width: 80px;
818
- max-width: 200px;
819
- }
820
-
821
- .agent-selector:disabled, .model-selector:disabled {
822
- opacity: 0.5;
823
- cursor: not-allowed;
824
- background-color: var(--color-bg-secondary);
825
- }
807
+ .agent-selector {
808
+ padding: 0.5rem;
809
+ border: none;
810
+ border-radius: 0.375rem;
811
+ background-color: var(--color-bg-secondary);
812
+ color: var(--color-text-primary);
813
+ font-size: 0.8rem;
814
+ cursor: pointer;
815
+ flex-shrink: 0;
816
+ width: auto;
817
+ min-width: 80px;
818
+ max-width: 200px;
819
+ }
820
+
821
+ .cli-agent-selector {
822
+ /* CLI agents have a different background color */
823
+ background-color: var(--color-bg-secondary);
824
+ }
825
+
826
+ .acp-agent-selector {
827
+ /* ACP agents have a different background color */
828
+ background-color: var(--color-bg-secondary);
829
+ }
830
+
831
+ .agent-selector:disabled, .model-selector:disabled {
832
+ opacity: 0.5;
833
+ cursor: not-allowed;
834
+ background-color: var(--color-bg-secondary);
835
+ }
826
836
 
827
837
  .model-selector {
828
838
  padding: 0.5rem;
@@ -3167,17 +3177,18 @@
3167
3177
  </div>
3168
3178
 
3169
3179
  <!-- Input area: fixed at bottom -->
3170
- <div class="input-section">
3171
- <div class="input-wrapper">
3172
- <select class="agent-selector" data-agent-selector title="Select agent"></select>
3173
- <select class="model-selector" data-model-selector title="Select model" data-empty="true"></select>
3174
- <textarea
3175
- class="message-textarea"
3176
- data-message-input
3177
- placeholder="Message AgentGUI... (Ctrl+Enter to send)"
3178
- aria-label="Message input"
3179
- rows="1"
3180
- ></textarea>
3180
+ <div class="input-section">
3181
+ <div class="input-wrapper">
3182
+ <select class="agent-selector cli-agent-selector" data-cli-agent-selector title="Select CLI agent"></select>
3183
+ <select class="agent-selector acp-agent-selector" data-acp-agent-selector title="Select ACP agent"></select>
3184
+ <select class="model-selector" data-model-selector title="Select model" data-empty="true"></select>
3185
+ <textarea
3186
+ class="message-textarea"
3187
+ data-message-input
3188
+ placeholder="Message AgentGUI... (Ctrl+Enter to send)"
3189
+ aria-label="Message input"
3190
+ rows="1"
3191
+ ></textarea>
3181
3192
  <button class="inject-btn" id="injectBtn" title="Inject instructions into running agent" aria-label="Inject instructions">
3182
3193
  <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
3183
3194
  <path d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"/>
@@ -341,30 +341,50 @@ class AgentGUIClient {
341
341
  });
342
342
  }
343
343
 
344
- /**
345
- * Setup UI elements
346
- */
347
- setupUI() {
348
- const container = document.getElementById(this.config.containerId);
349
- if (!container) {
350
- throw new Error(`Container not found: ${this.config.containerId}`);
351
- }
352
-
353
- // Get references to key UI elements
354
- this.ui.statusIndicator = document.querySelector('[data-status-indicator]');
355
- this.ui.messageInput = document.querySelector('[data-message-input]');
356
- this.ui.sendButton = document.querySelector('[data-send-button]');
357
- this.ui.agentSelector = document.querySelector('[data-agent-selector]');
358
- this.ui.modelSelector = document.querySelector('[data-model-selector]');
359
-
360
- if (this.ui.agentSelector) {
361
- this.ui.agentSelector.addEventListener('change', () => {
362
- if (!this._agentLocked) {
363
- this.loadModelsForAgent(this.ui.agentSelector.value);
364
- this.saveAgentAndModelToConversation();
365
- }
366
- });
367
- }
344
+ /**
345
+ * Setup UI elements
346
+ */
347
+ setupUI() {
348
+ const container = document.getElementById(this.config.containerId);
349
+ if (!container) {
350
+ throw new Error(`Container not found: ${this.config.containerId}`);
351
+ }
352
+
353
+ // Get references to key UI elements
354
+ this.ui.statusIndicator = document.querySelector('[data-status-indicator]');
355
+ this.ui.messageInput = document.querySelector('[data-message-input]');
356
+ this.ui.sendButton = document.querySelector('[data-send-button]');
357
+ this.ui.cliAgentSelector = document.querySelector('[data-cli-agent-selector]');
358
+ this.ui.acpAgentSelector = document.querySelector('[data-acp-agent-selector]');
359
+ this.ui.modelSelector = document.querySelector('[data-model-selector]');
360
+
361
+ if (this.ui.cliAgentSelector) {
362
+ this.ui.cliAgentSelector.addEventListener('change', () => {
363
+ if (!this._agentLocked) {
364
+ this.loadModelsForAgent(this.ui.cliAgentSelector.value);
365
+ this.saveAgentAndModelToConversation();
366
+ // Hide ACP selector when CLI is selected
367
+ if (this.ui.acpAgentSelector) {
368
+ this.ui.acpAgentSelector.value = '';
369
+ this.ui.acpAgentSelector.style.display = 'none';
370
+ }
371
+ }
372
+ });
373
+ }
374
+
375
+ if (this.ui.acpAgentSelector) {
376
+ this.ui.acpAgentSelector.addEventListener('change', () => {
377
+ if (!this._agentLocked) {
378
+ this.loadModelsForAgent(this.ui.acpAgentSelector.value);
379
+ this.saveAgentAndModelToConversation();
380
+ // Hide CLI selector when ACP is selected
381
+ if (this.ui.cliAgentSelector) {
382
+ this.ui.cliAgentSelector.value = '';
383
+ this.ui.cliAgentSelector.style.display = 'none';
384
+ }
385
+ }
386
+ });
387
+ }
368
388
 
369
389
  if (this.ui.modelSelector) {
370
390
  this.ui.modelSelector.addEventListener('change', () => {
@@ -1244,12 +1264,12 @@ class AgentGUIClient {
1244
1264
  }
1245
1265
  }
1246
1266
 
1247
- async startExecution() {
1248
- const prompt = this.ui.messageInput?.value || '';
1249
- const conv = this.state.currentConversation;
1250
- const isNewConversation = conv && !conv.messageCount && !this.state.streamingConversations.has(conv.id);
1251
- const agentId = (isNewConversation ? this.ui.agentSelector?.value : null) || conv?.agentType || this.ui.agentSelector?.value || 'claude-code';
1252
- const model = this.ui.modelSelector?.value || null;
1267
+ async startExecution() {
1268
+ const prompt = this.ui.messageInput?.value || '';
1269
+ const conv = this.state.currentConversation;
1270
+ const isNewConversation = conv && !conv.messageCount && !this.state.streamingConversations.has(conv.id);
1271
+ const agentId = (isNewConversation ? this.getCurrentAgent() : null) || conv?.agentType || this.getCurrentAgent() || 'claude-code';
1272
+ const model = this.ui.modelSelector?.value || null;
1253
1273
 
1254
1274
  if (!prompt.trim()) {
1255
1275
  this.showError('Please enter a prompt');
@@ -1816,29 +1836,53 @@ class AgentGUIClient {
1816
1836
  /**
1817
1837
  * Load agents
1818
1838
  */
1819
- async loadAgents() {
1820
- return this._dedupedFetch('loadAgents', async () => {
1821
- try {
1822
- const { agents } = await window.wsClient.rpc('agent.ls');
1823
- this.state.agents = agents;
1824
-
1825
- if (this.ui.agentSelector) {
1826
- this.ui.agentSelector.innerHTML = agents
1827
- .map(agent => `<option value="${agent.id}">${agent.name}</option>`)
1828
- .join('');
1829
- }
1830
-
1831
- window.dispatchEvent(new CustomEvent('agents-loaded', { detail: { agents } }));
1832
- if (agents.length > 0 && !this._agentLocked) {
1833
- this.loadModelsForAgent(agents[0].id);
1834
- }
1835
- return agents;
1836
- } catch (error) {
1837
- console.error('Failed to load agents:', error);
1838
- return [];
1839
- }
1840
- });
1841
- }
1839
+ async loadAgents() {
1840
+ return this._dedupedFetch('loadAgents', async () => {
1841
+ try {
1842
+ const { agents } = await window.wsClient.rpc('agent.ls');
1843
+ this.state.agents = agents;
1844
+
1845
+ // Split agents by protocol
1846
+ const cliAgents = agents.filter(agent => agent.protocol === 'cli');
1847
+ const acpAgents = agents.filter(agent => agent.protocol === 'acp');
1848
+
1849
+ // Populate CLI agent selector
1850
+ if (this.ui.cliAgentSelector) {
1851
+ if (cliAgents.length > 0) {
1852
+ this.ui.cliAgentSelector.innerHTML = cliAgents
1853
+ .map(agent => `<option value="${agent.id}">${agent.name}</option>`)
1854
+ .join('');
1855
+ this.ui.cliAgentSelector.style.display = 'inline-block';
1856
+ } else {
1857
+ this.ui.cliAgentSelector.innerHTML = '';
1858
+ this.ui.cliAgentSelector.style.display = 'none';
1859
+ }
1860
+ }
1861
+
1862
+ // Populate ACP agent selector
1863
+ if (this.ui.acpAgentSelector) {
1864
+ if (acpAgents.length > 0) {
1865
+ this.ui.acpAgentSelector.innerHTML = acpAgents
1866
+ .map(agent => `<option value="${agent.id}">${agent.name}</option>`)
1867
+ .join('');
1868
+ this.ui.acpAgentSelector.style.display = 'inline-block';
1869
+ } else {
1870
+ this.ui.acpAgentSelector.innerHTML = '';
1871
+ this.ui.acpAgentSelector.style.display = 'none';
1872
+ }
1873
+ }
1874
+
1875
+ window.dispatchEvent(new CustomEvent('agents-loaded', { detail: { agents } }));
1876
+ if (agents.length > 0 && !this._agentLocked) {
1877
+ this.loadModelsForAgent(agents[0].id);
1878
+ }
1879
+ return agents;
1880
+ } catch (error) {
1881
+ console.error('Failed to load agents:', error);
1882
+ return [];
1883
+ }
1884
+ });
1885
+ }
1842
1886
 
1843
1887
  async checkSpeechStatus() {
1844
1888
  try {
@@ -1890,48 +1934,105 @@ class AgentGUIClient {
1890
1934
  .join('');
1891
1935
  }
1892
1936
 
1893
- lockAgentAndModel(agentId, model) {
1894
- this._agentLocked = true;
1895
- if (this.ui.agentSelector) {
1896
- this.ui.agentSelector.value = agentId;
1897
- this.ui.agentSelector.disabled = true;
1898
- }
1899
- this.loadModelsForAgent(agentId).then(() => {
1900
- if (this.ui.modelSelector && model) {
1901
- this.ui.modelSelector.value = model;
1902
- }
1903
- });
1904
- }
1905
-
1906
- unlockAgentAndModel() {
1907
- this._agentLocked = false;
1908
- if (this.ui.agentSelector) {
1909
- this.ui.agentSelector.disabled = false;
1910
- }
1911
- }
1937
+ lockAgentAndModel(agentId, model) {
1938
+ this._agentLocked = true;
1939
+
1940
+ // Find the agent to determine protocol
1941
+ const agent = this.state.agents.find(a => a.id === agentId);
1942
+
1943
+ if (agent) {
1944
+ if (agent.protocol === 'cli') {
1945
+ if (this.ui.cliAgentSelector) {
1946
+ this.ui.cliAgentSelector.value = agentId;
1947
+ this.ui.cliAgentSelector.disabled = true;
1948
+ }
1949
+ if (this.ui.acpAgentSelector) {
1950
+ this.ui.acpAgentSelector.value = '';
1951
+ this.ui.acpAgentSelector.disabled = true;
1952
+ this.ui.acpAgentSelector.style.display = 'none';
1953
+ }
1954
+ } else {
1955
+ if (this.ui.acpAgentSelector) {
1956
+ this.ui.acpAgentSelector.value = agentId;
1957
+ this.ui.acpAgentSelector.disabled = true;
1958
+ }
1959
+ if (this.ui.cliAgentSelector) {
1960
+ this.ui.cliAgentSelector.value = '';
1961
+ this.ui.cliAgentSelector.disabled = true;
1962
+ this.ui.cliAgentSelector.style.display = 'none';
1963
+ }
1964
+ }
1965
+ }
1966
+
1967
+ this.loadModelsForAgent(agentId).then(() => {
1968
+ if (this.ui.modelSelector && model) {
1969
+ this.ui.modelSelector.value = model;
1970
+ }
1971
+ });
1972
+ }
1973
+
1974
+ unlockAgentAndModel() {
1975
+ this._agentLocked = false;
1976
+ if (this.ui.cliAgentSelector) {
1977
+ this.ui.cliAgentSelector.disabled = false;
1978
+ }
1979
+ if (this.ui.acpAgentSelector) {
1980
+ this.ui.acpAgentSelector.disabled = false;
1981
+ }
1982
+
1983
+ // Show both selectors again when unlocking
1984
+ if (this.ui.cliAgentSelector && this.state.agents.some(a => a.protocol === 'cli')) {
1985
+ this.ui.cliAgentSelector.style.display = 'inline-block';
1986
+ }
1987
+ if (this.ui.acpAgentSelector && this.state.agents.some(a => a.protocol === 'acp')) {
1988
+ this.ui.acpAgentSelector.style.display = 'inline-block';
1989
+ }
1990
+ }
1912
1991
 
1913
1992
  /**
1914
1993
  * Apply agent and model selection based on conversation state
1915
1994
  * Consolidates duplicate logic for cached and fresh conversation loads
1916
1995
  */
1917
- applyAgentAndModelSelection(conversation, hasActivity) {
1918
- const agentId = conversation.agentId || conversation.agentType || 'claude-code';
1919
- const model = conversation.model || null;
1920
-
1921
- if (hasActivity) {
1922
- this.lockAgentAndModel(agentId, model);
1923
- } else {
1924
- this.unlockAgentAndModel();
1925
- if (this.ui.agentSelector) {
1926
- this.ui.agentSelector.value = agentId;
1927
- }
1928
- this.loadModelsForAgent(agentId).then(() => {
1929
- if (model && this.ui.modelSelector) {
1930
- this.ui.modelSelector.value = model;
1931
- }
1932
- });
1933
- }
1934
- }
1996
+ applyAgentAndModelSelection(conversation, hasActivity) {
1997
+ const agentId = conversation.agentId || conversation.agentType || 'claude-code';
1998
+ const model = conversation.model || null;
1999
+
2000
+ if (hasActivity) {
2001
+ this.lockAgentAndModel(agentId, model);
2002
+ } else {
2003
+ this.unlockAgentAndModel();
2004
+ // Find the agent to determine protocol
2005
+ const agent = this.state.agents.find(a => a.id === agentId);
2006
+
2007
+ if (agent) {
2008
+ if (agent.protocol === 'cli') {
2009
+ if (this.ui.cliAgentSelector) {
2010
+ this.ui.cliAgentSelector.value = agentId;
2011
+ this.ui.cliAgentSelector.style.display = 'inline-block';
2012
+ }
2013
+ if (this.ui.acpAgentSelector) {
2014
+ this.ui.acpAgentSelector.value = '';
2015
+ this.ui.acpAgentSelector.style.display = 'none';
2016
+ }
2017
+ } else {
2018
+ if (this.ui.acpAgentSelector) {
2019
+ this.ui.acpAgentSelector.value = agentId;
2020
+ this.ui.acpAgentSelector.style.display = 'inline-block';
2021
+ }
2022
+ if (this.ui.cliAgentSelector) {
2023
+ this.ui.cliAgentSelector.value = '';
2024
+ this.ui.cliAgentSelector.style.display = 'none';
2025
+ }
2026
+ }
2027
+ }
2028
+
2029
+ this.loadModelsForAgent(agentId).then(() => {
2030
+ if (model && this.ui.modelSelector) {
2031
+ this.ui.modelSelector.value = model;
2032
+ }
2033
+ });
2034
+ }
2035
+ }
1935
2036
 
1936
2037
  /**
1937
2038
  * Load conversations
@@ -2493,12 +2594,25 @@ class AgentGUIClient {
2493
2594
  }
2494
2595
  }
2495
2596
 
2496
- /**
2497
- * Get application state
2498
- */
2499
- getState() {
2500
- return { ...this.state };
2501
- }
2597
+ /**
2598
+ * Get current selected agent
2599
+ */
2600
+ getCurrentAgent() {
2601
+ if (this.ui.cliAgentSelector?.value) {
2602
+ return this.ui.cliAgentSelector.value;
2603
+ }
2604
+ if (this.ui.acpAgentSelector?.value) {
2605
+ return this.ui.acpAgentSelector.value;
2606
+ }
2607
+ return 'claude-code';
2608
+ }
2609
+
2610
+ /**
2611
+ * Get current selected model
2612
+ */
2613
+ getCurrentModel() {
2614
+ return this.ui.modelSelector?.value || null;
2615
+ }
2502
2616
 
2503
2617
  /**
2504
2618
  * Get metrics
@@ -1,106 +1,135 @@
1
- (function() {
2
- var ws = null;
3
- var term = null;
4
- var fitAddon = null;
5
- var termActive = false;
6
- var BASE = window.__BASE_URL || '';
7
-
8
- function getWsUrl() {
9
- var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
10
- return proto + '//' + location.host + BASE + '/sync';
11
- }
12
-
13
- function ensureTerm() {
14
- var output = document.getElementById('terminalOutput');
15
- if (!output) return false;
16
- if (term) return true;
17
- if (!window.Terminal || !window.FitAddon) return false;
18
-
19
- term = new Terminal({
20
- cursorBlink: true,
21
- fontSize: 14,
22
- fontFamily: 'Menlo, Monaco, "Courier New", monospace',
23
- theme: {
24
- background: '#0d1117',
25
- foreground: '#e6edf3',
26
- cursor: '#e6edf3',
27
- selectionBackground: '#3b4455'
28
- },
29
- convertEol: false,
30
- scrollback: 5000
31
- });
32
- fitAddon = new FitAddon.FitAddon();
33
- term.loadAddon(fitAddon);
34
- output.innerHTML = '';
35
- term.open(output);
36
- fitAddon.fit();
37
-
38
- term.onData(function(data) {
39
- if (ws && ws.readyState === WebSocket.OPEN) {
40
- var encoded = btoa(unescape(encodeURIComponent(data)));
41
- ws.send(JSON.stringify({ type: 'terminal_input', data: encoded }));
42
- }
43
- });
44
-
45
- window.addEventListener('resize', function() {
46
- if (fitAddon) try { fitAddon.fit(); } catch(_) {}
47
- });
48
- return true;
49
- }
50
-
51
- function connectAndStart() {
52
- if (ws && ws.readyState === WebSocket.OPEN) {
53
- ws.send(JSON.stringify({ type: 'terminal_start', cwd: window.__STARTUP_CWD || undefined }));
54
- return;
55
- }
56
- ws = new WebSocket(getWsUrl());
57
- ws.onopen = function() {
58
- ws.send(JSON.stringify({ type: 'terminal_start', cwd: window.__STARTUP_CWD || undefined }));
59
- };
60
- ws.onmessage = function(e) {
61
- try {
62
- var msg = JSON.parse(e.data);
63
- if (msg.type === 'terminal_output' && term) {
64
- var raw = msg.encoding === 'base64'
65
- ? decodeURIComponent(escape(atob(msg.data)))
66
- : msg.data;
67
- term.write(raw);
68
- } else if (msg.type === 'terminal_exit' && term) {
69
- term.write('\r\n[Process exited with code ' + msg.code + ']\r\n');
70
- if (termActive) setTimeout(connectAndStart, 2000);
71
- }
72
- } catch(_) {}
73
- };
74
- ws.onclose = function() {
75
- if (termActive) setTimeout(connectAndStart, 2000);
76
- };
77
- }
78
-
79
- function startTerminal() {
80
- if (!ensureTerm()) {
81
- setTimeout(startTerminal, 200);
82
- return;
83
- }
84
- termActive = true;
85
- connectAndStart();
86
- setTimeout(function() { if (fitAddon) try { fitAddon.fit(); } catch(_) {} }, 100);
87
- }
88
-
89
- function stopTerminal() {
90
- termActive = false;
91
- if (ws && ws.readyState === WebSocket.OPEN) {
92
- ws.send(JSON.stringify({ type: 'terminal_stop' }));
93
- }
94
- }
95
-
96
- window.addEventListener('view-switched', function(e) {
97
- if (e.detail.view === 'terminal') {
98
- startTerminal();
99
- } else if (termActive) {
100
- stopTerminal();
101
- }
102
- });
103
-
1
+ (function() {
2
+ var ws = null;
3
+ var term = null;
4
+ var fitAddon = null;
5
+ var termActive = false;
6
+ var BASE = window.__BASE_URL || '';
7
+
8
+ function getWsUrl() {
9
+ var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
10
+ return proto + '//' + location.host + BASE + '/sync';
11
+ }
12
+
13
+ function ensureTerm() {
14
+ var output = document.getElementById('terminalOutput');
15
+ if (!output) return false;
16
+ if (term) return true;
17
+ if (!window.Terminal || !window.FitAddon) return false;
18
+
19
+ term = new Terminal({
20
+ cursorBlink: true,
21
+ fontSize: 14,
22
+ fontFamily: 'Menlo, Monaco, "Courier New", monospace',
23
+ theme: {
24
+ background: '#0d1117',
25
+ foreground: '#e6edf3',
26
+ cursor: '#e6edf3',
27
+ selectionBackground: '#3b4455'
28
+ },
29
+ convertEol: false,
30
+ scrollback: 5000
31
+ });
32
+ fitAddon = new FitAddon.FitAddon();
33
+ term.loadAddon(fitAddon);
34
+ output.innerHTML = '';
35
+ term.open(output);
36
+ fitAddon.fit();
37
+
38
+ term.onData(function(data) {
39
+ if (ws && ws.readyState === WebSocket.OPEN) {
40
+ var encoded = btoa(unescape(encodeURIComponent(data)));
41
+ ws.send(JSON.stringify({ type: 'terminal_input', data: encoded }));
42
+ }
43
+ });
44
+
45
+ window.addEventListener('resize', function() {
46
+ if (fitAddon) try { fitAddon.fit(); } catch(_) {}
47
+ });
48
+ return true;
49
+ }
50
+
51
+ function connectAndStart() {
52
+ console.log('Terminal: Connecting to WebSocket');
53
+ if (ws && ws.readyState === WebSocket.OPEN) {
54
+ console.log('Terminal: Sending terminal_start command');
55
+ ws.send(JSON.stringify({ type: 'terminal_start', cwd: window.__STARTUP_CWD || undefined }));
56
+ return;
57
+ }
58
+ if (ws && ws.readyState === WebSocket.CONNECTING) {
59
+ console.log('Terminal: WebSocket already connecting');
60
+ return;
61
+ }
62
+
63
+ ws = new WebSocket(getWsUrl());
64
+ ws.onopen = function() {
65
+ console.log('Terminal: WebSocket connected, starting terminal');
66
+ ws.send(JSON.stringify({ type: 'terminal_start', cwd: window.__STARTUP_CWD || undefined }));
67
+ };
68
+ ws.onmessage = function(e) {
69
+ try {
70
+ var msg = JSON.parse(e.data);
71
+ if (msg.type === 'terminal_output' && term) {
72
+ var raw = msg.encoding === 'base64'
73
+ ? decodeURIComponent(escape(atob(msg.data)))
74
+ : msg.data;
75
+ term.write(raw);
76
+ } else if (msg.type === 'terminal_exit' && term) {
77
+ term.write('\r\n[Process exited with code ' + msg.code + ']\r\n');
78
+ if (termActive) setTimeout(connectAndStart, 2000);
79
+ } else if (msg.type === 'terminal_started') {
80
+ console.log('Terminal: Started successfully');
81
+ }
82
+ } catch(_) {}
83
+ };
84
+ ws.onclose = function() {
85
+ console.log('Terminal: WebSocket closed');
86
+ ws = null;
87
+ if (termActive) setTimeout(connectAndStart, 2000);
88
+ };
89
+ ws.onerror = function(error) {
90
+ console.error('Terminal: WebSocket error:', error);
91
+ };
92
+ }
93
+
94
+ function startTerminal() {
95
+ console.log('Terminal: Starting terminal module');
96
+ if (!ensureTerm()) {
97
+ console.log('Terminal: Terminal not ready, retrying');
98
+ setTimeout(startTerminal, 200);
99
+ return;
100
+ }
101
+ termActive = true;
102
+ connectAndStart();
103
+ setTimeout(function() { if (fitAddon) try { fitAddon.fit(); } catch(_) {} }, 100);
104
+ }
105
+
106
+ function stopTerminal() {
107
+ console.log('Terminal: Stopping terminal module');
108
+ termActive = false;
109
+ if (ws && ws.readyState === WebSocket.OPEN) {
110
+ ws.send(JSON.stringify({ type: 'terminal_stop' }));
111
+ }
112
+ if (ws) {
113
+ ws.close();
114
+ ws = null;
115
+ }
116
+ }
117
+
118
+ // Check if terminal view is initially active
119
+ if (document.getElementById('terminalContainer') &&
120
+ window.getComputedStyle(document.getElementById('terminalContainer')).display !== 'none') {
121
+ console.log('Terminal: Terminal view initially visible, starting');
122
+ startTerminal();
123
+ }
124
+
125
+ window.addEventListener('view-switched', function(e) {
126
+ if (e.detail.view === 'terminal') {
127
+ startTerminal();
128
+ } else if (termActive) {
129
+ stopTerminal();
130
+ }
131
+ });
132
+
104
133
  window.terminalModule = {
105
134
  start: startTerminal,
106
135
  stop: stopTerminal,