Skip to content

Bcpeinhardt/ai agent session in vscode #488

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bfa33eb
update deps to kyles branch and make helper for getting agent informa…
bcpeinhardt Mar 18, 2025
92c3bfd
agents in sidebar
bcpeinhardt Mar 18, 2025
c5f6dcb
janky working demo for meeting
bcpeinhardt Mar 18, 2025
b2f6bb8
clean up some horrible claude code
bcpeinhardt Mar 18, 2025
c7001d5
separate metadata and tasks
bcpeinhardt Mar 18, 2025
f39f458
tweaks to terminal sizing
bcpeinhardt Mar 19, 2025
c31cb7c
put coder dep back on main post merge of ai work
bcpeinhardt Mar 31, 2025
063b27e
statuses updates
bcpeinhardt Apr 1, 2025
be1e137
coder ssh strategy finally working
bcpeinhardt Apr 1, 2025
08c93ae
show apps in need of attention only when there are some to show
bcpeinhardt Apr 1, 2025
99f3b1d
working with goose and claude
bcpeinhardt Apr 1, 2025
cd7c68c
switch up labels
bcpeinhardt Apr 1, 2025
26f740d
remove hand raise emojis
bcpeinhardt Apr 1, 2025
ac8d5fd
app not application
bcpeinhardt Apr 1, 2025
d0386d0
resolve conflict
bcpeinhardt Apr 1, 2025
69f9b97
remove unused commands
bcpeinhardt Apr 1, 2025
68c2e04
terminal names
bcpeinhardt Apr 1, 2025
b1e281c
use built in coder cli
bcpeinhardt Apr 2, 2025
5c66430
Merge branch 'main' into bcpeinhardt/ai-agent-session-in-vscode
bcpeinhardt Apr 14, 2025
83fafb0
Merge branch 'main' into bcpeinhardt/ai-agent-session-in-vscode
bcpeinhardt Apr 15, 2025
22246b9
reset back to working state pre tmux and reverse app statuses so most…
bcpeinhardt Apr 21, 2025
80f74f9
only show terminal after ssh command and app commands run
bcpeinhardt Apr 21, 2025
0f7dd65
loading indicator
bcpeinhardt Apr 21, 2025
8f996dd
update loading msg
bcpeinhardt Apr 21, 2025
c24b675
don't mess with terminal size
bcpeinhardt Apr 21, 2025
6f83606
workspace name isn't optional
bcpeinhardt Apr 21, 2025
579ad4e
changelog and format
bcpeinhardt Apr 21, 2025
9340b7d
remove unused icon code
bcpeinhardt Apr 21, 2025
8dac372
cleanup
bcpeinhardt Apr 21, 2025
866956a
remove unnecessary label assignment (I think there used to be an icon…
bcpeinhardt Apr 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

### Added

- Coder extension sidebar now displays available app statuses, and let's
the user click them to drop into a session with a running AI Agent.

## [v1.7.1](https://github.com/coder/vscode-coder/releases/tag/v1.7.1) (2025-04-14)

### Fixed
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,12 @@
"title": "Coder: View Logs",
"icon": "$(list-unordered)",
"when": "coder.authenticated"
},
{
"command": "coder.openAppStatus",
"title": "Coder: Open App Status",
"icon": "$(robot)",
"when": "coder.authenticated"
}
],
"menus": {
Expand Down
58 changes: 58 additions & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Api } from "coder/site/src/api/api"
import { getErrorMessage } from "coder/site/src/api/errors"
import { User, Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
import path from "node:path"
import * as vscode from "vscode"
import { makeCoderSdk, needToken } from "./api"
import { extractAgents } from "./api-helper"
Expand Down Expand Up @@ -407,6 +408,63 @@ export class Commands {
}
}

public async openAppStatus(app: {
name?: string
url?: string
agent_name?: string
command?: string
workspace_name: string
}): Promise<void> {
// Launch and run command in terminal if command is provided
if (app.command) {
return vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: `Connecting to AI Agent...`,
cancellable: false,
},
async () => {
const terminal = vscode.window.createTerminal(app.name)

// If workspace_name is provided, run coder ssh before the command

const url = this.storage.getUrl()
if (!url) {
throw new Error("No coder url found for sidebar")
}
const binary = await this.storage.fetchBinary(this.restClient, toSafeHost(url))
const escape = (str: string): string => `"${str.replace(/"/g, '\\"')}"`
terminal.sendText(
`${escape(binary)} ssh --global-config ${escape(
path.dirname(this.storage.getSessionTokenPath(toSafeHost(url))),
)} ${app.workspace_name}`,
)
await new Promise((resolve) => setTimeout(resolve, 5000))
terminal.sendText(app.command ?? "")
terminal.show(false)
},
)
}
// Check if app has a URL to open
if (app.url) {
return vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: `Opening ${app.name || "application"} in browser...`,
cancellable: false,
},
async () => {
await vscode.env.openExternal(vscode.Uri.parse(app.url!))
},
)
}

// If no URL or command, show information about the app status
vscode.window.showInformationMessage(`${app.name}`, {
detail: `Agent: ${app.agent_name || "Unknown"}`,
})
}

/**
* Open a workspace belonging to the currently logged-in deployment.
*
Expand Down
1 change: 1 addition & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
vscode.commands.registerCommand("coder.open", commands.open.bind(commands))
vscode.commands.registerCommand("coder.openDevContainer", commands.openDevContainer.bind(commands))
vscode.commands.registerCommand("coder.openFromSidebar", commands.openFromSidebar.bind(commands))
vscode.commands.registerCommand("coder.openAppStatus", commands.openAppStatus.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))
Expand Down
125 changes: 120 additions & 5 deletions src/workspacesProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Api } from "coder/site/src/api/api"
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
import { Workspace, WorkspaceAgent, WorkspaceApp } from "coder/site/src/api/typesGenerated"
import { EventSource } from "eventsource"
import * as path from "path"
import * as vscode from "vscode"
Expand Down Expand Up @@ -146,9 +146,36 @@ export class WorkspaceProvider implements vscode.TreeDataProvider<vscode.TreeIte
}
})

return resp.workspaces.map((workspace) => {
return new WorkspaceTreeItem(workspace, this.getWorkspacesQuery === WorkspaceQuery.All, showMetadata)
})
// Create tree items for each workspace
const workspaceTreeItems = await Promise.all(
resp.workspaces.map(async (workspace) => {
const workspaceTreeItem = new WorkspaceTreeItem(
workspace,
this.getWorkspacesQuery === WorkspaceQuery.All,
showMetadata,
)

// Get app status from the workspace agents
const agents = extractAgents(workspace)
agents.forEach((agent) => {
// Check if agent has apps property with status reporting
if (agent.apps && Array.isArray(agent.apps)) {
workspaceTreeItem.appStatus = agent.apps.map((app: WorkspaceApp) => ({
name: app.display_name,
url: app.url,
agent_id: agent.id,
agent_name: agent.name,
command: app.command,
workspace_name: workspace.name,
}))
}
})

return workspaceTreeItem
}),
)

return workspaceTreeItems
}

/**
Expand Down Expand Up @@ -207,14 +234,58 @@ export class WorkspaceProvider implements vscode.TreeDataProvider<vscode.TreeIte
const agentTreeItems = agents.map(
(agent) => new AgentTreeItem(agent, element.workspaceOwner, element.workspaceName, element.watchMetadata),
)

return Promise.resolve(agentTreeItems)
} else if (element instanceof AgentTreeItem) {
const watcher = this.agentWatchers[element.agent.id]
if (watcher?.error) {
return Promise.resolve([new ErrorTreeItem(watcher.error)])
}

const items: vscode.TreeItem[] = []

// Add app status section with collapsible header
if (element.agent.apps && element.agent.apps.length > 0) {
const appStatuses = []
for (const app of element.agent.apps) {
if (app.statuses && app.statuses.length > 0) {
for (const status of app.statuses) {
// Show all statuses, not just ones needing attention.
// We need to do this for now because the reporting isn't super accurate
// yet.
appStatuses.push(
new AppStatusTreeItem({
name: status.message,
command: app.command,
workspace_name: element.workspaceName,
}),
)
}
}
}

// Show the section if it has any items
if (appStatuses.length > 0) {
const appStatusSection = new SectionTreeItem("App Statuses", appStatuses.reverse())
items.push(appStatusSection)
}
}

const savedMetadata = watcher?.metadata || []
return Promise.resolve(savedMetadata.map((metadata) => new AgentMetadataTreeItem(metadata)))

// Add agent metadata section with collapsible header
if (savedMetadata.length > 0) {
const metadataSection = new SectionTreeItem(
"Agent Metadata",
savedMetadata.map((metadata) => new AgentMetadataTreeItem(metadata)),
)
items.push(metadataSection)
}

return Promise.resolve(items)
} else if (element instanceof SectionTreeItem) {
// Return the children of the section
return Promise.resolve(element.children)
}

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

/**
* A tree item that represents a collapsible section with child items
*/
class SectionTreeItem extends vscode.TreeItem {
constructor(
label: string,
public readonly children: vscode.TreeItem[],
) {
super(label, vscode.TreeItemCollapsibleState.Collapsed)
this.contextValue = "coderSectionHeader"
}
}

class ErrorTreeItem extends vscode.TreeItem {
constructor(error: unknown) {
super("Failed to query metadata: " + errToStr(error, "no error provided"), vscode.TreeItemCollapsibleState.None)
Expand All @@ -285,6 +369,28 @@ class AgentMetadataTreeItem extends vscode.TreeItem {
}
}

class AppStatusTreeItem extends vscode.TreeItem {
constructor(
public readonly app: {
name: string
url?: string
command?: string
workspace_name?: string
},
) {
super("", vscode.TreeItemCollapsibleState.None)
this.description = app.name
this.contextValue = "coderAppStatus"

// Add command to handle clicking on the app
this.command = {
command: "coder.openAppStatus",
title: "Open App Status",
arguments: [app],
}
}
}

type CoderOpenableTreeItemType = "coderWorkspaceSingleAgent" | "coderWorkspaceMultipleAgents" | "coderAgent"

export class OpenableTreeItem extends vscode.TreeItem {
Expand Down Expand Up @@ -335,6 +441,15 @@ class AgentTreeItem extends OpenableTreeItem {
}

export class WorkspaceTreeItem extends OpenableTreeItem {
public appStatus: {
name: string
url?: string
agent_id?: string
agent_name?: string
command?: string
workspace_name?: string
}[] = []

constructor(
public readonly workspace: Workspace,
public readonly showOwner: boolean,
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1714,7 +1714,7 @@ [email protected]:

"coder@https://github.com/coder/coder#main":
version "0.0.0"
resolved "https://github.com/coder/coder#3ac844ad3d341d2910542b83d4f33df7bd0be85e"
resolved "https://github.com/coder/coder#2efb8088f4d923d1884fe8947dc338f9d179693b"

collapse-white-space@^1.0.2:
version "1.0.6"
Expand Down