Skip to content

Commit 7b9cb65

Browse files
Merge pull request #8191 from continuedev/tomasz/con-4203
Background / Async / Remote mode: Kickoff in /agents when you are in the extension
2 parents dcfb556 + a5de87a commit 7b9cb65

File tree

11 files changed

+822
-11
lines changed

11 files changed

+822
-11
lines changed

core/control-plane/client.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,4 +439,138 @@ export class ControlPlaneClient {
439439
);
440440
}
441441
}
442+
443+
/**
444+
* Create a new background agent
445+
*/
446+
public async createBackgroundAgent(
447+
prompt: string,
448+
repoUrl: string,
449+
name: string,
450+
branch?: string,
451+
organizationId?: string,
452+
contextItems?: any[],
453+
selectedCode?: any[],
454+
agent?: string,
455+
): Promise<{ id: string }> {
456+
if (!(await this.isSignedIn())) {
457+
throw new Error("Not signed in to Continue");
458+
}
459+
460+
const requestBody: any = {
461+
prompt,
462+
repoUrl,
463+
name,
464+
branchName: branch,
465+
};
466+
467+
if (organizationId) {
468+
requestBody.organizationId = organizationId;
469+
}
470+
471+
// Include context items if provided
472+
if (contextItems && contextItems.length > 0) {
473+
requestBody.contextItems = contextItems.map((item) => ({
474+
content: item.content,
475+
description: item.description,
476+
name: item.name,
477+
uri: item.uri,
478+
}));
479+
}
480+
481+
// Include selected code if provided
482+
if (selectedCode && selectedCode.length > 0) {
483+
requestBody.selectedCode = selectedCode.map((code) => ({
484+
filepath: code.filepath,
485+
range: code.range,
486+
contents: code.contents,
487+
}));
488+
}
489+
490+
// Include agent configuration if provided
491+
if (agent) {
492+
requestBody.agent = agent;
493+
}
494+
495+
const resp = await this.requestAndHandleError("agents", {
496+
method: "POST",
497+
headers: {
498+
"Content-Type": "application/json",
499+
},
500+
body: JSON.stringify(requestBody),
501+
});
502+
503+
return (await resp.json()) as { id: string };
504+
}
505+
506+
/**
507+
* List all background agents for the current user or organization
508+
* @param organizationId - Optional organization ID to filter agents by organization scope
509+
* @param limit - Optional limit for number of agents to return (default: 5)
510+
*/
511+
public async listBackgroundAgents(
512+
organizationId?: string,
513+
limit?: number,
514+
): Promise<{
515+
agents: Array<{
516+
id: string;
517+
name: string | null;
518+
status: string;
519+
repoUrl: string;
520+
createdAt: string;
521+
metadata?: {
522+
github_repo?: string;
523+
};
524+
}>;
525+
totalCount: number;
526+
}> {
527+
if (!(await this.isSignedIn())) {
528+
return { agents: [], totalCount: 0 };
529+
}
530+
531+
try {
532+
// Build URL with query parameters
533+
const params = new URLSearchParams();
534+
if (organizationId) {
535+
params.set("organizationId", organizationId);
536+
}
537+
if (limit !== undefined) {
538+
params.set("limit", limit.toString());
539+
}
540+
541+
const url = `agents${params.toString() ? `?${params.toString()}` : ""}`;
542+
543+
const resp = await this.requestAndHandleError(url, {
544+
method: "GET",
545+
});
546+
547+
const result = (await resp.json()) as {
548+
agents: any[];
549+
totalCount: number;
550+
};
551+
552+
return {
553+
agents: result.agents.map((agent: any) => ({
554+
id: agent.id,
555+
name: agent.name || agent.metadata?.name || null,
556+
status: agent.status,
557+
repoUrl: agent.metadata?.repo_url || agent.repo_url || "",
558+
createdAt:
559+
agent.created_at || agent.create_time_ms
560+
? new Date(agent.created_at || agent.create_time_ms).toISOString()
561+
: new Date().toISOString(),
562+
metadata: {
563+
github_repo:
564+
agent.metadata?.github_repo || agent.metadata?.repo_url,
565+
},
566+
})),
567+
totalCount: result.totalCount,
568+
};
569+
} catch (e) {
570+
Logger.error(e, {
571+
context: "control_plane_list_background_agents",
572+
});
573+
return { agents: [], totalCount: 0 };
574+
}
575+
}
442576
}

core/core.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,9 +472,11 @@ export class Core {
472472
const urlPath = msg.data.path.startsWith("/")
473473
? msg.data.path.slice(1)
474474
: msg.data.path;
475-
let url = `${env.APP_URL}${urlPath}`;
475+
let url;
476476
if (msg.data.orgSlug) {
477-
url += `?org=${msg.data.orgSlug}`;
477+
url = `${env.APP_URL}organizations/${msg.data.orgSlug}/${urlPath}`;
478+
} else {
479+
url = `${env.APP_URL}${urlPath}`;
478480
}
479481
await this.messenger.request("openUrl", url);
480482
});

core/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ export interface PromptLog {
462462
completion: string;
463463
}
464464

465-
export type MessageModes = "chat" | "agent" | "plan";
465+
export type MessageModes = "chat" | "agent" | "plan" | "background";
466466

467467
export type ToolStatus =
468468
| "generating" // Tool call arguments are being streamed from the LLM

core/protocol/ideWebview.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import {
66
AddToChatPayload,
77
ApplyState,
88
ApplyToFilePayload,
9+
ContextItemWithId,
910
HighlightedCodePayload,
1011
MessageContent,
12+
RangeInFile,
1113
RangeInFileWithContents,
1214
SetCodeToEditPayload,
1315
ShowFilePayload,
@@ -50,6 +52,32 @@ export type ToIdeFromWebviewProtocol = ToIdeFromWebviewOrCoreProtocol & {
5052
"edit/addCurrentSelection": [undefined, void];
5153
"edit/clearDecorations": [undefined, void];
5254
"session/share": [{ sessionId: string }, void];
55+
createBackgroundAgent: [
56+
{
57+
content: MessageContent;
58+
contextItems: ContextItemWithId[];
59+
selectedCode: RangeInFile[];
60+
organizationId?: string;
61+
agent?: string;
62+
},
63+
void,
64+
];
65+
listBackgroundAgents: [
66+
{ organizationId?: string; limit?: number },
67+
{
68+
agents: Array<{
69+
id: string;
70+
name: string | null;
71+
status: string;
72+
repoUrl: string;
73+
createdAt: string;
74+
metadata?: {
75+
github_repo?: string;
76+
};
77+
}>;
78+
totalCount: number;
79+
},
80+
];
5381
};
5482

5583
export type ToWebviewFromIdeProtocol = ToWebviewFromIdeOrCoreProtocol & {

extensions/vscode/src/extension/VsCodeMessenger.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { getExtensionUri } from "../util/vscode";
3131
import { VsCodeIde } from "../VsCodeIde";
3232
import { VsCodeWebviewProtocol } from "../webviewProtocol";
3333

34+
import { encodeFullSlug } from "../../../../packages/config-yaml/dist";
3435
import { VsCodeExtension } from "./VsCodeExtension";
3536

3637
type ToIdeOrWebviewFromCoreProtocol = ToIdeFromCoreProtocol &
@@ -263,6 +264,167 @@ export class VsCodeMessenger {
263264
);
264265
});
265266

267+
this.onWebview("createBackgroundAgent", async (msg) => {
268+
const configHandler = await configHandlerPromise;
269+
const { content, contextItems, selectedCode, organizationId } = msg.data;
270+
271+
// Convert resolved content to plain text prompt
272+
const prompt = stripImages(content);
273+
274+
if (!prompt || prompt.trim().length === 0) {
275+
vscode.window.showErrorMessage(
276+
"Please enter a prompt to create a background agent",
277+
);
278+
return;
279+
}
280+
281+
// Get workspace information
282+
const workspaceDirs = await this.ide.getWorkspaceDirs();
283+
if (workspaceDirs.length === 0) {
284+
vscode.window.showErrorMessage(
285+
"No workspace folder found. Please open a workspace to create a background agent.",
286+
);
287+
return;
288+
}
289+
290+
const workspaceDir = workspaceDirs[0];
291+
let repoUrl = "";
292+
let branch = "";
293+
294+
try {
295+
// Get repo name/URL
296+
const repoName = await this.ide.getRepoName(workspaceDir);
297+
if (repoName) {
298+
// If repo name looks like "owner/repo", convert to GitHub URL
299+
if (repoName.includes("/") && !repoName.startsWith("http")) {
300+
repoUrl = `https://github.com/${repoName}`;
301+
} else {
302+
repoUrl = repoName;
303+
}
304+
}
305+
306+
// Get current branch
307+
const branchInfo = await this.ide.getBranch(workspaceDir);
308+
if (branchInfo) {
309+
branch = branchInfo;
310+
}
311+
} catch (e) {
312+
console.error("Error getting repo info:", e);
313+
}
314+
315+
if (!repoUrl) {
316+
vscode.window.showErrorMessage(
317+
"Unable to determine repository URL. Make sure you're in a git repository.",
318+
);
319+
return;
320+
}
321+
322+
// Generate a name from the prompt (first 50 chars, cleaned up)
323+
let name = prompt.substring(0, 50).replace(/\n/g, " ").trim();
324+
if (prompt.length > 50) {
325+
name += "...";
326+
}
327+
// Fallback to a generic name if prompt is too short
328+
if (name.length < 3) {
329+
const repoName = await this.ide.getRepoName(workspaceDir);
330+
name = `Agent for ${repoName || "repository"}`;
331+
}
332+
333+
// debugger;
334+
335+
// Get the current agent configuration from the selected profile
336+
let agent: string | undefined;
337+
try {
338+
const currentProfile = configHandler.currentProfile;
339+
if (
340+
currentProfile &&
341+
currentProfile.profileDescription.profileType !== "local"
342+
) {
343+
// Encode the full slug to pass as the agent parameter
344+
agent = encodeFullSlug(currentProfile.profileDescription.fullSlug);
345+
}
346+
} catch (e) {
347+
console.error("Error getting agent configuration from profile:", e);
348+
// Continue without agent config - will use default
349+
}
350+
351+
// Create the background agent
352+
try {
353+
console.log("Creating background agent with:", {
354+
name,
355+
prompt: prompt.substring(0, 50) + "...",
356+
repoUrl,
357+
branch,
358+
contextItemsCount: contextItems?.length || 0,
359+
selectedCodeCount: selectedCode?.length || 0,
360+
agent: agent || "default",
361+
});
362+
363+
const result =
364+
await configHandler.controlPlaneClient.createBackgroundAgent(
365+
prompt,
366+
repoUrl,
367+
name,
368+
branch,
369+
organizationId,
370+
contextItems,
371+
selectedCode,
372+
agent,
373+
);
374+
375+
vscode.window.showInformationMessage(
376+
`Background agent created successfully! Agent ID: ${result.id}`,
377+
);
378+
} catch (e) {
379+
console.error("Failed to create background agent:", e);
380+
const errorMessage =
381+
e instanceof Error ? e.message : "Unknown error occurred";
382+
383+
// Check if this is a GitHub authorization error
384+
if (
385+
errorMessage.includes("GitHub token") ||
386+
errorMessage.includes("GitHub App")
387+
) {
388+
const selection = await vscode.window.showErrorMessage(
389+
"Background agents need GitHub access. Please connect your GitHub account to Continue.",
390+
"Connect GitHub",
391+
"Cancel",
392+
);
393+
394+
if (selection === "Connect GitHub") {
395+
await this.inProcessMessenger.externalRequest(
396+
"controlPlane/openUrl",
397+
{
398+
path: "settings/integrations",
399+
orgSlug: configHandler.currentOrg?.slug,
400+
},
401+
);
402+
}
403+
} else {
404+
vscode.window.showErrorMessage(
405+
`Failed to create background agent: ${errorMessage}`,
406+
);
407+
}
408+
}
409+
});
410+
411+
this.onWebview("listBackgroundAgents", async (msg) => {
412+
const configHandler = await configHandlerPromise;
413+
const { organizationId, limit } = msg.data;
414+
415+
try {
416+
const result =
417+
await configHandler.controlPlaneClient.listBackgroundAgents(
418+
organizationId,
419+
limit,
420+
);
421+
return result;
422+
} catch (e) {
423+
console.error("Error listing background agents:", e);
424+
return { agents: [], totalCount: 0 };
425+
}
426+
});
427+
266428
/** PASS THROUGH FROM WEBVIEW TO CORE AND BACK **/
267429
WEBVIEW_TO_CORE_PASS_THROUGH.forEach((messageType) => {
268430
this.onWebview(messageType, async (msg) => {

0 commit comments

Comments
 (0)