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" }, );