diff --git a/.voidrules b/.voidrules index 7bac91a3..0c9e6204 100644 --- a/.voidrules +++ b/.voidrules @@ -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. diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 2b685a00..7aa63188 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -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 =
@@ -1805,13 +1807,14 @@ const CommandTool = ({ toolMessage, type, threadId }: { threadId: string } & ({ } else if (toolMessage.type === 'running_now') { - componentParams.children =
+ if (type === 'run_command') + componentParams.children =
} else if (toolMessage.type === 'rejected' || toolMessage.type === 'tool_request') { } return <> - + } @@ -2292,7 +2295,9 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, 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 diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index a52e1516..f3c0a8fe 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -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 persistentTerminalExists(terminalId: string): boolean + readTerminal(terminalId: string): Promise + createPersistentTerminal(opts: { cwd: string | null }): Promise killPersistentTerminal(terminalId: string): Promise getPersistentTerminal(terminalId: string): ITerminalInstance | undefined getTemporaryTerminal(terminalId: string): ITerminalInstance | undefined } - export const ITerminalToolService = createDecorator('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(res => { - let data = '' - const d = terminal.onData(newData => { - data += newData - if (isCommandComplete(data)) { res() } - }) - disposables.push(d) - }) - const waitForTimeout = new Promise(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(res => { + // let data = '' + // const d = terminal.onData(newData => { + // data += newData + // if (isCommandComplete(data)) { res() } + // }) + // disposables.push(d) + // }) + // const waitForTimeout = new Promise(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((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((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(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) diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 9423acd4..e9eec31b 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -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 }, }