Skip to content

Post-turn UI freeze: terminal unresponsive for seconds after stream ends before copy/paste/selection works #234

Description

@Hmbown

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions