Skip to content

chore(release): prepare v0.8.27 - #1375

Merged
Hmbown merged 41 commits into
mainfrom
work/v0.8.27
May 10, 2026
Merged

chore(release): prepare v0.8.27#1375
Hmbown merged 41 commits into
mainfrom
work/v0.8.27

Conversation

@Hmbown

@Hmbown Hmbown commented May 10, 2026

Copy link
Copy Markdown
Owner

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)

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 (/skills density), #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

/mode unification (#1247), /status runtime diagnostics (#1223), /feedback (#1185), session artifact metadata (#1220), subagent self-report compaction (#1140), global AGENTS fallback (#1197), --yolo CLI→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 test mcp_connection_supports_streamable_http_event_stream_responses passes 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
  • Installed locally: deepseek 0.8.27 (f267d4b87486)

Test plan

  • Smoke-test deepseek interactive on Ghostty / VSCode terminal — confirm flicker is gone
  • Smoke-test Alt+V/Ctrl+O pager + press c — confirm clipboard receives content + status toast
  • Type a long URL into a model response — confirm the URL wraps at the right edge (no overflow)
  • Paste 50k-char file → confirm @-mention appears in composer BEFORE Enter, with toast
  • Paste short CJK text with newline (e.g. 请联网搜索:\nSTM32 …) → confirm both lines land
  • Edit ~/.deepseek/mcp.json while running, then trigger an MCP tool — confirm it reconnects with the new config
  • Smoke-test deepseek -p "ping" returns a response and exits cleanly
  • CI matrix passes (Ubuntu / macOS / Windows)

🤖 Generated with Claude Code

tuohai666 and others added 30 commits May 10, 2026 08:15
  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>
Hmbown and others added 6 commits May 10, 2026 09:51
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>
Copilot AI review requested due to automatic review settings May 10, 2026 15:18

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +132 to +138
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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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
}

Comment thread crates/tui/src/mcp.rs
Comment on lines +1494 to +1527
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)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Comment on lines +373 to +467
#[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"
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Comment thread crates/tui/src/mcp.rs Dismissed
Hmbown and others added 2 commits May 10, 2026 10:29
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Hmbown and others added 3 commits May 10, 2026 10:43
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>
@Hmbown
Hmbown merged commit aaccaee into main May 10, 2026
12 checks passed
@Hmbown
Hmbown deleted the work/v0.8.27 branch May 10, 2026 17:13
Hmbown added a commit that referenced this pull request May 10, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet