Merge pull request #501 from voideditor/model-selection

Terminal improvements
This commit is contained in:
Andrew Pareles 2025-05-09 03:43:03 -07:00 committed by GitHub
commit 1992a86211
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 147 additions and 73 deletions

View file

@ -5,4 +5,6 @@ Most code we care about lives in src/vs/workbench/contrib/void.
You may often need to explore the full repo to find relevant parts of code.
Look for services and built-in functions that you might need to use to solve the problem.
NEVER lazily cast to 'any' in typescript. Find the correct type to apply and use it.
In typescript, do NOT cast to types if not neccessary. NEVER lazily cast to 'any'. Find the correct type to apply and use it.
Do not add or remove semicolons to any of my files. Just go with convention and make the least number of changes.

View file

@ -1727,7 +1727,6 @@ const CommandTool = ({ toolMessage, type, threadId }: { threadId: string } & ({
const commandService = accessor.get('ICommandService')
const terminalToolsService = accessor.get('ITerminalToolService')
const toolsService = accessor.get('IToolsService')
const terminalService = accessor.get('ITerminalService')
const isError = false
const title = getTitle(toolMessage)
const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor)
@ -1754,21 +1753,20 @@ const CommandTool = ({ toolMessage, type, threadId }: { threadId: string } & ({
const terminal = terminalToolsService.getTemporaryTerminal(toolMessage.params.terminalId);
if (!terminal) return;
terminal.detachFromElement();
terminal.attachToElement(container);
try {
terminal.attachToElement(container);
terminal.setVisible(true)
} catch {
}
// Listen for size changes
// Listen for size changes of the container and keep the terminal layout in sync.
const resizeObserver = new ResizeObserver((entries) => {
const height = entries[0].borderBoxSize[0].blockSize
const width = entries[0].borderBoxSize[0].inlineSize
// Layout terminal to fit container dimensions
const height = entries[0].borderBoxSize[0].blockSize;
const width = entries[0].borderBoxSize[0].inlineSize;
if (typeof terminal.layout === 'function') {
terminalService.setActiveInstance(terminal)
terminal.attachToElement(container);
terminal.layout({ width, height });
}
})
});
resizeObserver.observe(container);
return () => { terminal.detachFromElement(); resizeObserver?.disconnect(); }
@ -1790,6 +1788,10 @@ const CommandTool = ({ toolMessage, type, threadId }: { threadId: string } & ({
if (type === 'run_command') msg = toolsService.stringOfResult['run_command'](toolMessage.params, result)
else msg = toolsService.stringOfResult['run_persistent_command'](toolMessage.params, result)
if (type === 'run_persistent_command') {
componentParams.info = persistentTerminalNameOfId(toolMessage.params.persistentTerminalId)
}
componentParams.children = <ToolChildrenWrapper className='whitespace-pre text-nowrap overflow-auto text-sm'>
<div className='!select-text cursor-auto'>
<BlockCode initValue={`${msg.trim()}`} language='shellscript' />
@ -1805,13 +1807,14 @@ const CommandTool = ({ toolMessage, type, threadId }: { threadId: string } & ({
</BottomChildren>
}
else if (toolMessage.type === 'running_now') {
componentParams.children = <div ref={divRef} className='relative h-[300px] text-sm' />
if (type === 'run_command')
componentParams.children = <div ref={divRef} className='relative h-[300px] text-sm' />
}
else if (toolMessage.type === 'rejected' || toolMessage.type === 'tool_request') {
}
return <>
<ToolHeaderWrapper {...componentParams} isOpen={toolMessage.type === 'running_now' ? true : undefined} />
<ToolHeaderWrapper {...componentParams} isOpen={type === 'run_command' && toolMessage.type === 'running_now' ? true : undefined} />
</>
}
@ -2292,7 +2295,9 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
const { rawParams, params } = toolMessage
const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, }
componentParams.info = params.cwd ? `Running in ${getRelative(URI.file(params.cwd), accessor)}` : ''
const relativePath = params.cwd ? getRelative(URI.file(params.cwd), accessor) : ''
componentParams.info = relativePath ? `Running in ${relativePath}` : undefined
if (toolMessage.type === 'success') {
const { result } = toolMessage
const { persistentTerminalId } = result

View file

@ -5,6 +5,7 @@
import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js';
import { ITerminalCapabilityImplMap, TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js';
import { URI } from '../../../../base/common/uri.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
@ -13,6 +14,7 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/
import { ITerminalService, ITerminalInstance, ICreateTerminalOptions } from '../../../../workbench/contrib/terminal/browser/terminal.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';
import { timeout } from '../../../../base/common/async.js';
@ -24,24 +26,25 @@ export interface ITerminalToolService {
focusPersistentTerminal(terminalId: string): Promise<void>
persistentTerminalExists(terminalId: string): boolean
readTerminal(terminalId: string): Promise<string>
createPersistentTerminal(opts: { cwd: string | null }): Promise<string>
killPersistentTerminal(terminalId: string): Promise<void>
getPersistentTerminal(terminalId: string): ITerminalInstance | undefined
getTemporaryTerminal(terminalId: string): ITerminalInstance | undefined
}
export const ITerminalToolService = createDecorator<ITerminalToolService>('TerminalToolService');
function isCommandComplete(output: string) {
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
const completionMatch = output.match(/\]633;D(?:;(\d+))?/)
if (!completionMatch) { return false }
if (completionMatch[1] !== undefined) return { exitCode: parseInt(completionMatch[1]) }
return { exitCode: 0 }
}
// function isCommandComplete(output: string) {
// // https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
// const completionMatch = output.match(/\]633;D(?:;(\d+))?/)
// if (!completionMatch) { return false }
// if (completionMatch[1] !== undefined) return { exitCode: parseInt(completionMatch[1]) }
// return { exitCode: 0 }
// }
export const persistentTerminalNameOfId = (id: string) => {
@ -114,32 +117,41 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
}
private async _createTerminal(props: { cwd: string | null, config: ICreateTerminalOptions['config'] }) {
const { cwd: override_cwd, config } = props
private async _createTerminal(props: { cwd: string | null, config: ICreateTerminalOptions['config'], hidden?: boolean }) {
const { cwd: override_cwd, config, hidden } = props;
const cwd: URI | string | undefined = (override_cwd ?? undefined) ?? this.workspaceContextService.getWorkspace().folders[0]?.uri
const cwd: URI | string | undefined = (override_cwd ?? undefined) ?? this.workspaceContextService.getWorkspace().folders[0]?.uri;
// create new terminal and return its ID
const terminal = await this.terminalService.createTerminal({
cwd: cwd,
location: TerminalLocation.Panel,
config: config,
})
const options: ICreateTerminalOptions = {
cwd,
location: hidden ? undefined : TerminalLocation.Panel,
config: {
name: config && 'name' in config ? config.name : undefined,
forceShellIntegration: true,
hideFromUser: hidden ? true : undefined,
// Copy any other properties from the provided config
...config,
},
// Skip profile check to ensure the terminal is created quickly
skipContributedProfileCheck: true,
};
// when a new terminal is created, there is an initial command that gets run which is empty, wait for it to end before returning
const disposables: IDisposable[] = []
const waitForMount = new Promise<void>(res => {
let data = ''
const d = terminal.onData(newData => {
data += newData
if (isCommandComplete(data)) { res() }
})
disposables.push(d)
})
const waitForTimeout = new Promise<void>(res => { setTimeout(() => { res() }, 5000) })
const terminal = await this.terminalService.createTerminal(options)
await Promise.any([waitForMount, waitForTimeout,])
disposables.forEach(d => d.dispose())
// // when a new terminal is created, there is an initial command that gets run which is empty, wait for it to end before returning
// const disposables: IDisposable[] = []
// const waitForMount = new Promise<void>(res => {
// let data = ''
// const d = terminal.onData(newData => {
// data += newData
// if (isCommandComplete(data)) { res() }
// })
// disposables.push(d)
// })
// const waitForTimeout = new Promise<void>(res => { setTimeout(() => { res() }, 5000) })
// await Promise.any([waitForMount, waitForTimeout,])
// disposables.forEach(d => d.dispose())
return terminal
@ -192,15 +204,64 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
readTerminal: ITerminalToolService['readTerminal'] = async (terminalId) => {
// Try persistent first, then temporary
const terminal = this.getPersistentTerminal(terminalId) ?? this.getTemporaryTerminal(terminalId);
if (!terminal) {
throw new Error(`Read Terminal: Terminal with ID ${terminalId} does not exist.`);
}
// Ensure the xterm.js instance has been created otherwise we cannot access the buffer.
if (!terminal.xterm) {
throw new Error('Read Terminal: The requested terminal has not yet been rendered and therefore has no scrollback buffer available.');
}
// Collect lines from the buffer iterator (oldest to newest)
const lines: string[] = [];
for (const line of terminal.xterm.getBufferReverseIterator()) {
lines.unshift(line);
}
let result = removeAnsiEscapeCodes(lines.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);
}
return result
};
private async _waitForCommandDetectionCapability(terminal: ITerminalInstance) {
const cmdCap = terminal.capabilities.get(TerminalCapability.CommandDetection);
if (cmdCap) return cmdCap
const disposables: IDisposable[] = []
const waitFiveSeconds = timeout(5000)
const waitForCapability = new Promise<ITerminalCapabilityImplMap[TerminalCapability.CommandDetection]>((res) => {
disposables.push(
terminal.capabilities.onDidAddCapability((e) => {
if (e.id === TerminalCapability.CommandDetection) res(e.capability)
})
)
})
const capability = await Promise.any([waitFiveSeconds, waitForCapability])
.finally(() => { disposables.forEach((d) => d.dispose()) })
return capability ?? undefined
}
runCommand: ITerminalToolService['runCommand'] = async (command, params) => {
const { type } = params
await this.terminalService.whenConnected;
const { type } = params
const isPersistent = type === 'persistent'
let terminal: ITerminalInstance
const disposables: IDisposable[] = []
const isPersistent = type === 'persistent'
if (isPersistent) { // BG process
const { persistentTerminalId } = params
terminal = this.persistentTerminalInstanceOfId[persistentTerminalId];
@ -208,7 +269,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
}
else {
const { cwd } = params
terminal = await this._createTerminal({ cwd: cwd, config: { name: 'Void Temporary Terminal', title: 'Void Temporary Terminal' } })
terminal = await this._createTerminal({ cwd: cwd, config: undefined, hidden: true })
this.temporaryTerminalInstanceOfId[params.terminalId] = terminal
}
@ -224,29 +285,28 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
this.terminalService.setActiveInstance(terminal)
await this.terminalService.focusActiveInstance()
}
let result: string = ''
let resolveReason: TerminalResolveReason | undefined = undefined
let resolveReason: TerminalResolveReason | 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
}
const cmdCap = await this._waitForCommandDetectionCapability(terminal)
if (!cmdCap) throw new Error(`There was an error using the terminal: CommandDetection capability did not mount yet. Please try again in a few seconds or report this to the Void team.`)
// Prefer the structured command-detection capability when available
const waitUntilDone = new Promise<void>(resolve => {
const l = cmdCap.onCommandFinished(cmd => {
if (resolveReason) return // already resolved
resolveReason = { type: 'done', exitCode: cmd.exitCode ?? 0 };
result = cmd.getOutput() ?? ''
l.dispose()
resolve()
})
disposables.push(d2)
disposables.push(l)
})
// send the command here
// send the command now that listeners are attached
await terminal.sendText(command, true)
const waitUntilInterrupt = isPersistent ?
@ -277,18 +337,25 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
// wait for result
await Promise.any([waitUntilDone, waitUntilInterrupt])
.finally(() => disposables.forEach(d => d.dispose()))
disposables.forEach(d => d.dispose())
if (!isPersistent) {
interrupt()
}
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')
// read result if timed out, since we didn't get it (could clean this code up but it's ok)
if (resolveReason.type === 'timeout') {
const terminalId = isPersistent ? params.persistentTerminalId : params.terminalId
result = await this.readTerminal(terminalId)
}
if (!isPersistent) result = `$ ${command}\n${result}`
result = removeAnsiEscapeCodes(result)
// trim
if (result.length > MAX_TERMINAL_CHARS) {
const half = MAX_TERMINAL_CHARS / 2
result = result.slice(0, half)

View file

@ -1220,10 +1220,10 @@ const openRouterSettings: VoidStaticProviderInfo = {
// TODO!!! send a query to openrouter to get the price, etc.
modelOptionsFallback: (modelName) => {
const res = extensiveModelOptionsFallback(modelName)
// // openRouter does not support gemini-style, use openai-style instead
// if (res?.specialToolFormat === 'gemini-style') {
// res.specialToolFormat = 'openai-style'
// }
// openRouter does not support gemini-style, use openai-style instead
if (res?.specialToolFormat === 'gemini-style') {
res.specialToolFormat = 'openai-style'
}
return res
},
}