Skip to content

Conversation

@OEvortex
Copy link

This pull request adds support for the Jules Cloud Agent to the extension, allowing users to interact with the Jules agent via the chat panel. The integration includes agent registration, session management, API key handling, and necessary contributions to the extension framework. The changes are grouped below by theme.

Jules Agent Integration

  • Added a new chat participant for Jules in package.json, enabling the agent to appear in the chat panel.
  • Implemented the JulesAgent class, which manages session creation, activity polling, and chat interactions with the Jules API.
  • Created the JulesAgentContrib class to register the Jules agent as an extension contribution, ensuring it is properly instantiated and managed.
  • Registered JulesAgentContrib in the extension's contribution list so it is loaded and available in VS Code. [1] [2]

BYOK (Bring Your Own Key) Provider Support

  • Added the JulesProvider class to manage Jules API keys, including user prompts for key entry and support for environment variable configuration.
  • Registered JulesProvider in the BYOK provider map, allowing users to manage Jules API keys alongside other providers. [1] [2]

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request integrates the Jules Cloud Agent into the VS Code Copilot extension, enabling users to interact with Jules through the chat panel. The implementation adds complete BYOK (Bring Your Own Key) support for Jules API keys, session management with activity polling, and all necessary infrastructure to register the agent as an extension contribution.

Key changes:

  • Added Jules chat participant with BYOK API key management
  • Implemented session creation and real-time activity monitoring via Jules API
  • Integrated Jules provider into the BYOK framework with special handling (not registered as LM provider)

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
package.json Registers the Jules chat participant in the VS Code chat panel
package-lock.json Updates peer dependencies for various packages (likely from dependency resolution)
src/extension/extension/vscode-node/contributions.ts Adds JulesAgentContrib to the contribution list for extension initialization
src/extension/byok/vscode-node/byokContribution.ts Registers JulesProvider in BYOK system but excludes it from LM provider registration
src/extension/byok/vscode-node/julesProvider.ts Implements BYOK provider for Jules with API key management and environment variable support
src/extension/agents/jules/vscode-node/julesAgentContrib.ts Creates contribution class that instantiates JulesAgent with storage service
src/extension/agents/jules/node/julesAgent.ts Implements core Jules agent functionality including session management, source matching, and activity polling

Comment on lines +11 to +53
export class JulesProvider implements BYOKModelProvider<LanguageModelChatInformation> {
public static readonly providerName = 'jules';
public readonly authType = BYOKAuthType.GlobalApiKey;

constructor(
private readonly _storageService: IBYOKStorageService
) { }

public async updateAPIKey(): Promise<void> {
await handleAPIKeyUpdate(JulesProvider.providerName.toLowerCase(), this._storageService, async (providerName, reconfigure) => {
const result = await window.showInputBox({
title: reconfigure ? `Update API Key for ${JulesProvider.providerName}` : `Enter API Key for ${JulesProvider.providerName}`,
prompt: 'Enter your Jules API Key',
password: true,
ignoreFocusOut: true
});
return result;
});
}

public async updateAPIKeyViaCmd(envVarName: string, action: 'update' | 'remove'): Promise<void> {
if (action === 'remove') {
await this._storageService.deleteAPIKey(JulesProvider.providerName.toLowerCase(), this.authType);
} else {
const apiKey = process.env[envVarName];
if (apiKey) {
await this._storageService.storeAPIKey(JulesProvider.providerName.toLowerCase(), apiKey, this.authType);
}
}
}

public async prepareChat(context: any): Promise<any> {
// Not used for agents usually, but required by interface if we registered it as a LM provider.
// Since we are not registering it as LM provider, this method is not called.
// But BYOKModelProvider extends LanguageModelChatProvider.
throw new Error('Method not implemented.');
}

// Required by LanguageModelChatProvider
async sendChatRequest(): Promise<any> {
throw new Error('Method not implemented.');
}
}
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new JulesProvider class lacks test coverage. Given that other BYOK providers in this directory (e.g., azureProvider, anthropicProvider) have comprehensive test suites, similar test coverage should be added for JulesProvider to ensure API key management, storage operations, and environment variable handling work correctly.

Copilot uses AI. Check for mistakes.
Comment on lines 56 to 260
export class JulesAgent extends Disposable {
private static readonly BaseURL = 'https://jules.googleapis.com/v1alpha';

constructor(
@ILogService private readonly _logService: ILogService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
private readonly _byokStorageService: IBYOKStorageServiceLike // Removed decorator as it is passed explicitly
) {
super();
this._register(vscode.chat.createChatParticipant('jules', this.handler.bind(this)));
}

private async handler(
request: vscode.ChatRequest,
context: vscode.ChatContext,
stream: vscode.ChatResponseStream,
token: vscode.CancellationToken
): Promise<vscode.ChatResult | void> {
this._logService.info('[JulesAgent] Handling request');

const apiKey = await this._byokStorageService.getAPIKey(JulesProvider.providerName.toLowerCase());
if (!apiKey) {
stream.markdown('Please provide a Jules API key using the `GitHub Copilot: Manage Bring Your Own Key API Key` command and selecting "Jules".');
return;
}

try {
// Check for existing session in history or context
// Note: VS Code Chat API doesn't persist custom session state easily across requests unless we use the history or a map.
// For simplicity, we'll assume a new session or try to infer from context if possible.
// But since `context.history` gives previous messages, we might need to store the session ID in a way we can retrieve it.
// For now, let's treat every request as a potential new session OR a message to an ongoing session if we can find a session ID in the history (not easy).

// Actually, we can use a map keyed by the vscode session ID (if available, but `context` doesn't give a stable session ID for the chat window itself easily in all versions).
// `vscode.chat.createChatParticipant` doesn't pass a session object, but `context` has `history`.

// Let's create a new session for now if it's the first turn, or try to continue if we can.
// Since we can't easily persist state per chat window without a session ID from VS Code,
// we will create a new session for the first request.

// Wait, `context` might have metadata? No.

// Let's just implement creating a session and polling for now.

stream.progress('Initializing Jules session...');
const session = await this.createSession(apiKey, request.prompt);
stream.markdown(`Started Jules Session: [${session.id}](${session.url})\n\n`);

await this.monitorSession(apiKey, session.id, stream, token);

} catch (err: any) {
this._logService.error('[JulesAgent] Error:', err);
stream.markdown(`Error: ${err.message}`);
}
}

private async createSession(apiKey: string, prompt: string): Promise<JulesSession> {
const sources = await this.listSources(apiKey);
if (sources.length === 0) {
throw new Error('No sources found. Please connect a repository in Jules settings.');
}

// Try to match the current workspace folder name with the repository name
let matchedSource: any | undefined;
if (sources.length === 1) {
matchedSource = sources[0];
}

const workspaceFolders = vscode.workspace.workspaceFolders;

if (workspaceFolders && workspaceFolders.length > 0) {
const currentFolder = workspaceFolders[0].name.toLowerCase();
// Assuming source name format "sources/github-owner-repo" or "sources/github-org-repo"
// We'll look for the repo name at the end
const matches = sources.filter(s => {
return s.name.toLowerCase().includes(currentFolder) || (s.githubRepo && s.githubRepo.repo.toLowerCase() === currentFolder);
});

if (matches.length > 0) {
matchedSource = matches[0];
}
}

if (!matchedSource) {
throw new Error(`Multiple sources found (${sources.map((s: any) => s.name).join(', ')}). Could not automatically determine which one to use for the current workspace. Please ensure your workspace folder matches the repository name.`);
}

const source = matchedSource.name;

let branch = matchedSource.githubRepo?.defaultBranch?.displayName || 'main';
try {
const gitExtension = vscode.extensions.getExtension('vscode.git');
if (gitExtension) {
const git = gitExtension.exports.getAPI(1);
if (git.repositories.length > 0) {
const repo = git.repositories[0];
if (repo.state.HEAD?.name) {
branch = repo.state.HEAD.name;
}
}
}
} catch (e) {
// Ignore error, fallback to default branch
this._logService.warn('[JulesAgent] Failed to get git branch:', e);
}

const response = await fetch(`${JulesAgent.BaseURL}/sessions`, {
method: 'POST',
headers: {
'x-goog-api-key': apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
prompt: prompt,
sourceContext: {
source: source,
githubRepoContext: {
startingBranch: branch
}
}
})
});

if (!response.ok) {
const text = await response.text();
throw new Error(`Failed to create session: ${response.status} ${text}`);
}

return await response.json() as JulesSession;
}

private async listSources(apiKey: string): Promise<any[]> {
const response = await fetch(`${JulesAgent.BaseURL}/sources?pageSize=10`, {
headers: {
'x-goog-api-key': apiKey
}
});

if (!response.ok) {
const text = await response.text();
throw new Error(`Failed to list sources: ${response.status} ${text}`);
}

const data = await response.json() as any;
return data.sources || [];
}

private async monitorSession(apiKey: string, sessionId: string, stream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise<void> {
let pageToken = '';
const seenActivities = new Set<string>();

while (!token.isCancellationRequested) {
const url = new URL(`${JulesAgent.BaseURL}/sessions/${sessionId}/activities`);
if (pageToken) {
url.searchParams.append('pageToken', pageToken);
}

const response = await fetch(url.toString(), {
headers: {
'x-goog-api-key': apiKey
}
});

if (!response.ok) {
// Ignore transient errors or break?
await new Promise(resolve => setTimeout(resolve, 5000));
continue;
}

const data = await response.json() as any;
const activities = data.activities as JulesActivity[] || [];

for (const activity of activities) {
if (seenActivities.has(activity.id)) {
continue;
}
seenActivities.add(activity.id);

if (activity.agentMessaged) {
stream.markdown(activity.agentMessaged.agentMessage + '\n\n');
} else if (activity.progressUpdated) {
stream.progress(activity.progressUpdated.description);
} else if (activity.planGenerated) {
stream.markdown('**Plan Generated**\n');
// Render plan steps?
} else if (activity.sessionCompleted) {
stream.markdown('**Session Completed**\n');
return;
} else if (activity.sessionFailed) {
stream.markdown(`**Session Failed**: ${activity.sessionFailed.reason}\n`);
return;
}

// Handle artifacts if needed
}

if (data.nextPageToken) {
pageToken = data.nextPageToken;
}

// Poll interval
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
}
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JulesAgent class lacks test coverage for critical functionality including session creation, activity polling, error handling, and source matching logic. Given that other parts of the codebase have comprehensive test coverage (e.g., BYOK providers, agent adapters), tests should be added to verify the agent's behavior in various scenarios such as API failures, multiple sources, missing API keys, and cancellation handling.

Copilot uses AI. Check for mistakes.
Comment on lines +106 to +109
} catch (err: any) {
this._logService.error('[JulesAgent] Error:', err);
stream.markdown(`Error: ${err.message}`);
}
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling in this catch block is inconsistent with the rest of the codebase. Other parts of the codebase use the toErrorMessage utility function to properly format error messages for display. Consider importing toErrorMessage from '../../../util/common/errorMessage' and using toErrorMessage(err) instead of err.message to handle cases where err might not have a message property or might be in an unexpected format.

Copilot uses AI. Check for mistakes.
});

if (!response.ok) {
// Ignore transient errors or break?
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to line 195, there's inconsistent spacing before the comment on line 220. The comment alignment doesn't match the standard indentation patterns in the codebase. Consider fixing the indentation to align with the surrounding code.

Suggested change
// Ignore transient errors or break?
// Ignore transient errors or break?

Copilot uses AI. Check for mistakes.
Comment on lines +219 to +223
if (!response.ok) {
// Ignore transient errors or break?
await new Promise(resolve => setTimeout(resolve, 5000));
continue;
}
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling in the monitorSession method silently swallows errors and continues polling. This could lead to infinite loops if there are persistent API failures. Consider logging the error, tracking consecutive failures, and breaking after a certain number of failures. This would improve debuggability and prevent unnecessary API calls when there's a persistent issue.

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +24
const storageService = new BYOKStorageService(this._context);
this._register(this._instantiationService.createInstance(JulesAgent, storageService));
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JulesAgentContrib is creating a new instance of BYOKStorageService directly instead of using dependency injection. This is inconsistent with the architecture patterns in this codebase where services should be injected through the instantiation service. This also means multiple instances of BYOKStorageService might exist, which could lead to state inconsistencies. Consider using @IBYOKStorageService decorator to inject the service properly.

Copilot uses AI. Check for mistakes.
constructor(
@ILogService private readonly _logService: ILogService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
private readonly _byokStorageService: IBYOKStorageServiceLike // Removed decorator as it is passed explicitly
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 62 is inaccurate and should be removed or updated. The parameter is passed explicitly as stated, but this comment doesn't add value and could be confusing since it seems to explain why something was done rather than document the current state. If the decorator was intentionally removed, this should be explained in the commit message rather than in a code comment.

Suggested change
private readonly _byokStorageService: IBYOKStorageServiceLike // Removed decorator as it is passed explicitly
private readonly _byokStorageService: IBYOKStorageServiceLike

Copilot uses AI. Check for mistakes.
stream.progress(activity.progressUpdated.description);
} else if (activity.planGenerated) {
stream.markdown('**Plan Generated**\n');
// Render plan steps?
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "Render plan steps?" on line 240 indicates incomplete implementation. If plan rendering is not yet implemented, consider adding a TODO with specifics about what needs to be done, or implement the feature before merging. Incomplete features can lead to confusion for users who see "Plan Generated" but don't see the actual plan details.

Suggested change
// Render plan steps?
// TODO(jules-agent): When the backend `planGenerated` payload shape is finalized,
// extract the plan details (for example, step descriptions or structured actions)
// and render them here as markdown so users can see the full execution plan.

Copilot uses AI. Check for mistakes.
@OEvortex
Copy link
Author

@microsoft-github-policy-service agree

OEvortex and others added 2 commits December 22, 2025 14:52
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@OEvortex
Copy link
Author

@copilot open a new pull request to apply changes based on the comments in this thread

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants