misc fixes + (empty message) improvements

This commit is contained in:
Andrew Pareles 2025-05-10 18:06:58 -07:00
parent 8b45aea8a8
commit 7748dc4f2d
5 changed files with 65 additions and 47 deletions

View file

@ -12,7 +12,7 @@ import { URI } from '../../../../base/common/uri.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { chat_userMessageContent, ToolName, } from '../common/prompt/prompts.js';
import { getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { AnthropicReasoning, getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
@ -540,7 +540,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const { name, id, rawParams } = lastMsg
const errorMessage = this.errMsgs.rejected
const errorMessage = this.toolErrMsgs.rejected
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams })
this._setStreamState(threadId, undefined)
}
@ -557,7 +557,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
// add tool that's running
else if (this.streamState[threadId]?.isRunning === 'tool') {
const { toolName, toolParams, id, content, rawParams } = this.streamState[threadId].toolInfo
const { toolName, toolParams, id, content: content_, rawParams } = this.streamState[threadId].toolInfo
const content = content_ || this.toolErrMsgs.interrupted
this._updateLatestTool(threadId, { role: 'tool', name: toolName, params: toolParams, id, content, rawParams, type: 'rejected', result: null })
}
// reject the tool for the user if relevant
@ -581,8 +582,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
private readonly errMsgs = {
private readonly toolErrMsgs = {
rejected: 'Tool call was rejected by the user.',
interrupted: 'Tool call was interrupted by the user.',
errWhenStringifying: (error: any) => `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}`
}
@ -671,7 +673,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
try {
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
} catch (error) {
const errorMessage = this.errMsgs.errWhenStringifying(error)
const errorMessage = this.toolErrMsgs.errWhenStringifying(error)
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
return {}
}
@ -749,8 +751,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
shouldRetryLLM = false
nAttempts += 1
let resMessageIsDonePromise: (res: { type: 'llmDone', toolCall?: RawToolCallObj } | { type: 'llmError', error?: { message: string; fullError: Error | null; } } | { type: 'llmAborted' }) => void // resolves when user approves this tool use (or if tool doesn't require approval)
const messageIsDonePromise = new Promise<{ type: 'llmDone', toolCall?: RawToolCallObj } | { type: 'llmError', error?: { message: string; fullError: Error | null; } } | { type: 'llmAborted' }>((res, rej) => { resMessageIsDonePromise = res })
type ResTypes =
| { type: 'llmDone', toolCall?: RawToolCallObj, info: { fullText: string, fullReasoning: string, anthropicReasoning: AnthropicReasoning[] | null } }
| { type: 'llmError', error?: { message: string; fullError: Error | null; } }
| { type: 'llmAborted' }
let resMessageIsDonePromise: (res: ResTypes) => void // resolves when user approves this tool use (or if tool doesn't require approval)
const messageIsDonePromise = new Promise<ResTypes>((res, rej) => { resMessageIsDonePromise = res })
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
@ -765,9 +772,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._setStreamState(threadId, { isRunning: 'LLM', llmInfo: { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall ?? null }, interrupt: Promise.resolve(() => { if (llmCancelToken) this._llmMessageService.abort(llmCancelToken) }) })
},
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning })
resMessageIsDonePromise({ type: 'llmDone', toolCall }) // resolve with tool calls
resMessageIsDonePromise({ type: 'llmDone', toolCall, info: { fullText, fullReasoning, anthropicReasoning } }) // resolve with tool calls
},
onError: async (error) => {
resMessageIsDonePromise({ type: 'llmError', error: error })
@ -826,11 +831,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
}
// llm res success
const { toolCall } = llmRes
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative, for clarity
const { toolCall, info } = llmRes
this._addMessageToThread(threadId, { role: 'assistant', displayContent: info.fullText, reasoning: info.fullReasoning, anthropicReasoning: info.anthropicReasoning })
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative for clarity
// call tool if there is one
if (toolCall) {

View file

@ -17,6 +17,7 @@ import { IVoidModelService } from '../common/voidModelService.js';
import { URI } from '../../../../base/common/uri.js';
import { EndOfLinePreference } from '../../../../editor/common/model.js';
export const EMPTY_MESSAGE = '(empty message)'
@ -36,7 +37,6 @@ type SimpleLLMMessage = {
}
const EMPTY_MESSAGE = '(empty message)'
const CHARS_PER_TOKEN = 4 // assume abysmal chars per token
const TRIM_TO_LEN = 120
@ -405,14 +405,24 @@ const prepareOpenAIOrAnthropicMessages = ({
// ================ no empty message ================
for (const currMsg of llmMessages) {
for (let i = 0; i < llmMessages.length; i += 1) {
const currMsg: AnthropicOrOpenAILLMMessage = llmMessages[i]
const nextMsg: AnthropicOrOpenAILLMMessage | undefined = llmMessages[i + 1]
if (currMsg.role === 'tool') continue
// if content is a string, replace string with empty msg
if (typeof currMsg.content === 'string')
if (typeof currMsg.content === 'string') {
currMsg.content = currMsg.content || EMPTY_MESSAGE
}
else {
// if content is an array, replace any empty text entries with empty msg, and make sure there's at least 1 entry
// allowed to be empty if has a tool in it or following it
if (currMsg.content.find(c => c.type === 'tool_result' || c.type === 'tool_use')) {
continue
}
if (nextMsg?.role === 'tool') continue
// replace any empty text entries with empty msg, and make sure there's at least 1 entry
for (const c of currMsg.content) {
if (c.type === 'text') c.text = c.text || EMPTY_MESSAGE
}

View file

@ -311,7 +311,6 @@ export const ApplyButtonsHTML = ({
const currStreamState = currStreamStateRef.current
console.log('currStreamState...', currStreamState)
if (currStreamState === 'streaming') {
return <IconShell1

View file

@ -120,31 +120,34 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
const [didComputeCodespanLink, setDidComputeCodespanLink] = useState<boolean>(false)
let link: CodespanLocationLink | undefined = undefined
if (!rawText.endsWith('`')) return null
// get link from cache
link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId })
if (link === undefined) {
// if no link, generate link and add to cache
chatThreadService.generateCodespanLink({ codespanStr: text, threadId })
.then(link => {
chatThreadService.addCodespanLink({ newLinkText: text, newLinkLocation: link, messageIdx, threadId })
setDidComputeCodespanLink(true) // rerender
})
}
// If it's a file path, shorten it and add tooltip (whether or not it's a link)
let displayText = link?.displayText || text
let tooltip: string | undefined = undefined
if (isValidUri(displayText)) {
tooltip = getRelative(URI.file(displayText), accessor) // Full path as tooltip
displayText = getBasename(displayText)
let displayText = text
if (rawText.endsWith('`')) {
// get link from cache
link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId })
if (link === undefined) {
// if no link, generate link and add to cache
chatThreadService.generateCodespanLink({ codespanStr: text, threadId })
.then(link => {
chatThreadService.addCodespanLink({ newLinkText: text, newLinkLocation: link, messageIdx, threadId })
setDidComputeCodespanLink(true) // rerender
})
}
if (link?.displayText) {
displayText = link.displayText
}
if (isValidUri(displayText)) {
tooltip = getRelative(URI.file(displayText), accessor) // Full path as tooltip
displayText = getBasename(displayText)
}
}
const onClick = () => {
if (!link) return;
// Use the updated voidOpenFileFn to open the file and handle selection

View file

@ -500,7 +500,7 @@ export const getRelative = (uri: URI, accessor: ReturnType<typeof useAccessor>)
let path: string
const isInside = workspaceContextService.isInsideWorkspace(uri)
if (isInside) {
const f = workspaceContextService.getWorkspace().folders.find(f => uri.fsPath.startsWith(f.uri.fsPath))
const f = workspaceContextService.getWorkspace().folders.find(f => uri.fsPath?.startsWith(f.uri.fsPath))
if (f) { path = uri.fsPath.replace(f.uri.fsPath, '') }
else { path = uri.fsPath }
}
@ -1651,8 +1651,8 @@ const LintErrorChildren = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => {
}
const BottomChildren = ({ children, title }: { children: React.ReactNode, title: string }) => {
if (!children) return null;
const [isOpen, setIsOpen] = useState(false);
if (!children) return null;
return (
<div className="w-full px-2 mt-0.5">
<div
@ -1905,7 +1905,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
</SmallProseWrapper>
</ToolChildrenWrapper>
}
else {
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
@ -1960,7 +1960,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
}
</ToolChildrenWrapper>
}
else {
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
@ -2009,7 +2009,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
</ToolChildrenWrapper>
}
else {
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
@ -2064,7 +2064,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
</ToolChildrenWrapper>
}
else {
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>