11
class NetdataMCPChat {
12
constructor() {
13
// Log version on startup
14
- console.log('🚀 Netdata MCP Web Client v1.0.9 - Simplified resume using sendMessage');
14
+ console.log('🚀 Netdata MCP Web Client v1.0.67 - Multi-Chat Input Management Fixes');
15
16
this.mcpServers = new Map(); // Multiple MCP servers
17
this.mcpConnections = new Map(); // Active MCP connections
20
this.communicationLog = []; // Universal log (not saved)
21
this.tokenUsageHistory = new Map(); // Track token usage per chat
22
this.toolInclusionStates = new Map(); // Track which tools are included/excluded per chat
23
- this.currentContextWindow = 0; // Running total for delta calculation during rendering
24
- this.shouldStopProcessing = false; // Flag to stop processing between requests
25
- this.isProcessing = false; // Track if we're currently processing messages
23
+ // Removed global currentContextWindow - now stored per chat
24
+ // Removed global shouldStopProcessing - now stored per chat
25
+ // Removed global isProcessing - now stored per chat
26
this.modelPricing = {}; // Initialize model pricing storage
27
this.modelLimits = {}; // Initialize model context limits storage
28
this.copiedModel = null; // Track copied model for paste functionality
31
this.safetyChecker = new SafetyChecker();
32
33
// Per-chat DOM management
34
- this.chatContainers = new Map(); // Map of chatId -> DOM container
34
+ this.chatContainers = new Map(); // Map of chatId -> DOM container (includes both main and sub-chats)
35
36
// Models will be loaded dynamically from the proxy server
37
// No hardcoded model list needed
99
this.pendingNewChatTimeout = setTimeout(() => {
100
// Double-check user hasn't selected a chat in the meantime
101
if (!this.userHasSelectedChat && this.pendingNewChatLoad) {
102
- this.loadChat(newChatId);
102
+ this.loadChat(this.pendingNewChatId);
103
}
104
// Clear the pending flag
105
this.pendingNewChatLoad = false;
204
// Update cumulative token pricing
205
this.updateChatTokenPricing(chat);
206
207
+ // NOTE: Sub-chat cost accumulation is now handled in processSingleLLMResponse
208
+ // after tool-results are added to the parent chat, ensuring proper timing
209
+
210
// Update the cumulative token display
211
this.updateCumulativeTokenDisplay(chatId);
212
314
hasTokens = true;
315
break;
316
}
317
+ } else if (message.role === 'accounting' && message.cumulativeTokens) {
318
+ // Also check accounting nodes for tokens
319
+ const tokens = message.cumulativeTokens;
320
+ if ((tokens.inputTokens || 0) > 0 ||
321
+ (tokens.outputTokens || 0) > 0 ||
322
+ (tokens.cacheReadTokens || 0) > 0 ||
323
+ (tokens.cacheCreationTokens || 0) > 0) {
324
+ hasTokens = true;
325
+ break;
326
+ }
327
}
328
}
329
351
tokens.cacheCreationTokens += message.usage.cacheCreationInputTokens || 0;
352
tokens.cacheReadTokens += message.usage.cacheReadInputTokens || 0;
353
tokens.messageCount++;
354
+ } else if (message.role === 'accounting' && message.model && message.cumulativeTokens) {
355
+ // CRITICAL: Also collect tokens from accounting nodes being replaced
356
+ const model = message.model;
357
+ if (!tokensByModel.has(model)) {
358
+ tokensByModel.set(model, {
359
+ inputTokens: 0,
360
+ outputTokens: 0,
361
+ cacheReadTokens: 0,
362
+ cacheCreationTokens: 0,
363
+ messageCount: 0
364
+ });
365
+ }
366
+
367
+ const tokens = tokensByModel.get(model);
368
+ const cumTokens = message.cumulativeTokens;
369
+ tokens.inputTokens += cumTokens.inputTokens || 0;
370
+ tokens.outputTokens += cumTokens.outputTokens || 0;
371
+ tokens.cacheCreationTokens += cumTokens.cacheCreationTokens || 0;
372
+ tokens.cacheReadTokens += cumTokens.cacheReadTokens || 0;
373
+ tokens.messageCount += message.discardedMessages || 0;
374
}
375
}
376
442
).length;
443
}
444
445
+ /**
446
+ * Check if a string contains markdown formatting
447
+ */
448
+ isMarkdownContent(content) {
449
+ if (typeof content !== 'string' || !content.trim()) {
450
+ return false;
451
+ }
452
+
453
+ // Common markdown patterns
454
+ const markdownPatterns = [
455
+ /^#+\s/m, // Headers: # ## ###
456
+ /\*\*.*\*\*/, // Bold: **text**
457
+ /\*.*\*/, // Italic: *text*
458
+ /`.*`/, // Inline code: `code`
459
+ /```[\s\S]*?```/, // Code blocks: ```code```
460
+ /^\s*[-*+]\s/m, // Unordered lists: - * +
461
+ /^\s*\d+\.\s/m, // Ordered lists: 1. 2.
462
+ /^\s*>\s/m, // Blockquotes: >
463
+ /\[.*\]\(.*\)/, // Links: [text](url)
464
+ /!\[.*\]\(.*\)/, // Images: 
465
+ /^\s*\|.*\|/m, // Tables: | col1 | col2 |
466
+ /^---+$/m, // Horizontal rules: ---
467
+ /~~.*~~/, // Strikethrough: ~~text~~
468
+ ];
469
+
470
+ return markdownPatterns.some(pattern => pattern.test(content));
471
+ }
472
+
473
/**
474
* Auto-save with debouncing for performance
475
* Saves only the specific chat that was modified
500
*/
501
saveChatConfigSmart(chatId, config) {
502
const chat = this.chats.get(chatId);
442
- if (!chat) return;
503
+ if (!chat) {
504
+ console.error(`[saveChatConfigSmart] Chat not found for chatId: ${chatId}`);
505
+ return;
506
+ }
507
508
// Always save as last config for new chats to inherit
509
ChatConfig.saveLastConfig(config);
581
if (!chat.perModelTokensPrice) {
582
chat.perModelTokensPrice = {};
583
}
520
-
584
+
585
// Reset totals
586
chat.totalTokensPrice = {
587
input: 0,
639
// They represent aggregated tokens from deleted messages
640
}
641
}
642
+
643
+ // Add sub-chat costs from tool-results
644
+ this.aggregateSubChatCostsFromToolResults(chat);
645
+ }
646
+
647
+ /**
648
+ * Update parent's tool-result with sub-chat costs
649
+ */
650
+ updateParentToolResultCosts(parentChatId, toolCallId, subChat) {
651
+ const parentChat = this.chats.get(parentChatId);
652
+ if (!parentChat) {
653
+ console.error(`[updateParentToolResultCosts] Parent chat ${parentChatId} not found`);
654
+ return;
655
+ }
656
+
657
+
658
+ // Find the tool-result in parent messages
659
+ for (const message of parentChat.messages) {
660
+ if (message.role === 'tool-results' && message.toolResults) {
661
+ const toolResult = message.toolResults.find(tr => tr.toolCallId === toolCallId);
662
+ if (toolResult) {
663
+ // Store the sub-chat's current costs
664
+ toolResult.subChatCosts = {
665
+ totalTokens: { ...subChat.totalTokensPrice },
666
+ perModel: {}
667
+ };
668
+
669
+ // Deep copy per-model costs
670
+ for (const [model, costs] of Object.entries(subChat.perModelTokensPrice || {})) {
671
+ toolResult.subChatCosts.perModel[model] = { ...costs };
672
+ }
673
+
674
+ // Save parent chat to persist the updated costs
675
+ this.autoSave(parentChatId);
676
+ return;
677
+ }
678
+ }
679
+ }
680
+
681
+ console.error(`[updateParentToolResultCosts] Tool result ${toolCallId} not found in parent messages`);
682
+ }
683
+
684
+ /**
685
+ * Aggregate sub-chat costs from tool-results that have subChatCosts
686
+ */
687
+ aggregateSubChatCostsFromToolResults(chat) {
688
+ for (const message of chat.messages) {
689
+ if (message.role === 'tool-results' && message.toolResults) {
690
+ for (const toolResult of message.toolResults) {
691
+ if (toolResult.subChatCosts) {
692
+ const costs = toolResult.subChatCosts;
693
+
694
+ // Add to total tokens
695
+ chat.totalTokensPrice.input += costs.totalTokens.input || 0;
696
+ chat.totalTokensPrice.output += costs.totalTokens.output || 0;
697
+ chat.totalTokensPrice.cacheRead += costs.totalTokens.cacheRead || 0;
698
+ chat.totalTokensPrice.cacheCreation += costs.totalTokens.cacheCreation || 0;
699
+ chat.totalTokensPrice.totalCost += costs.totalTokens.totalCost || 0;
700
+
701
+ // Add to per-model tokens
702
+ for (const [model, modelCosts] of Object.entries(costs.perModel)) {
703
+ if (!chat.perModelTokensPrice[model]) {
704
+ chat.perModelTokensPrice[model] = {
705
+ input: 0,
706
+ output: 0,
707
+ cacheRead: 0,
708
+ cacheCreation: 0,
709
+ totalCost: 0
710
+ };
711
+ }
712
+
713
+ chat.perModelTokensPrice[model].input += modelCosts.input || 0;
714
+ chat.perModelTokensPrice[model].output += modelCosts.output || 0;
715
+ chat.perModelTokensPrice[model].cacheRead += modelCosts.cacheRead || 0;
716
+ chat.perModelTokensPrice[model].cacheCreation += modelCosts.cacheCreation || 0;
717
+ chat.perModelTokensPrice[model].totalCost += modelCosts.totalCost || 0;
718
+ }
719
+ }
720
+ }
721
+ }
722
+ }
723
}
724
725
// Migrate old chat data to include token pricing
797
798
// These will be set when switching chats for backward compatibility
799
this.chatTitle = null;
655
- this.chatInput = null;
800
this.sendMessageBtn = null;
801
this.reconnectMcpBtn = null;
802
this.copyMetricsBtn = null;
1064
let targetDropdown = dropdown || this.llmModelDropdown;
1065
1066
const chat = this.chats.get(targetChatId);
923
- if (!chat) {return;}
1067
+ if (!chat) {
1068
+ console.error(`[showModelSelector] Chat not found for chatId: ${targetChatId}`);
1069
+ return;
1070
+ }
1071
1072
const provider = this.llmProviders.get(chat.llmProviderId);
926
- if (!provider || !provider.availableProviders) {return;}
1073
+ if (!provider || !provider.availableProviders) {
1074
+ console.error(`[showModelSelector] Provider not found or has no available providers for providerId: ${chat.llmProviderId}`, { provider, hasAvailableProviders: provider?.availableProviders });
1075
+ return;
1076
+ }
1077
1078
// Create a modal overlay instead of using the dropdown
1079
const overlay = document.createElement('div');
1166
// Get current config
1167
const config = chat.config || ChatConfig.loadChatConfig(chatId);
1168
1169
+ // Ensure the config is assigned to the chat object
1170
+ if (!chat.config) {
1171
+ chat.config = config;
1172
+ }
1173
+
1174
// Create header section (fixed)
1175
const headerSection = document.createElement('div');
1176
headerSection.style.cssText = `
1308
`;
1309
section.appendChild(chatModelDiv);
1310
1156
- // Tool Summarization Option (DISABLED - Not Implemented)
1311
+ // Tool Summarization Option
1312
const toolSumDiv = document.createElement('div');
1158
- const _isEnabled = false; // Force disabled - not implemented
1159
- toolSumDiv.style.cssText = `display: flex; align-items: center; gap: 8px; margin-bottom: 8px; opacity: 0.4; color: var(--text-secondary);`;
1313
+ const _isEnabled = true; // Feature is now implemented
1314
+ toolSumDiv.style.cssText = `display: flex; align-items: center; gap: 8px; margin-bottom: 8px;`;
1315
1161
- const currentThreshold = config.optimisation.toolSummarisation.thresholdKiB || 20; // Default 20KB
1162
- const toolSumModel = ChatConfig.modelConfigToString(config.optimisation.toolSummarisation.model) || ChatConfig.getChatModelString(chat);
1316
+ const currentThreshold = chat.config.optimisation.toolSummarisation.thresholdKiB ?? 20; // Default 20KB, allow 0
1317
+ const toolSumModel = ChatConfig.modelConfigToString(chat.config.optimisation.toolSummarisation.model) || ChatConfig.getChatModelString(chat);
1318
1319
toolSumDiv.innerHTML = `
1165
- <label style="display: flex; align-items: center; cursor: not-allowed;">
1166
- <input type="checkbox" id="toolSummarization_${chatId}" disabled
1320
+ <label style="display: flex; align-items: center; cursor: pointer;">
1321
+ <input type="checkbox" id="toolSummarization_${chatId}" ${_isEnabled ? '' : 'disabled'}
1322
+ ${chat.config.optimisation.toolSummarisation.enabled ? 'checked' : ''}
1323
style="margin-right: 6px;">
1168
- <span style="text-decoration: line-through;">Summarize tool responses of at least</span>
1324
+ <span>Summarize tool responses of at least</span>
1325
</label>
1170
- <select id="toolThreshold_${chatId}" disabled
1326
+ <select id="toolThreshold_${chatId}" ${_isEnabled ? '' : 'disabled'}
1327
style="width: 70px; padding: 2px 4px; border: 1px solid var(--border-color);
1328
border-radius: 4px; background: var(--background-color); color: var(--text-primary);
1173
- cursor: not-allowed; text-decoration: line-through;">
1174
- <option value="0">0 (all)</option>
1175
- <option value="5">5</option>
1176
- <option value="10">10</option>
1329
+ cursor: pointer;">
1330
+ <option value="0" ${currentThreshold === 0 ? 'selected' : ''}>0 (all)</option>
1331
+ <option value="5" ${currentThreshold === 5 ? 'selected' : ''}>5</option>
1332
+ <option value="10" ${currentThreshold === 10 ? 'selected' : ''}>10</option>
1333
<option value="20" ${currentThreshold === 20 ? 'selected' : ''}>20</option>
1178
- <option value="30">30</option>
1179
- <option value="40">40</option>
1180
- <option value="50">50</option>
1181
- <option value="60">60</option>
1182
- <option value="70">70</option>
1183
- <option value="80">80</option>
1184
- <option value="90">90</option>
1185
- <option value="100">100</option>
1334
+ <option value="30" ${currentThreshold === 30 ? 'selected' : ''}>30</option>
1335
+ <option value="40" ${currentThreshold === 40 ? 'selected' : ''}>40</option>
1336
+ <option value="50" ${currentThreshold === 50 ? 'selected' : ''}>50</option>
1337
+ <option value="60" ${currentThreshold === 60 ? 'selected' : ''}>60</option>
1338
+ <option value="70" ${currentThreshold === 70 ? 'selected' : ''}>70</option>
1339
+ <option value="80" ${currentThreshold === 80 ? 'selected' : ''}>80</option>
1340
+ <option value="90" ${currentThreshold === 90 ? 'selected' : ''}>90</option>
1341
+ <option value="100" ${currentThreshold === 100 ? 'selected' : ''}>100</option>
1342
</select>
1187
- <span style="text-decoration: line-through;">KiB size, with</span>
1343
+ <span>KiB size, with</span>
1344
<div class="model-select-wrapper" style="position: relative; display: inline-block;">
1189
- <button class="model-select-btn" id="toolSumModel_${chatId}" disabled
1345
+ <button class="model-select-btn" id="toolSumModel_${chatId}" ${_isEnabled ? '' : 'disabled'}
1346
style="padding: 2px 8px; border: 1px solid var(--border-color);
1347
border-radius: 4px; background: var(--background-color);
1192
- color: var(--text-primary); cursor: not-allowed;
1193
- display: flex; align-items: center; gap: 4px; text-decoration: line-through;">
1348
+ color: var(--text-primary); cursor: pointer;
1349
+ display: flex; align-items: center; gap: 4px;">
1350
<span class="model-name">${toolSumModel || 'Select model'}</span>
1351
<i class="fas fa-chevron-down" style="font-size: 10px;"></i>
1352
</button>
1355
1356
section.appendChild(toolSumDiv);
1357
1202
- // Auto-summarization Option (DISABLED - Not Implemented)
1358
+ // Auto-summarization Option
1359
const autoSumDiv = document.createElement('div');
1204
- const _autoSumEnabled = false; // Force disabled - not implemented
1205
- autoSumDiv.style.cssText = `display: flex; align-items: center; gap: 8px; margin-bottom: 8px; opacity: 0.4; color: var(--text-secondary);`;
1360
+ const _autoSumEnabled = true; // Auto-summarization is now implemented
1361
+ autoSumDiv.style.cssText = `display: flex; align-items: center; gap: 8px; margin-bottom: 8px;`;
1362
1207
- const currentPercent = config.optimisation.autoSummarisation.triggerPercent || 50;
1208
- const autoSumModel = ChatConfig.modelConfigToString(config.optimisation.autoSummarisation.model) || ChatConfig.getChatModelString(chat);
1363
+ const currentPercent = chat.config.optimisation.autoSummarisation.triggerPercent || 50;
1364
+ const autoSumModel = ChatConfig.modelConfigToString(chat.config.optimisation.autoSummarisation.model) || ChatConfig.getChatModelString(chat);
1365
1366
autoSumDiv.innerHTML = `
1211
- <label style="display: flex; align-items: center; cursor: not-allowed;">
1212
- <input type="checkbox" id="autoSummarization_${chatId}" disabled
1367
+ <label style="display: flex; align-items: center; cursor: pointer;">
1368
+ <input type="checkbox" id="autoSummarization_${chatId}" ${chat.config.optimisation.autoSummarisation.enabled ? 'checked' : ''}
1369
style="margin-right: 6px;">
1214
- <span style="text-decoration: line-through;">Summarize conversation when context window above</span>
1370
+ <span>Summarize conversation when context window above</span>
1371
</label>
1216
- <select id="autoSumThreshold_${chatId}" disabled
1372
+ <select id="autoSumThreshold_${chatId}"
1373
style="width: 70px; padding: 2px 4px; border: 1px solid var(--border-color);
1374
border-radius: 4px; background: var(--background-color); color: var(--text-primary);
1219
- cursor: not-allowed; text-decoration: line-through;">
1375
+ cursor: pointer;">
1376
<option value="30">30%</option>
1377
<option value="40">40%</option>
1378
<option value="50" ${currentPercent === 50 ? 'selected' : ''}>50%</option>
1381
<option value="80">80%</option>
1382
<option value="90">90%</option>
1383
</select>
1228
- <span style="text-decoration: line-through;">with</span>
1384
+ <span>with</span>
1385
<div class="model-select-wrapper" style="position: relative; display: inline-block;">
1230
- <button class="model-select-btn" id="autoSumModel_${chatId}" disabled
1386
+ <button class="model-select-btn" id="autoSumModel_${chatId}"
1387
style="padding: 2px 8px; border: 1px solid var(--border-color);
1388
border-radius: 4px; background: var(--background-color);
1233
- color: var(--text-primary); cursor: not-allowed;
1234
- display: flex; align-items: center; gap: 4px; text-decoration: line-through;">
1389
+ color: var(--text-primary); cursor: pointer;
1390
+ display: flex; align-items: center; gap: 4px;">
1391
<span class="model-name">${autoSumModel || 'Select model'}</span>
1392
<i class="fas fa-chevron-down" style="font-size: 10px;"></i>
1393
</button>
1398
1399
// Title Generation Option
1400
const titleGenDiv = document.createElement('div');
1245
- const titleGenEnabled = config.optimisation.titleGeneration?.enabled !== false; // Default to true
1401
+ const titleGenEnabled = chat.config.optimisation.titleGeneration?.enabled !== false; // Default to true
1402
titleGenDiv.style.cssText = `display: flex; align-items: center; gap: 8px; margin-bottom: 8px; ${!titleGenEnabled ? 'opacity: 0.5;' : ''}`;
1403
1248
- const titleGenModel = ChatConfig.modelConfigToString(config.optimisation.titleGeneration?.model);
1404
+ const titleGenModel = ChatConfig.modelConfigToString(chat.config.optimisation.titleGeneration?.model);
1405
1406
titleGenDiv.innerHTML = `
1407
<label style="display: flex; align-items: center; cursor: pointer;">
1426
1427
// Tool Memory Option
1428
const toolMemoryDiv = document.createElement('div');
1273
- const toolMemoryEnabled = config.optimisation.toolMemory.enabled;
1429
+ const toolMemoryEnabled = chat.config.optimisation.toolMemory.enabled;
1430
toolMemoryDiv.style.cssText = `display: flex; align-items: center; gap: 8px; margin-bottom: 8px; ${!toolMemoryEnabled ? 'opacity: 0.5;' : ''}`;
1431
1276
- const forgetAfterConclusions = config.optimisation.toolMemory.forgetAfterConclusions;
1432
+ const forgetAfterConclusions = chat.config.optimisation.toolMemory.forgetAfterConclusions;
1433
1434
toolMemoryDiv.innerHTML = `
1435
<label style="display: flex; align-items: center; cursor: pointer;">
1452
1453
section.appendChild(toolMemoryDiv);
1454
1299
- // Cache Control Option (only for Anthropic provider)
1300
- const isAnthropicProvider = config.model && config.model.provider === 'anthropic';
1455
+ // Cache Control Option (for Anthropic provider)
1456
+ const isAnthropicProvider = chat.config.model && chat.config.model.provider === 'anthropic';
1457
const cacheControlDiv = document.createElement('div');
1302
- const cacheControlEnabled = config.optimisation.cacheControl.enabled;
1303
- const cacheControlDisabled = !isAnthropicProvider || toolMemoryEnabled;
1458
+ const cacheControlMode = chat.config.optimisation.cacheControl;
1459
+ const cacheControlDisabled = !isAnthropicProvider;
1460
cacheControlDiv.style.cssText = `display: flex; align-items: center; gap: 8px; margin-bottom: 8px; ${cacheControlDisabled ? 'opacity: 0.5;' : ''}`;
1461
1462
cacheControlDiv.innerHTML = `
1307
- <label style="display: flex; align-items: center; cursor: ${cacheControlDisabled ? 'default' : 'pointer'};">
1308
- <input type="checkbox" id="cacheControl_${chatId}" ${cacheControlEnabled ? 'checked' : ''}
1309
- style="margin-right: 6px;"
1310
- ${cacheControlDisabled ? 'disabled' : ''}>
1311
- <span>Enable Anthropic's cache control${toolMemoryEnabled ? ' (disabled: tool memory is on)' : ''}</span>
1463
+ <label style="display: flex; align-items: center;">
1464
+ <span>Cache control:</span>
1465
</label>
1466
+ <select id="cacheControl_${chatId}"
1467
+ style="width: 100px; padding: 2px 4px; border: 1px solid var(--border-color);
1468
+ border-radius: 4px; background: var(--background-color); color: var(--text-primary);
1469
+ cursor: pointer;"
1470
+ ${cacheControlDisabled ? 'disabled' : ''}>
1471
+ <option value="all-off" ${cacheControlMode === 'all-off' ? 'selected' : ''}>Off</option>
1472
+ <option value="system" ${cacheControlMode === 'system' ? 'selected' : ''}>System</option>
1473
+ <option value="cached" ${cacheControlMode === 'cached' ? 'selected' : ''}>Cached</option>
1474
+ </select>
1475
+ ${!isAnthropicProvider ? '<span style="color: var(--text-secondary); font-size: 12px;">Anthropic only</span>' : ''}
1476
`;
1477
1478
section.appendChild(cacheControlDiv);
1567
1568
thresholdSelect.addEventListener('change', (e) => {
1569
e.stopPropagation();
1407
- const kbValue = parseInt(e.target.value, 10) || 20;
1408
- const byteValue = kbValue * 1024;
1570
+ const kbValue = parseInt(e.target.value, 10);
1571
+ const validKbValue = isNaN(kbValue) ? 20 : kbValue; // Default 20KB only if invalid, allow 0
1572
+ const byteValue = validKbValue * 1024;
1573
this.updateToolThreshold(chatId, byteValue);
1574
});
1575
1615
toolMemorySelect.disabled = !enabled;
1616
toolMemoryDiv.style.opacity = enabled ? '1' : '0.5';
1617
1454
- // Update cache control state for Anthropic (mutually exclusive with tool memory)
1455
- if (isAnthropicProvider) {
1456
- const cacheControlCheckbox = section.querySelector(`#cacheControl_${chatId}`);
1457
- const cacheControlLabel = cacheControlCheckbox.closest('label');
1458
- const cacheControlSpan = cacheControlLabel.querySelector('span');
1459
-
1460
- if (enabled) {
1461
- // Disable cache control when tool memory is enabled
1462
- cacheControlCheckbox.disabled = true;
1463
- cacheControlCheckbox.closest('div').style.opacity = '0.5';
1464
- cacheControlSpan.textContent = 'Enable Anthropic\'s cache control (disabled: tool memory is on)';
1465
- if (cacheControlCheckbox.checked) {
1466
- cacheControlCheckbox.checked = false;
1467
- this.updateOptimizationSetting(chatId, 'cacheControl', false);
1468
- }
1469
- } else {
1470
- // Re-enable cache control when tool memory is disabled
1471
- cacheControlCheckbox.disabled = false;
1472
- cacheControlCheckbox.closest('div').style.opacity = '1';
1473
- cacheControlSpan.textContent = 'Enable Anthropic\'s cache control';
1474
- }
1475
- }
1618
+ // Cache control is no longer mutually exclusive with tool memory
1619
1620
this.updateOptimizationSetting(chatId, 'toolMemory', enabled);
1621
});
1626
this.updateToolMemoryThreshold(chatId, newForgetAfterConclusions);
1627
});
1628
1629
+ // Cache control dropdown event listener
1630
+ const cacheControlSelect = section.querySelector(`#cacheControl_${chatId}`);
1631
+ if (cacheControlSelect) {
1632
+ cacheControlSelect.addEventListener('change', (e) => {
1633
+ e.stopPropagation();
1634
+ const newCacheMode = e.target.value;
1635
+ this.updateCacheControlMode(chatId, newCacheMode);
1636
+ });
1637
+ }
1638
+
1639
// Other checkboxes (smart filtering, cache control)
1640
section.querySelectorAll('input[type="checkbox"]:not(#toolSummarization_' + chatId + '):not(#autoSummarization_' + chatId + ')').forEach(checkbox => {
1641
checkbox.addEventListener('change', (e) => {
1771
<tr>
1772
<td style="padding: 4px 6px; color: var(--text-secondary);">Cache Control:</td>
1773
<td style="padding: 4px 6px; text-align: right; font-size: 10px;">
1621
- ${status(config.optimisation.cacheControl.enabled)}
1622
- ${config.optimisation.cacheControl.enabled ? `Strategy: ${config.optimisation.cacheControl.strategy}` : 'Disabled'}
1774
+ ${config.optimisation.cacheControl === 'all-off' ? 'Off' :
1775
+ config.optimisation.cacheControl === 'system' ? 'System only' :
1776
+ config.optimisation.cacheControl === 'cached' ? 'Cached' : config.optimisation.cacheControl}
1777
</td>
1778
</tr>
1779
<tr>
2030
// Focus search input when dropdown opens
2031
setTimeout(() => searchInput.focus(), 0);
2032
2033
+ // Function to update dropdown position on scroll/resize
2034
+ const updateDropdownPosition = () => {
2035
+ const newButtonRect = button.getBoundingClientRect();
2036
+ const newViewportHeight = window.innerHeight;
2037
+ const newViewportWidth = window.innerWidth;
2038
+ const newSpaceBelow = newViewportHeight - newButtonRect.bottom;
2039
+ const newShouldShowAbove = newSpaceBelow < dropdownHeight && newButtonRect.top > dropdownHeight;
2040
+
2041
+ if (newShouldShowAbove) {
2042
+ dropdown.style.top = 'auto';
2043
+ dropdown.style.bottom = `${newViewportHeight - newButtonRect.top + 4}px`;
2044
+ } else {
2045
+ dropdown.style.bottom = 'auto';
2046
+ dropdown.style.top = `${newButtonRect.bottom + 4}px`;
2047
+ }
2048
+
2049
+ // Update horizontal position
2050
+ let newLeftPosition = newButtonRect.left;
2051
+ if (newLeftPosition + dropdownMinWidth > newViewportWidth) {
2052
+ newLeftPosition = Math.max(10, newViewportWidth - dropdownMinWidth - 10);
2053
+ }
2054
+ dropdown.style.left = `${newLeftPosition}px`;
2055
+ };
2056
+
2057
// Create pricing table
2058
const models = this.getAllAvailableModels();
2059
2085
`;
2086
document.body.appendChild(dropdown);
2087
1910
- // Function to update dropdown position on scroll/resize
1911
- const updateDropdownPosition = () => {
1912
- const newButtonRect = button.getBoundingClientRect();
1913
- const newViewportHeight = window.innerHeight;
1914
- const newViewportWidth = window.innerWidth;
1915
- const newSpaceBelow = newViewportHeight - newButtonRect.bottom;
1916
- const newShouldShowAbove = newSpaceBelow < dropdownHeight && newButtonRect.top > dropdownHeight;
1917
-
1918
- if (newShouldShowAbove) {
1919
- dropdown.style.top = 'auto';
1920
- dropdown.style.bottom = `${newViewportHeight - newButtonRect.top + 4}px`;
1921
- } else {
1922
- dropdown.style.bottom = 'auto';
1923
- dropdown.style.top = `${newButtonRect.bottom + 4}px`;
1924
- }
1925
-
1926
- // Update horizontal position
1927
- let newLeftPosition = newButtonRect.left;
1928
- if (newLeftPosition + dropdownMinWidth > newViewportWidth) {
1929
- newLeftPosition = Math.max(10, newViewportWidth - dropdownMinWidth - 10);
1930
- }
1931
- dropdown.style.left = `${newLeftPosition}px`;
1932
- };
1933
-
2088
// Close dropdown on outside click
2089
const closeDropdown = (evt) => {
2090
if (!dropdown.contains(evt.target) && !button.contains(evt.target)) {
2301
}
2302
});
2303
2150
- // Define functions before they're used
2151
- // Function to update dropdown position on scroll/resize
2152
- const updateDropdownPosition = () => {
2153
- const newButtonRect = button.getBoundingClientRect();
2154
- const newViewportHeight = window.innerHeight;
2155
- const newViewportWidth = window.innerWidth;
2156
- const newSpaceBelow = newViewportHeight - newButtonRect.bottom;
2157
- const newShouldShowAbove = newSpaceBelow < dropdownHeight && newButtonRect.top > dropdownHeight;
2158
-
2159
- if (newShouldShowAbove) {
2160
- dropdown.style.top = 'auto';
2161
- dropdown.style.bottom = `${newViewportHeight - newButtonRect.top + 4}px`;
2162
- } else {
2163
- dropdown.style.bottom = 'auto';
2164
- dropdown.style.top = `${newButtonRect.bottom + 4}px`;
2165
- }
2166
-
2167
- // Update horizontal position
2168
- let newLeftPosition = newButtonRect.left;
2169
- if (newLeftPosition + dropdownMinWidth > newViewportWidth) {
2170
- newLeftPosition = Math.max(10, newViewportWidth - dropdownMinWidth - 10);
2171
- }
2172
- dropdown.style.left = `${newLeftPosition}px`;
2173
- };
2174
-
2304
// Close dropdown on outside click
2305
const closeDropdown = (evt) => {
2306
// Check if click is outside dropdown and button
2356
2357
updateOptimizationSetting(chatId, settingType, enabled) {
2358
const chat = this.chats.get(chatId);
2230
- if (!chat) return;
2359
+ if (!chat) {
2360
+ console.error(`[updateOptimizationSetting] Chat not found for chatId: ${chatId}, settingType: ${settingType}`);
2361
+ return;
2362
+ }
2363
2364
// Get current config or create defaults
2365
const config = chat.config || ChatConfig.loadChatConfig(chatId);
2372
case 'toolMemory':
2373
config.optimisation.toolMemory.enabled = enabled;
2374
break;
2243
- case 'cacheControl':
2244
- config.optimisation.cacheControl.enabled = enabled;
2245
- break;
2375
case 'autoSummarization':
2376
config.optimisation.autoSummarisation.enabled = enabled;
2377
break;
2405
this.autoSave(chatId);
2406
}
2407
2408
+ updateCacheControlMode(chatId, cacheMode) {
2409
+ const chat = this.chats.get(chatId);
2410
+ if (!chat) {
2411
+ console.error(`[updateCacheControlMode] Chat not found for chatId: ${chatId}, cacheMode: ${cacheMode}`);
2412
+ return;
2413
+ }
2414
+
2415
+ // Get current config or create defaults
2416
+ const config = chat.config || ChatConfig.loadChatConfig(chatId);
2417
+
2418
+ // Update cache control mode
2419
+ config.optimisation.cacheControl = cacheMode;
2420
+
2421
+ // Update chat config
2422
+ chat.config = config;
2423
+
2424
+ // Recreate MessageOptimizer with new settings
2425
+ const optimizerSettings = {
2426
+ ...config,
2427
+ llmProviderFactory: config.optimisation.toolSummarisation.enabled ? window.createLLMProvider : undefined
2428
+ };
2429
+
2430
+ try {
2431
+ chat.messageOptimizer = new MessageOptimizer(optimizerSettings);
2432
+ } catch (error) {
2433
+ console.error('[updateCacheControlMode] Failed to create MessageOptimizer:', error);
2434
+ }
2435
+
2436
+ // Save config
2437
+ this.saveChatConfigSmart(chatId, config);
2438
+
2439
+ // Auto-save chat
2440
+ this.autoSave(chatId);
2441
+ }
2442
2443
updateChatModel(chatId, model) {
2444
const chat = this.chats.get(chatId);
2282
- if (!chat) return;
2445
+ if (!chat) {
2446
+ console.error(`[updateChatModel] Chat not found for chatId: ${chatId}, model: ${model}`);
2447
+ return;
2448
+ }
2449
2450
// Update config
2451
const config = chat.config || ChatConfig.loadChatConfig(chatId);
2472
2473
updateToolSummarizationModel(chatId, model) {
2474
const chat = this.chats.get(chatId);
2309
- if (!chat) return;
2475
+ if (!chat) {
2476
+ console.error(`[updateToolSummarizationModel] Chat not found for chatId: ${chatId}, model: ${model}`);
2477
+ return;
2478
+ }
2479
2480
const config = chat.config || ChatConfig.loadChatConfig(chatId);
2481
config.optimisation.toolSummarisation.model = ChatConfig.modelConfigFromString(model);
2488
2489
updateAutoSummarizationModel(chatId, model) {
2490
const chat = this.chats.get(chatId);
2322
- if (!chat) return;
2491
+ if (!chat) {
2492
+ console.error(`[updateAutoSummarizationModel] Chat not found for chatId: ${chatId}, model: ${model}`);
2493
+ return;
2494
+ }
2495
2496
const config = chat.config || ChatConfig.loadChatConfig(chatId);
2497
config.optimisation.autoSummarisation.model = ChatConfig.modelConfigFromString(model);
2504
2505
updateTitleGenerationModel(chatId, model) {
2506
const chat = this.chats.get(chatId);
2335
- if (!chat) return;
2507
+ if (!chat) {
2508
+ console.error(`[updateTitleGenerationModel] Chat not found for chatId: ${chatId}, model: ${model}`);
2509
+ return;
2510
+ }
2511
2512
const config = chat.config || ChatConfig.loadChatConfig(chatId);
2513
// Use feature-specific defaults for title generation
3086
}
3087
3088
// Update error state
2914
- this.showError(chatId, errorMessage, errorType);
3089
+ this.showError(errorMessage, chatId, false, errorType);
3090
3091
// Add error message
3092
this.addMessage(chatId, {
3114
3115
// Continue the loop
3116
while (true) {
2942
- // Check if we should stop processing
2943
- if (this.shouldStopProcessing) {
3117
+ // Check if we should stop processing for this chat
3118
+ if (chat.shouldStopProcessing) {
3119
break;
3120
}
3121
3230
3231
// Clean and add to messages for API
3232
const cleanedContent = this.cleanContentForAPI(response.content);
3058
- if (cleanedContent && cleanedContent.trim()) {
3059
- messages.push({ role: 'assistant', content: cleanedContent });
3060
- }
3233
+ // Always add assistant message when no tool calls, even if content is empty
3234
+ // This prevents infinite loops when LLM responds with only thinking tags
3235
+ messages.push({ role: 'assistant', content: cleanedContent || '' });
3236
}
3237
return;
3238
}
3343
turn: chat.currentTurn
3344
});
3345
3346
+ // CRITICAL: Now that tool-results are added, update sub-chat costs
3347
+ for (const toolResult of toolResults) {
3348
+ if (toolResult.subChatId && toolResult.wasProcessedBySubChat) {
3349
+ const subChat = this.chats.get(toolResult.subChatId);
3350
+ if (subChat) {
3351
+ // Ensure sub-chat has up-to-date token pricing
3352
+ this.updateChatTokenPricing(subChat);
3353
+ // Store costs in parent tool-result
3354
+ this.updateParentToolResultCosts(chat.id, toolResult.toolCallId, subChat);
3355
+ } else {
3356
+ console.error(`[processSingleLLMResponse] ERROR: Sub-chat ${toolResult.subChatId} not found when trying to update parent costs for tool ${toolResult.toolCallId}`);
3357
+ }
3358
+ }
3359
+ }
3360
+
3361
+ // CRITICAL FIX: Update parent chat token pricing to include all sub-chat costs
3362
+ this.updateChatTokenPricing(chat);
3363
+ this.updateAllTokenDisplays(chat.id);
3364
+
3365
// Add to messages for API
3366
const includedResults = toolResults.filter(tr => tr.includeInContext !== false);
3367
if (includedResults.length > 0) {
3376
3377
messages.push(toolResultsMessage);
3378
3185
- // Tool summarization if enabled
3186
- if (chat.messageOptimizer && chat.config?.optimisation?.toolSummarisation?.enabled) {
3187
- try {
3188
- const toolSchemas = new Map();
3189
- for (const tool of tools) {
3190
- toolSchemas.set(tool.name, tool);
3191
- }
3192
-
3193
- const summarizedMessages = await chat.messageOptimizer.performToolSummarization(
3194
- messages,
3195
- {
3196
- toolSchemas,
3197
- providerInfo: { url: provider.proxyUrl }
3198
- }
3199
- );
3200
-
3201
- messages.length = 0;
3202
- messages.push(...summarizedMessages);
3203
- } catch (error) {
3204
- console.error('[Tool Summarization] Failed:', error);
3205
- }
3206
- }
3379
+ // Sub-chats are now processed immediately during tool execution (interleaved)
3380
}
3381
3382
// Reset assistant group and show thinking spinner for next iteration
3425
? result.length
3426
: JSON.stringify(result).length;
3427
3255
- // Show result
3256
- this.processRenderEvent({
3257
- type: 'tool-result',
3258
- name: toolCall.name,
3259
- result,
3260
- toolCallId: toolCall.id,
3261
- responseTime: toolResponseTime,
3262
- responseSize,
3263
- messageIndex: assistantMessageIndex
3264
- }, chat.id);
3428
+ // Check if we should create a sub-chat for this tool response
3429
+ // eslint-disable-next-line no-await-in-loop
3430
+ const shouldCreateSubChat = await this.shouldCreateSubChat(chat, responseSize, toolCall);
3431
+
3432
+ // Show result only if we're not creating a sub-chat
3433
+ if (!shouldCreateSubChat) {
3434
+ this.processRenderEvent({
3435
+ type: 'tool-result',
3436
+ name: toolCall.name,
3437
+ result,
3438
+ toolCallId: toolCall.id,
3439
+ responseTime: toolResponseTime,
3440
+ responseSize,
3441
+ messageIndex: assistantMessageIndex
3442
+ }, chat.id);
3443
+ }
3444
3266
- toolResults.push({
3267
- toolCallId: toolCall.id,
3268
- name: toolCall.name,
3269
- result,
3270
- includeInContext: true
3271
- });
3445
+ if (shouldCreateSubChat) {
3446
+ console.log(`[executeToolCalls] Creating sub-chat for tool ${toolCall.id} (${toolCall.name})`);
3447
+
3448
+ // Create sub-chat for processing this tool response
3449
+ // eslint-disable-next-line no-await-in-loop
3450
+ const subChatId = await this.createSubChatForTool(chat, toolCall, result);
3451
+
3452
+ console.log(`[executeToolCalls] Created sub-chat ${subChatId} for tool ${toolCall.id}`);
3453
+
3454
+ // Show secondary assistant waiting spinner for main chat
3455
+ this.showSecondaryAssistantWaiting(chat.id);
3456
+
3457
+ // Render sub-chat DOM BEFORE processing starts so users can see it populate
3458
+ this.renderSubChatAsItem(chat.id, subChatId, toolCall.id, 'processing');
3459
+
3460
+ // CRITICAL: Ensure the sub-chat container is available before processing
3461
+ // The container should have been created by renderSubChatAsItem
3462
+ const subChatContainer = this.chatContainers.get(subChatId);
3463
+ if (!subChatContainer) {
3464
+ console.error(`[executeToolCalls] Sub-chat container not found after renderSubChatAsItem for ${subChatId}`);
3465
+ // Update status to failed since we can't process without a container
3466
+ this.updateSubChatStatus(chat.id, toolCall.id, 'failed');
3467
+ continue;
3468
+ }
3469
+
3470
+ // Process the sub-chat immediately (interleaved execution)
3471
+ let summarizedResult = null;
3472
+ try {
3473
+ // eslint-disable-next-line no-await-in-loop
3474
+ summarizedResult = await this.processSubChat(subChatId, chat.id, toolCall.id);
3475
+
3476
+ if (!summarizedResult) {
3477
+ console.warn('[Sub-chat Processing] No summarized result returned, keeping original');
3478
+ } else {
3479
+ console.log(`[Sub-chat Processing] Replacing tool result for ${toolCall.id} with summarized content (${summarizedResult.length} chars)`);
3480
+ }
3481
+
3482
+ // Update sub-chat status to final state
3483
+ this.updateSubChatStatus(chat.id, toolCall.id, summarizedResult ? 'success' : 'failed');
3484
+ } catch (error) {
3485
+ console.error('[Sub-chat Processing] Failed:', error);
3486
+ // Update sub-chat status to failed
3487
+ this.updateSubChatStatus(chat.id, toolCall.id, 'failed');
3488
+ }
3489
+
3490
+ // Hide secondary assistant waiting spinner
3491
+ this.hideSecondaryAssistantWaiting(chat.id);
3492
+
3493
+ this.processRenderEvent({
3494
+ type: 'tool-result',
3495
+ name: toolCall.name,
3496
+ result: summarizedResult || result, // Use summarized result if available
3497
+ toolCallId: toolCall.id,
3498
+ responseTime: toolResponseTime,
3499
+ responseSize,
3500
+ messageIndex: assistantMessageIndex,
3501
+ subChatId,
3502
+ wasProcessedBySubChat: !!summarizedResult
3503
+ }, chat.id);
3504
+
3505
+ // Add the processed tool result
3506
+ const processedToolResult = {
3507
+ toolCallId: toolCall.id,
3508
+ name: toolCall.name,
3509
+ result: summarizedResult || result, // Use summarized result if available
3510
+ includeInContext: true,
3511
+ subChatId, // Keep for tracking
3512
+ wasProcessedBySubChat: !!summarizedResult,
3513
+ subChatFailed: !summarizedResult
3514
+ };
3515
+ toolResults.push(processedToolResult);
3516
+
3517
+ // Update the DOM display to show final state
3518
+ this.updateToolResultDisplay(chat.id, toolCall.id, processedToolResult);
3519
+
3520
+ // CRITICAL: Save the updated parent chat with summarized results
3521
+ this.autoSave(chat.id);
3522
+ } else {
3523
+ toolResults.push({
3524
+ toolCallId: toolCall.id,
3525
+ name: toolCall.name,
3526
+ result,
3527
+ includeInContext: true
3528
+ });
3529
+ }
3530
3531
} catch (error) {
3532
const errorMsg = `Tool error (${toolCall.name}): ${error.message}`;
3580
try {
3581
// Track timing
3582
const llmStartTime = Date.now();
3325
- const response = await provider.sendMessage(messages, tools, temperature, cacheControlIndex);
3583
+ const response = await provider.sendMessage(messages, tools, temperature, chat.config.optimisation.cacheControl || 'all-off', cacheControlIndex, chat);
3584
const llmResponseTime = Date.now() - llmStartTime;
3585
3586
// Store response time
3658
chat.isProcessing = false;
3659
3660
// Clear stop-related flags to ensure they're ready for next time
3403
- this.shouldStopProcessing = false;
3661
+ chat.shouldStopProcessing = false;
3662
chat.processingWasStoppedByUser = false;
3663
3664
// Clear error state on successful conclusion
3677
chat.updatedAt = new Date().toISOString();
3678
this.autoSave(chatId);
3679
3422
- // Re-enable input if it's the active chat
3423
- if (chatId === this.getActiveChatId()) {
3424
- const container = this.getChatContainer(chatId);
3425
- if (container && container._elements) {
3426
- const input = container._elements.input;
3427
- if (input) {
3428
- input.disabled = false;
3680
+ // Always re-enable input for the chat that concluded
3681
+ const container = this.getChatContainer(chatId);
3682
+ if (container && container._elements) {
3683
+ const input = container._elements.input;
3684
+ if (input) {
3685
+ // Always re-enable contentEditable
3686
+ input.contentEditable = true;
3687
+
3688
+ // Only focus if this is the active chat
3689
+ if (chatId === this.getActiveChatId()) {
3690
input.focus();
3691
}
3431
-
3432
- // Update send button state
3433
- const sendBtn = container._elements.sendBtn;
3434
- if (sendBtn && input) {
3435
- sendBtn.disabled = !input.value.trim();
3692
+ }
3693
+
3694
+ // Update send button state
3695
+ const sendBtn = container._elements.sendBtn;
3696
+ if (sendBtn && input) {
3697
+ try {
3698
+ const content = this.getEditableContent(input).trim();
3699
+ sendBtn.disabled = !content;
3700
+ } catch (error) {
3701
+ console.error('[assistantConcluded] ERROR getting editable content:', error);
3702
+ sendBtn.disabled = true;
3703
}
3704
}
3705
}
3734
chat.isProcessing = false;
3735
3736
// Clear stop-related flags when failure is handled
3470
- this.shouldStopProcessing = false;
3737
+ chat.shouldStopProcessing = false;
3738
chat.processingWasStoppedByUser = false;
3739
3740
// Clear current assistant group
3750
chat.updatedAt = new Date().toISOString();
3751
this.autoSave(chatId);
3752
3486
- // Re-enable input if it's the active chat and not rate limited
3487
- if (!isRateLimitHandled && chatId === this.getActiveChatId()) {
3753
+ // Re-enable input if not rate limited (always for the chat that failed)
3754
+ if (!isRateLimitHandled) {
3755
const container = this.getChatContainer(chatId);
3756
if (container && container._elements) {
3757
const input = container._elements.input;
3758
if (input) {
3492
- input.disabled = false;
3493
- input.focus();
3759
+ // Always re-enable contentEditable
3760
+ input.contentEditable = true;
3761
+
3762
+ // Only focus if this is the active chat
3763
+ if (chatId === this.getActiveChatId()) {
3764
+ input.focus();
3765
+ }
3766
}
3767
}
3768
}
3769
}
3770
3499
- showError(message, chatId, saveToMessages = true) {
3771
+ /**
3772
+ * Shows a professional confirmation dialog modal
3773
+ * @param {string} title - Dialog title
3774
+ * @param {string} message - Confirmation message
3775
+ * @param {string} confirmText - Text for confirm button (default: 'OK')
3776
+ * @param {string} cancelText - Text for cancel button (default: 'Cancel')
3777
+ * @param {boolean} isDanger - Whether this is a dangerous action (shows red confirm button)
3778
+ * @returns {Promise<boolean>} - Resolves to true if confirmed, false if cancelled
3779
+ */
3780
+ showConfirmDialog(title, message, confirmText = 'OK', cancelText = 'Cancel', isDanger = false) {
3781
+ return new Promise((resolve) => {
3782
+ // Create modal container
3783
+ const modal = document.createElement('div');
3784
+ modal.className = 'confirm-modal-container';
3785
+ modal.innerHTML = `
3786
+ <div class="confirm-modal-backdrop"></div>
3787
+ <div class="confirm-modal">
3788
+ <div class="confirm-modal-header">
3789
+ <h3>${this.escapeHtml(title)}</h3>
3790
+ </div>
3791
+ <div class="confirm-modal-body">
3792
+ <p>${this.escapeHtml(message)}</p>
3793
+ </div>
3794
+ <div class="confirm-modal-footer">
3795
+ <button class="btn btn-secondary confirm-cancel">${this.escapeHtml(cancelText)}</button>
3796
+ <button class="btn ${isDanger ? 'btn-danger' : 'btn-primary'} confirm-ok">${this.escapeHtml(confirmText)}</button>
3797
+ </div>
3798
+ </div>
3799
+ `;
3800
+
3801
+ document.body.appendChild(modal);
3802
+
3803
+ // Focus the confirm button
3804
+ const confirmBtn = modal.querySelector('.confirm-ok');
3805
+ const cancelBtn = modal.querySelector('.confirm-cancel');
3806
+ confirmBtn.focus();
3807
+
3808
+ // Define all functions using function declarations to avoid hoisting issues
3809
+ function cleanup() {
3810
+ modal.remove();
3811
+ document.removeEventListener('keydown', handleKeydown);
3812
+ }
3813
+
3814
+ function handleConfirm() {
3815
+ cleanup();
3816
+ resolve(true);
3817
+ }
3818
+
3819
+ function handleCancel() {
3820
+ cleanup();
3821
+ resolve(false);
3822
+ }
3823
+
3824
+ function handleKeydown(e) {
3825
+ if (e.key === 'Enter') {
3826
+ e.preventDefault();
3827
+ handleConfirm();
3828
+ } else if (e.key === 'Escape') {
3829
+ e.preventDefault();
3830
+ handleCancel();
3831
+ }
3832
+ }
3833
+
3834
+ // Add event listeners
3835
+ confirmBtn.addEventListener('click', handleConfirm);
3836
+ cancelBtn.addEventListener('click', handleCancel);
3837
+ modal.querySelector('.confirm-modal-backdrop').addEventListener('click', handleCancel);
3838
+ document.addEventListener('keydown', handleKeydown);
3839
+ });
3840
+ }
3841
+
3842
+ /**
3843
+ * Escapes HTML to prevent XSS
3844
+ */
3845
+ escapeHtml(text) {
3846
+ const div = document.createElement('div');
3847
+ div.textContent = text;
3848
+ return div.innerHTML;
3849
+ }
3850
+
3851
+ showError(message, chatId, saveToMessages = true, errorType = 'general') {
3852
// Log to console
3853
console.error('MCP Client Error:', message, chatId ? `(Chat ID: ${chatId})` : '(Global)');
3854
3872
if (chatId) {
3873
const chat = this.chats.get(chatId);
3874
3523
- // Save to messages if requested and chat exists
3524
- if (saveToMessages && chat) {
3525
- const lastUserMessageIndex = chat.messages.findLastIndex(m => m.role === 'user');
3526
- this.addMessage(chatId, {
3527
- role: 'error',
3528
- content: message,
3529
- errorMessageIndex: lastUserMessageIndex,
3530
- timestamp: new Date().toISOString()
3531
- });
3875
+ if (chat) {
3876
+ // Clear any spinners (from second method)
3877
+ this.clearSpinnerState(chatId);
3878
+
3879
+ // Save to messages if requested
3880
+ if (saveToMessages) {
3881
+ const lastUserMessageIndex = chat.messages.findLastIndex(m => m.role === 'user');
3882
+ this.addMessage(chatId, {
3883
+ role: 'error',
3884
+ content: message,
3885
+ errorMessageIndex: lastUserMessageIndex,
3886
+ timestamp: new Date().toISOString()
3887
+ });
3888
+ }
3889
3533
- // Set error state so the chat list shows the warning icon
3890
+ // Set error state with structured error object (enhanced from second method)
3891
chat.hasError = true;
3535
- chat.lastError = message;
3892
+ chat.lastError = {
3893
+ message,
3894
+ type: errorType,
3895
+ timestamp: Date.now()
3896
+ };
3897
+
3898
+ // Update UI
3899
this.updateChatSessions();
3900
+ this.updateChatTileStatus(chatId);
3901
}
3902
3903
const container = this.getChatContainer(chatId);
4325
}
4326
4327
clearLog() {
3964
- if (confirm('Clear all communication logs?')) {
3965
- this.communicationLog = [];
3966
- this.logContent.innerHTML = '';
3967
- }
4328
+ this.showConfirmDialog(
4329
+ 'Clear Communication Log',
4330
+ 'Are you sure you want to clear all communication logs?',
4331
+ 'Clear',
4332
+ 'Cancel'
4333
+ ).then(confirmed => {
4334
+ if (confirmed) {
4335
+ this.communicationLog = [];
4336
+ this.logContent.innerHTML = '';
4337
+ }
4338
+ });
4339
}
4340
4341
downloadLog() {
4585
// Auto-detect the proxy URL from the current origin
4586
const proxyUrl = window.location.origin;
4587
4217
- // console.log('Fetching models from:', `${proxyUrl}/models`);
4218
-
4588
try {
4589
// Fetch available models from the same origin
4590
const response = await fetch(`${proxyUrl}/models`);
4595
const data = await response.json();
4596
const providers = data.providers || {};
4597
4229
- // console.log('Received providers data from proxy:', providers);
4230
-
4598
if (Object.keys(providers).length === 0) {
4599
console.warn('No LLM providers configured in proxy');
4600
this.showNoModelsModal(proxyUrl);
4924
}
4925
4926
async removeMcpServer(serverId) {
4560
- if (confirm('Remove this MCP server?')) {
4927
+ const server = this.mcpServers.get(serverId);
4928
+ const serverName = server ? server.name : 'this MCP server';
4929
+
4930
+ const confirmed = await this.showConfirmDialog(
4931
+ 'Remove MCP Server',
4932
+ `Are you sure you want to remove "${serverName}"?`,
4933
+ 'Remove',
4934
+ 'Cancel',
4935
+ true // danger style
4936
+ );
4937
+
4938
+ if (confirmed) {
4939
// Disconnect if connected
4940
const connection = this.mcpConnections.get(serverId);
4941
if (connection) {
4975
// Check if there's an unsaved chat
4976
const unsavedChat = Array.from(this.chats.values()).find(chat => chat.isSaved === false);
4977
if (unsavedChat) {
4978
+ console.log(`[createNewChatDirectly] Found unsaved chat: ${unsavedChat.id}, will switch to it instead of creating new`);
4979
const activeChatId = this.getActiveChatId();
4601
- console.log('[createNewChatDirectly] Found unsaved chat:', unsavedChat.id, 'Active chat:', activeChatId);
4602
-
4980
+
4981
// Check if we're already in the unsaved chat
4982
if (activeChatId === unsavedChat.id) {
4983
// Already in the unsaved chat, just show toast
4984
this.showToast('Please use the current chat or save it by sending a message before creating a new one.');
4985
} else {
4986
// Switch to the unsaved chat instead of creating a new one
4609
- console.log('[createNewChatDirectly] Switching to unsaved chat:', unsavedChat.id);
4987
this.loadChat(unsavedChat.id);
4988
}
4989
return;
5051
5052
// Per-chat DOM management
5053
getChatContainer(chatId) {
4677
- if (!this.chatContainers.has(chatId)) {
4678
- const container = this.createChatDOM(chatId);
4679
- if (container) {
4680
- this.chatContainersEl.appendChild(container);
4681
- this.chatContainers.set(chatId, container);
4682
-
4683
- // Apply any pending connection state
4684
- const chat = this.chats.get(chatId);
4685
- if (chat && chat.pendingConnectionState) {
4686
- this.updateChatConnectionUI(chatId, chat.pendingConnectionState.state, chat.pendingConnectionState.details);
4687
- delete chat.pendingConnectionState;
4688
- }
5054
+ // All containers (including sub-chats) are now in chatContainers
5055
+ // No more temporary containers!
5056
+
5057
+ // First check if container already exists
5058
+ if (this.chatContainers.has(chatId)) {
5059
+ return this.chatContainers.get(chatId);
5060
+ }
5061
+
5062
+ // Container doesn't exist - need to create one
5063
+ const chat = this.chats.get(chatId);
5064
+ if (!chat) {
5065
+ console.error(`[getChatContainer] Chat ${chatId} not found`);
5066
+ return null;
5067
+ }
5068
+
5069
+ // Create container based on chat type
5070
+ if (chat.isSubChat) {
5071
+ // Sub-chats should ONLY have containers created by renderSubChatAsItem
5072
+ // If we're here, it means there's a sequencing error
5073
+ console.error(`[getChatContainer] CRITICAL: Attempted to get container for sub-chat ${chatId} before renderSubChatAsItem was called. This is a bug in the code flow.`);
5074
+ console.trace(); // Show stack trace to debug the issue
5075
+ return null; // Return null to fail fast
5076
+ }
5077
+
5078
+ // Regular chat - create full DOM
5079
+ const container = this.createChatDOM(chatId);
5080
+ if (container) {
5081
+ this.chatContainersEl.appendChild(container);
5082
+ this.chatContainers.set(chatId, container);
5083
+
5084
+ // Apply any pending connection state
5085
+ if (chat.pendingConnectionState) {
5086
+ this.updateChatConnectionUI(chatId, chat.pendingConnectionState.state, chat.pendingConnectionState.details);
5087
+ delete chat.pendingConnectionState;
5088
}
5089
}
4691
- return this.chatContainers.get(chatId);
5090
+
5091
+ return container;
5092
+ }
5093
+
5094
+ // Helper method to get the input element for a specific chat
5095
+ getChatInput(chatId) {
5096
+ const container = this.getChatContainer(chatId);
5097
+ if (container && container._elements && container._elements.input) {
5098
+ return container._elements.input;
5099
+ }
5100
+ return null;
5101
}
5102
5103
createChatDOM(chatId) {
5200
<div class="chat-input-container">
5201
<button class="reconnect-mcp-btn btn btn-primary" style="display: none;">Reconnect MCP Server</button>
5202
<div class="chat-input-wrapper">
4794
- <textarea
5203
+ <div
5204
class="chat-input"
4796
- placeholder="Ask about your Netdata metrics..."
4797
- rows="3"
4798
- ></textarea>
5205
+ contenteditable="true"
5206
+ data-placeholder="Ask about your Netdata metrics..."
5207
+ style="min-height: 4.5em; max-height: 200px; overflow-y: auto; white-space: pre-wrap;"
5208
+ ></div>
5209
<button class="send-message-btn btn btn-send">Send</button>
5210
</div>
5211
</div>
5265
5266
// Send button
5267
elements.sendBtn.addEventListener('click', () => {
4858
- if (this.isProcessing) {
4859
- // Stop processing
4860
- console.log('[Stop Button] Setting shouldStopProcessing = true');
4861
- this.shouldStopProcessing = true;
4862
- this.isProcessing = false;
4863
- this.updateSendButton();
4864
- this.chatInput.disabled = false;
5268
+ if (chat && chat.isProcessing) {
5269
+ // Stop processing for this specific chat
5270
+ chat.shouldStopProcessing = true;
5271
+ chat.isProcessing = false;
5272
+ this.updateSendButton(chatId);
5273
+ // Re-enable chat-specific input
5274
+ if (elements.input) {
5275
+ elements.input.contentEditable = true;
5276
+ } else if (!chat.isSubChat) {
5277
+ // Only log error for main chats - sub-chats don't have input elements
5278
+ console.error('[sendBtn.click] ERROR: Could not find input element when stopping processing for chat', chatId);
5279
+ }
5280
// Don't add a system message as it breaks message sequencing
5281
// The assistantFailed handler will take care of the UI feedback
5282
} else {
5290
5291
// Input field
5292
elements.input.addEventListener('input', (e) => {
4878
- // Save draft in memory only - don't update UI or save to storage on every keystroke
4879
- chat.draftMessage = e.target.value;
5293
+ // For contentEditable, get content while preserving formatting
5294
+ const content = this.getEditableContent(e.target);
5295
+ chat.draftMessage = content;
5296
5297
// Update send button state
4882
- elements.sendBtn.disabled = !e.target.value.trim();
5298
+ elements.sendBtn.disabled = !content.trim();
5299
5300
// Debounce saving to storage - save after 2 seconds of no typing
5301
if (this.draftSaveTimeout) {
5307
}, 2000);
5308
});
5309
5310
+ // Handle paste events to convert HTML to markdown immediately
5311
+ elements.input.addEventListener('paste', (e) => {
5312
+ e.preventDefault(); // Prevent default paste
5313
+
5314
+ // Get clipboard data
5315
+ const clipboardData = e.clipboardData || window.clipboardData;
5316
+ if (!clipboardData) return;
5317
+
5318
+ // Try to get HTML content first, fall back to plain text
5319
+ let content = clipboardData.getData('text/html');
5320
+ const hasHtml = content && content.trim() !== '';
5321
+
5322
+ if (!hasHtml) {
5323
+ // No HTML, just use plain text
5324
+ content = clipboardData.getData('text/plain');
5325
+ if (content) {
5326
+ // Insert plain text at cursor position
5327
+ document.execCommand('insertText', false, content);
5328
+ }
5329
+ return;
5330
+ }
5331
+
5332
+ // Convert HTML to markdown
5333
+ const markdown = this.convertHtmlToMarkdown(content);
5334
+
5335
+ // Insert markdown as plain text
5336
+ if (markdown) {
5337
+ document.execCommand('insertText', false, markdown);
5338
+ }
5339
+ });
5340
+
5341
// Enter to send
5342
elements.input.addEventListener('keydown', (e) => {
5343
if (e.key === 'Enter' && !e.shiftKey) {
5412
this.chatContainers.forEach(container => {
5413
const elements = container._elements;
5414
if (elements) {
4968
- if (elements.llmModelDropdown !== dropdownEl) {
5415
+ if (elements.llmModelDropdown && elements.llmModelDropdown !== dropdownEl) {
5416
elements.llmModelDropdown.style.display = 'none';
5417
}
4971
- if (elements.mcpServerDropdown !== dropdownEl) {
5418
+ if (elements.mcpServerDropdown && elements.mcpServerDropdown !== dropdownEl) {
5419
elements.mcpServerDropdown.style.display = 'none';
5420
}
5421
}
5430
if (this.pendingNewChatId === chatId && this.userHasSelectedChat) {
5431
const activeChatId = this.getActiveChatId();
5432
if (activeChatId && activeChatId !== chatId) {
4986
- console.log('Blocking DOM switch to new chat - user already selected:', activeChatId);
5433
return;
5434
}
5435
}
5441
5442
// Hide all chat containers
5443
this.chatContainers.forEach((container, id) => {
4998
- container.classList.remove('active');
5444
+ if (container && container.classList) {
5445
+ container.classList.remove('active');
5446
+ }
5447
const chat = this.chats.get(id);
5448
if (chat) {
5449
chat.isActive = false;
5460
if (chat) {
5461
chat.isActive = true;
5462
5015
- // Update global references for compatibility
5016
- this.chatInput = container._elements.input;
5463
+ // No longer need to update global chatInput reference
5464
this.sendMessageBtn = container._elements.sendBtn;
5465
this.reconnectMcpBtn = container._elements.reconnectBtn;
5466
this.chatTitle = container._elements.title;
5528
const uniqueModels = [...new Set(modelsInUse.values())];
5529
const modelDisplay = uniqueModels.join(' / ');
5530
5084
- // Create tooltip content
5085
- const tooltipLines = [];
5086
- modelsInUse.forEach((model, purpose) => {
5087
- tooltipLines.push(`${purpose.padEnd(10)} | ${model}`);
5088
- });
5089
-
5531
// Update model display in the header
5532
const modelNameSpan = elements.llmMeta.querySelector('.model-name');
5533
if (modelNameSpan) {
5595
if (mcpConnection && mcpConnection.isReady()) {
5596
// Only enable if connection ready AND chat not busy
5597
if (!chat.isProcessing && !chat.spinnerState) {
5157
- elements.input.disabled = false;
5598
+ elements.input.contentEditable = true;
5599
elements.sendBtn.disabled = false;
5159
- elements.input.placeholder = 'Ask about your Netdata metrics...';
5600
+ elements.input.setAttribute('data-placeholder', 'Ask about your Netdata metrics...');
5601
elements.reconnectBtn.style.display = 'none';
5602
5603
// Focus input if this is the active chat
5606
}
5607
} else {
5608
// Chat is busy - keep input disabled but hide reconnect button
5168
- elements.input.disabled = true;
5609
+ elements.input.contentEditable = false;
5610
elements.sendBtn.disabled = true;
5170
- elements.input.placeholder = 'Processing...';
5611
+ elements.input.setAttribute('data-placeholder', 'Processing...');
5612
elements.reconnectBtn.style.display = 'none';
5613
}
5614
5632
let title = options.title || '';
5633
const isSaved = options.isSaved !== undefined ? options.isSaved : true;
5634
5635
+ // Sub-chat support
5636
+ const parentChatId = options.parentChatId || null;
5637
+ const parentToolCallId = options.parentToolCallId || null;
5638
+ const toolMetadata = options.toolMetadata || null;
5639
+
5640
if (!mcpServerId || !llmProviderId) {
5641
this.showError('Cannot create chat: Missing MCP server or LLM provider', null);
5642
return;
5704
// Per-chat assistant group tracking (DOM element for grouping messages)
5705
currentAssistantGroup: null,
5706
// Per-chat pending tool calls map
5261
- pendingToolCalls: new Map()
5707
+ pendingToolCalls: new Map(),
5708
+ // Sub-chat support
5709
+ parentChatId,
5710
+ isSubChat: !!parentChatId,
5711
+ parentToolCallId,
5712
+ toolMetadata
5713
};
5714
5715
// Create isolated MessageOptimizer instance for this chat
5727
model: selectedModel
5728
});
5729
5279
- // Save the config for next time
5280
- ChatConfig.saveLastConfig(chatConfig);
5730
+ // Save the config for next time (but not for sub-chats)
5731
+ if (!chat.isSubChat) {
5732
+ ChatConfig.saveLastConfig(chatConfig);
5733
+ }
5734
5735
// Only save settings if this is a saved chat
5736
if (isSaved) {
5783
}
5784
5785
const sortedChats = Array.from(this.chats.values())
5786
+ .filter(chat => !chat.isSubChat) // Hide sub-chats from sidebar
5787
.sort((a, b) => {
5788
// Show unsaved chats first
5789
if (a.isSaved === false && b.isSaved !== false) {return -1;}
6020
6021
// If user has already selected a chat and this is the auto-created new chat trying to load, ignore it
6022
if (this.userHasSelectedChat && this.pendingNewChatId === chatId && this.getActiveChatId() !== chatId) {
5569
- console.log('Blocking auto-load of new chat because user already selected a different chat');
6023
return;
6024
}
6025
6028
6029
// Cancel any pending new chat load if this is a different chat
6030
if (this.pendingNewChatId && this.pendingNewChatId !== chatId) {
5578
- console.log('User selected different chat, cancelling new chat load');
6031
this.pendingNewChatLoad = false;
6032
// Don't clear pendingNewChatId here - we need it to block the switch
6033
6074
6075
// Migrate old chat data if needed
6076
if (!chat.totalTokensPrice || !chat.perModelTokensPrice) {
5625
- // console.log('[loadChat] Migrating token pricing for chat:', chatId);
6077
this.migrateTokenPricing(chat);
6078
}
6079
5629
- // Log the totalTokensPrice after migration
5630
- // console.log('[loadChat] totalTokensPrice after migration:', chat.totalTokensPrice);
5631
-
6080
// Initialize token usage history for this chat if it doesn't exist
6081
if (!this.tokenUsageHistory.has(chatId)) {
6082
this.tokenUsageHistory.set(chatId, {
6131
if (mcpConnection && mcpConnection.isReady()) {
6132
// Only enable if connection ready AND chat not busy
6133
if (!chat.isProcessing && !chat.spinnerState) {
5686
- elements.input.disabled = false;
6134
+ elements.input.contentEditable = true;
6135
elements.sendBtn.disabled = false;
5688
- elements.input.placeholder = 'Ask about your Netdata metrics...';
6136
+ elements.input.setAttribute('data-placeholder', 'Ask about your Netdata metrics...');
6137
elements.reconnectBtn.style.display = 'none';
6138
} else {
6139
// Chat is busy - keep input disabled
5692
- elements.input.disabled = true;
6140
+ elements.input.contentEditable = false;
6141
elements.sendBtn.disabled = true;
5694
- elements.input.placeholder = 'Processing...';
6142
+ elements.input.setAttribute('data-placeholder', 'Processing...');
6143
elements.reconnectBtn.style.display = 'none';
6144
}
6145
6146
} else {
6147
// Connection exists but not ready yet
5700
- elements.input.disabled = true;
6148
+ elements.input.contentEditable = false;
6149
elements.sendBtn.disabled = true;
5702
- elements.input.placeholder = 'Connecting to MCP server...';
6150
+ elements.input.setAttribute('data-placeholder', 'Connecting to MCP server...');
6151
elements.reconnectBtn.style.display = 'none';
6152
6153
// Check again in a moment
6159
}
6160
} else {
6161
// No connection yet - try to establish it
5714
- elements.input.disabled = true;
6162
+ elements.input.contentEditable = false;
6163
elements.sendBtn.disabled = true;
5716
- elements.input.placeholder = 'Connecting to MCP server...';
6164
+ elements.input.setAttribute('data-placeholder', 'Connecting to MCP server...');
6165
elements.reconnectBtn.style.display = 'none';
6166
6167
// Try to establish connection
6176
// Connection failed
6177
console.error('Failed to connect to MCP server:', error);
6178
if (this.getActiveChatId() === chatId) {
5731
- elements.input.placeholder = 'MCP server connection failed - click Reconnect';
6179
+ elements.input.setAttribute('data-placeholder', 'MCP server connection failed - click Reconnect');
6180
elements.reconnectBtn.style.display = 'block';
6181
}
6182
});
6183
}
6184
} else {
5737
- elements.input.disabled = true;
6185
+ elements.input.contentEditable = false;
6186
elements.sendBtn.disabled = true;
6187
6188
if (!server) {
5741
- elements.input.placeholder = 'MCP server not found';
6189
+ elements.input.setAttribute('data-placeholder', 'MCP server not found');
6190
} else if (!provider) {
5743
- elements.input.placeholder = 'LLM provider not found';
6191
+ elements.input.setAttribute('data-placeholder', 'LLM provider not found');
6192
} else {
5745
- elements.input.placeholder = 'MCP server or LLM provider not available';
6193
+ elements.input.setAttribute('data-placeholder', 'MCP server or LLM provider not available');
6194
}
6195
}
6196
6206
currentStepInTurn: 1
6207
};
6208
}
5761
- this.currentContextWindow = 0; // Reset context window counter for delta calculation
6209
+ // Reset context window counter for delta calculation (stored per chat)
6210
+ chat.currentContextWindow = 0;
6211
6212
// Check if we need to re-render messages
6213
// Re-render if: 1) Never rendered before, 2) DOM is empty (switched from another chat), 3) Force render requested
6368
6369
// Mark chat as rendered
6370
chat.hasBeenRendered = true;
6371
+
6372
+ // For loaded chats, sub-chats are already rendered as part of tool results
6373
+ // via the sub-chat-indicator in addToolResult, so we don't need to render them separately
6374
}
6375
6376
// Update global toggle UI based on chat's tool inclusion mode
6403
6404
// Focus the chat input after scrolling
6405
const chatInput = container && container._elements && container._elements.input;
5954
- if (chatInput && !chatInput.disabled) {
6406
+ if (chatInput && chatInput.contentEditable === 'true') {
6407
chatInput.focus();
6408
}
6409
6410
// Restore draft message if exists
6411
if (chatInput && chat.draftMessage) {
5960
- chatInput.value = chat.draftMessage;
6412
+ // Convert markdown to HTML for contentEditable display
6413
+ const htmlContent = chat.draftMessage
6414
+ .replace(/&/g, '&')
6415
+ .replace(/</g, '<')
6416
+ .replace(/>/g, '>')
6417
+ .replace(/\n/g, '<br>');
6418
+ chatInput.innerHTML = htmlContent;
6419
+
6420
// Update send button state
6421
const sendBtn = container._elements.sendBtn;
6422
if (sendBtn) {
6542
name: result.name || result.toolName,
6543
result: result.result,
6544
toolCallId: result.toolCallId, // Required tool call ID for matching
6086
- includeInContext: result.includeInContext
6545
+ includeInContext: result.includeInContext,
6546
+ subChatId: result.subChatId, // Include sub-chat ID if present
6547
+ wasProcessedBySubChat: result.wasProcessedBySubChat // Include processing status
6548
});
6549
}
6550
// Reset assistant group after tool results
6656
break;
6657
6658
case 'tool-result':
6198
- this.addToolResult(event.name, event.result, chatId, event.responseTime || 0, event.responseSize || null, event.includeInContext, event.messageIndex, event.toolCallId);
6659
+ // Check if we need to render sub-chat DOM for loaded chats
6660
+ if (event.subChatId) {
6661
+ const subChat = this.chats.get(event.subChatId);
6662
+ if (!subChat) {
6663
+ console.error(`[processRenderEvent] Sub-chat not found for subChatId: ${event.subChatId} in tool-result event for chatId: ${chatId}, toolCallId: ${event.toolCallId}`);
6664
+ break;
6665
+ }
6666
+ if (subChat) {
6667
+ // Check if sub-chat DOM already exists
6668
+ const container = this.getChatContainer(chatId);
6669
+ const existingSubChatDom = container && container._elements.messages.querySelector(`[data-sub-chat-id="${event.subChatId}"]`);
6670
+
6671
+ if (!existingSubChatDom) {
6672
+ console.log(`[processRenderEvent] Sub-chat DOM not found for ${event.subChatId}, creating it now`);
6673
+ // Determine status based on whether the sub-chat successfully processed the tool
6674
+ const status = event.wasProcessedBySubChat ? 'success' : 'failed';
6675
+ // Render sub-chat DOM element
6676
+ this.renderSubChatAsItem(chatId, event.subChatId, event.toolCallId, status);
6677
+ } else {
6678
+ console.log(`[processRenderEvent] Sub-chat DOM already exists for ${event.subChatId}, skipping duplicate creation`);
6679
+ }
6680
+ }
6681
+ }
6682
+
6683
+ this.addToolResult(event.name, event.result, chatId, event.responseTime || 0, event.responseSize || null, event.includeInContext, event.messageIndex, event.toolCallId, event.subChatId);
6684
break;
6685
6686
6755
};
6756
6757
messageDiv.appendChild(retryBtn);
6273
- } else if (event.errorType !== 'safety_limit' && (event.errorMessageIndex !== undefined && event.errorMessageIndex >= 0)) {
6274
- // This error has context - use redo button
6758
+ } else if (!chat.isSubChat && event.errorType !== 'safety_limit' && (event.errorMessageIndex !== undefined && event.errorMessageIndex >= 0)) {
6759
+ // This error has context - use redo button (but not in sub-chats)
6760
const redoBtn = document.createElement('button');
6761
redoBtn.className = 'redo-button';
6762
redoBtn.textContent = 'Redo';
6782
6783
// Remove the error message from chat
6784
const currentChat = this.chats.get(retryChatId);
6300
- if (!currentChat) return;
6785
+ if (!currentChat) {
6786
+ console.error(`[processRenderEvent] Chat not found for retry operation, retryChatId: ${retryChatId}`);
6787
+ return;
6788
+ }
6789
6790
const errorIndex = currentChat.messages.findIndex(m => m.role === 'error' && m.content === event.content);
6791
if (errorIndex !== -1) {
6872
this.moveSpinnerToBottom(chatId);
6873
}
6874
6387
- deleteChat(chatId) {
6875
+ async deleteChat(chatId) {
6876
if (!chatId) {return;}
6877
6878
const chat = this.chats.get(chatId);
6879
if (!chat) {return;}
6880
6393
- if (confirm(`Delete chat "${chat.title}"?`)) {
6881
+ const confirmed = await this.showConfirmDialog(
6882
+ 'Delete Chat',
6883
+ `Are you sure you want to delete "${chat.title}"?`,
6884
+ 'Delete',
6885
+ 'Cancel',
6886
+ true // danger style
6887
+ );
6888
+
6889
+ if (confirmed) {
6890
+ // Delete sub-chats first (cascade delete)
6891
+ for (const [subChatId, subChat] of this.chats) {
6892
+ if (subChat.parentChatId === chatId) {
6893
+ // Clean up sub-chat container
6894
+ if (this.chatContainers.has(subChatId)) {
6895
+ this.chatContainers.delete(subChatId);
6896
+ }
6897
+ this.chats.delete(subChatId);
6898
+ localStorage.removeItem(subChatId);
6899
+ localStorage.removeItem(`chatConfig_${subChatId}`);
6900
+ }
6901
+ }
6902
+
6903
this.chats.delete(chatId);
6904
6905
// Remove from storage
6980
}
6981
6982
// Update send button appearance based on processing state
6474
- updateSendButton() {
6475
- if (this.isProcessing) {
6983
+ updateSendButton(chatId) {
6984
+ const chat = this.chats.get(chatId);
6985
+ if (chat && chat.isProcessing) {
6986
this.sendMessageBtn.textContent = 'Stop';
6987
this.sendMessageBtn.classList.remove('btn-send');
6988
this.sendMessageBtn.classList.add('btn-danger');
6991
this.sendMessageBtn.textContent = 'Send';
6992
this.sendMessageBtn.classList.remove('btn-danger');
6993
this.sendMessageBtn.classList.add('btn-send');
6484
- this.sendMessageBtn.disabled = !this.chatInput.value.trim();
6994
+ // Get chat-specific input
6995
+ const chatInput = this.getChatInput(chatId);
6996
+ if (!chatInput) {
6997
+ const chat = this.chats.get(chatId);
6998
+ if (!chat || !chat.isSubChat) {
6999
+ // Only log error for main chats - sub-chats don't have input elements
7000
+ console.error('[updateSendButton] ERROR: Could not find input for chat', chatId);
7001
+ }
7002
+ this.sendMessageBtn.disabled = true;
7003
+ } else {
7004
+ try {
7005
+ const content = this.getEditableContent(chatInput);
7006
+ this.sendMessageBtn.disabled = !content.trim();
7007
+ } catch (error) {
7008
+ console.error('[updateSendButton] ERROR getting editable content:', error);
7009
+ this.sendMessageBtn.disabled = true;
7010
+ }
7011
+ }
7012
}
7013
}
7014
7017
// If no message provided, get it from the input
7018
let message = messageParam;
7019
if (message === null) {
6493
- message = this.chatInput.value.trim();
6494
- if (!message && !isResume) {return;}
7020
+ // Get chat-specific input
7021
+ const chatInput = this.getChatInput(chatId);
7022
+ if (!chatInput) {
7023
+ if (!chat.isSubChat) {
7024
+ // Only log error for main chats - sub-chats don't have input elements
7025
+ console.error('[sendMessage] ERROR: Could not find input for chat', chatId);
7026
+ this.showError('Chat input not found', chatId);
7027
+ return;
7028
+ }
7029
+ // For sub-chats, continue without input element
7030
+ }
7031
+
7032
+ // For contentEditable input, extract formatted content
7033
+ try {
7034
+ message = this.getEditableContent(chatInput).trim();
7035
+ if (!message && !isResume) {return;}
7036
+ } catch (error) {
7037
+ console.error('[sendMessage] ERROR extracting message content:', error);
7038
+ this.showError('Failed to get message content', chatId);
7039
+ return;
7040
+ }
7041
}
7042
7043
const chat = this.chats.get(chatId);
7081
this.updateChatSessions(); // Update UI to remove draft indicator
7082
7083
// Disable input and update button to Stop
6538
- if (!isResume) {
6539
- this.chatInput.value = '';
7084
+ // Get chat-specific input element (only for main chats, not sub-chats)
7085
+ const container = this.getChatContainer(chatId);
7086
+ if (container && container._elements && container._elements.input) {
7087
+ const input = container._elements.input;
7088
+ if (!isResume) {
7089
+ input.innerHTML = '';
7090
+ }
7091
+ input.contentEditable = false;
7092
+ } else if (!chat.isSubChat) {
7093
+ // Only log error for main chats - sub-chats don't have input elements
7094
+ console.error('[sendMessage] ERROR: Could not find chat input element for chat', chatId);
7095
}
6541
- this.chatInput.disabled = true;
6542
- this.isProcessing = true;
6543
- this.shouldStopProcessing = false;
6544
- this.updateSendButton();
7096
+ chat.isProcessing = true;
7097
+ chat.shouldStopProcessing = false;
7098
+ this.updateSendButton(chatId);
7099
7100
// Only add user message if this is not a resume (resume continues from existing messages)
7101
if (!isResume) {
7103
chat.currentTurn = (chat.currentTurn || 0) + 1;
7104
7105
// CRITICAL: Add and display the user's message immediately for better UX
6552
- this.addMessage(chat.id, { role: 'user', content: message, turn: chat.currentTurn });
7106
+ // Add stable timestamp to ensure cache control works properly
7107
+ this.addMessage(chat.id, {
7108
+ role: 'user',
7109
+ content: message,
7110
+ turn: chat.currentTurn,
7111
+ timestamp: new Date().toISOString()
7112
+ });
7113
const userMessageIndex = chat.messages.length - 1;
7114
this.processRenderEvent({ type: 'user-message', content: message, messageIndex: userMessageIndex }, chat.id);
7115
}
7136
} catch (error) {
7137
this.showError(`Failed to connect to MCP server: ${error.message}`, chat.id);
7138
// Re-enable input so user can try again
6579
- this.chatInput.disabled = false;
6580
- this.isProcessing = false;
6581
- this.updateSendButton();
7139
+ const errorContainer = this.getChatContainer(chatId);
7140
+ if (errorContainer && errorContainer._elements && errorContainer._elements.input) {
7141
+ errorContainer._elements.input.contentEditable = true;
7142
+ } else if (!chat.isSubChat) {
7143
+ // Only log error for main chats - sub-chats don't have input elements
7144
+ console.error('[sendMessage] ERROR: Could not find chat input element when re-enabling after MCP error for chat', chatId);
7145
+ }
7146
+ chat.isProcessing = false;
7147
+ this.updateSendButton(chatId);
7148
return;
7149
}
7150
7174
const lastMessage = chat.messages[chat.messages.length - 1];
7175
const hasCompleteSequence = lastMessage && lastMessage.role === 'assistant';
7176
6611
- if (!this.shouldStopProcessing && hasCompleteSequence && this.isFirstUserMessage(chat) && TitleGenerator.shouldGenerateTitleAutomatically(chat)) {
7177
+ if (!chat.shouldStopProcessing && hasCompleteSequence && this.isFirstUserMessage(chat) && TitleGenerator.shouldGenerateTitleAutomatically(chat)) {
7178
const llmProxy = this.llmProviders.get(chat.llmProviderId);
7179
if (llmProxy) {
7180
const titleProvider = TitleGenerator.getTitleGenerationProvider(
7209
// Success - assistant has concluded
7210
this.assistantConcluded(chat.id);
7211
6646
- // Reset global processing state
6647
- this.isProcessing = false;
6648
- this.shouldStopProcessing = false;
7212
+ // Reset processing state for this chat
7213
+ chat.isProcessing = false;
7214
+ chat.shouldStopProcessing = false;
7215
7216
// Re-enable send button if the input has text
6651
- if (this.chatInput && this.chatInput.value.trim()) {
6652
- this.sendBtn.disabled = false;
7217
+ const chatInput = this.getChatInput(chatId);
7218
+ if (chatInput) {
7219
+ try {
7220
+ const content = this.getEditableContent(chatInput).trim();
7221
+ if (content && this.sendBtn) {
7222
+ this.sendBtn.disabled = false;
7223
+ }
7224
+ } catch (error) {
7225
+ console.error('[sendMessage] ERROR checking input content:', error);
7226
+ }
7227
+ } else if (!chat.isSubChat) {
7228
+ // Only log warning for main chats - sub-chats don't have input elements
7229
+ console.error('[sendMessage] WARNING: Could not find input for chat when trying to re-enable send button', chatId);
7230
}
7231
} catch (error) {
7232
// Check if the user stopped processing
6656
- if (this.shouldStopProcessing || chat.processingWasStoppedByUser || error.isUserStop) {
6657
- console.log('[Stop Detection] Flags:', {
6658
- shouldStopProcessing: this.shouldStopProcessing,
6659
- processingWasStoppedByUser: chat.processingWasStoppedByUser,
6660
- isUserStop: error.isUserStop
6661
- });
6662
-
7233
+ if (chat.shouldStopProcessing || chat.processingWasStoppedByUser || error.isUserStop) {
7234
// Clear the flag for next time
7235
chat.processingWasStoppedByUser = false;
7236
7312
}
7313
}
7314
6744
- // Reset global processing state (for all cases)
6745
- this.isProcessing = false;
6746
- this.shouldStopProcessing = false;
6747
- this.updateSendButton();
7315
+ // Reset processing state for this chat (for all cases)
7316
+ chat.isProcessing = false;
7317
+ chat.shouldStopProcessing = false;
7318
+ this.updateSendButton(chatId);
7319
}
7320
}
7321
7403
const mcpInstructions = mcpConnection && mcpConnection.instructions ? mcpConnection.instructions : null;
7404
7405
// Delegate to the chat's MessageOptimizer
6835
- const result = chat.messageOptimizer.buildMessagesForAPI(chat, freezeCache, mcpInstructions);
6836
-
6837
- // Log optimization stats if available
6838
- if (result.stats) {
6839
- // console.log(`[buildMessagesForAPI] Optimization stats for chat ${chat.id}:`, result.stats);
6840
- }
6841
-
6842
- return result;
7406
+ return chat.messageOptimizer.buildMessagesForAPI(chat, freezeCache, mcpInstructions);
7407
} catch (error) {
7408
console.error('[buildMessagesForAPI] MessageOptimizer failed:', error);
7409
this.showError(`Message optimization failed: ${error.message}`, chat.id);
7419
}
7420
7421
// Clear any stop flags
6858
- this.shouldStopProcessing = false;
7422
+ chat.shouldStopProcessing = false;
7423
chat.processingWasStoppedByUser = false;
7424
7425
// Simply call sendMessage without adding a new user message
7461
while (true) {
7462
// attempts++;
7463
6900
- // Check if we should stop processing
6901
- if (this.shouldStopProcessing) {
7464
+ // Check if we should stop processing for this chat
7465
+ if (chat.shouldStopProcessing) {
7466
// Mark that processing was stopped in the chat object
7467
chat.processingWasStoppedByUser = true;
7468
// Throw an error to trigger the catch block in sendMessage
7590
messageDiv.className = `message ${role}`;
7591
}
7592
7029
- // Add redo button only for user and assistant messages
7030
- if (messageIndex !== undefined && (role === 'user' || role === 'assistant')) {
7593
+ // Add redo button only for user and assistant messages (but not in sub-chats)
7594
+ const chat = this.chats.get(chatId);
7595
+ const isSubChat = chat && chat.isSubChat;
7596
+
7597
+ if (!isSubChat && messageIndex !== undefined && (role === 'user' || role === 'assistant')) {
7598
const redoBtn = document.createElement('button');
7599
redoBtn.className = 'redo-button';
7600
redoBtn.textContent = 'Redo';
7746
deleteBtn.style.marginLeft = '10px';
7747
deleteBtn.onclick = (e) => {
7748
e.stopPropagation(); // Prevent header toggle
7182
- const chat = this.chats.get(chatId);
7183
- if (chat) {
7749
+ const summaryChat = this.chats.get(chatId);
7750
+ if (summaryChat) {
7751
// Find the most recent summary message
7185
- for (let i = chat.messages.length - 1; i >= 0; i--) {
7186
- if (chat.messages[i]?.role === 'summary') {
7187
- if (confirm('Delete this summary and replace with accounting record?')) {
7188
- this.deleteSummaryMessages(i, chatId);
7189
- }
7752
+ for (let i = summaryChat.messages.length - 1; i >= 0; i--) {
7753
+ if (summaryChat.messages[i]?.role === 'summary') {
7754
+ this.showConfirmDialog(
7755
+ 'Delete Summary',
7756
+ 'Delete this summary and replace with accounting record?',
7757
+ 'Delete',
7758
+ 'Cancel',
7759
+ true // danger style
7760
+ ).then(confirmed => {
7761
+ if (confirmed) {
7762
+ this.deleteSummaryMessages(i, chatId);
7763
+ }
7764
+ });
7765
break;
7766
}
7767
}
8303
const container = this.getChatContainer(chatId);
8304
if (!container || !container._elements) {return;}
8305
8306
+ const chat = this.chats.get(chatId);
8307
+ if (!chat) {
8308
+ console.error('appendMetricsToChat: chat not found for chatId', chatId);
8309
+ return;
8310
+ }
8311
+
8312
const chatMessages = container._elements.messages;
8313
8314
const metricsFooter = document.createElement('div');
8364
// Don't update currentContextWindow
8365
} else if (messageType === 'summary') {
8366
// Summaries reset context to just their output tokens
7786
- deltaTokens = (usage.completionTokens || 0) - this.currentContextWindow;
8367
+ deltaTokens = (usage.completionTokens || 0) - (chat.currentContextWindow || 0);
8368
// Reset context window to just the summary's output
7788
- this.currentContextWindow = usage.completionTokens || 0;
8369
+ chat.currentContextWindow = usage.completionTokens || 0;
8370
} else {
8371
// Regular assistant messages
7791
- deltaTokens = totalTokens - this.currentContextWindow;
8372
+ deltaTokens = totalTokens - (chat.currentContextWindow || 0);
8373
// Update the running total
7793
- this.currentContextWindow = totalTokens;
8374
+ chat.currentContextWindow = totalTokens;
8375
}
8376
8377
// Always show delta, even if zero
8393
8394
8395
8396
+ /**
8397
+ * Extracts content from a contentEditable div while preserving formatting
8398
+ * Converts HTML to markdown format to preserve visual formatting
8399
+ */
8400
+ getEditableContent(contentDiv) {
8401
+ // CRITICAL: Add error checking for undefined contentDiv
8402
+ if (!contentDiv) {
8403
+ console.error('[getEditableContent] ERROR: contentDiv is undefined or null');
8404
+ return '';
8405
+ }
8406
+
8407
+ // Get the HTML content
8408
+ let htmlContent = contentDiv.innerHTML || '';
8409
+
8410
+ // Convert HTML tables to markdown tables before other processing
8411
+ htmlContent = this.convertHtmlTablesToMarkdown(htmlContent);
8412
+
8413
+ // Convert HTML to markdown-like format to preserve formatting
8414
+ return htmlContent
8415
+ // Convert headers (h1-h6)
8416
+ .replace(/<h1[^>]*>(.*?)<\/h1>/gi, '\n# $1\n')
8417
+ .replace(/<h2[^>]*>(.*?)<\/h2>/gi, '\n## $1\n')
8418
+ .replace(/<h3[^>]*>(.*?)<\/h3>/gi, '\n### $1\n')
8419
+ .replace(/<h4[^>]*>(.*?)<\/h4>/gi, '\n#### $1\n')
8420
+ .replace(/<h5[^>]*>(.*?)<\/h5>/gi, '\n##### $1\n')
8421
+ .replace(/<h6[^>]*>(.*?)<\/h6>/gi, '\n###### $1\n')
8422
+
8423
+ // Convert bold and strong
8424
+ .replace(/<(b|strong)[^>]*>(.*?)<\/(b|strong)>/gi, '**$2**')
8425
+
8426
+ // Convert italic and emphasis
8427
+ .replace(/<(i|em)[^>]*>(.*?)<\/(i|em)>/gi, '*$2*')
8428
+
8429
+ // Convert underline to emphasis (markdown doesn't have underline)
8430
+ .replace(/<u[^>]*>(.*?)<\/u>/gi, '*$1*')
8431
+
8432
+ // Convert strikethrough
8433
+ .replace(/<(s|strike|del)[^>]*>(.*?)<\/(s|strike|del)>/gi, '~~$2~~')
8434
+
8435
+ // Convert code blocks
8436
+ .replace(/<pre[^>]*><code[^>]*>(.*?)<\/code><\/pre>/gi, '\n```\n$1\n```\n')
8437
+
8438
+ // Convert inline code
8439
+ .replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`')
8440
+
8441
+ // Convert ordered lists
8442
+ .replace(/<ol[^>]*>/gi, '\n')
8443
+ .replace(/<\/ol>/gi, '\n')
8444
+
8445
+ // Convert unordered lists
8446
+ .replace(/<ul[^>]*>/gi, '\n')
8447
+ .replace(/<\/ul>/gi, '\n')
8448
+
8449
+ // Convert list items
8450
+ .replace(/<li[^>]*>(.*?)<\/li>/gi, (match, content) => {
8451
+ // Check if it's within an ordered list by looking at context
8452
+ // For now, use bullet points for all lists
8453
+ return '- ' + content.trim() + '\n';
8454
+ })
8455
+
8456
+ // Convert blockquotes
8457
+ .replace(/<blockquote[^>]*>(.*?)<\/blockquote>/gi, '\n> $1\n')
8458
+
8459
+ // Convert horizontal rules
8460
+ .replace(/<hr[^>]*>/gi, '\n---\n')
8461
+
8462
+ // Convert line breaks
8463
+ .replace(/<br\s*\/?>/gi, '\n')
8464
+
8465
+ // Convert paragraphs
8466
+ .replace(/<p[^>]*>/gi, '\n')
8467
+ .replace(/<\/p>/gi, '\n')
8468
+
8469
+ // Convert divs (contentEditable creates these)
8470
+ .replace(/<div[^>]*>/gi, '\n')
8471
+ .replace(/<\/div>/gi, '')
8472
+
8473
+ // Convert links
8474
+ .replace(/<a[^>]+href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi, '[$2]($1)')
8475
+
8476
+ // Replace non-breaking spaces
8477
+ .replace(/ /gi, ' ')
8478
+
8479
+ // Remove any remaining HTML tags
8480
+ .replace(/<[^>]*>/g, '')
8481
+
8482
+ // Decode HTML entities
8483
+ .replace(/</g, '<')
8484
+ .replace(/>/g, '>')
8485
+ .replace(/&/g, '&')
8486
+ .replace(/"/g, '"')
8487
+ .replace(/'/g, "'")
8488
+ .replace(/'/g, "'")
8489
+ .replace(/&#(\d+);/g, (match, dec) => String.fromCharCode(dec))
8490
+ .replace(/&#x([a-f0-9]+);/gi, (match, hex) => String.fromCharCode(parseInt(hex, 16)))
8491
+
8492
+ // Clean up excessive whitespace
8493
+ .replace(/\n\s*\n\s*\n/g, '\n\n')
8494
+ .replace(/[ \t]+$/gm, '')
8495
+ .trim();
8496
+ }
8497
+
8498
+ /**
8499
+ * Converts HTML tables to markdown tables
8500
+ */
8501
+ convertHtmlTablesToMarkdown(html) {
8502
+ // Find all tables and convert them
8503
+ return html.replace(/<table[^>]*>([\s\S]*?)<\/table>/gi, (match, tableContent) => {
8504
+ try {
8505
+ // Parse the table content
8506
+ const rows = [];
8507
+ const tableRows = tableContent.match(/<tr[^>]*>([\s\S]*?)<\/tr>/gi) || [];
8508
+
8509
+ tableRows.forEach((tr, _rowIndex) => {
8510
+ const cells = [];
8511
+ // Match both th and td tags
8512
+ const cellMatches = tr.match(/<(th|td)[^>]*>([\s\S]*?)<\/(th|td)>/gi) || [];
8513
+
8514
+ cellMatches.forEach(cell => {
8515
+ // Extract cell content
8516
+ const cellContent = cell
8517
+ .replace(/<(th|td)[^>]*>/gi, '')
8518
+ .replace(/<\/(th|td)>/gi, '')
8519
+ .replace(/<br\s*\/?>/gi, ' ')
8520
+ .replace(/<[^>]*>/g, '')
8521
+ .replace(/ /gi, ' ')
8522
+ .trim();
8523
+ cells.push(cellContent);
8524
+ });
8525
+
8526
+ if (cells.length > 0) {
8527
+ rows.push(cells);
8528
+ }
8529
+ });
8530
+
8531
+ if (rows.length === 0) {
8532
+ return '';
8533
+ }
8534
+
8535
+ // Build markdown table
8536
+ let markdownTable = '\n\n';
8537
+
8538
+ // Add header row
8539
+ markdownTable += '| ' + rows[0].join(' | ') + ' |\n';
8540
+
8541
+ // Add separator row
8542
+ markdownTable += '|' + rows[0].map(() => ' --- ').join('|') + '|\n';
8543
+
8544
+ // Add data rows
8545
+ for (let i = 1; i < rows.length; i++) {
8546
+ // Ensure the row has the same number of columns as the header
8547
+ while (rows[i].length < rows[0].length) {
8548
+ rows[i].push('');
8549
+ }
8550
+ markdownTable += '| ' + rows[i].join(' | ') + ' |\n';
8551
+ }
8552
+
8553
+ markdownTable += '\n';
8554
+
8555
+ return markdownTable;
8556
+ } catch (error) {
8557
+ console.error('[convertHtmlTablesToMarkdown] Error converting table:', error);
8558
+ // Return the original table HTML if conversion fails
8559
+ return match;
8560
+ }
8561
+ });
8562
+ }
8563
+
8564
+ /**
8565
+ * Comprehensive HTML to Markdown converter
8566
+ * Handles full HTML documents from clipboard
8567
+ */
8568
+ convertHtmlToMarkdown(html) {
8569
+ let result = html;
8570
+
8571
+ // Remove any style tags and their content
8572
+ result = result.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
8573
+
8574
+ // Remove any script tags and their content
8575
+ result = result.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
8576
+
8577
+ // Remove HTML comments
8578
+ result = result.replace(/<!--[\s\S]*?-->/g, '');
8579
+
8580
+ // Extract body content if it's a full HTML document
8581
+ const bodyMatch = result.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
8582
+ if (bodyMatch) {
8583
+ result = bodyMatch[1];
8584
+ }
8585
+
8586
+ // Convert tables first (before other processing)
8587
+ result = this.convertHtmlTablesToMarkdown(result);
8588
+
8589
+ // Process nested elements properly by converting from innermost to outermost
8590
+ // Convert links with proper text extraction
8591
+ result = result.replace(/<a[^>]+href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (match, url, text) => {
8592
+ // Clean up the link text
8593
+ const cleanText = text.replace(/<[^>]*>/g, '').trim();
8594
+ return `[${cleanText}](${url})`;
8595
+ });
8596
+
8597
+ // Convert images
8598
+ result = result.replace(/<img[^>]+src=["']([^"']+)["'](?:[^>]+alt=["']([^"']+)["'])?[^>]*>/gi, (match, src, alt) => {
8599
+ return alt ? `` : ``;
8600
+ });
8601
+
8602
+ // Convert headers
8603
+ result = result.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, '\n# $1\n');
8604
+ result = result.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, '\n## $1\n');
8605
+ result = result.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, '\n### $1\n');
8606
+ result = result.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, '\n#### $1\n');
8607
+ result = result.replace(/<h5[^>]*>([\s\S]*?)<\/h5>/gi, '\n##### $1\n');
8608
+ result = result.replace(/<h6[^>]*>([\s\S]*?)<\/h6>/gi, '\n###### $1\n');
8609
+
8610
+ // Convert lists - handle nested lists
8611
+ // Process lists from innermost to outermost
8612
+ let previousHtml;
8613
+ do {
8614
+ previousHtml = result;
8615
+
8616
+ // Convert unordered lists
8617
+ result = result.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/gi, (match, content) => {
8618
+ const items = content.match(/<li[^>]*>([\s\S]*?)<\/li>/gi) || [];
8619
+ const converted = items.map(item => {
8620
+ const itemContent = item.replace(/<li[^>]*>/i, '').replace(/<\/li>/i, '').trim();
8621
+ return '- ' + itemContent;
8622
+ }).join('\n');
8623
+ return '\n' + converted + '\n';
8624
+ });
8625
+
8626
+ // Convert ordered lists
8627
+ result = result.replace(/<ol[^>]*>([\s\S]*?)<\/ol>/gi, (match, content) => {
8628
+ const items = content.match(/<li[^>]*>([\s\S]*?)<\/li>/gi) || [];
8629
+ const converted = items.map((item, index) => {
8630
+ const itemContent = item.replace(/<li[^>]*>/i, '').replace(/<\/li>/i, '').trim();
8631
+ return `${index + 1}. ${itemContent}`;
8632
+ }).join('\n');
8633
+ return '\n' + converted + '\n';
8634
+ });
8635
+ } while (result !== previousHtml);
8636
+
8637
+ // Convert code blocks
8638
+ result = result.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, '\n```\n$1\n```\n');
8639
+ result = result.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, '\n```\n$1\n```\n');
8640
+
8641
+ // Convert inline code
8642
+ result = result.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, '`$1`');
8643
+
8644
+ // Convert formatting
8645
+ result = result.replace(/<(b|strong)[^>]*>([\s\S]*?)<\/(b|strong)>/gi, '**$2**');
8646
+ result = result.replace(/<(i|em)[^>]*>([\s\S]*?)<\/(i|em)>/gi, '*$2*');
8647
+ result = result.replace(/<u[^>]*>([\s\S]*?)<\/u>/gi, '*$1*');
8648
+ result = result.replace(/<(s|strike|del)[^>]*>([\s\S]*?)<\/(s|strike|del)>/gi, '~~$2~~');
8649
+
8650
+ // Convert blockquotes
8651
+ result = result.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, '\n> $1\n');
8652
+
8653
+ // Convert horizontal rules
8654
+ result = result.replace(/<hr[^>]*>/gi, '\n---\n');
8655
+
8656
+ // Convert line breaks
8657
+ result = result.replace(/<br\s*\/?>/gi, '\n');
8658
+
8659
+ // Convert paragraphs
8660
+ result = result.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, '\n$1\n');
8661
+
8662
+ // Convert divs
8663
+ result = result.replace(/<div[^>]*>([\s\S]*?)<\/div>/gi, '\n$1\n');
8664
+
8665
+ // Remove any remaining HTML tags
8666
+ result = result.replace(/<[^>]*>/g, '');
8667
+
8668
+ // Decode HTML entities
8669
+ result = result.replace(/ /gi, ' ');
8670
+ result = result.replace(/</g, '<');
8671
+ result = result.replace(/>/g, '>');
8672
+ result = result.replace(/&/g, '&');
8673
+ result = result.replace(/"/g, '"');
8674
+ result = result.replace(/'/g, "'");
8675
+ result = result.replace(/'/g, "'");
8676
+ result = result.replace(/&#(\d+);/g, (match, dec) => String.fromCharCode(dec));
8677
+ result = result.replace(/&#x([a-f0-9]+);/gi, (match, hex) => String.fromCharCode(parseInt(hex, 16)));
8678
+
8679
+ // Clean up excessive whitespace
8680
+ result = result.replace(/\n\s*\n\s*\n/g, '\n\n');
8681
+ result = result.replace(/[ \t]+$/gm, '');
8682
+ result = result.trim();
8683
+
8684
+ return result;
8685
+ }
8686
+
8687
editUserMessage(contentDiv, originalContent, chatId) {
8688
if (!chatId) {return;}
8689
8721
return;
8722
}
8723
7852
- // Make content editable
8724
+ // Get the original raw content from chat history
8725
+ const originalMessage = chat.messages[messageIndex];
8726
+ const originalRawContent = originalMessage ? originalMessage.content : '';
8727
+
8728
+ // Make content editable and populate with original text for editing
8729
contentDiv.contentEditable = true;
8730
contentDiv.classList.add('editing');
7855
- const originalText = contentDiv.textContent;
8731
+
8732
+ // CRITICAL FIX: Convert newlines to <br> tags so they display properly in contentEditable
8733
+ const editableContent = originalRawContent
8734
+ .replace(/&/g, '&') // Escape ampersands first
8735
+ .replace(/</g, '<') // Escape less-than
8736
+ .replace(/>/g, '>') // Escape greater-than
8737
+ .replace(/\n/g, '<br>'); // Convert newlines to <br> tags
8738
+
8739
+ contentDiv.innerHTML = editableContent;
8740
8741
// Just focus, don't select all - let user position cursor
8742
contentDiv.focus();
8780
cancel = () => {
8781
contentDiv.contentEditable = false;
8782
contentDiv.classList.remove('editing');
7899
- contentDiv.textContent = originalText;
8783
+ // Restore the original rendered HTML content (markdown processed)
8784
+ contentDiv.innerHTML = marked.parse(originalRawContent, {
8785
+ breaks: true, gfm: true, sanitize: false
8786
+ });
8787
buttonsDiv.remove();
8788
// Clean up event listeners
8789
contentDiv.removeEventListener('keydown', keyHandler);
8790
document.removeEventListener('click', clickOutside);
8791
// Restore the edit trigger
7905
- this.addEditTrigger(contentDiv, originalText, 'user', chatId);
8792
+ this.addEditTrigger(contentDiv, originalRawContent, 'user', chatId);
8793
};
8794
8795
save = async () => {
7909
- const newContent = contentDiv.textContent.trim();
8796
+ // CRITICAL FIX: Get edited content while preserving formatting
8797
+ const newContent = this.getEditableContent(contentDiv).trim();
8798
if (!newContent) {
8799
this.showError('Message cannot be empty', chatId);
8800
return;
8809
this.loadChat(chatId, true);
8810
8811
// Send the new message
7924
- this.chatInput.value = newContent;
7925
- await this.sendMessage(chatId);
8812
+ const chatInput = this.getChatInput(chatId);
8813
+ if (chatInput) {
8814
+ // Convert markdown to HTML for contentEditable
8815
+ const htmlContent = newContent
8816
+ .replace(/&/g, '&')
8817
+ .replace(/</g, '<')
8818
+ .replace(/>/g, '>')
8819
+ .replace(/\n/g, '<br>');
8820
+ chatInput.innerHTML = htmlContent;
8821
+ await this.sendMessage(chatId);
8822
+ } else {
8823
+ const editChat = this.chats.get(chatId);
8824
+ if (!editChat || !editChat.isSubChat) {
8825
+ // Only log error for main chats - sub-chats don't have input elements
8826
+ console.error('[editUserMessage.save] ERROR: Could not find input for chat', chatId);
8827
+ }
8828
+ // Fall back to sending message directly with the new content
8829
+ await this.sendMessage(chatId, newContent);
8830
+ }
8831
};
8832
8833
buttonsDiv.querySelector('.btn-primary').onclick = save;
8873
toolDiv.dataset.turn = chat.currentTurn || 0;
8874
}
8875
8876
+ // Extract metadata fields from args if present
8877
+ const metadataFields = ['tool_purpose', 'expected_format', 'key_information', 'success_indicators', 'context_for_interpretation'];
8878
+ const metadata = {};
8879
+ const cleanedArgs = { ...args };
8880
+
8881
+ // Extract and remove metadata fields from args
8882
+ for (const field of metadataFields) {
8883
+ if (args && field in args) {
8884
+ metadata[field] = args[field];
8885
+ delete cleanedArgs[field];
8886
+ }
8887
+ }
8888
+
8889
+ // Store metadata for later use
8890
+ toolDiv.dataset.metadata = JSON.stringify(metadata);
8891
+
8892
+ // Don't display metadata in tool header - it will be shown in sub-chat
8893
+ const metadataHtml = '';
8894
+
8895
const toolHeader = document.createElement('div');
8896
toolHeader.className = 'tool-header';
8897
toolHeader.innerHTML = `
8898
+ ${metadataHtml}
8899
<span class="tool-toggle">▶</span>
8900
<span class="tool-label"><i class="fas fa-wrench"></i> ${toolName}</span>
8901
<span class="tool-info">
8919
const requestCopyBtn = this.createCopyButton({
8920
buttonClass: 'tool-section-copy',
8921
tooltip: 'Copy request',
7997
- onCopy: () => JSON.stringify(args, null, 2)
8922
+ onCopy: () => JSON.stringify(cleanedArgs, null, 2)
8923
});
8924
8925
controlsDiv.appendChild(requestCopyBtn);
8926
requestSection.appendChild(controlsDiv);
8927
8003
- // Add the request content
8928
+ // Add the request content (without metadata fields)
8929
const preElement = document.createElement('pre');
8005
- preElement.textContent = JSON.stringify(args, null, 2);
8930
+ preElement.textContent = JSON.stringify(cleanedArgs, null, 2);
8931
requestSection.appendChild(preElement);
8932
8933
toolContent.appendChild(requestSection);
8976
this.moveSpinnerToBottom(chatId);
8977
}
8978
8054
- addToolResult(toolName, result, chatId, responseTime, responseSize, includeInContext, _messageIndex, toolCallId) {
8979
+ addToolResult(toolName, result, chatId, responseTime, responseSize, includeInContext, _messageIndex, toolCallId, _subChatId) {
8980
if (!chatId) {
8981
console.error('addToolResult called without chatId');
8982
return;
9053
9054
if (responseSection) {
9055
let formattedResult;
9056
+ let isMarkdown = false;
9057
+
9058
if (typeof result === 'object') {
9059
if (result.error) {
9060
formattedResult = `<span style="color: var(--danger-color);">${result.error}</span>`;
9062
formattedResult = `<pre>${JSON.stringify(result, null, 2)}</pre>`;
9063
}
9064
} else {
8138
- formattedResult = result;
9065
+ // For string responses, check if it looks like markdown
9066
+ const textResult = String(result);
9067
+ if (this.isMarkdownContent(textResult)) {
9068
+ formattedResult = marked.parse(textResult, {
9069
+ breaks: true,
9070
+ gfm: true,
9071
+ sanitize: false
9072
+ });
9073
+ isMarkdown = true;
9074
+ } else {
9075
+ formattedResult = `<pre>${textResult}</pre>`;
9076
+ }
9077
}
9078
9079
// Clear and rebuild response section
9096
9097
// Add the formatted result
9098
const resultDiv = document.createElement('div');
9099
+ if (isMarkdown) {
9100
+ resultDiv.className = 'message-content';
9101
+ }
9102
resultDiv.innerHTML = formattedResult;
9103
responseSection.appendChild(resultDiv);
9104
9107
if (separator) {
9108
separator.style.display = 'block';
9109
}
9110
+
9111
+ // Sub-chats are rendered as separate chat items via renderSubChatAsItem
9112
+ // No need for expandable UI here
9113
}
9114
9115
// Remove from pending using the toolCallId
9122
this.moveSpinnerToBottom(chatId);
9123
}
9124
9125
+ /**
9126
+ * Check if a sub-chat should be created for tool response
9127
+ */
9128
+ async shouldCreateSubChat(chat, responseSize, _toolCall) {
9129
+ // Never create sub-chats within sub-chats
9130
+ if (chat.isSubChat) {
9131
+ return false;
9132
+ }
9133
+
9134
+ // Check if tool summarization is enabled
9135
+ if (!chat.config?.optimisation?.toolSummarisation?.enabled) {
9136
+ return false;
9137
+ }
9138
+
9139
+ // Get threshold in bytes (convert from KiB)
9140
+ const thresholdKiB = chat.config.optimisation.toolSummarisation.thresholdKiB ?? 20;
9141
+ const thresholdBytes = thresholdKiB * 1024;
9142
+
9143
+ // If threshold is 0, process all tools
9144
+ if (thresholdKiB === 0) {
9145
+ return true;
9146
+ }
9147
+
9148
+ // Check if response exceeds threshold
9149
+ return responseSize > thresholdBytes;
9150
+ }
9151
+
9152
+ /**
9153
+ * Create a sub-chat for processing tool response
9154
+ */
9155
+ async createSubChatForTool(parentChat, toolCall, toolResult) {
9156
+ // Try to find the tool div in the current chat's container
9157
+ const chatContainer = this.chatContainers.get(parentChat.id);
9158
+ let toolDiv = null;
9159
+
9160
+ if (chatContainer) {
9161
+ toolDiv = chatContainer.querySelector(`[data-tool-id="${toolCall.id}"]`);
9162
+ }
9163
+
9164
+ // Fallback: search globally
9165
+ if (!toolDiv) {
9166
+ toolDiv = document.querySelector(`[data-tool-id="${toolCall.id}"]`);
9167
+ }
9168
+
9169
+ // Debug: check all tool divs in current chat
9170
+ if (chatContainer) {
9171
+ const _allToolDivs = chatContainer.querySelectorAll('[data-tool-id]');
9172
+ }
9173
+
9174
+ const metadata = toolDiv ? JSON.parse(toolDiv.dataset.metadata || '{}') : {};
9175
+
9176
+ // Determine sub-chat model
9177
+ const subChatModel = parentChat.config.optimisation.toolSummarisation.model || {
9178
+ provider: parentChat.model.provider || 'anthropic',
9179
+ id: parentChat.model.id || 'claude-3-haiku-20240307'
9180
+ };
9181
+
9182
+ // Create sub-chat with minimal config - no optimizations
9183
+ const subChatOptions = {
9184
+ mcpServerId: parentChat.mcpServerId,
9185
+ llmProviderId: parentChat.llmProviderId,
9186
+ model: ChatConfig.modelConfigToString(subChatModel),
9187
+ title: `Processing: ${toolCall.name}`,
9188
+ parentChatId: parentChat.id,
9189
+ parentToolCallId: toolCall.id,
9190
+ toolMetadata: metadata,
9191
+ config: {
9192
+ model: subChatModel,
9193
+ optimisation: {
9194
+ // Disable ALL optimizations for sub-chats
9195
+ toolSummarisation: {
9196
+ enabled: false,
9197
+ thresholdKiB: 20,
9198
+ model: null
9199
+ },
9200
+ autoSummarisation: {
9201
+ enabled: false,
9202
+ triggerPercent: 50,
9203
+ model: null
9204
+ },
9205
+ toolMemory: {
9206
+ enabled: false,
9207
+ forgetAfterConclusions: 1
9208
+ },
9209
+ cacheControl: parentChat.config.optimisation.cacheControl || 'all-off', // Inherit parent's cache control
9210
+ titleGeneration: {
9211
+ enabled: false,
9212
+ model: null
9213
+ }
9214
+ },
9215
+ mcpServer: parentChat.mcpServerId
9216
+ }
9217
+ };
9218
+
9219
+ const subChatId = await this.createNewChat(subChatOptions);
9220
+ if (!subChatId) {
9221
+ console.error('[createSubChatForTool] Failed to create sub-chat');
9222
+ return null;
9223
+ }
9224
+
9225
+ const subChat = this.chats.get(subChatId);
9226
+ if (!subChat) {
9227
+ console.error('[createSubChatForTool] Sub-chat not found after creation');
9228
+ return null;
9229
+ }
9230
+
9231
+ // Sub-chat visibility will be handled when it's rendered as a separate item
9232
+
9233
+ // Initialize sub-chat with system prompt and tool result
9234
+ await this.initializeSubChat(subChatId, toolCall, toolResult, metadata);
9235
+
9236
+ // Sub-chat will be processed synchronously in processSingleLLMResponse
9237
+
9238
+ return subChatId;
9239
+ }
9240
+
9241
+ /**
9242
+ * Update sub-chat section visibility in tool result
9243
+ */
9244
+ updateSubChatVisibility(toolCallId, subChatId, subChat) {
9245
+ // Find the parent chat ID from the sub-chat
9246
+ const parentChatId = subChat.parentChatId;
9247
+ if (!parentChatId) {
9248
+ console.error('[updateSubChatVisibility] Sub-chat missing parentChatId');
9249
+ return;
9250
+ }
9251
+
9252
+ const container = this.getChatContainer(parentChatId);
9253
+ if (!container) {
9254
+ console.error(`[updateSubChatVisibility] Container not found for parent chat: ${parentChatId}`);
9255
+ return;
9256
+ }
9257
+
9258
+ const subChatSection = container.querySelector(`.sub-chat-section[data-tool-call-id="${toolCallId}"]`);
9259
+
9260
+ if (subChatSection) {
9261
+ subChatSection.dataset.subChatId = subChatId;
9262
+ subChatSection.style.display = 'block';
9263
+
9264
+ // Update stats
9265
+ const statsSpan = subChatSection.querySelector('.sub-chat-stats');
9266
+ if (statsSpan) {
9267
+ const messageCount = subChat.messages.length;
9268
+ const modelName = ChatConfig.getModelDisplayName(subChat.model);
9269
+ statsSpan.textContent = `${messageCount} messages • ${modelName}`;
9270
+ }
9271
+ } else {
9272
+ console.error(`[updateSubChatVisibility] Sub-chat section not found for toolCallId: ${toolCallId}`);
9273
+ // List all existing sub-chat sections for debugging
9274
+ const allSubChatSections = container.querySelectorAll('.sub-chat-section');
9275
+ allSubChatSections.forEach((_section, _index) => { });
9276
+ }
9277
+ }
9278
+
9279
+ /**
9280
+ * Initialize sub-chat with system prompt and tool result
9281
+ */
9282
+ async initializeSubChat(subChatId, toolCall, toolResult, metadata) {
9283
+ const subChat = this.chats.get(subChatId);
9284
+ if (!subChat) {
9285
+ console.error(`[initializeSubChat] Sub-chat not found for subChatId: ${subChatId}, toolCallId: ${toolCall?.id}`);
9286
+ return;
9287
+ }
9288
+
9289
+ // Create the sub-chat system prompt with full MCP capabilities
9290
+ const systemPrompt = SystemMsg.createSpecializedSystemPrompt('subchat');
9291
+
9292
+ // Update system message
9293
+ if (subChat.messages.length > 0 && subChat.messages[0].role === 'system') {
9294
+ subChat.messages[0].content = systemPrompt;
9295
+ }
9296
+
9297
+ // Clean the tool arguments by removing metadata fields
9298
+ const metadataFields = ['tool_purpose', 'expected_format', 'key_information', 'success_indicators', 'context_for_interpretation'];
9299
+ const cleanedArgs = { ...toolCall.arguments };
9300
+
9301
+ // Remove metadata fields from the arguments
9302
+ for (const field of metadataFields) {
9303
+ if (cleanedArgs && field in cleanedArgs) {
9304
+ delete cleanedArgs[field];
9305
+ }
9306
+ }
9307
+
9308
+ // Create a user message that combines the instructions from the primary LLM
9309
+ let userMessage = 'I am an AI assistant and I need your help to answer a broader question I am asked.';
9310
+
9311
+ if (metadata.tool_purpose) {
9312
+ userMessage += `\n\nYour task is: ${metadata.tool_purpose}. `;
9313
+ }
9314
+
9315
+ userMessage += `\n\nThis is usually done by executing the ${toolCall.name} tool. `;
9316
+ userMessage += `\n\nHere are the arguments I would use:\n\`\`\`json\n${JSON.stringify(cleanedArgs, null, 2)}\n\`\`\``;
9317
+ userMessage += `\n\nMy assumption that this tool and parameters will provide the desired result, may be wrong. `;
9318
+ userMessage += `In that case, come up with your own plan to answer the question.`;
9319
+
9320
+ if (metadata.key_information) {
9321
+ userMessage += `\n\nFocus on: ${metadata.key_information}`;
9322
+ }
9323
+
9324
+ if (metadata.expected_format) {
9325
+ userMessage += `\n\n**CRITICAL**: The expected format is: ${metadata.expected_format}`;
9326
+ }
9327
+
9328
+ if (metadata.success_indicators) {
9329
+ userMessage += `\n\nSuccess indicators to look for: ${metadata.success_indicators}`;
9330
+ }
9331
+
9332
+ if (metadata.context_for_interpretation) {
9333
+ userMessage += `\n\nAdditional context you may need during your investigation: ${metadata.context_for_interpretation}`;
9334
+ }
9335
+
9336
+ if (metadata.tool_purpose) {
9337
+ userMessage += `\n\nPlease use any of your tools, and adapt to ${metadata.tool_purpose}. `;
9338
+ }
9339
+ else {
9340
+ userMessage += '\n\nPlease use any of your tools, and adapt to provide the answer I seek. ';
9341
+ }
9342
+ userMessage += '\n\nImportant: Do not ask me any question back, or provide explanations on tool usage, or give up on the first try. ';
9343
+ userMessage += 'Check your tools available, adapt to the issues you face (wrong parameters, empty responses, wrong tool chosen, etc), ';
9344
+ userMessage += 'and provide an authoritative answer. ';
9345
+ userMessage += '\n\n**CRITICAL**: If you encounter large datasets or lists, process EVERY single item. ';
9346
+ userMessage += 'Never sample, never use "..." or "among others". Process all items and explicitly state how many you analyzed. ';
9347
+ userMessage += 'Your thoroughness is essential for accurate analysis.';
9348
+ userMessage += '\n\n**CRITICAL**: If tools return errors or empty data, DO NOT give up! ';
9349
+ userMessage += 'Try different parameters, broader time ranges, different filters, or alternative approaches. ';
9350
+ userMessage += 'Make multiple attempts before concluding no data exists. The primary assistant is counting on you to be persistent and thorough.';
9351
+ userMessage += '\n\n**IF TASK CANNOT BE COMPLETED**: Use the ESCALATION protocol to document your attempts, ';
9352
+ userMessage += 'provide any partial data you found, and suggest specific alternatives for the primary assistant to try.';
9353
+
9354
+ // Add the formatted user request
9355
+ subChat.messages.push({
9356
+ role: 'user',
9357
+ content: userMessage,
9358
+ timestamp: new Date().toISOString()
9359
+ });
9360
+
9361
+ // Simulate the assistant requesting the tool
9362
+ const toolRequestContent = [{
9363
+ type: 'text',
9364
+ text: `Let me first call the ${toolCall.name} tool to get some data.`
9365
+ }, {
9366
+ type: 'tool_use',
9367
+ id: toolCall.id,
9368
+ name: toolCall.name,
9369
+ input: cleanedArgs || {}
9370
+ }];
9371
+
9372
+ subChat.messages.push({
9373
+ role: 'assistant',
9374
+ content: toolRequestContent,
9375
+ timestamp: new Date().toISOString()
9376
+ });
9377
+
9378
+ // Add the tool result
9379
+ subChat.messages.push({
9380
+ role: 'tool-results',
9381
+ toolResults: [{
9382
+ toolCallId: toolCall.id,
9383
+ toolName: toolCall.name,
9384
+ result: toolResult,
9385
+ isError: false
9386
+ }],
9387
+ timestamp: new Date().toISOString()
9388
+ });
9389
+
9390
+ // Save sub-chat
9391
+ this.autoSave(subChatId);
9392
+ }
9393
+
9394
+ /**
9395
+ * Process sub-chat and update parent
9396
+ */
9397
+ async processSubChat(subChatId, parentChatId, _toolCallId) {
9398
+ const subChat = this.chats.get(subChatId);
9399
+ const parentChat = this.chats.get(parentChatId);
9400
+ if (!subChat || !parentChat) {
9401
+ console.error('[processSubChat] Missing chats:', { subChat: !!subChat, parentChat: !!parentChat });
9402
+ return;
9403
+ }
9404
+
9405
+ try {
9406
+ // Send message to process the tool result
9407
+ // Use a dummy message to trigger processing since sub-chat already has the tool result as a user message
9408
+ await this.sendMessage(subChatId, 'process', true); // isResume = true to skip adding new user message
9409
+
9410
+ // CRITICAL: Wait for the sub-chat to be completely done processing
9411
+ // The sub-chat might make multiple tool calls, so we need to wait until it's no longer processing
9412
+ while (subChat.isProcessing) {
9413
+ console.log(`[processSubChat] Sub-chat ${subChatId} is still processing, waiting...`);
9414
+ // eslint-disable-next-line no-await-in-loop
9415
+ await new Promise(resolve => { setTimeout(resolve, 100); }); // Check every 100ms
9416
+ }
9417
+
9418
+ // Find the latest assistant response (skip title and other special messages)
9419
+ let assistantMessage = null;
9420
+ for (let i = subChat.messages.length - 1; i >= 0; i--) {
9421
+ const msg = subChat.messages[i];
9422
+ if (msg.role === 'assistant') {
9423
+ assistantMessage = msg;
9424
+ break;
9425
+ }
9426
+ }
9427
+
9428
+ if (assistantMessage) {
9429
+ // NOTE: The assistant message has already been rendered by sendMessage
9430
+ // We don't need to render it again here
9431
+
9432
+ // Extract text content from assistant response
9433
+ let textContent = '';
9434
+ if (typeof assistantMessage.content === 'string') {
9435
+ textContent = assistantMessage.content;
9436
+ } else if (Array.isArray(assistantMessage.content)) {
9437
+ // Extract text from content blocks
9438
+ const textBlocks = assistantMessage.content
9439
+ .filter(block => block.type === 'text')
9440
+ .map(block => block.text)
9441
+ .join('\n\n');
9442
+ textContent = textBlocks;
9443
+ } else {
9444
+ textContent = JSON.stringify(assistantMessage.content);
9445
+ }
9446
+
9447
+ // Note: Parent tool result is now updated directly in executeToolCalls
9448
+ // during interleaved execution, so we don't need to update it here
9449
+
9450
+ // Update sub-chat stats display
9451
+ // Sub-chat DOM status will be updated through renderSubChatAsItem
9452
+
9453
+ // NOTE: Don't update parent tool-result costs here - it's too early
9454
+ // The parent chat's tool-results message hasn't been added yet
9455
+ // Cost accumulation happens later in executeToolCalls after tool-results are added
9456
+
9457
+ // Return the summarized result for the promise chain
9458
+ return textContent;
9459
+ } else {
9460
+ console.error('[processSubChat] No valid assistant response found');
9461
+ return null;
9462
+ }
9463
+ } catch (error) {
9464
+ console.error('[processSubChat] Error processing sub-chat:', error);
9465
+ return null;
9466
+ }
9467
+ }
9468
+
9469
+ /**
9470
+ * Update parent chat's tool result with summarized version
9471
+ */
9472
+ updateParentToolResult(parentChatId, toolCallId, summarizedResult) {
9473
+ const parentChat = this.chats.get(parentChatId);
9474
+ if (!parentChat) {
9475
+ console.error('[updateParentToolResult] Parent chat not found');
9476
+ return;
9477
+ }
9478
+
9479
+ // Get sub-chat costs by finding the sub-chat and using its existing totals
9480
+ let subChatCosts = null;
9481
+ for (const [_chatId, chat] of this.chats.entries()) {
9482
+ if (chat.isSubChat && chat.parentToolCallId === toolCallId) {
9483
+ // Ensure the sub-chat has up-to-date pricing
9484
+ this.updateChatTokenPricing(chat);
9485
+
9486
+ // Copy the totals
9487
+ subChatCosts = {
9488
+ totalTokens: { ...chat.totalTokensPrice },
9489
+ perModel: {}
9490
+ };
9491
+
9492
+ // Deep copy per-model data
9493
+ for (const [model, costs] of Object.entries(chat.perModelTokensPrice)) {
9494
+ subChatCosts.perModel[model] = { ...costs };
9495
+ }
9496
+ break;
9497
+ }
9498
+ }
9499
+
9500
+ // Find and update the tool result in parent messages
9501
+ let found = false;
9502
+ for (const message of parentChat.messages) {
9503
+ if (message.role === 'tool-results' && message.toolResults) {
9504
+ const toolResult = message.toolResults.find(tr => tr.toolCallId === toolCallId);
9505
+ if (!toolResult) {
9506
+ console.warn(`[updateParentToolResult] Tool result not found in message for toolCallId: ${toolCallId}, parentChatId: ${parentChatId}`);
9507
+ continue;
9508
+ }
9509
+ if (toolResult) {
9510
+ toolResult.result = summarizedResult;
9511
+ toolResult.wasProcessedBySubChat = true;
9512
+
9513
+ // Add sub-chat cost information
9514
+ toolResult.subChatCosts = subChatCosts;
9515
+
9516
+ found = true;
9517
+ break;
9518
+ }
9519
+ }
9520
+ }
9521
+
9522
+ if (!found) {
9523
+ console.error(`[updateParentToolResult] Tool result not found in parent messages for toolCallId: ${toolCallId}, parentChatId: ${parentChatId}`);
9524
+ }
9525
+
9526
+ // Update the DOM display
9527
+ const container = this.getChatContainer(parentChatId);
9528
+ if (container) {
9529
+ const toolDiv = container.querySelector(`[data-tool-id="${toolCallId}"]`);
9530
+ if (toolDiv) {
9531
+ const responseSection = toolDiv.querySelector('.tool-response-section');
9532
+ if (responseSection) {
9533
+ // Find the actual result content div (not the controls or indicator)
9534
+ const resultDivs = responseSection.querySelectorAll('div');
9535
+ let actualResultDiv = null;
9536
+
9537
+ for (const div of resultDivs) {
9538
+ // Skip controls div and sub-chat indicator
9539
+ if (!div.classList.contains('tool-section-controls') &&
9540
+ !div.classList.contains('sub-chat-indicator') &&
9541
+ !div.querySelector('.sub-chat-indicator')) {
9542
+ actualResultDiv = div;
9543
+ break;
9544
+ }
9545
+ }
9546
+
9547
+ if (actualResultDiv) {
9548
+ // Format as markdown if the content looks like markdown
9549
+ let formattedContent;
9550
+ if (this.isMarkdownContent(summarizedResult)) {
9551
+ formattedContent = marked.parse(summarizedResult, {
9552
+ breaks: true,
9553
+ gfm: true,
9554
+ sanitize: false
9555
+ });
9556
+ } else {
9557
+ formattedContent = this.escapeHtml(summarizedResult);
9558
+ }
9559
+ actualResultDiv.innerHTML = `<div class="tool-summarized-result message-content">${formattedContent}</div>`;
9560
+ }
9561
+ }
9562
+ }
9563
+ }
9564
+
9565
+ // Update parent chat token pricing to include new sub-chat costs
9566
+ if (found && subChatCosts) {
9567
+ this.updateChatTokenPricing(parentChat);
9568
+ this.updateCumulativeTokenDisplay(parentChatId);
9569
+ }
9570
+
9571
+ // Save parent chat
9572
+ this.autoSave(parentChatId);
9573
+ }
9574
+
9575
+ /**
9576
+ * Update the status of an existing sub-chat item
9577
+ */
9578
+ updateSubChatStatus(parentChatId, toolCallId, newStatus) {
9579
+ const container = this.getChatContainer(parentChatId);
9580
+ if (!container) {
9581
+ console.error(`[updateSubChatStatus] Container not found for parent chat ${parentChatId}`);
9582
+ return;
9583
+ }
9584
+
9585
+ // Find the sub-chat item
9586
+ const subChatItem = container.querySelector(`[data-tool-call-id="${toolCallId}"]`);
9587
+ if (!subChatItem) {
9588
+ console.error(`[updateSubChatStatus] Sub-chat item not found for tool ${toolCallId}`);
9589
+ return;
9590
+ }
9591
+
9592
+ // Find the status element
9593
+ const statusElement = subChatItem.querySelector('.tool-status');
9594
+ if (!statusElement) {
9595
+ console.error(`[updateSubChatStatus] Status element not found in sub-chat`);
9596
+ return;
9597
+ }
9598
+
9599
+ // Update status based on newStatus
9600
+ let iconClass, text;
9601
+ if (newStatus === 'processing') {
9602
+ iconClass = 'fas fa-hourglass-half';
9603
+ text = 'Processing...';
9604
+ } else if (newStatus === 'success') {
9605
+ iconClass = 'fas fa-check-circle';
9606
+ text = 'Summarized';
9607
+ } else if (newStatus === 'failed') {
9608
+ iconClass = 'fas fa-times-circle';
9609
+ text = 'Failed';
9610
+ } else {
9611
+ console.error(`[updateSubChatStatus] Unknown status: ${newStatus}`);
9612
+ return;
9613
+ }
9614
+
9615
+ // Update the status element
9616
+ statusElement.innerHTML = `<i class="${iconClass}"></i> ${text}`;
9617
+ }
9618
+
9619
+ /**
9620
+ * Render sub-chat as a separate chat item in the main chat flow
9621
+ */
9622
+ renderSubChatAsItem(parentChatId, subChatId, toolCallId, status = 'success') {
9623
+ const parentContainer = this.getChatContainer(parentChatId);
9624
+ const subChat = this.chats.get(subChatId);
9625
+
9626
+ if (!parentContainer || !subChat) {
9627
+ console.error(`[renderSubChatAsItem] Missing required data - parentContainer: ${!!parentContainer}, subChat: ${!!subChat}, parentChatId: ${parentChatId}, subChatId: ${subChatId}, toolCallId: ${toolCallId}`);
9628
+ return;
9629
+ }
9630
+
9631
+ // Find the tool result to insert sub-chat after
9632
+ let toolDiv = parentContainer.querySelector(`[data-tool-id="${toolCallId}"]`);
9633
+
9634
+ // For loaded chats, we may need to find the tool div differently or create a placeholder
9635
+ if (!toolDiv) {
9636
+ // Try to find by searching all tool blocks and matching the content
9637
+ const allToolBlocks = parentContainer.querySelectorAll('.tool-block');
9638
+ for (const block of allToolBlocks) {
9639
+ // Check if this tool block might be our target
9640
+ // This is a fallback for loaded chats where tool IDs might not match
9641
+ if (block.textContent.includes(subChat.parentToolName || '')) {
9642
+ toolDiv = block;
9643
+ break;
9644
+ }
9645
+ }
9646
+
9647
+ if (!toolDiv) {
9648
+ // If we still can't find it, create a standalone sub-chat display
9649
+ // This ensures sub-chats are visible even if we can't find the parent tool
9650
+ const messagesContainer = parentContainer._elements.messages;
9651
+ const standaloneSubChat = document.createElement('div');
9652
+ standaloneSubChat.className = 'tool-block sub-chat-standalone';
9653
+ standaloneSubChat.dataset.subChatId = subChatId;
9654
+ messagesContainer.appendChild(standaloneSubChat);
9655
+ toolDiv = standaloneSubChat; // Use this as our insertion point
9656
+ }
9657
+ }
9658
+
9659
+ // Create sub-chat item container - use tool-block class for consistent styling
9660
+ const subChatItem = document.createElement('div');
9661
+ subChatItem.className = 'tool-block sub-chat-item';
9662
+ subChatItem.dataset.subChatId = subChatId;
9663
+ subChatItem.dataset.toolCallId = toolCallId;
9664
+
9665
+ // Get tool metadata from the tool div
9666
+ let toolMetadata = {};
9667
+ try {
9668
+ const metadataStr = toolDiv.dataset.metadata;
9669
+ if (metadataStr) {
9670
+ toolMetadata = JSON.parse(metadataStr);
9671
+ }
9672
+ } catch (e) {
9673
+ console.warn('[renderSubChatAsItem] Failed to parse tool metadata:', e);
9674
+ }
9675
+
9676
+ // Create header for expandable section - use tool-header class
9677
+ const subChatHeader = document.createElement('div');
9678
+ subChatHeader.className = 'tool-header sub-chat-header';
9679
+
9680
+ // Handle different status types with appropriate icons and colors
9681
+ let statusIcon, statusLabel;
9682
+ if (status === 'processing') {
9683
+ statusIcon = 'fa-hourglass-half';
9684
+ statusLabel = 'Processing...';
9685
+ } else if (status === 'success') {
9686
+ statusIcon = 'fa-check-circle';
9687
+ statusLabel = 'Summarized';
9688
+ } else { // 'failed' or any other status
9689
+ statusIcon = 'fa-times-circle';
9690
+ statusLabel = 'Failed';
9691
+ }
9692
+
9693
+ subChatHeader.innerHTML = `
9694
+ <span class="tool-toggle">▶</span>
9695
+ <span class="tool-label"><i class="fas fa-robot"></i> Tool Summarization </span>
9696
+ <span class="tool-info">
9697
+ <span class="tool-status"><i class="fas ${statusIcon}"></i> ${statusLabel}</span>
9698
+ <span class="tool-metric"><i class="fas fa-comments"></i> ${subChat.messages.length}</span>
9699
+ ${toolMetadata.tool_purpose ? `<span class="tool-metric" title="${this.escapeHtml(toolMetadata.tool_purpose)}"><i class="fas fa-info-circle"></i> Purpose</span>` : ''}
9700
+ </span>
9701
+ `;
9702
+
9703
+ // Create content area - EXACTLY like tool-content
9704
+ const subChatContent = document.createElement('div');
9705
+ subChatContent.className = 'tool-content collapsed'; // Use tool-content class with collapsed
9706
+
9707
+ // CRITICAL CHANGE: Create permanent messages container immediately
9708
+ // This ensures sub-chat has a real DOM container from the start
9709
+ const messagesDiv = document.createElement('div');
9710
+ messagesDiv.className = 'chat-messages';
9711
+ messagesDiv.style.maxHeight = '400px'; // Limit height for sub-chats
9712
+ messagesDiv.style.overflowY = 'auto';
9713
+ messagesDiv.style.backgroundColor = 'var(--sub-chat-bg, rgba(0, 0, 0, 0.02))'; // Theme-aware background
9714
+ messagesDiv.style.borderRadius = '8px';
9715
+ messagesDiv.style.padding = '10px';
9716
+ messagesDiv.style.margin = '10px';
9717
+
9718
+ // Add the messages div to content immediately (even if collapsed)
9719
+ subChatContent.appendChild(messagesDiv);
9720
+
9721
+ // Create permanent container structure for the sub-chat
9722
+ const permanentContainer = {
9723
+ _elements: {
9724
+ messages: messagesDiv
9725
+ },
9726
+ _isSubChatContainer: true,
9727
+ _parentChatId: parentChatId
9728
+ };
9729
+
9730
+ // Store the permanent container in regular chatContainers
9731
+ // This ensures getChatContainer will return the permanent container
9732
+ this.chatContainers.set(subChatId, permanentContainer);
9733
+
9734
+ // Add click handler for expand/collapse - EXACTLY like tools
9735
+ subChatHeader.addEventListener('click', () => {
9736
+ const isCollapsed = subChatContent.classList.contains('collapsed');
9737
+ subChatContent.classList.toggle('collapsed');
9738
+ subChatHeader.querySelector('.tool-toggle').textContent = isCollapsed ? '▼' : '▶';
9739
+
9740
+ // Always load full sub-chat history when expanding
9741
+ // This ensures both live and loaded chats show complete history
9742
+ if (isCollapsed) {
9743
+ this.loadSubChatMessagesIntoContainer(subChatId, messagesDiv);
9744
+ }
9745
+ });
9746
+
9747
+ subChatItem.appendChild(subChatHeader);
9748
+ subChatItem.appendChild(subChatContent);
9749
+
9750
+ // Insert sub-chat item after the tool result
9751
+ toolDiv.parentNode.insertBefore(subChatItem, toolDiv.nextSibling);
9752
+ }
9753
+
9754
+ /**
9755
+ * Load and render sub-chat messages into an existing messages container
9756
+ */
9757
+ loadSubChatMessagesIntoContainer(subChatId, messagesDiv) {
9758
+ const subChat = this.chats.get(subChatId);
9759
+ if (!subChat) {
9760
+ console.error(`[loadSubChatMessagesIntoContainer] Sub-chat not found for subChatId: ${subChatId}`);
9761
+ return;
9762
+ }
9763
+
9764
+ // Clear existing content
9765
+ messagesDiv.innerHTML = '';
9766
+
9767
+ // Initialize rendering state for sub-chat if needed
9768
+ if (!subChat.renderingState) {
9769
+ subChat.renderingState = {
9770
+ lastDisplayedTurn: 0,
9771
+ currentStepInTurn: 1
9772
+ };
9773
+ }
9774
+
9775
+ // Add Processing Guidance as a system message at the beginning
9776
+ const parentChat = this.chats.get(subChat.parentChatId);
9777
+ if (parentChat) {
9778
+ const container = this.getChatContainer(subChat.parentChatId);
9779
+ if (container) {
9780
+ const toolDiv = container.querySelector(`[data-tool-id="${subChat.parentToolCallId}"]`);
9781
+ if (toolDiv && toolDiv.dataset.metadata) {
9782
+ try {
9783
+ const metadata = JSON.parse(toolDiv.dataset.metadata);
9784
+ if (Object.keys(metadata).length > 0) {
9785
+ // Create a guidance div with proper chat message styling
9786
+ const guidanceDiv = document.createElement('div');
9787
+ guidanceDiv.className = 'message-wrapper system';
9788
+ guidanceDiv.innerHTML = `
9789
+ <div class="message">
9790
+ <div class="message-content">
9791
+ <div class="processing-guidance">
9792
+ <h4>
9793
+ <i class="fas fa-info-circle"></i> Processing Guidance
9794
+ </h4>
9795
+ ${metadata.tool_purpose ? `<div class="guidance-item"><i class="fas fa-lightbulb"></i><strong>Purpose:</strong> ${metadata.tool_purpose}</div>` : ''}
9796
+ ${metadata.expected_format ? `<div class="guidance-item"><i class="fas fa-file-alt"></i><strong>Expected:</strong> ${metadata.expected_format}</div>` : ''}
9797
+ ${metadata.key_information ? `<div class="guidance-item"><i class="fas fa-search"></i><strong>Looking for:</strong> ${metadata.key_information}</div>` : ''}
9798
+ ${metadata.success_indicators ? `<div class="guidance-item"><i class="fas fa-check-circle"></i><strong>Success:</strong> ${metadata.success_indicators}</div>` : ''}
9799
+ ${metadata.context_for_interpretation ? `<div class="guidance-item"><i class="fas fa-book"></i><strong>Context:</strong> ${metadata.context_for_interpretation}</div>` : ''}
9800
+ </div>
9801
+ </div>
9802
+ </div>
9803
+ `;
9804
+ messagesDiv.appendChild(guidanceDiv);
9805
+ }
9806
+ } catch (e) {
9807
+ console.warn('[loadSubChatMessagesIntoContainer] Failed to parse metadata:', e);
9808
+ }
9809
+ }
9810
+ }
9811
+ }
9812
+
9813
+ // Use the regular chat rendering system for each message
9814
+ for (let i = 0; i < subChat.messages.length; i++) {
9815
+ const message = subChat.messages[i];
9816
+
9817
+ // Special handling for system messages to make them collapsible
9818
+ if (message.role === 'system') {
9819
+ this.displaySystemPrompt(message.content, subChatId);
9820
+ } else {
9821
+ this.displayStoredMessage(message, i, subChatId);
9822
+ }
9823
+ }
9824
+ }
9825
+
9826
+ /**
9827
+ * Render sub-chats for loaded chat history
9828
+ */
9829
+ renderSubChatsForLoadedChat(parentChatId) {
9830
+ const parentChat = this.chats.get(parentChatId);
9831
+ if (!parentChat) {
9832
+ console.error(`[renderSubChatsForLoadedChat] Parent chat not found for parentChatId: ${parentChatId}`);
9833
+ return;
9834
+ }
9835
+
9836
+ // Find all sub-chats for this parent
9837
+ this.chats.forEach((chat, chatId) => {
9838
+ if (chat.isSubChat && chat.parentChatId === parentChatId) {
9839
+ // Determine status based on whether sub-chat has valid assistant response
9840
+ let status = 'failed';
9841
+ const lastMessage = chat.messages[chat.messages.length - 1];
9842
+ if (lastMessage && lastMessage.role === 'assistant') {
9843
+ status = 'success';
9844
+ }
9845
+
9846
+ // Render the sub-chat item
9847
+ this.renderSubChatAsItem(parentChatId, chatId, chat.parentToolCallId, status);
9848
+ }
9849
+ });
9850
+ }
9851
+
9852
+ /**
9853
+ * Load sub-chat content into a container
9854
+ */
9855
+
9856
+ /**
9857
+ * Update the tool result display in the DOM to show actual results instead of "Processing"
9858
+ */
9859
+ updateToolResultDisplay(chatId, toolCallId, toolResult) {
9860
+ // Skip updating DOM if this tool result was already processed by sub-chat
9861
+ // The sub-chat processing already updated the DOM with summarized content
9862
+ if (toolResult.wasProcessedBySubChat) {
9863
+ return;
9864
+ }
9865
+
9866
+ const container = this.getChatContainer(chatId);
9867
+ if (!container) {
9868
+ console.error('[updateToolResultDisplay] Chat container not found');
9869
+ return;
9870
+ }
9871
+
9872
+ const toolDiv = container.querySelector(`[data-tool-id="${toolCallId}"]`);
9873
+ if (!toolDiv) {
9874
+ console.error('[updateToolResultDisplay] Tool div not found');
9875
+ return;
9876
+ }
9877
+
9878
+ const responseSection = toolDiv.querySelector('.tool-response-section');
9879
+ if (responseSection) {
9880
+ // Update the processing message with actual result
9881
+ const processingDiv = responseSection.querySelector('.sub-chat-processing');
9882
+ if (processingDiv) {
9883
+ // Replace processing message with actual result
9884
+ const resultContent = typeof toolResult.result === 'string'
9885
+ ? toolResult.result
9886
+ : JSON.stringify(toolResult.result, null, 2);
9887
+
9888
+ responseSection.innerHTML = `
9889
+ <div class="tool-result-content">
9890
+ <pre>${this.escapeHtml(resultContent.substring(0, 1000))}${resultContent.length > 1000 ? '...' : ''}</pre>
9891
+ </div>
9892
+ `;
9893
+ }
9894
+ }
9895
+ }
9896
+
9897
+ /**
9898
+ * Recalculate total tokens price for a chat
9899
+ */
9900
+ recalculateTotalTokensPrice(chatId) {
9901
+ const chat = this.chats.get(chatId);
9902
+ if (!chat || !chat.perModelTokensPrice) return;
9903
+
9904
+ const total = {
9905
+ input: 0,
9906
+ output: 0,
9907
+ cacheRead: 0,
9908
+ cacheCreation: 0,
9909
+ totalCost: 0
9910
+ };
9911
+
9912
+ for (const data of Object.values(chat.perModelTokensPrice)) {
9913
+ total.input += data.input || 0;
9914
+ total.output += data.output || 0;
9915
+ total.cacheRead += data.cacheRead || 0;
9916
+ total.cacheCreation += data.cacheCreation || 0;
9917
+ total.totalCost += data.totalCost || 0;
9918
+ }
9919
+
9920
+ chat.totalTokensPrice = total;
9921
+ }
9922
+
9923
scrollToBottom(chatId, force = false) {
9924
if (!chatId) {
9925
console.error('scrollToBottom called without chatId');
10135
this.clearSpinnerState(chatId);
10136
}
10137
10138
+ /**
10139
+ * Shows secondary assistant waiting spinner
10140
+ */
10141
+ showSecondaryAssistantWaiting(chatId) {
10142
+ const chat = this.chats.get(chatId);
10143
+ if (!chat) return;
10144
+
10145
+ this.setSpinnerState(chatId, 'secondary-waiting', 'Waiting secondary assistant...');
10146
+ }
10147
+
10148
+
10149
+ /**
10150
+ * Hides secondary assistant waiting spinner
10151
+ */
10152
+ hideSecondaryAssistantWaiting(chatId) {
10153
+ this.clearSpinnerState(chatId);
10154
+ }
10155
+
10156
/**
10157
* Central method to set spinner state
10158
*/
10165
10166
// Special handling for rate limit countdown
10167
if (chat.isInRateLimitCountdown && type !== 'waiting') {
8408
- console.log(`[setSpinnerState] Ignoring ${type} spinner during rate limit countdown`);
10168
return;
10169
}
10170
10190
*/
10191
clearSpinnerState(chatId) {
10192
const chat = this.chats.get(chatId);
8434
- if (!chat) return;
10193
+ if (!chat) {
10194
+ console.error(`[clearSpinnerState] Chat not found for chatId: ${chatId}`);
10195
+ return;
10196
+ }
10197
10198
// Don't clear spinner if we're in a rate limit countdown
10199
if (chat.isInRateLimitCountdown) {
8438
- console.log('[clearSpinnerState] Preserving rate limit countdown spinner');
10200
return;
10201
}
10202
10385
statusIcon.className = 'status-icon ' + statusClass;
10386
}
10387
8627
- /**
8628
- * Show error and update chat state
8629
- */
8630
- showError(chatId, errorMessage, errorType = 'general') {
8631
- const chat = this.chats.get(chatId);
8632
- if (!chat) {
8633
- console.error(`[showError] Chat not found: ${chatId}`);
8634
- return;
8635
- }
8636
-
8637
- // Clear any spinners
8638
- this.clearSpinnerState(chatId);
8639
-
8640
- // Set error state
8641
- chat.hasError = true;
8642
- chat.lastError = {
8643
- message: errorMessage,
8644
- type: errorType,
8645
- timestamp: Date.now()
8646
- };
8647
-
8648
- // Update tile
8649
- this.updateChatTileStatus(chatId);
8650
-
8651
- // Error display is handled by existing addMessage/processRenderEvent
8652
- }
8653
-
10388
/**
10389
* Clear error state
10390
*/
10648
10649
default:
10650
// For any other states, just log them
8917
- console.log(`Unhandled connection state: ${connectionState}`, details);
10651
+ console.warn(`Unhandled connection state: ${connectionState}`, details);
10652
break;
10653
}
10654
}
10905
10906
const container = this.chatContainers.get(chatId);
10907
if (!container) {
9174
- console.error('updateContextWindowIndicator: container not found for chat', chatId);
10908
+ console.error(`[updateContextWindowIndicator] Container not found for chatId: ${chatId}`);
10909
return;
10910
}
10911
10986
10987
const chat = this.chats.get(chatId);
10988
if (!chat) {
9255
- // console.log('[getCumulativeTokenUsage] Chat not found:', chatId);
9256
- return {
10989
+ return {
10990
inputTokens: 0,
10991
outputTokens: 0,
10992
cacheCreationTokens: 0,
10995
}
10996
10997
if (!chat.totalTokensPrice) {
9265
- // console.log('[getCumulativeTokenUsage] No totalTokensPrice for chat:', chatId);
9266
- return {
10998
+ return {
10999
inputTokens: 0,
11000
outputTokens: 0,
11001
cacheCreationTokens: 0,
11003
};
11004
}
11005
9274
- // console.log('[getCumulativeTokenUsage] totalTokensPrice:', chat.totalTokensPrice);
9275
-
11006
// Use stored token counts
11007
return {
11008
inputTokens: chat.totalTokensPrice.input || 0,
11044
return;
11045
}
11046
11047
+ // Skip token display updates for sub-chats - they don't have token counter UI
11048
+ const chat = this.chats.get(chatId);
11049
+ if (chat && chat.isSubChat) {
11050
+ return;
11051
+ }
11052
+
11053
const cumulative = this.getCumulativeTokenUsage(chatId);
9318
- // console.log('[updateCumulativeTokenDisplay] Cumulative tokens for chat', chatId, cumulative);
9319
-
11054
// Get the chat-specific DOM elements
11055
const container = this.getChatContainer(chatId);
11056
if (!container || !container._elements) {
11058
return;
11059
}
11060
9327
- // console.log('[updateCumulativeTokenDisplay] Container elements:', container._elements);
9328
-
11061
const inputElement = container._elements.cumulativeInputTokens;
11062
const outputElement = container._elements.cumulativeOutputTokens;
11063
const cacheReadElement = container._elements.cumulativeCacheReadTokens;
11064
const cacheCreationElement = container._elements.cumulativeCacheCreationTokens;
11065
9334
- // console.log('[updateCumulativeTokenDisplay] Elements found:', {
9335
- // inputElement: Boolean(inputElement),
9336
- // outputElement: Boolean(outputElement),
9337
- // cacheReadElement: Boolean(cacheReadElement),
9338
- // cacheCreationElement: Boolean(cacheCreationElement)
9339
- // });
9340
-
11066
// Format numbers with k suffix for thousands
11067
const formatTokens = (num) => {
11068
if (num >= 1000) {
11554
});
11555
}
11556
9832
- console.log('Messages before adding summary request:', messages.length, messages.map(m => ({ role: m.role, contentLength: m.content?.length || 0 })));
9833
-
11557
// IMPORTANT: The summary request should be added AFTER building messages
11558
// but BEFORE the system-summary message is added to chat history
11559
// This way it's not included in the conversation being summarized
11564
// For summaries, we can use a simple cache control on the last conversational message
11565
const cacheControlIndex = messages.length - 2; // Before the summary request
11566
9844
- console.log('Total messages being sent:', messages.length);
9845
-
11567
// Send request with low temperature for consistent summaries
11568
const temperature = 0.5;
11569
const response = await this.callAssistant({
11581
return { rateLimitHandled: true };
11582
}
11583
11584
+ // Extract response time from the response object
11585
+ const llmResponseTime = response._responseTime || 0;
11586
+
11587
// Update metrics
11588
if (response.usage) {
11589
this.updateTokenUsage(chat.id, response.usage, ChatConfig.getChatModelString(chat) || provider.model);
11805
}
11806
11807
// Check if automatic summary should be generated
10084
- shouldGenerateSummary(_chat) {
10085
- // Placeholder implementation - customize conditions as needed
10086
- // Examples of conditions you might want:
10087
- // - After X messages
10088
- // - After Y tokens used
10089
- // - After Z time elapsed
10090
- // - When context window is X% full
10091
- // - Every N user messages
10092
-
10093
- // For now, return false - no automatic summaries
10094
- return false;
10095
-
10096
- // Example implementation (uncomment and customize):
10097
- /*
10098
- // Don't summarize if already has a summary
10099
- if (chat.summaryGenerated) return false;
10100
-
10101
- // Check message count (e.g., after 20 exchanges)
10102
- const userMessages = chat.messages.filter(m => m.role === 'user' && !['system-title', 'system-summary'].includes(m.role));
10103
- const assistantMessages = chat.messages.filter(m => m.role === 'assistant');
10104
- if (userMessages.length < 10 || assistantMessages.length < 10) return false;
11808
+ shouldGenerateSummary(chat) {
11809
+ // Check if auto-summarization is enabled
11810
+ if (!chat.config?.optimisation?.autoSummarisation?.enabled) {
11811
+ return false;
11812
+ }
11813
10106
- // Check context window usage (e.g., when 80% full)
10107
- const contextTokens = this.calculateContextWindowTokens(chat.id);
10108
- const modelLimit = this.getModelContextLimit(ChatConfig.getChatModelString(chat));
10109
- if (contextTokens < modelLimit * 0.8) return false;
11814
+ // Don't summarize if we recently created a summary (within 10 minutes)
11815
+ const recentSummary = chat.messages.findLast(m => m.role === 'system-summary');
11816
+ if (recentSummary) {
11817
+ const summaryAge = Date.now() - new Date(recentSummary.timestamp).getTime();
11818
+ if (summaryAge < 10 * 60 * 1000) {
11819
+ console.log('[Auto-summarize] Skipping - recent summary exists', {
11820
+ summaryAge: Math.round(summaryAge / 1000 / 60) + ' minutes'
11821
+ });
11822
+ return false;
11823
+ }
11824
+ }
11825
10111
- // Check time elapsed (e.g., after 30 minutes)
10112
- const firstMessage = chat.messages.find(m => m.timestamp);
10113
- if (firstMessage) {
10114
- const elapsed = Date.now() - new Date(firstMessage.timestamp).getTime();
10115
- if (elapsed < 30 * 60 * 1000) return false;
11826
+ // Need at least a few exchanges before summarizing
11827
+ const userMessages = chat.messages.filter(m => m.role === 'user');
11828
+ const assistantMessages = chat.messages.filter(m => m.role === 'assistant');
11829
+ if (userMessages.length < 3 || assistantMessages.length < 3) {
11830
+ return false;
11831
}
11832
10118
- return true;
10119
- */
11833
+ // Calculate current context window usage
11834
+ const contextTokens = this.calculateContextWindowTokens(chat.id);
11835
+ const modelString = ChatConfig.getChatModelString(chat);
11836
+ const modelLimit = this.modelLimits[modelString] || 128000; // fallback to 128k
11837
+
11838
+ const percentUsed = Math.round((contextTokens / modelLimit) * 100);
11839
+ const triggerPercent = chat.config.optimisation.autoSummarisation.triggerPercent || 50;
11840
+
11841
+ console.log('[Auto-summarize] Context check', {
11842
+ contextTokens,
11843
+ modelLimit,
11844
+ percentUsed: percentUsed + '%',
11845
+ triggerPercent: triggerPercent + '%',
11846
+ willTrigger: percentUsed >= triggerPercent
11847
+ });
11848
+
11849
+ return percentUsed >= triggerPercent;
11850
}
11851
11852
11865
11866
clearCurrentAssistantGroup(chatId) {
11867
const chat = this.chats.get(chatId);
10138
- if (chat) {
10139
- chat.currentAssistantGroup = null;
11868
+ if (!chat) {
11869
+ console.error(`[clearCurrentAssistantGroup] Chat not found for chatId: ${chatId}`);
11870
+ return;
11871
}
11872
+ chat.currentAssistantGroup = null;
11873
}
11874
11875
// Get callbacks for title generation
11893
11894
// Reset global state when switching chats
11895
resetGlobalChatState() {
10164
- // Clear processing state
10165
- this.isProcessing = false;
10166
- this.shouldStopProcessing = false;
11896
+ // isProcessing is now per-chat, no need to reset globally
11897
+ // shouldStopProcessing is now per-chat, no need to reset globally
11898
10168
- // Clear token display state
10169
- this.currentContextWindow = 0;
11899
+ // currentContextWindow is now per-chat, no need to reset globally
11900
11901
// Clear UI state
11902
if (this.spinnerInterval) {
11919
window.migrateCurrentChat = () => {
11920
const activeChatId = window.app.getActiveChatId();
11921
if (!activeChatId) {
10192
- console.log('No chat is currently loaded');
11922
return;
11923
}
11924
const chat = window.app.chats.get(activeChatId);
11925
if (!chat) {
10197
- console.log('Chat not found');
11926
return;
11927
}
10200
- console.log('Migrating chat:', chat.title);
10201
- console.log('Before migration:', {
10202
- totalTokensPrice: chat.totalTokensPrice,
10203
- perModelTokensPrice: chat.perModelTokensPrice
10204
- });
11928
window.app.migrateTokenPricing(chat);
10206
- console.log('After migration:', {
10207
- totalTokensPrice: chat.totalTokensPrice,
10208
- perModelTokensPrice: chat.perModelTokensPrice
10209
- });
11929
// Update displays
11930
window.app.updateAllTokenDisplays(activeChatId);
10212
- console.log('Migration complete!');
11931
};
11932
});
11933