diff --git a/src/vs/platform/agentHost/node/agentHostMain.ts b/src/vs/platform/agentHost/node/agentHostMain.ts index 4bed4cd5921..874fc0b2b27 100644 --- a/src/vs/platform/agentHost/node/agentHostMain.ts +++ b/src/vs/platform/agentHost/node/agentHostMain.ts @@ -26,6 +26,9 @@ import { DefaultURITransformer } from '../../../base/common/uriIpc.js'; import product from '../../product/common/product.js'; import { IProductService } from '../../product/common/productService.js'; import { localize } from '../../../nls.js'; +import { FileService } from '../../files/common/fileService.js'; +import { DiskFileSystemProvider } from '../../files/node/diskFileSystemProvider.js'; +import { Schemas } from '../../../base/common/network.js'; // Entry point for the agent host utility process. // Sets up IPC, logging, and registers agent providers (Copilot). @@ -54,10 +57,14 @@ function startAgentHost(): void { const logService = new LogService(logger); logService.info('Agent Host process started successfully'); + // File service + const fileService = disposables.add(new FileService(logService)); + disposables.add(fileService.registerProvider(Schemas.file, disposables.add(new DiskFileSystemProvider(logService)))); + // Create the real service implementation that lives in this process let agentService: AgentService; try { - agentService = new AgentService(logService); + agentService = new AgentService(logService, fileService); agentService.registerProvider(new CopilotAgent(logService)); } catch (err) { logService.error('Failed to create AgentService', err); diff --git a/src/vs/platform/agentHost/node/agentHostServerMain.ts b/src/vs/platform/agentHost/node/agentHostServerMain.ts index dcae29e9eee..b02c9944711 100644 --- a/src/vs/platform/agentHost/node/agentHostServerMain.ts +++ b/src/vs/platform/agentHost/node/agentHostServerMain.ts @@ -34,6 +34,9 @@ import { AgentSideEffects } from './agentSideEffects.js'; import { SessionStateManager } from './sessionStateManager.js'; import { WebSocketProtocolServer } from './webSocketTransport.js'; import { ProtocolServerHandler } from './protocolServerHandler.js'; +import { FileService } from '../../files/common/fileService.js'; +import { DiskFileSystemProvider } from '../../files/node/diskFileSystemProvider.js'; +import { Schemas } from '../../../base/common/network.js'; /** Log to stderr so messages appear in the terminal alongside the process. */ function log(msg: string): void { @@ -140,6 +143,10 @@ async function main(): Promise { // Observable agents list for root state const registeredAgents = observableValue('agents', []); + // File service + const fileService = disposables.add(new FileService(logService)); + disposables.add(fileService.registerProvider(Schemas.file, disposables.add(new DiskFileSystemProvider(logService)))); + // Shared side-effect handler const sideEffects = disposables.add(new AgentSideEffects(stateManager, { getAgent(session) { @@ -147,7 +154,7 @@ async function main(): Promise { return provider ? agents.get(provider) : agents.values().next().value; }, agents: registeredAgents, - }, logService)); + }, logService, fileService)); function registerAgent(agent: IAgent): void { agents.set(agent.id, agent); diff --git a/src/vs/platform/agentHost/node/agentService.ts b/src/vs/platform/agentHost/node/agentService.ts index 7f25de86424..8ba039ddcb2 100644 --- a/src/vs/platform/agentHost/node/agentService.ts +++ b/src/vs/platform/agentHost/node/agentService.ts @@ -8,6 +8,7 @@ import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; import { observableValue } from '../../../base/common/observable.js'; import { URI } from '../../../base/common/uri.js'; import { ILogService } from '../../log/common/log.js'; +import { IFileService } from '../../files/common/files.js'; import { AgentProvider, IAgentCreateSessionConfig, IAgent, IAgentService, IAgentSessionMetadata, AgentSession, IAgentDescriptor } from '../common/agentService.js'; import type { IActionEnvelope, INotification, ISessionAction } from '../common/state/sessionActions.js'; import type { IBrowseDirectoryResult, IStateSnapshot } from '../common/state/sessionProtocol.js'; @@ -52,6 +53,7 @@ export class AgentService extends Disposable implements IAgentService { constructor( private readonly _logService: ILogService, + private readonly _fileService: IFileService, ) { super(); this._logService.info('AgentService initialized'); @@ -61,7 +63,7 @@ export class AgentService extends Disposable implements IAgentService { this._sideEffects = this._register(new AgentSideEffects(this._stateManager, { getAgent: session => this._findProviderForSession(session), agents: this._agents, - }, this._logService)); + }, this._logService, this._fileService)); } // ---- provider registration ---------------------------------------------- diff --git a/src/vs/platform/agentHost/node/agentSideEffects.ts b/src/vs/platform/agentHost/node/agentSideEffects.ts index b355d9f10d3..91171282c79 100644 --- a/src/vs/platform/agentHost/node/agentSideEffects.ts +++ b/src/vs/platform/agentHost/node/agentSideEffects.ts @@ -6,8 +6,8 @@ import { Disposable, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js'; import { autorun, IObservable } from '../../../base/common/observable.js'; import { URI } from '../../../base/common/uri.js'; -import * as fs from 'fs'; import * as os from 'os'; +import { IFileService } from '../../files/common/files.js'; import { ILogService } from '../../log/common/log.js'; import { AgentProvider, IAgent, IAgentAttachment } from '../common/agentService.js'; import type { ISessionAction } from '../common/state/sessionActions.js'; @@ -49,6 +49,7 @@ export class AgentSideEffects extends Disposable implements IProtocolSideEffectH private readonly _stateManager: SessionStateManager, private readonly _options: IAgentSideEffectsOptions, private readonly _logService: ILogService, + private readonly _fileService: IFileService, ) { super(); @@ -234,27 +235,21 @@ export class AgentSideEffects extends Disposable implements IProtocolSideEffectH } async handleBrowseDirectory(uri: URI): Promise { - const dirPath = uri.fsPath || '/'; - let dirents; + let stat; try { - dirents = await fs.promises.readdir(dirPath, { withFileTypes: true }); - } catch (err) { - const code = (err as NodeJS.ErrnoException).code; - if (code === 'ENOENT') { - throw new ProtocolError(JSON_RPC_INTERNAL_ERROR, `Directory not found: ${uri.toString()}`); - } - if (code === 'ENOTDIR') { - throw new ProtocolError(JSON_RPC_INTERNAL_ERROR, `Not a directory: ${uri.toString()}`); - } - if (code === 'EACCES' || code === 'EPERM') { - throw new ProtocolError(JSON_RPC_INTERNAL_ERROR, `Cannot access directory: ${uri.toString()}`); - } - throw err; + stat = await this._fileService.resolve(uri); + } catch { + throw new ProtocolError(JSON_RPC_INTERNAL_ERROR, `Directory not found: ${uri.toString()}`); } - const entries = dirents.map(dirent => { - const type: IDirectoryEntry['type'] = dirent.isDirectory() ? 'directory' : 'file'; - return { name: dirent.name, type }; - }); + + if (!stat.isDirectory) { + throw new ProtocolError(JSON_RPC_INTERNAL_ERROR, `Not a directory: ${uri.toString()}`); + } + + const entries: IDirectoryEntry[] = (stat.children ?? []).map(child => ({ + name: child.name, + type: child.isDirectory ? 'directory' : 'file', + })); return { entries }; } diff --git a/src/vs/platform/agentHost/test/node/agentService.test.ts b/src/vs/platform/agentHost/test/node/agentService.test.ts index 80150b428a1..83540a9e561 100644 --- a/src/vs/platform/agentHost/test/node/agentService.test.ts +++ b/src/vs/platform/agentHost/test/node/agentService.test.ts @@ -8,6 +8,7 @@ import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle import { URI } from '../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { NullLogService } from '../../../log/common/log.js'; +import { FileService } from '../../../files/common/fileService.js'; import { AgentSession } from '../../common/agentService.js'; import { IActionEnvelope } from '../../common/state/sessionActions.js'; import { AgentService } from '../../node/agentService.js'; @@ -20,7 +21,7 @@ suite('AgentService (node dispatcher)', () => { let copilotAgent: MockAgent; setup(() => { - service = disposables.add(new AgentService(new NullLogService())); + service = disposables.add(new AgentService(new NullLogService(), disposables.add(new FileService(new NullLogService())))); copilotAgent = new MockAgent('copilot'); disposables.add(toDisposable(() => copilotAgent.dispose())); }); diff --git a/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts b/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts index a71600cb0f2..1404f73d1fe 100644 --- a/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts +++ b/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts @@ -4,10 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { VSBuffer } from '../../../../base/common/buffer.js'; import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../base/common/network.js'; import { observableValue } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { FileService } from '../../../files/common/fileService.js'; +import { InMemoryFileSystemProvider } from '../../../files/common/inMemoryFilesystemProvider.js'; import { NullLogService } from '../../../log/common/log.js'; import { AgentSession, IAgent } from '../../common/agentService.js'; import { IActionEnvelope, ISessionAction } from '../../common/state/sessionActions.js'; @@ -15,13 +19,13 @@ import { SessionStatus } from '../../common/state/sessionState.js'; import { AgentSideEffects } from '../../node/agentSideEffects.js'; import { SessionStateManager } from '../../node/sessionStateManager.js'; import { MockAgent } from './mockAgent.js'; -import { cwd } from '../../../../base/common/process.js'; // ---- Tests ------------------------------------------------------------------ suite('AgentSideEffects', () => { const disposables = new DisposableStore(); + let fileService: FileService; let stateManager: SessionStateManager; let agent: MockAgent; let sideEffects: AgentSideEffects; @@ -48,7 +52,16 @@ suite('AgentSideEffects', () => { ); } - setup(() => { + setup(async () => { + fileService = disposables.add(new FileService(new NullLogService())); + const memFs = disposables.add(new InMemoryFileSystemProvider()); + disposables.add(fileService.registerProvider(Schemas.inMemory, memFs)); + + // Seed a file so the handleBrowseDirectory tests can distinguish files from dirs + const testDir = URI.from({ scheme: Schemas.inMemory, path: '/testDir' }); + await fileService.createFolder(testDir); + await fileService.writeFile(URI.from({ scheme: Schemas.inMemory, path: '/testDir/file.txt' }), VSBuffer.fromString('hello')); + agent = new MockAgent(); disposables.add(toDisposable(() => agent.dispose())); stateManager = disposables.add(new SessionStateManager(new NullLogService())); @@ -56,10 +69,12 @@ suite('AgentSideEffects', () => { sideEffects = disposables.add(new AgentSideEffects(stateManager, { getAgent: () => agent, agents: agentList, - }, new NullLogService())); + }, new NullLogService(), fileService)); }); - teardown(() => disposables.clear()); + teardown(() => { + disposables.clear(); + }); ensureNoDisposablesAreLeakedInTestSuite(); // ---- handleAction: session/turnStarted ------------------------------ @@ -88,7 +103,7 @@ suite('AgentSideEffects', () => { const noAgentSideEffects = disposables.add(new AgentSideEffects(stateManager, { getAgent: () => undefined, agents: emptyAgents, - }, new NullLogService())); + }, new NullLogService(), fileService)); const envelopes: IActionEnvelope[] = []; disposables.add(stateManager.onDidEmitEnvelope(e => envelopes.push(e))); @@ -233,7 +248,7 @@ suite('AgentSideEffects', () => { const noAgentSideEffects = disposables.add(new AgentSideEffects(stateManager, { getAgent: () => undefined, agents: emptyAgents, - }, new NullLogService())); + }, new NullLogService(), fileService)); await assert.rejects( () => noAgentSideEffects.handleCreateSession({ session: sessionUri, provider: 'nonexistent' }), @@ -277,14 +292,14 @@ suite('AgentSideEffects', () => { test('throws when the directory does not exist', async () => { await assert.rejects( - () => sideEffects.handleBrowseDirectory(URI.file('/path/that/does/not/exist')), + () => sideEffects.handleBrowseDirectory(URI.from({ scheme: Schemas.inMemory, path: '/nonexistent' })), /Directory not found/, ); }); test('throws when the target is not a directory', async () => { await assert.rejects( - () => sideEffects.handleBrowseDirectory(URI.file(cwd() + '/package.json')), + () => sideEffects.handleBrowseDirectory(URI.from({ scheme: Schemas.inMemory, path: '/testDir/file.txt' })), /Not a directory/, ); }); diff --git a/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts b/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts index 61cc9266712..e0bd001fd5a 100644 --- a/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts +++ b/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts @@ -342,7 +342,7 @@ suite('ProtocolServerHandler', () => { const resp = findResponse(transport.sent, 1); assert.ok(resp); const result = (resp as { result: IInitializeResult }).result; - assert.strictEqual(URI.revive(result.defaultDirectory!).fsPath, '/home/testuser'); + assert.strictEqual(URI.revive(result.defaultDirectory!).path, '/home/testuser'); }); test('browseDirectory routes to side effect handler', async () => { @@ -355,7 +355,7 @@ suite('ProtocolServerHandler', () => { const resp = await responsePromise; assert.strictEqual(sideEffects.browsedUris.length, 1); - assert.strictEqual(sideEffects.browsedUris[0].fsPath, '/home/user/project'); + assert.strictEqual(sideEffects.browsedUris[0].path, '/home/user/project'); assert.ok(resp); const result = (resp as { result: { entries: { name: string; uri: unknown; type: string }[] } }).result;