Skip to content

Commit 4662ebd

Browse files
ytchengkylecarbsrodrimaia
authored
feat: multiple agent support (#59)
Co-authored-by: Kyle Carberry <kyle@carberry.com> Co-authored-by: Rodrigo Maia <rodrigo.maia.pereira@gmail.com>
1 parent 5a54059 commit 4662ebd

File tree

5 files changed

+183
-88
lines changed

5 files changed

+183
-88
lines changed

package.json

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@
9494
"title": "Coder: Open Workspace",
9595
"icon": "$(play)"
9696
},
97+
{
98+
"command": "coder.openFromSidebar",
99+
"title": "Coder: Open Workspace",
100+
"icon": "$(play)"
101+
},
97102
{
98103
"command": "coder.createWorkspace",
99104
"title": "Create Workspace",
@@ -147,18 +152,18 @@
147152
],
148153
"view/item/context": [
149154
{
150-
"command": "coder.open",
151-
"when": "coder.authenticated && view == myWorkspaces || coder.authenticated && view == allWorkspaces",
155+
"command": "coder.openFromSidebar",
156+
"when": "coder.authenticated && viewItem == coderWorkspaceSingleAgent || coder.authenticated && viewItem == coderAgent",
152157
"group": "inline"
153158
},
154159
{
155160
"command": "coder.navigateToWorkspace",
156-
"when": "coder.authenticated && view == myWorkspaces || coder.authenticated && view == allWorkspaces",
161+
"when": "coder.authenticated && viewItem == coderWorkspaceSingleAgent || coder.authenticated && viewItem == coderWorkspaceMultipleAgents",
157162
"group": "inline"
158163
},
159164
{
160165
"command": "coder.navigateToWorkspaceSettings",
161-
"when": "coder.authenticated && view == myWorkspaces || coder.authenticated && view == allWorkspaces",
166+
"when": "coder.authenticated && viewItem == coderWorkspaceSingleAgent || coder.authenticated && viewItem == coderWorkspaceMultipleAgents",
162167
"group": "inline"
163168
}
164169
]
@@ -223,4 +228,4 @@
223228
"ws": "^8.11.0",
224229
"yaml": "^1.10.0"
225230
}
226-
}
231+
}

src/api-helper.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
11
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
22

3-
export function extractAgentsAndFolderPath(
4-
workspace: Workspace,
5-
): [agents: WorkspaceAgent[], folderPath: string | undefined] {
6-
// TODO: multiple agent support
3+
export function extractAgents(workspace: Workspace): WorkspaceAgent[] {
74
const agents = workspace.latest_build.resources.reduce((acc, resource) => {
85
return acc.concat(resource.agents || [])
96
}, [] as WorkspaceAgent[])
107

11-
let folderPath = undefined
12-
if (agents.length === 1) {
13-
folderPath = agents[0].expanded_directory
14-
}
15-
return [agents, folderPath]
8+
return agents
169
}

src/commands.ts

Lines changed: 132 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import axios from "axios"
22
import { getAuthenticatedUser, getWorkspaces, updateWorkspaceVersion } from "coder/site/src/api/api"
3-
import { Workspace } from "coder/site/src/api/typesGenerated"
3+
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
44
import * as vscode from "vscode"
5-
import { extractAgentsAndFolderPath } from "./api-helper"
5+
import { extractAgents } from "./api-helper"
66
import { Remote } from "./remote"
77
import { Storage } from "./storage"
88
import { WorkspaceTreeItem } from "./workspacesProvider"
@@ -143,9 +143,21 @@ export class Commands {
143143
}
144144
}
145145

146+
public async openFromSidebar(treeItem: WorkspaceTreeItem) {
147+
if (treeItem) {
148+
await openWorkspace(
149+
treeItem.workspaceOwner,
150+
treeItem.workspaceName,
151+
treeItem.workspaceAgent,
152+
treeItem.workspaceFolderPath,
153+
)
154+
}
155+
}
156+
146157
public async open(...args: unknown[]): Promise<void> {
147158
let workspaceOwner: string
148159
let workspaceName: string
160+
let workspaceAgent: string | undefined
149161
let folderPath: string | undefined
150162

151163
if (args.length === 0) {
@@ -200,83 +212,61 @@ export class Commands {
200212
workspaceOwner = workspace.owner_name
201213
workspaceName = workspace.name
202214

203-
const [, folderPathExtracted] = extractAgentsAndFolderPath(workspace)
204-
folderPath = folderPathExtracted
205-
} else if (args.length === 2) {
206-
// opening a workspace from the sidebar
207-
const workspaceTreeItem = args[0] as WorkspaceTreeItem
208-
workspaceOwner = workspaceTreeItem.workspaceOwner
209-
workspaceName = workspaceTreeItem.workspaceName
210-
folderPath = workspaceTreeItem.workspaceFolderPath
211-
} else {
212-
workspaceOwner = args[0] as string
213-
workspaceName = args[1] as string
214-
// workspaceAgent is reserved for args[2], but multiple agents aren't supported yet.
215-
folderPath = args[3] as string | undefined
216-
}
217-
218-
// A workspace can have multiple agents, but that's handled
219-
// when opening a workspace unless explicitly specified.
220-
const remoteAuthority = `ssh-remote+${Remote.Prefix}${workspaceOwner}--${workspaceName}`
215+
const agents = extractAgents(workspace)
221216

222-
let newWindow = true
223-
// Open in the existing window if no workspaces are open.
224-
if (!vscode.workspace.workspaceFolders?.length) {
225-
newWindow = false
226-
}
217+
if (agents.length === 1) {
218+
folderPath = agents[0].expanded_directory
219+
workspaceAgent = agents[0].name
220+
} else {
221+
const agentQuickPick = vscode.window.createQuickPick()
222+
agentQuickPick.title = `Select an agent`
227223

228-
// If a folder isn't specified, we can try to open a recently opened folder.
229-
if (!folderPath) {
230-
const output: {
231-
workspaces: { folderUri: vscode.Uri; remoteAuthority: string }[]
232-
} = await vscode.commands.executeCommand("_workbench.getRecentlyOpened")
233-
const opened = output.workspaces.filter(
234-
// Filter out `/` since that's added below.
235-
(opened) => opened.folderUri?.authority === remoteAuthority,
236-
)
237-
if (opened.length > 0) {
238-
let selected: (typeof opened)[0]
224+
agentQuickPick.busy = true
225+
const lastAgents = agents
226+
const agentItems: vscode.QuickPickItem[] = agents.map((agent) => {
227+
let icon = "$(debug-start)"
228+
if (agent.status !== "connected") {
229+
icon = "$(debug-stop)"
230+
}
231+
return {
232+
alwaysShow: true,
233+
label: `${icon} ${agent.name}`,
234+
detail: `${agent.name} • Status: ${agent.status}`,
235+
}
236+
})
237+
agentQuickPick.items = agentItems
238+
agentQuickPick.busy = false
239+
agentQuickPick.show()
239240

240-
if (opened.length > 1) {
241-
const items: vscode.QuickPickItem[] = opened.map((folder): vscode.QuickPickItem => {
242-
return {
243-
label: folder.folderUri.path,
244-
}
241+
const agent = await new Promise<WorkspaceAgent | undefined>((resolve) => {
242+
agentQuickPick.onDidHide(() => {
243+
resolve(undefined)
245244
})
246-
const item = await vscode.window.showQuickPick(items, {
247-
title: "Select a recently opened folder",
245+
agentQuickPick.onDidChangeSelection((selected) => {
246+
if (selected.length < 1) {
247+
return resolve(undefined)
248+
}
249+
const agent = lastAgents[agentQuickPick.items.indexOf(selected[0])]
250+
resolve(agent)
248251
})
249-
if (!item) {
250-
return
251-
}
252-
selected = opened[items.indexOf(item)]
252+
})
253+
254+
if (agent) {
255+
folderPath = agent.expanded_directory
256+
workspaceAgent = agent.name
253257
} else {
254-
selected = opened[0]
258+
folderPath = ""
259+
workspaceAgent = ""
255260
}
256-
257-
folderPath = selected.folderUri.path
258261
}
262+
} else {
263+
workspaceOwner = args[0] as string
264+
workspaceName = args[1] as string
265+
// workspaceAgent is reserved for args[2], but multiple agents aren't supported yet.
266+
folderPath = args[3] as string | undefined
259267
}
260268

261-
if (folderPath) {
262-
await vscode.commands.executeCommand(
263-
"vscode.openFolder",
264-
vscode.Uri.from({
265-
scheme: "vscode-remote",
266-
authority: remoteAuthority,
267-
path: folderPath,
268-
}),
269-
// Open this in a new window!
270-
newWindow,
271-
)
272-
return
273-
}
274-
275-
// This opens the workspace without an active folder opened.
276-
await vscode.commands.executeCommand("vscode.newWindow", {
277-
remoteAuthority: remoteAuthority,
278-
reuseWindow: !newWindow,
279-
})
269+
await openWorkspace(workspaceOwner, workspaceName, workspaceAgent, folderPath)
280270
}
281271

282272
public async updateWorkspace(): Promise<void> {
@@ -297,3 +287,76 @@ export class Commands {
297287
}
298288
}
299289
}
290+
291+
async function openWorkspace(
292+
workspaceOwner: string,
293+
workspaceName: string,
294+
workspaceAgent: string | undefined,
295+
folderPath: string | undefined,
296+
) {
297+
// A workspace can have multiple agents, but that's handled
298+
// when opening a workspace unless explicitly specified.
299+
let remoteAuthority = `ssh-remote+${Remote.Prefix}${workspaceOwner}--${workspaceName}`
300+
if (workspaceAgent) {
301+
remoteAuthority += `--${workspaceAgent}`
302+
}
303+
304+
let newWindow = true
305+
// Open in the existing window if no workspaces are open.
306+
if (!vscode.workspace.workspaceFolders?.length) {
307+
newWindow = false
308+
}
309+
310+
// If a folder isn't specified, we can try to open a recently opened folder.
311+
if (!folderPath) {
312+
const output: {
313+
workspaces: { folderUri: vscode.Uri; remoteAuthority: string }[]
314+
} = await vscode.commands.executeCommand("_workbench.getRecentlyOpened")
315+
const opened = output.workspaces.filter(
316+
// Filter out `/` since that's added below.
317+
(opened) => opened.folderUri?.authority === remoteAuthority,
318+
)
319+
if (opened.length > 0) {
320+
let selected: (typeof opened)[0]
321+
322+
if (opened.length > 1) {
323+
const items: vscode.QuickPickItem[] = opened.map((folder): vscode.QuickPickItem => {
324+
return {
325+
label: folder.folderUri.path,
326+
}
327+
})
328+
const item = await vscode.window.showQuickPick(items, {
329+
title: "Select a recently opened folder",
330+
})
331+
if (!item) {
332+
return
333+
}
334+
selected = opened[items.indexOf(item)]
335+
} else {
336+
selected = opened[0]
337+
}
338+
339+
folderPath = selected.folderUri.path
340+
}
341+
}
342+
343+
if (folderPath) {
344+
await vscode.commands.executeCommand(
345+
"vscode.openFolder",
346+
vscode.Uri.from({
347+
scheme: "vscode-remote",
348+
authority: remoteAuthority,
349+
path: folderPath,
350+
}),
351+
// Open this in a new window!
352+
newWindow,
353+
)
354+
return
355+
}
356+
357+
// This opens the workspace without an active folder opened.
358+
await vscode.commands.executeCommand("vscode.newWindow", {
359+
remoteAuthority: remoteAuthority,
360+
reuseWindow: !newWindow,
361+
})
362+
}

src/extension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
8787
vscode.commands.registerCommand("coder.login", commands.login.bind(commands))
8888
vscode.commands.registerCommand("coder.logout", commands.logout.bind(commands))
8989
vscode.commands.registerCommand("coder.open", commands.open.bind(commands))
90+
vscode.commands.registerCommand("coder.openFromSidebar", commands.openFromSidebar.bind(commands))
9091
vscode.commands.registerCommand("coder.workspace.update", commands.updateWorkspace.bind(commands))
9192
vscode.commands.registerCommand("coder.createWorkspace", commands.createWorkspace.bind(commands))
9293
vscode.commands.registerCommand("coder.navigateToWorkspace", commands.navigateToWorkspace.bind(commands))

src/workspacesProvider.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { getWorkspaces } from "coder/site/src/api/api"
2+
import { WorkspaceAgent } from "coder/site/src/api/typesGenerated"
23
import * as path from "path"
34
import * as vscode from "vscode"
4-
import { extractAgentsAndFolderPath } from "./api-helper"
5+
import { extractAgents } from "./api-helper"
56

67
export enum WorkspaceQuery {
78
Mine = "owner:me",
@@ -24,7 +25,19 @@ export class WorkspaceProvider implements vscode.TreeDataProvider<WorkspaceTreeI
2425
return element
2526
}
2627

27-
getChildren(): Thenable<WorkspaceTreeItem[]> {
28+
getChildren(element?: WorkspaceTreeItem): Thenable<WorkspaceTreeItem[]> {
29+
if (element) {
30+
if (element.agents.length > 0) {
31+
return Promise.resolve(
32+
element.agents.map((agent) => {
33+
const label = agent.name
34+
const detail = `Status: ${agent.status}`
35+
return new WorkspaceTreeItem(label, detail, "", "", agent.name, agent.expanded_directory, [], "coderAgent")
36+
}),
37+
)
38+
}
39+
return Promise.resolve([])
40+
}
2841
return getWorkspaces({ q: this.getWorkspacesQuery }).then((workspaces) => {
2942
return workspaces.workspaces.map((workspace) => {
3043
const status =
@@ -35,22 +48,42 @@ export class WorkspaceProvider implements vscode.TreeDataProvider<WorkspaceTreeI
3548
? `${workspace.owner_name} / ${workspace.name}`
3649
: workspace.name
3750
const detail = `Template: ${workspace.template_display_name || workspace.template_name} • Status: ${status}`
38-
const [, folderPath] = extractAgentsAndFolderPath(workspace)
39-
return new WorkspaceTreeItem(label, detail, workspace.owner_name, workspace.name, folderPath)
51+
const agents = extractAgents(workspace)
52+
return new WorkspaceTreeItem(
53+
label,
54+
detail,
55+
workspace.owner_name,
56+
workspace.name,
57+
undefined,
58+
agents[0]?.expanded_directory,
59+
agents,
60+
agents.length > 1 ? "coderWorkspaceMultipleAgents" : "coderWorkspaceSingleAgent",
61+
)
4062
})
4163
})
4264
}
4365
}
4466

67+
type CoderTreeItemType = "coderWorkspaceSingleAgent" | "coderWorkspaceMultipleAgents" | "coderAgent"
68+
4569
export class WorkspaceTreeItem extends vscode.TreeItem {
4670
constructor(
4771
public readonly label: string,
4872
public readonly tooltip: string,
4973
public readonly workspaceOwner: string,
5074
public readonly workspaceName: string,
75+
public readonly workspaceAgent: string | undefined,
5176
public readonly workspaceFolderPath: string | undefined,
77+
public readonly agents: WorkspaceAgent[],
78+
contextValue: CoderTreeItemType,
5279
) {
53-
super(label, vscode.TreeItemCollapsibleState.None)
80+
super(
81+
label,
82+
contextValue === "coderWorkspaceMultipleAgents"
83+
? vscode.TreeItemCollapsibleState.Collapsed
84+
: vscode.TreeItemCollapsibleState.None,
85+
)
86+
this.contextValue = contextValue
5487
}
5588

5689
iconPath = {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy