From 07c9a72c836d89cfcb35cc6a1aec941db4334206 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 8 May 2025 23:53:19 -0700 Subject: [PATCH 1/6] openrouter gemini fix --- .../react/src/sidebar-tsx/SidebarChat.tsx | 20 +++++++++---------- .../contrib/void/common/modelCapabilities.ts | 8 ++++---- 2 files changed, 13 insertions(+), 15 deletions(-) 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..e574db9e 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(); } 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 }, } From ae55e97c181076480ebfc984eece1153006c13de Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 9 May 2025 01:06:27 -0700 Subject: [PATCH 2/6] add background terminal (not jittery) --- .../void/browser/terminalToolService.ts | 149 ++++++++++++------ 1 file changed, 101 insertions(+), 48 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index a52e1516..331a85a0 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'; @@ -26,22 +28,22 @@ export interface ITerminalToolService { createPersistentTerminal(opts: { cwd: string | null }): Promise killPersistentTerminal(terminalId: string): Promise + // readTerminal(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 +116,39 @@ 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, + }, + }; - // 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,6 +201,53 @@ 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]) + return capability ?? undefined + } + runCommand: ITerminalToolService['runCommand'] = async (command, params) => { const { type } = params await this.terminalService.whenConnected; @@ -208,7 +264,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 +280,28 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ this.terminalService.setActiveInstance(terminal) await this.terminalService.focusActiveInstance() } - 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((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 ? @@ -286,8 +341,6 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ 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 From 6ada92e6f5b6b055db0ab13c9915dc2ea5088b54 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 9 May 2025 01:39:15 -0700 Subject: [PATCH 3/6] terminal tool improvements --- .voidrules | 4 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 11 +++- .../void/browser/terminalToolService.ts | 61 +++++++++++-------- 3 files changed, 46 insertions(+), 30 deletions(-) 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 e574db9e..97529092 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 @@ -1788,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 =
@@ -1803,7 +1807,8 @@ 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') { } @@ -2290,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 331a85a0..c4e62906 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -26,9 +26,10 @@ 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 - // readTerminal(terminalId: string): Promise getPersistentTerminal(terminalId: string): ITerminalInstance | undefined getTemporaryTerminal(terminalId: string): ITerminalInstance | undefined @@ -201,33 +202,33 @@ 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.`); - // } + 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.'); - // } + // 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); - // } + // 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')); + 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); - // } + 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; - // }; + return result + }; private async _waitForCommandDetectionCapability(terminal: ITerminalInstance) { const cmdCap = terminal.capabilities.get(TerminalCapability.CommandDetection); @@ -249,14 +250,14 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ } 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]; @@ -281,7 +282,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ await this.terminalService.focusActiveInstance() } let result: string = '' - let resolveReason: TerminalResolveReason | undefined = undefined + let resolveReason: TerminalResolveReason | undefined const cmdCap = await this._waitForCommandDetectionCapability(terminal) @@ -340,6 +341,12 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.') + // 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) + } + result = removeAnsiEscapeCodes(result) if (result.length > MAX_TERMINAL_CHARS) { From 215efed10cf2866f490b4eb870e9f30e06801e55 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 9 May 2025 02:03:23 -0700 Subject: [PATCH 4/6] disposables --- .../void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 2 +- src/vs/workbench/contrib/void/browser/terminalToolService.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 97529092..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 @@ -1814,7 +1814,7 @@ const CommandTool = ({ toolMessage, type, threadId }: { threadId: string } & ({ } return <> - + } diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index c4e62906..86510b39 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -246,6 +246,8 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ }) const capability = await Promise.any([waitFiveSeconds, waitForCapability]) + .finally(() => { disposables.forEach((d) => d.dispose()) }) + return capability ?? undefined } @@ -333,8 +335,8 @@ 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() } From a1bf83b5a6efd248b0be82f4449e6b1ef3845e55 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 9 May 2025 03:35:18 -0700 Subject: [PATCH 5/6] faster --- src/vs/workbench/contrib/void/browser/terminalToolService.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index 86510b39..2701df21 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -132,6 +132,8 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ // Copy any other properties from the provided config ...config, }, + // Skip profile check to ensure the terminal is created quickly + skipContributedProfileCheck: true, }; const terminal = await this.terminalService.createTerminal(options) From 70fe79c4fe7d6cf9c502a16033da750de5fe0dd3 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 9 May 2025 03:41:46 -0700 Subject: [PATCH 6/6] add terminal --- src/vs/workbench/contrib/void/browser/terminalToolService.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index 2701df21..f3c0a8fe 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -351,8 +351,11 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ result = await this.readTerminal(terminalId) } - result = removeAnsiEscapeCodes(result) + + 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)