Previously, ModelRegistry::resolve() lowercased the requested model name
before looking it up in the alias map, and always returned the registry's
canonical (lowercase) model ID. This broke third-party API providers
that enforce case-sensitive model name matching.
Now when the resolved model ID differs from the requested name only in
case (eq_ignore_ascii_case), the requested casing is preserved.
Closes#729
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
percent_decode in web_run.rs builds the result via `out.push(b as char)`,
mapping each decoded byte to its Latin-1 codepoint. For percent-encoded
multi-byte UTF-8 sequences (e.g. %E4%B8%AA = 个) this produces visible
mojibake: bytes E4 B8 AA become three Latin-1 chars `个` and the final
String is the UTF-8 encoding of those codepoints, not the original
character.
The sister module web_search.rs::percent_decode (line 490) already uses
the correct pattern: collect bytes into Vec<u8>, then finalize with
String::from_utf8_lossy. Align web_run.rs with that implementation.
Affects DuckDuckGo redirect URLs containing non-ASCII paths, where
normalize_search_url -> percent_decode previously corrupted the decoded
URL before it was shown to the model and to the user.
Add a regression test covering percent-encoded CJK input, raw UTF-8
input, and mixed ASCII+UTF-8.
Co-authored-by: Elowen <xrnc@outlook.com>
* feat(commands): add /rename command to set a custom session title
Adds a `/rename <new title>` slash command that lets users set a
human-readable name for the current session. The new title is
persisted immediately to the session JSON file so it appears in
the session picker on the next open.
- Max title length capped at 100 characters (char-count aware, handles CJK)
- Errors on missing/empty arg or no active session
- Inner `rename_with_manager` helper keeps unit tests fully isolated
from ~/.deepseek/sessions
- Localized descriptions in en, ja, zh-Hans, pt-BR
* fix(rename): sync App state before saving to prevent data loss
Use update_session() to merge current in-memory messages and tokens
into the session before writing the renamed title, preventing stale
disk data from overwriting unsaved App state.
* style: format rename command
---------
Co-authored-by: Hunter Bown <hmbown@gmail.com>
* fix(tools): enforce network policy in web_run
The web_run tool bypassed the network policy that fetch_url and
web_search respect. URL fetches (open/click) now check the configured
network policy before making outbound requests, consistent with other
network tools.
* fix: gate web run fetches by network policy
---------
Co-authored-by: Hunter Bown <hmbown@gmail.com>
create_test_app() in commands/{core,debug,provider}.rs constructs App
via App::new, which reads system LANG. On a Chinese host this
auto-selects ZhHans for ui_locale (translating asserted strings) and
DeepseekCN for api_provider (which then differs from the
ApiProvider::Deepseek that tests pass to provider()).
Pin both fields after construction, matching the existing pattern in
home_dashboard_localizes_in_zh_hans which already sets app.ui_locale
to test Chinese rendering.
All 17 tests listed in the issue now pass on a host with
LANG=zh_CN.UTF-8.
Closes#791.
Co-authored-by: Elowen <xrnc@outlook.com>
* fix: write config files with restrictive permissions in all paths
Three config-file write paths in the TUI crate used `fs::write()` with
default permissions (typically 0644), leaving API keys world-readable:
- `save_api_key_to_config_file` (initial key storage)
- `save_api_key_for` (provider-specific key storage)
- `clear_api_keys_from_config_file` (logout/credential wipe)
Add a `write_config_file_secure` helper that uses `OpenOptions` with
mode 0o600 on Unix, and route all config writes through it. Also harden
`ensure_parent_dir` to strip group/other permissions from the config
directory.
* fix: harden tui config file permissions
---------
Co-authored-by: Hunter Bown <hmbown@gmail.com>
* fix: save config with restrictive permissions and improve secret redaction
- Config files containing API keys were written with default permissions
(typically 0644), making them world-readable on multi-user systems. Use
OpenOptions with mode 0o600 on Unix to restrict access to the file owner.
- `redact_secret` threshold raised from 8 to 16 characters — previously a
9-character secret would leak 8 of its 9 characters (4 prefix + 4
suffix). Now secrets up to 16 chars are fully masked with "********".
* fix(config): keep secret saves warning-free on windows
---------
Co-authored-by: Hunter Bown <hmbown@gmail.com>
Three `unreachable!()` calls panicked if `save_api_key_for` or the API key
apply path ever received a DeepSeek/DeepseekCN variant past the early-return
guard. Replace them with explicit `Err` returns so a future refactor that
breaks the guard produces a recoverable error instead of a crash.
* fix(command_safety): fix path_escapes_workspace false positive for ".." in names
The path_escapes_workspace function used `path.contains("..")` to detect
directory traversal, which incorrectly flagged paths containing consecutive
dots in file or directory names (e.g., `foo..bar`, `dir..name/file.txt`).
Replace the substring matching with a component-level walk that tracks
nesting depth. Each `..` path component decrements the depth; if depth
ever goes negative the path has escaped the workspace. Non-`..` components
increment depth. This correctly:
- Flags genuine traversal like `../outside` and `./sub/../../etc/passwd`
- Ignores names with embedded double-dots like `some..file.txt`
- Allows safe intra-workspace `..` usage like `./subdir/../other`
* test(command_safety): cover absolute traversal paths
---------
Co-authored-by: Hunter Bown <hmbown@gmail.com>
The `execute` method of `ShellWaitTool` is async, but used
`std::thread::sleep` which blocks the tokio worker thread. Replace with
`tokio::time::sleep().await` so other tasks can make progress during the
50ms poll interval.
* feat(tui): support Shift+Enter to insert newline in composer
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(tui): cover shift-enter newline handling
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Hunter Bown <hmbown@gmail.com>
* fix(web_search): complete HTML entity decoding with numeric character references
The decode_html_entities function only handled 7 named entities and lacked
support for decimal (&#NN;) and hex (&#xHH;) numeric character references,
which are common in search engine result snippets. This caused garbled text.
Replace the hard-coded replace chains with a regex-based decoder that
handles:
- All named entities (amp, lt, gt, quot, apos, nbsp, copy, reg, mdash,
ndash, lsquo, rsquo, ldquo, rdquo, hellip)
- Decimal numeric references (A → A)
- Hex numeric references (A → A)
- Unknown entities are passed through unchanged
* style(web_search): apply rustfmt
---------
Co-authored-by: Hunter Bown <hmbown@gmail.com>
* fix(snapshot): refuse to snapshot home directory (#793)
When the TUI is launched from $HOME, the snapshot system would run
`git add -A` on the entire home directory, consuming unbounded CPU and
disk. This manifests as a multi-GB side-repo under ~/.deepseek/snapshots
and makes the TUI appear frozen.
Add a guard in SnapshotRepo::open_or_init that compares the canonical
workspace path against the canonical home directory and returns an error
if they match. The error is non-fatal (snapshots are a safety net, not a
correctness gate) so the turn loop continues without snapshots.
Closes#793
* test(snapshot): fix home guard test portability
* test(snapshot): avoid env-dependent home guard test
---------
Co-authored-by: Hunter Bown <hmbown@gmail.com>
Allow model IDs like accounts/fireworks/models/deepseek-v4-flash
by checking for /deepseek in addition to starts_with(deepseek).
Fixes#753
Signed-off-by: wucm667 <stevenwucongmin@gmail.com>
* Add shell completion setup examples
Show actionable bash, zsh, and fish completion setup commands in the completion subcommand help, and cover them in the CLI help
surface test.
* docs(cli): clarify completion setup examples
---------
Co-authored-by: Hunter Bown <hmbown@gmail.com>
Keep auto as a local routing mode, resolve concrete model/thinking before API requests, and wire auto routing through CLI, TUI, runtime threads, and subagents.
The should_scroll_with_arrows gate was hardcoded to false since the
initial commit, which meant bare Up/Down arrows always navigated
composer history — they never scrolled the transcript view. Users in
virtual terminals (Ghostty, Codex, Kitty-protocol terminals) were
especially affected because they couldn't use the specialized Cmd+Up
/ Alt+Up shortcuts.
The gate now returns true when the composer input is empty (or
whitespace-only), so bare arrows scroll the transcript. When text
is present, arrows still navigate composer history to recall
previous prompts.
Added 3 tests covering empty, whitespace-only, and text-filled
composer states.
Deletes crates/tui/src/responses_api_proxy/ (443 LOC), client/responses.rs
(406 LOC), and removes the ResponsesApiProxy CLI command, the
EXPERIMENTAL_RESPONSES_API_ENV env var plumbing, chat_fallback_counter,
use_chat_completions, RESPONSES_RECOVERY_INTERVAL, and the
RequestPayloadMode::ResponsesApi variant. The experimental Responses API
path was never instantiated and had no documented users; removing it
simplifies the client surface for the upcoming --anthropic-wire flag.
Closes#723
V4 occasionally emits non-canonical tool names (Read_file, readFile,
read-file, read_file_tool). The new resolver runs a deterministic
ladder — lowercase exact, kebab/space → snake, CamelCase → snake,
strip _tool suffix twice, fuzzy prefix match — before falling through
to "Unknown tool". Recovers ~80% of these for free.
Closes#713
The new schema_sanitize module collapses Pydantic-style nullable
anyOf/oneOf unions to {type, nullable: true}, injects empty properties
on bare objects, prunes dangling required entries, and collapses
single-element unions. Run on every tool schema before
build_api_tools() returns the catalog.
Closes a class of silent 400s on /beta/chat/completions strict tool
mode for MCP-derived schemas.
Closes#715
`normalize_model_name` now passes v-series snapshots through unchanged
(deepseek-v4-flash-20260423 stays pinned, future v5-* matches via
regex). Removes ~245 LOC of legacy alias machinery: deepseek_legacy_aliases,
the chat/reasoner/r1/v3/v3.2 fold-arm, is_current_deepseek_v4_alias,
v4 fallback branch, alias capacity test seeds, alias config test block.
The migration from V3 → V4 is over; users on legacy names route their
own request to DeepSeek and see the server actual response (404 if
deprecated, success if still served). No more silent renaming.
Closes#717
PR #646 imported `MessageBeep` from `windows::Win32::UI::WindowsAndMessaging`,
but in `windows` crate 0.60 the function lives at
`windows::Win32::System::Diagnostics::Debug::MessageBeep` and now takes a
typed `MESSAGEBOX_STYLE` returning `Result<()>`. The wrong import broke
every Windows build (Test, npm wrapper smoke, and the windows-msvc release
matrix entry). Fix the import path, enable the `Win32_System_Diagnostics_Debug`
feature, pass `MESSAGEBOX_STYLE(0)` for MB_OK, and discard the Result.
The v0.8.12 release also tripped on a transient `apt-get update` mirror
sync error on the ubuntu-24.04-arm runner, cascading via fail-fast. Wrap
every apt-get update in CI/release with a 5-attempt retry so flaky
ports.ubuntu.com mirrors no longer take down the binary matrix.
Verified: cargo check --target x86_64-pc-windows-gnu compiles cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reported by @Hmbown as the recurring "scrolling uses the parent
terminal scrollbar instead of the TUI's" / "TUI rendered only at the
bottom of the viewport" symptom. Same family as 5c72e5f4 and 899c703d,
but neither covered the cancellation case — they fixed the per-turn
scroll lock and panic-time cleanup respectively.
The bug
=======
`execute_tool_with_lock` in tool_execution.rs sent
`Event::PauseEvents` before an interactive tool ran (which makes the
TUI leave alt-screen, disable raw mode, release mouse capture so the
child sees a normal terminal) and `Event::ResumeEvents` after it
returned. Both sends were `let _ = tx_event.send(...).await`.
If the future was dropped between the two awaits — Ctrl+C, agent
cancel, parent task aborted, sub-agent terminated mid-tool — the
second `await` never reached and `ResumeEvents` was never sent. The
TUI sat in the paused state until the next pause/resume cycle, with
the symptoms above.
The fix
=======
Replace the two `send` calls with a `InteractiveTerminalGuard` RAII
type. `engage()` sends `PauseEvents` (only when `interactive` is
true) and arms the guard. `Drop` synchronously sends `ResumeEvents`
via `try_send` (Drop can't await). The guard fires on every exit
path: Ok return, Err return, panic, future cancellation.
`try_send` can fail on a full bounded channel; the guard logs a
`tracing::warn!` rather than swallowing silently so the rare
cancel-with-saturated-event-channel case is visible in traces. The
TUI's own teardown path remains a final backstop.
Verified locally
================
* `cargo build` clean
* `cargo fmt --all -- --check` clean
* `cargo clippy --workspace --all-targets --all-features --locked
-- -D warnings` clean
* `cargo test --workspace --all-features --locked` — 2062 passed,
0 failed
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reported by @Hmbown: launching `deepseek` from any directory silently
auto-recovered the most recent interrupted session, even if that
session originated in a completely different workspace. Tools then
operated on file paths from the prior workspace while the status bar
showed the *current* workspace name — a confusing trust-boundary
violation that also leaks api_messages, working_set entries, and any
secrets the prior session had accumulated into a new terminal that
was never meant to see them.
`try_recover_checkpoint()` now compares the saved session's workspace
to `std::env::current_dir()` (canonicalised, with a strict-equality
fallback when canonicalisation fails — e.g. the original workspace
was deleted) and only auto-recovers on a match.
On a mismatch the checkpoint is still persisted as a regular session
and cleared, so the user can recover it explicitly via
`deepseek resume <ID>` after `cd`-ing back to the original workspace —
no data is lost. A one-line stderr notice points at the resume command.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>