The `/files` picker ranks workspace files by three signals harvested
from the session: which files git reports as modified, which the user
@-mentioned in the composer, and which recent tool calls touched. The
scoring code — `open_file_picker` plus 9 helpers (build_relevance,
modified_workspace_paths, parse_git_status_path,
mark_tool_detail_paths / from_value / from_text, workspace_file_candidate,
clean_path_token, workspace_path_to_picker_string) — was ~218 lines
of self-contained logic mid-ui.rs.
Moved to `tui/file_picker_relevance.rs`. Same behavior; the picker
view file (`tui/file_picker.rs`) keeps the rendering layer, and the
new module owns the per-session ranking that fed it.
ui.rs is now 9073 lines (down ~950 from the pre-refactor 10025).
The Key Features list was missing several capabilities that meaningfully
distinguish DeepSeek TUI from other terminal coding agents:
* Prefix-cache stability tracking (the footer chip from #1473).
* OS-level sandboxing — Seatbelt on macOS, Landlock on Linux, Windows
Job Objects. The README previously said "approval gates" without
noting that shell execution actually runs through OS isolation.
* Bundled starter skill set (11 skills) so `/skills` is useful on
first launch, before any community skill is installed.
* Terminal-native notifications across OSC 9 / 99 / 777 plus the
desktop fallback.
* Theme picker covering Catppuccin / Tokyo Night / Dracula / Gruvbox.
* CNY cost display when the session locale is `zh-Hans`.
No promotional language added; entries describe shipped behavior that
already existed but went unnamed in the elevator pitch.
`crates/tui/src/modules/` was never registered with `mod modules;` in
the binary and nothing in the workspace called `run_deepseek_chat` /
`run_official_chat`. The file is a 23 KB text-REPL workflow from a
much earlier iteration of the project — superseded by the actual
TUI runtime in `crates/tui/src/tui/` long ago, but never deleted.
Removing it cuts ~580 lines of unreachable code and 44 `println!`
calls from the workspace audit, with zero behavior change (the
compiler confirms nothing imports from this path).
`crates/tui/src/core/engine.rs` is the agent-loop core (1983 lines after
this commit, down from 2077). One of the cleanest seams in there is the
public `EngineHandle` method surface: 9 short async/sync methods that
just push operations down the engine's mpsc mailbox — `send`, `cancel`
+ `cancel_with_reason` + `is_cancelled`, `approve_tool_call` /
`deny_tool_call` / `retry_tool_with_policy`, `submit_user_input` /
`cancel_user_input`, and `steer`.
Move that impl block to a new `engine/handle.rs` submodule. The struct
itself stays in `engine.rs` because two construction sites
(`Engine::new` and the test-only `mock_engine_handle`) need access to
its private channels; child modules see parent private items, so the
impl in `handle.rs` works without widening any field visibility.
No behavior change. All 100 `core::engine::*` tests still pass.
The tool dispatch path (`Engine::execute_tool_with_lock`) had no
observability around the actual call: we knew when a tool was
*requested* (via the existing engine-loop logs) and when it *audited*
(via `DEEPSEEK_TOOL_AUDIT_LOG` JSONL), but not how long any individual
tool took, what its outcome was, or how the request was routed (MCP
vs in-process registry).
Add two tracing events around every tool call:
* `tool.exec.start` — tool name, dispatch kind (`mcp` / `registry` /
`missing`), interactive flag, supports-parallel flag, input byte
size. `tracing::debug!` so the noise is opt-in via
`RUST_LOG=engine.tool_execution=debug`.
* `tool.exec.end` — same tool/dispatch fields plus `duration_ms`,
`success` (or `error_kind` mapped from `ToolError` variants), and
output byte size. Successful calls log at `debug`; failures log at
`warn` so they surface under the default filter.
Targets are flat strings (`engine.tool_execution`) following the
existing convention already used by `InteractiveTerminalGuard`. CLAUDE.md
documents `RUST_LOG=deepseek_cli::core::engine=debug` as the canonical
filter for agent-loop events; this is the per-tool-call layer beneath
that.
The notification dispatch primitives (`Method`, `notify_done`,
`humanize_duration`, OSC 9 / OSC 99 / OSC 777 formatters) already live
in `tui/notifications.rs`. The five composition helpers that decided
*when* and *what* to notify still lived in ui.rs:
* `notification_settings` → `notifications::settings`
* `completed_turn_notification_message` → `notifications::completed_turn_message`
* `subagent_completion_notification_message` → `notifications::subagent_completion_message`
* `latest_assistant_notification_text` → `notifications::latest_assistant_text`
* `notification_text_summary` → `notifications::text_summary`
Move them so the whole notification stack lives in one file. The lone
streaming sanitizer they share, `sanitize_stream_chunk`, stays in ui.rs
(it has three other call sites in the streaming render path) but is
now `pub(super)` so notifications.rs can reuse it via `super::ui::`.
ui.rs drops to ~9290 lines (down ~735 from the pre-refactor baseline
of 10025). All 954 tui:: tests still pass.
Three more cohesive function clusters move out of the 10k-line ui.rs:
* `tui::vim_mode` (new) — `handle_vim_normal_key`, the composer's
Normal-mode dispatch (h/j/k/l, w/b, x/dd, i/a/o/v/G, plus the
pending-`d` operator state).
* `tui::workspace_context` (new) — the composer-header git context
badge: `refresh_if_needed`, `collect`, `branch`, `change_summary`,
`run_git`, the `ChangeSummary` struct, and the `REFRESH_SECS` TTL
constant. Replaces `refresh_workspace_context_if_needed` /
`collect_workspace_context` / `workspace_git_branch` /
`workspace_git_change_summary` / `run_git_query` / `WorkspaceChangeSummary`
/ `WORKSPACE_CONTEXT_REFRESH_SECS` in ui.rs.
* `tui::streaming_thinking` (new) — the 10-function lifecycle that
manages the live "Thinking" entry inside `active_cell`: ensuring an
entry exists, appending chunks, animating the translation
placeholder, replacing it with finalized text, starting / finalizing
a block, and stashing the reasoning buffer onto `app.last_reasoning`
so it survives compaction.
Plus the unused `ActiveCell` import in ui.rs that became dead after
the streaming-thinking move.
ui.rs is now ~9434 lines (down from ~10025). All 954 tui::* tests
still pass; no runtime behavior change.
ui.rs has grown past 10k lines; the first two extraction targets are
self-contained function clusters that don't share state with the rest
of the file:
* `tui::auto_router` (new module) gathers the 7 functions that decide
whether to run the auto-route flash model and what context to send
it: `should_resolve_auto_model_selection`,
`resolve_auto_model_selection`, `normalize_auto_routed_effort`,
`recent_auto_router_context`, and three private helpers
(`content_blocks_text`, `append_router_text`,
`truncate_for_auto_router`). Adds unit tests for the pure pieces.
* `tui::onboarding` (existing module) grows three state-machine
transitions and one API-key validator: `validate_api_key_for_onboarding`,
`advance_onboarding_from_welcome`,
`advance_onboarding_after_language`,
`sync_api_key_validation_status`, plus the `ApiKeyValidation` enum.
These belong next to the existing onboarding screen renderers.
Adds unit tests for the validator.
Net effect: ui.rs is ~160 lines shorter and two cohesive concerns are
now reviewable without scrolling through unrelated UI plumbing. No
behavior change. All 954 tui:: tests still pass.
The /skills menu renders one history cell containing every discoverable
skill with full description. With the v0.8.34 bundled-skill set (11
first-party skills), a typical 40-row terminal viewport no longer fits
the message, and the top of the list scrolls off — including any
workspace-level user skill at .agents/skills/, which the registry
returns first thanks to precedence ordering.
The qa_pty `skills_menu_shows_local_and_global_skills` test exercises
exactly this case: a workspace skill plus a sealed-home global skill
plus the 11 bundled skills come to 13 entries, the viewport cuts off
above /global-alpha, and the workspace skill is invisible. Discovery
was healthy — confirmed by the new unit test
`discover_finds_both_workspace_and_global_skills`.
Render the menu in two sections so user-created skills stay prominent:
- `Your skills (N):` lists every user-installed skill with its full
description.
- `Built-in skills (M):` either lists every bundled skill with its
description (when there are no user skills to surface) or compacts
them into a comma-separated name list with a "run /skills <name>
for details" hint (when user skills are present).
Filtered views (`/skills <prefix>`) keep the old flat list — the user
has already narrowed the catalog and section headers would be noise.
Expose `skills::is_bundled_skill_name` so the renderer can partition
without a side-channel marker. Replace one unit test that asserted
the old between-entry blank-line spacing with one that asserts the
section structure.
Some terminal emulators (observed on Tabby / xterm.js) re-trigger a
FocusGained event when the application sends EnableFocusChange
(\e[?1004h) inside recover_terminal_modes(). This creates a tight
event→redraw→event loop that manifests as continuous screen flicker
whenever the terminal window has focus.
Root cause chain:
FocusGained → recover_terminal_modes() → EnableFocusChange
↑ ↓
└── Tabby retriggers FocusGained ←─────────────┘
Fix: add a 200 ms debounce window on FocusGained-triggered mode
recovery. If a FocusGained arrives within 200 ms of the last
recovery call, skip recover_terminal_modes() (avoiding the
EnableFocusChange that feeds the loop) but still mark a repaint.
The debounce constant FOCUS_RECOVERY_DEBOUNCE uses a 200 ms window,
which is short enough that legitimate app-switch focus events (>200 ms
apart) still get full mode recovery, while spurious back-to-back
events from terminal quirks are suppressed.
Fixes flickering reported when running deepseek-tui inside Tabby on
macOS (deepseek-tui 0.8.32).
--- Fixed with DeepSeek V4 Pro ---
Signed-off-by: DeepSeek V4 Pro <via deepseek-tui>