Skip to content

Commit 5a414e4

Browse files
author
Brendan Gray
committed
v1.7.22: Fix append_to_file UI integration, stuck detector, overflow tool stripping, dropped tool status display, HTML escaping in chat
Fix 1: Rewrite _appendToFile() with full UI integration - IPC events (files-changed, agent-file-modified), undo backup, projectPath guard. Add append_to_file to isFileOp and open-file lists in sendToolExecutionEvents. Fix 2: Change STUCK_THRESHOLD from 2 to 3. Fix paramsHash to include p.content (not just p.text) so append_to_file calls with different content produce different hashes. Apply to both local and cloud paths. Fix 3: Overflow Step 3 no longer strips ALL tools. Replace buildStaticPrompt('chat') (which returned empty tool list) with preamble + getCompactToolHint('general', {minimal:true}) providing 7 core tools. Fix 4: Dropped tool calls (write_file with no content) now show 'Skipped - no content provided' instead of false 'Completed OK' checkmark. Applied in both persisted message and inline segment renderers. Fix 5: Escape HTML tags in markdownInlineToHTML() before markdown processing. Model-generated raw HTML no longer renders as DOM elements in chat.
1 parent ff792dc commit 5a414e4

5 files changed

Lines changed: 68 additions & 14 deletions

File tree

main/agenticChat.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ function register(ctx) {
108108
ipcMain.handle('ai-chat', async (_, message, context) => {
109109
const mainWindow = ctx.getMainWindow();
110110
const MAX_AGENTIC_ITERATIONS = context?.maxIterations || _readConfig()?.userSettings?.maxAgenticIterations || 100; // Default 100; overridable via Settings UI
111-
const STUCK_THRESHOLD = 2; // Same tool+params repeated this many times = stuck
111+
const STUCK_THRESHOLD = 3; // Same tool+params repeated this many times = stuck
112112
const CYCLE_MIN_REPEATS = 3; // A 2-4 tool cycle must repeat this many times to be flagged
113113
let _completenessCheckedFiles = null; // One-shot guard for post-write completeness checks
114114

@@ -518,7 +518,7 @@ function register(ctx) {
518518
// Enable via Settings → enableLoopDetection: true.
519519
for (const tr of iterationToolResults) {
520520
const p = tr.params || {};
521-
const paramsHash = `${p.filePath || p.url || p.ref || p.query || p.command || p.selector || ''}:${p.text || ''}`.substring(0, 200);
521+
const paramsHash = `${p.filePath || p.dirPath || p.url || p.ref || p.query || p.command || p.selector || ''}:${(p.text || p.content || '').substring(0, 80)}`.substring(0, 200);
522522
recentCloudToolCalls.push({ tool: tr.tool, paramsHash });
523523
}
524524
if (recentCloudToolCalls.length > 20) recentCloudToolCalls = recentCloudToolCalls.slice(-20);
@@ -1182,19 +1182,23 @@ function register(ctx) {
11821182
_ftRetryText = (currentPrompt.systemContext || '') + (currentPrompt.userMessage || '');
11831183
_ftRetryTokens = estimateTokens(_ftRetryText);
11841184
}
1185-
// Step 3: If still tight after stripping dynamic context, strip tool
1186-
// descriptions from the static prompt. Better to respond without tools than
1187-
// to overflow and truncate mid-tool-call.
1185+
// Step 3: If still tight after stripping dynamic context, use a minimal
1186+
// tool hint instead of the full tool prompt. The model keeps essential file
1187+
// tools (write_file, append_to_file, read_file, edit_file, list_directory,
1188+
// run_command, web_search) but skips browser/memory/planning tools.
11881189
if (totalCtx - _ftRetryTokens < Math.floor(totalCtx * 0.15)) {
1190+
const minimalToolHint = mcpToolServer.getCompactToolHint('general', { minimal: true });
1191+
// Build preamble without tools, then append minimal hint
11891192
_staticPromptCache.clear();
1193+
const preambleOnly = buildStaticPrompt('chat'); // preamble + project instructions, no tools
11901194
currentPrompt = {
1191-
systemContext: buildStaticPrompt('chat'), // 'chat' skips tool injection
1195+
systemContext: preambleOnly + '\n' + minimalToolHint + '\n',
11921196
userMessage: message
11931197
};
11941198
_ftRetryText = (currentPrompt.systemContext || '') + (currentPrompt.userMessage || '');
11951199
_ftRetryTokens = estimateTokens(_ftRetryText);
1196-
_staticPromptCache.clear(); // Clear so subsequent iterations get tools back
1197-
console.log(`[AI Chat] Overflow step 3: stripped tools from static prompt, now ~${_ftRetryTokens} tokens`);
1200+
_staticPromptCache.clear(); // Clear so subsequent iterations get full tools back
1201+
console.log(`[AI Chat] Overflow step 3: using minimal tool hint, now ~${_ftRetryTokens} tokens`);
11981202
}
11991203
if (totalCtx - _ftRetryTokens < 128) {
12001204
// Still too tight — cap effectiveMaxTokens to whatever room remains
@@ -2459,7 +2463,7 @@ function register(ctx) {
24592463
// STUCK_THRESHOLD=3 consecutive identical calls = stuck.
24602464
for (const tr of toolResults.results) {
24612465
const p = tr.params || {};
2462-
const paramsHash = `${p.filePath || p.dirPath || p.url || p.ref || p.query || p.command || p.selector || ''}:${p.text || ''}`.substring(0, 200);
2466+
const paramsHash = `${p.filePath || p.dirPath || p.url || p.ref || p.query || p.command || p.selector || ''}:${(p.text || p.content || '').substring(0, 80)}`.substring(0, 200);
24632467
recentToolCalls.push({ tool: tr.tool, paramsHash });
24642468
}
24652469
if (recentToolCalls.length > 20) recentToolCalls = recentToolCalls.slice(-20);

main/agenticChatHelpers.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,11 @@ function sendToolExecutionEvents(mainWindow, toolResults, playwrightBrowser, opt
158158
if (tr.tool?.startsWith('browser_') && !playwrightBrowser?.isLaunched) {
159159
mainWindow.webContents.send('show-browser', { url: tr.params?.url || '' });
160160
}
161-
const isFileOp = ['write_file', 'create_directory', 'edit_file', 'delete_file', 'rename_file'].includes(tr.tool);
161+
const isFileOp = ['write_file', 'append_to_file', 'create_directory', 'edit_file', 'delete_file', 'rename_file'].includes(tr.tool);
162162
const passed = checkSuccess ? tr.result?.success : true;
163163
if (isFileOp && passed) {
164164
filesChanged = true;
165-
if (['write_file', 'edit_file'].includes(tr.tool) && tr.params?.filePath) {
165+
if (['write_file', 'append_to_file', 'edit_file'].includes(tr.tool) && tr.params?.filePath) {
166166
mainWindow.webContents.send('open-file', tr.params.filePath);
167167
}
168168
}

main/mcpToolServer.js

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1929,11 +1929,43 @@ class MCPToolServer {
19291929
}
19301930

19311931
async _appendToFile(filePath, content) {
1932-
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(this.projectPath || '', filePath);
1932+
if (!this.projectPath) {
1933+
return {
1934+
success: false,
1935+
error: 'No project folder is open. Please open a folder first (File > Open Folder or Ctrl+K Ctrl+O), then retry.',
1936+
};
1937+
}
1938+
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(this.projectPath, filePath);
19331939
try {
1940+
// Backup existing content for undo (before append)
1941+
let isNew = true;
1942+
try {
1943+
const existingContent = await fs.readFile(fullPath, 'utf8');
1944+
this._setFileBackup(fullPath, { original: existingContent, timestamp: Date.now(), tool: 'append_to_file', isNew: false });
1945+
isNew = false;
1946+
} catch {
1947+
this._setFileBackup(fullPath, { original: null, timestamp: Date.now(), tool: 'append_to_file', isNew: true });
1948+
}
1949+
19341950
await fs.mkdir(path.dirname(fullPath), { recursive: true });
19351951
await fs.appendFile(fullPath, content, 'utf8');
1936-
return { success: true, path: fullPath, message: `Appended ${content.length} chars to ${path.basename(fullPath)}` };
1952+
1953+
// Read full file content after append for UI notification
1954+
let fullContent = content;
1955+
try { fullContent = await fs.readFile(fullPath, 'utf8'); } catch { /* use appended content */ }
1956+
1957+
// Notify renderer so file tree refreshes and editor shows changes
1958+
if (this.browserManager?.parentWindow) {
1959+
this.browserManager.parentWindow.webContents.send('files-changed');
1960+
this.browserManager.parentWindow.webContents.send('agent-file-modified', {
1961+
filePath: fullPath,
1962+
newContent: fullContent,
1963+
isNew,
1964+
tool: 'append_to_file',
1965+
});
1966+
}
1967+
1968+
return { success: true, path: fullPath, isNew, message: `Appended ${content.length} chars to ${path.basename(fullPath)}` };
19371969
} catch (error) {
19381970
return { success: false, error: error.message };
19391971
}

src/components/Chat/ChatPanel.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,6 +1569,14 @@ ${e.message}`,
15691569
<CodeBlock code={writeContent} language={writeLang} onApply={() => onApplyCode(currentFile, writeContent)} isToolCall={true} />
15701570
</div>
15711571
);
1572+
} else if (isWriteTool && !writeContent && !result) {
1573+
// Write tool was in the response text but had no content and no execution result
1574+
// — it was dropped by repair (e.g., empty content). Show as skipped, not completed.
1575+
allToolElements.push(
1576+
<CollapsibleToolBlock key={`t-${i}`} label={getToolLabel(toolCall, 'fail')} icon="✗">
1577+
<div className="text-[11px] text-[#858585]">Skipped — no content provided</div>
1578+
</CollapsibleToolBlock>
1579+
);
15721580
} else if (result) {
15731581
allToolElements.push(
15741582
<CollapsibleToolBlock key={`t-${i}`} label={getToolLabel(toolCall, result.isOk ? 'ok' : 'fail')} icon={result.isOk ? '✓' : '✗'}>
@@ -1711,6 +1719,12 @@ ${e.message}`,
17111719
<CodeBlock code={inlineWriteContent} language={inlineWriteLang} onApply={() => onApplyCode(currentFile, inlineWriteContent)} isToolCall={true} />
17121720
</div>
17131721
);
1722+
} else if (isInlineWriteTool && !inlineWriteContent && !result) {
1723+
allToolElements.push(
1724+
<CollapsibleToolBlock key={`inline-${i}-${j}-${allToolElements.length}`} label={getToolLabel(seg.toolCall, 'fail')} icon="✗">
1725+
<div className="text-[11px] text-[#858585]">Skipped — no content provided</div>
1726+
</CollapsibleToolBlock>
1727+
);
17141728
} else if (result) {
17151729
allToolElements.push(
17161730
<CollapsibleToolBlock key={`inline-${i}-${j}-${allToolElements.length}`} label={getToolLabel(seg.toolCall, result.isOk ? 'ok' : 'fail')} icon={result.isOk ? '✓' : '✗'}>

src/utils/sanitize.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ export function markdownInlineToHTML(text: string): string {
6666
const output: string[] = [];
6767
let inList = false;
6868

69-
for (const line of lines) {
69+
for (let line of lines) {
70+
// Escape raw HTML tags so model output doesn't render as actual DOM elements.
71+
// This runs BEFORE markdown processing, so markdown-generated HTML (from
72+
// applyInlineMarkdown) is unaffected. Only raw model-output HTML is escaped.
73+
line = line.replace(/</g, '&lt;').replace(/>/g, '&gt;');
7074
if (/^#{4} /.test(line)) {
7175
if (inList) { output.push('</ul>'); inList = false; }
7276
output.push(`<h4>${applyInlineMarkdown(line.slice(5))}</h4>`);

0 commit comments

Comments
 (0)