@cryptotaxi247 / netdata-1 / commits / 8da0e7cc9

Mcp5 (#20529)

* working sub-chats, with problems * subchats working * sub-chats and optimization settings * allow zero theshold * fixed syncronization issues * proper editing of markdown messages * edit html/markdown * custom confirmation modals * fix cut llm responses * improve sub-chat prompts * added escalation protocol for secondary assistant to report issues * fixed typo * fixed lint errors * fix summary button * implement auto-summarization in MCP web client - Fixed llmResponseTime reference error in generateChatSummary - Implemented shouldGenerateSummary() logic to check context window usage against configured threshold - Enabled UI controls for auto-summarization (checkbox, threshold dropdown, model selector) - Auto-summarization triggers when context window exceeds configured percentage (30%-90%) - Added 10-minute cooldown to prevent excessive summarization - Requires minimum 3 user/assistant exchanges before summarizing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix MCP segfault when comparing null fields with numeric conditions Fixed a crash in value_matches_condition() that occurred when: 1. A numeric comparison (e.g., "Local Port" > 8000) encounters a NULL field value 2. The code falls back to string comparison but incorrectly accesses v_str from the union even though v_type is COND_VALUE_NUMBER 3. This causes strcmp() to receive an invalid pointer (the binary representation of the numeric value) leading to segmentation fault Fixed an infinite loop bug in MCP web client and null string handling: - Fixed infinite loop in processLLMResponseLoop when LLM responds with only thinking tags - Always add assistant message when no tool calls, preventing repeated requests - Added null check for string values in MCP execute function condition matching * Fix multi-chat input management and rate limit retry detection - Improved multi-chat input isolation: - Always re-enable input for chat that concludes/fails, only focus if active - Eliminates 2 unnecessary getActiveChatId() calls in assistantConcluded/Failed - Prevents background chat completion from interfering with active chat - Fixed rate limit retry detection: - All LLM providers (OpenAI, Anthropic, Google) now include "429" in error messages - Extract and include retry-after headers from rate limit responses - Ensures existing retry logic properly detects and handles rate limits - Version bump to v1.0.67 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix spurious error messages for sub-chats without input elements Sub-chats are background processing chats that don't have UI elements like input fields. The error handling code was incorrectly logging errors when sub-chats didn't have input elements, which is expected behavior. Changes: - Add isSubChat checks before logging input element errors - Only log errors for main chats that should have input elements - Sub-chats continue processing without input elements as expected 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix TypeError: modelLimits.get is not a function Fixed incorrect Map-style access (.get()) on modelLimits object. modelLimits is initialized as {} and used as an object throughout the codebase, but one location incorrectly used Map syntax. Changed: this.modelLimits?.get(modelString) To: this.modelLimits[modelString] 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>

Costa Tsaousis committed Jun 27, 2025 at 20:39 UTC 8da0e7cc91e47b61c3240d7e9c9a0df7a56c4702
14 files changed +3333 -772
src/web/mcp/mcp-tools-execute-function.c
+6 -2
@@ -288,6 +288,10 @@ static bool value_matches_condition(struct json_object *value, const CONDITION *
288 // Try to parse string as number
289 char *endptr;
290 const char *str = json_object_get_string(value);
291 + if (!str) {
292 + // NULL string cannot be converted to number, fall back to string comparison
293 + goto string_compare;
294 + }
295 val_num = strtod(str, &endptr);
296 if (endptr == str || *endptr != '\0') {
297 // Not a valid number, do string comparison
@@ -332,7 +336,7 @@ string_compare:
336 {
337 // String comparisons (including when condition is string or as fallback)
338 const char *val_str = json_object_get_string(value);
335 - const char *cond_str = condition->v_str;
339 + const char *cond_str = (condition->v_type == COND_VALUE_STRING) ? condition->v_str : NULL;
340
341 // Handle NULL condition string
342 if (!cond_str) {
@@ -2405,7 +2409,7 @@ static bool check_requirements_and_violations(MCP_FUNCTION_DATA *data,
2409
2410 // Check for pattern matching with wildcards (except for "*" column which is full-text search)
2411 if (cond->op == OP_MATCH && cond->pattern && strcmp(cond->column_name, "*") != 0) {
2408 - const char *pattern_str = cond->v_str;
2412 + const char *pattern_str = (cond->v_type == COND_VALUE_STRING) ? cond->v_str : NULL;
2413 if (pattern_str && (strchr(pattern_str, '*') || strchr(pattern_str, '?'))) {
2414 invalid_wildcard_patterns = true;
2415 if (buffer_strlen(invalid_conditions) > 0)
src/web/mcp/mcp-web-client/CLAUDE.md
+69 -13
@@ -1,5 +1,35 @@
1 # ASSISTANTS **MUST** FOLLOW THESE RULES
2
3 +## 🚨 PRINCIPLE-001: ERROR VISIBILITY AND FAILURE HANDLING 🚨
4 +
5 +**CRITICAL RULE: WHEN WRITING CODE WE REVEAL UNEXPECTED ERRORS. WE DON'T WORK AROUND THEM.**
6 +
7 +- **UNEXPECTED ERRORS MUST BE PROMINENT** - Make them visible so developers can see and fix them
8 +- **SILENT ERROR HANDLING IS NEVER PERMITTED** - Unless it's expected behavior in normal processing flow
9 +- **UNEXPECTED ERRORS MUST BE LOGGED** - For developers to see (console.error)
10 +- **ERRORS AFFECTING USER FLOW MUST BE SHOWN ON UI** - Users must be informed when something fails
11 +- **ANY CODE NOT COMPLYING IS INCORRECT AND MUST BE FIXED IMMEDIATELY**
12 +
13 +**JavaScript developers have the BAD habit of providing fallbacks and default values on UNEXPECTED things. This is a SEVERE FLAW and IS NOT ACCEPTED in this project.**
14 +
15 +**Every fallback, default, and workaround MUST HAVE SPECIFIC BUSINESS LOGIC REASONING, otherwise it MUST be immediately fixed to log and FAIL.**
16 +
17 +**Examples of INCORRECT patterns:**
18 +```javascript
19 +// WRONG - Silent failure
20 +const chat = this.chats.get(chatId) || {};
21 +
22 +// WRONG - Fallback without reasoning
23 +const result = data?.property || 'default';
24 +
25 +// CORRECT - Explicit error handling
26 +const chat = this.chats.get(chatId);
27 +if (!chat) {
28 + console.error(`[functionName] Chat not found for chatId: ${chatId}`);
29 + return; // or throw, or show UI error
30 +}
31 +```
32 +
33 1. This is a new application. No need for backward compatibility.
34 2. When a task is concluded, existing `eslint` configuration MUST show zero errors and zero warnings.
35 3. Do not use any deprecated or legacy code. This is a new application, so everything should be up-to-date.
@@ -85,10 +115,32 @@ When building messages for the LLM API:
115
116 ## Cache Control Management
117
118 +### Cache Control Modes
119 +The cache control system now uses a simple 3-value configuration:
120 +
121 +- **`all-off`**: No cache control headers are emitted (default for new chats)
122 +- **`system`**: Cache control is applied ONLY to the system prompt
123 +- **`cached`**: Smart cache control - caches system prompt AND applies message-level caching
124 +
125 +### Configuration Changes
126 +- **Old Format**: `{ enabled: boolean, strategy: string }`
127 +- **New Format**: Single string value (`'all-off'`, `'system'`, `'cached'`)
128 +- **Migration**: Old configs are automatically migrated on load
129 +- **UI**: Dropdown selection instead of checkbox + mutual exclusivity
130 +
131 ### Cache Position Tracking
132 - Each assistant message stores `cacheControlIndex` indicating where cache control was applied
133 - This allows freezing the cache position for cost-effective operations
134
135 +### System Prompt Caching
136 +- **`system` mode**: Only the system prompt gets cache control headers
137 +- **`cached` mode**: System prompt + message-level smart caching
138 +- **`all-off` mode**: No caching anywhere
139 +
140 +### Message-Level Caching (cached mode only)
141 +- Uses smart strategy: caches up to 70% of messages, avoiding recent tool results
142 +- Can be frozen during summary operations to prevent cache creation surcharge
143 +
144 ### Frozen Cache for Summaries
145 - When requesting a summary, `buildMessagesForAPI(chat, provider, true)` freezes the cache
146 - The cache control mark stays at its previous position instead of advancing
@@ -111,14 +163,18 @@ The context window includes:
163 ### Why Include Completion Tokens
164 The assistant's response (completion tokens) becomes part of the conversation history sent in the next request, so they must be counted as part of the context.
165
114 -## Tool Inclusion Modes
166 +## Tool Filtering
167 +
168 +Tool filtering is now handled exclusively by the Message Optimizer:
169 +- **Single Source of Truth**: The Message Optimizer determines which tools to include
170 +- **No Provider Filtering**: LLM providers no longer have tool filtering logic
171 +- **Context-Aware**: Tools are filtered based on Tool Memory settings and conversation context
172 +- **Automatic**: No manual tool inclusion modes - all handled transparently
173
116 -The `toolInclusionMode` property controls how tools are included:
117 -- `auto` - Automatic inclusion based on context
118 -- `cached` - Always include tools with cache control (default)
119 -- `all-on` - Include all tools
120 -- `all-off` - Exclude all tools
121 -- `manual` - User controls individual tool inclusion
174 +### Removed Concepts
175 +- `toolInclusionMode` parameter is no longer used in providers
176 +- Manual tool filtering has been removed in favor of automatic optimization
177 +- All tool filtering logic consolidated in the Message Optimizer
178
179 ## Summary Workflow
180
@@ -295,9 +351,9 @@ Result: When in Turn 2, tools A,B are filtered, but C,D are still visible
351 ### Purpose
352 This feature helps manage context window size and reduces costs by automatically removing old tool interactions that are no longer relevant to the current conversation flow.
353
298 -### Important: Mutual Exclusivity with Cache Control
299 -For Anthropic models, Tool Memory and Cache Control are mutually exclusive features:
300 -- When Tool Memory is enabled, Cache Control is automatically disabled
301 -- This prevents wasting money on caching content that will be filtered out
302 -- The cached content would include tools that Tool Memory removes in later turns
303 -- The UI enforces this by disabling the cache control option when tool memory is active
\ No newline at end of file
354 +### Cache Control Interaction
355 +Tool Memory and Cache Control can now be used together:
356 +- **Independent Features**: Tool Memory filtering and Cache Control are separate optimizations
357 +- **No Mutual Exclusivity**: Users can enable both features simultaneously
358 +- **Smart Optimization**: The Message Optimizer handles both features intelligently
359 +- **Cost Efficiency**: Tool Memory reduces context size, Cache Control reduces repeated processing
\ No newline at end of file
src/web/mcp/mcp-web-client/llm-proxy.js
+4 -3
@@ -1658,13 +1658,14 @@ const server = http.createServer(async (req, res) => {
1658 }
1659 });
1660
1661 - // Collect request body
1662 - let body = '';
1661 + // Collect request body using Buffer for proper handling of large payloads
1662 + const chunks = [];
1663 req.on('data', chunk => {
1664 - body += chunk.toString();
1664 + chunks.push(chunk);
1665 });
1666
1667 req.on('end', () => {
1668 + const body = Buffer.concat(chunks).toString();
1669 // Prepare options for the outgoing request
1670 const options = {
1671 hostname: targetUrl.hostname,
src/web/mcp/mcp-web-client/web/app.js
+2198 -480
@@ -11,7 +11,7 @@ import {SafetyChecker, SafetyLimitError, SAFETY_LIMITS} from './safety-limits.js
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,9 +20,9 @@ class NetdataMCPChat {
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,7 +31,7 @@ class NetdataMCPChat {
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,7 +99,7 @@ class NetdataMCPChat {
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,6 +204,9 @@ class NetdataMCPChat {
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
@@ -311,6 +314,16 @@ class NetdataMCPChat {
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
@@ -338,6 +351,26 @@ class NetdataMCPChat {
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
@@ -409,6 +442,34 @@ class NetdataMCPChat {
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: ![alt](url)
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
@@ -439,7 +500,10 @@ class NetdataMCPChat {
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);
@@ -517,7 +581,7 @@ class NetdataMCPChat {
581 if (!chat.perModelTokensPrice) {
582 chat.perModelTokensPrice = {};
583 }
520 -
584 +
585 // Reset totals
586 chat.totalTokensPrice = {
587 input: 0,
@@ -575,6 +639,87 @@ class NetdataMCPChat {
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
@@ -652,7 +797,6 @@ class NetdataMCPChat {
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;
@@ -920,10 +1064,16 @@ class NetdataMCPChat {
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');
@@ -1016,6 +1166,11 @@ class NetdataMCPChat {
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 = `
@@ -1153,44 +1308,45 @@ class NetdataMCPChat {
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>
@@ -1199,24 +1355,24 @@ class NetdataMCPChat {
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>
@@ -1225,13 +1381,13 @@ class NetdataMCPChat {
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>
@@ -1242,10 +1398,10 @@ class NetdataMCPChat {
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;">
@@ -1270,10 +1426,10 @@ class NetdataMCPChat {
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;">
@@ -1296,20 +1452,27 @@ class NetdataMCPChat {
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);
@@ -1404,8 +1567,9 @@ class NetdataMCPChat {
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
@@ -1451,28 +1615,7 @@ class NetdataMCPChat {
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 });
@@ -1483,6 +1626,16 @@ class NetdataMCPChat {
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) => {
@@ -1618,8 +1771,9 @@ class NetdataMCPChat {
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>
@@ -1876,6 +2030,30 @@ class NetdataMCPChat {
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
@@ -1907,30 +2085,6 @@ class NetdataMCPChat {
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)) {
@@ -2147,31 +2301,6 @@ class NetdataMCPChat {
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
@@ -2227,7 +2356,10 @@ class NetdataMCPChat {
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);
@@ -2240,9 +2372,6 @@ class NetdataMCPChat {
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;
@@ -2276,10 +2405,47 @@ class NetdataMCPChat {
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);
@@ -2306,7 +2472,10 @@ class NetdataMCPChat {
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);
@@ -2319,7 +2488,10 @@ class NetdataMCPChat {
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);
@@ -2332,7 +2504,10 @@ class NetdataMCPChat {
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
@@ -2911,7 +3086,7 @@ class NetdataMCPChat {
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, {
@@ -2939,8 +3114,8 @@ class NetdataMCPChat {
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
@@ -3055,9 +3230,9 @@ class NetdataMCPChat {
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 }
@@ -3168,6 +3343,25 @@ class NetdataMCPChat {
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) {
@@ -3182,28 +3376,7 @@ class NetdataMCPChat {
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
@@ -3252,23 +3425,108 @@ class NetdataMCPChat {
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}`;
@@ -3322,7 +3580,7 @@ class NetdataMCPChat {
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
@@ -3400,7 +3658,7 @@ class NetdataMCPChat {
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
@@ -3419,20 +3677,29 @@ class NetdataMCPChat {
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 }
@@ -3467,7 +3734,7 @@ class NetdataMCPChat {
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
@@ -3483,20 +3750,105 @@ class NetdataMCPChat {
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
@@ -3520,20 +3872,32 @@ class NetdataMCPChat {
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);
@@ -3961,10 +4325,17 @@ class NetdataMCPChat {
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() {
@@ -4214,8 +4585,6 @@ class NetdataMCPChat {
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`);
@@ -4226,8 +4595,6 @@ class NetdataMCPChat {
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);
@@ -4557,7 +4924,18 @@ class NetdataMCPChat {
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) {
@@ -4597,16 +4975,15 @@ class NetdataMCPChat {
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;
@@ -4674,21 +5051,53 @@ class NetdataMCPChat {
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) {
@@ -4791,11 +5200,12 @@ class NetdataMCPChat {
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>
@@ -4855,13 +5265,18 @@ class NetdataMCPChat {
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 {
@@ -4875,11 +5290,12 @@ class NetdataMCPChat {
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) {
@@ -4891,6 +5307,37 @@ class NetdataMCPChat {
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) {
@@ -4965,10 +5412,10 @@ class NetdataMCPChat {
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 }
@@ -4983,7 +5430,6 @@ class NetdataMCPChat {
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 }
@@ -4995,7 +5441,9 @@ class NetdataMCPChat {
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;
@@ -5012,8 +5460,7 @@ class NetdataMCPChat {
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;
@@ -5081,12 +5528,6 @@ class NetdataMCPChat {
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) {
@@ -5154,9 +5595,9 @@ class NetdataMCPChat {
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
@@ -5165,9 +5606,9 @@ class NetdataMCPChat {
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
@@ -5191,6 +5632,11 @@ class NetdataMCPChat {
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;
@@ -5258,7 +5704,12 @@ class NetdataMCPChat {
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
@@ -5276,8 +5727,10 @@ class NetdataMCPChat {
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) {
@@ -5330,6 +5783,7 @@ class NetdataMCPChat {
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;}
@@ -5566,7 +6020,6 @@ class NetdataMCPChat {
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
@@ -5575,7 +6028,6 @@ class NetdataMCPChat {
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
@@ -5622,13 +6074,9 @@ class NetdataMCPChat {
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, {
@@ -5683,23 +6131,23 @@ class NetdataMCPChat {
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
@@ -5711,9 +6159,9 @@ class NetdataMCPChat {
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
@@ -5728,21 +6176,21 @@ class NetdataMCPChat {
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
@@ -5758,7 +6206,8 @@ class NetdataMCPChat {
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
@@ -5919,6 +6368,9 @@ class NetdataMCPChat {
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
@@ -5951,13 +6403,20 @@ class NetdataMCPChat {
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, '&amp;')
6415 + .replace(/</g, '&lt;')
6416 + .replace(/>/g, '&gt;')
6417 + .replace(/\n/g, '<br>');
6418 + chatInput.innerHTML = htmlContent;
6419 +
6420 // Update send button state
6421 const sendBtn = container._elements.sendBtn;
6422 if (sendBtn) {
@@ -6083,7 +6542,9 @@ class NetdataMCPChat {
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
@@ -6195,7 +6656,31 @@ class NetdataMCPChat {
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
@@ -6270,8 +6755,8 @@ class NetdataMCPChat {
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';
@@ -6297,7 +6782,10 @@ class NetdataMCPChat {
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) {
@@ -6384,13 +6872,34 @@ class NetdataMCPChat {
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
@@ -6471,8 +6980,9 @@ class NetdataMCPChat {
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');
@@ -6481,7 +6991,24 @@ class NetdataMCPChat {
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
@@ -6490,8 +7017,27 @@ class NetdataMCPChat {
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);
@@ -6535,13 +7081,21 @@ class NetdataMCPChat {
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) {
@@ -6549,7 +7103,13 @@ class NetdataMCPChat {
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 }
@@ -6576,9 +7136,15 @@ class NetdataMCPChat {
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
@@ -6608,7 +7174,7 @@ class NetdataMCPChat {
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(
@@ -6643,23 +7209,28 @@ class NetdataMCPChat {
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
@@ -6741,10 +7312,10 @@ class NetdataMCPChat {
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
@@ -6832,14 +7403,7 @@ class NetdataMCPChat {
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);
@@ -6855,7 +7419,7 @@ class NetdataMCPChat {
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
@@ -6897,8 +7461,8 @@ class NetdataMCPChat {
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
@@ -7026,8 +7590,11 @@ class NetdataMCPChat {
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';
@@ -7179,14 +7746,22 @@ class NetdataMCPChat {
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 }
@@ -7728,6 +8303,12 @@ class NetdataMCPChat {
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');
@@ -7783,14 +8364,14 @@ class NetdataMCPChat {
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
@@ -7812,6 +8393,297 @@ class NetdataMCPChat {
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(/&nbsp;/gi, ' ')
8478 +
8479 + // Remove any remaining HTML tags
8480 + .replace(/<[^>]*>/g, '')
8481 +
8482 + // Decode HTML entities
8483 + .replace(/&lt;/g, '<')
8484 + .replace(/&gt;/g, '>')
8485 + .replace(/&amp;/g, '&')
8486 + .replace(/&quot;/g, '"')
8487 + .replace(/&#39;/g, "'")
8488 + .replace(/&apos;/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(/&nbsp;/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 ? `![${alt}](${src})` : `![](${src})`;
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(/&nbsp;/gi, ' ');
8670 + result = result.replace(/&lt;/g, '<');
8671 + result = result.replace(/&gt;/g, '>');
8672 + result = result.replace(/&amp;/g, '&');
8673 + result = result.replace(/&quot;/g, '"');
8674 + result = result.replace(/&#39;/g, "'");
8675 + result = result.replace(/&apos;/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
@@ -7849,10 +8721,22 @@ class NetdataMCPChat {
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, '&amp;') // Escape ampersands first
8735 + .replace(/</g, '&lt;') // Escape less-than
8736 + .replace(/>/g, '&gt;') // 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();
@@ -7896,17 +8780,21 @@ class NetdataMCPChat {
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;
@@ -7921,8 +8809,25 @@ class NetdataMCPChat {
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, '&amp;')
8817 + .replace(/</g, '&lt;')
8818 + .replace(/>/g, '&gt;')
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;
@@ -7968,9 +8873,29 @@ class NetdataMCPChat {
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">
@@ -7994,15 +8919,15 @@ class NetdataMCPChat {
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);
@@ -8051,7 +8976,7 @@ class NetdataMCPChat {
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;
@@ -8128,6 +9053,8 @@ class NetdataMCPChat {
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>`;
@@ -8135,7 +9062,18 @@ class NetdataMCPChat {
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
@@ -8158,6 +9096,9 @@ class NetdataMCPChat {
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
@@ -8166,6 +9107,9 @@ class NetdataMCPChat {
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
@@ -8178,6 +9122,804 @@ class NetdataMCPChat {
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');
@@ -8393,6 +10135,24 @@ class NetdataMCPChat {
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 */
@@ -8405,7 +10165,6 @@ class NetdataMCPChat {
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
@@ -8431,11 +10190,13 @@ class NetdataMCPChat {
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
@@ -8624,33 +10385,6 @@ class NetdataMCPChat {
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 */
@@ -8914,7 +10648,7 @@ class NetdataMCPChat {
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 }
@@ -9171,7 +10905,7 @@ class NetdataMCPChat {
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
@@ -9252,8 +10986,7 @@ class NetdataMCPChat {
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,
@@ -9262,8 +10995,7 @@ class NetdataMCPChat {
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,
@@ -9271,8 +11003,6 @@ class NetdataMCPChat {
11003 };
11004 }
11005
9274 - // console.log('[getCumulativeTokenUsage] totalTokensPrice:', chat.totalTokensPrice);
9275 -
11006 // Use stored token counts
11007 return {
11008 inputTokens: chat.totalTokensPrice.input || 0,
@@ -9314,9 +11044,13 @@ class NetdataMCPChat {
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) {
@@ -9324,20 +11058,11 @@ class NetdataMCPChat {
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) {
@@ -9829,8 +11554,6 @@ class NetdataMCPChat {
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
@@ -9841,8 +11564,6 @@ class NetdataMCPChat {
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({
@@ -9860,6 +11581,9 @@ class NetdataMCPChat {
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);
@@ -10081,42 +11805,48 @@ class NetdataMCPChat {
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
@@ -10135,9 +11865,11 @@ class NetdataMCPChat {
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
@@ -10161,12 +11893,10 @@ class NetdataMCPChat {
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) {
@@ -10189,27 +11919,15 @@ document.addEventListener('DOMContentLoaded', () => {
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
src/web/mcp/mcp-web-client/web/chat-config.js
+20 -16
@@ -3,8 +3,8 @@
3 // Default configuration schema
4 const DEFAULT_CONFIG = {
5 model: {
6 - provider: "anthropic",
7 - id: "claude-3-haiku-20240307",
6 + provider: 'anthropic',
7 + id: 'claude-sonnet-4-20250514',
8 params: {
9 temperature: 0.7,
10 topP: 0.9,
@@ -30,15 +30,12 @@ const DEFAULT_CONFIG = {
30 enabled: true,
31 forgetAfterConclusions: 0
32 },
33 - cacheControl: {
34 - enabled: false,
35 - strategy: 'smart'
36 - },
33 + cacheControl: 'system',
34 titleGeneration: {
35 enabled: true,
36 model: {
40 - provider: "google",
41 - id: "gemini-1.5-flash-8b",
37 + provider: 'google',
38 + id: 'gemini-1.5-flash-8b',
39 params: {
40 temperature: 0.7,
41 topP: 0.9,
@@ -51,7 +48,7 @@ const DEFAULT_CONFIG = {
48 }
49 }
50 },
54 - mcpServer: "prod_aws_parent0"
51 + mcpServer: 'prod_aws_parent0'
52 };
53
54 // Feature-specific default model parameters
@@ -207,9 +204,12 @@ export function normalizeConfig(config) {
204 }
205
206 // Cache Control normalization
210 - if (!opt.cacheControl || typeof opt.cacheControl !== 'object') {
211 - console.warn('[normalizeConfig] cacheControl missing, creating default');
212 - opt.cacheControl = { enabled: false, strategy: 'smart' };
207 + if (!opt.cacheControl || typeof opt.cacheControl !== 'string') {
208 + // Don't warn for missing cacheControl - it's expected for old chats
209 + opt.cacheControl = 'all-off';
210 + } else if (!['all-off', 'system', 'cached'].includes(opt.cacheControl)) {
211 + console.warn('[normalizeConfig] Invalid cacheControl value:', opt.cacheControl, '- using default');
212 + opt.cacheControl = 'all-off';
213 }
214
215 // MCP Server validation happens in app.js where the server list is available
@@ -265,10 +265,14 @@ export function migrateConfig(oldConfig) {
265 }
266
267 if (oldConfig.cacheControl) {
268 - newConfig.optimisation.cacheControl = {
269 - enabled: oldConfig.cacheControl.enabled || false,
270 - strategy: oldConfig.cacheControl.strategy || 'smart'
271 - };
268 + // Migrate old cache control format to new format
269 + if (oldConfig.cacheControl.enabled) {
270 + // If enabled, use cached mode (keeps current strategy behavior)
271 + newConfig.optimisation.cacheControl = 'cached';
272 + } else {
273 + // If disabled, use all-off mode
274 + newConfig.optimisation.cacheControl = 'all-off';
275 + }
276 }
277
278 if (oldConfig.mcpServer) {
src/web/mcp/mcp-web-client/web/eslint.config.js
+30 -3
@@ -1,6 +1,11 @@
1 - export default [
1 + import sonarjs from 'eslint-plugin-sonarjs';
2 +
3 +export default [
4 {
5 files: ['**/*.js'],
6 + plugins: {
7 + sonarjs
8 + },
9 languageOptions: {
10 ecmaVersion: 2022,
11 sourceType: 'module',
@@ -109,8 +114,10 @@
114 'no-unused-vars': ['error', {
115 'argsIgnorePattern': '^_',
116 'varsIgnorePattern': '^_',
112 - 'caughtErrorsIgnorePattern': '^_'
117 + 'caughtErrorsIgnorePattern': '^_',
118 + 'ignoreRestSiblings': true
119 }],
120 + 'no-unused-private-class-members': 'error',
121 'no-console': 'off',
122 'no-constant-condition': ['error', { 'checkLoops': false }],
123 'no-empty': ['error', { 'allowEmptyCatch': true }],
@@ -122,6 +129,7 @@
129 'no-duplicate-case': 'error',
130 'no-dupe-keys': 'error',
131 'no-dupe-args': 'error',
132 + 'no-dupe-class-members': 'error',
133 'no-sparse-arrays': 'error',
134 'no-func-assign': 'error',
135 'no-invalid-regexp': 'error',
@@ -181,7 +189,26 @@
189 'prefer-rest-params': 'error',
190 'prefer-spread': 'error',
191 'rest-spread-spacing': ['error', 'never'],
184 - 'template-curly-spacing': ['error', 'never']
192 + 'template-curly-spacing': ['error', 'never'],
193 +
194 + // SonarJS duplicate detection rules
195 + 'sonarjs/no-identical-functions': 'error',
196 + 'sonarjs/no-duplicated-branches': 'error',
197 + 'sonarjs/no-identical-conditions': 'error',
198 + 'sonarjs/no-identical-expressions': 'error',
199 +
200 + // SonarJS code quality rules
201 + 'sonarjs/no-redundant-assignments': 'error',
202 + 'sonarjs/no-unused-collection': 'error',
203 + 'sonarjs/no-useless-catch': 'error',
204 + 'sonarjs/prefer-immediate-return': 'warn',
205 + 'sonarjs/no-all-duplicated-branches': 'error',
206 + 'sonarjs/no-element-overwrite': 'error',
207 + 'sonarjs/no-empty-collection': 'warn',
208 + 'sonarjs/no-one-iteration-loop': 'error',
209 + 'sonarjs/no-redundant-jump': 'error',
210 + 'sonarjs/prefer-object-literal': 'warn',
211 + 'sonarjs/prefer-single-boolean-return': 'warn'
212 }
213 }
214 ];
\ No newline at end of file
src/web/mcp/mcp-web-client/web/llm-providers.js
+348 -184
@@ -488,7 +488,7 @@ class LLMProvider {
488 * @param {number|null} _cachePosition - Cache position for Anthropic
489 * @returns {Promise<LLMResponse>}
490 */
491 - async sendMessage(_messages, _tools = [], _temperature = 0.7, _mode = 'cached', _cachePosition = null) {
491 + async sendMessage(_messages, _tools, _temperature, _mode, _cachePosition = null, _chat = null) {
492 const error = 'sendMessage must be implemented by subclass';
493 console.error('[LLMProvider]', error);
494 throw new Error(error);
@@ -510,6 +510,77 @@ class LLMProvider {
510 }
511 }
512
513 + /**
514 + * Check if tool metadata should be injected
515 + * @returns {boolean}
516 + */
517 + shouldInjectToolMetadata(chat) {
518 + // If no chat provided, can't inject metadata
519 + if (!chat) {
520 + return false;
521 + }
522 +
523 + // Don't inject metadata in sub-chats to prevent recursion
524 + if (chat.isSubChat) {
525 + return false;
526 + }
527 +
528 + // Check if tool summarization is enabled
529 + return chat.config?.optimisation?.toolSummarisation?.enabled === true;
530 + }
531 +
532 + /**
533 + * Inject metadata fields into tool schema
534 + * @param {Object} tool - The tool object
535 + * @param {Object} chat - The chat context
536 + * @returns {Object} - Tool with injected metadata fields
537 + */
538 + injectToolMetadata(tool, chat) {
539 + if (!this.shouldInjectToolMetadata(chat)) {
540 + return tool;
541 + }
542 +
543 + // Clone the tool to avoid modifying the original
544 + const modifiedTool = JSON.parse(JSON.stringify(tool));
545 +
546 + // Ensure inputSchema exists
547 + if (!modifiedTool.inputSchema) {
548 + modifiedTool.inputSchema = { type: 'object', properties: {} };
549 + }
550 + if (!modifiedTool.inputSchema.properties) {
551 + modifiedTool.inputSchema.properties = {};
552 + }
553 +
554 + // Inject metadata fields
555 + const metadataFields = {
556 + tool_purpose: {
557 + type: 'string',
558 + description: 'Why this tool is being used in the context of the user query'
559 + },
560 + expected_format: {
561 + type: 'string',
562 + description: 'Expected structure or format of the data you want from this tool'
563 + },
564 + key_information: {
565 + type: 'string',
566 + description: 'Specific values, patterns, or information to extract from the response'
567 + },
568 + success_indicators: {
569 + type: 'string',
570 + description: 'How to determine if the tool response is useful'
571 + },
572 + context_for_interpretation: {
573 + type: 'string',
574 + description: 'Additional context from the user discussion that may needed to interpret the tool results'
575 + }
576 + };
577 +
578 + // Add metadata fields to the tool schema
579 + Object.assign(modifiedTool.inputSchema.properties, metadataFields);
580 +
581 + return modifiedTool;
582 + }
583 +
584 /**
585 * Check request size before sending to API
586 * @param {Object} requestBody - The request body to check
@@ -540,6 +611,50 @@ class LLMProvider {
611 maxBytes: maxSizeBytes
612 });
613 }
614 +
615 + // Tool filtering removed - now handled entirely by message optimizer
616 + // The mode parameter now controls cache control behavior only
617 +
618 + /**
619 + * Get timezone info
620 + * @returns {{name: string, offset: string}}
621 + */
622 + getTimezoneInfo() {
623 + const date = new Date();
624 + const offset = date.getTimezoneOffset();
625 + const absOffset = Math.abs(offset);
626 + const hours = Math.floor(absOffset / 60);
627 + const minutes = absOffset % 60;
628 + const sign = offset <= 0 ? '+' : '-';
629 + const offsetString = `UTC${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
630 +
631 + let timezoneName;
632 + try {
633 + // This returns something like "America/New_York"
634 + timezoneName = Intl.DateTimeFormat().resolvedOptions().timeZone;
635 + } catch {
636 + // Fallback to basic timezone string
637 + timezoneName = date.toString().match(/\(([^)]+)\)/)?.[1] || offsetString;
638 + }
639 +
640 + return {
641 + name: timezoneName,
642 + offset: offsetString
643 + };
644 + }
645 +
646 + /**
647 + * Add datetime prefix to user message content
648 + * @param {string} content - The original user message content
649 + * @param {string} timestamp - The message timestamp (ISO string)
650 + * @returns {string} The content with datetime prefix
651 + */
652 + addDateTimePrefix(content, timestamp) {
653 + // Use the message's timestamp, fallback to current time if not provided
654 + const messageDateTime = timestamp || new Date().toISOString();
655 + const timezoneInfo = this.getTimezoneInfo();
656 + return `Current datetime in rfc3339: ${messageDateTime}, timezone: ${timezoneInfo.name}\n\n${content}`;
657 + }
658 }
659
660 /**
@@ -599,7 +714,7 @@ class OpenAIProvider extends LLMProvider {
714 * @param {number|null} _cachePosition - Cache position (unused for OpenAI)
715 * @returns {Promise<LLMResponse>}
716 */
602 - async sendMessage(messages, tools = [], temperature = 0.7, mode = 'cached', _cachePosition = null) {
717 + async sendMessage(messages, tools, temperature, mode, _cachePosition = null, chat = null) {
718 // Check model configuration for endpoint and tool support
719 const modelConfig = MODEL_ENDPOINT_CONFIG[this.model];
720 const useResponsesEndpoint = modelConfig && modelConfig.endpoint === 'responses';
@@ -612,22 +727,28 @@ class OpenAIProvider extends LLMProvider {
727 const openaiMessages = this.convertMessages(messages, mode);
728
729 // Convert tools to OpenAI completions format (with nested function)
615 - const openaiCompletionsTools = tools.map(tool => ({
616 - type: 'function',
617 - function: {
618 - name: tool.name,
619 - description: tool.description,
620 - parameters: tool.inputSchema || {}
621 - }
622 - }));
730 + const openaiCompletionsTools = tools.map(tool => {
731 + const injectedTool = this.injectToolMetadata(tool, chat);
732 + return {
733 + type: 'function',
734 + function: {
735 + name: injectedTool.name,
736 + description: injectedTool.description,
737 + parameters: injectedTool.inputSchema || {}
738 + }
739 + };
740 + });
741
742 // Convert tools to OpenAI responses format (requires type and name fields)
625 - const openaiResponsesTools = tools.map(tool => ({
626 - type: 'function',
627 - name: tool.name,
628 - description: tool.description,
629 - parameters: tool.inputSchema || {}
630 - }));
743 + const openaiResponsesTools = tools.map(tool => {
744 + const injectedTool = this.injectToolMetadata(tool, chat);
745 + return {
746 + type: 'function',
747 + name: injectedTool.name,
748 + description: injectedTool.description,
749 + parameters: injectedTool.inputSchema || {}
750 + };
751 + });
752
753 let requestBody;
754
@@ -671,17 +792,14 @@ class OpenAIProvider extends LLMProvider {
792 }
793
794 // Build request for v1/responses endpoint
674 - requestBody = {
675 - model: this.model,
676 - input: inputMessages,
677 - max_output_tokens: 4096,
678 - stream: false,
679 - store: true
680 - };
795 + // Order fields consistently: tools → instructions (system) → input (messages) → model
796 + requestBody = {};
797
682 - // O3/O1 models don't support temperature parameter
683 - if (!this.model.startsWith('o3') && !this.model.startsWith('o1')) {
684 - requestBody.temperature = temperature;
798 + // Add tools first if supported
799 + if (supportsTools && openaiResponsesTools.length > 0) {
800 + requestBody.tools = openaiResponsesTools;
801 + requestBody.tool_choice = 'auto';
802 + requestBody.parallel_tool_calls = true;
803 }
804
805 // Add system prompt as instructions
@@ -689,11 +807,18 @@ class OpenAIProvider extends LLMProvider {
807 requestBody.instructions = systemPrompt;
808 }
809
692 - // Add tools if supported - responses endpoint format
693 - if (supportsTools && openaiResponsesTools.length > 0) {
694 - requestBody.tools = openaiResponsesTools;
695 - requestBody.tool_choice = 'auto';
696 - requestBody.parallel_tool_calls = true;
810 + // Add messages
811 + requestBody.input = inputMessages;
812 +
813 + // Add model and other parameters
814 + requestBody.model = this.model;
815 + requestBody.max_output_tokens = 4096;
816 + requestBody.stream = false;
817 + requestBody.store = true;
818 +
819 + // O3/O1 models don't support temperature parameter
820 + if (!this.model.startsWith('o3') && !this.model.startsWith('o1')) {
821 + requestBody.temperature = temperature;
822 }
823
824 // Optional: Add reasoning configuration for o3/o1 models
@@ -705,11 +830,12 @@ class OpenAIProvider extends LLMProvider {
830 }
831 } else {
832 // Regular models use standard v1/chat/completions structure
833 + // Order fields consistently: tools → messages → model
834 requestBody = {
709 - model: this.model,
710 - messages: openaiMessages,
835 tools: openaiCompletionsTools.length > 0 ? openaiCompletionsTools : undefined,
836 tool_choice: openaiCompletionsTools.length > 0 ? 'auto' : undefined,
837 + messages: openaiMessages,
838 + model: this.model,
839 temperature,
840 max_tokens: 4096
841 };
@@ -724,6 +850,9 @@ class OpenAIProvider extends LLMProvider {
850 // Check request size before sending
851 this.checkRequestSize(requestBody);
852
853 + // Log the full request
854 + console.log(`[OPENAI SEND] (${mode}, subchat: ${chat?.isSubChat || false}):`, requestBody);
855 +
856 let response;
857 try {
858 response = await fetch(this.apiUrl, {
@@ -754,6 +883,22 @@ class OpenAIProvider extends LLMProvider {
883 status: response.status,
884 statusText: response.statusText
885 });
886 +
887 + // Special handling for rate limit errors (429)
888 + if (response.status === 429) {
889 + const retryAfter = response.headers.get('retry-after') || response.headers.get('x-ratelimit-reset-after');
890 + const baseMessage = error.error?.message || 'Rate limit exceeded';
891 + let rateLimitMessage = `Rate limit exceeded: ${baseMessage}`;
892 +
893 + if (retryAfter) {
894 + rateLimitMessage += ` (retry after ${retryAfter}s)`;
895 + }
896 +
897 + const apiError = `OpenAI API error: ${rateLimitMessage} (429)`;
898 + console.error('[OpenAIProvider] Rate limit error:', apiError, '\nStatus:', response.status, '\nResponse:', error);
899 + throw new Error(apiError);
900 + }
901 +
902 const apiError = `OpenAI API error: ${error.error?.message || response.statusText}`;
903 console.error('[OpenAIProvider] API error:', apiError, '\nStatus:', response.status, '\nResponse:', error);
904 throw new Error(apiError);
@@ -763,6 +908,9 @@ class OpenAIProvider extends LLMProvider {
908 const data = await response.json();
909 this.log('received', JSON.stringify(data, null, 2), { provider: 'openai' });
910
911 + // Log the full response
912 + console.log(`[OPENAI RECEIVED] (${mode}, subchat: ${chat?.isSubChat || false}):`, data);
913 +
914 let choice;
915
916 if (useResponsesEndpoint) {
@@ -1177,7 +1325,7 @@ class OpenAIProvider extends LLMProvider {
1325 return 'call_' + Math.random().toString(36).substring(2, 11);
1326 }
1327
1180 - convertMessages(messages, _mode = 'cached') {
1328 + convertMessages(messages, _mode) {
1329 // Check if we're using the responses endpoint
1330 const modelConfig = MODEL_ENDPOINT_CONFIG[this.model];
1331 const useResponsesEndpoint = modelConfig && modelConfig.endpoint === 'responses';
@@ -1209,7 +1357,7 @@ class OpenAIProvider extends LLMProvider {
1357 if (msgRole === 'user') {
1358 converted.push({
1359 role: 'user',
1212 - content: msg.content
1360 + content: this.addDateTimePrefix(msg.content, msg.timestamp)
1361 });
1362 } else if (msgRole === 'assistant') {
1363 // Extract text content and tool calls from message
@@ -1328,7 +1476,7 @@ class AnthropicProvider extends LLMProvider {
1476 * @param {number|null} cachePosition - Cache position for Anthropic
1477 * @returns {Promise<LLMResponse>}
1478 */
1331 - async sendMessage(messages, tools = [], temperature = 0.7, mode = 'cached', cachePosition = null) {
1479 + async sendMessage(messages, tools, temperature, mode, cachePosition = null, chat = null) {
1480 // Validate messages before processing
1481 validateMessagesForAPI(messages);
1482
@@ -1337,38 +1485,51 @@ class AnthropicProvider extends LLMProvider {
1485
1486 if (mode === 'cached') {
1487 // Use the caching version which returns different format
1340 - const result = this.convertMessagesWithCaching(messages, cachePosition, mode);
1488 + const result = this.convertMessagesWithCaching(messages, mode, cachePosition);
1489 anthropicMessages = result.converted;
1490 // Extract system from original messages for cached mode
1491 const systemMsg = messages.find(m => m.role === 'system');
1492 if (systemMsg) {
1493 system = [{
1494 type: 'text',
1347 - text: systemMsg.content
1495 + text: systemMsg.content,
1496 + cache_control: { type: 'ephemeral' } // Cache system prompt for 'cached' mode
1497 }];
1498 }
1499 } else {
1500 // Use regular conversion which handles system properly
1501 const result = this.convertMessages(messages, mode);
1502 anthropicMessages = result.messages;
1354 - system = result.system ? [{
1355 - type: 'text',
1356 - text: result.system
1357 - }] : undefined;
1503 + if (result.system) {
1504 + system = [{
1505 + type: 'text',
1506 + text: result.system
1507 + }];
1508 + // Add cache control to system prompt for 'system' mode
1509 + if (mode === 'system') {
1510 + system[0].cache_control = { type: 'ephemeral' };
1511 + }
1512 + }
1513 }
1514
1515 // Convert tools to Anthropic format (no cache control on tools)
1361 - const anthropicTools = tools.map(tool => ({
1362 - name: tool.name,
1363 - description: tool.description,
1364 - input_schema: tool.inputSchema || {}
1365 - }));
1516 + const anthropicTools = tools.map(tool => {
1517 + const injectedTool = this.injectToolMetadata(tool, chat);
1518 + return {
1519 + name: injectedTool.name,
1520 + description: injectedTool.description,
1521 + input_schema: injectedTool.inputSchema || {}
1522 + };
1523 + });
1524
1525 + // Order fields according to Anthropic's cache hierarchy: tools → system → messages → model
1526 + // This ensures efficient caching as tools change least frequently, then system, then messages
1527 + // Model comes after messages so cache can be reused across different models
1528 const requestBody = {
1368 - model: this.model,
1369 - messages: anthropicMessages,
1370 - system,
1529 tools: anthropicTools.length > 0 ? anthropicTools : undefined,
1530 + system,
1531 + messages: anthropicMessages,
1532 + model: this.model,
1533 max_tokens: 4096,
1534 temperature
1535 };
@@ -1397,6 +1558,9 @@ class AnthropicProvider extends LLMProvider {
1558 // Check request size before sending
1559 this.checkRequestSize(requestBody);
1560
1561 + // Log the full request
1562 + console.log(`[ANTHROPIC SEND] (${mode}, subchat: ${chat?.isSubChat || false}):`, requestBody);
1563 +
1564 let response;
1565 try {
1566 response = await fetch(this.apiUrl, {
@@ -1429,6 +1593,22 @@ class AnthropicProvider extends LLMProvider {
1593 status: response.status,
1594 statusText: response.statusText
1595 });
1596 +
1597 + // Special handling for rate limit errors (429)
1598 + if (response.status === 429) {
1599 + const retryAfter = response.headers.get('retry-after') || response.headers.get('x-ratelimit-reset-after');
1600 + const baseMessage = error.error?.message || 'Rate limit exceeded';
1601 + let rateLimitMessage = `Rate limit exceeded: ${baseMessage}`;
1602 +
1603 + if (retryAfter) {
1604 + rateLimitMessage += ` (retry after ${retryAfter}s)`;
1605 + }
1606 +
1607 + const apiError = `Anthropic API error: ${rateLimitMessage} (429)`;
1608 + console.error('[AnthropicProvider] Rate limit error:', apiError, '\nStatus:', response.status, '\nResponse:', error);
1609 + throw new Error(apiError);
1610 + }
1611 +
1612 const apiError = `Anthropic API error: ${error.error?.message || response.statusText}`;
1613 console.error('[AnthropicProvider] API error:', apiError, '\nStatus:', response.status, '\nResponse:', error);
1614 throw new Error(apiError);
@@ -1438,8 +1618,8 @@ class AnthropicProvider extends LLMProvider {
1618 const data = await response.json();
1619 this.log('received', JSON.stringify(data, null, 2), { provider: 'anthropic' });
1620
1441 -
1442 - return {
1621 + // Log the processed response
1622 + const processedResponse = {
1623 content: data.content,
1624 toolCalls: [],
1625 usage: data.usage ? {
@@ -1450,9 +1630,13 @@ class AnthropicProvider extends LLMProvider {
1630 cacheReadInputTokens: data.usage.cache_read_input_tokens
1631 } : null
1632 };
1633 +
1634 + console.log(`[ANTHROPIC RECEIVED] (${mode}, subchat: ${chat?.isSubChat || false}):`, data);
1635 +
1636 + return processedResponse;
1637 }
1638
1455 - convertMessagesWithCaching(messages, cachePosition = null, mode = 'cached') {
1639 + convertMessagesWithCaching(messages, mode, cachePosition = null) {
1640 // Convert messages WITHOUT adding cache control yet
1641 const converted = [];
1642 // let lastRole = null; // Removed - variable was never read
@@ -1528,8 +1712,6 @@ class AnthropicProvider extends LLMProvider {
1712 // Single object - extract text
1713 if (msg.content.text) {
1714 textContent = msg.content.text;
1531 - } else if (msg.content.type === 'text' && msg.content.text) {
1532 - textContent = msg.content.text;
1715 } else {
1716 console.warn('Unknown user message object format, using fallback:', msg.content);
1717 textContent = JSON.stringify(msg.content);
@@ -1545,7 +1727,7 @@ class AnthropicProvider extends LLMProvider {
1727
1728 converted.push({
1729 role: 'user',
1548 - content: [{ type: 'text', text: String(textContent) }]
1730 + content: [{ type: 'text', text: this.addDateTimePrefix(String(textContent), msg.timestamp) }]
1731 });
1732 // lastRole = 'user';
1733 } else if (msgRole === 'assistant') {
@@ -1598,66 +1780,70 @@ class AnthropicProvider extends LLMProvider {
1780 }
1781 } else if (msgRole === 'tool-results') {
1782 // Convert tool results to Anthropic format
1601 - // Only include if corresponding tool calls were included
1602 - if (this.shouldIncludeToolResults(msg, mode)) {
1603 - const content = [];
1604 - // STRICT: Only accept toolResults property
1605 - const toolResults = msg.toolResults || [];
1606 -
1607 - for (const result of toolResults) {
1608 - // Tool results for Anthropic need to be tool_result blocks
1609 - const formattedResult = this.formatToolResultForAnthropic(
1610 - result.toolCallId || result.id,
1611 - result.result,
1612 - result.toolName || result.name
1613 - );
1614 - content.push(formattedResult);
1615 - }
1616 -
1617 - if (content.length > 0) {
1618 - // Tool results must be in user messages
1619 - converted.push({
1620 - role: 'user',
1621 - content
1622 - });
1623 - // lastRole = 'user'; // Not needed - last assignment
1624 - }
1783 + // Tool filtering now handled by optimizer - include all tools sent to provider
1784 + const content = [];
1785 + // STRICT: Only accept toolResults property
1786 + const toolResults = msg.toolResults || [];
1787 +
1788 + for (const result of toolResults) {
1789 + // Tool results for Anthropic need to be tool_result blocks
1790 + const formattedResult = this.formatToolResultForAnthropic(
1791 + result.toolCallId || result.id,
1792 + result.result,
1793 + result.toolName || result.name
1794 + );
1795 + content.push(formattedResult);
1796 + }
1797 +
1798 + if (content.length > 0) {
1799 + // Tool results must be in user messages
1800 + converted.push({
1801 + role: 'user',
1802 + content
1803 + });
1804 + // lastRole = 'user'; // Not needed - last assignment
1805 }
1806 }
1807 }
1808
1629 - // Apply cache control based on cachePosition parameter
1630 - if (cachePosition !== null && cachePosition >= 0 && cachePosition < converted.length) {
1631 - // Apply cache control to specific position
1632 - const targetMsg = converted[cachePosition];
1633 - if (targetMsg && Array.isArray(targetMsg.content) && targetMsg.content.length > 0) {
1634 - // Add cache control to last content block of the specified message
1635 - targetMsg.content[targetMsg.content.length - 1].cache_control = { type: 'ephemeral' };
1636 - }
1637 - } else {
1638 - // Default behavior - find the absolute last content block across all messages
1639 - let lastContentBlock = null;
1640 -
1641 - // Iterate backwards through messages to find the last content block
1642 - for (let i = converted.length - 1; i >= 0; i--) {
1643 - const msg = converted[i];
1644 - if (Array.isArray(msg.content) && msg.content.length > 0) {
1645 - // Found a message with content, get its last block
1646 - lastContentBlock = msg.content[msg.content.length - 1];
1647 - break;
1809 + // Apply cache control based on mode and cachePosition parameter
1810 + // Note: System prompt cache control is handled separately above
1811 + if (mode === 'cached') {
1812 + // For 'cached' mode, apply cache control to the strategy-determined position
1813 + if (cachePosition !== null && cachePosition >= 0 && cachePosition < converted.length) {
1814 + // Apply cache control to specific position
1815 + const targetMsg = converted[cachePosition];
1816 + if (targetMsg && Array.isArray(targetMsg.content) && targetMsg.content.length > 0) {
1817 + // Add cache control to last content block of the specified message
1818 + targetMsg.content[targetMsg.content.length - 1].cache_control = { type: 'ephemeral' };
1819 + }
1820 + } else {
1821 + // Default behavior - find the absolute last content block across all messages
1822 + let lastContentBlock = null;
1823 +
1824 + // Iterate backwards through messages to find the last content block
1825 + for (let i = converted.length - 1; i >= 0; i--) {
1826 + const msg = converted[i];
1827 + if (Array.isArray(msg.content) && msg.content.length > 0) {
1828 + // Found a message with content, get its last block
1829 + lastContentBlock = msg.content[msg.content.length - 1];
1830 + break;
1831 + }
1832 + }
1833 +
1834 + // Add cache_control to only the very last content block
1835 + if (lastContentBlock) {
1836 + lastContentBlock.cache_control = { type: 'ephemeral' };
1837 }
1649 - }
1650 -
1651 - // Add cache_control to only the very last content block
1652 - if (lastContentBlock) {
1653 - lastContentBlock.cache_control = { type: 'ephemeral' };
1838 }
1839 }
1840 + // For 'system' mode: only system prompt is cached (handled above)
1841 + // For 'all-off' mode: no cache control applied
1842
1843 return { converted, summaryContent };
1844 }
1845
1660 - convertMessages(messages, _mode = 'cached') {
1846 + convertMessages(messages, _mode) {
1847 // Convert messages for Anthropic format
1848 const converted = [];
1849
@@ -1673,7 +1859,7 @@ class AnthropicProvider extends LLMProvider {
1859 // Convert user message to Anthropic format with content blocks
1860 converted.push({
1861 role: 'user',
1676 - content: [{ type: 'text', text: msg.content }]
1862 + content: [{ type: 'text', text: this.addDateTimePrefix(msg.content, msg.timestamp) }]
1863 });
1864 } else if (msgRole === 'assistant') {
1865 // Convert assistant message to Anthropic format
@@ -1752,30 +1938,6 @@ class AnthropicProvider extends LLMProvider {
1938 return { messages: converted, system: systemPrompt };
1939 }
1940
1755 - shouldIncludeToolCalls(msg, mode) {
1756 - // Determine if tool calls should be included based on mode
1757 - if (mode === 'all-off') return false;
1758 - if (mode === 'all-on') return true;
1759 - if (mode === 'manual') {
1760 - // Check individual tool inclusion state (would need to be passed in)
1761 - return true; // Default to include for now
1762 - }
1763 - // For 'auto' and 'cached' modes, include by default
1764 - return true;
1765 - }
1766 -
1767 - shouldIncludeToolResults(msg, mode) {
1768 - // Tool results should only be included if their corresponding calls were included
1769 - // This logic matches the tool call inclusion logic
1770 - if (mode === 'all-off') return false;
1771 - if (mode === 'all-on') return true;
1772 - if (mode === 'manual') {
1773 - // Check individual tool inclusion state (would need to be passed in)
1774 - return true; // Default to include for now
1775 - }
1776 - // For 'auto' and 'cached' modes, include by default
1777 - return true;
1778 - }
1941
1942 formatToolResultForAnthropic(toolCallId, result, _toolName) {
1943 // Format MCP tool results for Anthropic's tool_result blocks
@@ -1845,7 +2007,7 @@ class GoogleProvider extends LLMProvider {
2007 * @param {number|null} _cachePosition - Cache position (unused for Google)
2008 * @returns {Promise<LLMResponse>}
2009 */
1848 - async sendMessage(messages, tools = [], temperature = 0.7, mode = 'cached', _cachePosition = null) {
2010 + async sendMessage(messages, tools, temperature, mode, _cachePosition = null, chat = null) {
2011 // Validate messages before processing
2012 validateMessagesForAPI(messages);
2013
@@ -1853,19 +2015,24 @@ class GoogleProvider extends LLMProvider {
2015 const { contents, systemInstruction } = this.convertMessages(messages, mode);
2016
2017 // Convert tools to Gemini format
1856 - const functionDeclarations = tools.map(tool => ({
1857 - name: tool.name,
1858 - description: tool.description,
1859 - parameters: this.cleanSchemaForGoogle(tool.inputSchema || {})
1860 - }));
2018 + const functionDeclarations = tools.map(tool => {
2019 + const injectedTool = this.injectToolMetadata(tool, chat);
2020 + return {
2021 + name: injectedTool.name,
2022 + description: injectedTool.description,
2023 + parameters: this.cleanSchemaForGoogle(injectedTool.inputSchema || {})
2024 + };
2025 + });
2026
1862 - const requestBody = {
1863 - contents,
1864 - generationConfig: {
1865 - temperature,
1866 - maxOutputTokens: 4096
1867 - }
1868 - };
2027 + // Order fields consistently: tools → systemInstruction (system) → contents (messages) → generationConfig
2028 + const requestBody = {};
2029 +
2030 + // Add tools first if present
2031 + if (functionDeclarations.length > 0) {
2032 + requestBody.tools = [{
2033 + function_declarations: functionDeclarations
2034 + }];
2035 + }
2036
2037 // Add system instruction if present
2038 if (systemInstruction) {
@@ -1873,12 +2040,15 @@ class GoogleProvider extends LLMProvider {
2040 parts: [{ text: systemInstruction }]
2041 };
2042 }
1876 -
1877 - if (functionDeclarations.length > 0) {
1878 - requestBody.tools = [{
1879 - function_declarations: functionDeclarations
1880 - }];
1881 - }
2043 +
2044 + // Add messages
2045 + requestBody.contents = contents;
2046 +
2047 + // Add generation config (includes model info implicitly via this.model in the URL)
2048 + requestBody.generationConfig = {
2049 + temperature,
2050 + maxOutputTokens: 4096
2051 + };
2052
2053 this.log('sent', JSON.stringify(requestBody, null, 2), {
2054 provider: 'google',
@@ -1889,6 +2059,9 @@ class GoogleProvider extends LLMProvider {
2059 // Check request size before sending
2060 this.checkRequestSize(requestBody);
2061
2062 + // Log the full request
2063 + console.log(`[GOOGLE SEND] (${mode}, subchat: ${chat?.isSubChat || false}):`, requestBody);
2064 +
2065 let response;
2066 try {
2067 response = await fetch(this.apiUrl, {
@@ -1919,6 +2092,22 @@ class GoogleProvider extends LLMProvider {
2092 status: response.status,
2093 statusText: response.statusText
2094 });
2095 +
2096 + // Special handling for rate limit errors (429)
2097 + if (response.status === 429) {
2098 + const retryAfter = response.headers.get('retry-after') || response.headers.get('x-ratelimit-reset-after');
2099 + const baseMessage = error.error?.message || 'Rate limit exceeded';
2100 + let rateLimitMessage = `Rate limit exceeded: ${baseMessage}`;
2101 +
2102 + if (retryAfter) {
2103 + rateLimitMessage += ` (retry after ${retryAfter}s)`;
2104 + }
2105 +
2106 + const apiError = `Google API error: ${rateLimitMessage} (429)`;
2107 + console.error('[GoogleProvider] Rate limit error:', apiError, '\nStatus:', response.status, '\nResponse:', error);
2108 + throw new Error(apiError);
2109 + }
2110 +
2111 const apiError = `Google API error: ${error.error?.message || response.statusText}`;
2112 console.error('[GoogleProvider] API error:', apiError, '\nStatus:', response.status, '\nResponse:', error);
2113 throw new Error(apiError);
@@ -1927,6 +2116,9 @@ class GoogleProvider extends LLMProvider {
2116 /** @type {GoogleResponse} */
2117 const data = await response.json();
2118 this.log('received', JSON.stringify(data, null, 2), { provider: 'google' });
2119 +
2120 + // Log the full response
2121 + console.log(`[GOOGLE RECEIVED] (${mode}, subchat: ${chat?.isSubChat || false}):`, data);
2122 const candidate = data.candidates[0];
2123
2124 // Check finish reason for potential issues
@@ -2017,7 +2209,7 @@ class GoogleProvider extends LLMProvider {
2209 };
2210 }
2211
2020 - convertMessages(messages, mode = 'cached') {
2212 + convertMessages(messages, _mode) {
2213 // Convert messages for Google format
2214 /*
2215 messages.map((m, i) => ({
@@ -2040,14 +2232,14 @@ class GoogleProvider extends LLMProvider {
2232
2233 // Check for tool calls in assistant messages
2234 const toolCalls = extractToolCallsFromContent(msg.content);
2043 - if (msgRole === 'assistant' && toolCalls.length > 0 && this.shouldIncludeToolCalls(msg, mode)) {
2235 + if (msgRole === 'assistant' && toolCalls.length > 0) {
2236 for (const tc of toolCalls) {
2237 if (!allToolCalls.has(tc.name)) {
2238 allToolCalls.set(tc.name, []);
2239 }
2240 allToolCalls.get(tc.name).push(i);
2241 }
2050 - } else if (msgRole === 'tool-results' && this.shouldIncludeToolResults(msg, mode)) {
2242 + } else if (msgRole === 'tool-results') {
2243 // Handle internal tool-results format
2244 // STRICT: Only accept toolResults property
2245 const toolResults = msg.toolResults || [];
@@ -2114,10 +2306,7 @@ class GoogleProvider extends LLMProvider {
2306 const toolResults = msg.toolResults || [];
2307 // Process tool results
2308
2117 - // Only include if should be included
2118 - if (!this.shouldIncludeToolResults(msg, mode)) {
2119 - continue;
2120 - }
2309 + // Tool filtering now handled by optimizer - include all tools sent to provider
2310
2311 // Check if these tool responses have corresponding function calls
2312 if (!lastAssistantHadFunctionCalls) {
@@ -2148,8 +2337,7 @@ class GoogleProvider extends LLMProvider {
2337 // Reset the function call tracking when we encounter a new assistant message
2338 if (msgRole === 'assistant') {
2339 const toolCalls = extractToolCallsFromContent(msg.content);
2151 - lastAssistantHadFunctionCalls = toolCalls.length > 0 &&
2152 - this.shouldIncludeToolCalls(msg, mode);
2340 + lastAssistantHadFunctionCalls = toolCalls.length > 0;
2341 // Track if assistant message has function calls
2342 }
2343
@@ -2179,7 +2367,7 @@ class GoogleProvider extends LLMProvider {
2367 }
2368
2369 if (textContent || textContent === '') {
2182 - parts.push({ text: textContent });
2370 + parts.push({ text: this.addDateTimePrefix(textContent, msg.timestamp) });
2371 }
2372 } else if (msgRole === 'assistant') {
2373 // Assistant messages - include text and optionally tool calls
@@ -2201,7 +2389,7 @@ class GoogleProvider extends LLMProvider {
2389
2390 // Extract and add tool calls
2391 const toolCalls = extractToolCallsFromContent(msg.content);
2204 - if (toolCalls.length > 0 && this.shouldIncludeToolCalls(msg, mode)) {
2392 + if (toolCalls.length > 0) {
2393 for (const tc of toolCalls) {
2394 // Use destructuring to avoid direct 'arguments' reference
2395 const { arguments: tcArgs } = tc || {};
@@ -2258,30 +2446,6 @@ class GoogleProvider extends LLMProvider {
2446 return { contents, systemInstruction };
2447 }
2448
2261 - shouldIncludeToolCalls(msg, mode) {
2262 - // Determine if tool calls should be included based on mode
2263 - if (mode === 'all-off') return false;
2264 - if (mode === 'all-on') return true;
2265 - if (mode === 'manual') {
2266 - // Check individual tool inclusion state (would need to be passed in)
2267 - return true; // Default to include for now
2268 - }
2269 - // For 'auto' and 'cached' modes, include by default
2270 - return true;
2271 - }
2272 -
2273 - shouldIncludeToolResults(msg, mode) {
2274 - // Tool results should only be included if their corresponding calls were included
2275 - // This logic matches the tool call inclusion logic
2276 - if (mode === 'all-off') return false;
2277 - if (mode === 'all-on') return true;
2278 - if (mode === 'manual') {
2279 - // Check individual tool inclusion state (would need to be passed in)
2280 - return true; // Default to include for now
2281 - }
2282 - // For 'auto' and 'cached' modes, include by default
2283 - return true;
2284 - }
2449
2450 formatToolResultContent(result) {
2451 // Handle different types of results for Google format
src/web/mcp/mcp-web-client/web/message-optimizer.js
+36 -48
@@ -151,10 +151,7 @@ export class MessageOptimizer {
151 enabled: false,
152 forgetAfterConclusions: 1
153 },
154 - cacheControl: {
155 - enabled: false,
156 - strategy: 'smart'
157 - },
154 + cacheControl: 'all-off',
155 titleGeneration: {
156 enabled: true,
157 model: null
@@ -229,16 +226,13 @@ export class MessageOptimizer {
226 }
227
228 // Validate cache control settings
232 - if (opt.cacheControl) {
233 - const cc = opt.cacheControl;
234 - if (cc.enabled !== undefined && typeof cc.enabled !== 'boolean') {
235 - throw new Error('[MessageOptimizer] cacheControl.enabled must be boolean');
229 + if (opt.cacheControl !== undefined) {
230 + if (typeof opt.cacheControl !== 'string') {
231 + throw new Error('[MessageOptimizer] cacheControl must be a string');
232 }
237 - if (cc.strategy !== undefined) {
238 - const validStrategies = ['aggressive', 'smart', 'minimal'];
239 - if (!validStrategies.includes(cc.strategy)) {
240 - throw new Error(`[MessageOptimizer] cacheControl.strategy must be one of: ${validStrategies.join(', ')}`);
241 - }
233 + const validModes = ['all-off', 'system', 'cached'];
234 + if (!validModes.includes(opt.cacheControl)) {
235 + throw new Error(`[MessageOptimizer] cacheControl must be one of: ${validModes.join(', ')}`);
236 }
237 }
238
@@ -294,7 +288,7 @@ export class MessageOptimizer {
288 toolsFiltered: 0,
289 toolsSummarized: 0,
290 messagesSummarized: 0,
297 - cacheStrategy: this.settings.optimisation.cacheControl.strategy
291 + cacheMode: this.settings.optimisation.cacheControl
292 };
293
294 try {
@@ -564,13 +558,10 @@ export class MessageOptimizer {
558 return null;
559 }
560
567 - const filteredMsg = {
561 + return {
562 ...msg,
563 content: filteredContent
564 };
571 -
572 -
573 - return filteredMsg;
565 }
566
567 return msg;
@@ -658,45 +649,42 @@ export class MessageOptimizer {
649 * @returns {number} - Cache control index (-1 for no cache)
650 */
651 determineCacheControl(messages, freezeCache, lastCacheIndex) {
661 - if (!this.settings.optimisation.cacheControl.enabled) {
662 - return -1;
663 - }
652 + const cacheMode = this.settings.optimisation.cacheControl;
653
665 - // For Anthropic models, cache control and tool memory are mutually exclusive
666 - // When tool memory filters out old tools, cached content would be wasted
667 - if (this.settings.model.provider === 'anthropic' &&
668 - this.settings.optimisation.toolMemory.enabled) {
669 - // console.log('[MessageOptimizer] Cache control disabled - tool memory is enabled for Anthropic');
670 - return -1;
654 + // Handle different cache control modes
655 + switch (cacheMode) {
656 + case 'all-off':
657 + return -1; // No cache control
658 +
659 + case 'system':
660 + return -1; // System prompt caching handled by provider, no message-level cache
661 +
662 + case 'cached':
663 + // Use smart strategy logic for message-level caching
664 + // System prompt caching is handled by provider
665 + break;
666 +
667 + default:
668 + console.warn('[MessageOptimizer] Unknown cache control mode:', cacheMode);
669 + return -1;
670 }
671
672 + // Only 'cached' mode continues here - apply smart strategy
673 if (freezeCache && lastCacheIndex !== null) {
674 // console.log(`[MessageOptimizer] Using frozen cache index: ${lastCacheIndex}`);
675 return lastCacheIndex;
676 }
677
678 - const strategy = this.settings.optimisation.cacheControl.strategy;
679 - // console.log(`[MessageOptimizer] Applying cache strategy: ${strategy}`);
680 -
681 - switch (strategy) {
682 - case 'aggressive':
683 - return Math.max(0, messages.length - 2);
684 -
685 - case 'minimal':
686 - return 0;
687 -
688 - case 'smart':
689 - default:
690 - // Cache up to 70% of messages, avoiding recent tool results
691 - const seventyPercent = Math.floor(messages.length * 0.7);
692 -
693 - for (let i = seventyPercent; i >= 0; i--) {
694 - if (messages[i] && messages[i].role !== 'tool-results') {
695 - return i;
696 - }
697 - }
698 - return 0;
678 + // Apply smart strategy for 'cached' mode
679 + // Cache up to 70% of messages, avoiding recent tool results
680 + const seventyPercent = Math.floor(messages.length * 0.7);
681 +
682 + for (let i = seventyPercent; i >= 0; i--) {
683 + if (messages[i] && messages[i].role !== 'tool-results') {
684 + return i;
685 + }
686 }
687 + return 0;
688 }
689
690 /**
src/web/mcp/mcp-web-client/web/message-optimizer.test.js
+2 -2
@@ -24,7 +24,7 @@ function createSettings(forgetAfterConclusions = 0, toolMemoryEnabled = true) {
24 toolSummarisation: { enabled: false, thresholdKiB: 20, model: null },
25 autoSummarisation: { enabled: false, triggerPercent: 50, model: null },
26 toolMemory: { enabled: toolMemoryEnabled, forgetAfterConclusions },
27 - cacheControl: { enabled: false, strategy: 'smart' },
27 + cacheControl: 'all-off',
28 titleGeneration: { enabled: true, model: null }
29 },
30 mcpServer: 'test'
@@ -153,7 +153,7 @@ runTest('Test 2: Real-world scenario with 4 turns', () => {
153 toolSummarisation: { enabled: false, thresholdKiB: 20, model: null },
154 autoSummarisation: { enabled: false, triggerPercent: 50, model: null },
155 toolMemory: { enabled: true, forgetAfterConclusions: 0 },
156 - cacheControl: { enabled: false, strategy: 'smart' },
156 + cacheControl: 'all-off',
157 titleGeneration: { enabled: true, model: null }
158 },
159 mcpServer: 'demos_registry'
src/web/mcp/mcp-web-client/web/styles.css
+488
@@ -29,6 +29,7 @@
29 --code-bg: #f5f5f5;
30 --scrollbar-thumb: #c1c1c1;
31 --scrollbar-track: #f3f3f3;
32 + --sub-chat-bg: #f9f9f9; /* Light gray background for sub-chats */
33 }
34
35 :root[data-theme="dark"] {
@@ -61,6 +62,7 @@
62 --code-bg: #1e1e1e;
63 --scrollbar-thumb: #464647;
64 --scrollbar-track: #252526;
65 + --sub-chat-bg: #2a2a2a; /* Darker background for sub-chats in dark theme */
66 }
67
68 * {
@@ -1145,6 +1147,15 @@ body {
1147 border-left: 3px solid var(--border-color);
1148 }
1149
1150 +/* Blockquotes in user messages */
1151 +:root[data-theme="light"] .message.user .message-content blockquote {
1152 + border-left-color: rgba(255, 255, 255, 0.5); /* White border with transparency */
1153 +}
1154 +
1155 +:root[data-theme="dark"] .message.user .message-content blockquote {
1156 + border-left-color: rgba(30, 30, 30, 0.5); /* Dark border with transparency */
1157 +}
1158 +
1159 .message-content table {
1160 border-collapse: collapse;
1161 width: 100%;
@@ -1163,6 +1174,35 @@ body {
1174 font-weight: 600;
1175 }
1176
1177 +/* Special styling for tables in user messages to ensure readability */
1178 +/* Light theme: user bg is #00ab44 (green), make table headers darker green */
1179 +:root[data-theme="light"] .message.user .message-content th {
1180 + background-color: rgba(0, 140, 60, 0.25); /* Darker green */
1181 + color: var(--chat-user-text); /* White text */
1182 +}
1183 +
1184 +:root[data-theme="light"] .message.user .message-content td {
1185 + border-color: rgba(255, 255, 255, 0.3); /* White borders with transparency */
1186 +}
1187 +
1188 +:root[data-theme="light"] .message.user .message-content th {
1189 + border-color: rgba(255, 255, 255, 0.3); /* White borders with transparency */
1190 +}
1191 +
1192 +/* Dark theme: user bg is #00d952 (bright green), make table headers darker */
1193 +:root[data-theme="dark"] .message.user .message-content th {
1194 + background-color: rgba(0, 180, 70, 0.4); /* Much darker green */
1195 + color: var(--chat-user-text); /* Dark text on bright background */
1196 +}
1197 +
1198 +:root[data-theme="dark"] .message.user .message-content td {
1199 + border-color: rgba(30, 30, 30, 0.3); /* Dark borders with transparency */
1200 +}
1201 +
1202 +:root[data-theme="dark"] .message.user .message-content th {
1203 + border-color: rgba(30, 30, 30, 0.3); /* Dark borders with transparency */
1204 +}
1205 +
1206 .message-content a {
1207 color: var(--primary-color);
1208 text-decoration: none;
@@ -1172,12 +1212,41 @@ body {
1212 text-decoration: underline;
1213 }
1214
1215 +/* Special styling for links in user messages to ensure readability */
1216 +:root[data-theme="light"] .message.user .message-content a {
1217 + color: white; /* White links on green background */
1218 + text-decoration: underline; /* Always underlined to show it's a link */
1219 +}
1220 +
1221 +:root[data-theme="light"] .message.user .message-content a:hover {
1222 + opacity: 0.8;
1223 +}
1224 +
1225 +:root[data-theme="dark"] .message.user .message-content a {
1226 + color: var(--chat-user-text); /* Dark text on bright green */
1227 + text-decoration: underline;
1228 + font-weight: 500; /* Slightly bolder to stand out */
1229 +}
1230 +
1231 +:root[data-theme="dark"] .message.user .message-content a:hover {
1232 + opacity: 0.7;
1233 +}
1234 +
1235 .message-content hr {
1236 border: none;
1237 border-top: 1px solid var(--border-color);
1238 margin: 1em 0;
1239 }
1240
1241 +/* Horizontal rules in user messages */
1242 +:root[data-theme="light"] .message.user .message-content hr {
1243 + border-top-color: rgba(255, 255, 255, 0.3); /* White border with transparency */
1244 +}
1245 +
1246 +:root[data-theme="dark"] .message.user .message-content hr {
1247 + border-top-color: rgba(30, 30, 30, 0.3); /* Dark border with transparency */
1248 +}
1249 +
1250 /* Ensure markdown content doesn't overflow */
1251 .message-content img {
1252 max-width: 100%;
@@ -1416,6 +1485,56 @@ body {
1485 color: var(--info-color);
1486 }
1487
1488 +/* Processing Guidance in Sub-chats */
1489 +.processing-guidance {
1490 + background: var(--surface-color);
1491 + border: 1px solid var(--border-color);
1492 + padding: 15px;
1493 + border-radius: 8px;
1494 + margin-bottom: 10px;
1495 +}
1496 +
1497 +.processing-guidance h4 {
1498 + margin: 0 0 10px 0;
1499 + color: var(--info-color);
1500 + font-size: 14px;
1501 + font-weight: 600;
1502 +}
1503 +
1504 +.processing-guidance h4 i {
1505 + margin-right: 8px;
1506 +}
1507 +
1508 +.guidance-item {
1509 + margin: 5px 0;
1510 + color: var(--text-primary);
1511 + font-size: 13px;
1512 +}
1513 +
1514 +.guidance-item i {
1515 + margin-right: 8px;
1516 + width: 16px;
1517 + text-align: center;
1518 + color: var(--text-secondary);
1519 +}
1520 +
1521 +.guidance-item strong {
1522 + margin-right: 5px;
1523 + color: var(--text-secondary);
1524 +}
1525 +
1526 +/* Light theme adjustments */
1527 +[data-theme="light"] .processing-guidance {
1528 + background: #f5f9ff;
1529 + border-color: #d0e3ff;
1530 +}
1531 +
1532 +/* Dark theme adjustments */
1533 +[data-theme="dark"] .processing-guidance {
1534 + background: rgba(55, 148, 255, 0.1);
1535 + border-color: rgba(55, 148, 255, 0.3);
1536 +}
1537 +
1538 /* Thinking blocks */
1539 .thinking-block {
1540 margin: 4px 0;
@@ -1560,6 +1679,18 @@ body {
1679 color: var(--text-tertiary);
1680 }
1681
1682 +/* ContentEditable placeholder support */
1683 +.chat-input[contenteditable]:empty:before {
1684 + content: attr(data-placeholder);
1685 + color: var(--text-tertiary);
1686 + pointer-events: none;
1687 + position: absolute;
1688 +}
1689 +
1690 +.chat-input[contenteditable]:focus:empty:before {
1691 + opacity: 0.7;
1692 +}
1693 +
1694 /* Log Panel */
1695 .log-panel {
1696 width: 300px;
@@ -3645,3 +3776,360 @@ body {
3776 .tool-response-content.error .fa-exclamation-circle {
3777 font-size: 18px;
3778 }
3779 +
3780 +/* Sub-chat expandable section */
3781 +.sub-chat-section {
3782 + margin-top: 12px;
3783 + border: 1px solid var(--border-color);
3784 + border-radius: 8px;
3785 + background: var(--bg-secondary);
3786 + overflow: hidden;
3787 + transition: box-shadow 0.2s;
3788 +}
3789 +
3790 +.sub-chat-section:hover {
3791 + box-shadow: var(--shadow);
3792 +}
3793 +
3794 +.sub-chat-header {
3795 + display: flex;
3796 + align-items: center;
3797 + cursor: pointer;
3798 + user-select: none;
3799 + background: var(--bg-secondary);
3800 + transition: background-color 0.2s;
3801 +}
3802 +
3803 +.sub-chat-header:hover {
3804 + background: var(--hover-color);
3805 +}
3806 +
3807 +.sub-chat-toggle {
3808 + font-size: 12px;
3809 + margin-right: 8px;
3810 + transition: transform 0.3s ease;
3811 +}
3812 +
3813 +.sub-chat-section.expanded .sub-chat-toggle {
3814 + transform: rotate(90deg);
3815 +}
3816 +
3817 +.sub-chat-icon {
3818 + margin-right: 6px;
3819 +}
3820 +
3821 +.sub-chat-label {
3822 + font-weight: 500;
3823 + flex: 1;
3824 +}
3825 +
3826 +.sub-chat-stats {
3827 + font-size: 12px;
3828 + color: var(--text-secondary);
3829 + margin-left: 12px;
3830 +}
3831 +
3832 +.sub-chat-content {
3833 + border-top: 1px solid var(--border-color);
3834 + max-height: 0;
3835 + overflow: hidden;
3836 + transition: max-height 0.3s ease;
3837 + background: var(--background-color);
3838 +}
3839 +
3840 +.sub-chat-section.expanded .sub-chat-content {
3841 + max-height: 500px;
3842 + overflow-y: auto;
3843 + padding: 12px;
3844 +}
3845 +
3846 +/* Sub-chat messages */
3847 +.sub-chat-message {
3848 + margin-bottom: 12px;
3849 + padding: 10px;
3850 + border-radius: 6px;
3851 + border: 1px solid var(--border-color);
3852 + background: var(--surface-color);
3853 +}
3854 +
3855 +.sub-chat-system {
3856 + background: var(--bg-secondary);
3857 +}
3858 +
3859 +.sub-chat-system-header {
3860 + font-weight: 600;
3861 + margin-bottom: 8px;
3862 + color: var(--text-secondary);
3863 +}
3864 +
3865 +.sub-chat-metadata {
3866 + font-size: 12px;
3867 + color: var(--text-secondary);
3868 +}
3869 +
3870 +.sub-chat-metadata div {
3871 + margin: 4px 0;
3872 +}
3873 +
3874 +.sub-chat-user-label {
3875 + font-size: 12px;
3876 + color: var(--text-secondary);
3877 + margin-bottom: 6px;
3878 +}
3879 +
3880 +.sub-chat-user-content {
3881 + font-size: 12px;
3882 + white-space: pre-wrap;
3883 + word-break: break-word;
3884 + margin: 0;
3885 + padding: 8px;
3886 + background: var(--bg-secondary);
3887 + border-radius: 4px;
3888 +}
3889 +
3890 +.sub-chat-assistant-text {
3891 + margin: 8px 0;
3892 +}
3893 +
3894 +.sub-chat-tool-call {
3895 + display: inline-flex;
3896 + align-items: center;
3897 + background: var(--bg-secondary);
3898 + border: 1px solid var(--border-color);
3899 + border-radius: 6px;
3900 + padding: 6px 10px;
3901 + margin: 4px 0;
3902 + font-size: 12px;
3903 +}
3904 +
3905 +.sub-chat-tool-icon {
3906 + margin-right: 6px;
3907 +}
3908 +
3909 +.sub-chat-tool-name {
3910 + font-weight: 500;
3911 + margin-right: 8px;
3912 +}
3913 +
3914 +.sub-chat-tool-args {
3915 + color: var(--text-secondary);
3916 + font-family: 'IBM Plex Mono', monospace;
3917 + font-size: 11px;
3918 +}
3919 +
3920 +.sub-chat-tool-result {
3921 + margin-top: 8px;
3922 +}
3923 +
3924 +.sub-chat-tool-result-header {
3925 + font-size: 12px;
3926 + font-weight: 500;
3927 + margin-bottom: 4px;
3928 +}
3929 +
3930 +.sub-chat-tool-result-content {
3931 + font-size: 11px;
3932 + font-family: 'IBM Plex Mono', monospace;
3933 + white-space: pre-wrap;
3934 + word-break: break-all;
3935 + margin: 0;
3936 + padding: 8px;
3937 + background: var(--bg-secondary);
3938 + border-radius: 4px;
3939 + max-height: 200px;
3940 + overflow-y: auto;
3941 +}
3942 +
3943 +.sub-chat-error {
3944 + color: var(--danger-color);
3945 + text-align: center;
3946 + padding: 20px;
3947 +}
3948 +
3949 +/* Tool metadata display */
3950 +.tool-metadata {
3951 + background: var(--surface-color);
3952 + border: 1px solid var(--border-color);
3953 + border-radius: 8px;
3954 + padding: 12px 16px;
3955 + margin-bottom: 12px;
3956 + font-size: 13px;
3957 + position: relative;
3958 + overflow: hidden;
3959 +}
3960 +
3961 +.tool-metadata::before {
3962 + content: '';
3963 + position: absolute;
3964 + top: 0;
3965 + left: 0;
3966 + width: 4px;
3967 + height: 100%;
3968 + background: var(--primary-color);
3969 +}
3970 +
3971 +.tool-metadata-header {
3972 + display: flex;
3973 + align-items: center;
3974 + margin-bottom: 10px;
3975 + padding-bottom: 8px;
3976 + border-bottom: 1px solid var(--border-subtle);
3977 +}
3978 +
3979 +.tool-metadata-header i {
3980 + color: var(--primary-color);
3981 + margin-right: 8px;
3982 +}
3983 +
3984 +.tool-metadata-header span {
3985 + font-weight: 600;
3986 + color: var(--text-primary);
3987 +}
3988 +
3989 +.tool-metadata-item {
3990 + display: flex;
3991 + align-items: flex-start;
3992 + margin: 8px 0;
3993 + color: var(--text-secondary);
3994 + line-height: 1.5;
3995 +}
3996 +
3997 +.tool-metadata-icon {
3998 + margin-right: 10px;
3999 + flex-shrink: 0;
4000 + width: 16px;
4001 + text-align: center;
4002 + color: var(--primary-color);
4003 +}
4004 +
4005 +.tool-metadata-label {
4006 + font-weight: 500;
4007 + margin-right: 8px;
4008 + flex-shrink: 0;
4009 + color: var(--text-primary);
4010 +}
4011 +
4012 +.tool-metadata-value {
4013 + flex: 1;
4014 + word-break: break-word;
4015 +}
4016 +
4017 +.tool-metadata-value code {
4018 + background: var(--code-bg);
4019 + padding: 2px 4px;
4020 + border-radius: 3px;
4021 + font-size: 12px;
4022 + font-family: 'IBM Plex Mono', monospace;
4023 +}
4024 +
4025 +/* Confirmation Modal Styles */
4026 +.confirm-modal-container {
4027 + position: fixed;
4028 + top: 0;
4029 + left: 0;
4030 + right: 0;
4031 + bottom: 0;
4032 + z-index: 10000;
4033 + display: flex;
4034 + align-items: center;
4035 + justify-content: center;
4036 + animation: fadeIn 0.2s ease;
4037 +}
4038 +
4039 +@keyframes fadeIn {
4040 + from {
4041 + opacity: 0;
4042 + }
4043 + to {
4044 + opacity: 1;
4045 + }
4046 +}
4047 +
4048 +.confirm-modal-backdrop {
4049 + position: absolute;
4050 + top: 0;
4051 + left: 0;
4052 + right: 0;
4053 + bottom: 0;
4054 + background: var(--modal-backdrop);
4055 + backdrop-filter: blur(4px);
4056 +}
4057 +
4058 +.confirm-modal {
4059 + position: relative;
4060 + background: var(--background-color);
4061 + border: 1px solid var(--border-color);
4062 + border-radius: 8px;
4063 + box-shadow: var(--shadow-lg);
4064 + min-width: 400px;
4065 + max-width: 500px;
4066 + margin: 20px;
4067 + animation: slideIn 0.2s ease;
4068 +}
4069 +
4070 +@keyframes slideIn {
4071 + from {
4072 + transform: translateY(-20px);
4073 + opacity: 0;
4074 + }
4075 + to {
4076 + transform: translateY(0);
4077 + opacity: 1;
4078 + }
4079 +}
4080 +
4081 +.confirm-modal-header {
4082 + padding: 20px 24px;
4083 + border-bottom: 1px solid var(--border-subtle);
4084 +}
4085 +
4086 +.confirm-modal-header h3 {
4087 + margin: 0;
4088 + font-size: 18px;
4089 + font-weight: 600;
4090 + color: var(--text-primary);
4091 +}
4092 +
4093 +.confirm-modal-body {
4094 + padding: 20px 24px;
4095 +}
4096 +
4097 +.confirm-modal-body p {
4098 + margin: 0;
4099 + font-size: 14px;
4100 + line-height: 1.6;
4101 + color: var(--text-primary);
4102 +}
4103 +
4104 +.confirm-modal-footer {
4105 + padding: 16px 24px;
4106 + border-top: 1px solid var(--border-subtle);
4107 + display: flex;
4108 + justify-content: flex-end;
4109 + gap: 12px;
4110 +}
4111 +
4112 +.confirm-modal .btn {
4113 + min-width: 80px;
4114 + font-weight: 500;
4115 +}
4116 +
4117 +/* Focus styles for keyboard navigation */
4118 +.confirm-modal .btn:focus {
4119 + outline: 2px solid var(--primary-color);
4120 + outline-offset: 2px;
4121 +}
4122 +
4123 +/* Ensure proper button colors in modal */
4124 +.confirm-modal .btn-danger {
4125 + background: var(--danger-color);
4126 + color: white;
4127 +}
4128 +
4129 +.confirm-modal .btn-danger:hover {
4130 + background: #ff5757;
4131 +}
4132 +
4133 +.confirm-modal .btn-danger:focus {
4134 + outline-color: var(--danger-color);
4135 +}
src/web/mcp/mcp-web-client/web/system-msg.js
+124 -15
@@ -126,7 +126,7 @@ DO NOT DISCUSS OTHER MONITORING SOLUTIONS OR MAKE COMPARISONS.
126 * Get timezone information including name and UTC offset
127 * @returns {Object} Object with timezone name and offset string
128 */
129 -function getTimezoneInfo() {
129 +function _getTimezoneInfo() {
130 const date = new Date();
131
132 // Get UTC offset in minutes
@@ -157,22 +157,12 @@ function getTimezoneInfo() {
157 * @returns {string} Formatted date/time context
158 */
159 function buildDateTimeContext() {
160 - const currentTimestamp = new Date().toISOString();
161 - const timezoneInfo = getTimezoneInfo();
162 - const currentDate = new Date();
163 - const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
164 - const currentDayName = dayNames[currentDate.getDay()];
165 -
160 return `## CRITICAL DATE/TIME CONTEXT
167 -Current date and time: ${currentTimestamp}
168 -Current day: ${currentDayName}
169 -Current timezone: ${timezoneInfo.name} (${timezoneInfo.offset})
170 -Current year: ${currentDate.getFullYear()}
161
162 IMPORTANT DATE/TIME INTERPRETATION RULES FOR MONITORING DATA:
163
174 -1. When the user mentions dates without a year (e.g., "January 15", "last month"), use ${currentDate.getFullYear()} as the current year
175 -2. When the user mentions times without a timezone (e.g., "10pm", "14:30"), assume ${timezoneInfo.name} timezone
164 +1. When the user mentions dates without a year (e.g., "January 15", "last month"), use the current year
165 +2. When the user mentions times without a timezone (e.g., "10pm", "14:30"), assume the user's local timezone
166 3. ALL relative references refer to the PAST (this is a monitoring system analyzing historical data):
167 - "this morning" = earlier today, before noon
168 - "this afternoon" = earlier today, after noon
@@ -285,10 +275,129 @@ export function createSpecializedSystemPrompt(useCase, options = {}) {
275 case 'title':
276 return 'You are a helpful assistant that generates concise, descriptive and short titles for conversations.';
277
278 + case 'subchat':
279 + // Sub-chat system prompt with full MCP capabilities
280 + return `
281 +You are a helpful SRE/DevOps assistant and you are asked specific and
282 +concrete questions by another AI assistant, about some user's infrastructure.
283 +
284 +Your goal is to use the tools available to you, to provide accurate and
285 +complete answers to the questions asked, using the data available to you.
286 +
287 +**AI-TO-AI COMMUNICATION MODE**:
288 +You are communicating with another AI assistant, not a human user. This means:
289 +- No need for explanations about tool usage or methodology
290 +- Provide raw data in structured format (tables, lists, technical details)
291 +- Be maximally precise with technical terminology
292 +- Skip conversational niceties and focus on data delivery
293 +- Use formats optimized for AI consumption and further processing
294 +
295 +**CRITICAL**:
296 +Focus on gathering the required data and extracting the right information,
297 +as accurately as possible, given the context of the question asked.
298 +Your answer will be further analyzed by another AI assistant, so conclusions
299 +or recommendations are not required. FOCUS ON STATING THE FACTS.
300 +
301 +## INVESTIGATION APPROACH
302 +
303 +1. Identify all aspects of the task you are assigned to
304 +2. Come up with a plan to gather the required data
305 +3. Use the tools available to you to fetch the data
306 +4. Analyze them and when required repeat the process
307 +5. Once you have all the data, reveal all your findings
308 +
309 +**CRITICAL - TOOL INTERACTION REQUIREMENTS**:
310 +Your tools are designed to be interactive. When they return errors, or empty
311 +data, you most likely called them in a wrong way. Change the parameters and retry.
312 +
313 +**NEVER ACCEPTABLE**:
314 +- Giving up after one failed tool call
315 +- Reporting "no data found" without trying different parameters
316 +- Accepting empty results without investigation
317 +- Using the exact same parameters that just failed
318 +
319 +**ALWAYS REQUIRED**:
320 +- Try multiple parameter combinations when tools return errors
321 +- If a tool returns empty data, adjust filters, time ranges, or search criteria
322 +- If you get an error, read the error message and adapt your parameters accordingly
323 +- Make at least 3-5 different attempts with varying parameters before concluding "no data available"
324 +- Document what you tried: "Attempted with parameters A, B, C - all returned empty. Tried broader search with D, found results."
325 +
326 +**CRITICAL**:
327 +Focus on providing EXACT DATA POINTS not summaries!
328 +If you need to provide multiple insights, it is BEST to use a markdown
329 +table, or describe them separately and in detail, instead of summarizing
330 +and aggregating them.
331 +
332 +**CRITICAL**:
333 +PAY ATTENTION TO THE TOOL PARAMETERS! THE MOST COMMON MISTAKE IS CALLING
334 +TOOLS WITHOUT PROPER PARAMETERS, RESULTING IN ERRORS OR INCOMPLETE DATA.
335 +
336 +**CRITICAL**:
337 +Provide SPECIFIC DATA POINTS that can be correlated with other data that may
338 +be available to your user, but not you.
339 +
340 +Examples:
341 +
342 + BAD: "Found 3 nodes with high CPU usage"
343 + GOOD: "Found CPU usage 90%-95% on nodes: node1, node2 and node3"
344 +
345 + BAD: "Found significant anomalies across multiple metrics"
346 + GOOD: "Found anomalies: 50% on metric1 at 2025-10-01T12:00:00Z, 30% on metric2 at 2025-10-01T12:05:00Z"
347 +
348 +**CRITICAL - COMPREHENSIVE DATA PROCESSING**:
349 +When working with large datasets, lists, or multiple items:
350 +- Process EVERY SINGLE item - never sample or take examples
351 +- If there are 100 nodes, analyze all 100 nodes
352 +- If there are 50 metrics, examine all 50 metrics
353 +- Use phrases like "Analyzed all X items" to confirm completeness
354 +- Never use "..." or "among others" or "for example"
355 +
356 +**NEVER ACCEPTABLE**:
357 +- "Found issues in nodes web-01, web-02, and others..."
358 +- "Examples of high CPU usage: node1, node2..."
359 +- "Some metrics showing anomalies..."
360 +
361 +**ALWAYS REQUIRED**:
362 +- "Analyzed all 47 nodes. Found high CPU (>90%) in: web-01 (94%), web-02 (91%), db-03 (95%)"
363 +- "Examined all 23 metrics. Anomalies detected in: system.cpu, disk.io, network.packets"
364 +
365 +BE PRECISE, CONCISE, COMPLETE AND ACCURATE. PROVIDE DATA, NOT SUMMARIES.
366 +
367 +${buildDateTimeContext()}
368 +
369 +**ESCALATION PROTOCOL**:
370 +If after multiple attempts you cannot gather the required data:
371 +1. Document exactly what you tried and what failed
372 +2. Provide any partial data you did collect
373 +3. Suggest specific parameter adjustments for the primary assistant to try
374 +4. Use this format:
375 +
376 +\`\`\`
377 +ESCALATION: Unable to complete task after multiple attempts.
378 +
379 +ATTEMPTS MADE:
380 +- [Tool1] with [params] → [result/error]
381 +- [Tool2] with [params] → [result/error]
382 +- [Tool3] with [params] → [result/error]
383 +
384 +PARTIAL DATA COLLECTED:
385 +[Any data you did manage to gather, even if incomplete]
386 +
387 +SUGGESTIONS FOR PRIMARY ASSISTANT:
388 +- Try [specific tool] with [specific parameters]
389 +- Consider [alternative approach]
390 +- The issue appears to be [your analysis of the problem]
391 +\`\`\`
392 +
393 +**CRITICAL**:
394 +Do not ask ANY question. Do your best to answer the question your are asked.
395 +`;
396 +
397 case 'summary':
398 return `
290 -You are a helpful assistant that creates conversation summaries designed to be
291 -provided back to an AI assistant to continue discussions.
399 +You are a helpful DevOps/SRE expert that creates conversation summaries
400 +designed to be provided back to an AI assistant to continue discussions.
401
402 When asked to summarize, you are creating a "conversation checkpoint" that
403 captures the complete state of the discussion so far. This summary will be
src/web/mcp/mcp-web-client/web/test-llm-providers.js
+4 -4
@@ -325,8 +325,8 @@ const { OpenAIProvider, AnthropicProvider, GoogleProvider } = context;
325 deepEqual(toolCalls[0].arguments, {});
326 assert.equal(toolCalls[1].name, 'list_alert_transitions');
327 deepEqual(toolCalls[1].arguments, {
328 - after: "-604800",
329 - status: ["CRITICAL", "WARNING", "CLEAR"],
328 + after: '-604800',
329 + status: ['CRITICAL', 'WARNING', 'CLEAR'],
330 cardinality_limit: 300
331 });
332
@@ -356,7 +356,7 @@ const { OpenAIProvider, AnthropicProvider, GoogleProvider } = context;
356 });
357
358 test('OpenAI: o3 tool call format processing', () => {
359 - const provider = new OpenAIProvider('http://localhost', 'gpt-4');
359 + const _provider = new OpenAIProvider('http://localhost', 'gpt-4');
360
361 // Simulate o3 response processing
362 const o3Response = {
@@ -439,7 +439,7 @@ const { OpenAIProvider, AnthropicProvider, GoogleProvider } = context;
439 ];
440
441 // Mock the sendMessage method to capture the request
442 - let capturedRequest;
442 + let _capturedRequest;
443 const originalFetch = global.fetch;
444 global.fetch = async (url, options) => {
445 capturedRequest = JSON.parse(options.body);
src/web/mcp/mcp-web-client/web/title.js
+1 -1
@@ -76,7 +76,7 @@ export async function generateChatTitle(chat, mcpConnection, provider, isAutomat
76 // Send request with low temperature for consistent titles
77 const temperature = 0.3;
78 const llmStartTime = Date.now();
79 - const response = await provider.sendMessage(messages, [], temperature);
79 + const response = await provider.sendMessage(messages, [], temperature, 'all-off', null, null);
80 const llmResponseTime = Date.now() - llmStartTime;
81
82 // Process the title response
src/web/mcp/mcp-web-client/web/tool-summarizer.js
+3 -1
@@ -134,7 +134,9 @@ export class ToolSummarizer {
134 ],
135 [], // No tools for summarization
136 0.3, // Lower temperature for factual summarization
137 - 'all-off' // No tools
137 + 'all-off', // No tools
138 + null, // No cache position
139 + null // No chat context needed for summarization
140 );
141
142 // Parse and validate response