Skip to content

Commit f3209ea

Browse files
Merge pull request #8270 from continuedev/tomasz/con-4212
Build a local takeover MVP
2 parents 721d4c4 + 66be965 commit f3209ea

File tree

11 files changed

+1231
-54
lines changed

11 files changed

+1231
-54
lines changed

core/control-plane/client.ts

Lines changed: 108 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,39 @@ export interface RemoteSessionMetadata extends BaseSessionMetadata {
5454
remoteId: string;
5555
}
5656

57+
export interface AgentSessionMetadata {
58+
createdBy: string;
59+
github_repo: string;
60+
organizationId?: string;
61+
idempotencyKey?: string;
62+
source?: string;
63+
continueApiKeyId?: string;
64+
s3Url?: string;
65+
prompt?: string | null;
66+
createdBySlug?: string;
67+
}
68+
69+
export interface AgentSessionView {
70+
id: string;
71+
devboxId: string | null;
72+
name: string | null;
73+
icon: string | null;
74+
status: string;
75+
agentStatus: string | null;
76+
unread: boolean;
77+
state: string;
78+
metadata: AgentSessionMetadata;
79+
repoUrl: string;
80+
branch: string | null;
81+
pullRequestUrl: string | null;
82+
pullRequestStatus: string | null;
83+
tunnelUrl: string | null;
84+
createdAt: string;
85+
updatedAt: string;
86+
create_time_ms: string;
87+
end_time_ms: string;
88+
}
89+
5790
export class ControlPlaneClient {
5891
constructor(
5992
readonly sessionInfoPromise: Promise<ControlPlaneSessionInfo | undefined>,
@@ -545,23 +578,19 @@ export class ControlPlaneClient {
545578
});
546579

547580
const result = (await resp.json()) as {
548-
agents: any[];
581+
agents: AgentSessionView[];
549582
totalCount: number;
550583
};
551584

552585
return {
553-
agents: result.agents.map((agent: any) => ({
586+
agents: result.agents.map((agent) => ({
554587
id: agent.id,
555-
name: agent.name || agent.metadata?.name || null,
588+
name: agent.name,
556589
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(),
590+
repoUrl: agent.repoUrl,
591+
createdAt: agent.createdAt,
562592
metadata: {
563-
github_repo:
564-
agent.metadata?.github_repo || agent.metadata?.repo_url,
593+
github_repo: agent.metadata.github_repo,
565594
},
566595
})),
567596
totalCount: result.totalCount,
@@ -573,4 +602,73 @@ export class ControlPlaneClient {
573602
return { agents: [], totalCount: 0 };
574603
}
575604
}
605+
606+
/**
607+
* Get the full agent session information
608+
* @param agentSessionId - The ID of the agent session
609+
* @returns The agent session view including metadata and status
610+
*/
611+
public async getAgentSession(
612+
agentSessionId: string,
613+
): Promise<AgentSessionView | null> {
614+
if (!(await this.isSignedIn())) {
615+
return null;
616+
}
617+
618+
try {
619+
const resp = await this.requestAndHandleError(
620+
`agents/${agentSessionId}`,
621+
{
622+
method: "GET",
623+
},
624+
);
625+
626+
return (await resp.json()) as AgentSessionView;
627+
} catch (e) {
628+
Logger.error(e, {
629+
context: "control_plane_get_agent_session",
630+
agentSessionId,
631+
});
632+
return null;
633+
}
634+
}
635+
636+
/**
637+
* Get the state of a specific background agent
638+
* @param agentSessionId - The ID of the agent session
639+
* @returns The agent's session state including history, workspace, and branch
640+
*/
641+
public async getAgentState(agentSessionId: string): Promise<{
642+
session: Session;
643+
isProcessing: boolean;
644+
messageQueueLength: number;
645+
pendingPermission: any;
646+
} | null> {
647+
if (!(await this.isSignedIn())) {
648+
return null;
649+
}
650+
651+
try {
652+
const resp = await this.requestAndHandleError(
653+
`agents/${agentSessionId}/state`,
654+
{
655+
method: "GET",
656+
},
657+
);
658+
659+
const result = (await resp.json()) as {
660+
session: Session;
661+
isProcessing: boolean;
662+
messageQueueLength: number;
663+
pendingPermission: any;
664+
};
665+
return result;
666+
} catch (e) {
667+
Logger.error(e, {
668+
context: "control_plane_get_agent_state",
669+
agentSessionId,
670+
});
671+
return null;
672+
}
673+
}
576674
}

core/protocol/ideWebview.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ export type ToIdeFromWebviewProtocol = ToIdeFromWebviewOrCoreProtocol & {
7878
totalCount: number;
7979
},
8080
];
81+
openAgentLocally: [
82+
{
83+
agentSessionId: string;
84+
},
85+
void,
86+
];
8187
};
8288

8389
export type ToWebviewFromIdeProtocol = ToWebviewFromIdeOrCoreProtocol & {
@@ -94,6 +100,7 @@ export type ToWebviewFromIdeProtocol = ToWebviewFromIdeOrCoreProtocol & {
94100

95101
focusContinueSessionId: [{ sessionId: string | undefined }, void];
96102
newSession: [undefined, void];
103+
loadAgentSession: [{ session: any }, void];
97104
setTheme: [{ theme: any }, void];
98105
setColors: [{ [key: string]: string }, void];
99106
"jetbrains/editorInsetRefresh": [undefined, void];

core/util/repoUrl.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Utility functions for normalizing and handling repository URLs.
3+
*/
4+
5+
/**
6+
* Normalizes a repository URL to a consistent format.
7+
*
8+
* Handles various Git URL formats and converts them to a standard HTTPS GitHub URL:
9+
* - SSH format: `[email protected]:owner/repo.git` → `https://github.com/owner/repo`
10+
* - SSH protocol: `ssh://[email protected]/owner/repo.git` → `https://github.com/owner/repo`
11+
* - Shorthand: `owner/repo` → `https://github.com/owner/repo`
12+
* - Removes `.git` suffix
13+
* - Removes trailing slashes
14+
* - Normalizes to lowercase
15+
*
16+
* @param url - The repository URL to normalize
17+
* @returns The normalized repository URL in lowercase HTTPS format
18+
*
19+
* @example
20+
* ```typescript
21+
* normalizeRepoUrl("[email protected]:owner/repo.git")
22+
* // Returns: "https://github.com/owner/repo"
23+
*
24+
* normalizeRepoUrl("owner/repo")
25+
* // Returns: "https://github.com/owner/repo"
26+
*
27+
* normalizeRepoUrl("https://github.com/Owner/Repo.git")
28+
* // Returns: "https://github.com/owner/repo"
29+
* ```
30+
*/
31+
export function normalizeRepoUrl(url: string): string {
32+
if (!url) return "";
33+
34+
let normalized = url.trim();
35+
36+
// Convert SSH to HTTPS: git@github.com:owner/repo.git -> https://github.com/owner/repo
37+
if (normalized.startsWith("[email protected]:")) {
38+
normalized = normalized.replace("[email protected]:", "https://github.com/");
39+
}
40+
41+
// Convert SSH protocol to HTTPS: ssh://git@github.com/owner/repo.git -> https://github.com/owner/repo
42+
// Also handles: ssh://git@github.com:owner/repo.git (less common)
43+
if (normalized.startsWith("ssh://[email protected]")) {
44+
normalized = normalized
45+
.replace("ssh://[email protected]/", "https://github.com/")
46+
.replace("ssh://[email protected]:", "https://github.com/");
47+
}
48+
49+
// Convert shorthand owner/repo to full URL
50+
if (
51+
normalized.includes("/") &&
52+
!/^[a-z]+:\/\//i.test(normalized) &&
53+
!normalized.startsWith("git@")
54+
) {
55+
normalized = `https://github.com/${normalized}`;
56+
}
57+
58+
// Remove trailing slash before removing .git suffix
59+
if (normalized.endsWith("/")) {
60+
normalized = normalized.slice(0, -1);
61+
}
62+
63+
// Remove .git suffix
64+
if (normalized.endsWith(".git")) {
65+
normalized = normalized.slice(0, -4);
66+
}
67+
68+
// Normalize to lowercase
69+
return normalized.toLowerCase();
70+
}

0 commit comments

Comments
 (0)