Hunter Bown 3d3ff0c5cf Release v0.8.4: Phase 1 i18n + cache-prefix stability
* fix(pricing): extend V4 Pro 75% discount expiry to 2026-05-31 15:59 UTC

DeepSeek extended the promotional discount past the original 2026-05-05
cutoff. Without this update the TUI would have started showing 4× the
actual billed cost on May 6.

Source: https://api-docs.deepseek.com/quick_start/pricing — "extended
until 2026/05/31 15:59 UTC".

Adds a regression test pinning the new active window so a future revert
to the May 5 date trips the suite immediately.

Closes #267

* chore: remove stale TODO(integrate) markers from already-integrated modules

Five `// TODO(integrate)` comments and one matching "Not yet integrated"
note were misleading anyone grepping for integration work. Each module
is in fact wired up:

- execpolicy/mod.rs       → tools/shell.rs:1322 (load_default_policy)
- sandbox/mod.rs          → tools/shell.rs:28, main.rs:2647, tui/approval.rs:30
- sandbox/policy.rs       → main.rs:2752, tui/approval.rs:30 (SandboxPolicy)
- command_safety.rs       → tools/shell.rs:1321, tools/tasks.rs:13,
                            tools/approval_cache.rs:26
- tui/streaming/mod.rs    → tui/app.rs:38 (StreamingState)

The remaining TODO at mcp.rs:1771 covers a separate "wire legacy sync API
into CLI subcommands or remove" decision and is left in place.

Closes #266

* docs(release): add install + dual-binary template to GitHub Release page

Closes #265.

The Release page used the auto-generated commit-title body. New users
hitting the Release page from Twitter / npm-search had no on-page
guidance that the dispatcher (`deepseek`) and the TUI runtime
(`deepseek-tui`) ship as two binaries that must coexist; #258 was an
external user spending 11 minutes figuring this out and #272 was the
follow-on confusion.

The new body covers:
- npm wrapper as the recommended install
- `cargo install deepseek-tui-cli deepseek-tui --locked` (both crates)
- Manual download with a per-platform table showing both artifacts
- sha256 verify using the existing `deepseek-artifacts-sha256.txt`
- Changelog link

* feat(debug): add /cache command surfacing per-turn DeepSeek cache hit/miss

Step 1 of #263. Without per-turn telemetry the prefix-cache audit is
unfounded speculation; the rest of the issue's investigation steps
depend on this surface.

The DeepSeek API already returns `prompt_cache_hit_tokens` and
`prompt_cache_miss_tokens` per turn, and we already store the *latest*
on App. This adds a 50-turn ring (`turn_cache_history`) populated at
the same site as `last_prompt_cache_*_tokens`, plus a `/cache [count]`
slash command that renders a fixed-width table of the last N turns
with per-turn ratios and a session aggregate. Default count is 10;
larger values clamp to the ring size.

Edge cases the formatter handles:

- No telemetry yet → friendly "no turns recorded" message
- `cache_hit_tokens = None` (provider didn't report) → row renders all
  em-dashes and is excluded from session aggregates so one missing-
  telemetry turn can't make the average ratio look broken.
- `cache_hit_tokens = Some, cache_miss_tokens = None` → infer miss as
  `input − hit` and mark the cell with `*`. Footer documents the
  asterisk.
- Ring at cap (50) → push evicts oldest.

Tests cover all four paths plus the cap.

* test(prompts): add cache-prefix stability harness for #263 step 2

The DeepSeek prefix-cache only hits while the byte prefix of each
request matches the prior call. Anything in the cached prefix that
varies turn-to-turn for unchanged inputs is a cache buster.

Adds a focused harness next to the production surface so the property
is regression-guarded:

1. `first_divergence(a, b)` helper that returns the first divergent
   byte position with a `±32 byte` window of context, used by the
   custom assertion `assert_byte_identical`. Future suspect tests can
   reuse this to surface "where" rather than just "fail".

2. `compose_prompt_is_byte_stable_across_calls` — sweeps every
   (mode, personality) pair and pins that two consecutive calls
   produce identical bytes. Rules out suspect #4 (mode-prompt churn).

3. `system_prompt_for_mode_with_context_is_byte_stable_for_unchanged_workspace`
   — the call site `engine.rs::build_tool_context` actually invokes,
   pinned for an empty workspace across all three modes.

4. `system_prompt_with_working_set_summary_is_byte_stable_for_constant_summary`
   — pins that the surrounding prompt construction faithfully embeds
   the working_set summary it's given without injecting extra
   non-determinism. (The actual working_set summary stability lives
   in `working_set.rs` and is the next investigation target — see
   issue note in PR description.)

Foundation for the suspect-by-suspect bisection in the rest of #263.

* fix(secrets): never overwrite the secrets file when load_unlocked errors

`FileKeyringStore::set` and `delete` did
`self.load_unlocked().unwrap_or_default()`, which wiped every existing
secret if the read failed for any reason other than \"file is missing\":

- file mode != 0600 (`InsecurePermissions`) — easy on headless / CI
  environments where a permissive umask got applied
- corrupt JSON
- transient I/O error

In all of those, the next `store_unlocked` overwrote the file with an
empty-or-single-entry blob and reset perms to 0600, silently losing
every other provider's key.

Switch both call sites to `?`. `load_unlocked` already returns
`Ok(default)` for a missing file, so the first-write-creates-the-file
ergonomic is preserved (covered by the new
`file_store_set_still_creates_file_when_missing` test).

Adds four regression tests:

- set: insecure perms surface InsecurePermissions and leave the file
  byte-identical.
- delete: same.
- set: corrupt JSON surfaces the parse error and leaves the file
  byte-identical.
- set: missing file path still works (idempotence guard).

Closes #281

* fix(cache): make tool catalog byte-stable across calls and sessions

DeepSeek's KV prefix cache hits on the longest matching byte prefix of
the request. Two places in the tool-array path were silently introducing
divergence:

1. `ToolRegistry::to_api_tools()` iterated `self.tools.values()` directly.
   Rust's default `HashMap` is seeded with `RandomState` per process, so
   every `deepseek` launch produced a different tool order — the cross-
   session resume case (the one with the biggest cache wins) never hit.

2. `active_tool_list_from_catalog()` filtered the catalog `Vec` by the
   active set in catalog order. When ToolSearch activated a previously-
   deferred tool mid-conversation, the new tool appeared at its catalog
   index, shifting every later tool's byte offset and busting the cached
   prefix from there onwards.

Fixes:

- `to_api_tools()` now sorts by tool name before emitting the API tool
  array. Stable across calls AND across launches.
- `build_model_tool_catalog()` sorts each partition (built-ins first,
  contiguous; MCP tools after, also alphabetical). Mirrors Claude Code's
  `assembleToolPool` strategy where they explicitly call out cache
  stability as the reason: "a flat sort would interleave MCP tools into
  built-ins and invalidate all downstream cache keys whenever an MCP
  tool sorts between existing built-ins."
- `active_tool_list_from_catalog()` puts always-loaded tools in catalog
  order at the head and deferred-but-now-active tools at the tail. A
  deferred-tool activation during ToolSearch no longer shifts earlier
  tools' positions.

Adds three regression tests:

- `to_api_tools_emits_alphabetical_order_regardless_of_registration_order`
- `model_tool_catalog_sorts_each_partition_for_prefix_cache_stability`
- `active_tool_list_pushes_deferred_activations_to_the_tail`

Refs #263. Findings produced by reading reference Claude Code source
side-by-side with our request-building flow; full delta analysis in
the PR description.

* fix(sandbox): elevate Agent-mode shell sandbox to allow network access

The seatbelt-default policy is `WorkspaceWrite { network_access: false }`,
which on macOS emits `(deny default)` with no `(allow network-outbound)` /
`(allow system-socket)`. Every outbound socket call from a sandboxed
shell command — including `getaddrinfo` for DNS — gets denied by the
kernel. Symptom: "DNS resolution failed" for any URL the model tries to
reach via curl, yt-dlp, package managers, etc.

Engine.build_tool_context only elevated the policy in Yolo mode, leaving
Agent mode (the default) stuck on the strict default. That's tighter
than competitors (Claude Code, Codex) without buying any safety the
application-level NetworkPolicy or the approval flow doesn't already
provide.

Switch the elevation to a `match` so:

- Plan       → no elevation (read-only investigation; shell tool not registered)
- Agent      → WorkspaceWrite { network_access: true, … }
- Yolo       → WorkspaceWrite { network_access: true, … } (unchanged)

Adds `agent_and_yolo_modes_elevate_shell_sandbox_to_allow_network` so a
future revert to the no-network default trips CI immediately.

Closes #273

* fix(skills): treat bare github.com/<owner>/<repo> URLs as GitHubRepo

Closes #269.

`/skill install https://github.com/obra/superpowers` failed on every
platform with `invalid gzip header`. Root cause: `InstallSource::parse`
matched any `https://`-prefixed spec as `DirectUrl`, so the installer
downloaded the HTML repo page (200 OK, `text/html`) and tried to
gzip-decode HTML. The user reported it from Win11 + PowerShell but the
parse path is platform-independent.

Recognize bare GitHub repo URLs in `InstallSource::parse`:

- `https://github.com/<owner>/<repo>`
- `https://github.com/<owner>/<repo>/`
- `https://github.com/<owner>/<repo>.git`
- `https://github.com/<owner>/<repo>.git/`
- `https://www.github.com/<owner>/<repo>`
- `http://github.com/<owner>/<repo>` (legacy)

…all route to the existing `GitHubRepo` source, which already produces
`https://github.com/<repo>/archive/refs/heads/{main,master}.tar.gz`
candidates with proper fallback. URLs with a third path segment
(`/archive/...`, `/blob/...`, `/tree/...`) keep going through
`DirectUrl` because the user picked that exact path.

Adds two regression tests: one asserting the seven recognised forms
all canonicalize to `github:obra/superpowers`, and one pinning the
sub-resource paths to `DirectUrl`.

* fix(cache): drop volatile fields from working_set summary block (#280) (#287)

The working-set summary lands inside the system prompt before the
historical conversation, so any byte that drifts there cache-misses
everything that follows in DeepSeek's KV prefix cache. Two sources of
turn-over-turn drift are removed:

1. The rendered line is now `- {path} ({kind})`. The previous form
   interpolated `entry.touches` and `self.turn - entry.last_turn`,
   both of which advance on every user message even when no new
   paths are observed.

2. A new `sorted_for_prompt` helper sorts by (touches DESC, path ASC)
   instead of the turn-aware `sorted_entries`. The recency bonus in
   `score_entry` crosses bucket boundaries as turns advance, so even
   without rendering `last seen` the order — and which entries cross
   the `max_prompt_entries` cutoff — drifted. Compaction pinning
   still uses `sorted_entries` because it genuinely wants recency.

Adds a regression test that observes a fixed message set, calls
`summary_block` before and after `next_turn()`, and asserts the two
outputs are byte-identical. The shared `first_divergence` /
`assert_byte_identical` helpers (from #279) move from `prompts::tests`
into `test_support` so working_set tests can reuse them.

Closes #280.

* fix(cache): memoise tool catalog so descriptions stay byte-stable (#289)

`to_api_tools` previously re-sampled `tool.description()` and
`tool.input_schema()` on every call. Native tools return `&'static str`
and a `json!` literal, so the bytes were stable in practice — but the
`McpToolAdapter` returns `self.tool.description.as_deref()`, which can
drift when the upstream MCP server reconnects with a different
description string. Any drift mid-session rewrites the tool catalog
that lands in the cached prefix and busts every byte that follows.

Adds an `api_cache: OnceLock<Vec<Tool>>` field on `ToolRegistry`. The
first `to_api_tools` call materialises the catalog; subsequent calls
return a clone of the cached vector. Mutations (`register`, `remove`,
`clear`) reset the field so the next read rebuilds. Mirrors
reference-cc's `getToolSchemaCache` (`utils/api.ts:119–208`).

Tests:
- `to_api_tools_pins_description_bytes_across_calls` registers a tool
  whose `description()` advances through a script of pre-built strings
  on each call. After the cache is populated, the second `to_api_tools`
  read returns the original description because `description()` is no
  longer invoked. Without the cache the second read would return the
  next script entry.
- `register_invalidates_api_tools_cache` registers a tool, snapshots,
  registers another, snapshots again, and asserts the second snapshot
  reflects both tools (cache rebuilt) and that the varying tool's
  description advanced (proving the rebuild actually re-sampled).
- `remove_and_clear_invalidate_api_tools_cache` covers the other two
  invalidation paths.

* fix(cache): sort project_tree and summarize_project output (#290)

Both helpers walked the workspace via `ignore::WalkBuilder::build()`
and emitted entries in the OS readdir order — non-deterministic across
filesystems (htree-hash on ext4, insertion-order on APFS, etc.). Their
output lands in the fallback branch of the system prompt's project
context (when the workspace has no AGENTS.md / CLAUDE.md) and inside
the `project_map` tool surface, both of which feed the cached prefix.

`summarize_project` now sorts the collected key-files list before the
type-detection logic and the fallback `Project with key files: …` join.

`project_tree` collects `(rel_path, is_dir)` tuples, sorts by full
path, and only then formats the indented tree. Sorting by full path
preserves the visual tree shape — `"src" < "src/lib.rs"` because the
shorter string compares less — while making siblings deterministic.

Tests cover sibling order, parent-before-children invariant, byte
stability across two consecutive calls, and the fallback `Project
with key files:` branch (the only branch where the joined order
escapes into output without further sorting downstream).

* fix(client): unique fallback id for parallel streaming tool calls (#291)

When a streamed tool_call delta omits the `id` field, the chat-completion
decoder used to fall back to the literal string `"tool_call"` for every
call. With the V4 API's native parallel tool calls (multiple tool_calls
in one delta), every parallel call ended up with the same fallback id —
downstream tool-result routing then matched the first call's result
twice and the second call hung waiting for an answer that never arrived.

The fallback now indexes by the assigned `content_block` position,
producing `"call_0"`, `"call_1"`, … within a single response. Upstream-
supplied ids are still forwarded verbatim; only the fallback path
changes.

Tests pin both invariants:
- `decoder_assigns_unique_fallback_ids_to_parallel_tool_calls_missing_id`
  feeds two tool calls without `id` in one delta and asserts they get
  distinct ids.
- `decoder_preserves_upstream_tool_call_id_when_present` keeps the
  forward-as-is path honest.

* fix(cache): place handoff and working_set after static prompt blocks (#292)

* fix(cache): drop volatile fields from working_set summary block (#280)

The working-set summary lands inside the system prompt before the
historical conversation, so any byte that drifts there cache-misses
everything that follows in DeepSeek's KV prefix cache. Two sources of
turn-over-turn drift are removed:

1. The rendered line is now `- {path} ({kind})`. The previous form
   interpolated `entry.touches` and `self.turn - entry.last_turn`,
   both of which advance on every user message even when no new
   paths are observed.

2. A new `sorted_for_prompt` helper sorts by (touches DESC, path ASC)
   instead of the turn-aware `sorted_entries`. The recency bonus in
   `score_entry` crosses bucket boundaries as turns advance, so even
   without rendering `last seen` the order — and which entries cross
   the `max_prompt_entries` cutoff — drifted. Compaction pinning
   still uses `sorted_entries` because it genuinely wants recency.

Adds a regression test that observes a fixed message set, calls
`summary_block` before and after `next_turn()`, and asserts the two
outputs are byte-identical. The shared `first_divergence` /
`assert_byte_identical` helpers (from #279) move from `prompts::tests`
into `test_support` so working_set tests can reuse them.

Closes #280.

* fix(cache): place handoff and working_set after static prompt blocks

`system_prompt_for_mode_with_context_and_skills` previously interleaved
volatile content into the static prefix:

  1. mode prompt           static
  2. project context       static
  3. working_set_summary   ← volatile
  4. skills_block          static
  5. handoff_block         ← volatile
  6. ## Context Management static
  7. COMPACT_TEMPLATE      static

Anything past byte (3) cache-missed every time the working-set drifted
or `/compact` rewrote `.deepseek/handoff.md` — including the static
`## Context Management` and `## Compaction Handoff` blocks behind them.

New order keeps every static block in the cached prefix and pushes the
two volatile blocks to the end:

  1. mode prompt
  2. project context (or fallback automap)
  3. skills block
  4. ## Context Management (Agent / Yolo only)
  5. COMPACT_TEMPLATE
  ── volatile boundary ──
  6. handoff block
  7. working-set summary

Adds a doc comment on the function describing the volatile-content-last
invariant so future contributors don't reintroduce churn into the
prefix. Adds two regression tests:

- `system_prompt_with_handoff_file_is_byte_stable_when_file_is_unchanged`
  pins the handoff path with a fixture file.
- `handoff_and_working_set_appear_after_static_blocks` asserts the
  ordering invariant directly so a future reorder fails loudly.

Reference: Claude Code's own prompt builder marks this same boundary
with a `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` constant; we don't introduce
the abstraction yet but match the principle.

* feat(i18n): localize slash command help (Phase 1a, #285) (#294)

Adds 44 new MessageIds, one per slash command, and translations to all
four shipped locales (en/ja/zh-Hans/pt-BR). Refactors CommandInfo so the
English description now lives in localization.rs (single source of
truth) instead of being duplicated on the struct, and threads the
active Locale through the three render surfaces:

- crates/tui/src/tui/views/help.rs (the ?/F1/Ctrl+/ help overlay)
- crates/tui/src/tui/command_palette.rs (Ctrl+K palette)
- crates/tui/src/commands/core.rs (the /help text command)

Usage strings (e.g. /cache [count]) stay English by design — they're
placeholder syntax, not natural language.

The existing locale-coverage test
(`shipped_first_pack_has_no_missing_core_messages`) already iterates
ALL_MESSAGE_IDS across Locale::shipped(), so the 44 new IDs are
automatically required to be present in all four locale arms or CI
fails.

This is the first of several incremental Phase 1 PRs. Phase 1b covers
the debug commands (/tokens /cost /cache), 1c the footer hints, and
1d doctor output. Phases 2–3 cover onboarding and error surfaces.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): localize /tokens /cost /cache debug output (Phase 1b, #285) (#295)

Adds 13 new MessageIds covering the report templates and the
sub-strings shared across them, with translations for all four
shipped locales (en/ja/zh-Hans/pt-BR):

- CmdTokensReport, CmdTokensContextWithWindow, CmdTokensContextUnknownWindow
- CmdTokensCacheBoth, CmdTokensCacheHitOnly, CmdTokensCacheMissOnly
- CmdTokensNotReported
- CmdCostReport
- CmdCacheNoData, CmdCacheHeader, CmdCacheTotals, CmdCacheFootnote, CmdCacheAdvice

Each template uses {placeholder} substitution via String::replace
rather than format!, since format! requires a literal — the
locale-resolved &'static str isn't one. The placeholder convention
({active}, {hit}, {miss}, …) means a translator can re-order or
restructure a sentence freely without changing the call site.

Helpers `token_count`, `active_context_summary`, `cache_summary`, and
`format_cache_history` now take `Locale` so each can resolve their
templates from the same source of truth.

The English templates byte-match the previous hardcoded format strings
so the existing 16 debug-command tests pass unchanged.

Column headers in the cache table (`turn   in    out   hit   miss …`)
are intentionally NOT localized — the body rows are formatted with
fixed column widths and translating the header words would break
alignment. Numbers, ratios, and the model id stay in English form.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): localize footer state + help section labels (Phase 1c, #285) (#296)

Adds 11 new MessageIds covering visible footer chrome and the help-overlay
section headings, with translations for all four shipped locales:

Footer:
- FooterWorking — animated `working` / `working.` / … pulse
- FooterAgentSingular / FooterAgentsPlural — the sub-agent count chip
- FooterPressCtrlCAgain — the quit-confirmation toast

Help overlay sections (`?` / `F1` / `Ctrl+/`):
- HelpSectionNavigation, HelpSectionEditing, HelpSectionActions,
  HelpSectionModes, HelpSectionSessions, HelpSectionClipboard,
  HelpSectionHelp

`KeybindingSection::label` now takes Locale and returns tr(locale, …).
`footer_working_label` and `footer_agents_chip` likewise take Locale; the
two production callsites in tui/ui.rs pass `app.ui_locale`.

The mode chip itself (agent / yolo / plan) intentionally stays English —
those are brand/acronym labels, and translating them would mean explaining
to maintainers what `代理` means in a bug report.

The keybinding catalog DESCRIPTIONS (41 entries) are not translated in this
PR — those are technical prose that would dwarf the rest of i18n work and
can ship in v0.8.5. Section labels are translated so the help overlay
groups read as expected in any locale.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(commands): smoke-test that every / command dispatches to a handler (#299)

Adds two parallel-safe smoke tests in `crates/tui/src/commands/mod.rs`
that iterate the COMMANDS registry and verify every command — and every
declared alias — dispatches to a real handler. A dispatch miss surfaces
as the fall-through `Unknown command:` error message in `execute`,
which used to be invisible until a user typed the command and saw the
"did you mean" suggestion fire on a registered command.

The tests build a workspace-isolated app via `tempfile::TempDir` so
side-effecting handlers (`/init` writing AGENTS.md, `/save` and
`/export` writing files) do not pollute `crates/tui/` when CI runs from
there. `/save` and `/export` get an explicit tempdir-relative path
because their no-arg defaults still resolve relative to `cwd`.

`/restore` is skipped — it shells out to git for the snapshot repo and
its own dedicated tests in `commands/restore.rs` already serialize on
the global env mutex via `scoped_home`. The existing coverage there is
sufficient.

Closes a gap surfaced when verifying that the v0.8.4 i18n refactor
(#294, #295, #296) did not silently break any slash-command dispatch.
All 44 commands and their aliases pass (16 aliases on top of the
44 names; `/restore` is the only skip).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(release): bump version to 0.8.4 (#297)

CHANGELOG entry covers the v0.8.4 work landed since 0.8.3:

- Localization Phase 1 (#285) — slash command help (#294), debug command
  output (#295), footer state and help-overlay section labels (#296).
  Adds 68 new MessageIds across all four shipped locales (en/ja/zh-Hans/pt-BR).

- Cache-prefix stability (#263) — five companion fixes (#287, #288→#292,
  #289, #290, #291) that keep the DeepSeek prefix cache stable across turns.

- Plus the items already in [Unreleased]: agent-mode network exec (#272),
  /skill GitHub URL parsing (#269), and the V4 Pro discount expiry extension
  (#267).

Bumps:
- Cargo.toml workspace version 0.8.3 → 0.8.4
- npm/deepseek-tui/package.json version + deepseekBinaryVersion 0.8.3 → 0.8.4
- Cargo.lock regenerated from the new workspace version.

Phase 1d (doctor output), Phase 2 (onboarding/init/missing-companion),
and Phase 3 (tool errors / sandbox denials / approvals) deferred to v0.8.5.
The shipped Phase 1 surfaces (slash commands, debug telemetry, footer
chrome) cover the highest-traffic UI paths Chinese users see first.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(release): bump internal path-dep versions + repair doc link (#301)

CI on PR #300 (release feat/v0.8.4 → main) flagged two regressions
introduced by the 0.8.4 version bump:

1. Version drift — path-dependency `version = "0.8.3"` references
   inside the workspace crates (10 crates: agent, app-server, cli,
   config, core, execpolicy, hooks, mcp, tools, tui) did not move with
   the workspace `[workspace.package] version = "0.8.4"`. The CI guard
   `scripts/release/check-versions.sh` requires they match.

2. Broken intra-doc-link `[crate::localization::english]` in the
   CommandInfo doc comment — `english` is private. Replaced with a
   reference to the public `description_for` accessor and the public
   `tr()` function.

Verified with:
- scripts/release/check-versions.sh — Version state OK.
- RUSTDOCFLAGS=-Dwarnings cargo doc --workspace --no-deps — green.
- cargo fmt + clippy + test all green.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:02:38 -05:00
2026-04-28 21:54:43 -05:00
2026-04-25 07:59:01 -05:00
2026-04-24 16:29:01 -05:00
2026-01-20 08:57:35 -06:00

DeepSeek TUI

A terminal-native coding agent for DeepSeek V4 models — with 1M-token context, thinking-mode reasoning, and full tool-use.

简体中文 README

npm i -g deepseek-tui

CI npm crates.io

DeepSeek TUI screenshot


What is it?

DeepSeek TUI is a coding agent that runs entirely in your terminal. It gives DeepSeek's frontier models direct access to your workspace — reading and editing files, running shell commands, searching the web, managing git, and orchestrating sub-agents — all through a fast, keyboard-driven TUI.

Built for DeepSeek V4 (deepseek-v4-pro / deepseek-v4-flash) with 1M-token context windows and native thinking-mode (chain-of-thought) streaming. See the model's reasoning unfold in real time as it works through your tasks.

Key Features

  • Native RLM (rlm_query tool) — fans out 116 cheap deepseek-v4-flash children in parallel against the existing DeepSeek client for batched analysis, decomposition, or parallel reasoning
  • Thinking-mode streaming — shows DeepSeek's chain-of-thought as it reasons about your code
  • Full tool suite — file ops, shell execution, git, web search/browse, apply-patch, sub-agents, MCP servers
  • 1M-token context — automatic intelligent compaction when context fills up
  • Three interaction modes — Plan (read-only explore), Agent (interactive with approval), YOLO (auto-approved). Decomposition-first system prompts teach the model to checklist_write, update_plan, and spawn sub-agents before acting
  • Reasoning-effort tiers — cycle through off → high → max with Shift+Tab
  • Session save/resume — checkpoint and resume long sessions
  • Workspace rollback — side-git pre/post-turn snapshots with /restore and revert_turn, without touching your repo's .git
  • HTTP/SSE runtime APIdeepseek serve --http for headless agent workflows
  • MCP protocol — connect to Model Context Protocol servers for extended tooling; see docs/MCP.md
  • Live cost tracking — per-turn and session-level token usage and cost estimates
  • Dark theme — DeepSeek-blue palette

Quickstart

npm install -g deepseek-tui
deepseek

China / mirror-friendly install

If GitHub or npm downloads are slow from mainland China, install the Rust crates through a Cargo registry mirror:

# ~/.cargo/config.toml
[source.crates-io]
replace-with = "tuna"

[source.tuna]
registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"

Then install the canonical deepseek dispatcher and (optionally) the companion TUI binary:

cargo install deepseek-tui-cli --locked   # provides `deepseek`
cargo install deepseek-tui     --locked   # provides `deepseek-tui` (optional)
deepseek --version

You can also download prebuilt binaries directly from the GitHub Releases page when GitHub release assets are reachable. TUNA, rsproxy, Tencent COS, or Aliyun OSS mirrors can also be used with DEEPSEEK_TUI_RELEASE_BASE_URL when a mirrored release-asset directory is available.

On first launch you'll be prompted for your DeepSeek API key. You can also set it ahead of time:

# via CLI
deepseek login --api-key "YOUR_DEEPSEEK_API_KEY"

# via env var
export DEEPSEEK_API_KEY="YOUR_DEEPSEEK_API_KEY"
deepseek

Using NVIDIA NIM

deepseek auth set --provider nvidia-nim --api-key "YOUR_NVIDIA_API_KEY"
deepseek --provider nvidia-nim

# or per-process:
DEEPSEEK_PROVIDER=nvidia-nim NVIDIA_API_KEY="..." deepseek

Other DeepSeek V4 providers

deepseek auth set --provider fireworks --api-key "YOUR_FIREWORKS_API_KEY"
deepseek --provider fireworks --model deepseek-v4-pro

# SGLang is self-hosted; auth is optional for localhost deployments.
SGLANG_BASE_URL="http://localhost:30000/v1" deepseek --provider sglang --model deepseek-v4-flash
Install from source
git clone https://github.com/Hmbown/DeepSeek-TUI.git
cd DeepSeek-TUI
cargo install --path crates/tui --locked   # requires Rust 1.85+

What's new in v0.8.0

Shell stability and post-send responsiveness

Completed background shell jobs now release their live process and pipe handles as soon as completion is observed, while keeping the job record inspectable. This prevents long-running sessions from hitting Too many open files (os error 24), which could make checkpoint saves fail and cause shell spawning, message send, close, and Esc/cancel paths to lag or fail.

🪟 Windows REPL runtime CI hardening

Windows gets a longer Python bootstrap readiness timeout for the REPL runtime tests, matching GitHub runner startup contention without weakening bootstrap failures on other platforms.

🌏 Cargo mirror install docs

The README now includes a TUNA Cargo mirror setup and direct release asset guidance for users with slow GitHub/npm access.

🧪 Test hardening

New regression coverage proves completed background shell jobs drop their live process handles after exec_shell_wait.

Full changelog: CHANGELOG.md.


What's new in v0.7.8

Shell controls: foreground-to-background detach + exec_shell_cancel

A running foreground command can now be moved to the background interactive session — press Ctrl+B while a command is executing to open shell controls, then either detach it (it continues running and can be polled with exec_shell_wait) or cancel the current turn.

New tool: exec_shell_cancel — cancel a specific background shell task by task_id, or cancel all running background tasks with all: true.

Cancel-aware exec_shell_wait — canceling a turn while exec_shell_wait is blocking now stops the wait but leaves the background task running.

🐛 Unicode glob search fix

Filenames containing multi-byte characters (e.g., dialogue_line__冰糖.mp3) no longer panic the matches_glob function — byte-index slicing was replaced with char_indices() boundary-safe iteration.

🔄 Swarm UI reconciliation

The fanout card no longer pre-seeds with zero-state workers, eliminating the "0 done · 0 running · 0 failed · N pending" vs sidebar "N running" contradiction. The sidebar now shows "dispatching N" before the first progress event arrives from a agent_swarm invocation.

Full changelog: CHANGELOG.md.


What's new in v0.7.6

🌐 UI Localization

DeepSeek TUI now speaks your language. The new locale setting in settings.toml controls UI chrome — composer, history search, /config, help overlay, and status hints — without changing model output language.

Setting Display
locale = \"auto\" Checks LC_ALLLC_MESSAGESLANG (default)
locale = \"ja\" Japanese
locale = \"zh-Hans\" Chinese Simplified
locale = \"pt-BR\" Portuguese (Brazil)
locale = \"en\" English fallback

Unsure what to pick? Run locale in your terminal; the first matching tag is used automatically.

📋 Smarter paste handling

Paste-burst detection catches rapid-key pastes in terminals that don't send bracketed-paste events — CRLF is normalized, and multiline pastes stay buffered until you stop typing. Configurable via paste_burst_detection.

Forgot that prompt you wrote an hour ago? Alt+R opens a live search across input history and recovered drafts. Type to filter, Enter to accept, Esc to restore what you were typing.

👁️ Pending input preview

During a running turn, queued messages, pending steers, and context chips appear above the composer so you can see what will be sent next. Alt+↑ pops the last queued message back for editing.

⚙️ Grouped /config editor

/config now groups settings by section (Model, Permissions, Display, ...) with a live filter. ↑/↓ (or j/k when the filter is empty) navigate; Enter/e edit the selected row; Esc clears the filter or closes.

⌨️ Searchable help overlay

? (with empty input), F1, or Ctrl+/ opens a searchable help overlay. Type to filter commands and keybindings; multi-term searches act as AND.

Full history: CHANGELOG.md.


Models & Pricing

DeepSeek TUI targets DeepSeek V4 models with 1M-token context windows by default.

Model Context Input (cache hit) Input (cache miss) Output
deepseek-v4-pro 1M $0.003625 / 1M* $0.435 / 1M* $0.87 / 1M*
deepseek-v4-flash 1M $0.0028 / 1M $0.14 / 1M $0.28 / 1M

Legacy aliases deepseek-chat and deepseek-reasoner silently map to deepseek-v4-flash.

NVIDIA NIM hosted variants (deepseek-ai/deepseek-v4-pro, deepseek-ai/deepseek-v4-flash) use your NVIDIA account terms — no DeepSeek platform billing.

*DeepSeek lists the Pro rates above as a limited-time 75% discount valid until 2026-05-05 15:59 UTC; the TUI cost estimator falls back to base Pro rates after that timestamp.


Usage

deepseek                                      # interactive TUI
deepseek "explain this function"              # one-shot prompt
deepseek --model deepseek-v4-flash "summarize" # model override
deepseek --yolo                               # YOLO mode (auto-approve tools)
deepseek login --api-key "..."                # save API key
deepseek doctor                               # check setup & connectivity
deepseek doctor --json                        # machine-readable diagnostics
deepseek setup --status                       # read-only setup status
deepseek setup --tools --plugins              # scaffold local tool/plugin dirs
deepseek models                               # list live API models
deepseek sessions                             # list saved sessions
deepseek resume --last                        # resume latest session
deepseek serve --http                         # HTTP/SSE API server
deepseek mcp list                             # list configured MCP servers
deepseek mcp validate                         # validate MCP config/connectivity
deepseek mcp-server                           # run dispatcher MCP stdio server

Keyboard shortcuts

Key Action
Tab Complete / or @ entries; while a turn is running, queue the draft as a follow-up; otherwise cycle mode
Shift+Tab Cycle reasoning-effort: off → high → max
F1 Help
Esc Back / dismiss
Ctrl+K Command palette
Ctrl+R Resume an earlier session
Alt+R Search prompt history and recover cleared drafts
@path Attach file/directory context in composer
(at composer start) Select attachment row for removal
Alt+↑ Edit last queued message
/attach <path> Attach image/video media references; select the row with at composer start and remove with Backspace/Delete

Modes

Mode Behavior
Plan 🔍 Read-only investigation — model explores and proposes a decomposition plan (update_plan + checklist_write) before making changes
Agent 🤖 Default interactive mode — multi-step tool use with approval gates; model outlines work via checklist_write before requesting writes
YOLO Auto-approve all tools in a trusted workspace; model still creates checklist_write/update_plan to keep work visible and trackable

Configuration

~/.deepseek/config.toml — see config.example.toml for every option.

Key environment overrides:

Variable Purpose
DEEPSEEK_API_KEY API key
DEEPSEEK_BASE_URL API base URL
DEEPSEEK_MODEL Default model
DEEPSEEK_PROVIDER Provider: deepseek (default), nvidia-nim, fireworks, or sglang
DEEPSEEK_PROFILE Config profile name
NVIDIA_API_KEY NVIDIA NIM API key
FIREWORKS_API_KEY Fireworks AI API key
SGLANG_BASE_URL Self-hosted SGLang endpoint
SGLANG_API_KEY Optional SGLang bearer token

Quick diagnostics: deepseek setup --status checks API key, MCP, sandbox, and .env state without network calls; deepseek doctor --json is suitable for CI; deepseek setup --tools --plugins scaffolds local tool and plugin directories.

DeepSeek context caching is automatic — when the API returns cache hit/miss token fields, the TUI includes them in usage and cost tracking.

Full reference: docs/CONFIGURATION.md and docs/MCP.md.

UI locale is separate from model language — set locale in settings.toml or via the LC_ALL/LANG environment variables. See docs/CONFIGURATION.md.


Publishing your own skill

DeepSeek-TUI discovers skills from the active skills directory. Workspace-local .agents/skills wins when present, then ./skills, then the configured global directory (~/.deepseek/skills by default). Each skill is a directory with a SKILL.md file:

~/.deepseek/skills/my-skill/
└── SKILL.md

SKILL.md must start with YAML frontmatter:

---
name: my-skill
description: Use this when DeepSeek should follow my custom workflow.
---

# My Skill

Instructions for the agent go here.

Run /skills to list discovered skills, /skill <name> to activate one for the next message, or /skill new to use the bundled skill-creator helper. Installed skills are also listed in the model-visible session context so the agent can choose relevant skills when the user names them or when the task matches their descriptions.

DeepSeek-TUI can also install community skills directly from a GitHub repo, with no backend service in the loop:

  1. Create a public GitHub repo with a SKILL.md at the root containing the usual --- frontmatter (name, description).
  2. Multi-skill bundles use skills/<name>/SKILL.md instead — the installer picks the first match and names the install after the frontmatter name.
  3. Push to main (or master); the installer fetches archive/refs/heads/main.tar.gz and falls back to master.tar.gz.
  4. Users install via /skill install github:<owner>/<repo> — installs are gated by the [network] policy, validated for path traversal and size, and placed under ~/.deepseek/skills/<name>/.
  5. Submit a PR to the curated index.json (default registry) to make the skill installable by name (/skill install <name>) instead of the GitHub spec.
  6. Use /skill update <name>, /skill uninstall <name>, or /skill trust <name> for installed community skills. Trust is only needed when you want scripts bundled with a skill to be eligible for execution.

Documentation

Doc Topic
ARCHITECTURE.md Codebase internals
CONFIGURATION.md Full config reference
MODES.md Plan / Agent / YOLO modes
MCP.md Model Context Protocol integration
RUNTIME_API.md HTTP/SSE API server
RELEASE_RUNBOOK.md Release process
OPERATIONS_RUNBOOK.md Ops & recovery

Contributing

See CONTRIBUTING.md. Pull requests welcome!

Not affiliated with DeepSeek Inc.

License

MIT

S
Description
No description provided
Readme 24 MiB
Languages
Rust 94%
TypeScript 2.6%
JavaScript 1.6%
Shell 0.8%
Python 0.6%
Other 0.1%