From a4637fe7d1c1119ed5ef5ddfd024f7e920653ef1 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Tue, 12 May 2026 23:39:44 -0500 Subject: [PATCH] fix(settings): reduce motion in VTE flicker terminals Harvested from PR #1527 by @axobase001. Co-authored-by: axobase001 --- README.md | 1 + config.example.toml | 5 ++- crates/tui/src/settings.rs | 70 +++++++++++++++++++++++++++++++++++--- crates/tui/src/tui/ui.rs | 7 +++- docs/ACCESSIBILITY.md | 9 ++++- docs/CONFIGURATION.md | 29 ++++++++++++++++ 6 files changed, 113 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 483cc7ac..2d68128b 100644 --- a/README.md +++ b/README.md @@ -392,6 +392,7 @@ Key environment variables: | `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 | +| `DEEPSEEK_ALLOW_INSECURE_HTTP=1` | Allow non-local `http://` API base URLs on trusted networks | | `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 | | `OPENAI_BASE_URL` / `OPENAI_MODEL` | Generic OpenAI-compatible endpoint and model ID | | `SGLANG_BASE_URL` | Self-hosted SGLang endpoint | diff --git a/config.example.toml b/config.example.toml index ccd89ac8..c2ce1e55 100644 --- a/config.example.toml +++ b/config.example.toml @@ -178,7 +178,10 @@ max_subagents = 10 # optional (1-20) # base_url = "https://integrate.api.nvidia.com/v1" # model = "deepseek-ai/deepseek-v4-pro" # or deepseek-ai/deepseek-v4-flash -# Generic OpenAI-compatible endpoint +# Generic OpenAI-compatible endpoint. Use the built-in `openai` provider for +# third-party gateways; do not invent a custom provider name. For non-local +# http:// gateways, launch with DEEPSEEK_ALLOW_INSECURE_HTTP=1 only on a +# trusted network. [providers.openai] # api_key = "YOUR_OPENAI_COMPATIBLE_API_KEY" # base_url = "https://api.openai.com/v1" diff --git a/crates/tui/src/settings.rs b/crates/tui/src/settings.rs index 0cb0c6af..e36e1327 100644 --- a/crates/tui/src/settings.rs +++ b/crates/tui/src/settings.rs @@ -372,16 +372,18 @@ impl Settings { self.low_motion = true; self.fancy_animations = false; } - // VS Code (TERM_PROGRAM=vscode, #1356) and Ghostty (TERM_PROGRAM=ghostty, - // #1445) both produce visible flicker at 120 FPS: VS Code's compositor - // cannot keep pace; Ghostty's GPU compositor flash-renders each full-screen - // repaint. Drop to the 30 FPS low-motion cap for both automatically. + // VS Code (TERM_PROGRAM=vscode, #1356), Ghostty (TERM_PROGRAM=ghostty, + // #1445), and a few VTE terminals (#1470) produce visible flicker at + // 120 FPS. Drop to the 30 FPS low-motion cap for them automatically. // Like NO_ANIMATIONS above, this unconditionally overrides any // disk-loaded value — consistent precedence: env signals always win. + let vte_env_forces_low_motion = std::env::var_os("TILIX_ID").is_some_and(|v| !v.is_empty()) + || std::env::var_os("TERMINATOR_UUID").is_some_and(|v| !v.is_empty()); if matches!( std::env::var("TERM_PROGRAM").as_deref(), Ok("vscode") | Ok("ghostty") - ) { + ) || vte_env_forces_low_motion + { self.low_motion = true; self.fancy_animations = false; } @@ -1241,6 +1243,8 @@ mod tests { let prev = std::env::var_os("TERM_PROGRAM"); let prev_ssh_client = std::env::var_os("SSH_CLIENT"); let prev_ssh_tty = std::env::var_os("SSH_TTY"); + let prev_tilix_id = std::env::var_os("TILIX_ID"); + let prev_terminator_uuid = std::env::var_os("TERMINATOR_UUID"); // SAFETY: serialised by the guard. Clear SSH_* so a real // SSH session running the test suite doesn't make this // assertion trivially fail — the SSH path is exercised @@ -1248,6 +1252,8 @@ mod tests { unsafe { std::env::remove_var("SSH_CLIENT"); std::env::remove_var("SSH_TTY"); + std::env::remove_var("TILIX_ID"); + std::env::remove_var("TERMINATOR_UUID"); } for program in ["iTerm.app", "Apple_Terminal", "WezTerm", "xterm-256color"] { // SAFETY: serialised by the guard. @@ -1273,6 +1279,60 @@ mod tests { if let Some(v) = prev_ssh_tty { std::env::set_var("SSH_TTY", v); } + if let Some(v) = prev_tilix_id { + std::env::set_var("TILIX_ID", v); + } + if let Some(v) = prev_terminator_uuid { + std::env::set_var("TERMINATOR_UUID", v); + } + } + } + + #[test] + fn tilix_and_terminator_env_force_low_motion_on() { + let _g = term_program_test_guard(); + let prev_term_program = std::env::var_os("TERM_PROGRAM"); + let prev_tilix_id = std::env::var_os("TILIX_ID"); + let prev_terminator_uuid = std::env::var_os("TERMINATOR_UUID"); + + for (var, val) in [ + ("TILIX_ID", "d5b5b5d6-tilix-session"), + ("TERMINATOR_UUID", "urn:uuid:terminator-session"), + ] { + // SAFETY: serialised by the guard. + unsafe { + std::env::remove_var("TERM_PROGRAM"); + std::env::remove_var("TILIX_ID"); + std::env::remove_var("TERMINATOR_UUID"); + std::env::set_var(var, val); + } + let mut settings = Settings::default(); + assert!(!settings.low_motion, "default is animated"); + settings.apply_env_overrides(); + assert!( + settings.low_motion, + "{var} must enable low_motion to prevent VTE flicker (#1470)" + ); + assert!( + !settings.fancy_animations, + "{var} must disable fancy_animations" + ); + } + + // SAFETY: cleanup under the guard. + unsafe { + match prev_term_program { + Some(v) => std::env::set_var("TERM_PROGRAM", v), + None => std::env::remove_var("TERM_PROGRAM"), + } + match prev_tilix_id { + Some(v) => std::env::set_var("TILIX_ID", v), + None => std::env::remove_var("TILIX_ID"), + } + match prev_terminator_uuid { + Some(v) => std::env::set_var("TERMINATOR_UUID", v), + None => std::env::remove_var("TERMINATOR_UUID"), + } } } diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 764c5fd1..23047b63 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -6142,6 +6142,11 @@ fn render(f: &mut Frame, app: &mut App) { crate::config::ApiProvider::Vllm => Some("vLLM"), crate::config::ApiProvider::Ollama => Some("Ollama"), }; + let status_indicator_started_at = if app.low_motion { + None + } else { + app.turn_started_at + }; let header_data = HeaderData::new( app.mode, &model_label, @@ -6158,7 +6163,7 @@ fn render(f: &mut Frame, app: &mut App) { .with_reasoning_effort(Some(&effort_label)) .with_provider(provider_label) .with_status_indicator(crate::tui::widgets::header_status_indicator_frame( - app.turn_started_at, + status_indicator_started_at, &app.status_indicator, )); let header_widget = HeaderWidget::new(header_data); diff --git a/docs/ACCESSIBILITY.md b/docs/ACCESSIBILITY.md index 5fb89f73..3ac5fb68 100644 --- a/docs/ACCESSIBILITY.md +++ b/docs/ACCESSIBILITY.md @@ -10,8 +10,9 @@ visual motion and density for screen-reader and low-motion users. | Toggle | Default | Effect | | --- | --- | --- | | `NO_ANIMATIONS=1` env var | unset | At startup, forces `low_motion = true` and `fancy_animations = false`. Overrides whatever's saved in `settings.toml`. | -| `low_motion` setting | `false` | Suppresses spinners' motion, transcript fade-ins, footer drift, and the active-cell pulse. The frame-rate limiter also slows down idle redraws so the cursor doesn't blink as aggressively. | +| `low_motion` setting | `false` | Suppresses spinners' motion, transcript fade-ins, footer drift, the header status-indicator cycle, and the active-cell pulse. The frame-rate limiter also slows down idle redraws so the cursor doesn't blink as aggressively. | | `fancy_animations` setting | `false` | Footer water-spout strip and pulsing sub-agent counter. Off by default. | +| `status_indicator` setting | `whale` | Header status chip. Set to `dots` for the compact dot cycle or `off` to hide it. | | `calm_mode` setting | `false` | Collapses tool-output details by default and trims status messages. Useful for screen readers that announce every redraw. | | `show_thinking` setting | `true` | Set to `false` to hide model `reasoning_content` blocks entirely. | | `show_tool_details` setting | `true` | Set to `false` to render tool calls as one-liners without expanded payloads. | @@ -43,11 +44,17 @@ The same toggles are reachable from the command palette: * `/settings set low_motion on` * `/settings set fancy_animations off` * `/settings set calm_mode on` +* `/settings set status_indicator off` Settings written this way persist to `~/.config/deepseek/settings.toml`. The `NO_ANIMATIONS` env var still wins at startup if it's set, so unsetting the env var is the way to honor your saved choice. +Tilix and Terminator sessions automatically start in low-motion mode because +those VTE-based terminals have reported visible redraw flicker during active +turns. You can still override the saved settings after launch if your terminal +version renders cleanly. + ## Notes for screen-reader users * `low_motion` slows the idle redraw loop to ~120ms per frame so diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 961482aa..95e99c45 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -82,6 +82,35 @@ URLs (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) do not read the secret store unless API-key auth is explicitly requested; use an env var or config-file key when a local server does require bearer auth. +### Custom OpenAI-Compatible Gateways + +For a third-party service that implements the OpenAI Chat Completions API, use +the built-in `openai` provider name and point its provider table at the gateway: + +```toml +provider = "openai" +default_text_model = "your-model-id" + +[providers.openai] +api_key = "YOUR_OPENAI_COMPATIBLE_API_KEY" +base_url = "https://your-gateway.example/v1" +``` + +Do not invent a custom provider name; `provider` must be one of the known +providers listed above. Put the endpoint under `[providers.openai]`, not the +legacy top-level `base_url`, so the OpenAI-compatible provider receives it. +`default_text_model` is the model ID sent to the gateway; if you keep several +provider tables in one config, `[providers.openai].model` can be used as the +OpenAI-provider-specific override. + +Local HTTP endpoints such as Ollama, SGLang, and vLLM are allowed by default +when they use localhost or loopback addresses. For a non-local `http://` +gateway, launch with `DEEPSEEK_ALLOW_INSECURE_HTTP=1` only on a trusted network: + +```bash +DEEPSEEK_ALLOW_INSECURE_HTTP=1 deepseek +``` + Third-party OpenAI-compatible gateways that need extra request headers can set `http_headers = { "X-Model-Provider-Id" = "your-model-provider" }` at the top level or under a provider table such as `[providers.deepseek]`. When configured,