Commit Graph

504 Commits

Author SHA1 Message Date
AGSaturn 8e987a4702 fix: preserve requested model ID casing in registry resolution (#733)
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>
2026-05-06 04:52:10 -05:00
Hunter Bown b62af3e047 fix: skip repeated language step after api key setup (#844) 2026-05-06 04:42:12 -05:00
Gerry Qi 7d798cfa66 fix(core): wrap fire-and-forget spawn_blocking calls with panic dump … (#810)
* fix(core): wrap fire-and-forget spawn_blocking calls with panic dump protection

* test(utils): cover supervised blocking panic handling

---------

Co-authored-by: Hunter Bown <hmbown@gmail.com>
2026-05-06 04:32:37 -05:00
zhouyf d5f4d89352 fix(web_run): decode percent-encoded UTF-8 bytes correctly (#840)
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>
2026-05-06 04:28:30 -05:00
banqii 0a17f144c0 feat(skills): add .cursor/skills to skill discovery paths (#817)
* feat(skills): add .cursor/skills to skill discovery paths

    Adds Cursor IDE interop by scanning .cursor/skills alongside the
    existing .opencode/skills and .claude/skills directories (#432).
    Precedence: .agents/skills > skills > .opencode/skills >
    .claude/skills > .cursor/skills > ~/.deepseek/skills.

* test(skills): cover cursor skills discovery

---------

Co-authored-by: ornn.ban <ornn.ban@biuya.com>
Co-authored-by: Hunter Bown <hmbown@gmail.com>
2026-05-06 04:22:54 -05:00
kitty 719594636c feat(commands): add /rename command to set a custom session title (#836)
* 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>
2026-05-06 04:13:23 -05:00
Jason b86baa38ed fix(tools): enforce network policy in web_run (#800)
* 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>
2026-05-06 04:01:59 -05:00
zhouyf a212e44655 fix(tests): pin test app locale and provider to deterministic values (#825)
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>
2026-05-06 03:56:10 -05:00
Ziang Xie e2f596075f fix: write all config files with restrictive permissions (0o600) (#837)
* 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>
2026-05-06 03:51:48 -05:00
Ziang Xie df288c6734 fix: save config with restrictive permissions and improve secret redaction (#833)
* 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>
2026-05-06 03:42:27 -05:00
Hunter Bown 03e59c60ce fix(rlm): pin child calls to flash (#832) 2026-05-06 03:41:47 -05:00
Ziang Xie 69714819f8 fix: replace unreachable!() with proper error returns for DeepSeek providers (#835)
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.
2026-05-06 03:38:30 -05:00
Ziang Xie 4994ce99f8 fix(command_safety): fix path_escapes_workspace false positive for double-dots in names (#824)
* 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>
2026-05-06 03:33:28 -05:00
Ziang Xie 026e9558a9 fix: replace std::thread::sleep with tokio::time::sleep in async context (#831)
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.
2026-05-06 03:29:43 -05:00
Hunter Bown 77b6d43088 chore(doctor): show resolved API endpoint (#823)
* chore(doctor): show resolved API endpoint

* test(tui): serialize markdown OSC8 parity render
2026-05-06 03:17:58 -05:00
Hunter Bown ab92892cc4 fix(session): scope latest resume to workspace (#830) 2026-05-06 03:16:44 -05:00
YuanSheng Wang 3e93143079 feat(tui): support Shift+Enter to insert newline in composer (#801)
* 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>
2026-05-06 03:10:12 -05:00
Ziang Xie 950860768f fix(web_search): complete HTML entity decoding with numeric character references (#822)
* 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 (&#65; → A)
- Hex numeric references (&#x41; → A)
- Unknown entities are passed through unchanged

* style(web_search): apply rustfmt

---------

Co-authored-by: Hunter Bown <hmbown@gmail.com>
2026-05-06 02:58:20 -05:00
Hunter Bown 58a0039c92 fix(install): avoid unstable file locking API (#821) 2026-05-06 02:53:30 -05:00
Hunter Bown a09a72572f fix(network): add allowlist slash command (#819) 2026-05-06 02:46:02 -05:00
Hunter Bown 1171c5e401 fix(skills): ignore symlinks outside selected install root (#814) 2026-05-06 02:36:57 -05:00
Zhang Yonglun 4408b59852 fix(config): the skills directory should be read recursively (#811) 2026-05-06 02:27:06 -05:00
ccomma 0d50cab251 fix: skip snapshots for dangerous workspaces (#804) 2026-05-06 02:14:59 -05:00
Hunter Bown a2ca64018e feat(cost): support yuan display (#806) 2026-05-06 01:36:46 -05:00
Jason 1981c09970 fix(snapshot): refuse to snapshot $HOME directory (#798)
* 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>
2026-05-06 01:08:05 -05:00
Hunter Bown 8c091cce98 fix(tui): reflect approval mode in agent prompts (#797) 2026-05-06 00:47:04 -05:00
wucm667 05db50d182 fix(config): accept provider-prefixed DeepSeek model IDs (#794)
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>
2026-05-06 00:30:37 -05:00
Hunter Bown feaae514a6 fix(tui): recognize legacy ctrl-v paste input (#786) 2026-05-05 22:41:50 -05:00
Hunter Bown 3af6ef6f69 fix(tui): default mouse capture off on Windows (#785) 2026-05-05 22:40:31 -05:00
zxyasfas 0fded51824 docs: align Rust MSRV references with workspace (#739) 2026-05-05 22:39:50 -05:00
Hunter Bown ece6b88e79 feat(acp): add stdio adapter for editor agents (#782) 2026-05-05 22:30:17 -05:00
Reid 02456429ca Add shell completion setup examples (#742)
* 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>
2026-05-05 22:28:10 -05:00
Lawrence 76e5c90185 fix(tui): preserve UTF-8 while stripping ANSI
Preserve multi-byte UTF-8 characters when stripping ANSI/OSC8 sequences from transcript output.\n\nVerified locally with:\n- cargo fmt --all -- --check\n- cargo test -p deepseek-tui --bin deepseek-tui tui::osc8
2026-05-05 22:13:59 -05:00
Hunter Bown 82851aece4 fix(provider): complete vLLM integration on main 2026-05-05 21:27:02 -05:00
Agent007 a335ff5e4c feat(provider): add vLLM provider support (#737)
Add vLLM as a first-class OpenAI-compatible self-hosted provider with VLLM_BASE_URL, VLLM_API_KEY, and VLLM_MODEL wiring.
2026-05-05 21:22:24 -05:00
Hunter Bown 50780a5289 fix: restore auto model routing (#772)
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.
2026-05-05 21:22:03 -05:00
Hunter Bown 63e2234c6b fix(config): repair first-run auth and settings selection (#763)
* fix(auth): recover from env-only invalid API keys

Refs #759.

* fix(tui): repair config modal selection rendering

Refs #759.
2026-05-05 20:05:17 -05:00
Hunter Bown ab59ef8ff2 fix(cost): count V4 reasoning tokens in usage output (#762) 2026-05-05 19:57:25 -05:00
Hunter Bown cc6c6c6053 fix(scroll): Up/Down arrows now scroll transcript when composer is empty
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.
2026-05-05 13:54:21 -05:00
Hunter Bown c4cbd7c19f chore(release): finalize v0.8.13 stabilization 2026-05-05 13:06:09 -05:00
Hunter Bown 6b0a01d054 feat(client): remove dead responses_api_proxy module and EXPERIMENTAL_RESPONSES_API_ENV plumbing
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
2026-05-05 04:22:35 -05:00
Hunter Bown 38bcb5ca3d fix(tools): resolve hallucinated tool names via deterministic ladder
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
2026-05-05 04:09:05 -05:00
Hunter Bown c018afdf82 fix(tools): repair invalid JSON in tool-call arguments via deterministic ladder
DeepSeek streamed tool_calls.function.arguments occasionally arrives
malformed — trailing commas after SSE chunk boundaries, unclosed
braces from truncated streams, raw control chars from local-inference
backends. The new arg_repair module runs a 5-stage repair (strict parse
→ control-char strip → trailing-comma strip → brace balance → empty
object fallback) before tool dispatch.

Preserves V4 thinking-mode reasoning_content invariants on repair paths.

Closes #712
2026-05-05 04:03:58 -05:00
Hunter Bown b7f86ae19f fix(tools): sanitize tool schemas before sending to DeepSeek
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
2026-05-05 03:55:32 -05:00
Hunter Bown 310c975597 feat(models): preserve dated variant suffixes; remove legacy alias machinery
`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
2026-05-05 03:49:20 -05:00
Hunter Bown 16142b5f5e fix(release): unbreak Windows build + harden Linux apt step
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>
2026-05-05 03:04:17 -05:00
Hunter Bown 7a27e6bd1f chore(v0.8.12): bump internal dep pins to 0.8.12, sync lockfile, refresh README contributor thanks and zh-CN section
- Bump all internal deepseek-* path dependency versions from 0.8.11 → 0.8.12
- Sync Cargo.lock after version bump
- Refresh en/zh-CN README Thanks sections with v0.8.9–v0.8.12 contributors
- Update zh-CN README 'What's New' from v0.8.10 → v0.8.12
- Fix zh-CN pricing discount end date: May 5 → May 31
2026-05-05 02:47:12 -05:00
Hunter Bown 9d91a51064 fix(scroll): RAII pause/resume guard so cancelling an interactive tool restores the terminal
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>
2026-05-05 02:33:09 -05:00
Hunter Bown 02fc16e10f style: clippy sweep across community PRs (-D warnings)
13 clippy errors had accumulated from squash-merged community PRs:
collapsible-if (10), needless-late-init (1), derivable-impls (1),
sort-unstable hint (1). All auto-fixable mechanical lints — no
behaviour change. Required to satisfy CI's
`cargo clippy --workspace --all-targets --all-features --locked
-- -D warnings` gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 02:15:16 -05:00
Hunter Bown a6d9d7cf5b fix(security): refuse to auto-recover checkpoint from another workspace
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>
2026-05-05 02:07:58 -05:00