From bfa33ebb5952dca97790bdc9bdb9883bd40e8775 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 18 Mar 2025 07:51:52 -0500 Subject: [PATCH 01/27] update deps to kyles branch and make helper for getting agent information --- package.json | 2 +- src/api.ts | 44 +++++++++++++++++++++++++++++++++++++++++++- yarn.lock | 4 ++-- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 7d7d3862..6d1df6b1 100644 --- a/package.json +++ b/package.json @@ -287,7 +287,7 @@ "@vscode/test-electron": "^2.4.1", "@vscode/vsce": "^2.21.1", "bufferutil": "^4.0.8", - "coder": "https://github.com/coder/coder#main", + "coder": "https://github.com/coder/coder#kyle/tasks", "dayjs": "^1.11.13", "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", diff --git a/src/api.ts b/src/api.ts index 51e15416..b3c096a0 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,7 +1,7 @@ import { AxiosInstance } from "axios" import { spawn } from "child_process" import { Api } from "coder/site/src/api/api" -import { ProvisionerJobLog, Workspace } from "coder/site/src/api/typesGenerated" +import { ProvisionerJobLog, Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated" import { FetchLikeInit } from "eventsource" import fs from "fs/promises" import { ProxyAgent } from "proxy-agent" @@ -276,3 +276,45 @@ export async function waitForBuild( writeEmitter.fire(`Workspace is now ${updatedWorkspace.latest_build.status}\r\n`) return updatedWorkspace } + +// 1. First, get a workspace by owner and name +export async function getAITasksForWorkspace( + restClient: Api, + writeEmitter: vscode.EventEmitter, + workspace: Workspace, +) { + + // The workspace will include agents, and within each agent you can find tasks + // You can access the agents from the workspace resource + const resources = workspace.latest_build.resources; + + // Go through each resource + for (const resource of resources) { + // Each resource can have multiple agents + if (!resource.agents) { + continue + } + + for (const agent of resource.agents) { + // Check if this agent has any AI tasks + if (agent.tasks && agent.tasks.length > 0) { + // This agent has AI tasks! + console.log(`Agent ${agent.name} (${agent.id}) has ${agent.tasks.length} tasks`); + + // Examine task details + for (const task of agent.tasks) { + console.log(`Task: ${task.summary}`); + console.log(`Reporter: ${task.reporter}`); + console.log(`Status: ${task.completion ? 'Completed' : 'In Progress'}`); + console.log(`URL: ${task.url}`); + console.log(`Icon: ${task.icon}`); + } + + // Check if the agent is waiting for user input + if (agent.task_waiting_for_user_input) { + console.log("This agent is waiting for user input!"); + } + } + } + } +} diff --git a/yarn.lock b/yarn.lock index 907f0855..e0f43fc1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1592,9 +1592,9 @@ co@3.1.0: resolved "https://registry.yarnpkg.com/co/-/co-3.1.0.tgz#4ea54ea5a08938153185e15210c68d9092bc1b78" integrity sha512-CQsjCRiNObI8AtTsNIBDRMQ4oMR83CzEswHYahClvul7gKk+lDQiOKv+5qh7LQWf5sh6jkZNispz/QlsZxyNgA== -"coder@https://github.com/coder/coder#main": +"coder@https://github.com/coder/coder#kyle/tasks": version "0.0.0" - resolved "https://github.com/coder/coder#975ea23d6f49a4043131f79036d1bf5166eb9140" + resolved "https://github.com/coder/coder#87e086298f19fec18b2ba18bf4ff39081d670570" collapse-white-space@^1.0.2: version "1.0.6" From 92c3bfd6a5cd881ed8065dba119ffb09e81ce7c0 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 18 Mar 2025 08:49:45 -0500 Subject: [PATCH 02/27] agents in sidebar --- package.json | 16 ++++++++++ src/api.ts | 33 ++++++-------------- src/commands.ts | 11 ++++++- src/extension.ts | 9 ++++++ src/workspacesProvider.ts | 65 +++++++++++++++++++++++++++++++++++---- 5 files changed, 104 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 6d1df6b1..6174e802 100644 --- a/package.json +++ b/package.json @@ -204,6 +204,12 @@ "title": "Coder: View Logs", "icon": "$(list-unordered)", "when": "coder.authenticated" + }, + { + "command": "coder.viewAITasks", + "title": "Coder: View AI Tasks", + "icon": "$(robot)", + "when": "coder.authenticated" } ], "menus": { @@ -231,6 +237,11 @@ "command": "coder.refreshWorkspaces", "when": "coder.authenticated && view == myWorkspaces", "group": "navigation" + }, + { + "command": "coder.viewAITasks", + "when": "coder.authenticated && view == myWorkspaces", + "group": "navigation" } ], "view/item/context": [ @@ -259,6 +270,11 @@ "command": "coder.createWorkspace", "group": "remote_11_ssh_coder@2", "when": "coder.authenticated" + }, + { + "command": "coder.viewAITasks", + "group": "remote_11_ssh_coder@3", + "when": "coder.authenticated" } ] } diff --git a/src/api.ts b/src/api.ts index b3c096a0..2c976f00 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,7 +1,7 @@ import { AxiosInstance } from "axios" import { spawn } from "child_process" import { Api } from "coder/site/src/api/api" -import { ProvisionerJobLog, Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated" +import { ProvisionerJobLog, Workspace, WorkspaceAgent, WorkspaceAgentTask } from "coder/site/src/api/typesGenerated" import { FetchLikeInit } from "eventsource" import fs from "fs/promises" import { ProxyAgent } from "proxy-agent" @@ -282,7 +282,9 @@ export async function getAITasksForWorkspace( restClient: Api, writeEmitter: vscode.EventEmitter, workspace: Workspace, -) { +): Promise { + // We need to build up tasks + let awaiting_tasks: WorkspaceAgentTask[] = []; // The workspace will include agents, and within each agent you can find tasks // You can access the agents from the workspace resource @@ -290,31 +292,16 @@ export async function getAITasksForWorkspace( // Go through each resource for (const resource of resources) { - // Each resource can have multiple agents if (!resource.agents) { continue } - for (const agent of resource.agents) { - // Check if this agent has any AI tasks - if (agent.tasks && agent.tasks.length > 0) { - // This agent has AI tasks! - console.log(`Agent ${agent.name} (${agent.id}) has ${agent.tasks.length} tasks`); - - // Examine task details - for (const task of agent.tasks) { - console.log(`Task: ${task.summary}`); - console.log(`Reporter: ${task.reporter}`); - console.log(`Status: ${task.completion ? 'Completed' : 'In Progress'}`); - console.log(`URL: ${task.url}`); - console.log(`Icon: ${task.icon}`); - } - - // Check if the agent is waiting for user input - if (agent.task_waiting_for_user_input) { - console.log("This agent is waiting for user input!"); - } + resource.agents.forEach((agent) => { + for (const task of agent.tasks) { + awaiting_tasks.push(task); } - } + }) } + + return awaiting_tasks; } diff --git a/src/commands.ts b/src/commands.ts index 3506d822..7df28c1f 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -2,7 +2,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 * as vscode from "vscode" -import { makeCoderSdk, needToken } from "./api" +import { getAITasksForWorkspace, makeCoderSdk, needToken } from "./api" import { extractAgents } from "./api-helper" import { CertificateError } from "./error" import { Storage } from "./storage" @@ -295,6 +295,15 @@ export class Commands { const doc = await vscode.workspace.openTextDocument(uri) await vscode.window.showTextDocument(doc) } + + /** + * Open a view to show AI tasks across all workspaces + */ + public async viewAITasks(): Promise { + vscode.window.showInformationMessage("Viewing AI tasks across workspaces") + // Refresh workspaces to ensure we have the latest tasks + vscode.commands.executeCommand("coder.refreshWorkspaces") + } /** * Log out from the currently logged-in deployment. diff --git a/src/extension.ts b/src/extension.ts index e5e2799a..831035ad 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -136,6 +136,15 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { allWorkspacesProvider.fetchAndRefresh() }) vscode.commands.registerCommand("coder.viewLogs", commands.viewLogs.bind(commands)) + vscode.commands.registerCommand("coder.viewAITasks", commands.viewAITasks.bind(commands)) + vscode.commands.registerCommand("coder.openAITask", (task) => { + // Open the task URL if available + if (task && task.url) { + vscode.env.openExternal(vscode.Uri.parse(task.url)) + } else { + vscode.window.showInformationMessage("This AI task has no associated URL") + } + }) // Since the "onResolveRemoteAuthority:ssh-remote" activation event exists // in package.json we're able to perform actions before the authority is diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index 0709487e..495dd0b5 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -1,9 +1,9 @@ import { Api } from "coder/site/src/api/api" -import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated" +import { Workspace, WorkspaceAgent, WorkspaceAgentTask } from "coder/site/src/api/typesGenerated" import { EventSource } from "eventsource" import * as path from "path" import * as vscode from "vscode" -import { createStreamingFetchAdapter } from "./api" +import { createStreamingFetchAdapter, getAITasksForWorkspace } from "./api" import { AgentMetadataEvent, AgentMetadataEventSchemaArray, @@ -146,9 +146,32 @@ export class WorkspaceProvider implements vscode.TreeDataProvider { - 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 + ) + + // Fetch AI tasks for the workspace + try { + // Create a dummy emitter for logs + const emitter = new vscode.EventEmitter() + const aiTasks = await getAITasksForWorkspace(restClient, emitter, workspace) + workspaceTreeItem.aiTasks = aiTasks + this.storage.writeToCoderOutputChannel(aiTasks.length.toString()) + console.log(aiTasks.length.toString()) + } catch (error) { + // Log the error but continue - we don't want to fail the whole tree if AI tasks fail + this.storage.writeToCoderOutputChannel(`Failed to fetch AI tasks for workspace ${workspace.name}: ${errToStr(error, "unknown error")}`) + + } + + return workspaceTreeItem + })) + + return workspaceTreeItems } /** @@ -207,7 +230,20 @@ export class WorkspaceProvider implements vscode.TreeDataProvider new AgentTreeItem(agent, element.workspaceOwner, element.workspaceName, element.watchMetadata), ) - return Promise.resolve(agentTreeItems) + + // Add AI task items to the workspace children if there are any + const aiTaskItems = element.aiTasks.map(task => new AITaskTreeItem(task)) + + // If there are AI tasks, add them at the beginning of the list + if (aiTaskItems.length == 0) { + return Promise.resolve(agentTreeItems) + } + // Create a separator item + const separator = new vscode.TreeItem("AI Tasks", vscode.TreeItemCollapsibleState.None) + separator.contextValue = "coderAITaskHeader" + + // Return AI task items first, then a separator, then agent items + return Promise.resolve([...aiTaskItems, separator, ...agentTreeItems]) } else if (element instanceof AgentTreeItem) { const watcher = this.agentWatchers[element.agent.id] if (watcher?.error) { @@ -285,6 +321,21 @@ class AgentMetadataTreeItem extends vscode.TreeItem { } } +class AITaskTreeItem extends vscode.TreeItem { + constructor(public readonly task: WorkspaceAgentTask) { + super(task.summary, vscode.TreeItemCollapsibleState.None) + this.description = task.summary + this.contextValue = "coderAITask" + + // Add command to handle clicking on the task + this.command = { + command: "coder.openAITask", + title: "Open AI Task", + arguments: [task] + } + } +} + type CoderOpenableTreeItemType = "coderWorkspaceSingleAgent" | "coderWorkspaceMultipleAgents" | "coderAgent" export class OpenableTreeItem extends vscode.TreeItem { @@ -335,6 +386,8 @@ class AgentTreeItem extends OpenableTreeItem { } export class WorkspaceTreeItem extends OpenableTreeItem { + public aiTasks: WorkspaceAgentTask[] = [] + constructor( public readonly workspace: Workspace, public readonly showOwner: boolean, From c5f6dcbfcc68a72f2f74b585ffbe748aad2d0057 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 18 Mar 2025 09:27:28 -0500 Subject: [PATCH 03/27] janky working demo for meeting --- package.json | 18 ++++++++++++++---- src/commands.ts | 20 ++++++++++++-------- src/extension.ts | 10 +--------- src/workspacesProvider.ts | 8 ++++---- 4 files changed, 31 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 6174e802..61b5ea47 100644 --- a/package.json +++ b/package.json @@ -170,6 +170,11 @@ "title": "Coder: Open Workspace", "icon": "$(play)" }, + { + "command": "coder.openFromSidebarAndOpenSession", + "title": "Coder: Open Workspace with Claude Code Session", + "icon": "$(terminal)" + }, { "command": "coder.createWorkspace", "title": "Create Workspace", @@ -206,8 +211,8 @@ "when": "coder.authenticated" }, { - "command": "coder.viewAITasks", - "title": "Coder: View AI Tasks", + "command": "coder.openAITask", + "title": "Coder: Open AI Task", "icon": "$(robot)", "when": "coder.authenticated" } @@ -239,7 +244,7 @@ "group": "navigation" }, { - "command": "coder.viewAITasks", + "command": "coder.openAITask", "when": "coder.authenticated && view == myWorkspaces", "group": "navigation" } @@ -250,6 +255,11 @@ "when": "coder.authenticated && viewItem == coderWorkspaceSingleAgent || coder.authenticated && viewItem == coderAgent", "group": "inline" }, + { + "command": "coder.openFromSidebarAndOpenSession", + "when": "coder.authenticated && viewItem == coderWorkspaceSingleAgent || coder.authenticated && viewItem == coderAgent", + "group": "inline" + }, { "command": "coder.navigateToWorkspace", "when": "coder.authenticated && viewItem == coderWorkspaceSingleAgent || coder.authenticated && viewItem == coderWorkspaceMultipleAgents", @@ -272,7 +282,7 @@ "when": "coder.authenticated" }, { - "command": "coder.viewAITasks", + "command": "coder.openAITask", "group": "remote_11_ssh_coder@3", "when": "coder.authenticated" } diff --git a/src/commands.ts b/src/commands.ts index 7df28c1f..4fcb58d3 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -296,14 +296,6 @@ export class Commands { await vscode.window.showTextDocument(doc) } - /** - * Open a view to show AI tasks across all workspaces - */ - public async viewAITasks(): Promise { - vscode.window.showInformationMessage("Viewing AI tasks across workspaces") - // Refresh workspaces to ensure we have the latest tasks - vscode.commands.executeCommand("coder.refreshWorkspaces") - } /** * Log out from the currently logged-in deployment. @@ -416,6 +408,18 @@ export class Commands { } } + public async openAISession(): Promise { + + // Then launch an integrated terminal with screen session + const terminal = vscode.window.createTerminal({ + name: "Claude Code Session", + }) + + // Show the terminal and run the screen command + terminal.show(true) + terminal.sendText("screen -xRR claude-code") + } + /** * Open a workspace belonging to the currently logged-in deployment. * diff --git a/src/extension.ts b/src/extension.ts index 831035ad..f04743ed 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -124,6 +124,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { 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.openAITask", commands.openAISession.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)) @@ -136,15 +137,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { allWorkspacesProvider.fetchAndRefresh() }) vscode.commands.registerCommand("coder.viewLogs", commands.viewLogs.bind(commands)) - vscode.commands.registerCommand("coder.viewAITasks", commands.viewAITasks.bind(commands)) - vscode.commands.registerCommand("coder.openAITask", (task) => { - // Open the task URL if available - if (task && task.url) { - vscode.env.openExternal(vscode.Uri.parse(task.url)) - } else { - vscode.window.showInformationMessage("This AI task has no associated URL") - } - }) // Since the "onResolveRemoteAuthority:ssh-remote" activation event exists // in package.json we're able to perform actions before the authority is diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index 495dd0b5..80c60597 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -238,12 +238,9 @@ export class WorkspaceProvider implements vscode.TreeDataProvider Date: Tue, 18 Mar 2025 15:55:40 -0500 Subject: [PATCH 04/27] clean up some horrible claude code --- src/api.ts | 29 ------------ src/commands.ts | 6 +-- src/workspacesProvider.ts | 92 ++++++++++++++++++++------------------- 3 files changed, 49 insertions(+), 78 deletions(-) diff --git a/src/api.ts b/src/api.ts index 2c976f00..b911f874 100644 --- a/src/api.ts +++ b/src/api.ts @@ -276,32 +276,3 @@ export async function waitForBuild( writeEmitter.fire(`Workspace is now ${updatedWorkspace.latest_build.status}\r\n`) return updatedWorkspace } - -// 1. First, get a workspace by owner and name -export async function getAITasksForWorkspace( - restClient: Api, - writeEmitter: vscode.EventEmitter, - workspace: Workspace, -): Promise { - // We need to build up tasks - let awaiting_tasks: WorkspaceAgentTask[] = []; - - // The workspace will include agents, and within each agent you can find tasks - // You can access the agents from the workspace resource - const resources = workspace.latest_build.resources; - - // Go through each resource - for (const resource of resources) { - if (!resource.agents) { - continue - } - - resource.agents.forEach((agent) => { - for (const task of agent.tasks) { - awaiting_tasks.push(task); - } - }) - } - - return awaiting_tasks; -} diff --git a/src/commands.ts b/src/commands.ts index 4fcb58d3..c33d1713 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -2,7 +2,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 * as vscode from "vscode" -import { getAITasksForWorkspace, makeCoderSdk, needToken } from "./api" +import { makeCoderSdk, needToken } from "./api" import { extractAgents } from "./api-helper" import { CertificateError } from "./error" import { Storage } from "./storage" @@ -295,7 +295,6 @@ export class Commands { const doc = await vscode.workspace.openTextDocument(uri) await vscode.window.showTextDocument(doc) } - /** * Log out from the currently logged-in deployment. @@ -409,12 +408,11 @@ export class Commands { } public async openAISession(): Promise { - // Then launch an integrated terminal with screen session const terminal = vscode.window.createTerminal({ name: "Claude Code Session", }) - + // Show the terminal and run the screen command terminal.show(true) terminal.sendText("screen -xRR claude-code") diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index 80c60597..0294c648 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -3,7 +3,7 @@ import { Workspace, WorkspaceAgent, WorkspaceAgentTask } from "coder/site/src/ap import { EventSource } from "eventsource" import * as path from "path" import * as vscode from "vscode" -import { createStreamingFetchAdapter, getAITasksForWorkspace } from "./api" +import { createStreamingFetchAdapter } from "./api" import { AgentMetadataEvent, AgentMetadataEventSchemaArray, @@ -147,29 +147,28 @@ export class WorkspaceProvider implements vscode.TreeDataProvider { - const workspaceTreeItem = new WorkspaceTreeItem( - workspace, - this.getWorkspacesQuery === WorkspaceQuery.All, - showMetadata - ) - - // Fetch AI tasks for the workspace - try { - // Create a dummy emitter for logs - const emitter = new vscode.EventEmitter() - const aiTasks = await getAITasksForWorkspace(restClient, emitter, workspace) - workspaceTreeItem.aiTasks = aiTasks - this.storage.writeToCoderOutputChannel(aiTasks.length.toString()) - console.log(aiTasks.length.toString()) - } catch (error) { - // Log the error but continue - we don't want to fail the whole tree if AI tasks fail - this.storage.writeToCoderOutputChannel(`Failed to fetch AI tasks for workspace ${workspace.name}: ${errToStr(error, "unknown error")}`) - - } - - return workspaceTreeItem - })) + const workspaceTreeItems = await Promise.all( + resp.workspaces.map(async (workspace) => { + const workspaceTreeItem = new WorkspaceTreeItem( + workspace, + this.getWorkspacesQuery === WorkspaceQuery.All, + showMetadata, + ) + + // Fetch AI tasks for the workspace + try { + // Create a dummy emitter for logs + const emitter = new vscode.EventEmitter() + } catch (error) { + // Log the error but continue - we don't want to fail the whole tree if AI tasks fail + this.storage.writeToCoderOutputChannel( + `Failed to fetch AI tasks for workspace ${workspace.name}: ${errToStr(error, "unknown error")}`, + ) + } + + return workspaceTreeItem + }), + ) return workspaceTreeItems } @@ -230,24 +229,25 @@ export class WorkspaceProvider implements vscode.TreeDataProvider new AgentTreeItem(agent, element.workspaceOwner, element.workspaceName, element.watchMetadata), ) - - // Add AI task items to the workspace children if there are any - const aiTaskItems = element.aiTasks.map(task => new AITaskTreeItem(task)) - - // If there are AI tasks, add them at the beginning of the list - if (aiTaskItems.length == 0) { - return Promise.resolve(agentTreeItems) - } - - // Return AI task items first, then a separator, then agent items - return Promise.resolve([...aiTaskItems, ...agentTreeItems]) + + 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 AI tasks first, if the agent has any associated tasks + const agentTasks = element.agent.tasks.map((task) => new AITaskTreeItem(task)) + items.push(...agentTasks) + + // Add agent metadata const savedMetadata = watcher?.metadata || [] - return Promise.resolve(savedMetadata.map((metadata) => new AgentMetadataTreeItem(metadata))) + items.push(...savedMetadata.map((metadata) => new AgentMetadataTreeItem(metadata))) + + return Promise.resolve(items) } return Promise.resolve([]) @@ -320,18 +320,16 @@ class AgentMetadataTreeItem extends vscode.TreeItem { class AITaskTreeItem extends vscode.TreeItem { constructor(public readonly task: WorkspaceAgentTask) { - super(task.summary, vscode.TreeItemCollapsibleState.None) + // Add a hand raise emoji (✋) to indicate tasks awaiting user input + super(task.icon, vscode.TreeItemCollapsibleState.None) this.description = task.summary this.contextValue = "coderAITask" - - // Add an icon using VSCode's built-in Codicons - this.iconPath = new vscode.ThemeIcon("sparkle") - + // Add command to handle clicking on the task this.command = { - command: "coder.openAITask", + command: "coder.openAITask", title: "Open AI Task", - arguments: [task] + arguments: [task], } } } @@ -382,12 +380,16 @@ class AgentTreeItem extends OpenableTreeItem { agent.expanded_directory, "coderAgent", ) + + if (agent.task_waiting_for_user_input) { + this.label = "🙋 " + this.label; + } } } export class WorkspaceTreeItem extends OpenableTreeItem { - public aiTasks: WorkspaceAgentTask[] = [] - + public aiTasks: {waiting: boolean, tasks: WorkspaceAgentTask[]}[] = [] + constructor( public readonly workspace: Workspace, public readonly showOwner: boolean, From c7001d5f03b5d62805447988b659a7a054a7e7a3 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 18 Mar 2025 16:10:08 -0500 Subject: [PATCH 05/27] separate metadata and tasks --- src/api.ts | 2 +- src/workspacesProvider.ts | 52 ++++++++++++++++++++++++++++++--------- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/api.ts b/src/api.ts index b911f874..51e15416 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,7 +1,7 @@ import { AxiosInstance } from "axios" import { spawn } from "child_process" import { Api } from "coder/site/src/api/api" -import { ProvisionerJobLog, Workspace, WorkspaceAgent, WorkspaceAgentTask } from "coder/site/src/api/typesGenerated" +import { ProvisionerJobLog, Workspace } from "coder/site/src/api/typesGenerated" import { FetchLikeInit } from "eventsource" import fs from "fs/promises" import { ProxyAgent } from "proxy-agent" diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index 0294c648..aa66a577 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -158,7 +158,7 @@ export class WorkspaceProvider implements vscode.TreeDataProvider() + const _emitter = new vscode.EventEmitter() } catch (error) { // Log the error but continue - we don't want to fail the whole tree if AI tasks fail this.storage.writeToCoderOutputChannel( @@ -236,18 +236,33 @@ export class WorkspaceProvider implements vscode.TreeDataProvider new AITaskTreeItem(task)) - items.push(...agentTasks) - - // Add agent metadata + + // Add AI tasks section with collapsible header + if (element.agent.tasks.length > 0) { + const aiTasksSection = new SectionTreeItem( + "AI Tasks", + element.agent.tasks.map((task) => new AITaskTreeItem(task)), + ) + items.push(aiTasksSection) + } + const savedMetadata = watcher?.metadata || [] - items.push(...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([]) @@ -298,6 +313,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.Expanded) + this.contextValue = "coderSectionHeader" + } +} + class ErrorTreeItem extends vscode.TreeItem { constructor(error: unknown) { super("Failed to query metadata: " + errToStr(error, "no error provided"), vscode.TreeItemCollapsibleState.None) @@ -382,13 +410,13 @@ class AgentTreeItem extends OpenableTreeItem { ) if (agent.task_waiting_for_user_input) { - this.label = "🙋 " + this.label; + this.label = "🙋 " + this.label } } } export class WorkspaceTreeItem extends OpenableTreeItem { - public aiTasks: {waiting: boolean, tasks: WorkspaceAgentTask[]}[] = [] + public aiTasks: { waiting: boolean; tasks: WorkspaceAgentTask[] }[] = [] constructor( public readonly workspace: Workspace, From f39f458af0cde633980fedd2a0f1f09d30172164 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Wed, 19 Mar 2025 10:28:38 -0500 Subject: [PATCH 06/27] tweaks to terminal sizing --- src/commands.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/commands.ts b/src/commands.ts index c33d1713..cd29afe4 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -411,10 +411,16 @@ export class Commands { // Then launch an integrated terminal with screen session const terminal = vscode.window.createTerminal({ name: "Claude Code Session", + location: vscode.TerminalLocation.Panel }) // Show the terminal and run the screen command terminal.show(true) + + // Hide sidebar and maximize terminal panel + // await vscode.commands.executeCommand("workbench.action.toggleSidebarVisibility") + await vscode.commands.executeCommand("workbench.action.toggleMaximizedPanel") + terminal.sendText("screen -xRR claude-code") } From c31cb7c27b0004e1460ef9f722bb7966b888d03b Mon Sep 17 00:00:00 2001 From: Benjamin Date: Mon, 31 Mar 2025 10:33:49 -0500 Subject: [PATCH 07/27] put coder dep back on main post merge of ai work --- package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 61b5ea47..18653143 100644 --- a/package.json +++ b/package.json @@ -313,7 +313,7 @@ "@vscode/test-electron": "^2.4.1", "@vscode/vsce": "^2.21.1", "bufferutil": "^4.0.8", - "coder": "https://github.com/coder/coder#kyle/tasks", + "coder": "https://github.com/coder/coder#main", "dayjs": "^1.11.13", "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", diff --git a/yarn.lock b/yarn.lock index e0f43fc1..798f958a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1592,9 +1592,9 @@ co@3.1.0: resolved "https://registry.yarnpkg.com/co/-/co-3.1.0.tgz#4ea54ea5a08938153185e15210c68d9092bc1b78" integrity sha512-CQsjCRiNObI8AtTsNIBDRMQ4oMR83CzEswHYahClvul7gKk+lDQiOKv+5qh7LQWf5sh6jkZNispz/QlsZxyNgA== -"coder@https://github.com/coder/coder#kyle/tasks": +"coder@https://github.com/coder/coder#main": version "0.0.0" - resolved "https://github.com/coder/coder#87e086298f19fec18b2ba18bf4ff39081d670570" + resolved "https://github.com/coder/coder#8ea956fc115c221f198dd2c54538c93fc03c91cf" collapse-white-space@^1.0.2: version "1.0.6" From 063b27e21f475942457bd88305a344f57bf12165 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 1 Apr 2025 09:06:19 -0500 Subject: [PATCH 08/27] statuses updates --- src/commands.ts | 29 +++++++------- src/extension.ts | 2 +- src/workspacesProvider.ts | 83 +++++++++++++++++++++++++++------------ yarn.lock | 2 +- 4 files changed, 75 insertions(+), 41 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index cd29afe4..491b4fcd 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -407,21 +407,22 @@ export class Commands { } } - public async openAISession(): Promise { - // Then launch an integrated terminal with screen session - const terminal = vscode.window.createTerminal({ - name: "Claude Code Session", - location: vscode.TerminalLocation.Panel - }) + public async openAppStatus(app: { + name?: string + status?: string + url?: string + agent_name?: string + }): Promise { + // Check if app has a URL to open + if (app.url) { + await vscode.env.openExternal(vscode.Uri.parse(app.url)) + return + } - // Show the terminal and run the screen command - terminal.show(true) - - // Hide sidebar and maximize terminal panel - // await vscode.commands.executeCommand("workbench.action.toggleSidebarVisibility") - await vscode.commands.executeCommand("workbench.action.toggleMaximizedPanel") - - terminal.sendText("screen -xRR claude-code") + // If no URL, show information about the app status + vscode.window.showInformationMessage(`${app.name || "Application"}: ${app.status || "Running"}`, { + detail: `Agent: ${app.agent_name || "Unknown"}`, + }) } /** diff --git a/src/extension.ts b/src/extension.ts index f04743ed..1dbb3836 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -124,7 +124,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { 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.openAITask", commands.openAISession.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)) diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index aa66a577..806c37f7 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -1,5 +1,5 @@ import { Api } from "coder/site/src/api/api" -import { Workspace, WorkspaceAgent, WorkspaceAgentTask } from "coder/site/src/api/typesGenerated" +import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated" import { EventSource } from "eventsource" import * as path from "path" import * as vscode from "vscode" @@ -155,14 +155,26 @@ export class WorkspaceProvider implements vscode.TreeDataProvider() + 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) => ({ + name: app.display_name || app.name || "App", + status: app.status || "Running", + icon: app.icon || "$(pulse)", + url: app.url, + agent_id: agent.id, + agent_name: agent.name, + })) + } + }) } catch (error) { - // Log the error but continue - we don't want to fail the whole tree if AI tasks fail + // Log the error but continue - we don't want to fail the whole tree if app status fails this.storage.writeToCoderOutputChannel( - `Failed to fetch AI tasks for workspace ${workspace.name}: ${errToStr(error, "unknown error")}`, + `Failed to get app status for workspace ${workspace.name}: ${errToStr(error, "unknown error")}`, ) } @@ -239,13 +251,24 @@ export class WorkspaceProvider implements vscode.TreeDataProvider 0) { - const aiTasksSection = new SectionTreeItem( - "AI Tasks", - element.agent.tasks.map((task) => new AITaskTreeItem(task)), + // Add app status section with collapsible header + if (element.agent.apps && element.agent.apps.length > 0) { + let needsAttention = [] + for (const app of element.agent.apps) { + if (app.statuses && app.statuses.length > 0) { + for (const status of app.statuses) { + if (status.needs_user_attention) { + needsAttention.push(new AppStatusTreeItem(status)) + } + } + } + } + + const appStatusSection = new SectionTreeItem( + "Applications in need of attention", + needsAttention, ) - items.push(aiTasksSection) + items.push(appStatusSection) } const savedMetadata = watcher?.metadata || [] @@ -346,18 +369,27 @@ class AgentMetadataTreeItem extends vscode.TreeItem { } } -class AITaskTreeItem extends vscode.TreeItem { - constructor(public readonly task: WorkspaceAgentTask) { - // Add a hand raise emoji (✋) to indicate tasks awaiting user input - super(task.icon, vscode.TreeItemCollapsibleState.None) - this.description = task.summary - this.contextValue = "coderAITask" +class AppStatusTreeItem extends vscode.TreeItem { + constructor( + public readonly app: { + name?: string + display_name?: string + status?: string + icon?: string + url?: string + agent_id?: string + agent_name?: string + }, + ) { + super(app.icon || "$(pulse)", vscode.TreeItemCollapsibleState.None) + this.description = app.status || "Running" + this.contextValue = "coderAppStatus" - // Add command to handle clicking on the task + // Add command to handle clicking on the app this.command = { - command: "coder.openAITask", - title: "Open AI Task", - arguments: [task], + command: "coder.openAppStatus", + title: "Open App Status", + arguments: [app], } } } @@ -409,14 +441,15 @@ class AgentTreeItem extends OpenableTreeItem { "coderAgent", ) - if (agent.task_waiting_for_user_input) { - this.label = "🙋 " + this.label + if (agent.apps && agent.apps.length > 0) { + // Add an icon to indicate this agent has running apps + this.label = "🖐️ " + this.label } } } export class WorkspaceTreeItem extends OpenableTreeItem { - public aiTasks: { waiting: boolean; tasks: WorkspaceAgentTask[] }[] = [] + public appStatus: { name: string; status: string; icon?: string }[] = [] constructor( public readonly workspace: Workspace, diff --git a/yarn.lock b/yarn.lock index 798f958a..fb2ad02d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1594,7 +1594,7 @@ co@3.1.0: "coder@https://github.com/coder/coder#main": version "0.0.0" - resolved "https://github.com/coder/coder#8ea956fc115c221f198dd2c54538c93fc03c91cf" + resolved "https://github.com/coder/coder#3a243c111b9abb5c38328169ff70064025bbe2fe" collapse-white-space@^1.0.2: version "1.0.6" From be1e137c85147f198d13cbe8a66d25fce1571012 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 1 Apr 2025 12:41:23 -0500 Subject: [PATCH 09/27] coder ssh strategy finally working --- package.json | 11 +++-------- src/commands.ts | 20 ++++++++++++++++++- src/workspacesProvider.ts | 41 ++++++++++++++++++++++++++------------- 3 files changed, 49 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 18653143..71e355bd 100644 --- a/package.json +++ b/package.json @@ -211,8 +211,8 @@ "when": "coder.authenticated" }, { - "command": "coder.openAITask", - "title": "Coder: Open AI Task", + "command": "coder.openAppStatus", + "title": "Coder: Open App Status", "icon": "$(robot)", "when": "coder.authenticated" } @@ -244,7 +244,7 @@ "group": "navigation" }, { - "command": "coder.openAITask", + "command": "coder.openAppStatus", "when": "coder.authenticated && view == myWorkspaces", "group": "navigation" } @@ -280,11 +280,6 @@ "command": "coder.createWorkspace", "group": "remote_11_ssh_coder@2", "when": "coder.authenticated" - }, - { - "command": "coder.openAITask", - "group": "remote_11_ssh_coder@3", - "when": "coder.authenticated" } ] } diff --git a/src/commands.ts b/src/commands.ts index 491b4fcd..7635fbfc 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -412,14 +412,32 @@ export class Commands { status?: string url?: string agent_name?: string + command?: string + workspace_name?: string }): Promise { + // Launch and run command in terminal if command is provided + if (app.command) { + const terminal = vscode.window.createTerminal(`${app.name || "Application"} Status`) + terminal.show(false) + vscode.commands.executeCommand("workbench.action.toggleMaximizedPanel") + // If workspace_name is provided, run coder ssh before the command + if (app.workspace_name) { + terminal.sendText(`coder ssh ${app.workspace_name}`) + // Sleep for 5 seconds + await new Promise((resolve) => setTimeout(resolve, 5000)) + terminal.sendText(app.command) + } else { + terminal.sendText("need workspace name") + } + return + } // Check if app has a URL to open if (app.url) { await vscode.env.openExternal(vscode.Uri.parse(app.url)) return } - // If no URL, show information about the app status + // If no URL or command, show information about the app status vscode.window.showInformationMessage(`${app.name || "Application"}: ${app.status || "Running"}`, { detail: `Agent: ${app.agent_name || "Unknown"}`, }) diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index 806c37f7..f795b96d 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -168,6 +168,8 @@ export class WorkspaceProvider implements vscode.TreeDataProvider 0) { - let needsAttention = [] + const needsAttention = [] for (const app of element.agent.apps) { if (app.statuses && app.statuses.length > 0) { for (const status of app.statuses) { if (status.needs_user_attention) { - needsAttention.push(new AppStatusTreeItem(status)) + needsAttention.push( + new AppStatusTreeItem({ + name: status.message, + command: app.command, + status: status.state, + workspace_name: element.workspaceName, + }), + ) } } } } - const appStatusSection = new SectionTreeItem( - "Applications in need of attention", - needsAttention, - ) + const appStatusSection = new SectionTreeItem("Applications in need of attention", needsAttention) items.push(appStatusSection) } @@ -372,17 +378,15 @@ class AgentMetadataTreeItem extends vscode.TreeItem { class AppStatusTreeItem extends vscode.TreeItem { constructor( public readonly app: { - name?: string - display_name?: string + name: string status?: string - icon?: string url?: string - agent_id?: string - agent_name?: string + command?: string + workspace_name?: string }, ) { - super(app.icon || "$(pulse)", vscode.TreeItemCollapsibleState.None) - this.description = app.status || "Running" + super(app.name, vscode.TreeItemCollapsibleState.None) + this.description = app.status this.contextValue = "coderAppStatus" // Add command to handle clicking on the app @@ -449,7 +453,16 @@ class AgentTreeItem extends OpenableTreeItem { } export class WorkspaceTreeItem extends OpenableTreeItem { - public appStatus: { name: string; status: string; icon?: string }[] = [] + public appStatus: { + name: string + status: string + icon?: string + url?: string + agent_id?: string + agent_name?: string + command?: string + workspace_name?: string + }[] = [] constructor( public readonly workspace: Workspace, From 08c93ae6ca981180932c531f469c6d7a9c6670cc Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 1 Apr 2025 13:17:32 -0500 Subject: [PATCH 10/27] show apps in need of attention only when there are some to show --- src/commands.ts | 2 +- src/workspacesProvider.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 7635fbfc..c6edff76 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -418,7 +418,7 @@ export class Commands { // Launch and run command in terminal if command is provided if (app.command) { const terminal = vscode.window.createTerminal(`${app.name || "Application"} Status`) - terminal.show(false) + terminal.show(true) vscode.commands.executeCommand("workbench.action.toggleMaximizedPanel") // If workspace_name is provided, run coder ssh before the command if (app.workspace_name) { diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index f795b96d..baff46ba 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -273,8 +273,11 @@ export class WorkspaceProvider implements vscode.TreeDataProvider 0) { + const appStatusSection = new SectionTreeItem("Applications in need of attention", needsAttention) + items.push(appStatusSection) + } } const savedMetadata = watcher?.metadata || [] From 99f3b1d8eea467bf1f9fd4ba61be28eb9b9ad100 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 1 Apr 2025 17:55:14 -0500 Subject: [PATCH 11/27] working with goose and claude --- src/workspacesProvider.ts | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index baff46ba..3de35e42 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -255,27 +255,26 @@ export class WorkspaceProvider implements vscode.TreeDataProvider 0) { - const needsAttention = [] + const appStatuses = [] for (const app of element.agent.apps) { if (app.statuses && app.statuses.length > 0) { for (const status of app.statuses) { - if (status.needs_user_attention) { - needsAttention.push( - new AppStatusTreeItem({ - name: status.message, - command: app.command, - status: status.state, - workspace_name: element.workspaceName, - }), - ) - } + // Show all statuses, not just ones needing attention + appStatuses.push( + new AppStatusTreeItem({ + name: status.message, + command: app.command, + status: status.state, + workspace_name: element.workspaceName, + }), + ) } } } - // Only show the section if it has items that need attention - if (needsAttention.length > 0) { - const appStatusSection = new SectionTreeItem("Applications in need of attention", needsAttention) + // Show the section if it has any items + if (appStatuses.length > 0) { + const appStatusSection = new SectionTreeItem("Application Statuses", appStatuses) items.push(appStatusSection) } } @@ -353,7 +352,7 @@ class SectionTreeItem extends vscode.TreeItem { label: string, public readonly children: vscode.TreeItem[], ) { - super(label, vscode.TreeItemCollapsibleState.Expanded) + super(label, vscode.TreeItemCollapsibleState.Collapsed) this.contextValue = "coderSectionHeader" } } From cd7c68c3f33b6bfb8912925f2f1d160aed262fcc Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 1 Apr 2025 18:27:29 -0500 Subject: [PATCH 12/27] switch up labels --- src/workspacesProvider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index 3de35e42..91760fc8 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -262,9 +262,9 @@ export class WorkspaceProvider implements vscode.TreeDataProvider Date: Tue, 1 Apr 2025 18:31:23 -0500 Subject: [PATCH 13/27] remove hand raise emojis --- src/workspacesProvider.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index 91760fc8..888f0b39 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -259,7 +259,9 @@ export class WorkspaceProvider implements vscode.TreeDataProvider 0) { for (const status of app.statuses) { - // Show all statuses, not just ones needing attention + // 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.icon, @@ -449,7 +451,7 @@ class AgentTreeItem extends OpenableTreeItem { if (agent.apps && agent.apps.length > 0) { // Add an icon to indicate this agent has running apps - this.label = "🖐️ " + this.label + this.label = this.label } } } From ac8d5fdca1c5c0baea61f4d12e57dfd65704fa26 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 1 Apr 2025 18:31:53 -0500 Subject: [PATCH 14/27] app not application --- src/workspacesProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index 888f0b39..3836fb4a 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -276,7 +276,7 @@ export class WorkspaceProvider implements vscode.TreeDataProvider 0) { - const appStatusSection = new SectionTreeItem("Application Statuses", appStatuses) + const appStatusSection = new SectionTreeItem("App Statuses", appStatuses) items.push(appStatusSection) } } From 69f9b97746345183ec5c0b88f9b9f05efc480092 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 1 Apr 2025 18:43:01 -0500 Subject: [PATCH 15/27] remove unused commands --- package.json | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/package.json b/package.json index d9a9ed78..a8292e61 100644 --- a/package.json +++ b/package.json @@ -170,11 +170,6 @@ "title": "Coder: Open Workspace", "icon": "$(play)" }, - { - "command": "coder.openFromSidebarAndOpenSession", - "title": "Coder: Open Workspace with Claude Code Session", - "icon": "$(terminal)" - }, { "command": "coder.createWorkspace", "title": "Create Workspace", @@ -242,11 +237,6 @@ "command": "coder.refreshWorkspaces", "when": "coder.authenticated && view == myWorkspaces", "group": "navigation" - }, - { - "command": "coder.openAppStatus", - "when": "coder.authenticated && view == myWorkspaces", - "group": "navigation" } ], "view/item/context": [ @@ -255,11 +245,6 @@ "when": "coder.authenticated && viewItem == coderWorkspaceSingleAgent || coder.authenticated && viewItem == coderAgent", "group": "inline" }, - { - "command": "coder.openFromSidebarAndOpenSession", - "when": "coder.authenticated && viewItem == coderWorkspaceSingleAgent || coder.authenticated && viewItem == coderAgent", - "group": "inline" - }, { "command": "coder.navigateToWorkspace", "when": "coder.authenticated && viewItem == coderWorkspaceSingleAgent || coder.authenticated && viewItem == coderWorkspaceMultipleAgents", From 68c2e04bf7ab380d951da357233739e2dd202b3e Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 1 Apr 2025 18:48:15 -0500 Subject: [PATCH 16/27] terminal names --- src/commands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands.ts b/src/commands.ts index c6edff76..f75d4174 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -417,7 +417,7 @@ export class Commands { }): Promise { // Launch and run command in terminal if command is provided if (app.command) { - const terminal = vscode.window.createTerminal(`${app.name || "Application"} Status`) + const terminal = vscode.window.createTerminal(app.status) terminal.show(true) vscode.commands.executeCommand("workbench.action.toggleMaximizedPanel") // If workspace_name is provided, run coder ssh before the command From b1e281cc3e0f6da9b8e983a75f4c78a4651f315d Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 1 Apr 2025 23:09:56 -0500 Subject: [PATCH 17/27] use built in coder cli --- src/commands.ts | 12 ++++++++++-- yarn.lock | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index f75d4174..026ca286 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -8,6 +8,7 @@ import { CertificateError } from "./error" import { Storage } from "./storage" import { AuthorityPrefix, toSafeHost } from "./util" import { OpenableTreeItem } from "./workspacesProvider" +import path from "node:path" export class Commands { // These will only be populated when actively connected to a workspace and are @@ -422,8 +423,15 @@ export class Commands { vscode.commands.executeCommand("workbench.action.toggleMaximizedPanel") // If workspace_name is provided, run coder ssh before the command if (app.workspace_name) { - terminal.sendText(`coder ssh ${app.workspace_name}`) - // Sleep for 5 seconds + let url = this.storage.getUrl() + if (!url) { + throw new Error("No coder url found for sidebar"); + } + let 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) } else { diff --git a/yarn.lock b/yarn.lock index 92284aee..ea065f24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1594,7 +1594,7 @@ co@3.1.0: "coder@https://github.com/coder/coder#main": version "0.0.0" - resolved "https://github.com/coder/coder#3a243c111b9abb5c38328169ff70064025bbe2fe" + resolved "https://github.com/coder/coder#2efb8088f4d923d1884fe8947dc338f9d179693b" collapse-white-space@^1.0.2: version "1.0.6" From 22246b92f1ff09b5e40af2ff1f6e7dd5739e283d Mon Sep 17 00:00:00 2001 From: Benjamin Date: Mon, 21 Apr 2025 15:53:42 -0500 Subject: [PATCH 18/27] reset back to working state pre tmux and reverse app statuses so most recent report is on top --- src/workspacesProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index 3836fb4a..a4294b49 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -276,7 +276,7 @@ export class WorkspaceProvider implements vscode.TreeDataProvider 0) { - const appStatusSection = new SectionTreeItem("App Statuses", appStatuses) + const appStatusSection = new SectionTreeItem("App Statuses", appStatuses.reverse()) items.push(appStatusSection) } } From 80f74f9a64cda40202572b945553c551eb36926d Mon Sep 17 00:00:00 2001 From: Benjamin Date: Mon, 21 Apr 2025 15:56:38 -0500 Subject: [PATCH 19/27] only show terminal after ssh command and app commands run --- src/commands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands.ts b/src/commands.ts index bce860ad..f827d6b3 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -419,7 +419,6 @@ export class Commands { // Launch and run command in terminal if command is provided if (app.command) { const terminal = vscode.window.createTerminal(app.status) - terminal.show(true) vscode.commands.executeCommand("workbench.action.toggleMaximizedPanel") // If workspace_name is provided, run coder ssh before the command if (app.workspace_name) { @@ -437,6 +436,7 @@ export class Commands { } else { terminal.sendText("need workspace name") } + terminal.show(false) return } // Check if app has a URL to open From 0f7dd65dc29f6d59c9ef5459a1e4a5c47958af16 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Mon, 21 Apr 2025 16:06:10 -0500 Subject: [PATCH 20/27] loading indicator --- src/commands.ts | 55 ++++++++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index f827d6b3..ca449059 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -418,31 +418,44 @@ export class Commands { }): Promise { // Launch and run command in terminal if command is provided if (app.command) { - const terminal = vscode.window.createTerminal(app.status) - vscode.commands.executeCommand("workbench.action.toggleMaximizedPanel") - // If workspace_name is provided, run coder ssh before the command - if (app.workspace_name) { - let url = this.storage.getUrl() - if (!url) { - throw new Error("No coder url found for sidebar"); + return vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: `Launching ${app.name || "application"}...`, + cancellable: false + }, async () => { + const terminal = vscode.window.createTerminal(app.status) + + // If workspace_name is provided, run coder ssh before the command + if (app.workspace_name) { + let url = this.storage.getUrl() + if (!url) { + throw new Error("No coder url found for sidebar"); + } + let 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 ?? "") + } else { + terminal.sendText("need workspace name") } - let 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) - } else { - terminal.sendText("need workspace name") - } - terminal.show(false) - return + + // Maximise the terminal and switch focus to the launch terminal window. + vscode.commands.executeCommand("workbench.action.toggleMaximizedPanel") + terminal.show(false) + }); } // Check if app has a URL to open if (app.url) { - await vscode.env.openExternal(vscode.Uri.parse(app.url)) - return + 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 From 8f996dd18f432c7a42611efdd5de7b9c56e2cc93 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Mon, 21 Apr 2025 16:07:45 -0500 Subject: [PATCH 21/27] update loading msg --- src/commands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands.ts b/src/commands.ts index ca449059..27ed5b48 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -420,7 +420,7 @@ export class Commands { if (app.command) { return vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, - title: `Launching ${app.name || "application"}...`, + title: `Connecting to AI Agent...`, cancellable: false }, async () => { const terminal = vscode.window.createTerminal(app.status) From c24b675dd4b501d406d7fc8c4a3098b6fee85e6d Mon Sep 17 00:00:00 2001 From: Benjamin Date: Mon, 21 Apr 2025 16:18:58 -0500 Subject: [PATCH 22/27] don't mess with terminal size --- src/commands.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 27ed5b48..9a4752e5 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -441,9 +441,6 @@ export class Commands { } else { terminal.sendText("need workspace name") } - - // Maximise the terminal and switch focus to the launch terminal window. - vscode.commands.executeCommand("workbench.action.toggleMaximizedPanel") terminal.show(false) }); } From 6f83606730f7f848cab99ca9fad449b45dea1084 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Mon, 21 Apr 2025 16:24:30 -0500 Subject: [PATCH 23/27] workspace name isn't optional --- src/commands.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 9a4752e5..7a472f4b 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -414,7 +414,7 @@ export class Commands { url?: string agent_name?: string command?: string - workspace_name?: string + workspace_name: string }): Promise { // Launch and run command in terminal if command is provided if (app.command) { @@ -426,7 +426,7 @@ export class Commands { const terminal = vscode.window.createTerminal(app.status) // If workspace_name is provided, run coder ssh before the command - if (app.workspace_name) { + let url = this.storage.getUrl() if (!url) { throw new Error("No coder url found for sidebar"); @@ -438,9 +438,6 @@ export class Commands { )} ${app.workspace_name}`) await new Promise((resolve) => setTimeout(resolve, 5000)) terminal.sendText(app.command ?? "") - } else { - terminal.sendText("need workspace name") - } terminal.show(false) }); } From 579ad4ebbc0a0df8a02e085251cdc8f3530da3ce Mon Sep 17 00:00:00 2001 From: Benjamin Date: Mon, 21 Apr 2025 16:28:18 -0500 Subject: [PATCH 24/27] changelog and format --- CHANGELOG.md | 4 ++++ src/commands.ts | 28 ++++++++++++++-------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14f3a583..6b1c4409 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 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 diff --git a/src/commands.ts b/src/commands.ts index 7a472f4b..bc14a00f 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -26,7 +26,7 @@ export class Commands { private readonly vscodeProposed: typeof vscode, private readonly restClient: Api, private readonly storage: Storage, - ) {} + ) { } /** * Find the requested agent if specified, otherwise return the agent if there @@ -424,20 +424,20 @@ export class Commands { cancellable: false }, async () => { const terminal = vscode.window.createTerminal(app.status) - + // If workspace_name is provided, run coder ssh before the command - - let url = this.storage.getUrl() - if (!url) { - throw new Error("No coder url found for sidebar"); - } - let 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 ?? "") + + let url = this.storage.getUrl() + if (!url) { + throw new Error("No coder url found for sidebar"); + } + let 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) }); } From 9340b7dff4919ba8525ff0c20bf23f91151f5476 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Mon, 21 Apr 2025 16:41:50 -0500 Subject: [PATCH 25/27] remove unused icon code --- src/workspacesProvider.ts | 41 ++++++++++++++------------------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index a4294b49..68f69113 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -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" @@ -156,29 +156,20 @@ export class WorkspaceProvider implements vscode.TreeDataProvider { - // Check if agent has apps property with status reporting - if (agent.apps && Array.isArray(agent.apps)) { - workspaceTreeItem.appStatus = agent.apps.map((app) => ({ - name: app.display_name || app.name || "App", - status: app.status || "Running", - icon: app.icon || "$(pulse)", - url: app.url, - agent_id: agent.id, - agent_name: agent.name, - command: app.command, - workspace_name: workspace.name, - })) - } - }) - } catch (error) { - // Log the error but continue - we don't want to fail the whole tree if app status fails - this.storage.writeToCoderOutputChannel( - `Failed to get app status for workspace ${workspace.name}: ${errToStr(error, "unknown error")}`, - ) - } + 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 }), @@ -459,8 +450,6 @@ class AgentTreeItem extends OpenableTreeItem { export class WorkspaceTreeItem extends OpenableTreeItem { public appStatus: { name: string - status: string - icon?: string url?: string agent_id?: string agent_name?: string From 8dac372f0ebc44ba2b404c2d1429a484267d4c9f Mon Sep 17 00:00:00 2001 From: Benjamin Date: Mon, 21 Apr 2025 16:49:03 -0500 Subject: [PATCH 26/27] cleanup --- src/commands.ts | 5 ++--- src/workspacesProvider.ts | 8 +++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index bc14a00f..63be87d2 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -410,7 +410,6 @@ export class Commands { public async openAppStatus(app: { name?: string - status?: string url?: string agent_name?: string command?: string @@ -423,7 +422,7 @@ export class Commands { title: `Connecting to AI Agent...`, cancellable: false }, async () => { - const terminal = vscode.window.createTerminal(app.status) + const terminal = vscode.window.createTerminal(app.name) // If workspace_name is provided, run coder ssh before the command @@ -453,7 +452,7 @@ export class Commands { } // If no URL or command, show information about the app status - vscode.window.showInformationMessage(`${app.name || "Application"}: ${app.status || "Running"}`, { + vscode.window.showInformationMessage(`${app.name}`, { detail: `Agent: ${app.agent_name || "Unknown"}`, }) } diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index 68f69113..dff3bbd7 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -255,9 +255,8 @@ export class WorkspaceProvider implements vscode.TreeDataProvider Date: Mon, 21 Apr 2025 17:07:17 -0500 Subject: [PATCH 27/27] remove unnecessary label assignment (I think there used to be an icon there) --- CHANGELOG.md | 1 + src/commands.ts | 70 ++++++++++++++++++++++----------------- src/workspacesProvider.ts | 7 +--- 3 files changed, 41 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b1c4409..aeba3f32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 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. diff --git a/src/commands.ts b/src/commands.ts index 63be87d2..830347e0 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -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" @@ -8,7 +9,6 @@ import { CertificateError } from "./error" import { Storage } from "./storage" import { toRemoteAuthority, toSafeHost } from "./util" import { OpenableTreeItem } from "./workspacesProvider" -import path from "node:path" export class Commands { // These will only be populated when actively connected to a workspace and are @@ -26,7 +26,7 @@ export class Commands { private readonly vscodeProposed: typeof vscode, private readonly restClient: Api, private readonly storage: Storage, - ) { } + ) {} /** * Find the requested agent if specified, otherwise return the agent if there @@ -417,38 +417,46 @@ export class Commands { }): Promise { // 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 - - let url = this.storage.getUrl() - if (!url) { - throw new Error("No coder url found for sidebar"); - } - let 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) - }); + 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!)) - }); + 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 diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index dff3bbd7..0f821a2f 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -251,7 +251,7 @@ export class WorkspaceProvider implements vscode.TreeDataProvider 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 + // We need to do this for now because the reporting isn't super accurate // yet. appStatuses.push( new AppStatusTreeItem({ @@ -437,11 +437,6 @@ class AgentTreeItem extends OpenableTreeItem { agent.expanded_directory, "coderAgent", ) - - if (agent.apps && agent.apps.length > 0) { - // Add an icon to indicate this agent has running apps - this.label = this.label - } } }