Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion build/buildfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const workerLocalFileSearch = createModuleDescription('vs/workbench/servi
export const workerProfileAnalysis = createModuleDescription('vs/platform/profiling/electron-browser/profileAnalysisWorkerMain');
export const workerOutputLinks = createModuleDescription('vs/workbench/contrib/output/common/outputLinkComputerMain');
export const workerBackgroundTokenization = createModuleDescription('vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.workerMain');
export const consoleCompactor = createModuleDescription('vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/consoleCompactor/consoleCompactor');

export const workbenchDesktop = [
createModuleDescription('vs/workbench/contrib/debug/node/telemetryApp'),
Expand All @@ -29,7 +30,8 @@ export const workbenchDesktop = [
createModuleDescription('vs/platform/agentHost/node/diffWorkerMain'),
createModuleDescription('vs/workbench/api/node/extensionHostProcess'),
createModuleDescription('vs/workbench/workbench.desktop.main'),
createModuleDescription('vs/sessions/sessions.desktop.main')
createModuleDescription('vs/sessions/sessions.desktop.main'),
consoleCompactor,
];

export const workbenchWeb = createModuleDescription('vs/workbench/workbench.web.main.internal');
Expand Down Expand Up @@ -74,6 +76,7 @@ const buildfile = {
workerProfileAnalysis,
workerOutputLinks,
workerBackgroundTokenization,
consoleCompactor,
workbenchDesktop,
workbenchWeb,
sessionsWeb,
Expand Down
1 change: 1 addition & 0 deletions build/gulpfile.reh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ const webEntryPoints = [
buildfile.workerOutputLinks,
buildfile.workerBackgroundTokenization,
buildfile.keyboardMaps,
buildfile.consoleCompactor,
buildfile.codeWeb
].flat();

Expand Down
1 change: 1 addition & 0 deletions build/gulpfile.vscode.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ const vscodeWebEntryPoints = [
buildfile.workerOutputLinks,
buildfile.workerBackgroundTokenization,
buildfile.keyboardMaps,
buildfile.consoleCompactor,
buildfile.workbenchWeb,
buildfile.sessionsWeb,
].flat();
Expand Down
4 changes: 4 additions & 0 deletions build/next/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { isWebExtension, type IScannedBuiltinExtension } from '../lib/extensions
import { runBuildFast } from './build-fast.ts';
import { bundleDevTunnelsWeb } from './devTunnelsWeb.ts';
import { copyFile, mapWithConcurrency, MAX_CONCURRENT_FILE_OPERATIONS, transpileFile } from './transpile.ts';
import { consoleCompactor } from '../buildfile.ts';

const globAsync = promisify(glob);

Expand Down Expand Up @@ -166,6 +167,7 @@ function getEntryPointsForTarget(target: BuildTarget): string[] {
...desktopWorkerEntryPoints,
...desktopEntryPoints,
...codeEntryPoints,
consoleCompactor.name,
];
case 'server':
return [
Expand All @@ -177,13 +179,15 @@ function getEntryPointsForTarget(target: BuildTarget): string[] {
...workerEntryPoints,
...webEntryPoints,
...keyboardMapEntryPoints,
consoleCompactor.name,
];
case 'web':
return [
...workerEntryPoints,
...webOnlyEntryPoints,
'vs/workbench/workbench.web.main.internal', // web workbench only (no browser shell)
...keyboardMapEntryPoints,
consoleCompactor.name,
];
default:
throw new Error(`Unknown target: ${target}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Shell-output compactor. Classifies shell commands and strips non-diagnostic
// noise from their output. This is a faithful TypeScript port of the original
// Rust implementation (lib.rs, report.rs, shell_output_compactor.rs).
// Bundled separately and loaded on demand; keep this module independent of workbench services.

//#region Public API types

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { appendEscapedMarkdownInlineCode, escapeMarkdownSyntaxTokens, MarkdownSt
import { Disposable, DisposableMap, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js';
import { ResourceMap } from '../../../../../../base/common/map.js';
import { getMediaMime } from '../../../../../../base/common/mime.js';
import { FileAccess } from '../../../../../../base/common/network.js';
import { basename, posix, win32 } from '../../../../../../base/common/path.js';
import { OperatingSystem, OS } from '../../../../../../base/common/platform.js';
import { count } from '../../../../../../base/common/strings.js';
Expand Down Expand Up @@ -86,7 +87,6 @@ import { isSessionAutoApproveLevel, isTerminalAutoApproveAllowed, isToolEligible
import type { IJSONSchemaMap } from '../../../../../../base/common/jsonSchema.js';
import { ChatElicitationRequestPart } from '../../../../chat/common/model/chatProgressTypes/chatElicitationRequestPart.js';
import { getSandboxPrecheckInputsForToolInvocation } from '../../../../chat/browser/tools/toolHelpers.js';
import { compact } from './consoleCompactor/consoleCompactor.js';
import { IChatSessionsService } from '../../../../chat/common/chatSessionsService.js';

// #region Tool data
Expand Down Expand Up @@ -2511,19 +2511,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
if (outputAnalyzerMessage) {
resultText.push(`${outputAnalyzerMessage}\n`);
}
let outputForResult = terminalResult;
if (this._configurationService.getValue<boolean>(TerminalChatAgentToolsSettingId.OutputCompaction) === true) {
try {
const commandForCompaction = toolSpecificData.commandLine.forDisplay ?? command;
const report = compact(commandForCompaction, terminalResult);
this._telemetry.logCompaction(report);
if (report.applied) {
outputForResult = report.compactedOutput;
}
} catch {
this._telemetry.logCompactionFailed();
}
}
const outputForResult = await this._compactOutput(toolSpecificData.commandLine.forDisplay ?? command, terminalResult);
// Process large output: write to file if needed, then truncate with file path
const processedOutput = await this._largeOutputFileWriter.processOutput(outputForResult);
resultText.push(processedOutput);
Expand Down Expand Up @@ -2634,6 +2622,27 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
return lines.join('\n');
}

protected _loadConsoleCompactor(): Promise<typeof import('./consoleCompactor/consoleCompactor.js')> {
return import(/* webpackIgnore: true */FileAccess.asBrowserUri('vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/consoleCompactor/consoleCompactor.js').toString(true));
}

protected async _compactOutput(command: string, output: string): Promise<string> {
if (this._configurationService.getValue<boolean>(TerminalChatAgentToolsSettingId.OutputCompaction) !== true) {
return output;
}

try {
const { compact } = await this._loadConsoleCompactor();
const report = compact(command, output);
this._telemetry.logCompaction(report);
return report.applied ? report.compactedOutput : output;
} catch (error) {
this._logService.warn('RunInTerminalTool: Failed to compact terminal output', error);
this._telemetry.logCompactionFailed();
return output;
}
}

private async _getOutputAnalyzerMessage(exitCode: number | undefined, exitResult: string, commandLine: string, isSandboxWrapped: boolean): Promise<string | undefined> {
for (const analyzer of this._outputAnalyzers) {
const message = await analyzer.analyze({ exitCode, exitResult, commandLine, isSandboxWrapped });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { ok, strictEqual } from 'assert';
import { deepStrictEqual, ok, strictEqual } from 'assert';
import { Separator } from '../../../../../../base/common/actions.js';
import { DeferredPromise } from '../../../../../../base/common/async.js';
import { CancellationToken } from '../../../../../../base/common/cancellation.js';
Expand All @@ -26,7 +26,7 @@ import type { TestInstantiationService } from '../../../../../../platform/instan
import { NullLogService } from '../../../../../../platform/log/common/log.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js';
import { TerminalCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js';
import { ITerminalProfile } from '../../../../../../platform/terminal/common/terminal.js';
import { ITerminalLogService, ITerminalProfile } from '../../../../../../platform/terminal/common/terminal.js';
import { IWorkspaceContextService, toWorkspaceFolder } from '../../../../../../platform/workspace/common/workspace.js';
import { Workspace } from '../../../../../../platform/workspace/test/common/testWorkspace.js';
import { IHistoryService } from '../../../../../services/history/common/history.js';
Expand Down Expand Up @@ -67,6 +67,22 @@ import { IChatSessionsService } from '../../../../chat/common/chatSessionsServic
class TestRunInTerminalTool extends RunInTerminalTool {
protected override _osBackend: Promise<OperatingSystem> = Promise.resolve(OperatingSystem.Windows);

compactorLoadCount = 0;
compactorLoadError: Error | undefined;

protected override async _loadConsoleCompactor() {
this.compactorLoadCount++;
if (this.compactorLoadError) {
throw this.compactorLoadError;
}
// The unit runner uses file:// rather than the workbench's vscode-file:// resource protocol.
return import('../../browser/tools/consoleCompactor/consoleCompactor.js');
}

compactOutput(command: string, output: string): Promise<string> {
return this._compactOutput(command, output);
}

get sessionTerminalAssociations() { return this._sessionTerminalAssociations; }
get sessionTerminalInstances() { return this._sessionTerminalInstances; }
get profileFetcher() { return this._profileFetcher; }
Expand Down Expand Up @@ -102,6 +118,7 @@ suite('RunInTerminalTool', () => {
let createTerminalCallCount: number;
let chatSessions: Map<string, ChatModel>;
let chatSessionContribution: ReturnType<IChatSessionsService['getChatSessionContribution']>;
let compactionWarnings: { message: string; details: string[] }[];

let runInTerminalTool: TestRunInTerminalTool;

Expand All @@ -112,6 +129,7 @@ suite('RunInTerminalTool', () => {
setup(() => {
configurationService = new TestConfigurationService();
workspaceContextService = new TestContextService();
compactionWarnings = [];

const logService = new NullLogService();
fileService = store.add(new FileService(logService));
Expand Down Expand Up @@ -202,6 +220,12 @@ suite('RunInTerminalTool', () => {
configurationService: () => configurationService,
fileService: () => fileService,
}, store);
instantiationService.stub(ITerminalLogService, store.add(new class extends NullLogService {
declare readonly _logBrand: undefined;
override warn(message: string, ...args: unknown[]): void {
compactionWarnings.push({ message, details: args.map(arg => String(arg)) });
}
}()));

const chatServiceStub = {
onDidDisposeSession: chatServiceDisposeEmitter.event,
Expand Down Expand Up @@ -460,6 +484,52 @@ suite('RunInTerminalTool', () => {
return getAutomaticSandboxRetryTitle(tool, 'allowNetwork', shellType, blockedDomains);
}

suite('output compaction', () => {
test('does not load the compactor when compaction is disabled', async () => {
setConfig(TerminalChatAgentToolsSettingId.OutputCompaction, false);
const output = await runInTerminalTool.compactOutput('echo hello', 'hello\n');

deepStrictEqual({ output, loads: runInTerminalTool.compactorLoadCount }, { output: 'hello\n', loads: 0 });
});

test('loads the compactor on first use and preserves unremarkable output', async () => {
setConfig(TerminalChatAgentToolsSettingId.OutputCompaction, true);
const loadsBeforeUse = runInTerminalTool.compactorLoadCount;
const output = await runInTerminalTool.compactOutput('echo hello', 'hello\n');

deepStrictEqual({ loadsBeforeUse, output, loads: runInTerminalTool.compactorLoadCount, warnings: compactionWarnings }, { loadsBeforeUse: 0, output: 'hello\n', loads: 1, warnings: [] });
});

test('compacts noisy output through the dynamically loaded module', async () => {
setConfig(TerminalChatAgentToolsSettingId.OutputCompaction, true);
const original = Array.from({ length: 400 }, (_, i) => `npm http fetch GET 200 https://registry.npmjs.org/pkg${i} ${i}ms (cache miss)`).join('\n') + '\nadded 400 packages in 3s\n';
const output = await runInTerminalTool.compactOutput('npm install', original);

deepStrictEqual({
compacted: output.length < original.length,
retainsSummary: output.includes('added 400 packages in 3s'),
loads: runInTerminalTool.compactorLoadCount,
warnings: compactionWarnings,
}, { compacted: true, retainsSummary: true, loads: 1, warnings: [] });
});

test('logs a load failure and preserves the command output', async () => {
setConfig(TerminalChatAgentToolsSettingId.OutputCompaction, true);
runInTerminalTool.compactorLoadError = new Error('Compactor could not be loaded');
const output = await runInTerminalTool.compactOutput('echo hello', 'hello\n');

deepStrictEqual({
output,
loads: runInTerminalTool.compactorLoadCount,
warnings: compactionWarnings,
}, {
output: 'hello\n',
loads: 1,
warnings: [{ message: 'RunInTerminalTool: Failed to compact terminal output', details: ['Error: Compactor could not be loaded'] }],
});
});
});

suite('sandbox invocation messaging', () => {
test('should instruct models to use $TMPDIR instead of /tmp when sandboxed', async () => {
sandboxEnabled = true;
Expand Down