Skip to content

Commit 0ede63f

Browse files
author
Brendan Gray
committed
v1.7.24: Fix naked code / dropped tool calls for Llama-1B/3B models
- Fix A (mcpToolParser): _detectFallbackFileOperations now extracts filePath hints from dropped write_file json headers in same response (_hintMap by extension), then applies the hint to the subsequent code block instead of falling back to generic 'main.ext' / 'index.html' filename - Fix B (mcpToolParser + agenticChat): repairToolCalls now returns droppedFilePaths. processResponse threads them to _detectFallbackFileOperations and returns them in result. agenticChat stores _pendingDroppedFilePaths across iterations and passes as lastDroppedFilePaths in textOpts so next iteration's code block gets correct filename hint when json header was in a previous generation - Fix C (ChatPanel): Write tool json fences with no content (repair-dropped) now set pendingWriteFP instead of rendering 'Skipped' CollapsibleToolBlock. Next matching code block renders as proper write_file flat widget with check icon, filename label, and Apply button. Language extension must match. - Fix D (mcpToolParser): _detectFallbackFileOperations now detects large raw HTML/code blobs without backtick fences (e.g. 77K dump from 1B context overflow). Calls _recoverWriteFileContent directly on the blob, using lastDroppedFilePaths[0] as filePath hint if available.
1 parent 0f8c486 commit 0ede63f

4 files changed

Lines changed: 91 additions & 20 deletions

File tree

main/agenticChat.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,7 @@ function register(ctx) {
943943
// Smart continuation: track tool call patterns for stuck detection
944944
let recentToolCalls = []; // [{tool, paramsHash}]
945945
const writeFileHistory = {}; // Track per-filePath write count + peak content length for regression detection
946+
let _pendingDroppedFilePaths = []; // filePaths from dropped write_file in previous iter — passed as hint to next processResponse
946947
const toolFailCounts = {}; // Track per-tool failure counts for enrichErrorFeedback
947948
let nudgesRemaining = 3; // Allow 3 nudges when model responds with text instead of tool calls
948949
let contextRotations = 0; // Track how many times we've rotated context
@@ -2091,8 +2092,9 @@ function register(ctx) {
20912092
// ── LEGACY TEXT PARSING PATH ──
20922093
// Use _stitchedForMcp (partial block from previous iter + this iter) so MCP can
20932094
// parse the reconstructed complete tool call when a fence was truncated mid-JSON.
2094-
const textOpts = { toolPaceMs: localToolPace, skipWriteDeferral: modelTier.tier === 'tiny', userMessage: message };
2095+
const textOpts = { toolPaceMs: localToolPace, skipWriteDeferral: modelTier.tier === 'tiny', userMessage: message, lastDroppedFilePaths: _pendingDroppedFilePaths };
20952096
toolResults = await mcpToolServer.processResponse(_stitchedForMcp, textOpts);
2097+
_pendingDroppedFilePaths = toolResults.droppedFilePaths || [];
20962098
}
20972099

20982100
// Cross-turn duplicate call detection — always active.

main/tools/mcpToolParser.js

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,7 @@ function parseToolCalls(text) {
625625
function repairToolCalls(toolCalls, responseText) {
626626
const repaired = [];
627627
const issues = [];
628+
const droppedFilePaths = []; // filePaths from dropped write_file (empty content) — for cross-iter fallback
628629

629630
for (const call of toolCalls) {
630631
if (!call || typeof call.tool !== 'string') continue;
@@ -645,6 +646,7 @@ function repairToolCalls(toolCalls, responseText) {
645646
}
646647
// Unrecoverable — drop it instead of executing an empty write_file
647648
issues.push(`Dropped write_file: empty content for "${filePath || '(no path)'}" and no recoverable code found in response.`);
649+
if (filePath) droppedFilePaths.push(filePath); // track for cross-iter fallback
648650
continue;
649651
}
650652

@@ -704,7 +706,7 @@ function repairToolCalls(toolCalls, responseText) {
704706
console.log(`[MCP Repair] ${issues.length} issue(s): ${issues.join(' | ')}`);
705707
}
706708

707-
return { repaired, issues };
709+
return { repaired, issues, droppedFilePaths };
708710
}
709711

710712
/**
@@ -903,8 +905,10 @@ async function processResponse(responseText, options = {}) {
903905
// Fix malformed calls BEFORE execution — recover empty write_file params,
904906
// drop unrecoverable calls, fix URLs, etc. This prevents tool errors from
905907
// polluting context and confusing the model for the rest of the session.
908+
let _repairDropped = []; // filePaths dropped here — threaded to fallback and returned for next-iter
906909
if (toolCalls.length > 0) {
907-
const { repaired, issues } = repairToolCalls(toolCalls, responseText);
910+
const { repaired, issues, droppedFilePaths: _rd } = repairToolCalls(toolCalls, responseText);
911+
_repairDropped = _rd || [];
908912
if (issues.length > 0) {
909913
console.log(`[MCP] Repair dropped/fixed ${issues.length} call(s)`);
910914
}
@@ -953,7 +957,7 @@ async function processResponse(responseText, options = {}) {
953957
toolCalls.push(...proseCommands);
954958
}
955959

956-
const fallbackCalls = this._detectFallbackFileOperations(responseText, options.userMessage);
960+
const fallbackCalls = this._detectFallbackFileOperations(responseText, options.userMessage, [..._repairDropped, ...(options.lastDroppedFilePaths || [])]);
957961
if (fallbackCalls.length > 0) {
958962
console.log('[MCP] Found fallback tool calls:', fallbackCalls.length);
959963
let effectiveFallbackCalls = fallbackCalls;
@@ -973,10 +977,10 @@ async function processResponse(responseText, options = {}) {
973977
const result = await this.executeTool(call.tool, call.params || {});
974978
results.push({ tool: call.tool, params: call.params, result });
975979
}
976-
return { hasToolCalls: true, results, capped: fbCapped, skippedToolCalls: fbSkipped, formalCallCount: 0 };
980+
return { hasToolCalls: true, results, capped: fbCapped, skippedToolCalls: fbSkipped, formalCallCount: 0, droppedFilePaths: [] };
977981
}
978982
console.log('[MCP] No fallback tool calls either');
979-
return { hasToolCalls: false, results: [], formalCallCount: 0 };
983+
return { hasToolCalls: false, results: [], formalCallCount: 0, droppedFilePaths: _repairDropped };
980984
}
981985

982986
// ── Browser Tool Capping ──
@@ -1062,14 +1066,14 @@ async function processResponse(responseText, options = {}) {
10621066
console.log(`[MCP] Browser cap enforced: executed ${browserStateChanges} state-changing actions, skipped ${browserSkipped}`);
10631067
}
10641068

1065-
return { hasToolCalls: true, results, capped: capped || browserCapped, skippedToolCalls: skippedCount + browserSkipped, formalCallCount: toolCalls.length };
1069+
return { hasToolCalls: true, results, capped: capped || browserCapped, skippedToolCalls: skippedCount + browserSkipped, formalCallCount: toolCalls.length, droppedFilePaths: _repairDropped };
10661070
}
10671071

10681072
/**
10691073
* Fallback detection for file operations when model doesn't use formal tool syntax.
10701074
* Looks for patterns like "```html\n<!DOCTYPE...```" with context suggesting file creation.
10711075
*/
1072-
function _detectFallbackFileOperations(responseText, userMessage) {
1076+
function _detectFallbackFileOperations(responseText, userMessage, lastDroppedFilePaths = []) {
10731077
const results = [];
10741078

10751079
// ── Phase 1: Bash/shell/cmd code blocks → run_command recovery ──
@@ -1114,7 +1118,22 @@ function _detectFallbackFileOperations(responseText, userMessage) {
11141118
// Explicit file-creation intent language is the only reliable signal.
11151119
// However, when the response is PREDOMINANTLY code blocks (>60% by character count),
11161120
// the model likely intended to create files, not explain concepts. Allow fallback in that case.
1117-
if (!hasFileIntent && !hasCodeBlocksWithLang) return results;
1121+
// Fix D: Detect large raw HTML/code blobs without backtick fences
1122+
// (e.g., models that dump HTML directly without tool structure after context rotation)
1123+
const _isLargeRawCodeBlob = !hasCodeBlocksWithLang && responseText.length > 1500 && (
1124+
/<html[\s>]|<!doctype\s+html/i.test(responseText) ||
1125+
(responseText.length > 4000 && /<\/html\s*>/i.test(responseText))
1126+
);
1127+
if (!hasFileIntent && !hasCodeBlocksWithLang && !_isLargeRawCodeBlob) return results;
1128+
if (!hasFileIntent && !hasCodeBlocksWithLang && _isLargeRawCodeBlob) {
1129+
const _recovered = _recoverWriteFileContent(responseText,
1130+
lastDroppedFilePaths.length > 0 ? lastDroppedFilePaths[0] : undefined);
1131+
if (_recovered) {
1132+
console.log(`[MCP] Fallback: raw code blob (${responseText.length} chars) → write_file "${_recovered.params.filePath}"`);
1133+
results.push(_recovered);
1134+
}
1135+
return results;
1136+
}
11181137
if (!hasFileIntent && hasCodeBlocksWithLang) {
11191138
// Measure code block ratio to avoid false positives on explanatory code
11201139
const _cbRegex = /```\w+\s*\n([\s\S]*?)```/g;
@@ -1127,6 +1146,7 @@ function _detectFallbackFileOperations(responseText, userMessage) {
11271146

11281147
const codeBlockRegex = /```(\w+)?\s*\n([\s\S]*?)```/g;
11291148
let match;
1149+
const _hintMap = {}; // ext→filePath hints extracted from same-response write_file json headers
11301150

11311151
const filePathPatterns = [
11321152
/(?:(?:create|write|save|make|generate).*?(?:file|document).*?(?:named?|called?|at)?)\s*[`"']([^`"'\n]+\.\w+)[`"']/gi,
@@ -1149,7 +1169,20 @@ function _detectFallbackFileOperations(responseText, userMessage) {
11491169
if (!content || content.length < 10) continue;
11501170

11511171
// Skip code blocks that look like tool call JSON (already handled by parseToolCalls)
1152-
if (lang === 'json' && /^\s*\{\s*["']?(?:tool|name)["']?\s*:/.test(content)) continue;
1172+
// But extract filePath hints from write_file/create_file headers with no content
1173+
if (lang === 'json' && /^\s*\{\s*["']?(?:tool|name)["']?\s*:/.test(content)) {
1174+
try {
1175+
const _jp = JSON.parse(content);
1176+
const _jt = _jp.tool || _jp.name;
1177+
const _jfp = _jp.params?.filePath || _jp.params?.file_path || _jp.filePath;
1178+
if ((_jt === 'write_file' || _jt === 'create_file') && _jfp &&
1179+
(!_jp.params?.content || String(_jp.params.content).length < 5)) {
1180+
const _jext = _jfp.includes('.') ? _jfp.split('.').pop().toLowerCase() : '';
1181+
if (_jext) _hintMap['.' + _jext] = _jfp;
1182+
}
1183+
} catch (_e) { /* ignore parse errors */ }
1184+
continue;
1185+
}
11531186

11541187
if (lang === 'python' || lang === 'py') {
11551188
if (/import os|open\s*\(|os\.makedirs|fs\.write/.test(content)) continue;
@@ -1202,6 +1235,23 @@ function _detectFallbackFileOperations(responseText, userMessage) {
12021235
}
12031236
}
12041237

1238+
// Use hints: 1) same-response json header (_hintMap), 2) cross-iter dropped filePaths
1239+
if (!filePath && lang && langToExt[lang] && _hintMap[langToExt[lang]]) {
1240+
filePath = _hintMap[langToExt[lang]];
1241+
delete _hintMap[langToExt[lang]]; // consume hint
1242+
console.log(`[MCP] Fallback: same-response hint "${filePath}" for ${lang} block`);
1243+
}
1244+
if (!filePath && lang && langToExt[lang] && lastDroppedFilePaths.length > 0) {
1245+
const _crossFp = lastDroppedFilePaths.find(fp => {
1246+
const _fext = fp.includes('.') ? '.' + fp.split('.').pop().toLowerCase() : '';
1247+
return langToExt[lang] === _fext;
1248+
});
1249+
if (_crossFp) {
1250+
filePath = _crossFp;
1251+
console.log(`[MCP] Fallback: cross-iter hint "${filePath}" for ${lang} block`);
1252+
}
1253+
}
1254+
12051255
if (!filePath && lang && langToExt[lang]) {
12061256
const folderMatch = textBefore.match(/(?:in|inside|within|to|folder|directory)[:\s]+[`"']?([a-zA-Z_][\w/\\-]*)[`"']?/i);
12071257
const folder = folderMatch ? folderMatch[1].replace(/\\/g, '/') : '';

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "guide-ide",
3-
"version": "1.7.23",
3+
"version": "1.7.24",
44
"description": "guIDE - AI-Powered Offline IDE with local LLM, RAG, MCP tools, browser automation, and integrated terminal",
55
"author": {
66
"name": "Brendan Gray",

src/components/Chat/ChatPanel.tsx

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1541,6 +1541,8 @@ ${e.message}`,
15411541
// ALL tool blocks are collected here — appended as a single ToolCallGroup at the
15421542
// bottom of the message. They are NEVER rendered inline in the text flow.
15431543
const allToolElements: React.ReactElement[] = [];
1544+
let pendingWriteFP: string | null = null; // filePath from write_file json header with no content — reconnects to next code block
1545+
let pendingWriteLang = ''; // mapped language for the pending filePath extension
15441546

15451547
for (let i = 0; i < parts.length; i++) {
15461548
const part = parts[i];
@@ -1575,13 +1577,12 @@ ${e.message}`,
15751577
</div>
15761578
);
15771579
} else if (isWriteTool && !writeContent && !result) {
1578-
// Write tool was in the response text but had no content and no execution result
1579-
// — it was dropped by repair (e.g., empty content). Show as skipped, not completed.
1580-
allToolElements.push(
1581-
<CollapsibleToolBlock key={`t-${i}`} label={getToolLabel(toolCall, 'fail')} icon="✗">
1582-
<div className="text-[11px] text-[#858585]">Skipped — no content provided</div>
1583-
</CollapsibleToolBlock>
1584-
);
1580+
// Write tool had no content — model likely emitted json header then code separately.
1581+
// Track the filePath so the next code block can be rendered as a write_file widget.
1582+
if (writeFilePath) {
1583+
pendingWriteFP = writeFilePath;
1584+
pendingWriteLang = writeLang;
1585+
}
15851586
} else if (result) {
15861587
allToolElements.push(
15871588
<CollapsibleToolBlock key={`t-${i}`} label={getToolLabel(toolCall, result.isOk ? 'ok' : 'fail')} icon={result.isOk ? '✓' : '✗'}>
@@ -1633,8 +1634,26 @@ ${e.message}`,
16331634
continue;
16341635
}
16351636

1636-
// Regular code block
1637-
elements.push(<CodeBlock key={i} code={code} language={lang} onApply={() => onApplyCode(currentFile, code)} />);
1637+
// Regular code block — check if preceding write_file json header gave a filePath hint
1638+
if (pendingWriteFP && (!lang || pendingWriteLang === lang)) {
1639+
const pFp = pendingWriteFP;
1640+
const pLang = pendingWriteLang || lang;
1641+
pendingWriteFP = null;
1642+
pendingWriteLang = '';
1643+
const pWriteCall = { tool: 'write_file', params: { filePath: pFp } };
1644+
elements.push(
1645+
<div key={`pending-write-${i}`} className="my-1.5">
1646+
<div className="flex items-center gap-1.5 mb-1 px-0.5">
1647+
<Check size={11} className="text-[#89d185] flex-shrink-0" />
1648+
<span className="text-[11px] text-[#d4d4d4] font-medium">{getToolLabel(pWriteCall, 'ok')}</span>
1649+
</div>
1650+
<CodeBlock code={code} language={pLang} onApply={() => onApplyCode(currentFile, code)} isToolCall={true} />
1651+
</div>
1652+
);
1653+
} else {
1654+
pendingWriteFP = null;
1655+
elements.push(<CodeBlock key={i} code={code} language={lang} onApply={() => onApplyCode(currentFile, code)} />);
1656+
}
16381657
continue;
16391658
}
16401659

0 commit comments

Comments
 (0)