chore(release): prepare v0.8.27 - #1375
Conversation
Replace the separate /agent, /plan, and /yolo commands with a single /mode command that can either open a picker or switch directly by name or number. This keeps mode switching in one command surface and avoids duplicating similar commands for each mode.
Add a dedicated /status command that reports the current runtime session state. The new report shows provider, model, workspace, mode, permissions, session, context usage, token telemetry, cache telemetry, cost, transcript counts, and rate-limit availability. /statusline remains available for footer configuration.
Add a /feedback command for opening project feedback links. The command shows a picker when run without arguments and supports direct bug, feature, and security targets. Bug and feature options open the matching GitHub issue templates, while security opens the repository security policy.
The CLI dispatcher accepted --yolo but only passed it to Exec(TuiPassthroughArgs), not to the plain Run(RunArgs) path used for interactive sessions. Fix: pass DEEPSEEK_YOLO=true env var to the TUI binary. The TUI already reads this env var (matching DEEPSEEK_SANDBOX_MODE pattern) and sets allow_shell + start_in_agent_mode + yolo. Also adds yolo field to CliRuntimeOverrides and ResolvedRuntimeOptions so the flag propagates through the full resolve chain.
…nals
Instead of unconditionally changing Up/Down behavior, gate the
empty-composer-scroll path behind a new `tui.composer_arrows_scroll`
config option (default false). Users whose terminals map trackpad
gestures to arrow keys can opt in via:
[tui]
composer_arrows_scroll = true
When enabled, empty-composer Up/Down scroll the transcript; otherwise
plain arrows always navigate input history (preserving #1117 default).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Promote COST_EQ_TOLERANCE from a function-local const to a module-level constant in sidebar.rs. Add SessionCostSnapshot::total_usd() and total_cny() helpers that encapsulate session+subagent cost summation, used during session restore. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> # Conflicts: # crates/tui/src/session_manager.rs # crates/tui/src/tui/sidebar.rs # crates/tui/src/tui/ui.rs
Pre-compute render caches to avoid re-parsing every frame: - Add output_summary: Option<String> to ExecCell and GenericToolCell - Add is_diff: bool to GenericToolCell (cached after first detection) - Populate caches once in handle_tool_call_complete / orphan path Live mode rendering simplified to one-line summary + expand affordance: - GenericToolCell and ExecCell now show a single muted summary line with "Enter to expand tool output" affordance in Live mode - Transcript mode still emits full output - render_tool_output_summary_line truncates to fit terminal width - Make output_looks_like_diff pub(crate) for pre-computation access Test plan: - cargo test -p deepseek-tui (2379 passed) - config_ui::build_document_reflects_app_state is a pre-existing failure Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wraps ExecCell and GenericToolCell rendered output with card-rail glyphs for visual structure, similar to Claude Code's card-style tool rendering. - wrap_card_rail(): adds ╭/│/╰ glyphs to rendered lines - Applied to ExecCell::render and GenericToolCell::lines_with_mode - Pre-computed caches (output_summary, is_diff) kept from previous commit for per-frame performance - Live mode output remains visible (head+tail+omitted), not collapsed - Card-rail glyphs reused from existing tool_card.rs CardRail enum Test plan: cargo test -p deepseek-tui (2380 passed, 0 failed) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Save provider choice to settings.default_provider on switch - Save model per-provider to settings.provider_models - On startup, load provider-specific model instead of global default - Hide DeepSeek models from picker on pass-through providers - Show friendly message for /models on unsupported providers
- Use CARGO_PKG_VERSION for User-Agent instead of hardcoded version - Restore default_model fallback for backward compatibility
Other providers (openai, ollama) should get their model from config.toml / env vars (e.g. OPENAI_MODEL), not from the global default_model setting which is DeepSeek-centric.
Previously OPENAI_MODEL only set default_text_model which was lower priority than the provider config model. Now it directly overrides the openai provider's model field.
The forced-repaint sequence written before each TurnComplete /
focus-gain / resize used to be:
\x1b[r\x1b[?6l\x1b[H\x1b[2J\x1b[3J
which combined with the immediately-following ratatui
`terminal.clear()` produced a double-clear. Terminals that don't
optimize successive clears against the alt-screen buffer (Ghostty,
VSCode integrated terminal, Win10 conhost) rendered the second
clear as a visible blank-then-repaint flicker on every redraw
trigger.
The lighter sequence `\x1b[r\x1b[?6l\x1b[H` resets DECSTBM and DECOM
and homes the cursor (still solving the original viewport-drift fix
that 0.8.22 added) but leaves the pixel-clear to ratatui's diff
renderer. The alt-screen buffer's double-buffering absorbs that
single clear without flicker on every terminal we tested. Terminals
that were already flicker-free (macOS Terminal.app, iTerm2,
alacritty) remain so.
Closes #1119, #1260, #1295, #1352, #1356, #1363, #1366.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rors
The user-facing error path already formatted the underlying anyhow
chain with `{err:#}`, but reqwest chains alone read as opaque
fragments ("error sending request: tcp connect: connection refused"
etc.) for users without low-level network experience.
`format_registry_error` now inspects the formatted chain for common
failure signatures and appends a one-line hint:
- DNS lookup / `getaddrinfo` failures
- connection refused / reset / aborted
- TLS handshake / certificate / SSL
- HTTP 404 / 401 / 403 / 429
- request timed out
Each hint points at the most likely cause (network reachability,
trust store, registry URL, rate limit) and a concrete next step.
The original chain is still rendered verbatim above the hint, so
power users keep their detail and casual users get a starting
point.
Closes #1329 (the diagnostic side; the actual root cause is now
diagnosable from the surfaced chain + hint).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pager intercepts mouse capture, so terminal-native selection is
disabled inside it. Until now there was no in-app way to copy the
content the user came specifically to see — high-frustration UX gap
for the Alt+V (tool details), Ctrl+O (thinking), shell-job, task,
MCP-manager, and selection pagers.
Both `c` (clipboard convention) and `y` (vim-yank convention) now
emit a `ViewEvent::CopyToClipboard` carrying the full pager body.
The host dispatcher in `ui.rs` writes through `app.clipboard` and
toasts a status confirmation ("Pager content copied" /
"Copy failed"). Empty-body pagers report the empty state instead of
silently no-op'ing.
Footer hint updated to surface the new keys:
j/k scroll Space page Ctrl+D/U half g/G top/bottom / search c copy q/Esc close
Mouse selection inside the pager remains intercepted (the alternative
— releasing capture inside the modal — would break vim-style
navigation), so this is the supported copy path.
Closes #1354.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both `render_line_with_links` (paragraphs, list items) and the standalone `wrap_text` (code blocks) were word-based wrappers: when a single word's display width exceeded the available column budget they placed the word alone on a line and let it overflow the right edge of the transcript silently. Long URLs, file paths, commit hashes, JWTs, and any no-whitespace CJK run all hit this in #1344 and #1351 reports. The fix mirrors the v0.8.25 table-cell fix (`wrap_cell_text`): extract the per-character width-aware splitter as a free helper `push_word_breaking_chars`, and call it from `wrap_text`, `wrap_cell_text`, and the new char-break branch in `render_line_with_links`. Each rendered line is now guaranteed to fit in the requested width; full content is preserved across the wrapped segments. Snapshot-style regression tests pin the invariant at widths 40, 60, 80, and 120 — covering 200-char `a`-runs, long URL fixtures, mixed-short+overlong-word fixtures, and the existing table-cell property. A regression guard also confirms short words still break on whitespace (no mid-word breaks for ordinary prose). Closes #1344 (output-side overflow). Partial fix for #1351 (the table-cell concern was already fixed in v0.8.25; the long-prompt input-area concern is a separate visible-window issue, not a wrap bug — the composer already uses a grapheme-based wrapper). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#1367) Plain Ctrl+C used to mean "cancel turn or arm exit" unconditionally, which fought the OS-wide copy convention on Windows: every time a user pressed Ctrl+C to copy a model response, they instead armed the exit prompt and lost their place. Ctrl+Shift+C and Cmd+C copied correctly but weren't discoverable. The handler is now a four-stage decision, factored into a `CtrlCDisposition` helper with a unit-tested priority table: 1. CopySelection — transcript selection active → copy + clear it (matches Windows / cross-platform Ctrl+C convention; #1337). 2. CancelTurn — turn in flight → cancel (unchanged). 3. ConfirmExit — quit-armed within the 2s window → exit. 4. ArmExit — idle, no selection → arm the "press Ctrl+C again" prompt for 2s (unchanged). A turn-in-flight beats a quit-arm even when both are true, so a Ctrl+C that lands while the user is mid-turn but had recently half-armed the exit prompt always cancels the turn rather than exiting. Pinned by `ctrl_c_disposition_loading_beats_armed_quit`. Cmd+C (macOS) and Ctrl+Shift+C continue to copy via `is_copy_shortcut` unchanged; only plain Ctrl+C now branches on selection state. For #1367, on TurnStarted the status-message slot now surfaces "Press Esc or Ctrl+C to cancel" if it's empty. Real transient messages still take precedence; the hint clears on the next status update. Closes the discoverability gap for users who didn't know how to interrupt a long-running task. Closes #1337, #1367. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pt2) v0.8.26 surfaced the spawn-error tail (`Stdio transport closed` → the underlying EACCES/sandbox-deny line). v0.8.27 closes the second half of the report: users no longer need to manually `/mcp reload` after editing `~/.deepseek/mcp.json`. `McpPool` gained three fields: a `config_source` path (set when the pool is built via `from_config_path`), a 64-bit content hash of the active config, and the most recently observed mtime of the source file. `reload_if_config_changed` does a cheap `stat` first; on mtime-equal it returns immediately. Only when the mtime has moved does the pool re-read the file, hash it, and compare to the stored hash — content-unchanged reloads (e.g. `touch` on a networked FS) are skipped. On a real content change the connections map is cleared so the next `get_or_connect` reattaches under the new config (sandbox flags, env, args, server set). `get_or_connect` now invokes `reload_if_config_changed` at entry and swallows its errors (a transient stat/parse failure can't take down the existing pool). Pools built via `McpPool::new` (tests, ad-hoc snapshots) are unaffected — they have no source path and short-circuit out. No file watcher, no long-lived task, no signature changes for the existing callers. Closes the part-2 follow-up on #1267. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new always-loaded tool spec `notify` that lets the model trigger a single desktop notification when a long-running task completes or genuinely needs the user's attention. Implementation delegates to the existing `tui::notifications` infrastructure, so the user's `[notifications].method` config drives delivery: - iTerm2 / Ghostty / WezTerm → OSC 9 (banner + sound) - macOS / Linux fallback → BEL - Windows → off by default; opt-in to BEL + MessageBeep - `method = "off"` → silent no-op (the tool still succeeds) Title and body are character-bounded (80 / 200 chars) and trim-checked, so a runaway model can't paint a paragraph into the terminal title bar or slice through a multi-byte sequence and emit invalid UTF-8. tmux passthrough is detected via `$TMUX` and OSC 9 is double-escaped so the outer terminal still receives it. The tool description steers the model away from chatter — only fire on real completion / attention beats, not as a "still alive" ping. Always-loaded (added to `should_default_defer_tool`'s allowlist) so the model sees it without a ToolSearch round-trip; auto-approval since the only side effect is a single terminal escape write. Closes #1322. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 17-community-PR opener was accurate for the first half of the cycle but undersells the user-issue work that landed afterwards (flicker, wrap, pager copy-out, Ctrl+C, MCP auto-reload, notify tool). Updated headline so the changelog matches the shipping release. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two small workspace-clippy gaps that snuck through the per-crate sweeps in this branch: * `crates/cli/src/lib.rs` — the OpenAI-provider passthrough test was building a `ResolvedRuntimeOptions` literal directly and missed the `yolo: Option<bool>` field that landed earlier on this branch in 665801b (`fix(cli): forward --yolo to TUI binary`). Set to `None` to match the test's non-yolo intent. * `crates/tui/src/mcp.rs` — the new `reload_if_config_changed` swap test was using `iter().any(|n| *n == "new")`, which is rust-1.94 clippy's `manual_contains` lint. Switched to `names.contains(&"new")`. `cargo clippy --workspace --all-targets --all-features --locked -- -D warnings` is now green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request updates the project to v0.8.27, introducing a suite of features and fixes such as a unified /mode command, a new /status diagnostic tool, a /feedback command, and a desktop notify tool. Critical bug fixes address terminal flickering, long-text wrapping, and the implementation of context-sensitive Ctrl+C shortcuts. Review feedback identifies an optimization opportunity for string previews, warns against blocking I/O in asynchronous functions within the MCP module, and notes the inclusion of substantial dead code and incomplete tests in the working set logic.
| fn preview_text(content: &str, max_chars: usize) -> String { | ||
| let mut preview: String = content.chars().take(max_chars).collect(); | ||
| if content.chars().count() > max_chars { | ||
| preview.push_str("..."); | ||
| } | ||
| preview | ||
| } |
There was a problem hiding this comment.
The preview_text function is inefficient for large strings because content.chars().count() iterates over the entire string ($O(N)$). Since you only need to know if there are more than max_chars, you can check the iterator directly after taking the required amount.
| fn preview_text(content: &str, max_chars: usize) -> String { | |
| let mut preview: String = content.chars().take(max_chars).collect(); | |
| if content.chars().count() > max_chars { | |
| preview.push_str("..."); | |
| } | |
| preview | |
| } | |
| fn preview_text(content: &str, max_chars: usize) -> String { | |
| let mut chars = content.chars(); | |
| let mut preview: String = chars.by_ref().take(max_chars).collect(); | |
| if chars.next().is_some() { | |
| preview.push_str("... "); | |
| } | |
| preview | |
| } |
| pub async fn reload_if_config_changed(&mut self) -> Result<bool> { | ||
| let Some(path) = self.config_source.clone() else { | ||
| return Ok(false); | ||
| }; | ||
| let current_mtime = match mcp_config_mtime(&path) { | ||
| Some(m) => m, | ||
| None => return Ok(false), | ||
| }; | ||
| if Some(current_mtime) == self.last_mtime { | ||
| return Ok(false); | ||
| } | ||
| // mtime moved — we owe a re-read. | ||
| let new_config: McpConfig = if path.exists() { | ||
| let contents = fs::read_to_string(&path) | ||
| .with_context(|| format!("Failed to re-read MCP config: {}", path.display()))?; | ||
| serde_json::from_str(&contents) | ||
| .with_context(|| format!("Failed to re-parse MCP config: {}", path.display()))? | ||
| } else { | ||
| McpConfig::default() | ||
| }; | ||
| let new_hash = hash_mcp_config(&new_config); | ||
| // Always advance last_mtime so a touched-but-unchanged file doesn't | ||
| // make us re-read on every subsequent call. | ||
| self.last_mtime = Some(current_mtime); | ||
| if new_hash == self.config_hash { | ||
| return Ok(false); | ||
| } | ||
| // Real content change — drop all live connections so the next | ||
| // get_or_connect picks up the new config (sandbox flags, env, args). | ||
| self.connections.clear(); | ||
| self.config = new_config; | ||
| self.config_hash = new_hash; | ||
| Ok(true) | ||
| } |
There was a problem hiding this comment.
The reload_if_config_changed function performs blocking I/O operations (mcp_config_mtime, fs::read_to_string, path.exists()) within an async context. This can block the async executor thread, especially if the filesystem is slow or networked. Consider using tokio::fs for these operations to maintain responsiveness.
| #[allow(dead_code)] | ||
| const LOCAL_REFERENCE_SCAN_LIMIT: usize = 4096; | ||
|
|
||
| #[allow(dead_code)] | ||
| #[allow(clippy::too_many_arguments)] | ||
| fn add_local_reference_completions( | ||
| root: &Path, | ||
| display_root: &Path, | ||
| needle: &str, | ||
| limit: usize, | ||
| prefix_hits: &mut Vec<String>, | ||
| substring_hits: &mut Vec<String>, | ||
| seen: &mut HashSet<PathBuf>, | ||
| ) { | ||
| if !should_try_local_reference_completion(needle) { | ||
| return; | ||
| } | ||
|
|
||
| for path in local_reference_paths(root, LOCAL_REFERENCE_SCAN_LIMIT) { | ||
| if prefix_hits.len() + substring_hits.len() >= limit { | ||
| break; | ||
| } | ||
| let Ok(rel) = path.strip_prefix(display_root) else { | ||
| continue; | ||
| }; | ||
| let rel_str = rel.to_string_lossy().replace('\\', "/"); | ||
| if rel_str.is_empty() || !seen.insert(path.clone()) { | ||
| continue; | ||
| } | ||
| let lower = rel_str.to_lowercase(); | ||
| if needle.is_empty() || lower.starts_with(needle) { | ||
| prefix_hits.push(rel_str); | ||
| } else if lower.contains(needle) { | ||
| substring_hits.push(rel_str); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[allow(dead_code)] | ||
| fn should_try_local_reference_completion(needle: &str) -> bool { | ||
| !needle.is_empty() && (needle.starts_with('.') || needle.contains('/') || needle.contains('\\')) | ||
| } | ||
|
|
||
| #[allow(dead_code)] | ||
| fn local_reference_paths(root: &Path, limit: usize) -> Vec<PathBuf> { | ||
| let mut out = Vec::new(); | ||
| let mut builder = WalkBuilder::new(root); | ||
| builder | ||
| .hidden(false) | ||
| .follow_links(false) | ||
| .max_depth(Some(COMPLETIONS_WALK_DEPTH)) | ||
| .git_ignore(false) | ||
| .git_global(false) | ||
| .git_exclude(false); | ||
| let _ = builder.add_custom_ignore_filename(".deepseekignore"); | ||
| builder.filter_entry(|entry| !should_skip_local_reference_dir(entry.path())); | ||
|
|
||
| for entry in builder.build().flatten() { | ||
| if out.len() >= limit { | ||
| break; | ||
| } | ||
| let path = entry.path(); | ||
| if path == root { | ||
| continue; | ||
| } | ||
| if entry | ||
| .file_type() | ||
| .is_some_and(|ft| ft.is_file() || ft.is_dir()) | ||
| { | ||
| out.push(path.to_path_buf()); | ||
| } | ||
| } | ||
| out | ||
| } | ||
|
|
||
| #[allow(dead_code)] | ||
| fn should_skip_local_reference_dir(path: &Path) -> bool { | ||
| let Some(name) = path.file_name().and_then(|name| name.to_str()) else { | ||
| return false; | ||
| }; | ||
| matches!( | ||
| name, | ||
| ".git" | ||
| | "target" | ||
| | "node_modules" | ||
| | ".venv" | ||
| | "venv" | ||
| | "env" | ||
| | "dist" | ||
| | "build" | ||
| | "__pycache__" | ||
| | ".ruff_cache" | ||
| ) | ||
| } | ||
|
|
There was a problem hiding this comment.
This block of code appears to be dead code as it is marked with #[allow(dead_code)] and is not called anywhere in the provided changes. Additionally, the associated tests are ignored with a message stating that wiring is incomplete. Including large amounts of unused code in a release PR should be avoided to maintain code clarity and reduce binary size.
Two paste-UX improvements that address recurring complaints: **1. Visible-before-submit consolidation.** v0.7.x added a 16 000-char safety cap that folded oversized inputs into `.deepseek/pastes/paste- …md` and swapped them for an `@`-mention so the model could read the full content via the normal mention-resolution path. The cap was checked inside `submit_input` only — meaning a user who pasted 50k chars and pressed Enter saw the file get created AND the message sent in the same frame, with no chance to review the @-mention beforehand. People reasonably read this as "the TUI auto-sent an @-mention I didn't authorise." Consolidation now also runs at the end of `insert_paste_text`, so the @-mention shows up in the composer (along with a "consolidated — sent as @mention" toast) the moment the paste lands. The submit-time path stays as a safety net for any other code path that fills the buffer above the cap, so the cap is still enforced exactly once. **2. Auto-disable paste-burst on verified bracketed paste.** The paste-burst heuristic (rapid-keystroke detection for terminals without bracketed paste) used to run unconditionally. On modern terminals (Ghostty / iTerm2 / WezTerm / Windows Terminal) bracketed paste is reliable, and paste-burst running alongside it created false positives — fast typing, IME commits, autocomplete bursts could all be mis-classified as a paste. The new `App::bracketed_paste_seen` flag flips to `true` the first time a real `Event::Paste` arrives; from that moment, `handle_paste_burst_ key` short-circuits. Terminals that never deliver bracketed paste (the original target audience) are unaffected — the heuristic still fires for them. Both changes have new unit tests: - `paste_consolidates_oversized_text_into_paste_file_visibly` - `paste_under_threshold_does_not_consolidate` - `paste_burst_short_circuits_after_bracketed_paste_observed` Existing `submit_input_consolidates_oversized_input_into_paste_file` still passes — it bypasses `insert_paste_text` and exercises the safety net. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cherry-picked from @reidliu41's PR #1342. Pasting `请联网搜索:\n…` (short non-ASCII first line + newline) used to fail the `decide_begin_buffer` heuristic — `grabbed.chars().any(is_whitespace)` is false on a 6-codepoint Chinese run, and `chars().count() >= 16` is false at 6 chars — so the trailing pasted newline fell through as a real Enter and submitted the first line on its own. The heuristic now also treats `!grabbed.is_ascii()` as paste-like, which captures the CJK case without false-firing on ASCII typing (plain ASCII typists still need either whitespace or 16+ chars to look like a paste). Includes the regression test from PR #1342, slightly reworded. Closes #1302. Thanks @reidliu41. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a user picked 简体中文 / 日本語 / Português (Brasil) on the
step-2 language picker, every subsequent onboarding screen used to
stay in English. The set_locale_from_onboarding path already
re-resolved `app.ui_locale`, but the hardcoded `Line::from(Span::
styled("Connect your DeepSeek API key", …))` strings in
`onboarding/api_key.rs`, `trust_directory.rs`, `language.rs`, and
the `tips_lines()` block in `onboarding/mod.rs` never consulted
the locale.
This commit:
- Adds 25 `MessageId` entries (`OnboardLanguageTitle`,
`OnboardApiKey*`, `OnboardTrust*`, `OnboardTips*`, …) covering
the title / body / hint / footer strings for each screen.
- Translates each into all four shipping locales (en / ja /
zh-Hans / pt-BR), with the same care the existing translation
surfaces use (no machine translation; idiomatic phrasing for
each locale).
- Threads the active locale through `language::lines`,
`api_key::lines`, `trust_directory::lines`, and `tips_lines`
via `app.tr(MessageId::…)`.
- Adds `api_key_screen_renders_in_selected_locale` regression
test pinning that the rendered lines actually contain the
translated strings for zh-Hans / ja / en.
Particularly noticeable for users on CJK input methods: picking
their language at step 2 now means the remaining setup runs in
that language rather than forcing IME juggling for English text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On top of v0.8.26's inter-row spacing for /skills (#1328 from @reidliu41), the list now also accepts an optional name-prefix argument so users with crowded skill folders can narrow the view without scrolling. /skills → full list (unchanged) /skills git → only skills whose name starts with "git" /skills GIT → same (case-insensitive) /skills nope → "No skills match prefix `nope` (out of 12 …)" /skills --remote → unchanged /skills sync → unchanged /skills --bogus → "Usage: …" error (rejected so future flags don't silently turn into no-match prefixes) The match-count header reflects both the matched count and the registry total, so the user can see at a glance how aggressive the filter is. Empty match sets explicitly say so and point back at unfiltered `/skills`. Skill names that start with `-` aren't allowed by the loader, so reserving the dash prefix for flags is safe. Plus the matching usage / description updates in the command metadata + all four shipping locales (en / ja / zh-Hans / pt-BR) so /help shows the new argument. Closes #1318. Thanks @simuusang for the report. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CodeQL's rust/path-injection scan flagged `mcp_config_mtime(path)` because the helper takes `&Path` and calls `fs::metadata(path)`. Both call sites already validate via `validate_mcp_config_path` — `from_config_path` runs the check before constructing the pool, and `reload_if_config_changed` only sees paths that came from a `from_config_path`-validated `config_source` field — so the alert is a false positive about cross-function data flow. The clean fix is to tie the validation to the call site rather than rely on cross-function reasoning: `mcp_config_mtime` now short-circuits to `None` for paths that fail the same allow-list check `load_config` and `save_config` already use. The lazy-reload caller already treats `None` as "skip the check this turn", so a rejected path simply degrades gracefully rather than producing an error path. Cost is one regex check per call on a path we're about to stat anyway. This also makes the helper safe-by-construction for any future caller that forgets to validate, which matches the pattern of the adjacent `load_config` / `save_config` helpers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the v0.8.27 handoff with a fresh v0.8.28 doc focused on what's actually outstanding after the v0.8.27 ship: - P1: CNB.cool mirror automation (token had no push perm during v0.8.27 release), Ctrl+Enter as newline config flag (#1372), Windows task_manager test timeout bump, general test flakiness audit. - P2: comment-pinged issues awaiting reporter (#1112 snapshot growth, #1357 input/runtime overlap, #1281 Cmux notifications). - P3: deferred items (#1338 Windows panic, #1062 capacity recovery, #1067 musl build, #1364 hooks v2, #1343 desktop GUI). The v0.8.27 doc had ~25 items inline; the v0.8.28 doc only carries what's still outstanding (everything else landed in the v0.8.27 cycle — see PR #1375). Starts smaller so the next agent can ship a focused release rather than wade through completed work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Substantial polish release: 17 community-PR cherry-picks plus a focused user-issue sweep over the 24–48 hours after v0.8.26 shipped.
Headline fixes (Phase 1 user-issue work)
2J/3J. Closes 7 reports: Response text is rendered with unexpected line breaks in Ghostty + Starship #1119, Versions 0.822 and 0.823 continuously flicker on Windows 10, while version 0.820 works normally. #1260, windows上运行持续闪屏 #1295, 在 ghostty里面运行时,界面总是在闪烁 #1352, terminal rapid flickering when used with vs code terminal #1356, When using the Ghostty command-line tool, it keeps flickering, but iTerm2 doesn't. #1363, Screen flickering when running TUI inside VSCode terminal #1366.c/y. The pager intercepted mouse capture so terminal-native selection wasn't available;candynow emit aCopyToClipboardevent. Closes Pager content cannot be selected or copied #1354.notifytool. Model-callable desktop notification reusing the existing OSC 9 / BEL pipeline; honours[notifications].methodconfig (silent no-op when off). No new dependency. Closes Feature: model-triggerable desktop notification tool #1322./skills --remotediagnostic hints. Inspects the anyhow chain for DNS/TLS/refused/HTTP/timeout patterns and appends a one-line hint. Closes Failed to fetch for /skills --remote #1329.McpPool::get_or_connectdoes a cheapstat-then-content-hash check before each connection. mtime-only churn doesn't trigger reloads. Closes part 2 of macOS Seatbelt 沙箱阻止 npx MCP 服务器启动,且 sandbox_mode 修改后需手动 /mcp reload #1267.@paste-…mdswap immediately on paste, not at submit, so users see the @-mention in the composer (with toast) before pressing Enter. Eliminates the "TUI auto-sent an @-mention I didn't authorise" surprise.Event::Pasteof a session, so fast typing / IME commits don't false-fire on terminals where bracketed paste already works.请联网搜索:\n…auto-submit case.Pre-release housekeeping (already shipped in v0.8.26)
Closed 7 issues that were fixed by v0.8.26 with the standard upgrade-instruction template (#1163, #1169, #1255, #1292, #1298, #1308, #1331).
Comment-pings on P2 / open follow-ups
#1112 (snapshot growth), #1318 (
/skillsdensity), #1357 (input/runtime-hint overlap), #1281 (Cmux notifications), #1338 (Win11 panic on Enter), #1372 (Ctrl+Enter newline) — each has a comment requesting reproduction details.Community PRs included
/modeunification (#1247),/statusruntime diagnostics (#1223),/feedback(#1185), session artifact metadata (#1220), subagent self-report compaction (#1140), global AGENTS fallback (#1197),--yoloCLI→TUI (#1233),composer_arrows_scroll(#1211), session cost persistence (#1192), provider-aware model picker (#1320), HTTP User-Agent (#1320), HTTP-400 quota retry (#1203), explicit hidden/ignored completions (#1270), Windows mouse-capture docs (#1181), README zh-CN sync (#1235), tool output render perf + card-rail (#1098), test coverage @tuohai666 (#1316, #1317), short CJK paste (#1342 from @reidliu41).Preflight
cargo fmt --all -- --check✅cargo clippy --workspace --all-targets --all-features --locked -- -D warnings✅cargo test --workspace --all-features --locked --no-fail-fast✅ (the documented flaky testmcp_connection_supports_streamable_http_event_stream_responsespasses in isolation)./scripts/release/check-versions.sh✅ (workspace=0.8.27, npm=0.8.27, lockfile in sync)./scripts/release/publish-crates.sh dry-run✅cargo build --release --locked -p deepseek-tui-cli -p deepseek-tui✅node scripts/release/npm-wrapper-smoke.js✅deepseek 0.8.27 (f267d4b87486)Test plan
deepseekinteractive on Ghostty / VSCode terminal — confirm flicker is goneAlt+V/Ctrl+Opager + pressc— confirm clipboard receives content + status toast请联网搜索:\nSTM32 …) → confirm both lines land~/.deepseek/mcp.jsonwhile running, then trigger an MCP tool — confirm it reconnects with the new configdeepseek -p "ping"returns a response and exits cleanly🤖 Generated with Claude Code