diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index b2616ff2..48791b7a 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -345,6 +345,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { else throw new Error(`setStreamState`) } + console.log('changeStreamState', threadId, state) this._onDidChangeStreamState.fire({ threadId }) } @@ -1202,7 +1203,8 @@ We only need to do it for files that were edited since `from`, ie files between let uris: URI[] = [] try { const { result } = await this._toolsService.callTool['search_pathnames_only']({ query: target, includePattern: null, pageNumber: 0 }) - uris = result.uris + const { uris: uris_ } = await result + uris = uris_ } catch (e) { return null } 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 4f5d4529..e4ce0c1a 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 @@ -6,7 +6,7 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useSettingsState, useActiveURI, useCommandBarState } from '../util/services.js'; +import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useSettingsState, useActiveURI, useCommandBarState, useFullChatThreadsStreamState } from '../util/services.js'; import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markdown/ChatMarkdownRender.js'; import { URI } from '../../../../../../../base/common/uri.js'; @@ -2131,7 +2131,13 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const Checkpoint = ({ message, threadId, messageIdx, isCheckpointGhost, threadIsRunning }: { message: CheckpointEntry, threadId: string; messageIdx: number, isCheckpointGhost: boolean, threadIsRunning: boolean }) => { const accessor = useAccessor() const chatThreadService = accessor.get('IChatThreadService') - const [showCheckpointIcon, setShowCheckpointIcon] = React.useState(false); // add icon state + const streamState = useFullChatThreadsStreamState() + + const isRunning = useChatThreadsStreamState(threadId)?.isRunning + const isDisabled = useMemo(() => { + if (isRunning) return true + return !!Object.keys(streamState).find((threadId2) => streamState[threadId2]?.isRunning) + }, [isRunning, streamState]) return
{ if (threadIsRunning) return + if (isDisabled) return chatThreadService.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: messageIdx === (chatThreadService.state.allThreads[threadId]?.messages.length ?? 0) - 1 }) }} + {...isDisabled ? { + 'data-tooltip-id': 'void-tooltip', + 'data-tooltip-content': `Disabled ${isRunning ? 'when running' : 'because another thread is running'}`, + 'data-tooltip-place': 'left', + } : {}} > Checkpoint
@@ -2296,39 +2309,10 @@ const CommandBarInChat = () => { const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') const commandService = accessor.get('ICommandService') - const chatThreadsService = accessor.get('IChatThreadService') const chatThreadsState = useChatThreadsState() const commandBarState = useCommandBarState() const chatThreadsStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId) - const settingsState = useSettingsState() - const convertService = accessor.get('IConvertToLLMMessageService') - - - - const currentThread = chatThreadsService.getCurrentThread() - const chatMode = settingsState.globalSettings.chatMode - const modelSelection = settingsState.modelSelectionOfFeature?.Chat ?? null - - const copyChatButton = { - const { messages } = await convertService.prepareLLMChatMessages({ - chatMessages: currentThread.messages, - chatMode, - modelSelection, - }) - return JSON.stringify(messages, null, 2) - }} - toolTipName={modelSelection === null ? 'Copy As Messages Payload' : `Copy As ${displayInfoOfProviderName(modelSelection.providerName).title} Payload`} - /> - - const copyChatButton2 = { - return JSON.stringify(currentThread.messages, null, 2) - }} - toolTipName={`Copy As Void Chat`} - /> - // ( // { const threadsState = useChatThreadsState() const { allThreads } = threadsState + const streamState = useFullChatThreadsStreamState() + + const runningThreadIds = new Set() + for (const threadId in streamState) { + if (streamState[threadId]?.isRunning) { runningThreadIds.add(threadId) } + } + if (!allThreads) { return
{`Error accessing chat history.`}
; } @@ -183,6 +190,7 @@ export const PastThreadsList = ({ className = '' }: { className?: string }) => { idx={i} hoveredIdx={hoveredIdx} setHoveredIdx={setHoveredIdx} + isRunning={runningThreadIds.has(pastThread.id)} /> ); }) @@ -276,13 +284,46 @@ const TrashButton = ({ threadId }: { threadId: string }) => { ) } -const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx }: { pastThread: ThreadType, idx: number, hoveredIdx: number | null, setHoveredIdx: (idx: number | null) => void }) => { +const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunning }: { + pastThread: ThreadType, + idx: number, + hoveredIdx: number | null, + setHoveredIdx: (idx: number | null) => void, + isRunning: boolean, +} + +) => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') const sidebarStateService = accessor.get('ISidebarStateService') + // const settingsState = useSettingsState() + // const convertService = accessor.get('IConvertToLLMMessageService') + // const chatMode = settingsState.globalSettings.chatMode + // const modelSelection = settingsState.modelSelectionOfFeature?.Chat ?? null + // const copyChatButton = { + // const { messages } = await convertService.prepareLLMChatMessages({ + // chatMessages: currentThread.messages, + // chatMode, + // modelSelection, + // }) + // return JSON.stringify(messages, null, 2) + // }} + // toolTipName={modelSelection === null ? 'Copy As Messages Payload' : `Copy As ${displayInfoOfProviderName(modelSelection.providerName).title} Payload`} + // /> + + + // const currentThread = chatThreadsService.getCurrentThread() + // const copyChatButton2 = { + // return JSON.stringify(currentThread.messages, null, 2) + // }} + // toolTipName={`Copy As Void Chat`} + // /> + let firstMsg = null; const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user'); @@ -319,13 +360,21 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx }: { pas >
+ {/* spinner */} + {isRunning ? : null} + {/* name */} {firstMsg}
{idx === hoveredIdx ? - - : detailsHTML + <> + {/* trash icon */} + + + : <> + {detailsHTML} + }
diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index dc8dcdcc..a38f6401 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -304,6 +304,16 @@ export const useChatThreadsStreamState = (threadId: string) => { return s } +export const useFullChatThreadsStreamState = () => { + const [s, ss] = useState(chatThreadsStreamState) + useEffect(() => { + ss(chatThreadsStreamState) + const listener = () => { ss(chatThreadsStreamState) } + chatThreadsStreamStateListeners.add(listener) + return () => { chatThreadsStreamStateListeners.delete(listener) } + }, [ss]) + return s +} diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index dff20924..7abc98d6 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -18,7 +18,7 @@ export interface ITerminalToolService { readonly _serviceBrand: undefined; listTerminalIds(): string[]; - runCommand(command: string, bgTerminalId: string | null): Promise<{ result: string, resolveReason: TerminalResolveReason }>; + runCommand(command: string, bgTerminalId: string | null): Promise<{ terminalId: string, resPromise: Promise<{ result: string, resolveReason: TerminalResolveReason }> }>; focusTerminal(terminalId: string): Promise terminalExists(terminalId: string): boolean @@ -178,77 +178,83 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ } - // focus the terminal about to run - this.terminalService.setActiveInstance(terminal) - await this.terminalService.focusActiveInstance() + const waitForResult = async () => { + // focus the terminal about to run + this.terminalService.setActiveInstance(terminal) + await this.terminalService.focusActiveInstance() - let result: string = '' - let resolveReason: TerminalResolveReason | undefined = undefined + let result: string = '' + let resolveReason: TerminalResolveReason | undefined = undefined - // create this before we send so that we don't miss events on terminal - const waitUntilDone = new Promise((res, rej) => { - const d2 = terminal.onData(async newData => { - if (resolveReason) return - result += newData - // onDone - const isDone = isCommandComplete(result) - if (isDone) { - resolveReason = { type: 'done', exitCode: isDone.exitCode } - res() - return - } - }) - disposables.push(d2) - }) - - - // send the command here - await terminal.sendText(command, true) - - - // inactivity-based timeout - const waitUntilInactive = new Promise(res => { - let globalTimeoutId: ReturnType; - const resetTimer = () => { - clearTimeout(globalTimeoutId); - globalTimeoutId = setTimeout(() => { + // create this before we send so that we don't miss events on terminal + const waitUntilDone = new Promise((res, rej) => { + const d2 = terminal.onData(async newData => { if (resolveReason) return + result += newData + // onDone + const isDone = isCommandComplete(result) + if (isDone) { + resolveReason = { type: 'done', exitCode: isDone.exitCode } + res() + return + } + }) + disposables.push(d2) + }) - resolveReason = { type: 'timeout' }; - res(); - }, MAX_TERMINAL_INACTIVE_TIME * 1000); - }; - const dTimeout = terminal.onData(() => { resetTimer(); }); - disposables.push(dTimeout, toDisposable(() => clearTimeout(globalTimeoutId))); - resetTimer(); - }); + // send the command here + await terminal.sendText(command, true) - // wait for result - await Promise.any([waitUntilDone, waitUntilInactive,]) + // inactivity-based timeout + const waitUntilInactive = new Promise(res => { + let globalTimeoutId: ReturnType; + const resetTimer = () => { + clearTimeout(globalTimeoutId); + globalTimeoutId = setTimeout(() => { + if (resolveReason) return + + resolveReason = { type: 'timeout' }; + res(); + }, MAX_TERMINAL_INACTIVE_TIME * 1000); + }; + + const dTimeout = terminal.onData(() => { resetTimer(); }); + disposables.push(dTimeout, toDisposable(() => clearTimeout(globalTimeoutId))); + resetTimer(); + }); + + // wait for result + await Promise.any([waitUntilDone, waitUntilInactive,]) + + disposables.forEach(d => d.dispose()) + if (!isBG) { + await this.killTerminal(terminalId) + } + + if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.') + + result = removeAnsiEscapeCodes(result) + .split('\n').slice(1, -1) // remove first and last line (first = command, last = andrewpareles/void %) + .join('\n') + + if (result.length > MAX_TERMINAL_CHARS) { + const half = MAX_TERMINAL_CHARS / 2 + result = result.slice(0, half) + + '\n...\n' + + result.slice(result.length - half, Infinity) + } + + return { result, resolveReason } - disposables.forEach(d => d.dispose()) - if (!isBG) { - await this.killTerminal(terminalId) } + const resPromise = waitForResult() - if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.') - - result = removeAnsiEscapeCodes(result) - .split('\n').slice(1, -1) // remove first and last line (first = command, last = andrewpareles/void %) - .join('\n') - - if (result.length > MAX_TERMINAL_CHARS) { - const half = MAX_TERMINAL_CHARS / 2 - result = result.slice(0, half) - + '\n...\n' - + result.slice(result.length - half, Infinity) - } - - return { result, resolveReason } + return { terminalId, resPromise } } + } registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 75429f0f..78383b85 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -27,7 +27,7 @@ import { IVoidSettingsService } from '../common/voidSettingsService.js' type ValidateParams = { [T in ToolName]: (p: RawToolParamsObj) => ToolCallParams[T] } -type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], interruptTool?: () => void }> } +type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T] | Promise, interruptTool?: () => void }> } type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited) => string } @@ -388,8 +388,11 @@ export class ToolsService implements IToolsService { }, // --- run_terminal: async ({ command, bgTerminalId }) => { - const { result, resolveReason } = await this.terminalToolService.runCommand(command, bgTerminalId) - return { result: { result, resolveReason } } + const { terminalId, resPromise } = await this.terminalToolService.runCommand(command, bgTerminalId) + const interruptTool = () => { + this.terminalToolService.killTerminal(terminalId) + } + return { result: resPromise, interruptTool } }, open_bg_terminal: async () => { // Open a new background terminal without waiting for completion