33 type WorkspaceAgent ,
44} from "coder/site/src/api/typesGenerated" ;
55import * as fs from "node:fs/promises" ;
6+ import * as os from "node:os" ;
67import * as path from "node:path" ;
78import * as semver from "semver" ;
89import * as vscode from "vscode" ;
@@ -22,7 +23,7 @@ import { type SecretsManager } from "./core/secretsManager";
2223import { type DeploymentManager } from "./deployment/deploymentManager" ;
2324import { CertificateError } from "./error/certificateError" ;
2425import { toError } from "./error/errorUtils" ;
25- import { featureSetForVersion } from "./featureSet" ;
26+ import { type FeatureSet , featureSetForVersion } from "./featureSet" ;
2627import { type Logger } from "./logging/logger" ;
2728import { type LoginCoordinator } from "./login/loginCoordinator" ;
2829import { withCancellableProgress , withProgress } from "./progress" ;
@@ -196,15 +197,17 @@ export class Commands {
196197 const trimmedDuration = duration . trim ( ) ;
197198
198199 const result = await withCancellableProgress (
199- async ( { signal } ) => {
200+ async ( { signal, progress } ) => {
201+ progress . report ( { message : "Resolving CLI..." } ) ;
200202 const env = await this . resolveCliEnv ( client ) ;
203+ progress . report ( { message : "Running..." } ) ;
201204 return cliExec . speedtest ( env , workspaceId , trimmedDuration , signal ) ;
202205 } ,
203206 {
204207 location : vscode . ProgressLocation . Notification ,
205208 title : trimmedDuration
206- ? `Running speed test (${ trimmedDuration } )... `
207- : "Running speed test..." ,
209+ ? `Speed test for ${ workspaceId } (${ trimmedDuration } )`
210+ : `Speed test for ${ workspaceId } ` ,
208211 cancellable : true ,
209212 } ,
210213 ) ;
@@ -228,6 +231,70 @@ export class Commands {
228231 ) ;
229232 }
230233
234+ public async supportBundle ( item ?: OpenableTreeItem ) : Promise < void > {
235+ const resolved = await this . resolveClientAndWorkspace ( item ) ;
236+ if ( ! resolved ) {
237+ return ;
238+ }
239+
240+ const { client, workspaceId } = resolved ;
241+
242+ const outputUri = await this . promptSupportBundlePath ( ) ;
243+ if ( ! outputUri ) {
244+ return ;
245+ }
246+
247+ const result = await withCancellableProgress (
248+ async ( { signal, progress } ) => {
249+ progress . report ( { message : "Resolving CLI..." } ) ;
250+ const env = await this . resolveCliEnv ( client ) ;
251+ if ( ! env . featureSet . supportBundle ) {
252+ throw new Error (
253+ "Support bundles require Coder CLI v2.10.0 or later. Please update your Coder deployment." ,
254+ ) ;
255+ }
256+
257+ progress . report ( { message : "Collecting diagnostics..." } ) ;
258+ await cliExec . supportBundle ( env , workspaceId , outputUri . fsPath , signal ) ;
259+ return outputUri ;
260+ } ,
261+ {
262+ location : vscode . ProgressLocation . Notification ,
263+ title : `Creating support bundle for ${ workspaceId } ` ,
264+ cancellable : true ,
265+ } ,
266+ ) ;
267+
268+ if ( result . ok ) {
269+ const action = await vscode . window . showInformationMessage (
270+ `Support bundle saved to ${ result . value . fsPath } ` ,
271+ "Reveal in File Explorer" ,
272+ ) ;
273+ if ( action === "Reveal in File Explorer" ) {
274+ await vscode . commands . executeCommand ( "revealFileInOS" , result . value ) ;
275+ }
276+ return ;
277+ }
278+
279+ if ( result . cancelled ) {
280+ return ;
281+ }
282+
283+ this . logger . error ( "Support bundle failed" , result . error ) ;
284+ vscode . window . showErrorMessage (
285+ `Support bundle failed: ${ toError ( result . error ) . message } ` ,
286+ ) ;
287+ }
288+
289+ private promptSupportBundlePath ( ) : Thenable < vscode . Uri | undefined > {
290+ const defaultName = `coder-support-${ Math . floor ( Date . now ( ) / 1000 ) } .zip` ;
291+ return vscode . window . showSaveDialog ( {
292+ defaultUri : vscode . Uri . file ( path . join ( os . homedir ( ) , defaultName ) ) ,
293+ filters : { "Zip files" : [ "zip" ] } ,
294+ title : "Save Support Bundle" ,
295+ } ) ;
296+ }
297+
231298 /**
232299 * View the logs for the currently connected workspace.
233300 */
@@ -720,8 +787,10 @@ export class Commands {
720787 location : vscode . ProgressLocation . Notification ,
721788 title : `Starting ping for ${ workspaceId } ...` ,
722789 } ,
723- async ( ) => {
790+ async ( progress ) => {
791+ progress . report ( { message : "Resolving CLI..." } ) ;
724792 const env = await this . resolveCliEnv ( client ) ;
793+ progress . report ( { message : "Starting..." } ) ;
725794 cliExec . ping ( env , workspaceId ) ;
726795 } ,
727796 ) ;
@@ -763,7 +832,9 @@ export class Commands {
763832 }
764833
765834 /** Resolve a CliEnv, preferring a locally cached binary over a network fetch. */
766- private async resolveCliEnv ( client : CoderApi ) : Promise < cliExec . CliEnv > {
835+ private async resolveCliEnv (
836+ client : CoderApi ,
837+ ) : Promise < cliExec . CliEnv & { featureSet : FeatureSet } > {
767838 const baseUrl = client . getAxiosInstance ( ) . defaults . baseURL ;
768839 if ( ! baseUrl ) {
769840 throw new Error ( "You are not logged in" ) ;
@@ -780,7 +851,7 @@ export class Commands {
780851 const configDir = this . pathResolver . getGlobalConfigDir ( safeHost ) ;
781852 const configs = vscode . workspace . getConfiguration ( ) ;
782853 const auth = resolveCliAuth ( configs , featureSet , baseUrl , configDir ) ;
783- return { binary, configs, auth } ;
854+ return { binary, configs, auth, featureSet } ;
784855 }
785856
786857 /**
0 commit comments