From a42f1ba74c670a05aab019a039a0da9aa37ed945 Mon Sep 17 00:00:00 2001 From: cheng Date: Mon, 13 Feb 2023 18:42:39 +0800 Subject: [PATCH 1/5] feat: multiple agent support --- src/commands.ts | 52 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 4db02219..fc41a65f 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -104,6 +104,7 @@ export class Commands { public async open(...args: string[]): Promise { let workspaceOwner: string let workspaceName: string + let workspaceAgent: string let folderPath: string | undefined if (args.length === 0) { @@ -158,24 +159,67 @@ export class Commands { workspaceOwner = workspace.owner_name workspaceName = workspace.name - // TODO: multiple agent support const agents = workspace.latest_build.resources.reduce((acc, resource) => { return acc.concat(resource.agents || []) }, [] as WorkspaceAgent[]) if (agents.length === 1) { folderPath = agents[0].expanded_directory - } + workspaceAgent = agents[0].name + }else{ + const agentQuickPick = vscode.window.createQuickPick() + let lastAgents: WorkspaceAgent[] + agentQuickPick.title = `Connect to a agent`; + + agentQuickPick.busy = true; + lastAgents = agents + const agentItems: vscode.QuickPickItem[] = agents.map((agent) => { + let icon = "$(debug-start)" + if (agent.status !== "connected") { + icon = "$(debug-stop)" + } + return { + alwaysShow: true, + label: `${icon} ${agent.name}`, + detail: `${agent.name} • Status: ${agent.status}`, + } + }) + agentQuickPick.items = agentItems + agentQuickPick.busy = false + agentQuickPick.show() + + const agent = await new Promise((resolve) => { + agentQuickPick.onDidHide(() => { + resolve(undefined) + }) + agentQuickPick.onDidChangeSelection((selected) => { + if (selected.length < 1) { + return resolve(undefined) + } + const agent = lastAgents[agentQuickPick.items.indexOf(selected[0])] + resolve(agent) + }) + }) + + if(agent != undefined){ + folderPath = agent.expanded_directory + workspaceAgent = agent.name + }else{ + folderPath = '' + workspaceAgent = '' + } + } + } else { workspaceOwner = args[0] workspaceName = args[1] - // workspaceAgent is reserved for args[2], but multiple agents aren't supported yet. + workspaceAgent = args[2] folderPath = args[3] } // A workspace can have multiple agents, but that's handled // when opening a workspace unless explicitly specified. - const remoteAuthority = `ssh-remote+${Remote.Prefix}${workspaceOwner}--${workspaceName}` + const remoteAuthority = `ssh-remote+${Remote.Prefix}${workspaceOwner}--${workspaceName}--${workspaceAgent}` let newWindow = true // Open in the existing window if no workspaces are open. From 5dba96c88a66a4f121c6fea1a5d20fe6aeecc4c8 Mon Sep 17 00:00:00 2001 From: ytcheng1 Date: Tue, 14 Feb 2023 09:04:05 +0800 Subject: [PATCH 2/5] Apply suggestions from code review Co-authored-by: Kyle Carberry --- src/commands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands.ts b/src/commands.ts index fc41a65f..1894b097 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -169,7 +169,7 @@ export class Commands { }else{ const agentQuickPick = vscode.window.createQuickPick() let lastAgents: WorkspaceAgent[] - agentQuickPick.title = `Connect to a agent`; + agentQuickPick.title = `Select an agent`; agentQuickPick.busy = true; lastAgents = agents From 5100372418627ba7380bcf81db6c45b12bd4335e Mon Sep 17 00:00:00 2001 From: cheng Date: Tue, 14 Feb 2023 10:23:16 +0800 Subject: [PATCH 3/5] feat: multiple agent support --- src/commands.ts | 84 ++++++++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 1894b097..98e2d196 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -166,50 +166,49 @@ export class Commands { if (agents.length === 1) { folderPath = agents[0].expanded_directory workspaceAgent = agents[0].name - }else{ - const agentQuickPick = vscode.window.createQuickPick() - let lastAgents: WorkspaceAgent[] - agentQuickPick.title = `Select an agent`; - - agentQuickPick.busy = true; - lastAgents = agents - const agentItems: vscode.QuickPickItem[] = agents.map((agent) => { - let icon = "$(debug-start)" - if (agent.status !== "connected") { - icon = "$(debug-stop)" - } - return { - alwaysShow: true, - label: `${icon} ${agent.name}`, - detail: `${agent.name} • Status: ${agent.status}`, - } + } else { + const agentQuickPick = vscode.window.createQuickPick() + let lastAgents: WorkspaceAgent[] + agentQuickPick.title = `Select an agent` + + agentQuickPick.busy = true + lastAgents = agents + const agentItems: vscode.QuickPickItem[] = agents.map((agent) => { + let icon = "$(debug-start)" + if (agent.status !== "connected") { + icon = "$(debug-stop)" + } + return { + alwaysShow: true, + label: `${icon} ${agent.name}`, + detail: `${agent.name} • Status: ${agent.status}`, + } + }) + agentQuickPick.items = agentItems + agentQuickPick.busy = false + agentQuickPick.show() + + const agent = await new Promise((resolve) => { + agentQuickPick.onDidHide(() => { + resolve(undefined) }) - agentQuickPick.items = agentItems - agentQuickPick.busy = false - agentQuickPick.show() - - const agent = await new Promise((resolve) => { - agentQuickPick.onDidHide(() => { - resolve(undefined) - }) - agentQuickPick.onDidChangeSelection((selected) => { - if (selected.length < 1) { - return resolve(undefined) - } - const agent = lastAgents[agentQuickPick.items.indexOf(selected[0])] - resolve(agent) - }) + agentQuickPick.onDidChangeSelection((selected) => { + if (selected.length < 1) { + return resolve(undefined) + } + const agent = lastAgents[agentQuickPick.items.indexOf(selected[0])] + resolve(agent) }) - - if(agent != undefined){ - folderPath = agent.expanded_directory - workspaceAgent = agent.name - }else{ - folderPath = '' - workspaceAgent = '' - } + }) + + if (agent != undefined) { + folderPath = agent.expanded_directory + workspaceAgent = agent.name + } else { + folderPath = "" + workspaceAgent = "" } - + } } else { workspaceOwner = args[0] workspaceName = args[1] @@ -219,7 +218,8 @@ export class Commands { // A workspace can have multiple agents, but that's handled // when opening a workspace unless explicitly specified. - const remoteAuthority = `ssh-remote+${Remote.Prefix}${workspaceOwner}--${workspaceName}--${workspaceAgent}` + let remoteAuthority = `ssh-remote+${Remote.Prefix}${workspaceOwner}--${workspaceName}` + if (workspaceAgent) remoteAuthority += `--${workspaceAgent}` let newWindow = true // Open in the existing window if no workspaces are open. From 6e121cc50f476db79aee0c6dea7af7b409529d56 Mon Sep 17 00:00:00 2001 From: Rodrigo Maia Date: Wed, 26 Apr 2023 18:31:47 -0300 Subject: [PATCH 4/5] chore: lint --- src/commands.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 98e2d196..d8e0226b 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -168,11 +168,10 @@ export class Commands { workspaceAgent = agents[0].name } else { const agentQuickPick = vscode.window.createQuickPick() - let lastAgents: WorkspaceAgent[] agentQuickPick.title = `Select an agent` agentQuickPick.busy = true - lastAgents = agents + const lastAgents = agents const agentItems: vscode.QuickPickItem[] = agents.map((agent) => { let icon = "$(debug-start)" if (agent.status !== "connected") { @@ -201,7 +200,7 @@ export class Commands { }) }) - if (agent != undefined) { + if (agent) { folderPath = agent.expanded_directory workspaceAgent = agent.name } else { @@ -219,7 +218,9 @@ export class Commands { // A workspace can have multiple agents, but that's handled // when opening a workspace unless explicitly specified. let remoteAuthority = `ssh-remote+${Remote.Prefix}${workspaceOwner}--${workspaceName}` - if (workspaceAgent) remoteAuthority += `--${workspaceAgent}` + if (workspaceAgent) { + remoteAuthority += `--${workspaceAgent}` + } let newWindow = true // Open in the existing window if no workspaces are open. From e40d8761c4b32756e9b1fdddd9bc056355553fb4 Mon Sep 17 00:00:00 2001 From: Rodrigo Maia Date: Fri, 28 Apr 2023 18:27:57 -0300 Subject: [PATCH 5/5] chore: updating sidebar to also support multiple agents --- package.json | 15 ++-- src/api-helper.ts | 11 +-- src/commands.ts | 156 +++++++++++++++++++++----------------- src/extension.ts | 1 + src/workspacesProvider.ts | 43 +++++++++-- 5 files changed, 136 insertions(+), 90 deletions(-) diff --git a/package.json b/package.json index 4b72504a..b3528d96 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,11 @@ "title": "Coder: Open Workspace", "icon": "$(play)" }, + { + "command": "coder.openFromSidebar", + "title": "Coder: Open Workspace", + "icon": "$(play)" + }, { "command": "coder.createWorkspace", "title": "Create Workspace", @@ -147,18 +152,18 @@ ], "view/item/context": [ { - "command": "coder.open", - "when": "coder.authenticated && view == myWorkspaces || coder.authenticated && view == allWorkspaces", + "command": "coder.openFromSidebar", + "when": "coder.authenticated && viewItem == coderWorkspaceSingleAgent || coder.authenticated && viewItem == coderAgent", "group": "inline" }, { "command": "coder.navigateToWorkspace", - "when": "coder.authenticated && view == myWorkspaces || coder.authenticated && view == allWorkspaces", + "when": "coder.authenticated && viewItem == coderWorkspaceSingleAgent || coder.authenticated && viewItem == coderWorkspaceMultipleAgents", "group": "inline" }, { "command": "coder.navigateToWorkspaceSettings", - "when": "coder.authenticated && view == myWorkspaces || coder.authenticated && view == allWorkspaces", + "when": "coder.authenticated && viewItem == coderWorkspaceSingleAgent || coder.authenticated && viewItem == coderWorkspaceMultipleAgents", "group": "inline" } ] @@ -223,4 +228,4 @@ "ws": "^8.11.0", "yaml": "^1.10.0" } -} \ No newline at end of file +} diff --git a/src/api-helper.ts b/src/api-helper.ts index 75c0af83..ea36a3b3 100644 --- a/src/api-helper.ts +++ b/src/api-helper.ts @@ -1,16 +1,9 @@ import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated" -export function extractAgentsAndFolderPath( - workspace: Workspace, -): [agents: WorkspaceAgent[], folderPath: string | undefined] { - // TODO: multiple agent support +export function extractAgents(workspace: Workspace): WorkspaceAgent[] { const agents = workspace.latest_build.resources.reduce((acc, resource) => { return acc.concat(resource.agents || []) }, [] as WorkspaceAgent[]) - let folderPath = undefined - if (agents.length === 1) { - folderPath = agents[0].expanded_directory - } - return [agents, folderPath] + return agents } diff --git a/src/commands.ts b/src/commands.ts index efca5528..9995e8bf 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -143,6 +143,17 @@ export class Commands { } } + public async openFromSidebar(treeItem: WorkspaceTreeItem) { + if (treeItem) { + await openWorkspace( + treeItem.workspaceOwner, + treeItem.workspaceName, + treeItem.workspaceAgent, + treeItem.workspaceFolderPath, + ) + } + } + public async open(...args: unknown[]): Promise { let workspaceOwner: string let workspaceName: string @@ -248,12 +259,6 @@ export class Commands { workspaceAgent = "" } } - } else if (args.length === 2) { - // opening a workspace from the sidebar - const workspaceTreeItem = args[0] as WorkspaceTreeItem - workspaceOwner = workspaceTreeItem.workspaceOwner - workspaceName = workspaceTreeItem.workspaceName - folderPath = workspaceTreeItem.workspaceFolderPath } else { workspaceOwner = args[0] as string workspaceName = args[1] as string @@ -261,71 +266,7 @@ export class Commands { folderPath = args[3] as string | undefined } - // A workspace can have multiple agents, but that's handled - // when opening a workspace unless explicitly specified. - let remoteAuthority = `ssh-remote+${Remote.Prefix}${workspaceOwner}--${workspaceName}` - if (workspaceAgent) { - remoteAuthority += `--${workspaceAgent}` - } - - let newWindow = true - // Open in the existing window if no workspaces are open. - if (!vscode.workspace.workspaceFolders?.length) { - newWindow = false - } - - // If a folder isn't specified, we can try to open a recently opened folder. - if (!folderPath) { - const output: { - workspaces: { folderUri: vscode.Uri; remoteAuthority: string }[] - } = await vscode.commands.executeCommand("_workbench.getRecentlyOpened") - const opened = output.workspaces.filter( - // Filter out `/` since that's added below. - (opened) => opened.folderUri?.authority === remoteAuthority, - ) - if (opened.length > 0) { - let selected: (typeof opened)[0] - - if (opened.length > 1) { - const items: vscode.QuickPickItem[] = opened.map((folder): vscode.QuickPickItem => { - return { - label: folder.folderUri.path, - } - }) - const item = await vscode.window.showQuickPick(items, { - title: "Select a recently opened folder", - }) - if (!item) { - return - } - selected = opened[items.indexOf(item)] - } else { - selected = opened[0] - } - - folderPath = selected.folderUri.path - } - } - - if (folderPath) { - await vscode.commands.executeCommand( - "vscode.openFolder", - vscode.Uri.from({ - scheme: "vscode-remote", - authority: remoteAuthority, - path: folderPath, - }), - // Open this in a new window! - newWindow, - ) - return - } - - // This opens the workspace without an active folder opened. - await vscode.commands.executeCommand("vscode.newWindow", { - remoteAuthority: remoteAuthority, - reuseWindow: !newWindow, - }) + await openWorkspace(workspaceOwner, workspaceName, workspaceAgent, folderPath) } public async updateWorkspace(): Promise { @@ -346,3 +287,76 @@ export class Commands { } } } + +async function openWorkspace( + workspaceOwner: string, + workspaceName: string, + workspaceAgent: string | undefined, + folderPath: string | undefined, +) { + // A workspace can have multiple agents, but that's handled + // when opening a workspace unless explicitly specified. + let remoteAuthority = `ssh-remote+${Remote.Prefix}${workspaceOwner}--${workspaceName}` + if (workspaceAgent) { + remoteAuthority += `--${workspaceAgent}` + } + + let newWindow = true + // Open in the existing window if no workspaces are open. + if (!vscode.workspace.workspaceFolders?.length) { + newWindow = false + } + + // If a folder isn't specified, we can try to open a recently opened folder. + if (!folderPath) { + const output: { + workspaces: { folderUri: vscode.Uri; remoteAuthority: string }[] + } = await vscode.commands.executeCommand("_workbench.getRecentlyOpened") + const opened = output.workspaces.filter( + // Filter out `/` since that's added below. + (opened) => opened.folderUri?.authority === remoteAuthority, + ) + if (opened.length > 0) { + let selected: (typeof opened)[0] + + if (opened.length > 1) { + const items: vscode.QuickPickItem[] = opened.map((folder): vscode.QuickPickItem => { + return { + label: folder.folderUri.path, + } + }) + const item = await vscode.window.showQuickPick(items, { + title: "Select a recently opened folder", + }) + if (!item) { + return + } + selected = opened[items.indexOf(item)] + } else { + selected = opened[0] + } + + folderPath = selected.folderUri.path + } + } + + if (folderPath) { + await vscode.commands.executeCommand( + "vscode.openFolder", + vscode.Uri.from({ + scheme: "vscode-remote", + authority: remoteAuthority, + path: folderPath, + }), + // Open this in a new window! + newWindow, + ) + return + } + + // This opens the workspace without an active folder opened. + await vscode.commands.executeCommand("vscode.newWindow", { + remoteAuthority: remoteAuthority, + reuseWindow: !newWindow, + }) +} diff --git a/src/extension.ts b/src/extension.ts index 7131dd95..b5f02c9a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -87,6 +87,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { vscode.commands.registerCommand("coder.login", commands.login.bind(commands)) vscode.commands.registerCommand("coder.logout", commands.logout.bind(commands)) vscode.commands.registerCommand("coder.open", commands.open.bind(commands)) + vscode.commands.registerCommand("coder.openFromSidebar", commands.openFromSidebar.bind(commands)) vscode.commands.registerCommand("coder.workspace.update", commands.updateWorkspace.bind(commands)) vscode.commands.registerCommand("coder.createWorkspace", commands.createWorkspace.bind(commands)) vscode.commands.registerCommand("coder.navigateToWorkspace", commands.navigateToWorkspace.bind(commands)) diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index f09b29e4..5cdee575 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -1,7 +1,8 @@ import { getWorkspaces } from "coder/site/src/api/api" +import { WorkspaceAgent } from "coder/site/src/api/typesGenerated" import * as path from "path" import * as vscode from "vscode" -import { extractAgentsAndFolderPath } from "./api-helper" +import { extractAgents } from "./api-helper" export enum WorkspaceQuery { Mine = "owner:me", @@ -24,7 +25,19 @@ export class WorkspaceProvider implements vscode.TreeDataProvider { + getChildren(element?: WorkspaceTreeItem): Thenable { + if (element) { + if (element.agents.length > 0) { + return Promise.resolve( + element.agents.map((agent) => { + const label = agent.name + const detail = `Status: ${agent.status}` + return new WorkspaceTreeItem(label, detail, "", "", agent.name, agent.expanded_directory, [], "coderAgent") + }), + ) + } + return Promise.resolve([]) + } return getWorkspaces({ q: this.getWorkspacesQuery }).then((workspaces) => { return workspaces.workspaces.map((workspace) => { const status = @@ -35,22 +48,42 @@ export class WorkspaceProvider implements vscode.TreeDataProvider 1 ? "coderWorkspaceMultipleAgents" : "coderWorkspaceSingleAgent", + ) }) }) } } +type CoderTreeItemType = "coderWorkspaceSingleAgent" | "coderWorkspaceMultipleAgents" | "coderAgent" + export class WorkspaceTreeItem extends vscode.TreeItem { constructor( public readonly label: string, public readonly tooltip: string, public readonly workspaceOwner: string, public readonly workspaceName: string, + public readonly workspaceAgent: string | undefined, public readonly workspaceFolderPath: string | undefined, + public readonly agents: WorkspaceAgent[], + contextValue: CoderTreeItemType, ) { - super(label, vscode.TreeItemCollapsibleState.None) + super( + label, + contextValue === "coderWorkspaceMultipleAgents" + ? vscode.TreeItemCollapsibleState.Collapsed + : vscode.TreeItemCollapsibleState.None, + ) + this.contextValue = contextValue } iconPath = {