From 4ba577333e0836460923feb09d8f17d7e424f11a Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Wed, 16 Jul 2025 23:35:23 +0300 Subject: [PATCH 1/3] Add agent metadata statusbar to monitor resource usage --- src/agentMetadataHelper.ts | 81 ++++++++++++++++++++++++++++++++++++++ src/remote.ts | 52 +++++++++++++++++++++++- src/workspacesProvider.ts | 79 ++++++------------------------------- 3 files changed, 143 insertions(+), 69 deletions(-) create mode 100644 src/agentMetadataHelper.ts diff --git a/src/agentMetadataHelper.ts b/src/agentMetadataHelper.ts new file mode 100644 index 00000000..d7c746ef --- /dev/null +++ b/src/agentMetadataHelper.ts @@ -0,0 +1,81 @@ +import { Api } from "coder/site/src/api/api"; +import { WorkspaceAgent } from "coder/site/src/api/typesGenerated"; +import { EventSource } from "eventsource"; +import * as vscode from "vscode"; +import { createStreamingFetchAdapter } from "./api"; +import { + AgentMetadataEvent, + AgentMetadataEventSchemaArray, + errToStr, +} from "./api-helper"; + +export type AgentMetadataWatcher = { + onChange: vscode.EventEmitter["event"]; + dispose: () => void; + metadata?: AgentMetadataEvent[]; + error?: unknown; +}; + +/** + * Opens an SSE connection to watch metadata for a given workspace agent. + * Emits onChange when metadata updates or an error occurs. + */ +export function createAgentMetadataWatcher( + agentId: WorkspaceAgent["id"], + restClient: Api, +): AgentMetadataWatcher { + // TODO: Is there a better way to grab the url and token? + const url = restClient.getAxiosInstance().defaults.baseURL; + const metadataUrl = new URL( + `${url}/api/v2/workspaceagents/${agentId}/watch-metadata`, + ); + const eventSource = new EventSource(metadataUrl.toString(), { + fetch: createStreamingFetchAdapter(restClient.getAxiosInstance()), + }); + + let disposed = false; + const onChange = new vscode.EventEmitter(); + const watcher: AgentMetadataWatcher = { + onChange: onChange.event, + dispose: () => { + if (!disposed) { + eventSource.close(); + disposed = true; + } + }, + }; + + eventSource.addEventListener("data", (event) => { + try { + const dataEvent = JSON.parse(event.data); + const metadata = AgentMetadataEventSchemaArray.parse(dataEvent); + + // Overwrite metadata if it changed. + if (JSON.stringify(watcher.metadata) !== JSON.stringify(metadata)) { + watcher.metadata = metadata; + onChange.fire(null); + } + } catch (error) { + watcher.error = error; + onChange.fire(null); + } + }); + + return watcher; +} + +export function formatMetadataError(error: unknown): string { + return "Failed to query metadata: " + errToStr(error, "no error provided"); +} + +export function formatEventLabel(metadataEvent: AgentMetadataEvent): string { + return getEventName(metadataEvent) + ": " + getEventValue(metadataEvent); +} + +export function getEventName(metadataEvent: AgentMetadataEvent): string { + return metadataEvent.description.display_name.trim(); +} + +export function getEventValue(metadataEvent: AgentMetadataEvent): string { + return metadataEvent.result.value.replace(/\n/g, "").trim(); +} diff --git a/src/remote.ts b/src/remote.ts index 7ce460c9..f49d94c2 100644 --- a/src/remote.ts +++ b/src/remote.ts @@ -1,6 +1,6 @@ import { isAxiosError } from "axios"; import { Api } from "coder/site/src/api/api"; -import { Workspace } from "coder/site/src/api/typesGenerated"; +import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"; import find from "find-process"; import * as fs from "fs/promises"; import * as jsonc from "jsonc-parser"; @@ -9,6 +9,12 @@ import * as path from "path"; import prettyBytes from "pretty-bytes"; import * as semver from "semver"; import * as vscode from "vscode"; +import { + createAgentMetadataWatcher, + getEventValue, + formatEventLabel, + formatMetadataError, +} from "./agentMetadataHelper"; import { createHttpAgent, makeCoderSdk, @@ -624,6 +630,8 @@ export class Remote { }), ); + this.createAgentMetadataStatusBar(agent, workspaceRestClient, disposables); + this.storage.output.info("Remote setup complete"); // Returning the URL and token allows the plugin to authenticate its own @@ -966,6 +974,48 @@ export class Remote { return loop(); } + /** + * Creates and manages a status bar item that displays metadata information for a given workspace agent. + * The status bar item updates dynamically based on changes to the agent's metadata, + * and hides itself if no metadata is available or an error occurs. + */ + private createAgentMetadataStatusBar( + agent: WorkspaceAgent, + restClient: Api, + disposables: vscode.Disposable[], + ): void { + const statusBarItem = vscode.window.createStatusBarItem( + "agentMetadata", + vscode.StatusBarAlignment.Left, + ); + disposables.push(statusBarItem); + + const agentWatcher = createAgentMetadataWatcher(agent.id, restClient); + disposables.push(agentWatcher); + + agentWatcher.onChange( + () => { + if (agentWatcher.error) { + this.storage.output.warn(formatMetadataError(agentWatcher.error)); + statusBarItem.hide(); + return; + } + + if (agentWatcher.metadata && agentWatcher.metadata.length > 0) { + statusBarItem.text = getEventValue(agentWatcher.metadata[0]); + statusBarItem.tooltip = agentWatcher.metadata + .map((metadata) => formatEventLabel(metadata)) + .join("\n"); + statusBarItem.show(); + } else { + statusBarItem.hide(); + } + }, + undefined, + disposables, + ); + } + // closeRemote ends the current remote session. public async closeRemote() { await vscode.commands.executeCommand("workbench.action.remote.close"); diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index 64b74e7d..d0ddc6c7 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -4,16 +4,18 @@ import { WorkspaceAgent, WorkspaceApp, } 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 { + AgentMetadataWatcher, + createAgentMetadataWatcher, + formatEventLabel, + formatMetadataError, +} from "./agentMetadataHelper"; import { AgentMetadataEvent, - AgentMetadataEventSchemaArray, extractAllAgents, extractAgents, - errToStr, } from "./api-helper"; import { Storage } from "./storage"; @@ -22,13 +24,6 @@ export enum WorkspaceQuery { All = "", } -type AgentWatcher = { - onChange: vscode.EventEmitter["event"]; - dispose: () => void; - metadata?: AgentMetadataEvent[]; - error?: unknown; -}; - /** * Polls workspaces using the provided REST client and renders them in a tree. * @@ -42,7 +37,8 @@ export class WorkspaceProvider { // Undefined if we have never fetched workspaces before. private workspaces: WorkspaceTreeItem[] | undefined; - private agentWatchers: Record = {}; + private agentWatchers: Record = + {}; private timeout: NodeJS.Timeout | undefined; private fetching = false; private visible = false; @@ -139,7 +135,7 @@ export class WorkspaceProvider return this.agentWatchers[agent.id]; } // Otherwise create a new watcher. - const watcher = monitorMetadata(agent.id, restClient); + const watcher = createAgentMetadataWatcher(agent.id, restClient); watcher.onChange(() => this.refresh()); this.agentWatchers[agent.id] = watcher; return watcher; @@ -313,53 +309,6 @@ export class WorkspaceProvider } } -// monitorMetadata opens an SSE endpoint to monitor metadata on the specified -// agent and registers a watcher that can be disposed to stop the watch and -// emits an event when the metadata changes. -function monitorMetadata( - agentId: WorkspaceAgent["id"], - restClient: Api, -): AgentWatcher { - // TODO: Is there a better way to grab the url and token? - const url = restClient.getAxiosInstance().defaults.baseURL; - const metadataUrl = new URL( - `${url}/api/v2/workspaceagents/${agentId}/watch-metadata`, - ); - const eventSource = new EventSource(metadataUrl.toString(), { - fetch: createStreamingFetchAdapter(restClient.getAxiosInstance()), - }); - - let disposed = false; - const onChange = new vscode.EventEmitter(); - const watcher: AgentWatcher = { - onChange: onChange.event, - dispose: () => { - if (!disposed) { - eventSource.close(); - disposed = true; - } - }, - }; - - eventSource.addEventListener("data", (event) => { - try { - const dataEvent = JSON.parse(event.data); - const metadata = AgentMetadataEventSchemaArray.parse(dataEvent); - - // Overwrite metadata if it changed. - if (JSON.stringify(watcher.metadata) !== JSON.stringify(metadata)) { - watcher.metadata = metadata; - onChange.fire(null); - } - } catch (error) { - watcher.error = error; - onChange.fire(null); - } - }); - - return watcher; -} - /** * A tree item that represents a collapsible section with child items */ @@ -375,20 +324,14 @@ class SectionTreeItem extends vscode.TreeItem { class ErrorTreeItem extends vscode.TreeItem { constructor(error: unknown) { - super( - "Failed to query metadata: " + errToStr(error, "no error provided"), - vscode.TreeItemCollapsibleState.None, - ); + super(formatMetadataError(error), vscode.TreeItemCollapsibleState.None); this.contextValue = "coderAgentMetadata"; } } class AgentMetadataTreeItem extends vscode.TreeItem { constructor(metadataEvent: AgentMetadataEvent) { - const label = - metadataEvent.description.display_name.trim() + - ": " + - metadataEvent.result.value.replace(/\n/g, "").trim(); + const label = formatEventLabel(metadataEvent); super(label, vscode.TreeItemCollapsibleState.None); const collected_at = new Date( From 9d0d6104e67b19badfe2927dbb3f67b98cbc19c6 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Sat, 19 Jul 2025 23:24:05 +0300 Subject: [PATCH 2/3] Review comment: return a disposables array when creating the statusbar --- CHANGELOG.md | 2 ++ src/remote.ts | 47 ++++++++++++++++++++++------------------------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80371d86..d9d1e65a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ update notifications. - Coder output panel enhancements: All log entries now include timestamps, and you can filter messages by log level in the panel. +- Added an agent metadata monitor status bar item, so you can view your active + agent metadata at a glance. ## [v1.9.2](https://github.com/coder/vscode-coder/releases/tag/v1.9.2) 2025-06-25 diff --git a/src/remote.ts b/src/remote.ts index f49d94c2..809c424b 100644 --- a/src/remote.ts +++ b/src/remote.ts @@ -630,7 +630,9 @@ export class Remote { }), ); - this.createAgentMetadataStatusBar(agent, workspaceRestClient, disposables); + disposables.push( + ...this.createAgentMetadataStatusBar(agent, workspaceRestClient), + ); this.storage.output.info("Remote setup complete"); @@ -982,38 +984,33 @@ export class Remote { private createAgentMetadataStatusBar( agent: WorkspaceAgent, restClient: Api, - disposables: vscode.Disposable[], - ): void { + ): vscode.Disposable[] { const statusBarItem = vscode.window.createStatusBarItem( "agentMetadata", vscode.StatusBarAlignment.Left, ); - disposables.push(statusBarItem); const agentWatcher = createAgentMetadataWatcher(agent.id, restClient); - disposables.push(agentWatcher); - agentWatcher.onChange( - () => { - if (agentWatcher.error) { - this.storage.output.warn(formatMetadataError(agentWatcher.error)); - statusBarItem.hide(); - return; - } + const onChangeDisposable = agentWatcher.onChange(() => { + if (agentWatcher.error) { + this.storage.output.warn(formatMetadataError(agentWatcher.error)); + statusBarItem.hide(); + return; + } - if (agentWatcher.metadata && agentWatcher.metadata.length > 0) { - statusBarItem.text = getEventValue(agentWatcher.metadata[0]); - statusBarItem.tooltip = agentWatcher.metadata - .map((metadata) => formatEventLabel(metadata)) - .join("\n"); - statusBarItem.show(); - } else { - statusBarItem.hide(); - } - }, - undefined, - disposables, - ); + if (agentWatcher.metadata && agentWatcher.metadata.length > 0) { + statusBarItem.text = getEventValue(agentWatcher.metadata[0]); + statusBarItem.tooltip = agentWatcher.metadata + .map((metadata) => formatEventLabel(metadata)) + .join("\n"); + statusBarItem.show(); + } else { + statusBarItem.hide(); + } + }); + + return [statusBarItem, agentWatcher, onChangeDisposable]; } // closeRemote ends the current remote session. From dfc1a3ab95b9ae7c2f3d1261ada5a3fd4f4c8584 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Tue, 22 Jul 2025 21:46:55 +0300 Subject: [PATCH 3/3] Show the statusbar even when fetching metadata fails --- src/remote.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/remote.ts b/src/remote.ts index 809c424b..d5967c1d 100644 --- a/src/remote.ts +++ b/src/remote.ts @@ -994,16 +994,29 @@ export class Remote { const onChangeDisposable = agentWatcher.onChange(() => { if (agentWatcher.error) { - this.storage.output.warn(formatMetadataError(agentWatcher.error)); - statusBarItem.hide(); + const errMessage = formatMetadataError(agentWatcher.error); + this.storage.output.warn(errMessage); + + statusBarItem.text = "$(warning) Agent Status Unavailable"; + statusBarItem.tooltip = errMessage; + statusBarItem.color = new vscode.ThemeColor( + "statusBarItem.warningForeground", + ); + statusBarItem.backgroundColor = new vscode.ThemeColor( + "statusBarItem.warningBackground", + ); + statusBarItem.show(); return; } if (agentWatcher.metadata && agentWatcher.metadata.length > 0) { - statusBarItem.text = getEventValue(agentWatcher.metadata[0]); + statusBarItem.text = + "$(dashboard) " + getEventValue(agentWatcher.metadata[0]); statusBarItem.tooltip = agentWatcher.metadata .map((metadata) => formatEventLabel(metadata)) .join("\n"); + statusBarItem.color = undefined; + statusBarItem.backgroundColor = undefined; statusBarItem.show(); } else { statusBarItem.hide(); 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