Add clearer prompts to bug reports and feature requests so users provide
reproduction details, impact, environment context, use cases, and supporting
materials up front.
`resolve_cli_auto_route` was hard-coding `reasoning_effort: None` when
`--model` is not `auto`, which silently dropped the value the user had
set in `~/.deepseek/config.toml` on every non-auto-route exec/one-shot
call.
For vllm + Qwen3 users with `reasoning_effort = "off"`, thinking was
therefore never disabled. The model emitted a long reasoning trace for
every prompt and SSE idle timeouts (`did not receive response headers
after 45s`) fired on any non-trivial prompt. After this fix, the same
prompts return in ~1.5s.
Route the configured value through `ReasoningEffort::from_setting`, the
same parser the TUI uses elsewhere for this field. Auto-route behaviour
(`--model auto`) is unchanged.
Verified by capturing the outgoing request body with `nc` before and
after; chat_template_kwargs.enable_thinking=false now appears in the
body on vllm exec runs.
Co-authored-by: hexin <he.xin@h3c.com>
The version-consistency test `prompts::tests::changelog_entry_
exists_for_current_package_version` requires a `## [0.8.32]`
section once the workspace `Cargo.toml` has advanced to that
version — release hygiene from the v0.8.x sequence. Promote
the existing `[Unreleased]` content to `## [0.8.32] - 2026-05-12`
and leave a fresh empty `[Unreleased]` placeholder above it so
future post-tag commits have somewhere to land.
Also tighten the opening paragraph: the v0.8.31 throat-clear
("release in progress") becomes a release-shape summary that
calls out the five new tools, six PR harvests, and the snapshot
2 GB cap that closes the multi-hundred-GB-workspace "scroll
demon".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Users reported a "scroll demon" — visible thrash where the
display would flicker / scroll / redraw spuriously while moving
the mouse. Root cause: the #376 native-selection escape hatch
watched every mouse event for `KeyModifiers::SHIFT`, and on each
transition (Shift pressed → released, or vice versa) it:
1. Toggled the alt-screen mouse-capture mode via crossterm
execute!(DisableMouseCapture / EnableMouseCapture).
2. Pushed a status toast ("Native selection — release Shift to
return" / "Mouse capture restored").
On terminals that report mouse-event modifier state aggressively
(notably the modern xterm-modifyOtherKeys / Kitty keyboard
protocol family the v0.8.32 Windows fix in PR #1483 just turned
on more broadly), the bypass would flip on stray Shift state
changes during ordinary scrolling — producing a tight cycle of
mouse-capture toggles and toast renders that the user perceived
as the display going haywire.
The feature was never load-bearing for native text selection on
modern terminals: macOS Terminal and iTerm honor Option-drag
(macOS convention), most Linux terminals honor Shift-drag at the
terminal layer regardless of what the TUI does with mouse
events, and Windows Terminal exposes its own copy mode. The
in-TUI bypass was a workaround for a narrower set of terminals
that bowed out of relevance once we got mouse capture cleaner
elsewhere.
Removed:
- `let mut shift_bypass_active = false;` state on `ui::run_app`
- The mouse-event Shift-modifier branch that flipped capture
modes and emitted toasts
- The 5-second redraw nudge that the toast cycle implied
Net delta: 23 lines deleted, 0 added. Mouse capture stays
on for the whole session (or off when `--no-mouse-capture` is
set at launch), and stray Shift events on mouse move are now
ordinary mouse events.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Workspace, all 9 path-pinned crate deps, and the npm wrapper's
package.json all advance from 0.8.31 → 0.8.32. `scripts/release/
check-versions.sh` passes (workspace ↔ npm ↔ Cargo.lock all in
sync).
Auto-tag only fires on push-to-main, so this bump on `work/v0.8.32`
doesn't accidentally cut a release; it just makes the
in-development binary identify itself correctly. When this branch
merges to main, the existing release pipeline takes over from
here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Users reported running `deepseek-tui` inside project directories
with hundreds of GB of content — ML datasets, model weights
(`.safetensors`, `.gguf`, `.pt`, `.onnx`), Docker image dumps,
parquet / arrow caches, anything that falls outside the snapshot
built-in excludes. The pre/post-turn snapshot path called
`SnapshotRepo::open_or_init` which initialized the side git repo
and then ran `git add -A` — which walked the entire workspace
indexing every file. On a 100-300 GB directory this hung the TUI
for minutes-to-hours while git churned through the index.
The pre-existing v0.8.27 fixes (#1112: retention cap, mid-session
prune, expanded built-in excludes) addressed the orthogonal
"snapshots grow unbounded over many turns" angle but did nothing
to prevent the first snapshot from being impossible to take.
This change adds `estimate_workspace_size_bounded()` — a bounded
`ignore::WalkBuilder` walk that respects `.gitignore` and the
snapshot module's existing skip list (`node_modules/`, `target/`,
`.next/`, `.venv/`, `__pycache__/`, etc.). The walk early-exits
at either the byte cap or 200,000 file entries, returning `None`
to signal "too big to snapshot."
`SnapshotRepo::open_or_init_with_cap(workspace, cap_bytes)` calls
the estimator *before* the side `git init`, and returns
`Err(InvalidInput)` with a "workspace too large" reason — which
`turn::snapshot_with_label` already logs at WARN and continues
past, so a too-large workspace silently disables snapshots
without blocking any turn. The check is paid only on first init;
subsequent snapshots through the existing side repo skip it.
Plumbing:
- `SnapshotsConfig.max_workspace_gb` (default 2, `0` disables)
- `EngineConfig.snapshots_max_workspace_bytes` resolved at engine
construction from `config.snapshots_config().max_workspace_gb`
- `pre_turn_snapshot` / `post_turn_snapshot` / `pre_tool_snapshot`
take a `cap_bytes: u64` argument threaded from the engine
- `SnapshotRepo::open_or_init` retains its v0.8.31 signature as a
thin wrapper over `open_or_init_with_cap` using the default cap
- `config.example.toml` documents the new `max_workspace_gb` knob
with the "set to 0 to disable" escape hatch for users with
legitimate large monorepos
Six new tests pin both the estimator (under-cap returns Some,
over-cap returns None, builtin-excluded dirs skipped, cap=0
disables the bound) and the `open_or_init_with_cap` integration
(oversized workspace fails with the right error and references
the config knob; cap=0 succeeds even on oversized content).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Node's `os.platform()` returns `openharmony` on HarmonyPC and on
OpenHarmony's Linux ABI-compatible userspace. The npm wrapper's
platform-asset matrix only covered `linux` / `darwin` / `win32`,
so `npm i -g deepseek-tui` aborted on those hosts with
Unsupported platform: openharmony. Supported platforms: …
even though the existing Linux x64 / arm64 binaries run unchanged
on that environment (OpenHarmony is Linux-ABI-compatible at the
ELF level).
Added a `PLATFORM_ALIASES = { openharmony: "linux" }` indirection
that resolves the raw platform name through the alias map before
the `ASSET_MATRIX` lookup. Genuinely unsupported platforms still
report the raw `os.platform()` value in the error so OS-mismatch
bug reports stay diagnostic.
Four pure-JS regression tests pin the behaviour:
- openharmony x64 → linux x64 binaries
- openharmony arm64 → linux arm64 binaries
- known platforms unchanged by the alias map
- freebsd still reports `Unsupported platform: freebsd`
Harvested from PR #1499 by @CrepuscularIRIS
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The center of the startup welcome view used to repeat
information already shown in the header and footer
(active model and mode names). It now shows three pieces of
context that first-time users don't otherwise see at a glance:
- the build version (so users on stale installs notice it
before reaching `deepseek doctor`)
- the active model with a `/model` hint so the picker is
discoverable from the empty state
- the current working directory so users can confirm the
workspace deepseek-tui anchored at
The header and footer continue to show the running model and
mode for the active session; this change is only about the
center "empty transcript" panel that sits in the gap before the
first user message lands.
Harvested from PR #1444 by @reidliu41
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A single 50-line `SKILL.md` encoding three V4-specific workflow
rules for multi-step thinking-mode tasks. Each rule maps to a
concrete observable failure class the maintainer or contributors
have hit when running V4-flash / V4-pro on long agent loops; the
text is opinionated but discovered through the existing skill
mechanism rather than baked into the always-on system prompt.
Follows the existing bundled-skill convention
(`crates/tui/assets/skills/skill-creator/`) so the discovery
walker picks it up alongside the workspace's own skills. Not
enabled by default — users see it in `/skills` and opt in
explicitly, keeping the always-on prompt-prefix footprint
unchanged for everyone who doesn't want the directive.
Single new file; zero Rust changes, zero new dependencies, zero
config schema changes. Fully reversible by deleting the directory.
Harvested from PR #1448 by @SamhandsomeLee
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`instructions = [...]` (the per-workspace config-driven block),
the user memory file (`/memory`), and the current session goal
(`/goal`) were being rendered at position 2.5 in the system
prompt — inside the static prefix layer that DeepSeek's KV
prefix cache hits.
Any edit to those files invalidated every cached byte from that
position onward. A `# foo` memory quick-add (or a `/goal` update)
on turn 5 meant the engine had to re-tokenize and re-charge the
full static suffix — skills block, context management, compact
template, environment, ~thousands of tokens — on turn 6.
Relocate the three blocks to position 6, immediately above the
previous-session handoff block, where the volatile-content
boundary already lives. The static prefix above the boundary
(mode, project context, env, skills, context management, compact
template) now stays cached across turns regardless of how often
the user edits their memory file or shifts session goals.
Resolved a 3-way merge against the v0.8.32 `translation_enabled`
addition (PR #1462). The new translation-output instruction stays
at position 2.3a (inside the static prefix layer) because it's a
per-session flag — `/translate` is a session toggle, not a
turn-by-turn knob, so the prompt-prefix bytes don't drift
mid-session.
Harvested from PR #1345 by @Duducoco
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a deferred tool's schema auto-loaded after the model
requested it, the resulting status toast (e.g. "Auto-loaded
deferred tool 'edit_file' after model request.") could render at
`footer_area.y - 1` — which on tight terminal layouts is the
bottom row of the composer area. The toast then visibly overwrote
the start of the user's typed text, corrupting the display until
the next redraw.
Root cause: `render_toast_stack_overlay` computed
`max_above = footer_area.y.min(full_area.height)` — bounded only
by the screen height, not by the composer's footprint. So on a
16-row terminal with composer rows 10–14 and footer at row 15,
`max_above` resolved to 15 and the renderer happily placed a
toast at row 14, on top of the composer.
The fix threads `composer_area: Rect` into the renderer and
clamps `max_above = footer_area.y.saturating_sub(composer_area.y
+ composer_area.height)`. When the composer and footer are
adjacent (no gap), `max_above` collapses to 0 and the overlay
returns early without drawing anything. Non-adjacent layouts —
which arise on taller terminals where the composer and footer
don't touch — render unchanged.
Replaced the contributor's confused test commentary with a tight
two-assertion pin: `max_above == 0` on an adjacent layout, plus a
sanity `max_above <= gap` invariant so any future regression that
re-introduces the overlap fails the test rather than the user's
display.
Harvested from PR #1485 by @MeAiRobot
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `/sessions` picker's selection background was subtle enough
to disappear in low-contrast dark themes — keyboard navigation
moved the focus indicator but the visual change between focused
and unfocused rows was hard to notice, especially when running
the TUI in a terminal with a near-black background.
The selected row now renders with a bolded label on a stronger
background so the focused row reads cleanly across the dark
palettes the TUI ships with. The non-selected rows are unchanged
so the change doesn't add visual noise on light terminals.
Test pin: `build_list_lines_selected_row_uses_strong_highlight`
ensures the rendered row at the selected index applies the
expected modifier and background combination.
Harvested from PR #1493 by @reidliu41
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#1299. The TUI's job panel refreshes every 2.5 seconds by
calling `job_snapshot()`, which previously called `full_output()`
to clone the entire accumulated stdout/stderr buffer under the
`ShellManager` mutex. For long-running jobs that flood stdout
(browser automation drivers, large `cargo build` runs, anything
streaming progress to a pipe) the buffer grew unboundedly;
cloning held the mutex for O(total_bytes) time, starving the
`crossterm::event::poll` loop and producing the input freeze
users reported as "TUI locks up after ~30 seconds of output."
The fix:
1. `job_snapshot()` no longer calls `full_output()`. A new
`tail_from_buffer()` reads only the last `max_tail_chars * 4`
bytes under the lock and decodes them. Lock hold time is now
O(1) regardless of total output volume. The job-panel display
only needs the tail anyway — never the whole stream.
2. `take_delta_from_buffer()` reduces its clone footprint: the
old code did `buffer.lock().map(|d| d.clone())` — eagerly
cloning the full buffer before slicing the unread delta out.
New code slices `[cursor..total]` inside the lock guard so
only the unread bytes are allocated.
3. `tail_start` can land mid-codepoint after the buffer wraps.
Before slicing, the code now skips any UTF-8 continuation
bytes (`& 0xC0 == 0x80`) so `from_utf8_lossy` never sees an
invalid leading byte and never emits a leading U+FFFD in the
job-panel tail.
`stdout_len` / `stderr_len` still report the true total byte
counts so no caller invariant changes. `job_detail()` (the
user-triggered detail view) still calls `full_output()`
intentionally — detail views are rare and not on the hot refresh
path. The orphan-grandchild `collect_output()` path is already
handled on Unix via `kill_child_process_group`; the equivalent
Windows fix is filed as a separate concern (see PR body).
Harvested from PR #1494 by @CrepuscularIRIS
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#1441. When `@`-mentioning a file larger than the 128 KB
`MAX_MENTION_FILE_BYTES` ceiling, the truncator clipped the buffer
to exactly the cap — which on CJK / emoji content frequently
landed mid-codepoint and left a stray U+FFFD replacement char at
the cut point.
The fix uses `str::from_utf8(...).error_len()` to distinguish the
two ways a truncated UTF-8 buffer can fail:
- `error_len() == None` means the failure is an incomplete tail
sequence — exactly the boundary case we want to handle. Round
`buffer.truncate()` down to `valid_up_to()` so the trailing
bytes are dropped cleanly.
- `error_len() == Some(_)` means the file genuinely contains
invalid UTF-8 bytes (not at the truncation boundary). Leave
the buffer intact so the subsequent `from_utf8(&buffer)` call
surfaces the canonical "file is not UTF-8" error rather than
silently dropping the invalid bytes.
Collapsed the if-let-then-if pattern to `if let Err(e) = ... &&
e.error_len().is_none()` to satisfy the workspace's
`collapsible_if` clippy gate.
Harvested from PR #1495 by @CrepuscularIRIS
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`apply_reasoning_effort`'s vLLM branch was injecting
`thinking: {type: "disabled"}` at the top of the request body to
turn off model reasoning. But vLLM speaks OpenAI's
chat-completions protocol, not Anthropic-native extension fields,
and silently ignored that directive — the model emitted a full
hidden reasoning trace into the non-OpenAI-standard `reasoning`
field (which this client does not surface), so users saw a
~13-second perceived freeze before the first content token
arrived.
The vLLM branch now emits the OpenAI extension
`chat_template_kwargs.enable_thinking` — the canonical way to
toggle Qwen3's `<think>` mode, DeepSeek-R1's reasoning trace, and
any other reasoning-capable model served via vLLM. End-to-end
measurement against vLLM hosting Qwen3.6-35B-A3B-FP8:
- TTFT: 13039ms → 274ms
- Total LLM call: 13s → 5.7s
- Output rate: 3 ch/s → 46 ch/s
The `high` / `max` reasoning levels likewise route through
`chat_template_kwargs` so the toggle is consistent across effort
levels. No change for any non-vLLM provider (NVIDIA NIM continues
to use the NVIDIA-specific `chat_template_kwargs.thinking` key;
Anthropic-native providers keep the Anthropic-native field).
Resolved a 3-way merge conflict against the v0.8.32 AtlasCloud
harvest (PR #1436) so AtlasCloud stays in the no-op match arm
alongside OpenAI / Ollama while the new vLLM arm gets its own
branch. Note for future Sglang / Fireworks / Novita work: those
servers likely have the same bug but each has its own
chat_template_kwargs schema; this PR is intentionally minimal
to the verified-fix scope.
Harvested from PR #1480 by @h3c-hexin
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`SessionManager::create_saved_session_with_id_and_mode` picks the
first `ContentBlock::Text` off the user's message via `find_map`
and uses that as the session title. The engine prepends a synthetic
`<turn_meta>...</turn_meta>` block (Block 0) ahead of the real user
text (Block 1), so the `/sessions` picker was rendering the metadata
blob as the session name.
Guard the find_map filter on `!text.starts_with("<turn_meta>")` so
titles fall through to the actual user input. Existing sessions
without the prefix block are unaffected (the guard is a no-op when
no metadata block is present); the existing `truncate_title` long-
input handling continues to apply.
Harvested from PR #1498 by @wdw8276
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The PR #1294 (Tavily / Bocha provider) and PR #1467 vision-tool
harvests both surfaced rustdoc warnings about bare URLs in doc
comments — `https://tavily.com` rather than `<https://tavily.com>`.
Under `RUSTDOCFLAGS=-D warnings` (which CI runs) those warnings
escalate to errors. Wrapping each URL in `<...>` produces the
clickable autolink rustdoc expects.
Fixed sites: `SearchProvider::{Tavily, Bocha}` doc comments in
`crates/tui/src/config.rs`, both Tavily/Bocha API endpoint refs in
`crates/tui/src/tools/web_search.rs`, and the pandoc.org link in
the `crates/tui/src/tools/pandoc.rs` module header.
Two pre-existing `WorkingSet::build_file_index` /
`WorkingSet::fuzzy_resolve` unresolved-link errors on `main` are
intentionally NOT touched in this commit — they were introduced
by `9759a77ae` and live outside the v0.8.32 scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#1359. On Windows 11 + VSCode integrated terminal +
PowerShell, pressing `Shift+Enter` in the composer submitted the
message instead of inserting a newline. `Alt+Enter` / `Ctrl+J`
were broken the same way. Root cause: crossterm's
`PushKeyboardEnhancementFlags` checks `is_ansi_code_supported()`
before emitting the escape, and on Windows that helper queries
the console mode rather than the VT capability and
unconditionally returns false — so the Kitty push `\x1b[>1u` was
never written. xterm.js then stayed in legacy mode where
`Shift+Enter` and `Enter` both encode as `\r`, indistinguishable.
The fix writes the push and pop escapes directly under
`#[cfg(windows)]`, bypassing the crossterm capability gate.
VSCode and Windows Terminal honour the Kitty keyboard protocol;
terminals that don't (older conhost without VT processing)
silently discard the unknown escapes. The same gate also meant
`PopKeyboardEnhancementFlags` was silently dropped on Windows in
the `main.rs` panic hook and in
`tui::external_editor::spawn_editor_for_input` — both call sites
now route through `pop_keyboard_enhancement_flags` so a crash or
`$EDITOR` invocation can't leave the parent shell with a
Kitty-enhanced keyboard state.
Two `#[cfg(windows)]` regression tests pin the direct-write path
so accidentally falling back to `execute!()` against
`crossterm::PushKeyboardEnhancementFlags` would now fail in CI:
- `push_keyboard_flags_writes_kitty_push_sequence_on_windows`
- `pop_keyboard_flags_writes_kitty_pop_sequence_on_windows`
Non-Windows behaviour is unchanged — the existing
`recover_terminal_modes_emits_expected_csi_sequences_with_gating`
test still passes on Linux and macOS.
Also adds a v0.8.29 audit note to `docs/KEYBINDINGS.md` and
documents a pre-existing FocusGained stack-depth bug for a
separate fix.
Harvested from PR #1483 by @CrepuscularIRIS
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`image_analyze` sends an image file to an OpenAI-compatible vision
endpoint and returns the model's natural-language description.
Complements `image_ocr` (which uses local tesseract for "what text
is on this image"); `image_analyze` is for "what is this image
about" — visual reasoning the local OCR engine can't do.
Trust-boundary scope: **two-step opt-in only**.
1. The feature is gated by `[features] vision_model = true` —
default `false`.
2. The tool needs a `[vision_model]` config block specifying
`model` (with optional `api_key` / `base_url` — falls back to
the main config api_key + the OpenAI base URL).
Without both, the tool isn't registered, so no install fires a
vision API call without explicit user setup. Workspace boundary:
the tool rejects absolute paths and any `..` parent-dir
traversal before any base64 encoding or HTTP call. Stateless —
each call sends only the requested image + optional prompt; no
session, no conversation history attached. Supports PNG, JPEG,
GIF, WebP, and BMP inputs.
**Billing**: each call hits the configured vision endpoint
(OpenAI by default — `gpt-4o-mini` / `gpt-4o` family commonly
configured). Users with their own deployments (Gemini, Claude
Vision via OpenAI shim, local llama.cpp) can point `base_url` /
`api_key` at the alternative.
Tests cover the tool metadata (read-only capability, correct
name), MIME-type detection across the supported formats and the
unsupported-format rejection path, and the workspace-boundary
checks (absolute paths and `..` traversal both reject before
any API call). Skipped from the upstream PR: the
`.github/workflows/sync-cnb.yml` rewrite, which v0.8.31 already
addressed with the concurrency/scoped-push refactor; landing the
older form would regress that commit.
Resolved a clippy::collapsible_if in tool_setup.rs (the
`if feature && let Some(cfg) = ...` form) to satisfy the
workspace -D warnings gate.
Harvested from PR #1467 by @MMMarcinho
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets the model OCR a screenshot, scanned receipt, whiteboard photo,
or image-only PDF the user drops into the workspace, without
bouncing through `exec_shell` (which would mean an approval prompt
plus the model having to remember tesseract's CLI surface). The
tool spawns `tesseract <image> -` and returns the recognised text
inline — no file is written. Capability is ReadOnly + parallel
since OCR is a side-effect-free read.
Registration is gated on `crate::dependencies::resolve_tesseract()`
via the new `ToolRegistryBuilder::with_image_ocr_tools()` builder,
hooked into `with_agent_tools` alongside `pandoc_convert`. When
tesseract is missing the tool isn't advertised — same
probe-then-decide pattern v0.8.31 introduced for Python. The
execute path also late-resolves so a concurrent uninstall surfaces
the install-tesseract hint rather than the raw spawn failure.
`deepseek doctor`'s "Tool Dependencies" section reports tesseract
status next to pandoc / node / python with platform-aware install
hints. For non-default language packs or PSM modes the user can
still drop into `exec_shell` with the full tesseract CLI surface.
Tests check the metadata (ReadOnly + parallel, not WritesFiles),
the missing-path rejection, and the happy-path OCR round-trip
against `crates/tui/tests/fixtures/ocr_hello.png` — a 2 KB
300×100 grayscale PNG generated with ImageMagick rendering
"HELLO OCR" in Helvetica. The happy-path test skips silently on
hosts without tesseract (matching the catalog-build behaviour) and
on hosts where the fixture isn't checked out (sparse / shallow
clones).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pandoc is the de-facto Swiss Army knife for moving prose between
the formats engineers and writers actually use: Markdown to HTML,
HTML to Markdown, reST to anything, anything to LaTeX / DOCX /
EPUB. Surfacing it as a model-callable tool unblocks a large class
of "rewrite this report as ..." / "publish this changelog as ..."
workflows that previously required the user to drop into a
terminal between turns.
The tool reads `source_path` (any pandoc-supported input format —
pandoc autodetects from the extension), converts to one of the
11 whitelisted target formats, and either writes the result to
`output_path` (when provided) or returns the converted text
inline. Target whitelist:
markdown, gfm, commonmark, html, rst, latex,
docx, odt, epub, plain, asciidoc
Picked for coverage of real document-handling without dragging in
additional system tooling (no PDF target — that needs a LaTeX
engine; no S5/Slidy — niche and surprising). Binary targets
(docx, odt, epub) reject inline-text requests with a clear error
naming the required `output_path`; text targets work in either
mode.
Registration is gated on `crate::dependencies::resolve_pandoc()`
through the new `ToolRegistryBuilder::with_pandoc_tools()` builder
method, hooked into `with_agent_tools` so the tool surface picks
it up everywhere the existing diagnostics / project tools do.
When pandoc is missing the tool simply isn't registered (same
probe-then-decide pattern v0.8.31 introduced for Python).
Approval routes through the WritesFiles / Suggest tier matching
other file-writing tools.
`deepseek doctor`'s "Tool Dependencies" section reports pandoc as
present / absent with platform-aware install hints
(`brew install pandoc` / `apt install pandoc` /
`winget install JohnMacFarlane.Pandoc`).
Tests cover the format whitelist round-tripping into the schema's
`enum` field, the binary-format rejection path, the unsupported-
format rejection path, the missing-source-file rejection, the
Markdown→HTML inline round-trip, and the `output_path` write
roundtrip + summary message.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors `code_execution` (Python) so the model sees a single
consistent surface for "run this snippet locally and tell me what
it printed" across both interpreters. Same tempfile spawn pattern,
same 120-second timeout, same stdout/stderr/return_code JSON
result shape — prompt-cache layouts covering one tool also cover
the other.
Registration is gated on `crate::dependencies::resolve_node()`:
when Node is missing the tool is simply not advertised in the
model's catalog (the same probe-then-decide pattern v0.8.31
introduced for Python), so the model never sees a runtime it
can't actually use. `deepseek doctor`'s "Tool Dependencies"
section reports Node alongside Python with platform-aware
install hints.
Approval, Plan-mode gating, missing-tool dispatch, and turn-loop
exec wiring follow the existing `CODE_EXECUTION_TOOL_NAME`
pathways one-for-one — the new tool name `js_execution` is added
to every site that already special-cased `code_execution` in
`turn_loop.rs`. Default approval description: "Run model-provided
JavaScript code in local Node.js execution sandbox" (Suggest tier).
Tests cover the tool definition shape, happy-path stdout capture,
non-zero exit on a thrown Error, and the `code` field requirement.
The two spawning tests skip cleanly on hosts without Node (the
catalog wouldn't advertise the tool there anyway, so failing the
suite would be a regression on the probe contract).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-layer design for users whose UI locale is not English:
1. **System-prompt directive (primary)**: when the user enables
translation via `/translate`, a `## Language Output Requirement`
block is appended to the system prompt instructing the model to
reply in the resolved session locale (Simplified Chinese,
Traditional Chinese, Japanese, or Brazilian Portuguese). Code
identifiers, technical terms without an established translation,
and code blocks the user explicitly requests in English are
exempt. The block is gated on `PromptSessionContext.translation_
enabled`, so it adds zero tokens for installs that don't opt in.
2. **Post-hoc heuristic (fallback)**: a lightweight detector in
`tui::translation` compares Latin-letter count against weighted
CJK characters (CJK chars carry ~3× the information per glyph,
so the ratio comparison stays fair across mixed code+prose).
When a reply still surfaces English despite the directive, the
detector flags it and a focused per-message `client.translate()`
call renders the localised version before display. The dedicated
translation request runs without conversation history, tool
calls, or streaming — the only role is translate-and-return.
Adds the `/translate` slash command, locale strings for the new UI
states, the post-hoc fallback module, the per-message
`TranslationStatus`, and threading through `core::ops`,
`core::engine`, `runtime_threads`, and the TUI app/UI surface.
Trust-boundary check: opt-in only — `translation_enabled` defaults
to false everywhere, so English-locale installs see zero behaviour
change. The system prompt addition is conditional on the runtime
flag, not the contributor's earlier always-on form. Threaded the
new `Locale::ZhHant` arm through the v0.8.32 `/change` slash
command match to keep the pattern exhaustiveness check passing.
Harvested from PR #1462 by @YaYII
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AtlasCloud (https://atlascloud.ai) hosts the V4 family on its own
DeepSeek-compatible endpoint at `https://api.atlascloud.ai/v1`, and
several contributors had been running it through the
OpenAI-compatible passthrough with manual `base_url` / model
overrides. Selecting `provider = "atlascloud"` in
`~/.deepseek/config.toml` (or via `DEEPSEEK_PROVIDER=atlascloud`)
now wires up:
- documented `DEFAULT_ATLASCLOUD_BASE_URL` /
`DEFAULT_ATLASCLOUD_MODEL` defaults so a fresh install needs
only the api_key
- a `[providers.atlascloud]` config block with the same fields
every other named provider exposes (api_key / base_url / model
/ http_headers)
- `ATLASCLOUD_API_KEY` env var path, including the secrets test
cleanup loop so per-test env hygiene continues to work
- the provider-picker / `/provider` slash command entries so the
provider is reachable from the runtime UI, not just config
- the env-driven `*_BASE_URL` override branch so users who pin a
proxy can still flip it without editing config.toml
Trust-boundary pins held: AtlasCloud is opt-in (default remains
DeepSeek), no API keys are hardcoded, the api_key resolution flows
through the same `secrets` crate path every other provider uses,
and the provider-config base_url stays settable per environment.
Resolved 3-way merge conflicts in `crates/secrets/src/lib.rs` (env
cleanup loop) and `crates/tui/src/config.rs` (per-provider
base_url match arm + `provider_passes_model_through` predicate)
so the contributor's AtlasCloud branch coexists with the v0.8.x
provider expansion already on `main`. Added the missing match arm
in `validate_provider_base_url` so the non-exhaustive-pattern
check passes after the new variant lands.
Harvested from PR #1436 by @lucaszhu-hue
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DuckDuckGo HTML scraping with Bing fallback remains the default
`web_search` backend — no API key required, no behaviour change for
installs that don't opt in. Users in regions where those scrapers
are rate-limited or unreliable can now set `[search]
provider = "tavily" | "bocha"` plus `api_key = "..."` in
`~/.deepseek/config.toml` (or via the `DEEPSEEK_SEARCH_PROVIDER` /
`DEEPSEEK_SEARCH_API_KEY` env vars) to route every `web_search`
call through the chosen API.
Tavily targets general AI-agent search; Bocha (博查) is the
mainland-China-friendly equivalent. Both providers are gated by
the existing `[network]` policy on their respective hosts
(`api.tavily.com`, `api.bochaai.com`) and surface a clear
`ToolError` (rather than a silent fallback to DuckDuckGo) when
the user has opted in but forgotten to set `api_key`. Test pins
the missing-key behaviour for both providers.
Resolved 3-way merge conflicts in `web_search.rs` (description
text and test module) so the contributor's helpers
(`truncate_error_body`, `sanitize_error_body`) coexist with the
v0.8.30 spam-detection and query-parsing tests already on `main`.
Folded the `SearchProvider::default()` impl into a `#[default]`
derive to satisfy the workspace `-D warnings` clippy gate, and
threaded `search: Option<SearchConfig>` through `merge_config` so
the multi-config layering doesn't break the field initialiser.
Harvested from PR #1294 by @sandofree
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the approval modal rendered as a full-screen takeover
card that hid the transcript behind it, so users had to dismiss the
prompt — losing the decision context — just to re-read the tool
call they were being asked to approve. The new collapsed mode flips
the modal to a single-line banner pinned at the bottom of the area
("<tool> — <risk badge> [Tab to expand]"), so the conversation
stays visible while the decision is pending. Tab toggles between
the two modes; the selected option, pending-confirm state, and
risk colour scheme are preserved across the toggle.
Test pin: a fresh `ApprovalView` starts expanded, the first Tab
collapses, the second Tab restores — and both return `ViewAction::None`
so no decision side effect leaks out of the toggle.
Harvested from PR #1455 by @tiger-dog
Note for the maintainer: PR #1455 also includes a Chinese-language
preamble to `prompts/base.md` that biases reasoning_content toward
Chinese on Chinese-language turns. That change touches the system
prompt and is left for a separate sign-off — see
.private/handoffs/v0.8.32-1455-prompt-preamble.md for the diff and
the suggested call.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before this change, the inline markdown parser greedily matched the
underscore in identifiers like `deepseek_tui` or `foo_bar_baz` against
the `_italic_` pattern, so the second half of the identifier rendered
in italic and transcript snippets that quoted code symbols read as
garbled prose. The same bug applied to `*italic*` against tokens like
`look_at*tail`.
Both italic delimiters now apply a CommonMark-style boundary check on
the closing run: when the character immediately after the closing
`_` / `*` is a letter, digit, or underscore, the delimiter is left
as literal text rather than treated as markup. Identifiers survive
intact; legitimate emphasis still renders.
Regression-pinned with `crate deepseek_tui handles approvals`,
`see foo_bar_baz for details`, and `look at *not_emphasised*tail`.
Harvested from PR #1455 by @tiger-dog
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`/change` reads the most recent `## [version]` section from the
workspace `CHANGELOG.md` (or the bundled release-notes copy when
no workspace changelog is available) and renders it inline in the
TUI. On non-English locales the command also queues a model-side
translation request so localised users see the changelog text in
their UI language; with no API key configured, the offline path
returns the section verbatim with a brief explanatory header.
Lets users discover what changed in the version they just upgraded
into without leaving the chat — and keeps the v0.8.32 release-notes
flow consistent with `deepseek update`'s newly-fixed sibling-TUI
refresh: now both binaries match the version, and `/change` shows
what that version actually delivered.
Resolved a `clippy::needless_range_loop` warning in the section
extractor (idiomatic `iter().enumerate().skip(...).find(...)` instead
of an indexed range loop) so the harvest passes the workspace's
`-D warnings` clippy gate without touching the contributor's design.
Harvested from PR #1416 by @zhuangbiaowei
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before this change, `deepseek update` would replace the running
dispatcher binary at `~/.cargo/bin/deepseek` but leave the sibling
`~/.cargo/bin/deepseek-tui` at whatever version was installed last.
The dispatcher then reported the new release while the TUI binary it
shells out to for every interactive turn stayed pinned to the old
build — most visible on Volta-managed npm installs and on any flow
that uses `deepseek update` instead of re-running both
`cargo install --path crates/{cli,tui}`.
The updater now enumerates the running binary plus an existing
colocated sibling up front, fetches and SHA256-verifies every needed
release asset before replacing anything on disk, then swaps the
sibling first and the running dispatcher last so a partial network
failure can't leave the launcher updated while the TUI remains stale.
The success message lists every refreshed binary by full path.
Tests cover sibling target detection (dispatcher present + sibling TUI
present → both targeted) and the no-sibling fallback (dispatcher only
→ single target).
Harvested from PR #1492 by @NorethSea
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before v0.8.32 the PDF branch in `read_file` shelled out to `pdftotext`
(Poppler), which meant first-time users on hosts without it saw the
tool return a `binary_unavailable` sentinel and had to install Poppler
before the model could open a PDF. The `pdf-extract` crate already
powered URL-fetched PDFs in `web_run`, so this change reuses it for
the local PDF path too — extraction is now zero-dependency on every
supported host. The `pages` parameter still filters by 1-indexed
inclusive page range; both whole-file and per-page variants run with
no system dependency.
Users with column-heavy or complex-table PDFs (academic papers,
financial filings) where `pdftotext -layout` still wins can opt into
the external path with `prefer_external_pdftotext = true` in
`~/.config/deepseek/settings.toml`. When set, the previous Poppler
dispatch returns — including the `binary_unavailable` install hint
when the binary is missing on PATH. `deepseek doctor` reframes
`pdftotext` as optional and explains the opt-in instead of treating
it as a missing dependency.
Tests round-trip a checked-in academic PDF through the new path and
verify the `pages` window still slices correctly. The pre-v0.8.32
"binary_unavailable on missing pdftotext" branch is exercised against
the new opt-in setting via a serialised env-var guard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`--force-with-lease` without an explicit value uses
`refs/remotes/<remote>/main` as the lease ref. The CNB push remote
is added fresh inside each workflow run (`git remote add cnb …`)
without a prior fetch, so that lease ref never exists in the
runner's local clone. The lease check then misfires with
`! [rejected] HEAD -> main (stale info)` even when CNB is correctly
behind GitHub.
Plain `--force` is the right primitive here: the CNB mirror is
one-way by design, so there's no contributor work on the CNB side
to protect against. The lease safety would only matter in a
multi-writer scenario, which we explicitly don't run.
Confirmed via failing run 25714171752 (2026-05-12T04:53:13Z) where
all three retry attempts failed with the same stale-info error
even though CNB was simply behind GitHub by two scrub commits.
Follow-up to the previous commit that removed two leaked handoff
files from `.claude/`. The actual removal landed, but the
`.gitignore` rules that would prevent a future accidental re-add
were left in the working tree by mistake.
Adds:
.claude/HANDOFF_*
.claude/CODEMAP_*
.claude/handoff_* (case-insensitive belt-and-suspenders)
.claude/codemap_*
so `git add .claude/HANDOFF_v0.8.32_*` etc. is silently dropped at
add time instead of slipping through review.
Verified with `git check-ignore`:
.gitignore:89:.claude/handoff_* .claude/HANDOFF_test_scratch.md