diff --git a/packages/cli/src/acp/acpClient.test.ts b/packages/cli/src/acp/acpClient.test.ts index 14295954dd..d2c8785e95 100644 --- a/packages/cli/src/acp/acpClient.test.ts +++ b/packages/cli/src/acp/acpClient.test.ts @@ -178,9 +178,10 @@ describe('GeminiAgent', () => { isPlanEnabled: vi.fn().mockReturnValue(true), getGemini31LaunchedSync: vi.fn().mockReturnValue(false), getHasAccessToPreviewModel: vi.fn().mockReturnValue(false), - getCheckpointingEnabled: vi.fn().mockReturnValue(false), getDisableAlwaysAllow: vi.fn().mockReturnValue(false), validatePathAccess: vi.fn().mockReturnValue(null), + getExtensions: vi.fn().mockReturnValue([]), + isTrustedFolder: vi.fn().mockReturnValue(true), getWorkspaceContext: vi.fn().mockReturnValue({ addReadOnlyPath: vi.fn(), }), @@ -198,6 +199,9 @@ describe('GeminiAgent', () => { setClientName: vi.fn(), }, setClientName: vi.fn(), + getSkillManager: vi.fn().mockReturnValue({ + discoverSkills: vi.fn().mockResolvedValue(undefined), + }), get config() { return this; }, @@ -510,6 +514,27 @@ describe('GeminiAgent', () => { ); }); + it('should create a new session with additional roots', async () => { + const _meta = { additionalRoots: ['/extra/path'] }; + + await agent.newSession({ + cwd: '/tmp', + mcpServers: [], + _meta, + } as acp.NewSessionRequest & { _meta?: Record }); + + expect(loadCliConfig).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + includeDirectories: expect.arrayContaining(['/extra/path']), + }), + }), + 'test-session-id', + mockArgv, + { cwd: '/tmp' }, + ); + }); + it('should handle authentication failure gracefully', async () => { mockConfig.refreshAuth.mockRejectedValue(new Error('Auth failed')); const debugSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); @@ -579,6 +604,51 @@ describe('GeminiAgent', () => { expect(result).toMatchObject({ stopReason: 'end_turn' }); }); + it('should support additional roots mid-session in prompt', async () => { + const _meta = { additionalRoots: ['/extra/path'] }; + + // Mock chat.sendMessageStream to avoid crash + const mockChatInTest = { + sendMessageStream: vi + .fn() + .mockResolvedValue( + createMockStream([ + { + type: StreamEventType.CHUNK, + value: { candidates: [{ content: { parts: [{ text: 'Hi' }] } }] }, + }, + ]), + ), + }; + ( + mockConfig.getGeminiClient().startChat as unknown as import('vitest').Mock + ).mockResolvedValue(mockChatInTest); + + await agent.newSession({ cwd: '/tmp', mcpServers: [] }); + const session = ( + agent as unknown as { sessions: Map } + ).sessions.get('test-session-id'); + if (!session) throw new Error('Session not found'); + + // Stub addDirectories and getDirectories on workspaceContext + const mockWorkspaceContext = mockConfig.getWorkspaceContext(); + mockWorkspaceContext.addDirectories = vi.fn(); + mockWorkspaceContext.getDirectories = vi + .fn() + .mockReturnValue(['/tmp', '/extra/path']); + + await agent.prompt({ + sessionId: 'test-session-id', + prompt: [], + _meta, + } as acp.PromptRequest & { _meta?: Record }); + + expect(mockWorkspaceContext.addDirectories).toHaveBeenCalledWith([ + '/extra/path', + ]); + expect(mockConfig.getSkillManager().discoverSkills).toHaveBeenCalled(); + }); + it('should delegate setMode to session', async () => { await agent.newSession({ cwd: '/tmp', mcpServers: [] }); const session = ( diff --git a/packages/cli/src/acp/acpClient.ts b/packages/cli/src/acp/acpClient.ts index 6b76ffdc7a..fbc15474e1 100644 --- a/packages/cli/src/acp/acpClient.ts +++ b/packages/cli/src/acp/acpClient.ts @@ -259,10 +259,21 @@ export class GeminiAgent { ); } - async newSession({ - cwd, - mcpServers, - }: acp.NewSessionRequest): Promise { + async newSession( + req: acp.NewSessionRequest, + ): Promise { + const { cwd, mcpServers } = req; + const meta = hasMeta(req) ? req._meta : undefined; + const additionalRootsRaw = meta?.['additionalRoots']; + const additionalRoots: string[] = []; + if (Array.isArray(additionalRootsRaw)) { + for (const root of additionalRootsRaw) { + if (typeof root === 'string') { + additionalRoots.push(root); + } + } + } + const sessionId = randomUUID(); const loadedSettings = loadSettings(cwd); const config = await this.newSessionConfig( @@ -270,6 +281,7 @@ export class GeminiAgent { cwd, mcpServers, loadedSettings, + additionalRoots, ); const authType = @@ -473,6 +485,7 @@ export class GeminiAgent { cwd: string, mcpServers: acp.McpServer[], loadedSettings?: LoadedSettings, + additionalRoots: string[] = [], ): Promise { const currentSettings = loadedSettings || this.settings; const mergedMcpServers = { ...currentSettings.merged.mcpServers }; @@ -513,6 +526,13 @@ export class GeminiAgent { const settings = { ...currentSettings.merged, mcpServers: mergedMcpServers, + context: { + ...currentSettings.merged.context, + includeDirectories: [ + ...(currentSettings.merged.context?.includeDirectories || []), + ...additionalRoots, + ], + }, }; const config = await loadCliConfig(settings, sessionId, this.argv, { cwd }); @@ -693,6 +713,29 @@ export class Session { const pendingSend = new AbortController(); this.pendingPrompt = pendingSend; + const meta = hasMeta(params) ? params._meta : undefined; + const additionalRootsRaw = meta?.['additionalRoots']; + const additionalRoots: string[] = []; + if (Array.isArray(additionalRootsRaw)) { + for (const root of additionalRootsRaw) { + if (typeof root === 'string') { + additionalRoots.push(root); + } + } + } + + if (additionalRoots.length > 0) { + this.context.config.getWorkspaceContext().addDirectories(additionalRoots); + await this.context.config + .getSkillManager() + .discoverSkills( + this.context.config.storage, + this.context.config.getExtensions(), + this.context.config.isTrustedFolder(), + [...this.context.config.getWorkspaceContext().getDirectories()], + ); + } + await this.context.config.waitForMcpInit(); const promptId = Math.random().toString(16).slice(2); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 4604b1ecbc..2760d52b78 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -1413,6 +1413,7 @@ export class Config implements McpContext, AgentLoopContext { this.storage, this.getExtensions(), this.isTrustedFolder(), + [...this.workspaceContext.getDirectories()], ); this.getSkillManager().setDisabledSkills(this.disabledSkills); @@ -3111,6 +3112,7 @@ export class Config implements McpContext, AgentLoopContext { this.storage, this.getExtensions(), this.isTrustedFolder(), + [...this.workspaceContext.getDirectories()], ); this.getSkillManager().setDisabledSkills(this.disabledSkills); diff --git a/packages/core/src/skills/skillManager.ts b/packages/core/src/skills/skillManager.ts index 108135af30..0dda62f336 100644 --- a/packages/core/src/skills/skillManager.ts +++ b/packages/core/src/skills/skillManager.ts @@ -48,6 +48,7 @@ export class SkillManager { storage: Storage, extensions: GeminiCLIExtension[] = [], isTrusted: boolean = false, + additionalRoots: string[] = [], ): Promise { this.clearSkills(); @@ -89,6 +90,26 @@ export class SkillManager { storage.getProjectAgentSkillsDir(), ); this.addSkillsWithPrecedence(projectAgentSkills); + + // 5. Additional roots (e.g. from includeDirectories) + if (additionalRoots && additionalRoots.length > 0) { + for (const root of additionalRoots) { + // Direct search (looks for SKILL.md and */SKILL.md) + const rootSkills = await loadSkillsFromDir(root); + this.addSkillsWithPrecedence(rootSkills); + + // Search in standard subdirectories inside the root + const agentSkills = await loadSkillsFromDir( + path.join(root, '.agents', 'skills'), + ); + this.addSkillsWithPrecedence(agentSkills); + + const geminiSkills = await loadSkillsFromDir( + path.join(root, '.gemini', 'skills'), + ); + this.addSkillsWithPrecedence(geminiSkills); + } + } } /**