This commit is contained in:
Sri Pasumarthi
2026-03-27 19:20:43 -07:00
parent ae123c547c
commit 287c167eab
4 changed files with 141 additions and 5 deletions
+71 -1
View File
@@ -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<string, unknown> });
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<string, Session> }
).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<string, unknown> });
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 = (
+47 -4
View File
@@ -259,10 +259,21 @@ export class GeminiAgent {
);
}
async newSession({
cwd,
mcpServers,
}: acp.NewSessionRequest): Promise<acp.NewSessionResponse> {
async newSession(
req: acp.NewSessionRequest,
): Promise<acp.NewSessionResponse> {
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<Config> {
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);
+2
View File
@@ -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);
+21
View File
@@ -48,6 +48,7 @@ export class SkillManager {
storage: Storage,
extensions: GeminiCLIExtension[] = [],
isTrusted: boolean = false,
additionalRoots: string[] = [],
): Promise<void> {
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);
}
}
}
/**