Symptoms
After a turn completes (the LLM finishes streaming, the final text is visible), the terminal becomes unresponsive for 1–5 seconds. During this window:
- Text selection is blocked or laggy
- Copy/paste doesn't register
- Keyboard input is queued but not processed
- The composer appears frozen
Once the post-turn work completes, the terminal snaps back to normal.
Likely causes
The engine's handle_send_message runs several blocking or long operations after emitting Event::TurnComplete but before the TUI event loop can process the next input:
1. Post-turn workspace snapshot (git operations)
// crates/tui/src/core/engine.rs — handle_send_message
if self.config.snapshots_enabled {
let post_workspace = self.session.workspace.clone();
let post_seq = self.turn_counter;
let _ = tokio::task::spawn_blocking(move || post_turn_snapshot(&post_workspace, post_seq)).await;
}
This .awaits the blocking task — the engine loop is stuck until the git snapshot completes. On large repos, git add -A && git commit can take seconds.
2. Cycle advancement (briefing generation)
if matches!(status, TurnOutcomeStatus::Completed) {
self.maybe_advance_cycle(mode).await;
}
maybe_advance_cycle can trigger a full LLM call to Flash to generate a cycle briefing, plus an archive write to disk. This is a network-bound operation blocking the engine loop.
3. Session state emission
self.emit_session_updated().await;
This clones the full message list and sends it over the channel. On long sessions with 500+ messages, this is a large allocation + serialization.
4. LSP diagnostics flush
self.flush_pending_lsp_diagnostics().await;
Can involve async I/O to LSP servers.
What makes this worse
The TurnComplete event is emitted before snapshots and cycle advancement:
let _ = self.tx_event.send(Event::TurnComplete { ... }).await;
// THEN post-turn snapshot blocks
// THEN cycle advancement blocks
So the UI shows "turn complete" (streaming indicator disappears) but the engine is still blocked, making the UI appear frozen with no visual feedback about what's happening.
Proposed fix
Short-term: make post-turn work non-blocking
Move snapshots, cycle advancement, LSP flush, and session persistence into tokio::spawn tasks that don't block the engine loop. The TurnComplete event is already emitted — the engine doesn't need to wait for these:
// Fire-and-forget: don't block the engine loop on post-turn bookkeeping.
let tx_event = self.tx_event.clone();
tokio::spawn(async move {
// snapshot, cycle, persist...
let _ = tx_event.send(Event::PostTurnBookkeepingComplete).await;
});
Medium-term: progress indicator
Add a PostTurnStatus event that the TUI can render as a small spinner or status line ("Saving snapshot…", "Generating briefing…") so the user knows the terminal isn't actually frozen.
Medium-term: move snapshots to a dedicated background worker
The side-git snapshot system (crates/tui/src/snapshot/) could run on its own dedicated thread/task with a work queue, completely decoupled from the engine loop.
Acceptance criteria
- After a turn completes, the composer accepts input (text entry, copy, paste) within 200ms
- Post-turn snapshots still happen reliably (check
~/.deepseek/snapshots/ after a session)
- Cycle advancement still works correctly
- No data loss if the process is killed during post-turn work
- Test: run a 10-turn session on a repo with 100+ files, verify no perceivable freeze after each turn
Symptoms
After a turn completes (the LLM finishes streaming, the final text is visible), the terminal becomes unresponsive for 1–5 seconds. During this window:
Once the post-turn work completes, the terminal snaps back to normal.
Likely causes
The engine's
handle_send_messageruns several blocking or long operations after emittingEvent::TurnCompletebut before the TUI event loop can process the next input:1. Post-turn workspace snapshot (git operations)
This
.awaits the blocking task — the engine loop is stuck until the git snapshot completes. On large repos,git add -A && git commitcan take seconds.2. Cycle advancement (briefing generation)
maybe_advance_cyclecan trigger a full LLM call to Flash to generate a cycle briefing, plus an archive write to disk. This is a network-bound operation blocking the engine loop.3. Session state emission
This clones the full message list and sends it over the channel. On long sessions with 500+ messages, this is a large allocation + serialization.
4. LSP diagnostics flush
Can involve async I/O to LSP servers.
What makes this worse
The
TurnCompleteevent is emitted before snapshots and cycle advancement:So the UI shows "turn complete" (streaming indicator disappears) but the engine is still blocked, making the UI appear frozen with no visual feedback about what's happening.
Proposed fix
Short-term: make post-turn work non-blocking
Move snapshots, cycle advancement, LSP flush, and session persistence into
tokio::spawntasks that don't block the engine loop. TheTurnCompleteevent is already emitted — the engine doesn't need to wait for these:Medium-term: progress indicator
Add a
PostTurnStatusevent that the TUI can render as a small spinner or status line ("Saving snapshot…", "Generating briefing…") so the user knows the terminal isn't actually frozen.Medium-term: move snapshots to a dedicated background worker
The side-git snapshot system (
crates/tui/src/snapshot/) could run on its own dedicated thread/task with a work queue, completely decoupled from the engine loop.Acceptance criteria
~/.deepseek/snapshots/after a session)