Skip to content

Commit 27615fd

Browse files
author
Brendan Gray
committed
v1.7.8: remove renderer truncation caps; fix fence-continuation stitching
1 parent 8604b16 commit 27615fd

3 files changed

Lines changed: 41 additions & 30 deletions

File tree

main/agenticChat.js

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,6 +1126,9 @@ function register(ctx) {
11261126
// Seamless continuation counter — tracks how many times we've continued a
11271127
// truncated response in the same bubble. Reset to 0 each new user message.
11281128
let continuationCount = 0;
1129+
// When EOS fires mid-tool-fence, save the partial block here so the next
1130+
// continuation iteration can prepend it for MCP to reconstruct the full tool call.
1131+
let _pendingPartialBlock = null;
11291132
while (iteration < effectiveMaxIterations) {
11301133
// Check if user cancelled or a newer request superseded us
11311134
if (isStale()) {
@@ -1670,11 +1673,11 @@ function register(ctx) {
16701673
let displayChunk = responseText
16711674
.replace(/\n?```(?:json|tool_call|tool)\b[\s\S]*?```\n?/g, '')
16721675
.replace(/\n{3,}/g, '\n\n');
1673-
// Strip echoed continuation prompt — small models sometimes echo our bracketed
1674-
// instruction back as output instead of continuing. This is NOT a user-input classifier;
1675-
// it detects only our own constant continuation prompt string being reflected by the model.
1676+
// Strip echoed continuation prompts — small models sometimes echo our bracketed
1677+
// instructions back as output instead of continuing. This is NOT a user-input classifier;
1678+
// it detects only our own constant continuation prompt strings being reflected by the model.
16761679
if (continuationCount > 0) {
1677-
displayChunk = displayChunk.replace(/\[Continue your response[\s\S]*?\]/gi, '');
1680+
displayChunk = displayChunk.replace(/\[(?:Continue your response|You were generating a tool call)[\s\S]*?\]/gi, '');
16781681
}
16791682
displayResponseText += displayChunk;
16801683

@@ -1685,8 +1688,17 @@ function register(ctx) {
16851688
// Also trigger seamless continuation when EOS fires mid-tool-call (unclosed fenced block).
16861689
// This happens with small models (e.g. Qwen3-0.6B) that emit the EOS token before
16871690
// closing the ```json block — stopReason comes back as 'eogToken', not 'maxTokens'.
1688-
const _fenceIdx = responseText.search(/```(?:json|tool_call|tool)\b/);
1689-
const _hasUnclosedToolFence = _fenceIdx !== -1 && !responseText.slice(_fenceIdx).includes('\n```');
1691+
//
1692+
// FENCE STITCHING: if the previous iteration had an unclosed tool fence, _pendingPartialBlock
1693+
// holds that partial JSON. Prepend it here so that:
1694+
// (a) _hasUnclosedToolFence evaluates on the complete accumulated block, and
1695+
// (b) MCP receives the reconstructed complete tool call for parsing & execution.
1696+
const _stitchedForMcp = _pendingPartialBlock
1697+
? _pendingPartialBlock + responseText
1698+
: responseText;
1699+
_pendingPartialBlock = null; // consumed — will be re-set below if this iteration also truncates
1700+
const _fenceIdx = _stitchedForMcp.search(/```(?:json|tool_call|tool)\b/);
1701+
const _hasUnclosedToolFence = _fenceIdx !== -1 && !_stitchedForMcp.slice(_fenceIdx).includes('\n```');
16901702
const _wasTruncated = (
16911703
(result?.stopReason === 'maxTokens' || result?.stopReason === 'max-tokens') ||
16921704
_hasUnclosedToolFence
@@ -1699,9 +1711,20 @@ function register(ctx) {
16991711
const _truncReason = _hasUnclosedToolFence ? 'unclosed tool fence (EOS mid-block)' : 'maxTokens';
17001712
console.log(`[AI Chat] Seamless continuation ${continuationCount}/3 — ${_truncReason}, continuing in same bubble`);
17011713
iteration--; // Continuation is not a new agentic step
1714+
let _continuationUserMsg;
1715+
if (_hasUnclosedToolFence) {
1716+
// EOS fired mid-JSON: tell the model exactly where the output was cut.
1717+
// It must output ONLY the remainder to complete the tool call — no restart.
1718+
const _partialFenceContent = _stitchedForMcp.slice(_fenceIdx);
1719+
_pendingPartialBlock = _partialFenceContent; // save so next iter MCP can reconstruct
1720+
_continuationUserMsg = `[You were generating a tool call and the context window was exhausted mid-JSON. The partial output is shown below — output ONLY the remainder of the JSON continuing from exactly where it ends, then close the object and the code fence. Do NOT restart the tool call from the beginning. Do NOT add any preamble or explanation. Continue from:\n\n${_partialFenceContent}]`;
1721+
} else {
1722+
// maxTokens hit mid-text — original behaviour
1723+
_continuationUserMsg = '[Continue your response exactly where you left off. If you were listing steps or planning, STOP — do not add more steps. Call the first tool now to begin execution. If you were in the middle of a tool call, call that tool now to complete the task — do not output tool content as raw text. Output only the continuation — no preamble, no summary, no repeated content.]';
1724+
}
17021725
currentPrompt = {
17031726
systemContext: currentPrompt.systemContext, // Unchanged — KV cache preserved
1704-
userMessage: '[Continue your response exactly where you left off. If you were listing steps or planning, STOP — do not add more steps. Call the first tool now to begin execution. If you were in the middle of a tool call, call that tool now to complete the task — do not output tool content as raw text. Output only the continuation — no preamble, no summary, no repeated content.]',
1727+
userMessage: _continuationUserMsg,
17051728
};
17061729
continue;
17071730
}
@@ -1880,8 +1903,10 @@ function register(ctx) {
18801903
};
18811904
} else {
18821905
// ── LEGACY TEXT PARSING PATH ──
1906+
// Use _stitchedForMcp (partial block from previous iter + this iter) so MCP can
1907+
// parse the reconstructed complete tool call when a fence was truncated mid-JSON.
18831908
const textOpts = { toolPaceMs: localToolPace, skipWriteDeferral: modelTier.tier === 'tiny' };
1884-
toolResults = await mcpToolServer.processResponse(responseText, textOpts);
1909+
toolResults = await mcpToolServer.processResponse(_stitchedForMcp, textOpts);
18851910
}
18861911

18871912
// Duplicate call detection — disabled by default.

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.7",
3+
"version": "1.7.8",
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: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,23 +1215,10 @@ ${e.message}`,
12151215
return detail ? `${tool}: ${detail}${statusText}` : `${tool}${statusText}`;
12161216
};
12171217

1218-
// Truncate tool call JSON content for display (especially large write_file content)
1219-
const truncateToolContent = (content: string, maxLines = 15): string => {
1220-
// Check if it's JSON with large content fields
1221-
try {
1222-
const parsed = JSON.parse(content);
1223-
if (parsed.params?.content && typeof parsed.params.content === 'string' && parsed.params.content.length > 500) {
1224-
const truncated = { ...parsed, params: { ...parsed.params, content: parsed.params.content.substring(0, 2000) + '...[truncated]' } };
1225-
return JSON.stringify(truncated, null, 2);
1226-
}
1227-
} catch { /* not JSON, truncate raw */ }
1228-
1229-
const lines = content.split('\n');
1230-
if (lines.length > maxLines) {
1231-
return lines.slice(0, maxLines).join('\n') + `\n...[${lines.length - maxLines} more lines]`;
1232-
}
1233-
return content;
1234-
};
1218+
// Display tool call JSON content in full — no truncation. The <pre> elements use
1219+
// max-h / overflow-y-auto so large content scrolls within the bubble instead of
1220+
// being cut off with a misleading '[truncated]' marker.
1221+
const truncateToolContent = (content: string): string => content;
12351222

12361223
// ── Generated Image Preview Component ──
12371224
const GeneratedImagePreview: React.FC<{
@@ -1564,7 +1551,7 @@ ${e.message}`,
15641551
<CollapsibleToolBlock key={`t-${i}`} label={getToolLabel(toolCall, result.isOk ? 'ok' : 'fail')} icon={result.isOk ? '✓' : '✗'}>
15651552
<div>
15661553
<div className="text-[10px] text-[#858585] mb-1 font-medium tracking-wide">PARAMETERS</div>
1567-
<pre className="whitespace-pre-wrap text-[11px] font-mono text-[#d4d4d4] bg-[#1e1e1e] rounded-md p-2 mb-2">{truncateToolContent(code)}</pre>
1554+
<pre className="whitespace-pre-wrap text-[11px] font-mono text-[#d4d4d4] bg-[#1e1e1e] rounded-md p-2 mb-2 max-h-[300px] overflow-y-auto">{code}</pre>
15681555
<div className="border-t border-[#333] pt-2">
15691556
<div className={`text-[10px] mb-1 font-medium tracking-wide ${result.isOk ? 'text-[#89d185]' : 'text-[#f14c4c]'}`}>RESULT</div>
15701557
<pre className="whitespace-pre-wrap text-[11px] font-mono text-[#d4d4d4] bg-[#1e1e1e] rounded-md p-2">{result.text}</pre>
@@ -1688,7 +1675,7 @@ ${e.message}`,
16881675
<CollapsibleToolBlock key={`inline-${i}-${j}-${allToolElements.length}`} label={getToolLabel(seg.toolCall, result.isOk ? 'ok' : 'fail')} icon={result.isOk ? '✓' : '✗'}>
16891676
<div>
16901677
<div className="text-[10px] text-[#858585] mb-1 font-medium tracking-wide">PARAMETERS</div>
1691-
<pre className="whitespace-pre-wrap text-[11px] font-mono text-[#d4d4d4] bg-[#1e1e1e] rounded-md p-2 mb-2">{truncateToolContent(seg.content)}</pre>
1678+
<pre className="whitespace-pre-wrap text-[11px] font-mono text-[#d4d4d4] bg-[#1e1e1e] rounded-md p-2 mb-2 max-h-[300px] overflow-y-auto">{seg.content}</pre>
16921679
<div className="border-t border-[#333] pt-2">
16931680
<div className={`text-[10px] mb-1 font-medium tracking-wide ${result.isOk ? 'text-[#89d185]' : 'text-[#f14c4c]'}`}>RESULT</div>
16941681
<pre className="whitespace-pre-wrap text-[11px] font-mono text-[#d4d4d4] bg-[#1e1e1e] rounded-md p-2">{result.text}</pre>
@@ -2555,7 +2542,6 @@ ${e.message}`,
25552542
}
25562543
} catch {}
25572544
const genLabel = partialDetail ? `${tc.functionName}: ${partialDetail}` : tc.functionName;
2558-
const displayText = tc.paramsText.length > 5000 ? tc.paramsText.substring(0, 5000) + '\n\u2026[truncated]' : tc.paramsText;
25592545
return (
25602546
<CollapsibleToolBlock key={`gen-${tc.callIndex}`} label={genLabel} icon="\u29d7">
25612547
<div>
@@ -2564,7 +2550,7 @@ ${e.message}`,
25642550
<span className="text-[11px] text-[#858585]">Generating tool call\u2026</span>
25652551
</div>
25662552
{tc.paramsText && (
2567-
<pre className="whitespace-pre-wrap text-[11px] font-mono text-[#d4d4d4] bg-[#1e1e1e] rounded-md p-2 max-h-[180px] overflow-y-auto">{displayText}</pre>
2553+
<pre className="whitespace-pre-wrap text-[11px] font-mono text-[#d4d4d4] bg-[#1e1e1e] rounded-md p-2 max-h-[180px] overflow-y-auto">{tc.paramsText}</pre>
25682554
)}
25692555
</div>
25702556
</CollapsibleToolBlock>

0 commit comments

Comments
 (0)