v0.8.19: endpoint, release workflow, IME + viewport fixes (#1128)

* fix(config): keep DeepSeek beta endpoint for legacy cn alias

* fix(ci): filter download-artifact to deepseek* pattern

Prevents the release aggregation job from picking up non-binary
artifacts (e.g. Docker .dockerbuild cache layers) that cause the
checksum manifest to include spurious entries and the Release to
carry files it shouldn't.

* fix(tui): enable focus events to restore IME after app-switch

On macOS, switching away (Cmd+Tab) and back suspends the IME compositor.
Without focus-event handling, the TUI never signals readiness to the
terminal, so CJK input methods (Pinyin, Zhuyin, etc.) stop working.

- EnableFocusChange on startup so the terminal reports FocusGained/FocusLost
- Re-push KeyboardEnhancementFlags on FocusGained (some terminals reset
  the enhanced keyboard mode on focus-loss)
- DisableFocusChange on shutdown for clean terminal handoff

* chore: cargo fmt

* docs: add DataWhale and DeepSeek to acknowledgments

* docs: fix DeepSeek name etymology in acknowledgments

* fix(tui): recapture viewport on focus restore

* docs: thank DeepSeek and DataWhale bilingually
This commit is contained in:
Hunter Bown
2026-05-07 23:05:39 -05:00
committed by GitHub
parent 3521f1af91
commit b31b93aaae
26 changed files with 167 additions and 124 deletions
+1
View File
@@ -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
+13
View File
@@ -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
Generated
+14 -14
View File
@@ -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"
+1 -1
View File
@@ -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
+14 -4
View File
@@ -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)
---
+1 -1
View File
@@ -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
+9 -9
View File
@@ -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
+7 -7
View File
@@ -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
+1 -1
View File
@@ -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
+8 -8
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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"
+8 -5
View File
@@ -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 <https://api-docs.deepseek.com/> (`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,
+11 -11
View File
@@ -2458,7 +2458,7 @@ fn doctor_timeout_recovery_lines(config: &Config) -> Vec<String> {
&& !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"));
}
+2 -2
View File
@@ -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"
);
}
+1 -14
View File
@@ -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);
-1
View File
@@ -391,7 +391,6 @@ mod tests {
names,
vec![
"DeepSeek",
"DeepSeek (中国)",
"NVIDIA NIM",
"OpenAI-compatible",
"OpenRouter",
+46 -16
View File
@@ -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<W: Write>(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,
+18
View File
@@ -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(
-18
View File
@@ -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 {
+3 -3
View File
@@ -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).
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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" },
);