diff --git a/CHANGELOG.md b/CHANGELOG.md index c32ffad0..3073ba28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ depending on how you connected, it could be possible to get two different sessions for an agent. Existing connections may still have this problem, only new connections are fixed. +- 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/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..d5967c1d 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,10 @@ export class Remote { }), ); + disposables.push( + ...this.createAgentMetadataStatusBar(agent, workspaceRestClient), + ); + this.storage.output.info("Remote setup complete"); // Returning the URL and token allows the plugin to authenticate its own @@ -966,6 +976,56 @@ 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, + ): vscode.Disposable[] { + const statusBarItem = vscode.window.createStatusBarItem( + "agentMetadata", + vscode.StatusBarAlignment.Left, + ); + + const agentWatcher = createAgentMetadataWatcher(agent.id, restClient); + + const onChangeDisposable = agentWatcher.onChange(() => { + if (agentWatcher.error) { + 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 = + "$(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(); + } + }); + + return [statusBarItem, agentWatcher, onChangeDisposable]; + } + // 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 59915e58..a33b43cc 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( 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