Skip to content

Commit 2db13ee

Browse files
author
Brendan Gray
committed
v1.6.11 — remove gen timeout, live tool-generation streaming bubble, conversational ack, typewriter race fix
1 parent c036364 commit 2db13ee

12 files changed

Lines changed: 163 additions & 19 deletions

File tree

.github/copilot-instructions.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,23 @@ When the user says "read your instructions", "read the instructions", "read copi
2020
2121
---
2222

23+
## ⚠ CRITICAL — THIS MACHINE IS THE DEV MACHINE ONLY — NEVER TOUCH THE WEBSITE SERVER FROM HERE
24+
25+
**This computer (`C:\Users\brend\IDE`) is the DEVELOPMENT machine. It is NOT the web server.**
26+
27+
- The live website (`graysoft.dev`) runs on a SEPARATE server.
28+
- That server is kept in sync via **Syncthing** — file changes pushed from this machine are automatically picked up by the server.
29+
- **NEVER run `npm run build` in `website/` from this machine** — the server handles its own build.
30+
- **NEVER run `pm2 restart`, `pm2 start`, or any PM2 command from a local terminal** on this machine.
31+
- **NEVER run `start-graysoft.bat` or `restart-graysoft.bat`** from this machine.
32+
- **NEVER attempt to restart the Cloudflare tunnel** from this machine.
33+
- To deploy a website change: edit the source file here → Syncthing syncs it to the server → then trigger rebuild via the control panel below.
34+
- **AUTHORIZED server control panel: https://cp.graysoft.dev** — login password: `diggabyte2026`
35+
- Use the control panel to trigger npm build, PM2 restart, or any server-side action needed to deploy changes.
36+
- After triggering via the panel, verify graysoft.dev visually to confirm the change is live.
37+
38+
---
39+
2340
## ⚠ MANDATORY — CLEAR LOGS AFTER EVERY BUILD/TEST ITERATION
2441

2542
After EVERY build, test run, or iteration where the user is about to test the app:
@@ -36,6 +53,7 @@ After EVERY build, test run, or iteration where the user is about to test the ap
3653
Read this list first. Every item has a full section below.
3754

3855
- **TRIPWIRE** — First line of EVERY response must be `[Task: X | Last: Y]`
56+
- **DEV MACHINE ONLY** — NEVER run `npm run build`, PM2, or any server command in `website/` — server is separate, updated via Syncthing
3957
- **Read full instructions** — SEE TOP OF FILE. Every single time, no exceptions, no "I already remember them"
4058
- **No green checkmarks** — NEVER use ✅ ✔️ or say "ready", "working", "all set" to describe a fix
4159
- **Read code before responding** — Never assume. Verify everything with actual file reads

main/agenticChat.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ function register(ctx) {
346346
const toolPrompt = mcpToolServer.getToolPromptForTask(cloudTaskType);
347347
const isBundledCloudProvider = cloudLLM._isBundledProvider(context.cloudProvider) && !cloudLLM.isUsingOwnKey(context.cloudProvider);
348348
const _brevityDirective = isBundledCloudProvider
349-
? '\n\nStyle rules (apply silently — never mention these rules to the user):\n- Always respond in a professional, clear, and articulate style with proper grammar, capitalization, and punctuation regardless of how the user writes.\n- Keep responses concise. For conversational or informational questions, use no more than 3 paragraphs. Never exceed 3 paragraphs for non-code responses.\n- For code or technical output, always provide the complete solution without padding or filler text.'
349+
? `\n\n## Style Rules (apply silently — never mention, reference, or apologize for these rules to the user)\n\n### Response length — hard limit\n- **Maximum 3 paragraphs** for any prose response. This limit is unconditional and applies to ALL non-code content: explanations, answers, summaries, stories, essays, creative writing, descriptions, and conversational replies.\n- If the user asks for something long or detailed (e.g. "write me a long story", "explain in depth", "be thorough") — write the best possible 3-paragraph version and stop. Do NOT explain the length, apologize for it, or mention that you are constrained. Simply deliver the best complete answer in 3 paragraphs.\n- Bullet lists count as prose when each bullet is a full sentence or longer. Keep bullet lists to a maximum of 5 items unless they are discrete technical items (file names, commands, error codes, parameters). Never use bullets as a way to extend past the 3-paragraph limit.\n- Code blocks, terminal output, file contents, structured data (tables, JSON, numbered technical steps), and inline code snippets are fully exempt from this limit. Always provide complete and correct code — never truncate.\n\n### Tone and style\n- Always write in a professional, clear, and articulate style with proper grammar, capitalization, and punctuation — regardless of how the user writes. Never mirror informal tone, typos, or lowercase writing.\n- Be direct. Lead with the answer. Never open with filler phrases like "That's a great question!", "Certainly!", "Of course!", "Absolutely!", or "Sure!".\n- Never end a response with hollow sign-offs like "I hope this helps!", "Let me know if you need anything else!", or "Feel free to ask!".`
350350
: '';
351351
const cloudSystemPrompt = systemPrompt + (toolPrompt ? '\n\n' + toolPrompt : '') + _brevityDirective;
352352

@@ -1415,6 +1415,13 @@ function register(ctx) {
14151415
// llm-token text stream caused duplicate code bubbles when parseToolCall failed
14161416
// on aliased or alternate-format tool calls from small models. Suppressed here.
14171417
void funcCall;
1418+
},
1419+
(toolChunk) => {
1420+
// Stream tool generation progress to the renderer for live bubble display.
1421+
// The renderer shows a CollapsibleToolBlock with partial params as they stream in.
1422+
if (mainWindow && !mainWindow.isDestroyed()) {
1423+
mainWindow.webContents.send('llm-tool-generating', toolChunk);
1424+
}
14181425
}
14191426
);
14201427
result = nativeResult;

main/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ const DEFAULT_COMPACT_PREAMBLE = `You are a local AI coding assistant with tools
8888
- **You do not know today's date or current real-world state. If asked for the date, time, or any live or time-sensitive information — call web_search immediately. Never state a current date, time, or real-world value from memory.**
8989
- Acknowledge the user's request, then call the tools needed — you have no knowledge of file contents until you read them
9090
- After tools return, explain what you found — don't just say a tool ran
91+
- After completing a tool call, always write at least one sentence confirming what was done — never end your response on a bare tool call with no acknowledgment
9192
- Never copy or repeat sentences you have already written in this response.
9293
- Ask a specific follow-up if you need more context
9394

main/llmEngine.js

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,10 @@ class LLMEngine extends EventEmitter {
7373
seed: -1,
7474
};
7575

76-
// User-configurable generation timeout (ms). Default 120s.
76+
// User-configurable generation timeout (ms). Default 0 = no timeout (user cancels manually).
7777
// Can be updated live via Settings without reloading the model.
78-
this.generationTimeoutMs = 120_000;
78+
// Set > 0 in Settings to re-enable a hard timeout.
79+
this.generationTimeoutMs = 0;
7980
}
8081

8182
/**
@@ -973,13 +974,13 @@ After your brief acknowledgment, output ONLY the tool call blocks — no extra t
973974
this.abortController = new AbortController();
974975

975976
// Generation safety timeout: abort if generation exceeds configured limit.
976-
// Configurable via Settings UI — default 120s. Updates live without model reload.
977+
// 0 = no timeout (users can cancel manually). Configurable in Settings.
977978
const GEN_TIMEOUT_MS = this.generationTimeoutMs;
978-
const genTimeoutTimer = setTimeout(() => {
979+
const genTimeoutTimer = GEN_TIMEOUT_MS > 0 ? setTimeout(() => {
979980
console.log(`[LLM] Generation timeout (${GEN_TIMEOUT_MS / 1000}s) — aborting to prevent hang`);
980981
this._lastAbortReason = 'timeout';
981982
this.cancelGeneration('timeout');
982-
}, GEN_TIMEOUT_MS);
983+
}, GEN_TIMEOUT_MS) : null;
983984

984985
let fullResponse = '';
985986
let rawResponse = '';
@@ -1722,7 +1723,7 @@ After your brief acknowledgment, output ONLY the tool call blocks — no extra t
17221723
* @param {Function} onFunctionCall - Called when a function call is generated
17231724
* @returns {Object} {text, functionCalls: [{functionName, params}], stopReason}
17241725
*/
1725-
async generateWithFunctions(input, functions, params = {}, onToken, onThinkingToken, onFunctionCall) {
1726+
async generateWithFunctions(input, functions, params = {}, onToken, onThinkingToken, onFunctionCall, onToolGenerating) {
17261727
if (!this.isReady || !this.chat) {
17271728
throw new Error('Model not loaded. Please load a model first.');
17281729
}
@@ -1758,15 +1759,18 @@ After your brief acknowledgment, output ONLY the tool call blocks — no extra t
17581759
this.abortController = new AbortController();
17591760

17601761
// Safety timeout — uses same configurable limit as generateStream()
1762+
// 0 = no timeout (user can cancel manually).
17611763
const GEN_TIMEOUT_MS = this.generationTimeoutMs;
1762-
const genTimeoutTimer = setTimeout(() => {
1764+
const genTimeoutTimer = GEN_TIMEOUT_MS > 0 ? setTimeout(() => {
17631765
console.log(`[LLM] Function-calling generation timeout — aborting`);
17641766
this._lastAbortReason = 'timeout';
17651767
this.cancelGeneration('timeout');
1766-
}, GEN_TIMEOUT_MS);
1768+
}, GEN_TIMEOUT_MS) : null;
17671769

17681770
let fullResponse = '';
17691771
let collectedFunctionCalls = [];
1772+
// Accumulate paramsChunk text per callIndex for live streaming to UI
1773+
const _paramsChunkBufs = {};
17701774

17711775
try {
17721776
this._compactHistory();
@@ -1821,9 +1825,18 @@ After your brief acknowledgment, output ONLY the tool call blocks — no extra t
18211825
if (onFunctionCall) onFunctionCall(funcCall);
18221826
},
18231827
onFunctionCallParamsChunk: (chunk) => {
1824-
// Stream function call params as they generate (for UI feedback)
1825-
if (chunk.done && onToken) {
1826-
onToken(`\n\`\`\`json\n{"tool":"${chunk.functionName}","params":...}\n\`\`\`\n`);
1828+
// Accumulate paramsChunk text per callIndex and stream live to UI.
1829+
// This powers the streaming tool generation bubble in the renderer
1830+
// so users can see what the model is writing instead of a blank screen.
1831+
if (!_paramsChunkBufs[chunk.callIndex]) _paramsChunkBufs[chunk.callIndex] = '';
1832+
if (chunk.paramsChunk) _paramsChunkBufs[chunk.callIndex] += chunk.paramsChunk;
1833+
if (onToolGenerating) {
1834+
onToolGenerating({
1835+
callIndex: chunk.callIndex,
1836+
functionName: chunk.functionName,
1837+
paramsText: _paramsChunkBufs[chunk.callIndex],
1838+
done: !!chunk.done,
1839+
});
18271840
}
18281841
},
18291842
} : {}),

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.6.10",
3+
"version": "1.6.11",
44
"description": "guIDE - AI-Powered Offline IDE with local LLM, RAG, MCP tools, browser automation, and integrated terminal",
55
"author": {
66
"name": "Brendan Gray",

preload.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
9191
onLlmReplaceLast: (callback) => _on('llm-replace-last', callback),
9292
onLlmStreamReset: (callback) => _on('llm-stream-reset', callback),
9393
onLlmIterationBegin: (callback) => _on('llm-iteration-begin', callback),
94+
onLlmToolGenerating: (callback) => _on('llm-tool-generating', callback),
9495
onDevLog: (callback) => _on('dev-log', callback),
9596

9697
// ── Model Management ──

src/components/Chat/ChatPanel.tsx

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,8 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
153153

154154
const streaming = useChatStreaming();
155155
const { streamingText, thinkingSegments, setStreamingText, setThinkingSegments,
156-
streamBufferRef, thinkingSegmentsRef, wasRespondingRef, streamEpochRef, activeEpochRef } = streaming;
156+
streamBufferRef, thinkingSegmentsRef, wasRespondingRef, streamEpochRef, activeEpochRef,
157+
waitForTypewriterDone } = streaming;
157158

158159
const addSystemMessage = useCallback((content: string) => {
159160
setMessages(prev => [...prev, {
@@ -346,6 +347,9 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
346347
let executingTimeout: ReturnType<typeof setTimeout> | null = null;
347348

348349
const cleanupExecuting = api.onToolExecuting?.((data: { tool: string; params: any }) => {
350+
// Tool is now executing — clear the generating-phase bubble
351+
generatingToolCallsRef.current = [];
352+
setGeneratingToolCalls([]);
349353
const updated = [...executingToolsRef.current, { tool: data.tool, params: data.params }];
350354
executingToolsRef.current = updated;
351355
setExecutingTools(updated);
@@ -359,6 +363,9 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
359363

360364
const cleanupResults = api.onMcpToolResults?.(() => {
361365
if (executingTimeout) clearTimeout(executingTimeout);
366+
// Clear any lingering generating-phase bubbles
367+
generatingToolCallsRef.current = [];
368+
setGeneratingToolCalls([]);
362369
// BUG-NEW-A: Move currently-executing tools to completed so their pills stay visible
363370
// as ✓ checkmarks instead of vanishing the instant the tool finishes.
364371
const finished = executingToolsRef.current;
@@ -371,6 +378,16 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
371378
refreshPendingChanges();
372379
});
373380

381+
const cleanupToolGenerating = api.onLlmToolGenerating?.((data: { callIndex: number; functionName: string; paramsText: string; done: boolean }) => {
382+
// Update or remove the entry for this callIndex
383+
const filtered = generatingToolCallsRef.current.filter(t => t.callIndex !== data.callIndex);
384+
if (!data.done) {
385+
filtered.push({ callIndex: data.callIndex, functionName: data.functionName, paramsText: data.paramsText });
386+
}
387+
generatingToolCallsRef.current = filtered;
388+
setGeneratingToolCalls([...filtered]);
389+
});
390+
374391
const cleanupProgress = api.onAgenticProgress?.((data: { iteration: number; maxIterations: number }) => {
375392
setAgenticProgress(data);
376393
});
@@ -430,6 +447,7 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
430447
if (executingTimeout) clearTimeout(executingTimeout);
431448
cleanupExecuting?.();
432449
cleanupResults?.();
450+
cleanupToolGenerating?.();
433451
cleanupProgress?.();
434452
cleanupPhase?.();
435453
cleanupTodo?.();
@@ -708,6 +726,8 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
708726
setStreamingText('');
709727
setThinkingSegments([]);
710728
setCompletedStreamingTools([]);
729+
generatingToolCallsRef.current = [];
730+
setGeneratingToolCalls([]);
711731
executingToolsRef.current = [];
712732

713733
try {
@@ -781,6 +801,16 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
781801
await new Promise(r => setTimeout(r, 0));
782802
}
783803

804+
// Wait for the typewriter to finish revealing all buffered chars before committing
805+
// the assistant message bubble. Prevents wall-of-text flash on fast cloud responses
806+
// where dispose() flushes all remaining chars in a single IPC call, delivering them
807+
// to the renderer faster than the 100 chars/sec typewriter can reveal them.
808+
// For local models this resolves instantly (typewriter always caught up in real-time).
809+
// Only runs when a buffer is present and the generation epoch is still valid.
810+
if (result.success && streamBufferRef.current.length > 0 && streamEpochRef.current === activeEpochRef.current) {
811+
await waitForTypewriterDone();
812+
}
813+
784814
// BUG-026: If model is unavailable, clear the queue — retrying queued messages
785815
// is pointless until the user loads a model, and draining them causes a stampede.
786816
if (!result.success && /model not loaded|no model loaded/i.test(result.error || '')) {
@@ -878,6 +908,8 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
878908
setStreamingText('');
879909
setThinkingSegments([]);
880910
setCompletedStreamingTools([]);
911+
generatingToolCallsRef.current = [];
912+
setGeneratingToolCalls([]);
881913
executingToolsRef.current = [];
882914
setAgenticProgress(null);
883915
setAgenticPhases([]);
@@ -2448,6 +2480,44 @@ ${e.message}`,
24482480
</div>
24492481
</div>
24502482
)}
2483+
{generatingToolCalls.length > 0 && (
2484+
<div className="mt-2">
2485+
<ToolCallGroup count={generatingToolCalls.length}>
2486+
{generatingToolCalls.map((tc) => {
2487+
// Extract meaningful detail from partial params text as it streams
2488+
let partialDetail = '';
2489+
try {
2490+
const fpMatch = tc.paramsText.match(/"filePath"\s*:\s*"([^"]+)"/);
2491+
const urlMatch = tc.paramsText.match(/"url"\s*:\s*"([^"]+)"/);
2492+
const qMatch = tc.paramsText.match(/"query"\s*:\s*"([^"]+)"/);
2493+
if (fpMatch) {
2494+
const fp = fpMatch[1];
2495+
partialDetail = fp.includes('/') ? fp.split('/').pop() || fp : fp.includes('\\') ? fp.split('\\').pop() || fp : fp;
2496+
} else if (urlMatch) {
2497+
try { partialDetail = new URL(urlMatch[1]).hostname; } catch { partialDetail = urlMatch[1].substring(0, 30); }
2498+
} else if (qMatch) {
2499+
partialDetail = qMatch[1].substring(0, 25) + (qMatch[1].length > 25 ? '\u2026' : '');
2500+
}
2501+
} catch {}
2502+
const genLabel = partialDetail ? `${tc.functionName}: ${partialDetail}` : tc.functionName;
2503+
const displayText = tc.paramsText.length > 1500 ? tc.paramsText.substring(0, 1500) + '\n\u2026[truncated]' : tc.paramsText;
2504+
return (
2505+
<CollapsibleToolBlock key={`gen-${tc.callIndex}`} label={genLabel} icon="\u29d7">
2506+
<div>
2507+
<div className="flex items-center gap-2 mb-2">
2508+
<Loader2 size={12} className="animate-spin text-[#007acc]" />
2509+
<span className="text-[11px] text-[#858585]">Generating tool call\u2026</span>
2510+
</div>
2511+
{tc.paramsText && (
2512+
<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>
2513+
)}
2514+
</div>
2515+
</CollapsibleToolBlock>
2516+
);
2517+
})}
2518+
</ToolCallGroup>
2519+
</div>
2520+
)}
24512521
{(completedStreamingTools.length > 0 || executingTools.length > 0) && (
24522522
<div className="mt-2">
24532523
<ToolCallGroup count={completedStreamingTools.length + executingTools.length}>

0 commit comments

Comments
 (0)