Each tool description now names what to use instead of (cat/head/tail/
sed/grep/find/curl/heredocs in exec_shell), the return shape, and the
limits. Steering language routes V4 toward our typed tools and away
from shell footguns.
Tools updated: read_file, write_file, edit_file, list_dir, grep_files,
file_search, web_search, apply_patch, fetch_url.
Removes the unused legacy normal.txt / plan.txt / yolo.txt prompt
templates and the YOLO_PROMPT / PLAN_PROMPT constants. Both constants
were referenced only by their own self-tests in prompts.rs; AGENT_PROMPT
is preserved (its companion .txt is in the scope of a separate issue).
All description strings stay under 1024 chars (max: 350) with no
embedded newlines or Markdown headers, so the cached tool catalogue
stays prefix-stable for V4's KV cache.
Closes#711
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous comment incorrectly suggested that a user-set low_motion=false
in the settings file could override the TERM_PROGRAM=vscode detection.
In fact, apply_env_overrides() runs after disk load and unconditionally
sets the flag, identical to the existing NO_ANIMATIONS precedent.
Update the comment to state the actual precedence clearly.
Signed-off-by: CrepuscularIRIS <serenitygp@qq.com>
VS Code's integrated terminal sets TERM_PROGRAM=vscode. Its compositor
cannot keep up with the default 120 FPS redraw rate, producing rapid
flickering on some machines while other terminal apps (Terminal.app,
iTerm2) are unaffected (#1356).
Extend apply_env_overrides() to detect TERM_PROGRAM=vscode and
automatically activate low_motion mode (30 FPS cap, no fancy animations),
matching the existing NO_ANIMATIONS env-var pattern. This is a zero-
config fix: users running in VS Code get a stable display with no
settings change required. Users who want the full animation rate can still
set low_motion = false explicitly in their settings file — that file-level
value is already loaded before apply_env_overrides() is called, so an
explicit false in the file wins over this auto-detection.
Two tests added:
- vscode_term_program_forces_low_motion_on: TERM_PROGRAM=vscode enables
low_motion and disables fancy_animations.
- non_vscode_term_program_does_not_force_low_motion: other well-known
terminal programs (iTerm.app, Apple_Terminal, WezTerm, xterm-256color)
are unaffected.
Signed-off-by: CrepuscularIRIS <serenitygp@qq.com>
`provider_switch_clears_turn_cache_history` in `tui/ui/tests.rs`
calls `switch_provider(..., ApiProvider::Ollama, ...)`, which
internally persists the new provider via `Settings::save()` —
writing `default_provider = "ollama"` to
`~/Library/Application Support/deepseek/settings.toml` (or its
`dirs::data_dir()` equivalent on Linux/Windows). Because the
test's `create_test_app` did not isolate `HOME` / `USERPROFILE`,
each run silently overwrote the developer's real preferences.
The contamination then leaked back into adjacent picker tests:
`model_picker::tests::arrow_keys_move_within_focused_pane`
became order-sensitive, passing when it happened to run before
`provider_switch_clears_turn_cache_history` and failing after,
because Ollama is a pass-through provider and
`ModelPickerView` then hid the DeepSeek model rows.
Two fixes:
* `tui/ui/tests.rs::provider_switch_clears_turn_cache_history`
now wraps the test in a `HomeGuard` that redirects HOME /
USERPROFILE to a tempdir for the test's lifetime and restores
the original values on drop. The guard owns the
`test_support::lock_test_env()` mutex so clippy's
`await_holding_lock` lint stays quiet through the
`.await` (the pattern mirrors `tools::recall_archive::HomeGuard`).
* `tui/model_picker.rs::create_test_app` now also pins
`app.api_provider = ApiProvider::Deepseek` alongside the
existing `app.model` / `app.reasoning_effort` overrides, so
the picker tests stop depending on whatever `default_provider`
happens to be in the developer's `settings.toml` for any other
reason.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When an engine error occurs (e.g. refusing an insecure HTTP base URL),
the same error was displayed twice: once as a HistoryCell::Error in the
transcript and again as a sticky toast in the footer/composer area.
The toast was created because apply_engine_error_to_app set
status_message, which sync_status_message_to_toasts() converted into a
sticky toast (15s TTL) since the text contained "error"/"failed".
Add turn_error_posted flag to App, set when an EngineEvent::Error is
posted to the transcript, reset on TurnStarted. The TurnComplete error
handler and apply_engine_error_to_app now skip setting status_message
when the flag is set, keeping the error display in the transcript only.
The auth+env_only onboarding path retains its status_message since that
flow relies on it to prompt the user for a saved API key.
Three additions to base.md's Verification Principle section:
1. Before reporting a task as complete, verify the result when
practical; if not verified, say so explicitly.
2. Preserve only key facts from tool results (paths, errors, exit
status, cache values); do not copy large raw outputs.
3. Inspect error before retrying a failed tool call; do not repeat
the identical action blindly.
`WorkingSet::build_file_index` walks the workspace tree (depth 6) plus
all `DISCOVERY_ALWAYS_DIRS` (depth 5) the first time `fuzzy_resolve` is
called. On huge workspaces that walk dominates the first turn's wall
clock, surfacing as the ~10-second `Working...` hang reported in #697.
Adds a `FILE_INDEX_MAX_ENTRIES = 50_000` cap. When the walk produces
more than 50K (file or directory) entries the index is returned early
with a warning. A surplus entry simply isn't fuzzy-resolvable; literal
paths still resolve via the existing fallback so functionality is
preserved on outsized workspaces.
50K is well above any realistic project's depth-6 entry count, so for
typical users the cap is a no-op. The existing `working_set` tests
(26/26) still pass — this is purely a defensive upper bound on a path
that previously had none.
Refs #697
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cmux typically does not set TERM_PROGRAM; it sets LC_TERMINAL=Cmux instead.
The previous resolve_method() only checked TERM_PROGRAM, causing Cmux users
to fall back to the Bel method instead of OSC 9 notifications (#1281).
Changes:
- Add LC_TERMINAL as a secondary env-var probe in resolve_method(), checked
after TERM_PROGRAM. This picks up Cmux (and any other OSC-9 capable
terminal that sets LC_TERMINAL rather than TERM_PROGRAM).
- Add Cmux to the OSC9_TERMINALS allowlist.
- Document that terminals setting neither env var can force OSC 9 with
[notifications].method = "osc9" in the config file.
- Add two new tests:
- auto_detect_picks_osc9_for_cmux_via_lc_terminal
- auto_detect_picks_osc9_for_wezterm_via_lc_terminal
- Harden existing auto_detect_picks_bel_for_unknown_on_unix to clear
LC_TERMINAL before asserting the Bel fallback, preventing flakiness in
test runner environments where LC_TERMINAL is set to a known terminal.
- Update NotificationsConfig.method doc to mention Cmux and the
LC_TERMINAL probe.
Signed-off-by: CrepuscularIRIS <serenitygp@qq.com>
Fixes a cluster of intermittent failures observed on macOS under
parallel test load. Root causes were tests mutating shared global
state (HOME, USERPROFILE, DEEPSEEK_CONFIG_PATH env vars and
~/.deepseek/ filesystem) without holding the process-wide test
lock, plus a few outdated-by-PR assertions and a tight 3s timeout
on Windows CI.
Changes:
* Three EnvGuard / HomeGuard types (commands/config.rs,
commands/network.rs, tools/recall_archive.rs) now acquire
crate::test_support::lock_test_env() and hold the MutexGuard
for their full lifetime, replacing local mutexes that
serialized only within a module. Call sites that previously
acquired lock_test_env() explicitly with `let _lock = ...`
before constructing the guard drop that redundant acquisition;
std::sync::Mutex is not reentrant and double-locking on the
same thread would deadlock.
* settings.rs::config_path_test_guard() now returns the global
test_env lock instead of an isolated module-local mutex.
* model_picker.rs create_test_app() now returns (App, MutexGuard)
so picker tests hold the same lock — eliminates env-var races
with config-mutating tests in adjacent modules.
* task_manager.rs: 4 tests using wait_for_terminal_state bump
3s -> 10s to give Windows CI file-I/O headroom (we saw one
intermittent timeout on the v0.8.27 PR Windows job).
* config.rs: 2 api-key tests now set DEEPSEEK_SECRET_BACKEND=local
so they exercise file-backed storage in CI rather than fail on
Keychain access.
* history.rs: removes streaming_thinking_live_collapses_unless_verbose
which asserted the OLD behavior PR #1390 (#861 RC4) intentionally
changed. The new contract is covered by the three tests PR #1390
added.
* .claude/HANDOFF_v0.8.28_user_issues.md: notes #1394 / PR #1393
as a deferred prompt-reliability enhancement.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two independent fixes:
1. **Prompt truthful reporting** (base.md): add explicit rules for honest
outcome reporting — if a tool fails/returns-empty say so; if cache
usage is unobserved treat it as unknown/null, not 0.
2. **Cache usage u64 → Option<u64>** (session.rs): when the API does
not report cache hit/miss tokens, the cumulative SessionUsage
defaulted to 0. Models interpreted this as "no cache hits" rather
than "unknown". Changing to Option<u64> ensures absent cache data
serializes as null in the model context.
Tests added for all three cases: starts None, stays None when API
omits cache, accumulates correctly when API reports cache.
Closes the visibility gap reported in #1324 ("Thinking 思考内容不能流式
输出,只能等到完全输出后通过 ctrl+O 查看完整思考内容") and root cause 4
of #861.
Today `render_thinking` blanks the body whenever `collapsed && streaming`:
```rust
let body_text = if collapsed && streaming {
String::new()
} else if collapsed { … } else { … };
```
That left the user staring at a "thinking..." placeholder for the
entire reasoning phase — V4-Pro thinking can run for tens of seconds,
so the live transcript looked frozen even though tokens were flowing.
Fix:
1. During `collapsed && streaming` we now render the raw content
instead of blanking. `extract_reasoning_summary` is meaningless
while the block is mid-flight (no completed reasoning to summarise),
so the streaming branch returns the body verbatim.
2. The `> THINKING_SUMMARY_LINE_LIMIT` truncation now drops *head*
lines while streaming, keeping the visible window tracking the live
cursor at the bottom — which is what users expect when watching a
model think.
3. The existing "thinking collapsed; press Ctrl+O for full text"
affordance was gated on `!streaming`; it now renders during
streaming as well, with a slightly different label ("thinking
continues; …") so the user knows there's more content above and
how to reach it.
Three new tests cover the new contract: streaming-collapsed shows
live content, the head is dropped not the tail, and the live
affordance fires when truncated.
Refs #861 (RC4), closes#1324
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the order-of-events race in #861 root cause 3: when the engine
bursts events, the dispatch loop can pull `MessageComplete` off the
channel ahead of `ThinkingComplete`. Today's `MessageComplete` reads
`app.last_reasoning.take()` to attach the reasoning block to the
assistant message in `api_messages`. If `ThinkingComplete` has not
fired yet, `last_reasoning` is `None` and the thinking content is
dropped — DeepSeek V4 then returns HTTP 400 on the next turn because
it requires `reasoning_content` replay for assistant messages that
carry tool calls.
Adds a defensive head-of-handler drain in `MessageComplete`: when
`streaming_thinking_active_entry.is_some()`, finalize the active
thinking entry and stash the reasoning buffer into `last_reasoning`
before the existing body runs. The drain is a no-op in the normal
case where `ThinkingComplete` arrived first (the entry has already
been cleared), so this branch is order-independent.
Adds `message_complete_drain_preserves_thinking_when_thinking_complete_lost`
which exercises the head-of-handler invariant directly: with a
thinking entry still active and `last_reasoning` empty, the drain
must move the buffer into `last_reasoning` before downstream reads.
Refs #861 (RC3). RC1 and RC2 are already addressed by the existing
`finalize_current_streaming_thinking` plumbing in
`apply_engine_error_to_app` and `start_streaming_thinking_block`;
RC4 (streaming-time truncation affordance) is left out of this PR
to keep the scope on the data-loss path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a user denies a tool call (e.g. edit_file), the tool_name was
inserted into approval_session_denied alongside the per-call
approval_key. Every subsequent invocation of the same tool type was
then auto-denied for the rest of the session without prompting.
Fix: only store the approval_key (per-call unique). This still
prevents the model's retry loop from re-prompting the exact same
command (#360), but allows the user to approve a fresh invocation of
the same tool type.
Replaces the v0.8.27 handoff with a fresh v0.8.28 doc focused on
what's actually outstanding after the v0.8.27 ship:
- P1: CNB.cool mirror automation (token had no push perm during
v0.8.27 release), Ctrl+Enter as newline config flag (#1372),
Windows task_manager test timeout bump, general test flakiness
audit.
- P2: comment-pinged issues awaiting reporter (#1112 snapshot
growth, #1357 input/runtime overlap, #1281 Cmux notifications).
- P3: deferred items (#1338 Windows panic, #1062 capacity recovery,
#1067 musl build, #1364 hooks v2, #1343 desktop GUI).
The v0.8.27 doc had ~25 items inline; the v0.8.28 doc only carries
what's still outstanding (everything else landed in the v0.8.27
cycle — see PR #1375). Starts smaller so the next agent can ship
a focused release rather than wade through completed work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the v0.8.25 / v0.8.26 What's New sections with the v0.8.27
headline summary across both READMEs. Covers cross-terminal flicker
fix, long-text wrap, pager copy-out, context-sensitive Ctrl+C, MCP
auto-reload, notify tool, onboarding localization, paste UX rebuild,
/skills filter + diagnostic hints, and the 17 community PRs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CodeQL's rust/path-injection scan flagged `mcp_config_mtime(path)`
because the helper takes `&Path` and calls `fs::metadata(path)`.
Both call sites already validate via `validate_mcp_config_path` —
`from_config_path` runs the check before constructing the pool, and
`reload_if_config_changed` only sees paths that came from a
`from_config_path`-validated `config_source` field — so the alert
is a false positive about cross-function data flow.
The clean fix is to tie the validation to the call site rather
than rely on cross-function reasoning: `mcp_config_mtime` now
short-circuits to `None` for paths that fail the same allow-list
check `load_config` and `save_config` already use. The lazy-reload
caller already treats `None` as "skip the check this turn", so a
rejected path simply degrades gracefully rather than producing an
error path. Cost is one regex check per call on a path we're about
to stat anyway.
This also makes the helper safe-by-construction for any future
caller that forgets to validate, which matches the pattern of the
adjacent `load_config` / `save_config` helpers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On top of v0.8.26's inter-row spacing for /skills (#1328 from
@reidliu41), the list now also accepts an optional name-prefix
argument so users with crowded skill folders can narrow the view
without scrolling.
/skills → full list (unchanged)
/skills git → only skills whose name starts with "git"
/skills GIT → same (case-insensitive)
/skills nope → "No skills match prefix `nope` (out of 12 …)"
/skills --remote → unchanged
/skills sync → unchanged
/skills --bogus → "Usage: …" error (rejected so future flags
don't silently turn into no-match prefixes)
The match-count header reflects both the matched count and the
registry total, so the user can see at a glance how aggressive
the filter is. Empty match sets explicitly say so and point back
at unfiltered `/skills`. Skill names that start with `-` aren't
allowed by the loader, so reserving the dash prefix for flags is
safe.
Plus the matching usage / description updates in the command
metadata + all four shipping locales (en / ja / zh-Hans / pt-BR)
so /help shows the new argument.
Closes#1318. Thanks @simuusang for the report.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a user picked 简体中文 / 日本語 / Português (Brasil) on the
step-2 language picker, every subsequent onboarding screen used to
stay in English. The set_locale_from_onboarding path already
re-resolved `app.ui_locale`, but the hardcoded `Line::from(Span::
styled("Connect your DeepSeek API key", …))` strings in
`onboarding/api_key.rs`, `trust_directory.rs`, `language.rs`, and
the `tips_lines()` block in `onboarding/mod.rs` never consulted
the locale.
This commit:
- Adds 25 `MessageId` entries (`OnboardLanguageTitle`,
`OnboardApiKey*`, `OnboardTrust*`, `OnboardTips*`, …) covering
the title / body / hint / footer strings for each screen.
- Translates each into all four shipping locales (en / ja /
zh-Hans / pt-BR), with the same care the existing translation
surfaces use (no machine translation; idiomatic phrasing for
each locale).
- Threads the active locale through `language::lines`,
`api_key::lines`, `trust_directory::lines`, and `tips_lines`
via `app.tr(MessageId::…)`.
- Adds `api_key_screen_renders_in_selected_locale` regression
test pinning that the rendered lines actually contain the
translated strings for zh-Hans / ja / en.
Particularly noticeable for users on CJK input methods: picking
their language at step 2 now means the remaining setup runs in
that language rather than forcing IME juggling for English text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cherry-picked from @reidliu41's PR #1342. Pasting `请联网搜索:\n…`
(short non-ASCII first line + newline) used to fail the
`decide_begin_buffer` heuristic — `grabbed.chars().any(is_whitespace)`
is false on a 6-codepoint Chinese run, and `chars().count() >= 16`
is false at 6 chars — so the trailing pasted newline fell through
as a real Enter and submitted the first line on its own.
The heuristic now also treats `!grabbed.is_ascii()` as paste-like,
which captures the CJK case without false-firing on ASCII typing
(plain ASCII typists still need either whitespace or 16+ chars to
look like a paste).
Includes the regression test from PR #1342, slightly reworded.
Closes#1302. Thanks @reidliu41.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two paste-UX improvements that address recurring complaints:
**1. Visible-before-submit consolidation.** v0.7.x added a 16 000-char
safety cap that folded oversized inputs into `.deepseek/pastes/paste-
…md` and swapped them for an `@`-mention so the model could read the
full content via the normal mention-resolution path. The cap was
checked inside `submit_input` only — meaning a user who pasted 50k
chars and pressed Enter saw the file get created AND the message
sent in the same frame, with no chance to review the @-mention
beforehand. People reasonably read this as "the TUI auto-sent an
@-mention I didn't authorise." Consolidation now also runs at the
end of `insert_paste_text`, so the @-mention shows up in the
composer (along with a "consolidated — sent as @mention" toast) the
moment the paste lands. The submit-time path stays as a safety net
for any other code path that fills the buffer above the cap, so the
cap is still enforced exactly once.
**2. Auto-disable paste-burst on verified bracketed paste.** The
paste-burst heuristic (rapid-keystroke detection for terminals
without bracketed paste) used to run unconditionally. On modern
terminals (Ghostty / iTerm2 / WezTerm / Windows Terminal) bracketed
paste is reliable, and paste-burst running alongside it created
false positives — fast typing, IME commits, autocomplete bursts
could all be mis-classified as a paste. The new
`App::bracketed_paste_seen` flag flips to `true` the first time a
real `Event::Paste` arrives; from that moment, `handle_paste_burst_
key` short-circuits. Terminals that never deliver bracketed paste
(the original target audience) are unaffected — the heuristic
still fires for them.
Both changes have new unit tests:
- `paste_consolidates_oversized_text_into_paste_file_visibly`
- `paste_under_threshold_does_not_consolidate`
- `paste_burst_short_circuits_after_bracketed_paste_observed`
Existing `submit_input_consolidates_oversized_input_into_paste_file`
still passes — it bypasses `insert_paste_text` and exercises the
safety net.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two small workspace-clippy gaps that snuck through the per-crate
sweeps in this branch:
* `crates/cli/src/lib.rs` — the OpenAI-provider passthrough test was
building a `ResolvedRuntimeOptions` literal directly and missed the
`yolo: Option<bool>` field that landed earlier on this branch in
665801bb8 (`fix(cli): forward --yolo to TUI binary`). Set to `None`
to match the test's non-yolo intent.
* `crates/tui/src/mcp.rs` — the new `reload_if_config_changed` swap
test was using `iter().any(|n| *n == "new")`, which is rust-1.94
clippy's `manual_contains` lint. Switched to `names.contains(&"new")`.
`cargo clippy --workspace --all-targets --all-features --locked --
-D warnings` is now green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 17-community-PR opener was accurate for the first half of the
cycle but undersells the user-issue work that landed afterwards
(flicker, wrap, pager copy-out, Ctrl+C, MCP auto-reload, notify
tool). Updated headline so the changelog matches the shipping
release.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new always-loaded tool spec `notify` that lets the model
trigger a single desktop notification when a long-running task
completes or genuinely needs the user's attention. Implementation
delegates to the existing `tui::notifications` infrastructure, so
the user's `[notifications].method` config drives delivery:
- iTerm2 / Ghostty / WezTerm → OSC 9 (banner + sound)
- macOS / Linux fallback → BEL
- Windows → off by default; opt-in to BEL + MessageBeep
- `method = "off"` → silent no-op (the tool still succeeds)
Title and body are character-bounded (80 / 200 chars) and
trim-checked, so a runaway model can't paint a paragraph into the
terminal title bar or slice through a multi-byte sequence and emit
invalid UTF-8. tmux passthrough is detected via `$TMUX` and OSC 9
is double-escaped so the outer terminal still receives it.
The tool description steers the model away from chatter — only fire
on real completion / attention beats, not as a "still alive" ping.
Always-loaded (added to `should_default_defer_tool`'s allowlist) so
the model sees it without a ToolSearch round-trip; auto-approval
since the only side effect is a single terminal escape write.
Closes#1322.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.8.26 surfaced the spawn-error tail (`Stdio transport closed` →
the underlying EACCES/sandbox-deny line). v0.8.27 closes the second
half of the report: users no longer need to manually `/mcp reload`
after editing `~/.deepseek/mcp.json`.
`McpPool` gained three fields: a `config_source` path (set when the
pool is built via `from_config_path`), a 64-bit content hash of the
active config, and the most recently observed mtime of the source
file. `reload_if_config_changed` does a cheap `stat` first; on
mtime-equal it returns immediately. Only when the mtime has moved
does the pool re-read the file, hash it, and compare to the stored
hash — content-unchanged reloads (e.g. `touch` on a networked FS)
are skipped. On a real content change the connections map is
cleared so the next `get_or_connect` reattaches under the new
config (sandbox flags, env, args, server set).
`get_or_connect` now invokes `reload_if_config_changed` at entry
and swallows its errors (a transient stat/parse failure can't take
down the existing pool). Pools built via `McpPool::new` (tests,
ad-hoc snapshots) are unaffected — they have no source path and
short-circuit out.
No file watcher, no long-lived task, no signature changes for the
existing callers.
Closes the part-2 follow-up on #1267.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plain Ctrl+C used to mean "cancel turn or arm exit" unconditionally,
which fought the OS-wide copy convention on Windows: every time a
user pressed Ctrl+C to copy a model response, they instead armed the
exit prompt and lost their place. Ctrl+Shift+C and Cmd+C copied
correctly but weren't discoverable.
The handler is now a four-stage decision, factored into a
`CtrlCDisposition` helper with a unit-tested priority table:
1. CopySelection — transcript selection active → copy + clear it
(matches Windows / cross-platform Ctrl+C convention; #1337).
2. CancelTurn — turn in flight → cancel (unchanged).
3. ConfirmExit — quit-armed within the 2s window → exit.
4. ArmExit — idle, no selection → arm the "press Ctrl+C again"
prompt for 2s (unchanged).
A turn-in-flight beats a quit-arm even when both are true, so a
Ctrl+C that lands while the user is mid-turn but had recently
half-armed the exit prompt always cancels the turn rather than
exiting. Pinned by `ctrl_c_disposition_loading_beats_armed_quit`.
Cmd+C (macOS) and Ctrl+Shift+C continue to copy via `is_copy_shortcut`
unchanged; only plain Ctrl+C now branches on selection state.
For #1367, on TurnStarted the status-message slot now surfaces
"Press Esc or Ctrl+C to cancel" if it's empty. Real transient
messages still take precedence; the hint clears on the next status
update. Closes the discoverability gap for users who didn't know
how to interrupt a long-running task.
Closes#1337, #1367.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both `render_line_with_links` (paragraphs, list items) and the
standalone `wrap_text` (code blocks) were word-based wrappers: when
a single word's display width exceeded the available column budget
they placed the word alone on a line and let it overflow the right
edge of the transcript silently. Long URLs, file paths, commit
hashes, JWTs, and any no-whitespace CJK run all hit this in #1344
and #1351 reports.
The fix mirrors the v0.8.25 table-cell fix (`wrap_cell_text`):
extract the per-character width-aware splitter as a free helper
`push_word_breaking_chars`, and call it from `wrap_text`,
`wrap_cell_text`, and the new char-break branch in
`render_line_with_links`. Each rendered line is now guaranteed to
fit in the requested width; full content is preserved across the
wrapped segments.
Snapshot-style regression tests pin the invariant at widths 40, 60,
80, and 120 — covering 200-char `a`-runs, long URL fixtures,
mixed-short+overlong-word fixtures, and the existing table-cell
property. A regression guard also confirms short words still break
on whitespace (no mid-word breaks for ordinary prose).
Closes#1344 (output-side overflow). Partial fix for #1351 (the
table-cell concern was already fixed in v0.8.25; the long-prompt
input-area concern is a separate visible-window issue, not a wrap
bug — the composer already uses a grapheme-based wrapper).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pager intercepts mouse capture, so terminal-native selection is
disabled inside it. Until now there was no in-app way to copy the
content the user came specifically to see — high-frustration UX gap
for the Alt+V (tool details), Ctrl+O (thinking), shell-job, task,
MCP-manager, and selection pagers.
Both `c` (clipboard convention) and `y` (vim-yank convention) now
emit a `ViewEvent::CopyToClipboard` carrying the full pager body.
The host dispatcher in `ui.rs` writes through `app.clipboard` and
toasts a status confirmation ("Pager content copied" /
"Copy failed"). Empty-body pagers report the empty state instead of
silently no-op'ing.
Footer hint updated to surface the new keys:
j/k scroll Space page Ctrl+D/U half g/G top/bottom / search c copy q/Esc close
Mouse selection inside the pager remains intercepted (the alternative
— releasing capture inside the modal — would break vim-style
navigation), so this is the supported copy path.
Closes#1354.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The user-facing error path already formatted the underlying anyhow
chain with `{err:#}`, but reqwest chains alone read as opaque
fragments ("error sending request: tcp connect: connection refused"
etc.) for users without low-level network experience.
`format_registry_error` now inspects the formatted chain for common
failure signatures and appends a one-line hint:
- DNS lookup / `getaddrinfo` failures
- connection refused / reset / aborted
- TLS handshake / certificate / SSL
- HTTP 404 / 401 / 403 / 429
- request timed out
Each hint points at the most likely cause (network reachability,
trust store, registry URL, rate limit) and a concrete next step.
The original chain is still rendered verbatim above the hint, so
power users keep their detail and casual users get a starting
point.
Closes#1329 (the diagnostic side; the actual root cause is now
diagnosable from the surfaced chain + hint).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The forced-repaint sequence written before each TurnComplete /
focus-gain / resize used to be:
\x1b[r\x1b[?6l\x1b[H\x1b[2J\x1b[3J
which combined with the immediately-following ratatui
`terminal.clear()` produced a double-clear. Terminals that don't
optimize successive clears against the alt-screen buffer (Ghostty,
VSCode integrated terminal, Win10 conhost) rendered the second
clear as a visible blank-then-repaint flicker on every redraw
trigger.
The lighter sequence `\x1b[r\x1b[?6l\x1b[H` resets DECSTBM and DECOM
and homes the cursor (still solving the original viewport-drift fix
that 0.8.22 added) but leaves the pixel-clear to ratatui's diff
renderer. The alt-screen buffer's double-buffering absorbs that
single clear without flicker on every terminal we tested. Terminals
that were already flicker-free (macOS Terminal.app, iTerm2,
alacritty) remain so.
Closes#1119, #1260, #1295, #1352, #1356, #1363, #1366.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously OPENAI_MODEL only set default_text_model which was lower
priority than the provider config model. Now it directly overrides
the openai provider's model field.
Other providers (openai, ollama) should get their model from
config.toml / env vars (e.g. OPENAI_MODEL), not from the global
default_model setting which is DeepSeek-centric.
- Save provider choice to settings.default_provider on switch
- Save model per-provider to settings.provider_models
- On startup, load provider-specific model instead of global default
- Hide DeepSeek models from picker on pass-through providers
- Show friendly message for /models on unsupported providers
Wraps ExecCell and GenericToolCell rendered output with card-rail
glyphs for visual structure, similar to Claude Code's card-style
tool rendering.
- wrap_card_rail(): adds ╭/│/╰ glyphs to rendered lines
- Applied to ExecCell::render and GenericToolCell::lines_with_mode
- Pre-computed caches (output_summary, is_diff) kept from previous
commit for per-frame performance
- Live mode output remains visible (head+tail+omitted), not collapsed
- Card-rail glyphs reused from existing tool_card.rs CardRail enum
Test plan: cargo test -p deepseek-tui (2380 passed, 0 failed)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pre-compute render caches to avoid re-parsing every frame:
- Add output_summary: Option<String> to ExecCell and GenericToolCell
- Add is_diff: bool to GenericToolCell (cached after first detection)
- Populate caches once in handle_tool_call_complete / orphan path
Live mode rendering simplified to one-line summary + expand affordance:
- GenericToolCell and ExecCell now show a single muted summary line
with "Enter to expand tool output" affordance in Live mode
- Transcript mode still emits full output
- render_tool_output_summary_line truncates to fit terminal width
- Make output_looks_like_diff pub(crate) for pre-computation access
Test plan:
- cargo test -p deepseek-tui (2379 passed)
- config_ui::build_document_reflects_app_state is a pre-existing failure
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Promote COST_EQ_TOLERANCE from a function-local const to a module-level
constant in sidebar.rs.
Add SessionCostSnapshot::total_usd() and total_cny() helpers that
encapsulate session+subagent cost summation, used during session restore.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
# Conflicts:
# crates/tui/src/session_manager.rs
# crates/tui/src/tui/sidebar.rs
# crates/tui/src/tui/ui.rs