Skip to content

Commit 77e3f0d

Browse files
authored
Bcpeinhardt/ai agent session in vscode (#488)
1 parent 7dae71d commit 77e3f0d

File tree

6 files changed

+191
-6
lines changed

6 files changed

+191
-6
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## Unreleased
44

5+
### Added
6+
7+
- Coder extension sidebar now displays available app statuses, and let's
8+
the user click them to drop into a session with a running AI Agent.
9+
510
## [v1.7.1](https://github.com/coder/vscode-coder/releases/tag/v1.7.1) (2025-04-14)
611

712
### Fixed

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,12 @@
204204
"title": "Coder: View Logs",
205205
"icon": "$(list-unordered)",
206206
"when": "coder.authenticated"
207+
},
208+
{
209+
"command": "coder.openAppStatus",
210+
"title": "Coder: Open App Status",
211+
"icon": "$(robot)",
212+
"when": "coder.authenticated"
207213
}
208214
],
209215
"menus": {

src/commands.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Api } from "coder/site/src/api/api"
22
import { getErrorMessage } from "coder/site/src/api/errors"
33
import { User, Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
4+
import path from "node:path"
45
import * as vscode from "vscode"
56
import { makeCoderSdk, needToken } from "./api"
67
import { extractAgents } from "./api-helper"
@@ -407,6 +408,63 @@ export class Commands {
407408
}
408409
}
409410

411+
public async openAppStatus(app: {
412+
name?: string
413+
url?: string
414+
agent_name?: string
415+
command?: string
416+
workspace_name: string
417+
}): Promise<void> {
418+
// Launch and run command in terminal if command is provided
419+
if (app.command) {
420+
return vscode.window.withProgress(
421+
{
422+
location: vscode.ProgressLocation.Notification,
423+
title: `Connecting to AI Agent...`,
424+
cancellable: false,
425+
},
426+
async () => {
427+
const terminal = vscode.window.createTerminal(app.name)
428+
429+
// If workspace_name is provided, run coder ssh before the command
430+
431+
const url = this.storage.getUrl()
432+
if (!url) {
433+
throw new Error("No coder url found for sidebar")
434+
}
435+
const binary = await this.storage.fetchBinary(this.restClient, toSafeHost(url))
436+
const escape = (str: string): string => `"${str.replace(/"/g, '\\"')}"`
437+
terminal.sendText(
438+
`${escape(binary)} ssh --global-config ${escape(
439+
path.dirname(this.storage.getSessionTokenPath(toSafeHost(url))),
440+
)} ${app.workspace_name}`,
441+
)
442+
await new Promise((resolve) => setTimeout(resolve, 5000))
443+
terminal.sendText(app.command ?? "")
444+
terminal.show(false)
445+
},
446+
)
447+
}
448+
// Check if app has a URL to open
449+
if (app.url) {
450+
return vscode.window.withProgress(
451+
{
452+
location: vscode.ProgressLocation.Notification,
453+
title: `Opening ${app.name || "application"} in browser...`,
454+
cancellable: false,
455+
},
456+
async () => {
457+
await vscode.env.openExternal(vscode.Uri.parse(app.url!))
458+
},
459+
)
460+
}
461+
462+
// If no URL or command, show information about the app status
463+
vscode.window.showInformationMessage(`${app.name}`, {
464+
detail: `Agent: ${app.agent_name || "Unknown"}`,
465+
})
466+
}
467+
410468
/**
411469
* Open a workspace belonging to the currently logged-in deployment.
412470
*

src/extension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
181181
vscode.commands.registerCommand("coder.open", commands.open.bind(commands))
182182
vscode.commands.registerCommand("coder.openDevContainer", commands.openDevContainer.bind(commands))
183183
vscode.commands.registerCommand("coder.openFromSidebar", commands.openFromSidebar.bind(commands))
184+
vscode.commands.registerCommand("coder.openAppStatus", commands.openAppStatus.bind(commands))
184185
vscode.commands.registerCommand("coder.workspace.update", commands.updateWorkspace.bind(commands))
185186
vscode.commands.registerCommand("coder.createWorkspace", commands.createWorkspace.bind(commands))
186187
vscode.commands.registerCommand("coder.navigateToWorkspace", commands.navigateToWorkspace.bind(commands))

src/workspacesProvider.ts

Lines changed: 120 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Api } from "coder/site/src/api/api"
2-
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
2+
import { Workspace, WorkspaceAgent, WorkspaceApp } from "coder/site/src/api/typesGenerated"
33
import { EventSource } from "eventsource"
44
import * as path from "path"
55
import * as vscode from "vscode"
@@ -146,9 +146,36 @@ export class WorkspaceProvider implements vscode.TreeDataProvider<vscode.TreeIte
146146
}
147147
})
148148

149-
return resp.workspaces.map((workspace) => {
150-
return new WorkspaceTreeItem(workspace, this.getWorkspacesQuery === WorkspaceQuery.All, showMetadata)
151-
})
149+
// Create tree items for each workspace
150+
const workspaceTreeItems = await Promise.all(
151+
resp.workspaces.map(async (workspace) => {
152+
const workspaceTreeItem = new WorkspaceTreeItem(
153+
workspace,
154+
this.getWorkspacesQuery === WorkspaceQuery.All,
155+
showMetadata,
156+
)
157+
158+
// Get app status from the workspace agents
159+
const agents = extractAgents(workspace)
160+
agents.forEach((agent) => {
161+
// Check if agent has apps property with status reporting
162+
if (agent.apps && Array.isArray(agent.apps)) {
163+
workspaceTreeItem.appStatus = agent.apps.map((app: WorkspaceApp) => ({
164+
name: app.display_name,
165+
url: app.url,
166+
agent_id: agent.id,
167+
agent_name: agent.name,
168+
command: app.command,
169+
workspace_name: workspace.name,
170+
}))
171+
}
172+
})
173+
174+
return workspaceTreeItem
175+
}),
176+
)
177+
178+
return workspaceTreeItems
152179
}
153180

154181
/**
@@ -207,14 +234,58 @@ export class WorkspaceProvider implements vscode.TreeDataProvider<vscode.TreeIte
207234
const agentTreeItems = agents.map(
208235
(agent) => new AgentTreeItem(agent, element.workspaceOwner, element.workspaceName, element.watchMetadata),
209236
)
237+
210238
return Promise.resolve(agentTreeItems)
211239
} else if (element instanceof AgentTreeItem) {
212240
const watcher = this.agentWatchers[element.agent.id]
213241
if (watcher?.error) {
214242
return Promise.resolve([new ErrorTreeItem(watcher.error)])
215243
}
244+
245+
const items: vscode.TreeItem[] = []
246+
247+
// Add app status section with collapsible header
248+
if (element.agent.apps && element.agent.apps.length > 0) {
249+
const appStatuses = []
250+
for (const app of element.agent.apps) {
251+
if (app.statuses && app.statuses.length > 0) {
252+
for (const status of app.statuses) {
253+
// Show all statuses, not just ones needing attention.
254+
// We need to do this for now because the reporting isn't super accurate
255+
// yet.
256+
appStatuses.push(
257+
new AppStatusTreeItem({
258+
name: status.message,
259+
command: app.command,
260+
workspace_name: element.workspaceName,
261+
}),
262+
)
263+
}
264+
}
265+
}
266+
267+
// Show the section if it has any items
268+
if (appStatuses.length > 0) {
269+
const appStatusSection = new SectionTreeItem("App Statuses", appStatuses.reverse())
270+
items.push(appStatusSection)
271+
}
272+
}
273+
216274
const savedMetadata = watcher?.metadata || []
217-
return Promise.resolve(savedMetadata.map((metadata) => new AgentMetadataTreeItem(metadata)))
275+
276+
// Add agent metadata section with collapsible header
277+
if (savedMetadata.length > 0) {
278+
const metadataSection = new SectionTreeItem(
279+
"Agent Metadata",
280+
savedMetadata.map((metadata) => new AgentMetadataTreeItem(metadata)),
281+
)
282+
items.push(metadataSection)
283+
}
284+
285+
return Promise.resolve(items)
286+
} else if (element instanceof SectionTreeItem) {
287+
// Return the children of the section
288+
return Promise.resolve(element.children)
218289
}
219290

220291
return Promise.resolve([])
@@ -265,6 +336,19 @@ function monitorMetadata(agentId: WorkspaceAgent["id"], restClient: Api): AgentW
265336
return watcher
266337
}
267338

339+
/**
340+
* A tree item that represents a collapsible section with child items
341+
*/
342+
class SectionTreeItem extends vscode.TreeItem {
343+
constructor(
344+
label: string,
345+
public readonly children: vscode.TreeItem[],
346+
) {
347+
super(label, vscode.TreeItemCollapsibleState.Collapsed)
348+
this.contextValue = "coderSectionHeader"
349+
}
350+
}
351+
268352
class ErrorTreeItem extends vscode.TreeItem {
269353
constructor(error: unknown) {
270354
super("Failed to query metadata: " + errToStr(error, "no error provided"), vscode.TreeItemCollapsibleState.None)
@@ -285,6 +369,28 @@ class AgentMetadataTreeItem extends vscode.TreeItem {
285369
}
286370
}
287371

372+
class AppStatusTreeItem extends vscode.TreeItem {
373+
constructor(
374+
public readonly app: {
375+
name: string
376+
url?: string
377+
command?: string
378+
workspace_name?: string
379+
},
380+
) {
381+
super("", vscode.TreeItemCollapsibleState.None)
382+
this.description = app.name
383+
this.contextValue = "coderAppStatus"
384+
385+
// Add command to handle clicking on the app
386+
this.command = {
387+
command: "coder.openAppStatus",
388+
title: "Open App Status",
389+
arguments: [app],
390+
}
391+
}
392+
}
393+
288394
type CoderOpenableTreeItemType = "coderWorkspaceSingleAgent" | "coderWorkspaceMultipleAgents" | "coderAgent"
289395

290396
export class OpenableTreeItem extends vscode.TreeItem {
@@ -335,6 +441,15 @@ class AgentTreeItem extends OpenableTreeItem {
335441
}
336442

337443
export class WorkspaceTreeItem extends OpenableTreeItem {
444+
public appStatus: {
445+
name: string
446+
url?: string
447+
agent_id?: string
448+
agent_name?: string
449+
command?: string
450+
workspace_name?: string
451+
}[] = []
452+
338453
constructor(
339454
public readonly workspace: Workspace,
340455
public readonly showOwner: boolean,

yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1749,7 +1749,7 @@ co@3.1.0:
17491749

17501750
"coder@https://github.com/coder/coder#main":
17511751
version "0.0.0"
1752-
resolved "https://github.com/coder/coder#3ac844ad3d341d2910542b83d4f33df7bd0be85e"
1752+
resolved "https://github.com/coder/coder#2efb8088f4d923d1884fe8947dc338f9d179693b"
17531753

17541754
collapse-white-space@^1.0.2:
17551755
version "1.0.6"

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