agentgui 1.0.31 → 1.0.32

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.
Files changed (3) hide show
  1. package/database.js +47 -23
  2. package/package.json +1 -1
  3. package/static/app.js +293 -94
package/database.js CHANGED
@@ -245,16 +245,34 @@ export const queries = {
245
245
  },
246
246
 
247
247
  getMessage(id) {
248
- const stmt = db.prepare('SELECT * FROM messages WHERE id = ?');
249
- return stmt.get(id);
250
- },
251
-
252
- getConversationMessages(conversationId) {
253
- const stmt = db.prepare(
254
- 'SELECT * FROM messages WHERE conversationId = ? ORDER BY created_at ASC'
255
- );
256
- return stmt.all(conversationId);
257
- },
248
+ const stmt = db.prepare('SELECT * FROM messages WHERE id = ?');
249
+ const msg = stmt.get(id);
250
+ if (msg && typeof msg.content === 'string') {
251
+ try {
252
+ msg.content = JSON.parse(msg.content);
253
+ } catch (_) {
254
+ // If it's not JSON, leave it as string
255
+ }
256
+ }
257
+ return msg;
258
+ },
259
+
260
+ getConversationMessages(conversationId) {
261
+ const stmt = db.prepare(
262
+ 'SELECT * FROM messages WHERE conversationId = ? ORDER BY created_at ASC'
263
+ );
264
+ const messages = stmt.all(conversationId);
265
+ return messages.map(msg => {
266
+ if (typeof msg.content === 'string') {
267
+ try {
268
+ msg.content = JSON.parse(msg.content);
269
+ } catch (_) {
270
+ // If it's not JSON, leave it as string
271
+ }
272
+ }
273
+ return msg;
274
+ });
275
+ },
258
276
 
259
277
  createSession(conversationId) {
260
278
  const id = generateId('sess');
@@ -499,19 +517,25 @@ export const queries = {
499
517
  if (content && !content.startsWith('[{"tool_use_id"')) {
500
518
  messages.push({ id: obj.uuid || generateId('msg'), role: 'user', content, created_at: new Date(obj.timestamp).getTime() });
501
519
  }
502
- } else if (obj.type === 'assistant' && obj.message?.content) {
503
- let text = '';
504
- const content = obj.message.content;
505
- if (Array.isArray(content)) {
506
- for (const c of content) {
507
- if (c.type === 'text' && c.text) text += c.text;
508
- }
509
- } else if (typeof content === 'string') {
510
- text = content;
511
- }
512
- if (text) {
513
- messages.push({ id: obj.uuid || generateId('msg'), role: 'assistant', content: text, created_at: new Date(obj.timestamp).getTime() });
514
- }
520
+ } else if (obj.type === 'assistant' && obj.message?.content) {
521
+ let text = '';
522
+ const content = obj.message.content;
523
+ if (Array.isArray(content)) {
524
+ // CRITICAL FIX: Join text blocks with newlines to preserve separation
525
+ const textBlocks = [];
526
+ for (const c of content) {
527
+ if (c.type === 'text' && c.text) {
528
+ textBlocks.push(c.text);
529
+ }
530
+ }
531
+ // Join with double newline to preserve logical separation
532
+ text = textBlocks.join('\n\n');
533
+ } else if (typeof content === 'string') {
534
+ text = content;
535
+ }
536
+ if (text) {
537
+ messages.push({ id: obj.uuid || generateId('msg'), role: 'assistant', content: text, created_at: new Date(obj.timestamp).getTime() });
538
+ }
515
539
  }
516
540
  } catch (_) {}
517
541
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.31",
3
+ "version": "1.0.32",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/static/app.js CHANGED
@@ -96,10 +96,15 @@ class GMGUIApp {
96
96
  console.error('[CRITICAL] GMGUIApp.init() failed:', err);
97
97
  console.error('[CRITICAL] Stack:', err.stack);
98
98
  throw err;
99
- });
100
- }
99
+ });
100
+ }
101
+
102
+ // Helper for authenticated API calls - ensures credentials sent for proxy auth
103
+ async apiFetch(url, options = {}) {
104
+ return fetch(url, { credentials: 'include', ...options });
105
+ }
101
106
 
102
- async init() {
107
+ async init() {
103
108
  console.log('[DEBUG] Init: Starting initialization');
104
109
  console.log('[DEBUG] Init: BASE_URL =', BASE_URL);
105
110
  console.log('[DEBUG] Init: Window width:', window.innerWidth);
@@ -155,7 +160,7 @@ class GMGUIApp {
155
160
  async verifyConsistency() {
156
161
  // Silent consistency check - only log if mismatch found
157
162
  try {
158
- const res = await fetch(BASE_URL + '/api/conversations');
163
+ const res = await this.apiFetch(BASE_URL + '/api/conversations');
159
164
  if (!res.ok) return;
160
165
 
161
166
  const data = await res.json();
@@ -176,7 +181,7 @@ class GMGUIApp {
176
181
 
177
182
  async autoImportClaudeCode() {
178
183
  try {
179
- await fetch(BASE_URL + '/api/import/claude-code');
184
+ await this.apiFetch(BASE_URL + '/api/import/claude-code');
180
185
  } catch (e) {
181
186
  console.error('autoImportClaudeCode:', e);
182
187
  }
@@ -356,7 +361,7 @@ class GMGUIApp {
356
361
 
357
362
  async fetchHome() {
358
363
  try {
359
- const res = await fetch(BASE_URL + '/api/home');
364
+ const res = await this.apiFetch(BASE_URL + '/api/home');
360
365
  if (res.ok) {
361
366
  const data = await res.json();
362
367
  localStorage.setItem('gmgui-home', data.home);
@@ -391,35 +396,129 @@ class GMGUIApp {
391
396
  return p.startsWith('~') ? p.replace('~', home) : p;
392
397
  }
393
398
 
394
- setupEventListeners() {
395
- window.addEventListener('focus', () => {
396
- this.autoImportClaudeCode().then(() => {
397
- this.fetchConversations().then(() => this.renderChatHistory());
398
- });
399
- });
400
- const input = document.getElementById('messageInput');
401
- if (input) {
402
- input.addEventListener('keydown', (e) => {
403
- if (e.key === 'Enter' && !e.shiftKey) {
404
- e.preventDefault();
405
- this.sendMessage();
399
+ setupEventListeners() {
400
+ window.addEventListener('focus', () => {
401
+ this.autoImportClaudeCode().then(() => {
402
+ this.fetchConversations().then(() => this.renderChatHistory());
403
+ });
404
+ });
405
+
406
+ // THEME CHANGE LISTENER: Update HTML blocks when theme changes
407
+ // Listen for theme changes on document element
408
+ const themeObserver = new MutationObserver(() => {
409
+ console.log('[THEME] Theme changed, updating HTML blocks');
410
+ this.updateHtmlBlockThemes();
411
+ });
412
+
413
+ themeObserver.observe(document.documentElement, {
414
+ attributes: true,
415
+ attributeFilter: ['data-theme']
416
+ });
417
+
418
+ // Also listen for system theme changes
419
+ if (window.matchMedia) {
420
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
421
+ console.log('[THEME] System theme changed, updating HTML blocks');
422
+ this.updateHtmlBlockThemes();
423
+ });
424
+ }
425
+
426
+ const input = document.getElementById('messageInput');
427
+ if (input) {
428
+ input.addEventListener('keydown', (e) => {
429
+ if (e.key === 'Enter' && !e.shiftKey) {
430
+ e.preventDefault();
431
+ this.sendMessage();
432
+ }
433
+ });
434
+ input.addEventListener('input', () => this.updateSendButtonState());
435
+ }
436
+ document.getElementById('autoScroll')?.addEventListener('change', (e) => {
437
+ this.settings.autoScroll = e.target.checked;
438
+ this.saveSettings();
439
+ });
440
+ document.getElementById('connectTimeout')?.addEventListener('change', (e) => {
441
+ this.settings.connectTimeout = parseInt(e.target.value) * 1000;
442
+ this.saveSettings();
443
+ });
444
+ }
445
+
446
+ updateHtmlBlockThemes() {
447
+ // Update theme attribute and CSS for all existing HTML blocks
448
+ const currentTheme = document.documentElement.getAttribute('data-theme') ||
449
+ (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
450
+
451
+ // CRITICAL: Remove old theme styles and inject new ones for all HTML blocks
452
+ document.querySelectorAll('.html-content').forEach(content => {
453
+ // Remove any existing theme style tags
454
+ const oldStyles = content.querySelectorAll('style');
455
+ oldStyles.forEach(style => {
456
+ if (style.textContent.includes('.html-content')) {
457
+ style.remove();
458
+ }
459
+ });
460
+
461
+ // Inject new theme-aware CSS
462
+ const themeCSS = currentTheme === 'dark'
463
+ ? `<style>
464
+ .html-content {
465
+ color: #f8fafc;
466
+ background: transparent;
467
+ }
468
+ .html-content p { color: #cbd5e1; }
469
+ .html-content h1, .html-content h2, .html-content h3,
470
+ .html-content h4, .html-content h5, .html-content h6 {
471
+ color: #f8fafc;
472
+ }
473
+ .html-content a { color: #6366f1; }
474
+ .html-content code { color: #c7d2fe; background: rgba(0,0,0,0.3); }
475
+ .html-content pre { background: rgba(0,0,0,0.5); color: #e0e7ff; }
476
+ .html-content table { border-color: #334155; }
477
+ .html-content th { background: #1a202c; color: #f8fafc; }
478
+ .html-content td { border-color: #334155; }
479
+ .html-content blockquote { border-color: #334155; color: #cbd5e1; }
480
+ .html-content ul, .html-content ol { color: #cbd5e1; }
481
+ .html-content li { color: #cbd5e1; }
482
+ </style>`
483
+ : `<style>
484
+ .html-content {
485
+ color: #1d2129;
486
+ background: transparent;
487
+ }
488
+ .html-content p { color: #475569; }
489
+ .html-content h1, .html-content h2, .html-content h3,
490
+ .html-content h4, .html-content h5, .html-content h6 {
491
+ color: #1d2129;
492
+ }
493
+ .html-content a { color: #4f46e5; }
494
+ .html-content code { color: #6366f1; background: rgba(99,102,241,0.1); }
495
+ .html-content pre { background: #f3f4f6; color: #1d2129; }
496
+ .html-content table { border-color: #e5e7eb; }
497
+ .html-content th { background: #f9fafb; color: #1d2129; }
498
+ .html-content td { border-color: #e5e7eb; }
499
+ .html-content blockquote { border-color: #e5e7eb; color: #475569; }
500
+ .html-content ul, .html-content ol { color: #475569; }
501
+ .html-content li { color: #475569; }
502
+ </style>`;
503
+
504
+ // Create a temporary wrapper to parse and insert the style
505
+ const tempDiv = document.createElement('div');
506
+ tempDiv.innerHTML = themeCSS;
507
+ const styleEl = tempDiv.querySelector('style');
508
+ if (styleEl) {
509
+ content.insertBefore(styleEl.cloneNode(true), content.firstChild);
406
510
  }
511
+
512
+ // Update data-theme attribute
513
+ content.setAttribute('data-theme', currentTheme);
407
514
  });
408
- input.addEventListener('input', () => this.updateSendButtonState());
515
+
516
+ console.log(`[THEME] Updated ${document.querySelectorAll('.html-content').length} HTML blocks to ${currentTheme} mode`);
409
517
  }
410
- document.getElementById('autoScroll')?.addEventListener('change', (e) => {
411
- this.settings.autoScroll = e.target.checked;
412
- this.saveSettings();
413
- });
414
- document.getElementById('connectTimeout')?.addEventListener('change', (e) => {
415
- this.settings.connectTimeout = parseInt(e.target.value) * 1000;
416
- this.saveSettings();
417
- });
418
- }
419
518
 
420
519
  async fetchAgents() {
421
520
  try {
422
- const res = await fetch(BASE_URL + '/api/agents');
521
+ const res = await this.apiFetch(BASE_URL + '/api/agents');
423
522
  const data = await res.json();
424
523
  if (data.agents) {
425
524
  data.agents.forEach(a => this.agents.set(a.id, a));
@@ -432,7 +531,7 @@ class GMGUIApp {
432
531
  async fetchConversations() {
433
532
  try {
434
533
  console.log('[DEBUG] fetchConversations: Starting fetch from', BASE_URL + '/api/conversations');
435
- const res = await fetch(BASE_URL + '/api/conversations');
534
+ const res = await this.apiFetch(BASE_URL + '/api/conversations');
436
535
  console.log('[DEBUG] fetchConversations: Response status:', res.status);
437
536
 
438
537
  if (!res.ok) {
@@ -470,7 +569,7 @@ class GMGUIApp {
470
569
 
471
570
  async fetchMessages(conversationId) {
472
571
  try {
473
- const res = await fetch(`${BASE_URL}/api/conversations/${conversationId}/messages`);
572
+ const res = await this.apiFetch(`${BASE_URL}/api/conversations/${conversationId}/messages`);
474
573
  const data = await res.json();
475
574
  return data.messages || [];
476
575
  } catch (e) {
@@ -589,7 +688,7 @@ class GMGUIApp {
589
688
 
590
689
  async deleteConversation(id) {
591
690
  try {
592
- const res = await fetch(`${BASE_URL}/api/conversations/${id}`, { method: 'DELETE' });
691
+ const res = await this.apiFetch(`${BASE_URL}/api/conversations/${id}`, { method: 'DELETE' });
593
692
  if (!res.ok) {
594
693
  console.error('deleteConversation failed:', res.status);
595
694
  return;
@@ -745,39 +844,129 @@ class GMGUIApp {
745
844
  return elements.length > 0 ? elements : null;
746
845
  }
747
846
 
748
- renderTextOrHtml(text) {
749
- if (this.looksLikeHtml(text)) {
750
- return this.createSandboxedHtml(text);
751
- }
752
- const bubble = document.createElement('div');
753
- bubble.className = 'message-bubble';
754
- bubble.textContent = text;
755
- return bubble;
756
- }
757
-
758
- createSandboxedHtml(rawHtml) {
759
- const wrap = document.createElement('div');
760
- wrap.className = 'html-block rendered-html';
761
- const content = document.createElement('div');
762
- content.className = 'html-content';
763
-
764
- // CRITICAL: Ensure RippleUI styles are available for agent HTML
765
- // Agent responses use RippleUI/Tailwind classes, so wrap in a context that has those styles
766
- let enhancedHtml = rawHtml;
767
-
768
- // If HTML doesn't already have the RippleUI wrapper classes, add them
769
- if (!rawHtml.includes('space-y-4') && !rawHtml.includes('card') && !rawHtml.includes('alert')) {
770
- // Wrap in RippleUI container if agent didn't already wrap it
771
- enhancedHtml = `<div class="space-y-4 p-6 max-w-4xl">${rawHtml}</div>`;
772
- console.log('[HTML] Wrapped agent HTML in RippleUI container for styling');
773
- } else {
774
- console.log('[HTML] Agent HTML already has RippleUI classes');
775
- }
776
-
777
- content.innerHTML = this.sanitizeHtml(enhancedHtml);
778
- wrap.appendChild(content);
779
- return wrap;
780
- }
847
+ renderTextOrHtml(text) {
848
+ if (this.looksLikeHtml(text)) {
849
+ return this.createSandboxedHtml(text);
850
+ }
851
+
852
+ // CRITICAL FIX: Don't bundle all text into one bubble
853
+ // Try splitting by paragraph breaks first (double newlines)
854
+ let parts = text.split('\n\n').filter(p => p.trim());
855
+
856
+ // If no paragraphs found, try splitting by single newlines
857
+ // (handles imported messages that may not have proper paragraph breaks)
858
+ if (parts.length === 1) {
859
+ const singleNewlines = text.split('\n').filter(p => p.trim());
860
+ // Only use single newlines if we get reasonable chunks (3+ non-empty lines)
861
+ if (singleNewlines.length >= 3) {
862
+ parts = singleNewlines;
863
+ }
864
+ }
865
+
866
+ // If still just one part and it's very long (>500 chars), split by sentences
867
+ if (parts.length === 1 && text.length > 500) {
868
+ const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
869
+ if (sentences.length > 1) {
870
+ parts = sentences.map(s => s.trim()).filter(s => s);
871
+ }
872
+ }
873
+
874
+ if (parts.length === 1) {
875
+ // Single item - just one bubble
876
+ const bubble = document.createElement('div');
877
+ bubble.className = 'message-bubble';
878
+ bubble.textContent = text;
879
+ return bubble;
880
+ }
881
+
882
+ // Multiple parts - create separate bubbles for each
883
+ const container = document.createElement('div');
884
+ container.className = 'message-bubbles-container';
885
+
886
+ for (const part of parts) {
887
+ const bubble = document.createElement('div');
888
+ bubble.className = 'message-bubble';
889
+ bubble.textContent = part;
890
+ container.appendChild(bubble);
891
+ }
892
+
893
+ return container;
894
+ }
895
+
896
+ createSandboxedHtml(rawHtml) {
897
+ const wrap = document.createElement('div');
898
+ wrap.className = 'html-block rendered-html';
899
+ const content = document.createElement('div');
900
+ content.className = 'html-content';
901
+
902
+ // Get current theme to apply to HTML content
903
+ const currentTheme = document.documentElement.getAttribute('data-theme') ||
904
+ (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
905
+
906
+ // CRITICAL: Inject theme-aware CSS to ensure text colors work in dark/light mode
907
+ const themeCSS = currentTheme === 'dark'
908
+ ? `<style>
909
+ .html-content {
910
+ color: #f8fafc;
911
+ background: transparent;
912
+ }
913
+ .html-content p { color: #cbd5e1; }
914
+ .html-content h1, .html-content h2, .html-content h3,
915
+ .html-content h4, .html-content h5, .html-content h6 {
916
+ color: #f8fafc;
917
+ }
918
+ .html-content a { color: #6366f1; }
919
+ .html-content code { color: #c7d2fe; background: rgba(0,0,0,0.3); }
920
+ .html-content pre { background: rgba(0,0,0,0.5); color: #e0e7ff; }
921
+ .html-content table { border-color: #334155; }
922
+ .html-content th { background: #1a202c; color: #f8fafc; }
923
+ .html-content td { border-color: #334155; }
924
+ .html-content blockquote { border-color: #334155; color: #cbd5e1; }
925
+ .html-content ul, .html-content ol { color: #cbd5e1; }
926
+ .html-content li { color: #cbd5e1; }
927
+ </style>`
928
+ : `<style>
929
+ .html-content {
930
+ color: #1d2129;
931
+ background: transparent;
932
+ }
933
+ .html-content p { color: #475569; }
934
+ .html-content h1, .html-content h2, .html-content h3,
935
+ .html-content h4, .html-content h5, .html-content h6 {
936
+ color: #1d2129;
937
+ }
938
+ .html-content a { color: #4f46e5; }
939
+ .html-content code { color: #6366f1; background: rgba(99,102,241,0.1); }
940
+ .html-content pre { background: #f3f4f6; color: #1d2129; }
941
+ .html-content table { border-color: #e5e7eb; }
942
+ .html-content th { background: #f9fafb; color: #1d2129; }
943
+ .html-content td { border-color: #e5e7eb; }
944
+ .html-content blockquote { border-color: #e5e7eb; color: #475569; }
945
+ .html-content ul, .html-content ol { color: #475569; }
946
+ .html-content li { color: #475569; }
947
+ </style>`;
948
+
949
+ // CRITICAL: Ensure RippleUI styles are available for agent HTML
950
+ // Agent responses use RippleUI/Tailwind classes, so wrap in a context that has those styles
951
+ let enhancedHtml = themeCSS + rawHtml;
952
+
953
+ // If HTML doesn't already have the RippleUI wrapper classes, add them
954
+ if (!rawHtml.includes('space-y-4') && !rawHtml.includes('card') && !rawHtml.includes('alert')) {
955
+ // Wrap in RippleUI container if agent didn't already wrap it
956
+ enhancedHtml = themeCSS + `<div class="space-y-4 p-6 max-w-4xl">${rawHtml}</div>`;
957
+ console.log('[HTML] Wrapped agent HTML in RippleUI container with theme CSS');
958
+ } else {
959
+ console.log('[HTML] Agent HTML already has RippleUI classes, applying theme CSS');
960
+ }
961
+
962
+ content.innerHTML = this.sanitizeHtml(enhancedHtml);
963
+ wrap.appendChild(content);
964
+
965
+ // Apply theme attribute to content so nested elements inherit
966
+ content.setAttribute('data-theme', currentTheme);
967
+
968
+ return wrap;
969
+ }
781
970
 
782
971
  addMessageToDisplay(msg) {
783
972
  const div = document.getElementById('chatMessages');
@@ -824,30 +1013,40 @@ class GMGUIApp {
824
1013
  });
825
1014
  }
826
1015
 
827
- // Display segmented content if available
828
- if (msg.content.segments && Array.isArray(msg.content.segments)) {
829
- console.log('[HTML] Rendering segments from agent response');
830
- msg.content.segments.forEach(segment => {
831
- el.appendChild(this.renderSegment(segment));
832
- });
833
- } else if (msg.content.text && !hasHtmlContent) {
834
- // Only use text rendering if no HTML blocks were rendered
835
- // But ALWAYS check if text itself contains HTML
836
- if (this.looksLikeHtml(msg.content.text)) {
837
- console.log('[HTML] Agent text content contains HTML - rendering as HTML');
838
- el.appendChild(this.createSandboxedHtml(msg.content.text));
839
- } else {
840
- const parsed = this.parseAndRenderContent(msg.content.text);
841
- if (parsed) {
842
- parsed.forEach(elem => el.appendChild(elem));
843
- } else {
844
- const bubble = document.createElement('div');
845
- bubble.className = 'message-bubble';
846
- bubble.textContent = msg.content.text;
847
- el.appendChild(bubble);
848
- }
849
- }
850
- }
1016
+ // CRITICAL: Agent responses are now HTML from system prompt
1017
+ // Check if we have text first (which should be HTML)
1018
+ if (msg.content.text && !hasHtmlContent) {
1019
+ // ALWAYS check if text itself contains HTML first
1020
+ if (this.looksLikeHtml(msg.content.text)) {
1021
+ console.log('[HTML] ✅ Agent response is HTML - rendering directly (NOT segmenting)');
1022
+ el.appendChild(this.createSandboxedHtml(msg.content.text));
1023
+ } else {
1024
+ // Only if NOT HTML, then try segmenting
1025
+ console.log('[HTML] Text is not HTML, attempting segmentation');
1026
+ if (msg.content.segments && Array.isArray(msg.content.segments)) {
1027
+ console.log('[HTML] Rendering', msg.content.segments.length, 'segments');
1028
+ msg.content.segments.forEach(segment => {
1029
+ el.appendChild(this.renderSegment(segment));
1030
+ });
1031
+ } else {
1032
+ const parsed = this.parseAndRenderContent(msg.content.text);
1033
+ if (parsed) {
1034
+ parsed.forEach(elem => el.appendChild(elem));
1035
+ } else {
1036
+ const bubble = document.createElement('div');
1037
+ bubble.className = 'message-bubble';
1038
+ bubble.textContent = msg.content.text;
1039
+ el.appendChild(bubble);
1040
+ }
1041
+ }
1042
+ }
1043
+ } else if (msg.content.segments && Array.isArray(msg.content.segments) && !hasHtmlContent) {
1044
+ // Fallback: only use segments if we have them and no text
1045
+ console.log('[HTML] No text content, rendering segments');
1046
+ msg.content.segments.forEach(segment => {
1047
+ el.appendChild(this.renderSegment(segment));
1048
+ });
1049
+ }
851
1050
 
852
1051
  // Display metadata if available
853
1052
  if (msg.content.metadata) {
@@ -1055,7 +1254,7 @@ class GMGUIApp {
1055
1254
  ? folderPath.split('/').pop() || folderPath
1056
1255
  : `Chat ${this.conversations.size + 1}`;
1057
1256
  try {
1058
- const res = await fetch(BASE_URL + '/api/conversations', {
1257
+ const res = await this.apiFetch(BASE_URL + '/api/conversations', {
1059
1258
  method: 'POST',
1060
1259
  headers: { 'Content-Type': 'application/json' },
1061
1260
  body: JSON.stringify({ agentId: this.selectedAgent || 'claude-code', title }),
@@ -1096,7 +1295,7 @@ class GMGUIApp {
1096
1295
 
1097
1296
  try {
1098
1297
  const folderPath = conv?.folderPath || localStorage.getItem('gmgui-home') || '/config';
1099
- const res = await fetch(`${BASE_URL}/api/conversations/${this.currentConversation}/messages`, {
1298
+ const res = await this.apiFetch(`${BASE_URL}/api/conversations/${this.currentConversation}/messages`, {
1100
1299
  method: 'POST',
1101
1300
  headers: { 'Content-Type': 'application/json' },
1102
1301
  body: JSON.stringify({
@@ -1142,7 +1341,7 @@ class GMGUIApp {
1142
1341
 
1143
1342
  this.pollingInterval = setInterval(async () => {
1144
1343
  try {
1145
- const res = await fetch(`${BASE_URL}/api/conversations/${conversationId}/messages`);
1344
+ const res = await this.apiFetch(`${BASE_URL}/api/conversations/${conversationId}/messages`);
1146
1345
  const data = await res.json();
1147
1346
  const messages = data.messages || [];
1148
1347
 
@@ -1309,7 +1508,7 @@ class GMGUIApp {
1309
1508
  if (!list) return;
1310
1509
  list.innerHTML = '<div style="padding: 1rem; color: var(--text-tertiary);">Loading...</div>';
1311
1510
  try {
1312
- const res = await fetch(BASE_URL + '/api/folders', {
1511
+ const res = await this.apiFetch(BASE_URL + '/api/folders', {
1313
1512
  method: 'POST',
1314
1513
  headers: { 'Content-Type': 'application/json' },
1315
1514
  body: JSON.stringify({ path: folderPath }),
@@ -1391,7 +1590,7 @@ function createChatInFolder() {
1391
1590
  async function importClaudeCodeConversations() {
1392
1591
  closeNewChatModal();
1393
1592
  try {
1394
- const res = await fetch(BASE_URL + '/api/import/claude-code');
1593
+ const res = await this.apiFetch(BASE_URL + '/api/import/claude-code');
1395
1594
  const data = await res.json();
1396
1595
 
1397
1596
  if (!data.imported) {