diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f4eadcd4..0eef3317 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -246,6 +246,7 @@ jobs:
- uses: actions/download-artifact@v4
with:
path: artifacts
+ pattern: deepseek*
- name: List artifacts
run: find artifacts -type f
- name: Generate checksum manifest
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b1b09709..c3cab001 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.8.19] - 2026-05-08
+
+### Fixed
+- **DeepSeek beta endpoint stays default for Chinese locales** - the legacy
+ `deepseek-cn` runtime path no longer routes users to the non-beta
+ `https://api.deepseek.com` base URL. It is now a backwards-compatible alias
+ for the normal `deepseek` provider default, `https://api.deepseek.com/beta`,
+ so strict tool mode and other beta-gated features stay available worldwide.
+- **Provider docs stop advertising `deepseek-cn` as a separate provider** -
+ runtime docs now describe it only as a legacy config alias. DeepSeek uses the
+ same official host worldwide; users with private mirrors should set
+ `base_url` explicitly.
+
## [0.8.18] - 2026-05-07
This is the v0.8.17 follow-up release: a tighter TUI/runtime/install pass with
diff --git a/Cargo.lock b/Cargo.lock
index c6c615f5..494597f6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1152,7 +1152,7 @@ dependencies = [
[[package]]
name = "deepseek-agent"
-version = "0.8.18"
+version = "0.8.19"
dependencies = [
"deepseek-config",
"serde",
@@ -1160,7 +1160,7 @@ dependencies = [
[[package]]
name = "deepseek-app-server"
-version = "0.8.18"
+version = "0.8.19"
dependencies = [
"anyhow",
"axum",
@@ -1182,7 +1182,7 @@ dependencies = [
[[package]]
name = "deepseek-config"
-version = "0.8.18"
+version = "0.8.19"
dependencies = [
"anyhow",
"deepseek-secrets",
@@ -1194,7 +1194,7 @@ dependencies = [
[[package]]
name = "deepseek-core"
-version = "0.8.18"
+version = "0.8.19"
dependencies = [
"anyhow",
"chrono",
@@ -1212,7 +1212,7 @@ dependencies = [
[[package]]
name = "deepseek-execpolicy"
-version = "0.8.18"
+version = "0.8.19"
dependencies = [
"anyhow",
"deepseek-protocol",
@@ -1221,7 +1221,7 @@ dependencies = [
[[package]]
name = "deepseek-hooks"
-version = "0.8.18"
+version = "0.8.19"
dependencies = [
"anyhow",
"async-trait",
@@ -1235,7 +1235,7 @@ dependencies = [
[[package]]
name = "deepseek-mcp"
-version = "0.8.18"
+version = "0.8.19"
dependencies = [
"anyhow",
"serde",
@@ -1244,7 +1244,7 @@ dependencies = [
[[package]]
name = "deepseek-protocol"
-version = "0.8.18"
+version = "0.8.19"
dependencies = [
"serde",
"serde_json",
@@ -1252,7 +1252,7 @@ dependencies = [
[[package]]
name = "deepseek-secrets"
-version = "0.8.18"
+version = "0.8.19"
dependencies = [
"dirs",
"keyring",
@@ -1265,7 +1265,7 @@ dependencies = [
[[package]]
name = "deepseek-state"
-version = "0.8.18"
+version = "0.8.19"
dependencies = [
"anyhow",
"chrono",
@@ -1277,7 +1277,7 @@ dependencies = [
[[package]]
name = "deepseek-tools"
-version = "0.8.18"
+version = "0.8.19"
dependencies = [
"anyhow",
"async-trait",
@@ -1290,7 +1290,7 @@ dependencies = [
[[package]]
name = "deepseek-tui"
-version = "0.8.18"
+version = "0.8.19"
dependencies = [
"anyhow",
"arboard",
@@ -1351,7 +1351,7 @@ dependencies = [
[[package]]
name = "deepseek-tui-cli"
-version = "0.8.18"
+version = "0.8.19"
dependencies = [
"anyhow",
"chrono",
@@ -1375,7 +1375,7 @@ dependencies = [
[[package]]
name = "deepseek-tui-core"
-version = "0.8.18"
+version = "0.8.19"
[[package]]
name = "deranged"
diff --git a/Cargo.toml b/Cargo.toml
index 17ffd3a6..df918b46 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,7 +19,7 @@ default-members = ["crates/cli", "crates/app-server", "crates/tui"]
resolver = "2"
[workspace.package]
-version = "0.8.18"
+version = "0.8.19"
edition = "2024"
# Rust 1.88 stabilized `let_chains` in `if`/`while` conditions, which the
# codebase relies on extensively. Cargo enforces this so users on older
diff --git a/README.md b/README.md
index ce242d4c..35decfce 100644
--- a/README.md
+++ b/README.md
@@ -225,11 +225,18 @@ deepseek --provider ollama --model deepseek-coder:1.3b
---
-## What's New In v0.8.18
+## What's New In v0.8.19
-A focused follow-up release for TUI/runtime/install polish.
+A hotfix release for DeepSeek endpoint defaults, plus the v0.8.18
+TUI/runtime/install polish.
[Full changelog](CHANGELOG.md).
+- **DeepSeek beta endpoint stays default worldwide** - Chinese locales and
+ legacy `deepseek-cn` configs now use `https://api.deepseek.com/beta`, so
+ strict tool mode and other beta-gated features remain available.
+- **`deepseek-cn` is legacy-only** - it is no longer advertised as a separate
+ provider. Existing configs still parse it as a backwards-compatible alias for
+ `deepseek`.
- **Plain `deepseek` starts fresh** - opening a second terminal in the same
folder now creates a new session instead of silently re-entering the same
interrupted checkpoint. Use `deepseek --continue` when you want recovery.
@@ -352,7 +359,7 @@ Key environment variables:
| `DEEPSEEK_HTTP_HEADERS` | Optional custom model request headers, e.g. `X-Model-Provider-Id=your-model-provider` |
| `DEEPSEEK_MODEL` | Default model |
| `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` | Stream idle timeout in seconds, default `300`, clamped to `1..=3600` |
-| `DEEPSEEK_PROVIDER` | `deepseek` (default), `deepseek-cn`, `nvidia-nim`, `openai`, `openrouter`, `novita`, `fireworks`, `sglang`, `vllm`, `ollama` |
+| `DEEPSEEK_PROVIDER` | `deepseek` (default), `nvidia-nim`, `openai`, `openrouter`, `novita`, `fireworks`, `sglang`, `vllm`, `ollama` |
| `DEEPSEEK_PROFILE` | Config profile name |
| `DEEPSEEK_MEMORY` | Set to `on` to enable user memory |
| `NVIDIA_API_KEY` / `OPENAI_API_KEY` / `OPENROUTER_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` | Provider auth |
@@ -434,6 +441,9 @@ Full Changelog: [CHANGELOG.md](CHANGELOG.md).
## Thanks
+- **[DeepSeek](https://github.com/deepseek-ai)** — thank you for the models and support that power every turn. 感谢 DeepSeek 提供模型与支持,让每一次交互成为可能。
+- **[DataWhale](https://github.com/datawhalechina)** 🐋 — thank you for your support and for welcoming us into the Whale Brother family. 感谢 DataWhale 的支持,并欢迎我们加入“鲸兄弟”大家庭。
+
This project ships with help from a growing community of contributors:
- **[merchloubna70-dot](https://github.com/merchloubna70-dot)** — 28 PRs spanning features, fixes, and VS Code extension scaffolding (#645–#681)
@@ -477,7 +487,7 @@ This project ships with help from a growing community of contributors:
- **[Duducoco](https://github.com/Duducoco)** and **[AlphaGogoo](https://github.com/AlphaGogoo)** — skills slash-menu and `/skills` coverage fix (#1068, #1083)
- **[ArronAI007](https://github.com/ArronAI007)** — window-resize artifact fix for macOS Terminal.app and ConHost (#993)
- **[THINKER-ONLY](https://github.com/THINKER-ONLY)** — OpenRouter and custom-endpoint model-ID preservation (#1066)
-- **[Jefsky](https://github.com/Jefsky)** — `deepseek-cn` official endpoint default (#1079, #1084)
+- **[Jefsky](https://github.com/Jefsky)** — DeepSeek endpoint correction report (#1079, #1084)
- **[wlon](https://github.com/wlon)** — NVIDIA NIM provider API-key preference diagnosis (#1081)
---
diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml
index af8b9a5b..ffecb63c 100644
--- a/crates/agent/Cargo.toml
+++ b/crates/agent/Cargo.toml
@@ -7,5 +7,5 @@ repository.workspace = true
description = "Model/provider registry and fallback strategy for DeepSeek workspace architecture"
[dependencies]
-deepseek-config = { path = "../config", version = "0.8.18" }
+deepseek-config = { path = "../config", version = "0.8.19" }
serde.workspace = true
diff --git a/crates/app-server/Cargo.toml b/crates/app-server/Cargo.toml
index 865204e3..4b8f1cf8 100644
--- a/crates/app-server/Cargo.toml
+++ b/crates/app-server/Cargo.toml
@@ -10,15 +10,15 @@ description = "Codex-style app-server transport for DeepSeek workspace architect
anyhow.workspace = true
axum.workspace = true
clap.workspace = true
-deepseek-agent = { path = "../agent", version = "0.8.18" }
-deepseek-config = { path = "../config", version = "0.8.18" }
-deepseek-core = { path = "../core", version = "0.8.18" }
-deepseek-execpolicy = { path = "../execpolicy", version = "0.8.18" }
-deepseek-hooks = { path = "../hooks", version = "0.8.18" }
-deepseek-mcp = { path = "../mcp", version = "0.8.18" }
-deepseek-protocol = { path = "../protocol", version = "0.8.18" }
-deepseek-state = { path = "../state", version = "0.8.18" }
-deepseek-tools = { path = "../tools", version = "0.8.18" }
+deepseek-agent = { path = "../agent", version = "0.8.19" }
+deepseek-config = { path = "../config", version = "0.8.19" }
+deepseek-core = { path = "../core", version = "0.8.19" }
+deepseek-execpolicy = { path = "../execpolicy", version = "0.8.19" }
+deepseek-hooks = { path = "../hooks", version = "0.8.19" }
+deepseek-mcp = { path = "../mcp", version = "0.8.19" }
+deepseek-protocol = { path = "../protocol", version = "0.8.19" }
+deepseek-state = { path = "../state", version = "0.8.19" }
+deepseek-tools = { path = "../tools", version = "0.8.19" }
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml
index 2cc4a88b..ef65eec6 100644
--- a/crates/cli/Cargo.toml
+++ b/crates/cli/Cargo.toml
@@ -14,13 +14,13 @@ path = "src/main.rs"
anyhow.workspace = true
clap.workspace = true
clap_complete.workspace = true
-deepseek-agent = { path = "../agent", version = "0.8.18" }
-deepseek-app-server = { path = "../app-server", version = "0.8.18" }
-deepseek-config = { path = "../config", version = "0.8.18" }
-deepseek-execpolicy = { path = "../execpolicy", version = "0.8.18" }
-deepseek-mcp = { path = "../mcp", version = "0.8.18" }
-deepseek-secrets = { path = "../secrets", version = "0.8.18" }
-deepseek-state = { path = "../state", version = "0.8.18" }
+deepseek-agent = { path = "../agent", version = "0.8.19" }
+deepseek-app-server = { path = "../app-server", version = "0.8.19" }
+deepseek-config = { path = "../config", version = "0.8.19" }
+deepseek-execpolicy = { path = "../execpolicy", version = "0.8.19" }
+deepseek-mcp = { path = "../mcp", version = "0.8.19" }
+deepseek-secrets = { path = "../secrets", version = "0.8.19" }
+deepseek-state = { path = "../state", version = "0.8.19" }
chrono.workspace = true
dirs.workspace = true
serde.workspace = true
diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml
index d72ff34e..1438a52a 100644
--- a/crates/config/Cargo.toml
+++ b/crates/config/Cargo.toml
@@ -8,7 +8,7 @@ description = "Config schema and precedence model for DeepSeek workspace archite
[dependencies]
anyhow.workspace = true
-deepseek-secrets = { path = "../secrets", version = "0.8.18" }
+deepseek-secrets = { path = "../secrets", version = "0.8.19" }
dirs.workspace = true
serde.workspace = true
toml.workspace = true
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
index 6c365c23..b3e787bc 100644
--- a/crates/core/Cargo.toml
+++ b/crates/core/Cargo.toml
@@ -9,13 +9,13 @@ description = "Core runtime boundaries for DeepSeek workspace architecture"
[dependencies]
anyhow.workspace = true
chrono.workspace = true
-deepseek-agent = { path = "../agent", version = "0.8.18" }
-deepseek-config = { path = "../config", version = "0.8.18" }
-deepseek-execpolicy = { path = "../execpolicy", version = "0.8.18" }
-deepseek-hooks = { path = "../hooks", version = "0.8.18" }
-deepseek-mcp = { path = "../mcp", version = "0.8.18" }
-deepseek-protocol = { path = "../protocol", version = "0.8.18" }
-deepseek-state = { path = "../state", version = "0.8.18" }
-deepseek-tools = { path = "../tools", version = "0.8.18" }
+deepseek-agent = { path = "../agent", version = "0.8.19" }
+deepseek-config = { path = "../config", version = "0.8.19" }
+deepseek-execpolicy = { path = "../execpolicy", version = "0.8.19" }
+deepseek-hooks = { path = "../hooks", version = "0.8.19" }
+deepseek-mcp = { path = "../mcp", version = "0.8.19" }
+deepseek-protocol = { path = "../protocol", version = "0.8.19" }
+deepseek-state = { path = "../state", version = "0.8.19" }
+deepseek-tools = { path = "../tools", version = "0.8.19" }
serde_json.workspace = true
uuid.workspace = true
diff --git a/crates/execpolicy/Cargo.toml b/crates/execpolicy/Cargo.toml
index 1e8846ce..501a62b4 100644
--- a/crates/execpolicy/Cargo.toml
+++ b/crates/execpolicy/Cargo.toml
@@ -8,5 +8,5 @@ description = "Execution policy and approval model parity for DeepSeek workspace
[dependencies]
anyhow.workspace = true
-deepseek-protocol = { path = "../protocol", version = "0.8.18" }
+deepseek-protocol = { path = "../protocol", version = "0.8.19" }
serde.workspace = true
diff --git a/crates/hooks/Cargo.toml b/crates/hooks/Cargo.toml
index 12a8d0c7..2132047a 100644
--- a/crates/hooks/Cargo.toml
+++ b/crates/hooks/Cargo.toml
@@ -10,7 +10,7 @@ description = "Hook dispatch and notifications parity for DeepSeek workspace arc
anyhow.workspace = true
async-trait.workspace = true
chrono.workspace = true
-deepseek-protocol = { path = "../protocol", version = "0.8.18" }
+deepseek-protocol = { path = "../protocol", version = "0.8.19" }
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
diff --git a/crates/tools/Cargo.toml b/crates/tools/Cargo.toml
index eb8b7727..cbb8ebd6 100644
--- a/crates/tools/Cargo.toml
+++ b/crates/tools/Cargo.toml
@@ -9,7 +9,7 @@ description = "Tool invocation lifecycle, schema validation, and scheduler paral
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
-deepseek-protocol = { path = "../protocol", version = "0.8.18" }
+deepseek-protocol = { path = "../protocol", version = "0.8.19" }
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
diff --git a/crates/tui/Cargo.toml b/crates/tui/Cargo.toml
index b123b09a..77da1c5f 100644
--- a/crates/tui/Cargo.toml
+++ b/crates/tui/Cargo.toml
@@ -21,8 +21,8 @@ path = "src/main.rs"
[dependencies]
anyhow = "1.0.100"
arboard = "3.4"
-deepseek-secrets = { path = "../secrets", version = "0.8.18" }
-deepseek-tools = { path = "../tools", version = "0.8.18" }
+deepseek-secrets = { path = "../secrets", version = "0.8.19" }
+deepseek-tools = { path = "../tools", version = "0.8.19" }
schemaui = { version = "0.12.0", default-features = false, optional = true }
async-stream = "0.3.6"
async-trait = "0.1"
diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs
index ed80f566..12dad28e 100644
--- a/crates/tui/src/config.rs
+++ b/crates/tui/src/config.rs
@@ -42,9 +42,13 @@ pub const DEFAULT_VLLM_FLASH_MODEL: &str = "deepseek-ai/DeepSeek-V4-Flash";
pub const DEFAULT_VLLM_BASE_URL: &str = "http://localhost:8000/v1";
pub const DEFAULT_OLLAMA_MODEL: &str = "deepseek-coder:1.3b";
pub const DEFAULT_OLLAMA_BASE_URL: &str = "http://localhost:11434/v1";
-/// Official DeepSeek API host per (`deepseek-cn` preset defaults here).
-/// Legacy typo hostname `api.deepseeki.com` remains recognized in URL heuristics for backward compatibility.
-pub const DEFAULT_DEEPSEEKCN_BASE_URL: &str = "https://api.deepseek.com";
+/// Legacy `deepseek-cn` provider alias.
+///
+/// DeepSeek's official API host is the same worldwide. Keep this alias for
+/// old configs, but route it through the normal beta-enabled DeepSeek default.
+/// Legacy typo hostname `api.deepseeki.com` remains recognized in URL
+/// heuristics for backward compatibility.
+pub const DEFAULT_DEEPSEEKCN_BASE_URL: &str = DEFAULT_DEEPSEEK_BASE_URL;
const API_KEYRING_SENTINEL: &str = "__KEYRING__";
pub const COMMON_DEEPSEEK_MODELS: &[&str] = &[
"deepseek-v4-pro",
@@ -111,7 +115,7 @@ impl ApiProvider {
pub fn display_name(self) -> &'static str {
match self {
Self::Deepseek => "DeepSeek",
- Self::DeepseekCN => "DeepSeek (中国)",
+ Self::DeepseekCN => "DeepSeek (legacy alias)",
Self::NvidiaNim => "NVIDIA NIM",
Self::Openai => "OpenAI-compatible",
Self::Openrouter => "OpenRouter",
@@ -128,7 +132,6 @@ impl ApiProvider {
pub fn all() -> &'static [Self] {
&[
Self::Deepseek,
- Self::DeepseekCN,
Self::NvidiaNim,
Self::Openai,
Self::Openrouter,
diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs
index 47571c75..d6b3f7aa 100644
--- a/crates/tui/src/main.rs
+++ b/crates/tui/src/main.rs
@@ -2458,7 +2458,7 @@ fn doctor_timeout_recovery_lines(config: &Config) -> Vec {
&& !target.base_url.contains("api.deepseeki.com") =>
{
lines.push(
- "If you are in mainland China, set `provider = \"deepseek-cn\"` or `base_url = \"https://api.deepseek.com\"` in ~/.deepseek/config.toml, then rerun `deepseek doctor`."
+ "If this is a custom DeepSeek-compatible endpoint, set its HTTPS base URL in ~/.deepseek/config.toml and rerun `deepseek doctor`."
.to_string(),
);
}
@@ -4419,7 +4419,7 @@ mod doctor_endpoint_tests {
}
#[test]
- fn doctor_api_target_reports_deepseek_cn_endpoint() {
+ fn doctor_api_target_routes_deepseek_cn_alias_to_beta_endpoint() {
let config = Config {
provider: Some("deepseek-cn".to_string()),
..Default::default()
@@ -4429,6 +4429,7 @@ mod doctor_endpoint_tests {
assert_eq!(target.provider, "deepseek-cn");
assert_eq!(target.base_url, crate::config::DEFAULT_DEEPSEEKCN_BASE_URL);
+ assert_eq!(target.base_url, crate::config::DEFAULT_DEEPSEEK_BASE_URL);
assert_eq!(target.model, crate::config::DEFAULT_TEXT_MODEL);
}
@@ -4479,7 +4480,7 @@ mod doctor_endpoint_tests {
}
#[test]
- fn strict_tool_mode_doctor_warns_for_deepseek_cn_default_endpoint() {
+ fn strict_tool_mode_doctor_accepts_deepseek_cn_alias_default_endpoint() {
let config = Config {
provider: Some("deepseek-cn".to_string()),
strict_tool_mode: Some(true),
@@ -4488,12 +4489,10 @@ mod doctor_endpoint_tests {
let status = doctor_strict_tool_mode_status(&config);
- assert_eq!(status.status, "fallback_non_beta");
- assert!(!status.function_strict_sent);
- assert_eq!(
- status.recommended_base_url.as_deref(),
- Some(crate::config::DEFAULT_DEEPSEEK_BASE_URL)
- );
+ assert_eq!(status.status, "ready");
+ assert!(status.function_strict_sent);
+ assert!(status.message.contains("beta endpoint"));
+ assert!(status.recommended_base_url.is_none());
}
#[test]
@@ -4547,13 +4546,14 @@ mod doctor_endpoint_tests {
}
#[test]
- fn timeout_recovery_points_global_deepseek_users_to_cn_endpoint() {
+ fn timeout_recovery_keeps_default_deepseek_users_on_default_endpoint() {
let config = Config::default();
let text = doctor_timeout_recovery_lines(&config).join("\n");
assert!(text.contains("api.deepseek.com"));
- assert!(text.contains("provider = \"deepseek-cn\""));
+ assert!(text.contains("custom DeepSeek-compatible endpoint"));
+ assert!(!text.contains("provider = \"deepseek-cn\""));
assert!(text.contains("deepseek doctor --json"));
}
diff --git a/crates/tui/src/prompts.rs b/crates/tui/src/prompts.rs
index cbffb765..4441018f 100644
--- a/crates/tui/src/prompts.rs
+++ b/crates/tui/src/prompts.rs
@@ -618,8 +618,8 @@ mod tests {
fn package_version_is_current_hotfix_release() {
assert_eq!(
env!("CARGO_PKG_VERSION"),
- "0.8.18",
- "0.8.18 release branch must report the release version before publishing"
+ "0.8.19",
+ "0.8.19 release branch must report the release version before publishing"
);
}
diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs
index d14a039c..9301774c 100644
--- a/crates/tui/src/tui/app.rs
+++ b/crates/tui/src/tui/app.rs
@@ -38,7 +38,6 @@ use crate::tui::selection::TranscriptSelection;
use crate::tui::streaming::StreamingState;
use crate::tui::transcript::TranscriptViewCache;
use crate::tui::views::ViewStack;
-use crate::utils::is_chinese_system_locale;
// === Types ===
@@ -1120,19 +1119,7 @@ impl App {
initial_input,
} = options;
- // If no provider is explicitly configured AND the system locale
- // indicates Chinese (zh-*), suggest DeepseekCN (official api.deepseek.com preset)
- // as the appropriate default.
- let provider = if config.provider.is_none() && is_chinese_system_locale() {
- let cn_base_url = crate::config::DEFAULT_DEEPSEEKCN_BASE_URL.to_string();
- // Store the suggested base URL in config so the first API call
- // uses the CN endpoint. We mutate a clone to avoid writing.
- let mut config = config.clone();
- config.base_url = Some(cn_base_url);
- config.api_provider()
- } else {
- config.api_provider()
- };
+ let provider = config.api_provider();
// Check if API key exists
let needs_api_key = !has_api_key(config);
diff --git a/crates/tui/src/tui/provider_picker.rs b/crates/tui/src/tui/provider_picker.rs
index 2d147960..ebfa5600 100644
--- a/crates/tui/src/tui/provider_picker.rs
+++ b/crates/tui/src/tui/provider_picker.rs
@@ -391,7 +391,6 @@ mod tests {
names,
vec![
"DeepSeek",
- "DeepSeek (中国)",
"NVIDIA NIM",
"OpenAI-compatible",
"OpenRouter",
diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs
index 74b6ba09..874b1354 100644
--- a/crates/tui/src/tui/ui.rs
+++ b/crates/tui/src/tui/ui.rs
@@ -9,9 +9,10 @@ use std::time::{Duration, Instant};
use anyhow::Result;
use crossterm::{
event::{
- self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
- Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent,
- MouseEventKind, PopKeyboardEnhancementFlags,
+ self, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
+ EnableFocusChange, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind,
+ KeyModifiers, KeyboardEnhancementFlags, MouseButton, MouseEvent, MouseEventKind,
+ PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
@@ -209,6 +210,10 @@ pub async fn run_tui(config: &Config, options: TuiOptions) -> Result<()> {
if use_bracketed_paste {
execute!(stdout, EnableBracketedPaste)?;
}
+ // Enable focus events so the terminal reports FocusGained/FocusLost.
+ // Necessary for IME compositor re-activation on macOS when the user
+ // switches away (Cmd+Tab) and returns.
+ execute!(stdout, EnableFocusChange)?;
// #442: opt into the Kitty keyboard protocol's escape-code
// disambiguation so terminals that support it (Kitty, Ghostty,
// Alacritty 0.13+, WezTerm, recent Konsole, recent xterm) report
@@ -222,18 +227,7 @@ pub async fn run_tui(config: &Config, options: TuiOptions) -> Result<()> {
// release events that the existing key handlers would mis-route
// as duplicate presses. Best-effort: failure to push is logged
// and ignored so a quirky terminal can't block startup.
- if let Err(err) = execute!(
- stdout,
- crossterm::event::PushKeyboardEnhancementFlags(
- crossterm::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
- )
- ) {
- tracing::debug!(
- target: "kitty_keyboard",
- ?err,
- "PushKeyboardEnhancementFlags ignored (terminal lacks support)"
- );
- }
+ push_keyboard_enhancement_flags(&mut stdout);
let color_depth = palette::ColorDepth::detect();
let palette_mode = palette::PaletteMode::detect();
tracing::debug!(
@@ -243,7 +237,7 @@ pub async fn run_tui(config: &Config, options: TuiOptions) -> Result<()> {
);
let backend = ColorCompatBackend::new(stdout, color_depth, palette_mode);
let mut terminal = Terminal::new(backend)?;
- terminal.clear()?;
+ reset_terminal_viewport(&mut terminal)?;
let event_broker = EventBroker::new();
// Local mutable copy so runtime config flips (e.g. `/provider` switch)
@@ -419,6 +413,7 @@ pub async fn run_tui(config: &Config, options: TuiOptions) -> Result<()> {
persistence_actor::persist(PersistRequest::Shutdown);
let _ = execute!(terminal.backend_mut(), PopKeyboardEnhancementFlags);
+ execute!(terminal.backend_mut(), DisableFocusChange)?;
disable_raw_mode()?;
if use_alt_screen {
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
@@ -1583,6 +1578,21 @@ async fn run_event_loop(
continue;
}
+ // Re-push keyboard enhancement flags on focus-gain and force a
+ // full viewport reset before repainting. App-switching and
+ // interactive handoffs can leave the host terminal scrolled away
+ // from row 0; treating focus as a recapture point prevents the
+ // native scrollback gutter / blank-top-row failure mode from
+ // persisting after the user returns.
+ // On macOS, switching away (Cmd+Tab) and back can reset the
+ // terminal's keyboard mode, which breaks IME compositor state.
+ // Acknowledging FocusGained and re-pushing the flags restores
+ // the IME so CJK input methods work after a focus toggle.
+ if terminal_event_needs_viewport_recapture(&evt) {
+ push_keyboard_enhancement_flags(terminal.backend_mut());
+ force_terminal_repaint = true;
+ app.needs_redraw = true;
+ }
if let Event::Resize(width, height) = evt {
tracing::debug!(
width,
@@ -6196,6 +6206,7 @@ fn pause_terminal(
// mode. Best-effort — terminals that didn't accept the flags
// silently ignore the pop. Matches the shutdown and panic paths.
let _ = execute!(terminal.backend_mut(), PopKeyboardEnhancementFlags);
+ execute!(terminal.backend_mut(), DisableFocusChange)?;
disable_raw_mode()?;
if use_alt_screen {
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
@@ -6225,6 +6236,8 @@ fn resume_terminal(
if use_bracketed_paste {
execute!(terminal.backend_mut(), EnableBracketedPaste)?;
}
+ execute!(terminal.backend_mut(), EnableFocusChange)?;
+ push_keyboard_enhancement_flags(terminal.backend_mut());
reset_terminal_viewport(terminal)?;
Ok(())
}
@@ -6240,6 +6253,23 @@ fn reset_terminal_viewport(terminal: &mut AppTerminal) -> Result<()> {
Ok(())
}
+fn push_keyboard_enhancement_flags(writer: &mut W) {
+ if let Err(err) = execute!(
+ writer,
+ PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)
+ ) {
+ tracing::debug!(
+ target: "kitty_keyboard",
+ ?err,
+ "PushKeyboardEnhancementFlags ignored (terminal lacks support)"
+ );
+ }
+}
+
+fn terminal_event_needs_viewport_recapture(evt: &Event) -> bool {
+ matches!(evt, Event::FocusGained)
+}
+
fn status_color(level: StatusToastLevel) -> ratatui::style::Color {
match level {
StatusToastLevel::Info => palette::DEEPSEEK_SKY,
diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs
index b9fa29e1..d681452c 100644
--- a/crates/tui/src/tui/ui/tests.rs
+++ b/crates/tui/src/tui/ui/tests.rs
@@ -33,6 +33,24 @@ fn format_resume_hint_omits_missing_session_id() {
assert_eq!(format_resume_hint(Some(" ")), None);
}
+#[test]
+fn focus_gained_forces_terminal_viewport_recapture() {
+ assert!(terminal_event_needs_viewport_recapture(&Event::FocusGained));
+ assert!(!terminal_event_needs_viewport_recapture(&Event::FocusLost));
+}
+
+#[test]
+fn terminal_origin_reset_resets_scroll_region_origin_and_clears() {
+ assert!(
+ TERMINAL_ORIGIN_RESET.starts_with(b"\x1b[r\x1b[?6l"),
+ "must reset scroll margins and origin mode before repaint"
+ );
+ assert!(
+ TERMINAL_ORIGIN_RESET.ends_with(b"\x1b[H\x1b[2J"),
+ "must home the cursor and clear the viewport"
+ );
+}
+
#[test]
fn composer_newline_shortcuts_do_not_steal_ctrl_enter() {
assert!(is_composer_newline_key(KeyEvent::new(
diff --git a/crates/tui/src/utils.rs b/crates/tui/src/utils.rs
index cd80ebd8..6b769ab7 100644
--- a/crates/tui/src/utils.rs
+++ b/crates/tui/src/utils.rs
@@ -402,24 +402,6 @@ pub fn display_path_with_home(path: &Path, home: Option<&Path>) -> String {
path.display().to_string()
}
-/// Check whether the system locale is Chinese (zh-*).
-///
-/// Reads `LC_ALL`, `LC_MESSAGES`, and `LANG` environment variables.
-/// Used by the first-run flow to suggest `DeepseekCN` as the default
-/// provider for users in China.
-#[must_use]
-pub fn is_chinese_system_locale() -> bool {
- for key in ["LC_ALL", "LC_MESSAGES", "LANG"] {
- if let Ok(value) = std::env::var(key) {
- let normalized = value.split('.').next().unwrap_or(&value).replace('_', "-");
- if normalized.to_ascii_lowercase().starts_with("zh") {
- return true;
- }
- }
- }
- false
-}
-
/// Estimate the total character count across message content blocks.
#[must_use]
pub fn estimate_message_chars(messages: &[Message]) -> usize {
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
index 46ca872a..8cf39388 100644
--- a/docs/CONFIGURATION.md
+++ b/docs/CONFIGURATION.md
@@ -155,7 +155,7 @@ fallbacks after saved config and keyring credentials:
- `DEEPSEEK_API_KEY`
- `DEEPSEEK_BASE_URL`
- `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs)
-- `DEEPSEEK_PROVIDER` (`deepseek|deepseek-cn|nvidia-nim|openai|openrouter|novita|fireworks|sglang|vllm|ollama`)
+- `DEEPSEEK_PROVIDER` (`deepseek|nvidia-nim|openai|openrouter|novita|fireworks|sglang|vllm|ollama`)
- `DEEPSEEK_MODEL` or `DEEPSEEK_DEFAULT_TEXT_MODEL`
- `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `300`, clamped to `1..=3600`)
- `NVIDIA_API_KEY` or `NVIDIA_NIM_API_KEY` (preferred when provider is `nvidia-nim`; falls back to `DEEPSEEK_API_KEY`)
@@ -356,9 +356,9 @@ If you are upgrading from older releases:
### Core keys (used by the TUI/engine)
-- `provider` (string, optional): `deepseek` (default), `deepseek-cn`, `nvidia-nim`, `openai`, `openrouter`, `novita`, `fireworks`, `sglang`, `vllm`, or `ollama`. `deepseek-cn` presets DeepSeek Platform for mainland China with the documented host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) (distinct from typo `api.deepseeki.com`, which older configs may still carry and the client accepts as a DeepSeek-compatible host); `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`.
+- `provider` (string, optional): `deepseek` (default), `nvidia-nim`, `openai`, `openrouter`, `novita`, `fireworks`, `sglang`, `vllm`, or `ollama`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`.
- `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and Ollama can omit it.
-- `base_url` (string, optional): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API in v0.8.16, `https://api.deepseek.com` for `provider = "deepseek-cn"`, `https://api.openai.com/v1` for `provider = "openai"`, or the provider-specific endpoint for hosted/self-hosted providers. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.
+- `base_url` (string, optional): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = "deepseek-cn"` configs, `https://api.openai.com/v1` for `provider = "openai"`, or the provider-specific endpoint for hosted/self-hosted providers. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.
- `default_text_model` (string, optional): defaults to `deepseek-v4-pro` for DeepSeek, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `gpt-4.1` for generic OpenAI-compatible endpoints, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, and `deepseek-coder:1.3b` for Ollama. Current public DeepSeek IDs are `deepseek-v4-pro` and `deepseek-v4-flash`, both with 1M context windows, 384K max output, and thinking mode enabled by default. Legacy `deepseek-chat` and `deepseek-reasoner` remain compatibility aliases for `deepseek-v4-flash` until July 24, 2026. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. Generic `openai` and Ollama model IDs are passed through unchanged. OpenRouter provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `deepseek models` to discover live IDs from your configured endpoint. `DEEPSEEK_MODEL` overrides this for a single process.
- `reasoning_effort` (string, optional): `off`, `low`, `medium`, `high`, or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.
- `allow_shell` (bool, optional): defaults to `true` (sandboxed).
diff --git a/docs/DOCKER.md b/docs/DOCKER.md
index 5f51d225..1a23c7b2 100644
--- a/docs/DOCKER.md
+++ b/docs/DOCKER.md
@@ -24,7 +24,7 @@ Use a pinned release tag for reproducible installs:
docker run --rm -it \
-e DEEPSEEK_API_KEY="$DEEPSEEK_API_KEY" \
-v ~/.deepseek:/home/deepseek/.deepseek \
- ghcr.io/hmbown/deepseek-tui:v0.8.18
+ ghcr.io/hmbown/deepseek-tui:v0.8.19
```
## Local build
diff --git a/npm/deepseek-tui/package.json b/npm/deepseek-tui/package.json
index 843abd19..e8451647 100644
--- a/npm/deepseek-tui/package.json
+++ b/npm/deepseek-tui/package.json
@@ -1,7 +1,7 @@
{
"name": "deepseek-tui",
- "version": "0.8.18",
- "deepseekBinaryVersion": "0.8.18",
+ "version": "0.8.19",
+ "deepseekBinaryVersion": "0.8.19",
"description": "Install and run deepseek and deepseek-tui binaries from GitHub release artifacts.",
"author": "Hmbown",
"license": "MIT",
diff --git a/npm/deepseek-tui/test/install.test.js b/npm/deepseek-tui/test/install.test.js
index 4cc87d36..3753f015 100644
--- a/npm/deepseek-tui/test/install.test.js
+++ b/npm/deepseek-tui/test/install.test.js
@@ -29,7 +29,7 @@ test("install failure hint explains release base override for blocked GitHub dow
try {
const error = Object.assign(
new Error(
- "fetch https://github.com/Hmbown/DeepSeek-TUI/releases/download/v0.8.18/deepseek-artifacts-sha256.txt failed after 5 attempts:\ngetaddrinfo ENOTFOUND github.com",
+ "fetch https://github.com/Hmbown/DeepSeek-TUI/releases/download/v0.8.19/deepseek-artifacts-sha256.txt failed after 5 attempts:\ngetaddrinfo ENOTFOUND github.com",
),
{ code: "ENOTFOUND" },
);