diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts
index 966749e9..d5d8fe80 100644
--- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts
+++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts
@@ -68,15 +68,17 @@ const toLLMChatMessages = (chatMessages: ChatMessage[]): LLMChatMessage[] => {
for (const c of chatMessages) {
if (c.role === 'assistant')
- llmChatMessages.push({ role: c.role, content: c.content, anthropicReasoning: c.anthropicReasoning })
+ llmChatMessages.push({ role: c.role, content: c.displayContent, anthropicReasoning: c.anthropicReasoning })
// merge all tool/user messages into one big user message
else if (c.role === 'user' || c.role === 'tool') {
- if (llmChatMessages.length === 0 || llmChatMessages[llmChatMessages.length - 1].role !== 'user') {
+ if (c.role === 'tool')
+ c.content = `TOOL_RESULT (${c.name}):\n${c.content}`
+
+ if (llmChatMessages.length === 0 || llmChatMessages[llmChatMessages.length - 1].role !== 'user')
llmChatMessages.push({ role: 'user', content: c.content })
- }
- else {
+ else
llmChatMessages[llmChatMessages.length - 1].content += '\n\n' + c.content
- }
+
}
else if (c.role === 'interrupted_streaming_tool') { // pass
}
@@ -146,7 +148,7 @@ export type ThreadStreamState = {
// streaming related - when streaming message
streamingToken?: string;
- messageSoFar?: string;
+ displayContentSoFar?: string;
reasoningSoFar?: string;
toolCallSoFar?: RawToolCallObj;
}
@@ -519,7 +521,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const isRunning = this.streamState[threadId]?.isRunning
if (isRunning === 'LLM') {
// abort the stream first so it doesn't change any state
- const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
+ const displayContentSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
console.log('toolInProgress', toolCallSoFar)
@@ -527,7 +529,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const llmCancelToken = this.streamState[threadId]?.streamingToken
if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) }
- this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
+ this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
if (toolCallSoFar) {
this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
@@ -716,19 +718,19 @@ class ChatThreadService extends Disposable implements IChatThreadService {
modelSelectionOptions,
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
onText: ({ fullText, fullReasoning, toolCall }) => {
- this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge')
+ this._setStreamState(threadId, { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge')
},
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
- this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning })
- this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
+ this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning })
+ this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
console.log('tool call!!', toolCall)
resMessageIsDonePromise(toolCall) // resolve with tool calls
},
onError: (error) => {
- const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
+ const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
// add assistant's message to chat history, and clear selection
- this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
+ this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
this._setStreamState(threadId, { error }, 'set')
resMessageIsDonePromise()
},
diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx
index 28c59250..be3e342a 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx
@@ -1075,7 +1075,7 @@ const AssistantMessageComponent = ({ chatMessage, isCheckpointGhost, isCommitted
const reasoningStr = chatMessage.reasoning?.trim() || null
const hasReasoning = !!reasoningStr
- const isDoneReasoning = !!chatMessage.content
+ const isDoneReasoning = !!chatMessage.displayContent
const thread = chatThreadsService.getCurrentThread()
@@ -1084,7 +1084,7 @@ const AssistantMessageComponent = ({ chatMessage, isCheckpointGhost, isCommitted
messageIdx: messageIdx,
}
- const isEmpty = !chatMessage.content && !chatMessage.reasoning
+ const isEmpty = !chatMessage.displayContent && !chatMessage.reasoning
if (isEmpty) return null
return <>
@@ -1108,7 +1108,7 @@ const AssistantMessageComponent = ({ chatMessage, isCheckpointGhost, isCommitted
{
const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
const isRunning = currThreadStreamState?.isRunning
const latestError = currThreadStreamState?.error
- const messageSoFar = currThreadStreamState?.messageSoFar
+ const displayContentSoFar = currThreadStreamState?.displayContentSoFar
const reasoningSoFar = currThreadStreamState?.reasoningSoFar
const toolCallSoFar = currThreadStreamState?.toolCallSoFar
@@ -2082,13 +2082,13 @@ export const SidebarChat = () => {
}, [previousMessages, isRunning, threadId])
const streamingChatIdx = previousMessagesHTML.length
- const currStreamingMessageHTML = reasoningSoFar || messageSoFar || isRunning ?
+ const currStreamingMessageHTML = reasoningSoFar || displayContentSoFar || isRunning ?
{
w-full h-full
overflow-x-hidden
overflow-y-auto
- ${previousMessagesHTML.length === 0 && !messageSoFar ? 'hidden' : ''}
+ ${previousMessagesHTML.length === 0 && !displayContentSoFar ? 'hidden' : ''}
`}
>
{/* previous messages */}
diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts
index 9a358c55..229eca8f 100644
--- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts
+++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts
@@ -56,7 +56,7 @@ export type ChatMessage =
}
} | {
role: 'assistant';
- content: string; // content received from LLM - allowed to be '', will be replaced with (empty)
+ displayContent: string; // content received from LLM - allowed to be '', will be replaced with (empty)
reasoning: string; // reasoning from the LLM, used for step-by-step thinking
anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning
diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts
index dc6b66c9..829369b5 100644
--- a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts
+++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts
@@ -29,6 +29,7 @@ export const extractReasoningWrapper = (
}
const newOnText: OnText = ({ fullText: fullText_, ...p }) => {
+
// until found the first think tag, keep adding to fullText
if (!foundTag1) {
const endsWithTag1 = endsWithAnyPrefixOf(fullText_, thinkTags[0])
@@ -293,6 +294,9 @@ export const extractToolsWrapper = (
const newOnFinalMessage: OnFinalMessage = (params) => {
// treat like just got text before calling onFinalMessage (or else we sometimes miss the final chunk that's new to finalMessage)
+ console.log('final message!!!', trueFullText)
+ console.log('----- returning ----\n', fullText)
+ console.log('----- tools ----\n', JSON.stringify(currentToolCalls, null, 2))
newOnText({ ...params })
console.log('final message!!!', trueFullText)
@@ -300,7 +304,7 @@ export const extractToolsWrapper = (
console.log('----- tools ----\n', JSON.stringify(currentToolCalls, null, 2))
fullText = fullText.trimEnd()
- const toolCall = currentToolCalls[0]
+ const toolCall = currentToolCalls.length > 0 ? currentToolCalls[0] : undefined
if (toolCall) {
// trim off all whitespace at and before first \n and after last \n for each param
for (const paramName in toolCall.rawParams) {
@@ -309,7 +313,9 @@ export const extractToolsWrapper = (
toolCall.rawParams[paramName] = trimBeforeAndAfterNewLines(orig)
}
}
- onFinalMessage({ ...params, fullText, toolCall: currentToolCalls.length > 0 ? currentToolCalls[0] : undefined })
+ console.log('----- toolCall ----\n', JSON.stringify(toolCall, null, 2))
+
+ onFinalMessage({ ...params, fullText, toolCall: toolCall })
}
return { newOnText, newOnFinalMessage };
}
diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sax.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sax.ts
index e27e0753..0d65e943 100644
--- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sax.ts
+++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sax.ts
@@ -54,30 +54,38 @@ export function createSaxParser(options: SaxParserOptions = {}): SaxParser {
// Set the current position to the end of the processed chunk.
this.position = globalPos - 1;
- let cursor: number = 0;
+ let cursor = 0;
+ // Flag to indicate if an incomplete tag was found.
+ let incompleteTagFound = false;
+ // This will mark the position in the buffer where the incomplete tag starts.
+ let incompleteStart = 0;
+
while (cursor < buffer.length) {
// Look for the next opening '<' character.
const ltIndex = buffer.indexOf('<', cursor);
if (ltIndex === -1) {
- // No more tags found. Emit any remaining text as a text node.
+ // No more tags found in the current buffer.
if (cursor < buffer.length && this.ontext) {
this.ontext(buffer.substring(cursor));
}
- // Clear the buffer since all content is processed.
+ // All content is processed.
buffer = '';
+ cursor = buffer.length;
break;
}
- // Emit any text that appears before the tag.
+ // Emit any text between the current cursor and the opening tag.
if (ltIndex > cursor && this.ontext) {
this.ontext(buffer.substring(cursor, ltIndex));
}
- // Look for the closing '>' character.
+ // Look for the closing '>' character starting from the found '<'.
const gtIndex = buffer.indexOf('>', ltIndex);
if (gtIndex === -1) {
- // Incomplete tag detected—retain the remaining content in the buffer.
- buffer = buffer.substring(ltIndex);
+ // Incomplete tag detected.
+ incompleteTagFound = true;
+ // Save the starting point of the incomplete tag.
+ incompleteStart = ltIndex;
break;
}
@@ -98,24 +106,27 @@ export function createSaxParser(options: SaxParserOptions = {}): SaxParser {
this.onclosetag(tagName);
}
} else {
- // Check for self-closing tags (ending with '/').
+ // Handle self-closing tags (ending with '/').
let selfClosing = false;
if (tagContent[tagContent.length - 1] === '/') {
selfClosing = true;
tagContent = tagContent.slice(0, -1).trim();
}
- // Determine the tag name (first word before whitespace).
+ // Determine the tag name (first word before any whitespace).
const spaceIndex = tagContent.indexOf(' ');
- let tagName = (spaceIndex !== -1 ? tagContent.substring(0, spaceIndex) : tagContent).trim();
+ let tagName =
+ spaceIndex !== -1
+ ? tagContent.substring(0, spaceIndex).trim()
+ : tagContent;
if (options.lowercase && tagName) {
tagName = tagName.toLowerCase();
}
- // Call onopentag with a minimal node object.
+ // Emit an open tag event.
if (this.onopentag) {
const node: SaxNode = { name: tagName, attributes: {} };
this.onopentag(node);
}
- // If the tag is self-closing, immediately emit the closing tag event.
+ // If it’s a self-closing tag, immediately emit a close tag event.
if (selfClosing && this.onclosetag) {
this.onclosetag(tagName);
}
@@ -124,10 +135,15 @@ export function createSaxParser(options: SaxParserOptions = {}): SaxParser {
cursor = gtIndex + 1;
}
- // Remove any content already processed from the buffer.
- buffer = buffer.slice(cursor);
- }
-
+ // If an incomplete tag was detected, preserve it.
+ if (incompleteTagFound) {
+ // Keep the incomplete portion starting from the '<'
+ buffer = buffer.substring(incompleteStart);
+ } else {
+ // Otherwise, remove all processed content.
+ buffer = buffer.substring(cursor);
+ }
+ },
};
return parser;