add visual feedback for tool that's being loaded (eg edit tool)

This commit is contained in:
Andrew Pareles 2025-03-21 05:50:31 -07:00
parent eed71d9582
commit 6a6cb56d87
6 changed files with 109 additions and 57 deletions

View file

@ -117,6 +117,8 @@ export type ThreadStreamState = {
streamingToken?: string;
messageSoFar?: string;
reasoningSoFar?: string;
toolNameSoFar?: string;
toolParamsSoFar?: string;
}
}
@ -874,11 +876,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
modelSelection,
modelSelectionOptions,
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
onText: ({ fullText, fullReasoning }) => { this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning }, 'merge') },
onText: ({ fullText, fullReasoning, fullToolName, fullToolParams }) => {
this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning, toolNameSoFar: fullToolName, toolParamsSoFar: fullToolParams }, 'merge')
},
onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => {
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning })
// added to history and no longer streaming this, so clear messages so far and streamingToken (but do not stop isRunning)
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, }, 'merge')
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolNameSoFar: undefined, toolParamsSoFar: undefined }, 'merge')
// resolve with tool calls
resMessageIsDonePromise(toolCalls)
},

View file

@ -1695,14 +1695,17 @@ class EditCodeService extends Disposable implements IEditCodeService {
// if this is the first time we're seeing this block, add it as a diffarea so we can start streaming in it
if (!(blockNum in addedTrackingZoneOfBlockNum)) {
const originalBounds = findTextInCode(block.orig, originalFileCode)
// if error
if (typeof originalBounds === 'string') {
console.log('Error finding text in code:')
console.log('--------------Error finding text in code:')
console.log('originalFileCode', { originalFileCode })
console.log('fullText', { fullText })
console.log('error:', originalBounds)
console.log('block.orig:', block.orig)
console.log('---------')
const content = errContentOfInvalidStr(originalBounds, block.orig, blockNum, blocks)
messages.push(
{ role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output
@ -1710,10 +1713,14 @@ class EditCodeService extends Disposable implements IEditCodeService {
)
// REVERT ALL BLOCKS
currStreamingBlockNum = 0
latestStreamLocationMutable = null
shouldUpdateOrigStreamStyle = true
oldBlocks = []
for (const trackingZone of addedTrackingZoneOfBlockNum)
this._deleteTrackingZone(trackingZone)
addedTrackingZoneOfBlockNum.splice(0, Infinity)
this._writeURIText(uri, originalFileCode, 'wholeFileRange', { shouldRealignDiffAreas: true })
// abort and resolve
@ -1729,7 +1736,14 @@ class EditCodeService extends Disposable implements IEditCodeService {
return
}
console.log('---------adding-------')
console.log('CURRENT TEXT!!!', { current: model?.getValue() })
console.log('block', deepClone(block))
console.log('origBounds', originalBounds)
const [startLine, endLine] = convertOriginalRangeToFinalRange(originalBounds)
console.log('start end', startLine, endLine)
// otherwise if no error, add the position as a diffarea
const adding: Omit<TrackingZone<SearchReplaceDiffAreaMetadata>, 'diffareaid'> = {
@ -1802,9 +1816,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
addedTrackingZoneOfBlockNum.sort((a, b) => a.metadata.originalBounds[0] - b.metadata.originalBounds[0])
const { model } = this._voidModelService.getModel(uri)
console.log('CURRENT!!!', { current: model?.getValue() })
console.log('ADDED', addedTrackingZoneOfBlockNum)
console.log('BLOX', blocks)
console.log('CURRENT TEXT!!!', { current: model?.getValue() })
console.log('addedTrackingZoneOfBlockNum', addedTrackingZoneOfBlockNum)
console.log('blocks', deepClone(blocks))
for (let blockNum = addedTrackingZoneOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) {
const { originalBounds } = addedTrackingZoneOfBlockNum[blockNum].metadata

View file

@ -27,7 +27,7 @@ import { WarningBox } from '../void-settings-tsx/WarningBox.js';
import { getModelCapabilities, getIsResoningEnabledState } from '../../../../common/modelCapabilities.js';
import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, X } from 'lucide-react';
import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js';
import { ToolCallParams, ToolName, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js';
import { ToolCallParams, ToolName, toolNames, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js';
import { JumpToFileButton, useApplyButtonHTML } from '../markdown/ApplyBlockHoverButtons.js';
import { IsRunningType } from '../../../chatThreadService.js';
@ -1040,7 +1040,7 @@ const ProseWrapper = ({ children }: { children: React.ReactNode }) => {
{children}
</div>
}
const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning }: { chatMessage: ChatMessage & { role: 'assistant' }, messageIdx: number, isCommitted: boolean, isLast: boolean, chatIsRunning: IsRunningType }) => {
const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning, isToolBeingWritten }: { chatMessage: ChatMessage & { role: 'assistant' }, messageIdx: number, isCommitted: boolean, isLast: boolean, chatIsRunning: IsRunningType, isToolBeingWritten: boolean }) => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
@ -1058,7 +1058,8 @@ const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLas
}
const isEmpty = !chatMessage.content && !chatMessage.reasoning
const isLastAndLoading = !isCommitted && isLast && (chatIsRunning === 'message' || chatIsRunning === 'awaiting_user')
const isLoading = !isCommitted && !isToolBeingWritten && (chatIsRunning === 'message' || chatIsRunning === 'awaiting_user')
const isLastAndLoading = isLast && isLoading
if (isEmpty && !isLastAndLoading) return null
return <>
@ -1083,7 +1084,7 @@ const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLas
isLinkDetectionEnabled={true}
/>
{/* loading indicator */}
{!isCommitted && <IconLoading className='opacity-50 text-sm' />}
{isLoading && <IconLoading className='opacity-50 text-sm' />}
</ProseWrapper>
</>
@ -1117,19 +1118,19 @@ const loadingTitleWrapper = (item: React.ReactNode) => {
</span>
}
const folderFileStr = (isFolder: boolean) => isFolder ? 'folder' : 'file'
const toolNameToTitle = {
const titleOfToolName = {
'read_file': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') },
'list_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') },
'pathname_search': { done: 'Searched by file name', proposed: 'Search by file name', running: loadingTitleWrapper('Searching by file name') },
'text_search': { done: 'Searched', proposed: 'Search text', running: loadingTitleWrapper('Searching') },
'create_uri': {
done: (isFolder: boolean) => `Created ${folderFileStr(isFolder)}`,
proposed: (isFolder: boolean) => `Create ${folderFileStr(isFolder)}`,
proposed: (isFolder: boolean | null) => isFolder === null ? 'Create URI' : `Create ${folderFileStr(isFolder)}`,
running: (isFolder: boolean) => loadingTitleWrapper(`Creating ${folderFileStr(isFolder)}`)
},
'delete_uri': {
done: (isFolder: boolean) => `Deleted ${folderFileStr(isFolder)}`,
proposed: (isFolder: boolean) => `Delete ${folderFileStr(isFolder)}`,
proposed: (isFolder: boolean | null) => isFolder === null ? 'Delete URI' : `Delete ${folderFileStr(isFolder)}`,
running: (isFolder: boolean) => loadingTitleWrapper(`Deleting ${folderFileStr(isFolder)}`)
},
'edit': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') },
@ -1259,7 +1260,7 @@ export const ToolChildrenWrapper = ({ children, className }: { children: React.R
</div>
</div>
}
export const ErrorChildren = ({ children }: { children: React.ReactNode }) => {
export const CodeChildren = ({ children }: { children: React.ReactNode }) => {
return <div className='bg-void-bg-3 p-1 rounded-sm font-mono overflow-auto text-sm'>
<div className='!select-text cursor-auto'>
{children}
@ -1315,7 +1316,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed
const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed
const { uri } = toolMessage.result.params ?? {}
const desc1 = uri ? getBasename(uri.fsPath) : '';
const icon = null
@ -1334,9 +1335,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
const { value, params } = toolMessage.result
if (params) componentParams.desc2 = <JumpToFileButton uri={params.uri} />
componentParams.children = <ToolChildrenWrapper>
<ErrorChildren>
<CodeChildren>
{value}
</ErrorChildren>
</CodeChildren>
</ToolChildrenWrapper>
}
@ -1349,7 +1350,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const explorerService = accessor.get('IExplorerService')
const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed
const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
const icon = null
@ -1381,9 +1382,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
else {
const { value, params } = toolMessage.result
componentParams.children = <ToolChildrenWrapper>
<ErrorChildren>
<CodeChildren>
{value}
</ErrorChildren>
</CodeChildren>
</ToolChildrenWrapper>
}
@ -1396,7 +1397,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const isError = toolMessage.result.type === 'error'
const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed
const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
const icon = null
@ -1424,9 +1425,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
else {
const { value, params } = toolMessage.result
componentParams.children = <ToolChildrenWrapper>
<ErrorChildren>
<CodeChildren>
{value}
</ErrorChildren>
</CodeChildren>
</ToolChildrenWrapper>
}
@ -1439,7 +1440,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const isError = toolMessage.result.type === 'error'
const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed
const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
const icon = null
@ -1467,9 +1468,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
else {
const { value, params } = toolMessage.result
componentParams.children = <ToolChildrenWrapper>
<ErrorChildren>
<CodeChildren>
{value}
</ErrorChildren>
</CodeChildren>
</ToolChildrenWrapper>
}
return <ToolHeaderWrapper {...componentParams} />
@ -1485,7 +1486,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
const explorerService = accessor.get('IExplorerService')
const isError = false
const isFolder = toolRequest.params.isFolder
const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed(isFolder) : toolNameToTitle[toolRequest.name].running(isFolder)
const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed(isFolder) : titleOfToolName[toolRequest.name].running(isFolder)
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
const icon = null
@ -1499,7 +1500,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
const isError = toolMessage.result.type === 'error'
const isRejected = toolMessage.result.type === 'rejected'
const isFolder = toolMessage.result.params?.isFolder ?? false
const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done(isFolder) : toolNameToTitle[toolMessage.name].proposed(isFolder)
const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done(isFolder) : titleOfToolName[toolMessage.name].proposed(isFolder)
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
const icon = null
@ -1517,9 +1518,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
const { params, value } = toolMessage.result
if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } }
componentParams.children = componentParams.children = <ToolChildrenWrapper>
<ErrorChildren>
<CodeChildren>
{value}
</ErrorChildren>
</CodeChildren>
</ToolChildrenWrapper>
}
@ -1532,7 +1533,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
const commandService = accessor.get('ICommandService')
const isError = false
const isFolder = toolRequest.params.isFolder
const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed(isFolder) : toolNameToTitle[toolRequest.name].running(isFolder)
const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed(isFolder) : titleOfToolName[toolRequest.name].running(isFolder)
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
const icon = null
@ -1549,7 +1550,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
const isFolder = toolMessage.result.params?.isFolder ?? false
const isError = toolMessage.result.type === 'error'
const isRejected = toolMessage.result.type === 'rejected'
const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done(isFolder) : toolNameToTitle[toolMessage.name].proposed(isFolder)
const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done(isFolder) : titleOfToolName[toolMessage.name].proposed(isFolder)
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
const icon = null
@ -1567,9 +1568,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
const { params, value } = toolMessage.result
if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } }
componentParams.children = componentParams.children = <ToolChildrenWrapper>
<ErrorChildren>
<CodeChildren>
{value}
</ErrorChildren>
</CodeChildren>
</ToolChildrenWrapper>
}
@ -1580,7 +1581,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
requestWrapper: ({ toolRequest, messageIdx, toolRequestState, threadId }) => {
const accessor = useAccessor()
const isError = false
const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed : toolNameToTitle[toolRequest.name].running
const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed : titleOfToolName[toolRequest.name].running
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
const icon = null
@ -1602,7 +1603,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
const accessor = useAccessor()
const isError = toolMessage.result.type === 'error'
const isRejected = toolMessage.result.type === 'rejected'
const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed
const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
const icon = null
@ -1641,9 +1642,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
if (params) {
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
{/* error */}
<ErrorChildren>
<CodeChildren>
{value}
</ErrorChildren>
</CodeChildren>
{/* content */}
<EditToolChildren
@ -1653,9 +1654,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
</ToolChildrenWrapper>
}
else {
componentParams.children = <ErrorChildren>
componentParams.children = <CodeChildren>
{value}
</ErrorChildren>
</CodeChildren>
}
}
}
@ -1669,7 +1670,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
const commandService = accessor.get('ICommandService')
const terminalToolsService = accessor.get('ITerminalToolService')
const isError = false
const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed : toolNameToTitle[toolRequest.name].running
const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed : titleOfToolName[toolRequest.name].running
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
const icon = null
@ -1688,7 +1689,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
const commandService = accessor.get('ICommandService')
const terminalToolsService = accessor.get('ITerminalToolService')
const isError = toolMessage.result.type === 'error'
const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed
const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
const icon = null
@ -1753,9 +1754,10 @@ type ChatBubbleProps = {
isLast: boolean, // includes the streaming message (if streaming, isLast is false except for the streaming message)
chatIsRunning: IsRunningType,
threadId: string,
isToolBeingWritten: boolean,
}
const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning, threadId }: ChatBubbleProps) => {
const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning, threadId, isToolBeingWritten }: ChatBubbleProps) => {
const role = chatMessage.role
if (role === 'user') {
@ -1772,6 +1774,7 @@ const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunnin
isCommitted={isCommitted}
chatIsRunning={chatIsRunning}
isLast={isLast}
isToolBeingWritten={isToolBeingWritten}
/>
}
else if (role === 'tool_request') {
@ -1838,6 +1841,10 @@ export const SidebarChat = () => {
const messageSoFar = currThreadStreamState?.messageSoFar
const reasoningSoFar = currThreadStreamState?.reasoningSoFar
const toolNameSoFar = currThreadStreamState?.toolNameSoFar
const toolParamsSoFar = currThreadStreamState?.toolParamsSoFar
const toolIsLoading = !!toolNameSoFar && toolNameSoFar === 'edit' // show loading for slow tools (right now just edit)
// ----- SIDEBAR CHAT state (local) -----
// state of current message
@ -1902,6 +1909,7 @@ export const SidebarChat = () => {
chatIsRunning={isRunning}
isLast={isLast}
threadId={threadId}
isToolBeingWritten={toolIsLoading}
/>
})
}, [previousMessages, isRunning, currentThread, numMessages])
@ -1921,9 +1929,17 @@ export const SidebarChat = () => {
chatIsRunning={isRunning}
isLast={true}
threadId={threadId}
isToolBeingWritten={toolIsLoading}
/> : null
const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML]
const proposed = toolNameSoFar && toolNames.includes(toolNameSoFar as ToolName) ? titleOfToolName[toolNameSoFar as ToolName]?.proposed : toolNameSoFar
const toolTitle = typeof proposed === 'function' ? proposed(null) : proposed
const currStreamingToolHTML = toolIsLoading ?
<ToolHeaderWrapper key={getChatBubbleId(currentThread.id, streamingChatIdx + 1)} title={toolTitle} desc1={<IconLoading />} />
: null
const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML, currStreamingToolHTML]
const threadSelector = <div
className={`w-full ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow ring-inset z-10`}

View file

@ -264,7 +264,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string
onText_(params)
}
const newOnText: OnText = ({ fullText: fullText_ }) => {
const newOnText: OnText = ({ fullText: fullText_, ...p }) => {
// until found the first think tag, keep adding to fullText
if (!foundTag1) {
const endsWithTag1 = endsWithAnyPrefixOf(fullText_, thinkTags[0])
@ -282,7 +282,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string
fullTextSoFar += fullText_.substring(0, tag1Index)
// Update latestAddIdx to after the first tag
latestAddIdx = tag1Index + thinkTags[0].length
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
@ -290,7 +290,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string
// add the text to fullText
fullTextSoFar = fullText_
latestAddIdx = fullText_.length
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
@ -314,7 +314,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string
fullReasoningSoFar += fullText_.substring(latestAddIdx, tag2Index)
// Update latestAddIdx to after the second tag
latestAddIdx = tag2Index + thinkTags[1].length
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
@ -327,7 +327,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string
latestAddIdx = fullText_.length
}
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
@ -340,7 +340,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string
latestAddIdx = fullText_.length
}
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
}
return newOnText

View file

@ -54,7 +54,7 @@ export type ToolCallType = {
export type AnthropicReasoning = ({ type: 'thinking'; thinking: any; signature: string; } | { type: 'redacted_thinking', data: any })
export type OnText = (p: { fullText: string; fullReasoning: string }) => void
export type OnText = (p: { fullText: string; fullReasoning: string; fullToolName: string; fullToolParams: string; }) => void
export type OnFinalMessage = (p: { fullText: string; fullReasoning: string; toolCalls?: ToolCallType[]; anthropicReasoning: AnthropicReasoning[] | null }) => void // id is tool_use_id
export type OnError = (p: { message: string; fullError: Error | null }) => void
export type OnAbort = () => void

View file

@ -195,6 +195,10 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
let fullReasoningSoFar = ''
let fullTextSoFar = ''
let fullToolName = ''
let fullToolParams = ''
const toolCallOfIndex: ToolCallOfIndex = {}
openai.chat.completions
.create(options)
@ -209,6 +213,9 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
toolCallOfIndex[index].name += tool.function?.name ?? ''
toolCallOfIndex[index].paramsStr += tool.function?.arguments ?? '';
toolCallOfIndex[index].id += tool.id ?? ''
fullToolName += tool.function?.name ?? ''
fullToolParams += tool.function?.arguments ?? ''
}
// message
const newText = chunk.choices[0]?.delta?.content ?? ''
@ -222,7 +229,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
fullReasoningSoFar += newReasoning
}
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, fullToolName, fullToolParams })
}
// on final
const toolCalls = toolCallsFrom_OpenAICompat(toolCallOfIndex)
@ -351,6 +358,9 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
let fullText = ''
let fullReasoning = ''
let fullToolName = ''
let fullToolParams = ''
// there are no events for tool_use, it comes in at the end
stream.on('streamEvent', e => {
// start block
@ -358,18 +368,22 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
if (e.content_block.type === 'text') {
if (fullText) fullText += '\n\n' // starting a 2nd text block
fullText += e.content_block.text
onText({ fullText, fullReasoning })
onText({ fullText, fullReasoning, fullToolName, fullToolParams })
}
else if (e.content_block.type === 'thinking') {
if (fullReasoning) fullReasoning += '\n\n' // starting a 2nd reasoning block
fullReasoning += e.content_block.thinking
onText({ fullText, fullReasoning })
onText({ fullText, fullReasoning, fullToolName, fullToolParams })
}
else if (e.content_block.type === 'redacted_thinking') {
console.log('delta', e.content_block.type)
if (fullReasoning) fullReasoning += '\n\n' // starting a 2nd reasoning block
fullReasoning += '[redacted_thinking]'
onText({ fullText, fullReasoning })
onText({ fullText, fullReasoning, fullToolName, fullToolParams })
}
else if (e.content_block.type === 'tool_use') {
fullToolName += e.content_block.name ?? '' // anthropic gives us the tool name in the start block
onText({ fullText, fullReasoning, fullToolName, fullToolParams })
}
}
@ -377,11 +391,15 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
else if (e.type === 'content_block_delta') {
if (e.delta.type === 'text_delta') {
fullText += e.delta.text
onText({ fullText, fullReasoning })
onText({ fullText, fullReasoning, fullToolName, fullToolParams })
}
else if (e.delta.type === 'thinking_delta') {
fullReasoning += e.delta.thinking
onText({ fullText, fullReasoning })
onText({ fullText, fullReasoning, fullToolName, fullToolParams })
}
else if (e.delta.type === 'input_json_delta') { // tool use
fullToolParams += e.delta.partial_json ?? '' // anthropic gives us the partial delta (string) here - https://docs.anthropic.com/en/api/messages-streaming
onText({ fullText, fullReasoning, fullToolName, fullToolParams })
}
}
})