When the onboarding flow writes the API key under `~/.deepseek/`, the
parent-dir and config-file chmod are propagated as hard errors if they
fail. On Docker-on-Windows where the container mounts `$USERPROFILE\
.deepseek` to `/home/deepseek/.deepseek`, the bind-mounted NTFS volume
can't accept Unix chmod, so `set_permissions` returns EPERM and the
user sees `Failed to save API key: Failed to set permissions on
/home/deepseek/.deepseek` — even though the directory and the secret
file were already created successfully.
The chmod is a hardening pass: the dir already lives under the user's
home and the file is created with `O_CREAT | mode(0o600)` via
OpenOptions. On filesystems where Unix permissions don't apply at all
(Docker bind-mount of NTFS, network shares, FAT, certain CI volumes),
the host's native ACL model is doing access control regardless. So
demote the chmod to best-effort on Unix: warn loudly via
`tracing::warn!` for security-sensitive operators who run with
`RUST_LOG=warn`, then continue.
Three sites:
- `config::ensure_parent_dir` — parent-dir 0o700 hardening
- `config::write_config_file_secure` — file 0o600 hardening
- `secrets::FileKeyringStore::store_unlocked` — file 0o600 hardening
(the parent-dir chmod here was already best-effort via `let _ =`)
Tests:
- `cargo test -p deepseek-secrets --all-features --locked` → 16/16 pass
(including `file_store_round_trips_with_secure_perms` which still
asserts mode 0o600 on a normal Linux test FS)
- `cargo test -p deepseek-tui --bin deepseek-tui --all-features
save_api_key --locked` → 5/5 pass
- `cargo clippy -p deepseek-tui -p deepseek-secrets --all-features
--locked -- -D warnings` clean
- `cargo fmt --all -- --check` clean
The GitBash paste failure mode reported in the same issue is a terminal
quirk (GitBash on Windows doesn't reliably forward Ctrl+V to TUI apps);
PowerShell + Shift+Insert work, as the reporter discovered. Not in
scope here.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The skill registry already walks workspace-local `.claude/skills` for
Claude Code interop, plus global `~/.agents/skills` and
`~/.deepseek/skills`. Picking up the global `~/.claude/skills` brings
DeepSeek TUI in line with the broader Claude-ecosystem convention so
users can inherit skills installed for other Claude-compatible tools
without re-authoring them in DeepSeek's native layout.
Adds `claude_global_skills_dir()` mirroring `agents_global_skills_dir()`
and inserts it into `skills_directories()` between the agentskills.io
global and the DeepSeek-native global. Workspace candidates still win
on name conflicts; first-match-wins is preserved.
Tests:
- claude_global_skills_dir_returns_home_relative_path
- existing_skill_dirs_orders_globals_agents_then_claude_then_deepseek
- All 55 pre-existing skills tests still pass
Docs synced (README publishing-skills section, CONFIGURATION).
docs/COMPETITIVE_ANALYSIS.md already advertised this lookup; this
brings the implementation in line with the documented contract.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The parent agent's turn ends right after `agent_spawn` returns
`status: Running`, leaving children to finish in the background with no
path back into the parent's inference loop. The model has to be poked
by a human before it resumes the plan.
Wire a wakeup channel from `run_subagent_task` into the engine's turn
loop. When the model produces no more tool calls but direct children
are still running, the loop now blocks on the next completion (with
cancel and steer escape hatches), drains all pending sentinels, and
re-enters inference with the existing `<deepseek:subagent.done>`
elements injected as user messages. This fulfils the contract already
documented in `prompts/base.md` (lines 189-205): the parent is promised
it'll see the sentinel when a child finishes.
The async `agent_spawn` semantics from #239 are preserved — only direct
children fire on the channel (gated by `spawn_depth == 1`), so
grandchildren spawned recursively don't flood the parent.
Accepts SKILL.md files without YAML frontmatter by using the first Markdown H1 heading as the skill name. This lets compatible plain Markdown skill files show up instead of being skipped with parse warnings.\n\nI added a small maintainer test commit to keep the command tests hermetic and cover both the plain-heading fallback and missing-heading warning path.\n\nVerification:\n- cargo test -p deepseek-tui skills --all-features\n- cargo fmt --all -- --check\n- git diff --check xieshutao/main...HEAD\n- cargo clippy -p deepseek-tui --all-targets --all-features -- -D warnings\n- GitGuardian Security Checks passed
Adds a PowerShell Set-Clipboard fallback on Windows when arboard clipboard writes are unavailable, before falling back to OSC 52.\n\nVerification:\n- cargo fmt --all -- --check\n- git diff --check origin/main...HEAD\n- cargo test -p deepseek-tui clipboard --all-features\n- cargo clippy -p deepseek-tui --all-targets --all-features -- -D warnings\n- contributor verified the same focused Windows checks on stable-x86_64-pc-windows-msvc\n\nNote: local x86_64-pc-windows-gnu cross-check is blocked on this macOS machine by missing x86_64-w64-mingw32-gcc.
Prints the canonical resume command after a successful TUI exit when a session id is available.\n\nVerified with local focused tests plus green CI.\n\nCloses #682.
* fix(tui): cache @mention completions to fix cursor lag (#792)
Cache file-mention completion results to avoid re-walking the filesystem
on every keystroke. The cache is invalidated when the partial text after
@, cursor position, or input content changes.
Fixes#792
* fix: keep mention completion cache stable on cursor moves
* style: satisfy mention cache clippy
---------
Co-authored-by: Hunter Bown <hmbown@gmail.com>
* feat(tui): show skills in slash command autocomplete menu
- add SlashMenuEntry struct with name, description, is_skill fields
- cache skill names/descriptions in App to avoid per-keystroke disk I/O
- render slash popup as two-column layout with precise Unicode width truncation
- include skill names in Tab autocomplete and visible_slash_menu_entries
- refresh skill cache after install/uninstall
* style(tui): apply rustfmt to slash menu and widgets
* fix: complete skills with valid slash command form
---------
Co-authored-by: Hunter Bown <hmbown@gmail.com>
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>