fix persistent terminal ID

This commit is contained in:
Andrew Pareles 2025-04-23 20:46:30 -07:00
parent 1a45baa23e
commit 71fc2895a8
4 changed files with 59 additions and 55 deletions

View file

@ -30,6 +30,7 @@ import { MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME, ToolName, toolNames }
import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js';
import ErrorBoundary from './ErrorBoundary.js';
import { ToolApprovalTypeSwitch } from '../void-settings-tsx/Settings.js';
import { terminalNameOfId } from '../../../terminalToolService.js';
@ -683,6 +684,7 @@ type ToolHeaderParams = {
children?: React.ReactNode;
bottomChildren?: React.ReactNode;
onClick?: () => void;
desc2OnClick?: () => void;
isOpen?: boolean;
className?: string;
}
@ -700,6 +702,7 @@ const ToolHeaderWrapper = ({
bottomChildren,
isError,
onClick,
desc2OnClick,
isOpen,
isRejected,
className, // applies to the main content
@ -769,7 +772,7 @@ const ToolHeaderWrapper = ({
data-tooltip-content={'Canceled'}
data-tooltip-place='top'
/>}
{desc2 && <span className="text-void-fg-4 text-xs">
{desc2 && <span className="text-void-fg-4 text-xs" onClick={desc2OnClick}>
{desc2}
</span>}
{numResults !== undefined && (
@ -1317,7 +1320,7 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName
const toolParams = _toolParams as ToolCallParams['run_command']
return {
desc1: `"${toolParams.command}"`,
desc1Info: toolParams.bgTerminalId
desc1Info: toolParams.persistentTerminalId
}
},
'open_persistent_terminal': () => {
@ -1326,7 +1329,7 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName
},
'kill_persistent_terminal': () => {
const toolParams = _toolParams as ToolCallParams['kill_persistent_terminal']
return { desc1: toolParams.terminalId }
return { desc1: toolParams.persistentTerminalId }
},
'get_dir_tree': () => {
const toolParams = _toolParams as ToolCallParams['get_dir_tree']
@ -2053,6 +2056,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const terminalToolsService = accessor.get('ITerminalToolService')
const toolsService = accessor.get('IToolsService')
const isError = toolMessage.type === 'tool_error'
const title = getTitle(toolMessage)
const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor)
@ -2062,19 +2066,21 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
const { rawParams, params } = toolMessage
const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, }
const { command, persistentTerminalId } = params
if (persistentTerminalId) {
componentParams.desc2 = terminalNameOfId(persistentTerminalId)
componentParams.desc2OnClick = () => terminalToolsService.focusTerminal(persistentTerminalId)
}
if (toolMessage.type === 'success') {
const { result } = toolMessage
const { command } = params
const { resolveReason, result: terminalResult } = result
// it's unclear that this is a button and not an icon.
// componentParams.desc2 = <JumpToTerminalButton
// onClick={() => { terminalToolsService.openTerminal(terminalId) }}
// />
const additionalDetailsStr = resolveReason.type === 'done' ? (resolveReason.exitCode !== 0 ? `\nError: exit code ${resolveReason.exitCode}` : null)
: resolveReason.type === 'timeout' ? `\n(timed out)`
: null
const msg = toolsService.stringOfResult['run_command'](params, result)
componentParams.children = <ToolChildrenWrapper className='whitespace-pre text-nowrap overflow-auto text-sm'>
<div className='!select-text cursor-auto'>
@ -2082,25 +2088,14 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
<span className="text-void-fg-1 font-sans">{`Ran command: `}</span>
<span className="font-mono">{command}</span>
</div>
{(terminalResult + additionalDetailsStr).length && <div>
{msg.length && <div>
<span className='text-void-fg-1'>{`Result: `}</span>
<span className="font-mono">{terminalResult}</span>
<span className="font-mono">{additionalDetailsStr}</span>
<span className="font-mono">{msg}</span>
</div>}
</div>
</ToolChildrenWrapper>
if (params.bgTerminalId)
componentParams.desc2 = `(terminal ${params.bgTerminalId})`
}
else if (toolMessage.type === 'rejected' || toolMessage.type === 'tool_error' || toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') {
const { bgTerminalId, command } = params
if (bgTerminalId) {
componentParams.desc2 = '(persistent terminal)'
if (terminalToolsService.terminalExists(bgTerminalId))
componentParams.onClick = () => terminalToolsService.focusTerminal(bgTerminalId)
}
if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.children = <ToolChildrenWrapper>{result}</ToolChildrenWrapper>
@ -2130,12 +2125,9 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
if (toolMessage.type === 'success') {
const { result } = toolMessage
const { terminalId } = result
if (terminalId) {
componentParams.desc2 = `(terminal ${terminalId})`
if (terminalToolsService.terminalExists(terminalId))
componentParams.onClick = () => terminalToolsService.focusTerminal(terminalId)
}
const { persistentTerminalId } = result
componentParams.desc1 = terminalNameOfId(persistentTerminalId)
componentParams.onClick = () => terminalToolsService.focusTerminal(persistentTerminalId)
}
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
@ -2153,6 +2145,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const terminalToolsService = accessor.get('ITerminalToolService')
const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor)
const title = getTitle(toolMessage)
@ -2167,7 +2160,9 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, }
if (toolMessage.type === 'success') {
const { result } = toolMessage
const { persistentTerminalId } = params
componentParams.desc1 = terminalNameOfId(persistentTerminalId)
componentParams.onClick = () => terminalToolsService.focusTerminal(persistentTerminalId)
}
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage

View file

@ -9,7 +9,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { TerminalExitReason, TerminalLocation } from '../../../../platform/terminal/common/terminal.js';
import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js';
import { MAX_TERMINAL_CHARS, MAX_TERMINAL_INACTIVE_TIME } from '../common/prompt/prompts.js';
import { MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_CHARS, MAX_TERMINAL_INACTIVE_TIME } from '../common/prompt/prompts.js';
import { TerminalResolveReason } from '../common/toolsServiceTypes.js';
@ -39,11 +39,11 @@ function isCommandComplete(output: string) {
}
const nameOfId = (id: string) => {
export const terminalNameOfId = (id: string) => {
if (id === '1') return 'Void Agent'
return `Void Agent (${id})`
}
const idOfName = (name: string) => {
export const idOfTerminalName = (name: string) => {
if (name === 'Void Agent') return '1'
const match = name.match(/Void Agent \((\d+)\)/)
@ -66,7 +66,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
const initializeTerminal = (terminal: ITerminalInstance) => {
// when exit, remove
const d = terminal.onExit(() => {
const terminalId = idOfName(terminal.title)
const terminalId = idOfTerminalName(terminal.title)
if (terminalId !== null && (terminalId in this.terminalInstanceOfId)) delete this.terminalInstanceOfId[terminalId]
d.dispose()
})
@ -75,7 +75,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
// initialize any terminals that are already open
for (const terminal of terminalService.instances) {
const proposedTerminalId = idOfName(terminal.title)
const proposedTerminalId = idOfTerminalName(terminal.title)
if (proposedTerminalId) this.terminalInstanceOfId[proposedTerminalId] = terminal
initializeTerminal(terminal)
@ -111,7 +111,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
const terminalId = this.getValidNewTerminalId();
const terminal = await this.terminalService.createTerminal({
location: TerminalLocation.Panel,
config: { name: nameOfId(terminalId), title: nameOfId(terminalId) },
config: { name: terminalNameOfId(terminalId), title: terminalNameOfId(terminalId) },
})
@ -207,26 +207,34 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
// 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(() => {
if (resolveReason) return
const waitUntilInterrupt = isBG ?
// timeout after X seconds
new Promise<void>((res) => {
setTimeout(() => {
resolveReason = { type: 'timeout' };
res();
}, MAX_TERMINAL_INACTIVE_TIME * 1000);
};
res()
}, MAX_TERMINAL_BG_COMMAND_TIME * 1000)
})
// inactivity-based timeout
: new Promise<void>(res => {
let globalTimeoutId: ReturnType<typeof setTimeout>;
const resetTimer = () => {
clearTimeout(globalTimeoutId);
globalTimeoutId = setTimeout(() => {
if (resolveReason) return
const dTimeout = terminal.onData(() => { resetTimer(); });
disposables.push(dTimeout, toDisposable(() => clearTimeout(globalTimeoutId)));
resetTimer();
});
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,])
await Promise.any([waitUntilDone, waitUntilInterrupt])
disposables.forEach(d => d.dispose())
if (!isBG) {

View file

@ -17,7 +17,7 @@ import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'
import { timeout } from '../../../../base/common/async.js'
import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js'
import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME, ToolName } from '../common/prompt/prompts.js'
import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_INACTIVE_TIME, ToolName } from '../common/prompt/prompts.js'
import { IVoidSettingsService } from '../common/voidSettingsService.js'
@ -507,7 +507,7 @@ export class ToolsService implements IToolsService {
// bg command
if (persistentTerminalId !== null) {
if (resolveReason.type === 'timeout') {
return `Terminal command is running in the background in terminal ${persistentTerminalId}. Here were the outputs after ${MAX_TERMINAL_INACTIVE_TIME} seconds:\n${result_}`
return `Terminal command is running in terminal ${persistentTerminalId}. Here are the current outputs (after ${MAX_TERMINAL_BG_COMMAND_TIME} seconds):\n${result_}`
}
}
// normal command
@ -521,10 +521,10 @@ export class ToolsService implements IToolsService {
},
open_persistent_terminal: (_params, result) => {
const { persistentTerminalId } = result;
return `Successfully created background terminal with ID ${persistentTerminalId}`;
return `Successfully created persistent terminal. persistentTerminalId="${persistentTerminalId}"`;
},
kill_persistent_terminal: (params, _result) => {
return `Successfully closed terminal ${params.persistentTerminalId}.`;
return `Successfully closed terminal "${params.persistentTerminalId}".`;
},
}

View file

@ -27,6 +27,7 @@ export const MAX_CHILDREN_URIs_PAGE = 500
// terminal tool info
export const MAX_TERMINAL_CHARS = 100_000
export const MAX_TERMINAL_INACTIVE_TIME = 8 // seconds
export const MAX_TERMINAL_BG_COMMAND_TIME = 5
// Maximum character limits for prefix and suffix context