agentgui 1.0.219 → 1.0.221

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/database.js CHANGED
@@ -228,7 +228,8 @@ try {
228
228
  lastSyncedAt: 'INTEGER',
229
229
  workingDirectory: 'TEXT',
230
230
  claudeSessionId: 'TEXT',
231
- isStreaming: 'INTEGER DEFAULT 0'
231
+ isStreaming: 'INTEGER DEFAULT 0',
232
+ model: 'TEXT'
232
233
  };
233
234
 
234
235
  let addedColumns = false;
@@ -270,19 +271,20 @@ function generateId(prefix) {
270
271
 
271
272
  export const queries = {
272
273
  _db: db,
273
- createConversation(agentType, title = null, workingDirectory = null) {
274
+ createConversation(agentType, title = null, workingDirectory = null, model = null) {
274
275
  const id = generateId('conv');
275
276
  const now = Date.now();
276
277
  const stmt = prep(
277
- `INSERT INTO conversations (id, agentId, agentType, title, created_at, updated_at, status, workingDirectory) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
278
+ `INSERT INTO conversations (id, agentId, agentType, title, created_at, updated_at, status, workingDirectory, model) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
278
279
  );
279
- stmt.run(id, agentType, agentType, title, now, now, 'active', workingDirectory);
280
+ stmt.run(id, agentType, agentType, title, now, now, 'active', workingDirectory, model);
280
281
 
281
282
  return {
282
283
  id,
283
284
  agentType,
284
285
  title,
285
286
  workingDirectory,
287
+ model,
286
288
  created_at: now,
287
289
  updated_at: now,
288
290
  status: 'active'
@@ -301,7 +303,7 @@ export const queries = {
301
303
 
302
304
  getConversationsList() {
303
305
  const stmt = prep(
304
- 'SELECT id, title, agentType, created_at, updated_at, messageCount, workingDirectory, isStreaming FROM conversations WHERE status != ? ORDER BY updated_at DESC'
306
+ 'SELECT id, title, agentType, created_at, updated_at, messageCount, workingDirectory, isStreaming, model FROM conversations WHERE status != ? ORDER BY updated_at DESC'
305
307
  );
306
308
  return stmt.all('deleted');
307
309
  },
@@ -306,14 +306,16 @@ class AgentRunner {
306
306
  if (initialized && !sessionCreated) {
307
307
  sessionCreated = true;
308
308
 
309
+ const sessionParams = {
310
+ cwd: cwd,
311
+ mcpServers: []
312
+ };
313
+ if (config.model) sessionParams.model = config.model;
309
314
  const sessionRequest = {
310
315
  jsonrpc: '2.0',
311
316
  id: ++requestId,
312
317
  method: 'session/new',
313
- params: {
314
- cwd: cwd,
315
- mcpServers: []
316
- }
318
+ params: sessionParams
317
319
  };
318
320
  proc.stdin.write(JSON.stringify(sessionRequest) + '\n');
319
321
  } else if (!initialized) {
@@ -482,7 +484,8 @@ registry.register({
482
484
  outputFormat = 'stream-json',
483
485
  print = true,
484
486
  resumeSessionId = null,
485
- systemPrompt = null
487
+ systemPrompt = null,
488
+ model = null
486
489
  } = config;
487
490
 
488
491
  const flags = [];
@@ -490,6 +493,7 @@ registry.register({
490
493
  if (verbose) flags.push('--verbose');
491
494
  flags.push(`--output-format=${outputFormat}`);
492
495
  flags.push('--dangerously-skip-permissions');
496
+ if (model) flags.push('--model', model);
493
497
  if (resumeSessionId) flags.push('--resume', resumeSessionId);
494
498
  if (systemPrompt) flags.push('--append-system-prompt', systemPrompt);
495
499
 
@@ -735,7 +739,11 @@ registry.register({
735
739
  protocol: 'acp',
736
740
  supportsStdin: false,
737
741
  supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
738
- buildArgs: () => ['--experimental-acp', '--yolo'],
742
+ buildArgs(prompt, config) {
743
+ const args = ['--experimental-acp', '--yolo'];
744
+ if (config?.model) args.push('--model', config.model);
745
+ return args;
746
+ },
739
747
  protocolHandler: acpProtocolHandler
740
748
  });
741
749
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.219",
3
+ "version": "1.0.221",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -202,6 +202,44 @@ function discoverAgents() {
202
202
 
203
203
  const discoveredAgents = discoverAgents();
204
204
 
205
+ const modelCache = new Map();
206
+
207
+ async function getModelsForAgent(agentId) {
208
+ if (agentId === 'claude-code') {
209
+ return [
210
+ { id: '', label: 'Default' },
211
+ { id: 'sonnet', label: 'Sonnet' },
212
+ { id: 'opus', label: 'Opus' },
213
+ { id: 'haiku', label: 'Haiku' },
214
+ { id: 'claude-sonnet-4-5-20250929', label: 'Sonnet 4.5' },
215
+ { id: 'claude-opus-4-6', label: 'Opus 4.6' },
216
+ { id: 'claude-haiku-4-5-20251001', label: 'Haiku 4.5' }
217
+ ];
218
+ }
219
+ if (agentId === 'opencode') {
220
+ try {
221
+ const result = execSync('opencode models 2>/dev/null', { encoding: 'utf-8', timeout: 10000 });
222
+ const lines = result.split('\n').map(l => l.trim()).filter(Boolean);
223
+ const models = [{ id: '', label: 'Default' }];
224
+ for (const line of lines) {
225
+ models.push({ id: line, label: line });
226
+ }
227
+ return models;
228
+ } catch (_) {
229
+ return [{ id: '', label: 'Default' }];
230
+ }
231
+ }
232
+ if (agentId === 'gemini') {
233
+ return [
234
+ { id: '', label: 'Default' },
235
+ { id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
236
+ { id: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
237
+ { id: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' }
238
+ ];
239
+ }
240
+ return [];
241
+ }
242
+
205
243
  const GEMINI_SCOPES = [
206
244
  'https://www.googleapis.com/auth/cloud-platform',
207
245
  'https://www.googleapis.com/auth/userinfo.email',
@@ -722,8 +760,8 @@ const server = http.createServer(async (req, res) => {
722
760
 
723
761
  if (pathOnly === '/api/conversations' && req.method === 'POST') {
724
762
  const body = await parseBody(req);
725
- const conversation = queries.createConversation(body.agentId, body.title, body.workingDirectory || null);
726
- queries.createEvent('conversation.created', { agentId: body.agentId, workingDirectory: conversation.workingDirectory }, conversation.id);
763
+ const conversation = queries.createConversation(body.agentId, body.title, body.workingDirectory || null, body.model || null);
764
+ queries.createEvent('conversation.created', { agentId: body.agentId, workingDirectory: conversation.workingDirectory, model: conversation.model }, conversation.id);
727
765
  broadcastSync({ type: 'conversation_created', conversation });
728
766
  sendJSON(req, res, 201, { conversation });
729
767
  return;
@@ -782,6 +820,7 @@ const server = http.createServer(async (req, res) => {
782
820
  if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
783
821
  const body = await parseBody(req);
784
822
  const agentId = body.agentId || conv.agentType || conv.agentId || 'claude-code';
823
+ const model = body.model || conv.model || null;
785
824
  const idempotencyKey = body.idempotencyKey || null;
786
825
  const message = queries.createMessage(conversationId, 'user', body.content, idempotencyKey);
787
826
  queries.createEvent('message.created', { role: 'user', messageId: message.id }, conversationId);
@@ -789,7 +828,7 @@ const server = http.createServer(async (req, res) => {
789
828
 
790
829
  if (activeExecutions.has(conversationId)) {
791
830
  if (!messageQueues.has(conversationId)) messageQueues.set(conversationId, []);
792
- messageQueues.get(conversationId).push({ content: body.content, agentId, messageId: message.id });
831
+ messageQueues.get(conversationId).push({ content: body.content, agentId, model, messageId: message.id });
793
832
  const queueLength = messageQueues.get(conversationId).length;
794
833
  broadcastSync({ type: 'queue_status', conversationId, queueLength, messageId: message.id, timestamp: Date.now() });
795
834
  sendJSON(req, res, 200, { message, queued: true, queuePosition: queueLength, idempotencyKey });
@@ -813,7 +852,7 @@ const server = http.createServer(async (req, res) => {
813
852
 
814
853
  sendJSON(req, res, 201, { message, session, idempotencyKey });
815
854
 
816
- processMessageWithStreaming(conversationId, message.id, session.id, body.content, agentId)
855
+ processMessageWithStreaming(conversationId, message.id, session.id, body.content, agentId, model)
817
856
  .catch(err => {
818
857
  console.error(`[messages] Uncaught error for conv ${conversationId}:`, err.message);
819
858
  debugLog(`[messages] Uncaught error: ${err.message}`);
@@ -831,6 +870,7 @@ const server = http.createServer(async (req, res) => {
831
870
 
832
871
  const prompt = body.content || body.message || '';
833
872
  const agentId = body.agentId || conv.agentType || conv.agentId || 'claude-code';
873
+ const model = body.model || conv.model || null;
834
874
 
835
875
  const userMessage = queries.createMessage(conversationId, 'user', prompt);
836
876
  queries.createEvent('message.created', { role: 'user', messageId: userMessage.id }, conversationId);
@@ -840,7 +880,7 @@ const server = http.createServer(async (req, res) => {
840
880
  if (activeExecutions.has(conversationId)) {
841
881
  debugLog(`[stream] Conversation ${conversationId} is busy, queuing message`);
842
882
  if (!messageQueues.has(conversationId)) messageQueues.set(conversationId, []);
843
- messageQueues.get(conversationId).push({ content: prompt, agentId, messageId: userMessage.id });
883
+ messageQueues.get(conversationId).push({ content: prompt, agentId, model, messageId: userMessage.id });
844
884
 
845
885
  const queueLength = messageQueues.get(conversationId).length;
846
886
  broadcastSync({ type: 'queue_status', conversationId, queueLength, messageId: userMessage.id, timestamp: Date.now() });
@@ -866,7 +906,7 @@ const server = http.createServer(async (req, res) => {
866
906
 
867
907
  sendJSON(req, res, 200, { message: userMessage, session, streamId: session.id });
868
908
 
869
- processMessageWithStreaming(conversationId, userMessage.id, session.id, prompt, agentId)
909
+ processMessageWithStreaming(conversationId, userMessage.id, session.id, prompt, agentId, model)
870
910
  .catch(err => debugLog(`[stream] Uncaught error: ${err.message}`));
871
911
  return;
872
912
  }
@@ -1127,6 +1167,24 @@ const server = http.createServer(async (req, res) => {
1127
1167
  return;
1128
1168
  }
1129
1169
 
1170
+ const modelsMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/models$/);
1171
+ if (modelsMatch && req.method === 'GET') {
1172
+ const agentId = modelsMatch[1];
1173
+ const cached = modelCache.get(agentId);
1174
+ if (cached && (Date.now() - cached.ts) < 300000) {
1175
+ sendJSON(req, res, 200, { models: cached.models });
1176
+ return;
1177
+ }
1178
+ try {
1179
+ const models = await getModelsForAgent(agentId);
1180
+ modelCache.set(agentId, { models, ts: Date.now() });
1181
+ sendJSON(req, res, 200, { models });
1182
+ } catch (err) {
1183
+ sendJSON(req, res, 200, { models: [] });
1184
+ }
1185
+ return;
1186
+ }
1187
+
1130
1188
  if (pathOnly === '/api/agents/auth-status' && req.method === 'GET') {
1131
1189
  const statuses = discoveredAgents.map(agent => {
1132
1190
  const status = { id: agent.id, name: agent.name, authenticated: false, detail: '' };
@@ -1438,11 +1496,6 @@ const server = http.createServer(async (req, res) => {
1438
1496
  }
1439
1497
 
1440
1498
  const speech = await getSpeech();
1441
- const status = speech.getStatus();
1442
- if (status.ttsError) {
1443
- sendJSON(req, res, 503, { error: status.ttsError, retryable: false });
1444
- return;
1445
- }
1446
1499
  const wavBuffer = await speech.synthesize(text, voiceId);
1447
1500
  res.writeHead(200, { 'Content-Type': 'audio/wav', 'Content-Length': wavBuffer.length });
1448
1501
  res.end(wavBuffer);
@@ -1465,11 +1518,6 @@ const server = http.createServer(async (req, res) => {
1465
1518
  return;
1466
1519
  }
1467
1520
  const speech = await getSpeech();
1468
- const status = speech.getStatus();
1469
- if (status.ttsError) {
1470
- sendJSON(req, res, 503, { error: status.ttsError, retryable: false });
1471
- return;
1472
- }
1473
1521
  res.writeHead(200, {
1474
1522
  'Content-Type': 'application/octet-stream',
1475
1523
  'Transfer-Encoding': 'chunked',
@@ -1717,7 +1765,7 @@ function createChunkBatcher() {
1717
1765
  return { add, drain };
1718
1766
  }
1719
1767
 
1720
- async function processMessageWithStreaming(conversationId, messageId, sessionId, content, agentId) {
1768
+ async function processMessageWithStreaming(conversationId, messageId, sessionId, content, agentId, model) {
1721
1769
  const startTime = Date.now();
1722
1770
 
1723
1771
  const conv = queries.getConversation(conversationId);
@@ -1854,6 +1902,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
1854
1902
  }
1855
1903
  };
1856
1904
 
1905
+ const resolvedModel = model || conv?.model || null;
1857
1906
  const config = {
1858
1907
  verbose: true,
1859
1908
  outputFormat: 'stream-json',
@@ -1861,6 +1910,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
1861
1910
  print: true,
1862
1911
  resumeSessionId,
1863
1912
  systemPrompt: SYSTEM_PROMPT,
1913
+ model: resolvedModel || undefined,
1864
1914
  onEvent,
1865
1915
  onPid: (pid) => {
1866
1916
  const entry = activeExecutions.get(conversationId);
@@ -1962,7 +2012,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
1962
2012
  conversationId,
1963
2013
  timestamp: Date.now()
1964
2014
  });
1965
- scheduleRetry(conversationId, messageId, content, agentId);
2015
+ scheduleRetry(conversationId, messageId, content, agentId, model);
1966
2016
  }, cooldownMs);
1967
2017
  return;
1968
2018
  }
@@ -1993,16 +2043,16 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
1993
2043
  }
1994
2044
  }
1995
2045
 
1996
- function scheduleRetry(conversationId, messageId, content, agentId) {
2046
+ function scheduleRetry(conversationId, messageId, content, agentId, model) {
1997
2047
  debugLog(`[rate-limit] scheduleRetry called for conv ${conversationId}, messageId=${messageId}`);
1998
-
2048
+
1999
2049
  if (!content) {
2000
2050
  const conv = queries.getConversation(conversationId);
2001
2051
  const lastMsg = queries.getLastUserMessage(conversationId);
2002
2052
  content = lastMsg?.content || 'continue';
2003
2053
  debugLog(`[rate-limit] Recovered content from last message: ${content?.substring?.(0, 50)}...`);
2004
2054
  }
2005
-
2055
+
2006
2056
  const newSession = queries.createSession(conversationId);
2007
2057
  queries.createEvent('session.created', { messageId, sessionId: newSession.id, retryReason: 'rate_limit' }, conversationId, newSession.id);
2008
2058
 
@@ -2017,7 +2067,7 @@ function scheduleRetry(conversationId, messageId, content, agentId) {
2017
2067
  });
2018
2068
 
2019
2069
  debugLog(`[rate-limit] Calling processMessageWithStreaming for retry`);
2020
- processMessageWithStreaming(conversationId, messageId, newSession.id, content, agentId)
2070
+ processMessageWithStreaming(conversationId, messageId, newSession.id, content, agentId, model)
2021
2071
  .catch(err => {
2022
2072
  debugLog(`[rate-limit] Retry failed: ${err.message}`);
2023
2073
  console.error(`[rate-limit] Retry error for conv ${conversationId}:`, err);
@@ -2052,7 +2102,7 @@ function drainMessageQueue(conversationId) {
2052
2102
  timestamp: Date.now()
2053
2103
  });
2054
2104
 
2055
- processMessageWithStreaming(conversationId, next.messageId, session.id, next.content, next.agentId)
2105
+ processMessageWithStreaming(conversationId, next.messageId, session.id, next.content, next.agentId, next.model)
2056
2106
  .catch(err => debugLog(`[queue] Error processing queued message: ${err.message}`));
2057
2107
  }
2058
2108
 
@@ -2366,7 +2416,7 @@ async function resumeInterruptedStreams() {
2366
2416
  const messageId = lastMsg?.id || null;
2367
2417
  console.log(`[RESUME] Resuming conv ${conv.id} (claude session: ${conv.claudeSessionId})`);
2368
2418
 
2369
- processMessageWithStreaming(conv.id, messageId, session.id, promptText, conv.agentType)
2419
+ processMessageWithStreaming(conv.id, messageId, session.id, promptText, conv.agentType, conv.model)
2370
2420
  .catch(err => debugLog(`[RESUME] Error resuming conv ${conv.id}: ${err.message}`));
2371
2421
 
2372
2422
  if (i < toResume.length - 1) {
package/static/index.html CHANGED
@@ -816,6 +816,28 @@
816
816
  flex-shrink: 0;
817
817
  }
818
818
 
819
+ .agent-selector:disabled, .model-selector:disabled {
820
+ opacity: 0.5;
821
+ cursor: not-allowed;
822
+ background-color: var(--color-bg-secondary);
823
+ }
824
+
825
+ .model-selector {
826
+ padding: 0.5rem;
827
+ border: none;
828
+ border-radius: 0.375rem;
829
+ background-color: var(--color-bg-secondary);
830
+ color: var(--color-text-primary);
831
+ font-size: 0.8rem;
832
+ cursor: pointer;
833
+ flex-shrink: 0;
834
+ max-width: 160px;
835
+ }
836
+
837
+ .model-selector:empty, .model-selector[data-empty="true"] {
838
+ display: none;
839
+ }
840
+
819
841
  .message-textarea {
820
842
  flex: 1;
821
843
  padding: 0.625rem 0.875rem;
@@ -1005,6 +1027,7 @@
1005
1027
  .messages-wrapper { padding: 0.375rem 0.5rem; }
1006
1028
  .input-section { padding: 0.5rem; padding-bottom: calc(0.5rem + env(safe-area-inset-bottom)); }
1007
1029
  .agent-selector { display: none; }
1030
+ .model-selector { display: none; }
1008
1031
  }
1009
1032
 
1010
1033
  /* ===== SCROLLBAR STYLING ===== */
@@ -2456,6 +2479,7 @@
2456
2479
  <div class="input-section">
2457
2480
  <div class="input-wrapper">
2458
2481
  <select class="agent-selector" data-agent-selector title="Select agent"></select>
2482
+ <select class="model-selector" data-model-selector title="Select model" data-empty="true"></select>
2459
2483
  <textarea
2460
2484
  class="message-textarea"
2461
2485
  data-message-input
@@ -42,9 +42,13 @@ class AgentGUIClient {
42
42
  statusIndicator: null,
43
43
  messageInput: null,
44
44
  sendButton: null,
45
- agentSelector: null
45
+ agentSelector: null,
46
+ modelSelector: null
46
47
  };
47
48
 
49
+ this._agentLocked = false;
50
+ this._modelCache = new Map();
51
+
48
52
  this.chunkPollState = {
49
53
  isPolling: false,
50
54
  lastFetchTimestamp: 0,
@@ -322,6 +326,15 @@ class AgentGUIClient {
322
326
  this.ui.messageInput = document.querySelector('[data-message-input]');
323
327
  this.ui.sendButton = document.querySelector('[data-send-button]');
324
328
  this.ui.agentSelector = document.querySelector('[data-agent-selector]');
329
+ this.ui.modelSelector = document.querySelector('[data-model-selector]');
330
+
331
+ if (this.ui.agentSelector) {
332
+ this.ui.agentSelector.addEventListener('change', () => {
333
+ if (!this._agentLocked) {
334
+ this.loadModelsForAgent(this.ui.agentSelector.value);
335
+ }
336
+ });
337
+ }
325
338
 
326
339
  // Setup event listeners
327
340
  if (this.ui.sendButton) {
@@ -352,10 +365,15 @@ class AgentGUIClient {
352
365
  this.setupScrollTracking();
353
366
 
354
367
  window.addEventListener('create-new-conversation', (event) => {
368
+ this.unlockAgentAndModel();
355
369
  const detail = event.detail || {};
356
370
  this.createNewConversation(detail.workingDirectory, detail.title);
357
371
  });
358
372
 
373
+ window.addEventListener('preparing-new-conversation', () => {
374
+ this.unlockAgentAndModel();
375
+ });
376
+
359
377
  // Listen for conversation selection
360
378
  window.addEventListener('conversation-selected', (event) => {
361
379
  const conversationId = event.detail.conversationId;
@@ -479,7 +497,7 @@ class AgentGUIClient {
479
497
  outputEl.innerHTML = `
480
498
  <div class="conversation-header">
481
499
  <h2>${this.escapeHtml(conv?.title || 'Conversation')}</h2>
482
- <p class="text-secondary">${conv?.agentType || 'unknown'} - ${new Date(conv?.created_at || Date.now()).toLocaleDateString()}${wdInfo}</p>
500
+ <p class="text-secondary">${conv?.agentType || 'unknown'}${conv?.model ? ' (' + this.escapeHtml(conv.model) + ')' : ''} - ${new Date(conv?.created_at || Date.now()).toLocaleDateString()}${wdInfo}</p>
483
501
  </div>
484
502
  <div class="conversation-messages"></div>
485
503
  `;
@@ -1098,7 +1116,9 @@ class AgentGUIClient {
1098
1116
 
1099
1117
  async startExecution() {
1100
1118
  const prompt = this.ui.messageInput?.value || '';
1101
- const agentId = this.ui.agentSelector?.value || 'claude-code';
1119
+ const conv = this.state.currentConversation;
1120
+ const agentId = conv?.agentType || this.ui.agentSelector?.value || 'claude-code';
1121
+ const model = this.ui.modelSelector?.value || null;
1102
1122
 
1103
1123
  if (!prompt.trim()) {
1104
1124
  this.showError('Please enter a prompt');
@@ -1116,24 +1136,27 @@ class AgentGUIClient {
1116
1136
  this.disableControls();
1117
1137
 
1118
1138
  try {
1119
- if (this.state.currentConversation?.id) {
1120
- await this.streamToConversation(this.state.currentConversation.id, savedPrompt, agentId);
1139
+ if (conv?.id) {
1140
+ await this.streamToConversation(conv.id, savedPrompt, agentId, model);
1121
1141
  this._confirmOptimisticMessage(pendingId);
1122
1142
  } else {
1143
+ const body = { agentId, title: savedPrompt.substring(0, 50) };
1144
+ if (model) body.model = model;
1123
1145
  const response = await fetch(window.__BASE_URL + '/api/conversations', {
1124
1146
  method: 'POST',
1125
1147
  headers: { 'Content-Type': 'application/json' },
1126
- body: JSON.stringify({ agentId, title: savedPrompt.substring(0, 50) })
1148
+ body: JSON.stringify(body)
1127
1149
  });
1128
1150
  const { conversation } = await response.json();
1129
1151
  this.state.currentConversation = conversation;
1152
+ this.lockAgentAndModel(agentId, model);
1130
1153
 
1131
1154
  if (window.conversationManager) {
1132
1155
  window.conversationManager.loadConversations();
1133
1156
  window.conversationManager.select(conversation.id);
1134
1157
  }
1135
1158
 
1136
- await this.streamToConversation(conversation.id, savedPrompt, agentId);
1159
+ await this.streamToConversation(conversation.id, savedPrompt, agentId, model);
1137
1160
  this._confirmOptimisticMessage(pendingId);
1138
1161
  }
1139
1162
  } catch (error) {
@@ -1361,7 +1384,7 @@ class AgentGUIClient {
1361
1384
  outputEl.innerHTML = `
1362
1385
  <div class="conversation-header">
1363
1386
  <h2>${this.escapeHtml(title)}</h2>
1364
- <p class="text-secondary">${conv?.agentType || 'unknown'} - ${conv ? new Date(conv.created_at).toLocaleDateString() : ''}${wdInfo}</p>
1387
+ <p class="text-secondary">${conv?.agentType || 'unknown'}${conv?.model ? ' (' + this.escapeHtml(conv.model) + ')' : ''} - ${conv ? new Date(conv.created_at).toLocaleDateString() : ''}${wdInfo}</p>
1365
1388
  </div>
1366
1389
  <div class="conversation-messages">
1367
1390
  <div class="skeleton-loading">
@@ -1374,29 +1397,33 @@ class AgentGUIClient {
1374
1397
  `;
1375
1398
  }
1376
1399
 
1377
- async streamToConversation(conversationId, prompt, agentId) {
1400
+ async streamToConversation(conversationId, prompt, agentId, model) {
1378
1401
  try {
1379
1402
  if (this.wsManager.isConnected) {
1380
1403
  this.wsManager.sendMessage({ type: 'subscribe', conversationId });
1381
1404
  }
1382
1405
 
1406
+ const streamBody = { content: prompt, agentId };
1407
+ if (model) streamBody.model = model;
1383
1408
  const response = await fetch(`${window.__BASE_URL}/api/conversations/${conversationId}/stream`, {
1384
1409
  method: 'POST',
1385
1410
  headers: { 'Content-Type': 'application/json' },
1386
- body: JSON.stringify({ content: prompt, agentId })
1411
+ body: JSON.stringify(streamBody)
1387
1412
  });
1388
1413
 
1389
1414
  if (response.status === 404) {
1390
1415
  console.warn('Conversation not found, recreating:', conversationId);
1391
1416
  const conv = this.state.currentConversation;
1417
+ const createBody = {
1418
+ agentId,
1419
+ title: conv?.title || prompt.substring(0, 50),
1420
+ workingDirectory: conv?.workingDirectory || null
1421
+ };
1422
+ if (model) createBody.model = model;
1392
1423
  const createResp = await fetch(window.__BASE_URL + '/api/conversations', {
1393
1424
  method: 'POST',
1394
1425
  headers: { 'Content-Type': 'application/json' },
1395
- body: JSON.stringify({
1396
- agentId,
1397
- title: conv?.title || prompt.substring(0, 50),
1398
- workingDirectory: conv?.workingDirectory || null
1399
- })
1426
+ body: JSON.stringify(createBody)
1400
1427
  });
1401
1428
  if (!createResp.ok) throw new Error(`Failed to recreate conversation: HTTP ${createResp.status}`);
1402
1429
  const { conversation: newConv } = await createResp.json();
@@ -1406,7 +1433,7 @@ class AgentGUIClient {
1406
1433
  window.conversationManager.select(newConv.id);
1407
1434
  }
1408
1435
  this.updateUrlForConversation(newConv.id);
1409
- return this.streamToConversation(newConv.id, prompt, agentId);
1436
+ return this.streamToConversation(newConv.id, prompt, agentId, model);
1410
1437
  }
1411
1438
 
1412
1439
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
@@ -1698,6 +1725,9 @@ class AgentGUIClient {
1698
1725
  }
1699
1726
 
1700
1727
  window.dispatchEvent(new CustomEvent('agents-loaded', { detail: { agents } }));
1728
+ if (agents.length > 0 && !this._agentLocked) {
1729
+ this.loadModelsForAgent(agents[0].id);
1730
+ }
1701
1731
  return agents;
1702
1732
  } catch (error) {
1703
1733
  console.error('Failed to load agents:', error);
@@ -1706,6 +1736,61 @@ class AgentGUIClient {
1706
1736
  });
1707
1737
  }
1708
1738
 
1739
+ async loadModelsForAgent(agentId) {
1740
+ if (!agentId || !this.ui.modelSelector) return;
1741
+ const cached = this._modelCache.get(agentId);
1742
+ if (cached) {
1743
+ this._populateModelSelector(cached);
1744
+ return;
1745
+ }
1746
+ try {
1747
+ const response = await fetch(window.__BASE_URL + `/api/agents/${agentId}/models`);
1748
+ const { models } = await response.json();
1749
+ this._modelCache.set(agentId, models || []);
1750
+ this._populateModelSelector(models || []);
1751
+ } catch (error) {
1752
+ console.error('Failed to load models:', error);
1753
+ this._populateModelSelector([]);
1754
+ }
1755
+ }
1756
+
1757
+ _populateModelSelector(models) {
1758
+ if (!this.ui.modelSelector) return;
1759
+ if (!models || models.length === 0) {
1760
+ this.ui.modelSelector.innerHTML = '';
1761
+ this.ui.modelSelector.setAttribute('data-empty', 'true');
1762
+ return;
1763
+ }
1764
+ this.ui.modelSelector.removeAttribute('data-empty');
1765
+ this.ui.modelSelector.innerHTML = models
1766
+ .map(m => `<option value="${m.id}">${this.escapeHtml(m.label)}</option>`)
1767
+ .join('');
1768
+ }
1769
+
1770
+ lockAgentAndModel(agentId, model) {
1771
+ this._agentLocked = true;
1772
+ if (this.ui.agentSelector) {
1773
+ this.ui.agentSelector.value = agentId;
1774
+ this.ui.agentSelector.disabled = true;
1775
+ }
1776
+ this.loadModelsForAgent(agentId).then(() => {
1777
+ if (this.ui.modelSelector) {
1778
+ if (model) this.ui.modelSelector.value = model;
1779
+ this.ui.modelSelector.disabled = true;
1780
+ }
1781
+ });
1782
+ }
1783
+
1784
+ unlockAgentAndModel() {
1785
+ this._agentLocked = false;
1786
+ if (this.ui.agentSelector) {
1787
+ this.ui.agentSelector.disabled = false;
1788
+ }
1789
+ if (this.ui.modelSelector) {
1790
+ this.ui.modelSelector.disabled = false;
1791
+ }
1792
+ }
1793
+
1709
1794
  /**
1710
1795
  * Load conversations
1711
1796
  */
@@ -1840,9 +1925,11 @@ class AgentGUIClient {
1840
1925
  async createNewConversation(workingDirectory, title) {
1841
1926
  try {
1842
1927
  const agentId = this.ui.agentSelector?.value || 'claude-code';
1928
+ const model = this.ui.modelSelector?.value || null;
1843
1929
  const convTitle = title || 'New Conversation';
1844
1930
  const body = { agentId, title: convTitle };
1845
1931
  if (workingDirectory) body.workingDirectory = workingDirectory;
1932
+ if (model) body.model = model;
1846
1933
 
1847
1934
  const response = await fetch(window.__BASE_URL + '/api/conversations', {
1848
1935
  method: 'POST',
@@ -1855,6 +1942,7 @@ class AgentGUIClient {
1855
1942
  }
1856
1943
 
1857
1944
  const { conversation } = await response.json();
1945
+ this.lockAgentAndModel(agentId, model);
1858
1946
 
1859
1947
  await this.loadConversations();
1860
1948
 
@@ -1930,6 +2018,7 @@ class AgentGUIClient {
1930
2018
  outputEl.appendChild(cached.dom.firstChild);
1931
2019
  }
1932
2020
  this.state.currentConversation = cached.conversation;
2021
+ this.lockAgentAndModel(cached.conversation.agentType || 'claude-code', cached.conversation.model || null);
1933
2022
  this.conversationCache.delete(conversationId);
1934
2023
  this.restoreScrollPosition(conversationId);
1935
2024
  this.enableControls();
@@ -1957,6 +2046,7 @@ class AgentGUIClient {
1957
2046
  const { conversation, isActivelyStreaming, latestSession, chunks: rawChunks, totalChunks, messages: allMessages } = await resp.json();
1958
2047
 
1959
2048
  this.state.currentConversation = conversation;
2049
+ this.lockAgentAndModel(conversation.agentType || 'claude-code', conversation.model || null);
1960
2050
 
1961
2051
  const chunks = (rawChunks || []).map(chunk => ({
1962
2052
  ...chunk,
@@ -1975,7 +2065,7 @@ class AgentGUIClient {
1975
2065
  outputEl.innerHTML = `
1976
2066
  <div class="conversation-header">
1977
2067
  <h2>${this.escapeHtml(conversation.title || 'Conversation')}</h2>
1978
- <p class="text-secondary">${conversation.agentType || 'unknown'} - ${new Date(conversation.created_at).toLocaleDateString()}${wdInfo}</p>
2068
+ <p class="text-secondary">${conversation.agentType || 'unknown'}${conversation.model ? ' (' + this.escapeHtml(conversation.model) + ')' : ''} - ${new Date(conversation.created_at).toLocaleDateString()}${wdInfo}</p>
1979
2069
  </div>
1980
2070
  <div class="conversation-messages"></div>
1981
2071
  `;
@@ -93,6 +93,7 @@ class ConversationManager {
93
93
  }
94
94
 
95
95
  async openFolderBrowser() {
96
+ window.dispatchEvent(new CustomEvent('preparing-new-conversation'));
96
97
  if (!this.folderBrowser.modal) {
97
98
  this.createNew();
98
99
  return;
@@ -375,8 +376,9 @@ class ConversationManager {
375
376
  const title = conv.title || `Conversation ${conv.id.slice(0, 8)}`;
376
377
  const timestamp = conv.created_at ? new Date(conv.created_at).toLocaleDateString() : 'Unknown';
377
378
  const agent = conv.agentType || 'unknown';
379
+ const modelLabel = conv.model ? ` (${conv.model})` : '';
378
380
  const wd = conv.workingDirectory ? conv.workingDirectory.split('/').pop() : '';
379
- const metaParts = [agent, timestamp];
381
+ const metaParts = [agent + modelLabel, timestamp];
380
382
  if (wd) metaParts.push(wd);
381
383
 
382
384
  const titleEl = el.querySelector('.conversation-item-title');
@@ -402,8 +404,9 @@ class ConversationManager {
402
404
  const title = conv.title || `Conversation ${conv.id.slice(0, 8)}`;
403
405
  const timestamp = conv.created_at ? new Date(conv.created_at).toLocaleDateString() : 'Unknown';
404
406
  const agent = conv.agentType || 'unknown';
407
+ const modelLabel = conv.model ? ` (${conv.model})` : '';
405
408
  const wd = conv.workingDirectory ? conv.workingDirectory.split('/').pop() : '';
406
- const metaParts = [agent, timestamp];
409
+ const metaParts = [agent + modelLabel, timestamp];
407
410
  if (wd) metaParts.push(wd);
408
411
 
409
412
  const streamingBadge = isStreaming
@@ -466,6 +469,7 @@ class ConversationManager {
466
469
  }
467
470
 
468
471
  createNew() {
472
+ window.dispatchEvent(new CustomEvent('preparing-new-conversation'));
469
473
  window.dispatchEvent(new CustomEvent('create-new-conversation'));
470
474
  }
471
475