mirror of
https://github.com/voideditor/void
synced 2026-05-23 17:38:23 +00:00
loader and misc
This commit is contained in:
parent
e94d2de641
commit
4d9b52ede6
6 changed files with 157 additions and 103 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue