Description
The TUI can enter a stale busy state where the footer keeps showing working..., new user input is moved into Pending inputs, but /task reports No tasks found.
Based on testing, this looks like a TUI state synchronization bug rather than a DeepSeek API/model failure. The frontend/composer state can believe a turn is still running even when the engine/task state has no active work.
There appears to be a mismatch between:
- frontend/composer state, especially
app.is_loading
- engine/task state, shown by
/task
When app.is_loading remains true, later input is treated as queued/pending. But /task can still report No tasks found, which leaves the user in a dead-end state: the TUI looks busy, but there is nothing obvious to wait for or cancel.
Steps to reproduce
- On Windows
cmd, start the npm-installed TUI with deepseek-tui.
- Send a normal prompt, for example
你是什么模型.
- Observe that the footer can remain stuck on
working....
- Send another normal prompt.
- Observe that the new prompt is moved into
Pending inputs.
- Run
/task.
- Observe that
/task can report No tasks found even though the footer still shows working....
Notes:
- This does not reproduce every time.
- Starting a clean session with
deepseek-tui --fresh can avoid the issue, and the model can answer normally in a clean session.
- This suggests the API key/model path is working, but the TUI can get stuck in an inconsistent frontend state.
Expected behavior
If the current turn is not actually active anymore, the TUI should not keep blocking future user input.
Expected behavior would be one of:
- The message is successfully dispatched to the engine and a normal turn starts.
- If dispatch fails before the turn starts, the TUI clears the busy state and shows a recoverable error.
- Future user input should not be silently queued forever when
/task says there are no active tasks.
Actual behavior
In the affected state:
- The footer remains on
working....
- Submitting another normal prompt does not start a new model turn.
- The prompt appears under
Pending inputs.
/task reports No tasks found.
- The UI appears busy, but there is no visible active task.
Environment
- OS: Windows
- DeepSeek CLI version: npm-installed
deepseek / deepseek-tui
- Model:
deepseek-v4-pro
- Shell:
cmd
Logs or screenshots
Likely cause
In crates/tui/src/tui/ui.rs, dispatch_user_message() sets the UI loading state before sending the message to the engine:
app.is_loading = true;
app.last_send_at = Some(Instant::now());
Then it sends Op::SendMessage to the engine.
If that send fails before the engine accepts the message or before a turn-started event is emitted, the function can return early without clearing the frontend loading state.
As a result:
dispatch_user_message()
-> sets app.is_loading = true
-> engine send fails before turn starts
-> app.is_loading is not reset
-> later Enter submits are classified as queued
-> UI shows Pending inputs + working...
-> /task can still say No tasks found
Suggested minimal fix
Clear the frontend busy state if sending the message to the engine fails before the turn starts.
Suggested change in dispatch_user_message():
if let Err(err) = engine_handle
.send(Op::SendMessage {
content,
mode: app.mode,
model: effective_model,
goal_objective: app.goal.goal_objective.clone(),
reasoning_effort: app.reasoning_effort.api_value().map(str::to_string),
allow_shell: app.allow_shell,
trust_mode: app.trust_mode,
auto_approve: app.mode == AppMode::Yolo,
})
.await
{
app.is_loading = false;
app.last_send_at = None;
return Err(err);
}
This keeps the normal success path unchanged, but prevents the composer from staying permanently busy when dispatch fails before the engine starts a turn.
Suggested regression test
A deterministic unit-level reproduction can simulate a closed engine channel:
#[tokio::test]
async fn dispatch_failure_before_turn_started_clears_loading_state() {
let mut app = create_test_app();
let engine = crate::core::engine::mock_engine_handle();
let handle = engine.handle.clone();
// Simulate the engine no longer receiving ops.
drop(engine.rx_op);
let result = dispatch_user_message(
&mut app,
&handle,
crate::tui::app::QueuedMessage::new("hello".to_string(), None),
)
.await;
assert!(result.is_err());
assert!(
!app.is_loading,
"failed dispatch must not leave future composer submits queued as if a turn were live"
);
assert!(app.last_send_at.is_none());
}
Broader design suggestion
The minimal fix above addresses one concrete stale-loading path. Longer term, it may be worth making the TUI busy state more resilient by deriving or reconciling it from a single source of truth.
Possible improvements:
- If
is_loading == true but there is no active turn/task/stream, show a recoverable error or reset the composer state.
- Make
Pending inputs explain why the input is pending.
- Add a timeout or consistency check for states where the footer says
working... but the task manager reports no active work.
- Ensure failed dispatch, engine shutdown, crash recovery, and resume/fresh-session paths all clean up frontend busy state consistently.
Description
The TUI can enter a stale busy state where the footer keeps showing
working..., new user input is moved intoPending inputs, but/taskreportsNo tasks found.Based on testing, this looks like a TUI state synchronization bug rather than a DeepSeek API/model failure. The frontend/composer state can believe a turn is still running even when the engine/task state has no active work.
There appears to be a mismatch between:
app.is_loading/taskWhen
app.is_loadingremains true, later input is treated as queued/pending. But/taskcan still reportNo tasks found, which leaves the user in a dead-end state: the TUI looks busy, but there is nothing obvious to wait for or cancel.Steps to reproduce
cmd, start the npm-installed TUI withdeepseek-tui.你是什么模型.working....Pending inputs./task./taskcan reportNo tasks foundeven though the footer still showsworking....Notes:
deepseek-tui --freshcan avoid the issue, and the model can answer normally in a clean session.Expected behavior
If the current turn is not actually active anymore, the TUI should not keep blocking future user input.
Expected behavior would be one of:
/tasksays there are no active tasks.Actual behavior
In the affected state:
working....Pending inputs./taskreportsNo tasks found.Environment
deepseek/deepseek-tuideepseek-v4-procmdLogs or screenshots
Likely cause
In
crates/tui/src/tui/ui.rs,dispatch_user_message()sets the UI loading state before sending the message to the engine:Then it sends
Op::SendMessageto the engine.If that send fails before the engine accepts the message or before a turn-started event is emitted, the function can return early without clearing the frontend loading state.
As a result:
Suggested minimal fix
Clear the frontend busy state if sending the message to the engine fails before the turn starts.
Suggested change in
dispatch_user_message():This keeps the normal success path unchanged, but prevents the composer from staying permanently busy when dispatch fails before the engine starts a turn.
Suggested regression test
A deterministic unit-level reproduction can simulate a closed engine channel:
Broader design suggestion
The minimal fix above addresses one concrete stale-loading path. Longer term, it may be worth making the TUI busy state more resilient by deriving or reconciling it from a single source of truth.
Possible improvements:
is_loading == truebut there is no active turn/task/stream, show a recoverable error or reset the composer state.Pending inputsexplain why the input is pending.working...but the task manager reports no active work.