@@ -258,11 +258,27 @@ export class NextEditEngine implements vscode.Disposable {
258258 await this . advance ( ) ;
259259 }
260260
261- /** Esc: dismiss. */
261+ /** Esc: dismiss. Also cancels any in-flight scan even before a session is established. */
262262 public dismiss ( reason = 'user' ) : void {
263- if ( ! this . session ) return ;
263+ const hadScan = ! ! this . scanCts ;
264+ if ( ! this . session ) {
265+ // No active candidate session, but a scan may still be in-flight
266+ // (e.g. the user pressed Esc, or Next Edit was just disabled, while
267+ // the AI request is mid-flight). Cancel the scan and clear the
268+ // status bar's spinner so the loading state doesn't get stuck.
269+ if ( hadScan ) {
270+ log ( `scan cancelled (no session): ${ reason } ` ) ;
271+ this . scanCts ?. cancel ( ) ;
272+ this . scanCts ?. dispose ( ) ;
273+ this . scanCts = undefined ;
274+ this . statusBar . setStatus ( 'idle' ) ;
275+ }
276+ return ;
277+ }
264278 log ( `session dismissed: ${ reason } ` ) ;
265279 this . scanCts ?. cancel ( ) ;
280+ this . scanCts ?. dispose ( ) ;
281+ this . scanCts = undefined ;
266282 this . session = undefined ;
267283 this . deco . clear ( ) ;
268284 this . statusBar . setStatus ( 'idle' ) ;
@@ -272,20 +288,41 @@ export class NextEditEngine implements vscode.Disposable {
272288 private async onEdit ( edit : RecentEdit , manual = false ) : Promise < void > {
273289 if ( ! this . config . enabled && ! manual ) return ;
274290 this . scanCts ?. cancel ( ) ;
275- this . scanCts = new vscode . CancellationTokenSource ( ) ;
276- const token = this . scanCts . token ;
291+ this . scanCts ?. dispose ( ) ;
292+ const cts = new vscode . CancellationTokenSource ( ) ;
293+ this . scanCts = cts ;
294+ const token = cts . token ;
277295
278296 this . statusBar . setStatus ( 'scanning' ) ;
279297 let candidates : Candidate [ ] ;
280298 try {
281299 candidates = await this . finder . find ( edit , token ) ;
282300 } catch ( err ) {
283301 log ( `scan error: ${ ( err as Error ) ?. message ?? err } ` ) ;
284- this . statusBar . setMessage ( 'scan failed' ) ;
285- this . statusBar . setStatus ( 'idle' ) ;
302+ // Only touch the status bar if our scan is still the active one.
303+ // A newer onEdit may have already replaced us and set its own state.
304+ if ( this . scanCts === cts ) {
305+ this . scanCts = undefined ;
306+ this . statusBar . setMessage ( 'scan failed' ) ;
307+ this . statusBar . setStatus ( 'idle' ) ;
308+ }
286309 return ;
287310 }
288- if ( token . isCancellationRequested ) return ;
311+ if ( token . isCancellationRequested ) {
312+ // We got cancelled (new edit, dismiss, or dispose). If a newer scan
313+ // has already taken over (this.scanCts !== cts), leave the status
314+ // alone — the newer scan owns it. Otherwise restore idle so the
315+ // spinner doesn't stay forever after a silent abort.
316+ if ( this . scanCts === cts ) {
317+ this . scanCts = undefined ;
318+ this . statusBar . setStatus ( 'idle' ) ;
319+ }
320+ return ;
321+ }
322+ // Scan finished naturally; release ownership of the cts.
323+ if ( this . scanCts === cts ) {
324+ this . scanCts = undefined ;
325+ }
289326
290327 if ( candidates . length === 0 ) {
291328 log ( 'no candidates' ) ;
0 commit comments