Skip to content

Commit f3cceef

Browse files
authored
feat: implement proper LLM request cancellation on ESC (#95)
* feat: implement proper LLM request cancellation on ESC - Add streaming cancellation as Priority 2 in handle_esc() before checking for idle state - Add priority handling for queued messages, pending approvals, and autocomplete visibility - Reset ESC timer after cancellation to prevent accidental quit - Use tokio::select! in streaming loops for faster cancellation response (50ms polling interval) * fix: remove needless return statements for Clippy
1 parent d5b4ba6 commit f3cceef

File tree

3 files changed

+196
-161
lines changed

3 files changed

+196
-161
lines changed

src/cortex-tui/src/runner/event_loop/core.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,9 @@ impl EventLoop {
446446
task.abort();
447447
}
448448

449+
// Reset ESC timer after cancellation to prevent accidental quit
450+
self.app_state.reset_esc();
451+
449452
self.stream_controller.reset();
450453
}
451454
}

src/cortex-tui/src/runner/event_loop/input.rs

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -368,24 +368,45 @@ impl EventLoop {
368368
return Ok(());
369369
}
370370

371-
// Check if app is idle (nothing active that ESC should cancel)
372-
let is_idle = !self.app_state.streaming.is_streaming
373-
&& self.app_state.pending_approval.is_none()
374-
&& !self.app_state.has_queued_messages()
375-
&& !self.app_state.autocomplete.visible;
376-
377-
if is_idle {
378-
if self.app_state.handle_esc() {
379-
self.app_state.set_quit();
380-
return Ok(());
381-
} else {
382-
self.app_state.toasts.info("Press ESC again to quit");
383-
self.render(terminal)?;
384-
return Ok(());
385-
}
371+
// Priority 2: If streaming is active, cancel it
372+
if self.app_state.streaming.is_streaming {
373+
self.cancel_streaming();
374+
self.render(terminal)?;
375+
return Ok(());
386376
}
387377

388-
Ok(())
378+
// Priority 3: If there are queued messages, cancel them
379+
if self.app_state.has_queued_messages() {
380+
let count = self.app_state.queued_count();
381+
self.app_state.clear_message_queue();
382+
self.add_system_message(&format!("Cancelled {} queued message(s)", count));
383+
self.render(terminal)?;
384+
return Ok(());
385+
}
386+
387+
// Priority 4: If there's pending approval, reject it
388+
if self.app_state.pending_approval.is_some() {
389+
self.app_state.reject();
390+
self.render(terminal)?;
391+
return Ok(());
392+
}
393+
394+
// Priority 5: If autocomplete is visible, hide it (already handled in handle_autocomplete_key)
395+
if self.app_state.autocomplete.visible {
396+
self.app_state.autocomplete.hide();
397+
self.render(terminal)?;
398+
return Ok(());
399+
}
400+
401+
// Priority 6: Double-tap ESC to quit when idle
402+
if self.app_state.handle_esc() {
403+
self.app_state.set_quit();
404+
Ok(())
405+
} else {
406+
self.app_state.toasts.info("Press ESC again to quit");
407+
self.render(terminal)?;
408+
Ok(())
409+
}
389410
}
390411

391412
/// Handle autocomplete navigation keys

0 commit comments

Comments
 (0)