loader and misc

This commit is contained in:
Andrew Pareles 2025-04-20 01:13:47 -07:00
parent e94d2de641
commit 4d9b52ede6
6 changed files with 157 additions and 103 deletions

View file

@ -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
}

View file

@ -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<T>,
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 <div
className={`flex items-center justify-center px-2 `}
@ -2140,18 +2146,25 @@ const Checkpoint = ({ message, threadId, messageIdx, isCheckpointGhost, threadIs
className={`
text-xs
text-void-fg-3
cursor-pointer select-none
select-none
${isCheckpointGhost ? 'opacity-50' : 'opacity-100'}
${isDisabled ? 'cursor-default' : 'cursor-pointer'}
`}
style={{ position: 'relative', display: 'inline-block' }} // allow absolute icon
onClick={() => {
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
</div>
@ -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 = <CopyButton
codeStr={async () => {
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 = <CopyButton
codeStr={async () => {
return JSON.stringify(currentThread.messages, null, 2)
}}
toolTipName={`Copy As Void Chat`}
/>
// (
// <IconShell1
// Icon={CopyIcon}

View file

@ -3,11 +3,11 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { useState } from 'react';
import { IconShell1 } from '../markdown/ApplyBlockHoverButtons.js';
import { useAccessor, useChatThreadsState } from '../util/services.js';
import { useMemo, useState } from 'react';
import { CopyButton, IconShell1 } from '../markdown/ApplyBlockHoverButtons.js';
import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useFullChatThreadsStreamState, useSettingsState } from '../util/services.js';
import { IconX } from './SidebarChat.js';
import { Check, Trash2, X } from 'lucide-react';
import { Check, LoaderCircle, Trash2, X } from 'lucide-react';
import { ThreadType } from '../../../chatThreadService.js';
@ -153,6 +153,13 @@ export const PastThreadsList = ({ className = '' }: { className?: string }) => {
const threadsState = useChatThreadsState()
const { allThreads } = threadsState
const streamState = useFullChatThreadsStreamState()
const runningThreadIds = new Set<string>()
for (const threadId in streamState) {
if (streamState[threadId]?.isRunning) { runningThreadIds.add(threadId) }
}
if (!allThreads) {
return <div key="error" className="p-1">{`Error accessing chat history.`}</div>;
}
@ -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 = <CopyButton
// codeStr={async () => {
// 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 = <CopyButton
// codeStr={async () => {
// 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
>
<div className="flex items-center justify-between gap-1">
<span className="flex items-center gap-2 min-w-0 overflow-hidden">
{/* spinner */}
{isRunning ? <LoaderCircle className="animate-spin bg-void-stroke-1" size={14} /> : null}
{/* name */}
<span className="truncate overflow-hidden text-ellipsis">{firstMsg}</span>
</span>
<div className="flex items-center gap-2 opacity-60">
{idx === hoveredIdx ?
<TrashButton threadId={pastThread.id} />
: detailsHTML
<>
{/* trash icon */}
<TrashButton threadId={pastThread.id} />
</>
: <>
{detailsHTML}
</>
}
</div>
</div>

View file

@ -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
}

View file

@ -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<void>
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<void>((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<void>(res => {
let globalTimeoutId: ReturnType<typeof setTimeout>;
const resetTimer = () => {
clearTimeout(globalTimeoutId);
globalTimeoutId = setTimeout(() => {
// create this before we send so that we don't miss events on terminal
const waitUntilDone = new Promise<void>((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<void>(res => {
let globalTimeoutId: ReturnType<typeof setTimeout>;
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);

View file

@ -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<ToolResultType[T]>, interruptTool?: () => void }> }
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited<ToolResultType[T]>) => 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