mirror of
https://github.com/voideditor/void
synced 2026-05-24 01:48:25 +00:00
Merge pull request #501 from voideditor/model-selection
Terminal improvements
This commit is contained in:
commit
1992a86211
4 changed files with 147 additions and 73 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue