@@ -12,11 +12,6 @@ import { type Deployment, type DeploymentWithAuth } from "./types";
1212import type { User } from "coder/site/src/api/typesGenerated" ;
1313import type * as vscode from "vscode" ;
1414
15- /**
16- * Internal state type that allows mutation of user property.
17- */
18- type DeploymentWithUser = Deployment & { user : User } ;
19-
2015/**
2116 * Manages deployment state for the extension.
2217 *
@@ -35,7 +30,7 @@ export class DeploymentManager implements vscode.Disposable {
3530 private readonly contextManager : ContextManager ;
3631 private readonly logger : Logger ;
3732
38- #deployment: DeploymentWithUser | null = null ;
33+ #deployment: Deployment | null = null ;
3934 #authListenerDisposable: vscode . Disposable | undefined ;
4035 #crossWindowSyncDisposable: vscode . Disposable | undefined ;
4136
@@ -78,7 +73,7 @@ export class DeploymentManager implements vscode.Disposable {
7873 * Check if we have an authenticated deployment (does not guarantee that the current auth data is valid).
7974 */
8075 public isAuthenticated ( ) : boolean {
81- return this . #deployment !== null ;
76+ return this . contextManager . get ( "coder.authenticated" ) ;
8277 }
8378
8479 /**
@@ -89,10 +84,10 @@ export class DeploymentManager implements vscode.Disposable {
8984 public async setDeploymentIfValid (
9085 deployment : Deployment & { token ?: string } ,
9186 ) : Promise < boolean > {
92- const auth = await this . secretsManager . getSessionAuth (
93- deployment . safeHostname ,
94- ) ;
95- const token = deployment . token ?? auth ?. token ;
87+ const token =
88+ deployment . token ??
89+ ( await this . secretsManager . getSessionAuth ( deployment . safeHostname ) )
90+ ?. token ;
9691 const tempClient = CoderApi . create ( deployment . url , token , this . logger ) ;
9792
9893 try {
@@ -132,7 +127,8 @@ export class DeploymentManager implements vscode.Disposable {
132127 // Register auth listener before setDeployment so background token refresh
133128 // can update client credentials via the listener
134129 this . registerAuthListener ( ) ;
135- this . updateAuthContexts ( ) ;
130+ // Contexts must be set before refresh (providers check isAuthenticated)
131+ this . updateAuthContexts ( deployment . user ) ;
136132 this . refreshWorkspaces ( ) ;
137133
138134 await this . oauthSessionManager . setDeployment ( deployment ) ;
@@ -143,16 +139,32 @@ export class DeploymentManager implements vscode.Disposable {
143139 * Clears the current deployment.
144140 */
145141 public async clearDeployment ( ) : Promise < void > {
142+ this . suspendSession ( ) ;
146143 this . #authListenerDisposable?. dispose ( ) ;
147144 this . #authListenerDisposable = undefined ;
148145 this . #deployment = null ;
149146
150- this . client . setCredentials ( undefined , undefined ) ;
147+ await this . secretsManager . setCurrentDeployment ( undefined ) ;
148+ }
149+
150+ /**
151+ * Suspend session: shows logged-out state but keeps deployment for easy re-login.
152+ * Auth listener remains active so recovery can happen automatically if tokens update.
153+ */
154+ public suspendSession ( ) : void {
151155 this . oauthSessionManager . clearDeployment ( ) ;
152- this . updateAuthContexts ( ) ;
153- this . refreshWorkspaces ( ) ;
156+ this . client . setCredentials ( undefined , undefined ) ;
157+ this . updateAuthContexts ( undefined ) ;
158+ this . clearWorkspaces ( ) ;
159+ }
154160
155- await this . secretsManager . setCurrentDeployment ( undefined ) ;
161+ /**
162+ * Clear all workspace providers without fetching.
163+ */
164+ private clearWorkspaces ( ) : void {
165+ for ( const provider of this . workspaceProviders ) {
166+ provider . clear ( ) ;
167+ }
156168 }
157169
158170 public dispose ( ) : void {
@@ -163,6 +175,7 @@ export class DeploymentManager implements vscode.Disposable {
163175 /**
164176 * Register auth listener for the current deployment.
165177 * Updates credentials when they change (token refresh, cross-window sync).
178+ * Also handles recovery from suspended session state.
166179 */
167180 private registerAuthListener ( ) : void {
168181 if ( ! this . #deployment) {
@@ -182,7 +195,18 @@ export class DeploymentManager implements vscode.Disposable {
182195 }
183196
184197 if ( auth ) {
185- this . client . setCredentials ( auth . url , auth . token ) ;
198+ if ( this . isAuthenticated ( ) ) {
199+ this . client . setCredentials ( auth . url , auth . token ) ;
200+ } else {
201+ this . logger . debug (
202+ "Token updated after session suspended, recovering" ,
203+ ) ;
204+ await this . setDeploymentIfValid ( {
205+ url : auth . url ,
206+ safeHostname,
207+ token : auth . token ,
208+ } ) ;
209+ }
186210 } else {
187211 await this . clearDeployment ( ) ;
188212 }
@@ -210,8 +234,7 @@ export class DeploymentManager implements vscode.Disposable {
210234 /**
211235 * Update authentication-related contexts.
212236 */
213- private updateAuthContexts ( ) : void {
214- const user = this . #deployment?. user ;
237+ private updateAuthContexts ( user : User | undefined ) : void {
215238 this . contextManager . set ( "coder.authenticated" , Boolean ( user ) ) ;
216239 const isOwner = user ?. roles . some ( ( r ) => r . name === "owner" ) ?? false ;
217240 this . contextManager . set ( "coder.isOwner" , isOwner ) ;
0 commit comments