e8b4f9911d6ee5d254ea3fd7bcf96e0154640243
1158 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e8b4f9911d |
feat(composer): display session title in top-right of composer border
Show the current session's persisted metadata.title in the composer border's top-right corner alongside the existing vim mode indicator. - app.rs: add `session_title: Option<String>` field to App - ui.rs: populate it from metadata.title in apply_loaded_session and SessionUpdated handler; add derive_session_title() fallback helper - widgets/mod.rs: render title (muted) + vim label in a single right-aligned title_top span to avoid overlap |
||
|
|
33822424d8 | fix(npm): bump deepseekBinaryVersion to 0.8.32 | ||
|
|
8d5a823724 |
docs(changelog): promote [Unreleased] block to dated [0.8.32] section
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>
|
||
|
|
9d05e6c293 |
fix(tui): remove Shift-bypass-mouse-capture escape hatch (#376)
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> |
||
|
|
c0b9bada27 |
chore(release): bump to 0.8.32
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> |
||
|
|
f964e2fd37 |
fix(snapshot): cap workspace size at 2 GB before first side-repo init
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> |
||
|
|
f8a3c6619e |
fix(npm): map openharmony platform to linux binaries (#1072)
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>
|
||
|
|
ae42bdb2c0 |
refactor(tui): clarify startup empty state with version / model / cwd context
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>
|
||
|
|
ebedd23807 |
feat(skills): add opt-in v4-best-practices bundled skill
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> |
||
|
|
a9412d7df8 |
perf(prompts): move volatile layers below KV-prefix-cache boundary
`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> |
||
|
|
58c39ebc91 |
fix(tui): toast stack overlay no longer renders on top of the composer input
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> |
||
|
|
e1b959a36f |
fix(session_picker): stronger highlight on the selected row in dark terminals
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> |
||
|
|
2950cadc36 |
fix(shell): O(1) job-panel refresh — drop the full-buffer clone under the mutex
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> |
||
|
|
6f70a2832e |
fix(file_mention): preserve UTF-8 codepoint boundary when truncating mention contents
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> |
||
|
|
dcc2c448eb |
fix(client): vLLM uses chat_template_kwargs to toggle reasoning, not the Anthropic field
`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>
|
||
|
|
f60a77e191 |
fix(session): skip turn_meta block when deriving session title
`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>
|
||
|
|
2a306d2b0f |
chore(docs): wrap bare URLs in <...> so rustdoc -D warnings stays green
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> |
||
|
|
c8614a24fd |
fix(tui): enable Kitty keyboard protocol on Windows so Shift+Enter inserts newline
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> |
||
|
|
3aaf0ad95e |
feat(vision): add image_analyze tool gated behind vision_model feature flag
`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> |
||
|
|
bd603a271c |
feat(tools): add image_ocr tool — extract text from images via tesseract
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> |
||
|
|
aed7dbefaa |
feat(tools): add pandoc_convert tool — universal document conversion
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> |
||
|
|
2566f3c546 |
feat(tools): add js_execution tool — model-provided JavaScript via local Node
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> |
||
|
|
d62facafac |
feat(translate): opt-in /translate command localises model replies
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> |
||
|
|
8f33e4bd48 |
feat(providers): add AtlasCloud as a first-class provider
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> |
||
|
|
40df46c73d |
feat(web_search): add configurable Tavily and Bocha provider backends
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> |
||
|
|
a57cb4dfa7 |
feat(approval): collapse approval modal to a one-line banner with Tab
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>
|
||
|
|
02f889f193 |
fix(markdown): preserve underscores inside identifiers when rendering inline spans
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> |
||
|
|
1f0065ccee |
feat(commands): add /change slash command to display latest CHANGELOG entry
`/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> |
||
|
|
23d3a71126 |
fix(cli): deepseek update refreshes sibling TUI binary alongside dispatcher
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>
|
||
|
|
f94bedf1ea |
feat(tools): swap read_file PDF path to bundled pdf-extract; pdftotext now opt-in
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> |
||
|
|
c188cade88 |
ci(cnb): use plain --force on main push, drop misleading --force-with-lease
`--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. |
||
|
|
eb451aefd7 |
chore(gitignore): block .claude/HANDOFF_* and .claude/CODEMAP_* patterns
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 |
||
|
|
e7aa722dda |
chore(security): remove .claude/ handoff + codemap notes from public repo
Two internal-strategy files had leaked into the public repo via `.claude/`: .claude/HANDOFF_v0.8.28_user_issues.md (10 KB, added in |
||
|
|
56a893563b |
ci(auto-close): replace heredoc with printf so YAML block scalar parses
The `<<EOF` heredoc inside the `run: |` step body broke YAML parsing — heredoc bodies have to start at column 0, but YAML block scalars require consistent indentation. Both runs of the new workflow on the v0.8.31 push failed at the workflow-file validation stage with `expected a comment or a line break, but found '$'` at line 128. Switching to `printf '%s\n' "line1" "line2" ...` keeps every line of the message at the same indent as the surrounding shell code, so the YAML `|` scalar stays consistent. Behaviour is identical from the contributor's perspective. Confirmed locally with `python3 -c 'import yaml; yaml.safe_load(...)'` before pushing. |
||
|
|
383ed44fe8 |
fix(child_env): preserve MSVC toolchain vars for Windows exec_shell
Harvested from PR #1487 by @Jianfengwu2024 — the rest of that PR (the TriadMind architecture-governance crate) needs a Discussion- level design conversation before it can land, but this Windows env-allowlist fix is a clean independent improvement and stands on its own. When the parent shell has already loaded VsDevCmd / vcvars (the standard pattern for running Rust + MSVC on Windows), `exec_shell` was stripping the toolchain env on its way to the child. The result: the model finds `link.exe` via `PATH` but the linker can't resolve `kernel32.lib` / `ucrt.lib` because LIB and the SDK roots were filtered out. Any model-driven `cargo build` from inside the TUI silently broke on Windows installs that don't run inside a Developer Command Prompt. Adds 13 MSVC-related env vars to the `is_allowed_parent_env_key` allowlist so they survive the sanitization pass: LIB / LIBPATH / INCLUDE VSINSTALLDIR / VCINSTALLDIR / VCTOOLSINSTALLDIR WINDOWSSDKDIR / WINDOWSSDKVERSION UNIVERSALCRTSDKDIR / UCRTVERSION EXTENSIONSDKDIR / DEVENVDIR / VISUALSTUDIOVERSION Also extends the `mcp_env_allowlist_inherits_base_keys` fixture and adds `sanitized_child_env_preserves_windows_toolchain_vars` as a regression test (locked under `env_lock()` so it serialises with the other env-mutating tests in the file). Pure additive — no non-Windows behaviour changes. Harvested from PR #1487 by @Jianfengwu2024 |
||
|
|
0407451150 |
chore(release): bump to 0.8.31
- workspace.package.version: 0.8.30 → 0.8.31 - per-crate path-dependency version pins: 0.8.30 → 0.8.31 (31 entries) - npm/deepseek-tui: version + deepseekBinaryVersion → 0.8.31 - Cargo.lock refreshed via `cargo update --workspace --offline` - CHANGELOG: [Unreleased] → [0.8.31] - 2026-05-12 with theme paragraph - new [Unreleased] anchor + [0.8.31] compare link Verified with `./scripts/release/check-versions.sh`: Version state OK: workspace=0.8.31, npm=0.8.31, lockfile in sync. Verified parity gates green pre-bump (see prior commits) and post-bump (cargo fmt --all --check, cargo clippy --workspace --all-targets --all-features --locked -- -D warnings). |
||
|
|
0397f96f10 | docs(changelog): write opening paragraph for [Unreleased] v0.8.31 theme | ||
|
|
2b54b7e79d |
feat(tools): probe Python interpreter at catalog-build time; doctor surfaces missing deps
Closes the "code_execution: program not found" failure reported by a
Windows contributor and adds the surrounding plumbing so future
external-binary dependencies can be added without each one repeating
the same probe-and-skip logic.
New module `crates/tui/src/dependencies.rs`:
* `PYTHON_CANDIDATES = ["python3", "python", "py -3"]` — try the
Unix-style name first, then the bare name (common on Windows and
modern macOS), then the Windows launcher as a last resort.
* `probe_executable(spec)` — splits the spec on whitespace, spawns
`<program> [args...] --version`, returns true on success. stdout
and stderr are routed to /dev/null so the probe doesn't print to
the TUI's first frame.
* `resolve_python_interpreter()` — OnceLock-cached resolver that
returns the first candidate that probed successfully, or None.
* `resolve_pdftotext()` — same shape, for doctor's PDF-path
diagnostic.
* `split_interpreter_spec("py -3") → ("py", ["-3"])` — needed so
`tokio::process::Command::new(program).args(args).arg(script)`
runs the Windows launcher correctly.
Wiring in `core/engine/tool_catalog.rs::ensure_advanced_tooling`:
* The hardcoded `catalog.push(Tool { name: "code_execution", … })`
is now gated on `resolve_python_interpreter().is_some()`. On a
machine without Python the tool is not advertised — the model
never sees a tool it can't actually run, which removes the
"Failed to execute tool: program not found" class of failure
entirely. On a machine WITH Python the behavior is unchanged
from the user's perspective.
Wiring in `execute_code_execution_tool`:
* Resolves the interpreter at call time (still cached) and splits
on whitespace so `py -3` runs as `py -3 /tmp/.../code.py`.
* Writes the user code to a tempfile under tempdir() and runs the
file rather than passing it through `python3 -c "..."`. Fixes
argv length limits on Windows, multiline code with quote
nesting, and traceback line numbers (real filename instead of
`<string>`). Folds in the contributor's Part 2 proposal.
* Returns a clear actionable error if the interpreter somehow
disappears between startup and the call (uninstall mid-session,
PATH manipulation, etc.).
Doctor (`crates/tui/src/main.rs::run_doctor`) gains two new sections:
* "Tool Dependencies": ✓/✗ for Python and pdftotext with a
platform-specific install hint when missing. The Python miss
explicitly tells users that `code_execution` is not advertised.
* "Terminal Quirks": shows which env-driven auto-overrides are
currently active (VS Code / Ghostty low-motion, Termius/SSH
low-motion per #1433, Ptyxis sync-output-off per v0.8.31).
Answers the "why is my flicker / motion behaving this way"
question without needing the user to read the source.
The contributor's Part 1 proposal (share the sandbox/approval
substrate with task_shell_start) is deferred to v0.8.32 — that
touches `sandbox/` which CLAUDE.md flags as "stop and ask before
changing" and needs an explicit design pass on approval semantics.
Six new unit tests in `dependencies.rs` covering probe behavior,
spec splitting, and cache stability. Existing tool_catalog tests
still pass.
|
||
|
|
80fc0046e1 |
ci: auto-close PRs whose code is harvested into main
Solves the long-standing hygiene problem where contributor PRs whose code lands via maintainer cherry-pick stay open + CONFLICTING forever, even though their fix is credited in the CHANGELOG. v0.8.29 alone left ~5 such PRs open (#1421, #1429, #1442, #1465 — verified separately). New workflow `.github/workflows/auto-close-harvested.yml`: * Triggers on push to main. * For each commit in the push, scans the commit body for lines matching `harvested from (PR )?#N` (case-insensitive). * For each matched PR number, closes the PR with a templated thank-you that links back to the merged commit, thanks the contributor by handle, and points them at CONTRIBUTING for landing future PRs via the faster direct-merge path. * Idempotent — already-closed PRs are skipped with a log line, not an error. * Concurrency-guarded so two near-simultaneous main pushes can't both try to close the same PR. Two commit-message patterns are recognised: * `Harvested from PR #1234 by @username` (preferred form, used in the templates the maintainer paste-uses for harvests) * `harvested from #1234` (case-insensitive fallback for older / shorter forms) The convention is documented in CONTRIBUTING.md, which also adds a new "How Your Contribution Lands" section explaining the harvest model upfront so contributors know what to expect — closing their PR isn't rejection, and the credit lives in the commit message and CHANGELOG. Permissions on the workflow: `pull-requests: write` to close + comment, `issues: write` for the comment (PR comments are issue comments under the hood), `contents: read` for the checkout. |
||
|
|
2e8322bb84 | docs(release): cross-link CNB mirror runbook from RELEASE_RUNBOOK | ||
|
|
90eaf04a84 |
ci(cnb): rewrite sync workflow with concurrency + scoped push + retry
Closes the persistent "fatal: could not read Username for 'https://cnb.cool'" failure that bit about half of recent sync-cnb runs. Root causes from the failure log of run 25705666413 (2026-05-12) and 25697452832 (2026-05-11): 1. The previous tencentcom/git-sync Docker action discovered every local ref via fetch-depth: 0 and tried to push them all — including dependabot/* branches that GitHub had locally. Those follow-on pushes ran without the configured credential helper in scope and failed with the basic-auth prompt error above. 2. No concurrency guard meant the back-to-back `main` push and `v*` tag push that auto-tag.yml fires within ~15s of each other raced. Two workflow runs would attempt to push to the same CNB remote simultaneously; one would lose. Rewrite: * Hand-rolled `git push` with the CNB token URL-encoded inline so the credential is always in scope. No Docker action dependency. * `concurrency: group: cnb-sync, cancel-in-progress: false` so queued runs serialize cleanly rather than racing or dropping. * Only push the ref that triggered the run — `main` on branch push (with --force-with-lease for safety), the specific tag on tag push. Feature branches and dependabot refs no longer mirror. * 3-attempt retry with linear backoff (5s, 10s) for transient failures (CNB rate-limit, DNS blips, etc.). * `workflow_dispatch: {}` trigger so the maintainer can re-run against a specific ref manually if the automated run fails. Adds docs/CNB_MIRROR.md with: the verification steps after a release, the manual fallback procedure (one-time `git remote add cnb`, then `git push cnb vX.Y.Z`), the token-rotation procedure, and a note on why binary release assets aren't on CNB today. Cross-links from docs/RELEASE_RUNBOOK.md so the verify-after-release step doesn't get forgotten. |
||
|
|
2e857b09b7 |
chore(web): bump next 15.5.16→15.5.18, mermaid 11.14→11.15 (security)
Clears the open Dependabot alerts on `Hmbown/DeepSeek-TUI`:
* GHSA-26hh-7cqf-hhc6 (high) — Next.js App Router middleware /
proxy bypass via segment-prefetch routes; fixed in 15.5.18.
* Four mermaid CVEs (all medium) — Gantt-chart infinite-loop
DoS, `classDef` HTML injection, configuration CSS injection,
`classDefs` CSS injection; fixed in 11.15.0.
Also bumps `eslint-config-next` to 15.5.18 to track the Next.js
release. `npm run build` is clean on the regenerated lockfile.
These are web/ only — the Rust TUI binary doesn't pull in any of
this. Affects the separately-deployed deepseek-tui.com site.
|
||
|
|
cc71ec191f |
fix(settings): auto low-motion in Termius + every SSH session (#1433)
Closes #1433. Harvested from PR #1479 by @CrepuscularIRIS / autoghclaw, with two changes from the original PR: * `is_some_and` instead of `.map_or(false, |v| !v.is_empty())` — the latter trips `clippy::unnecessary_map_or` on Rust 1.94+ under `-D warnings`, which is what blocked the PR's Lint check in CI. is_some_and reads cleaner and ships the same behavior. * `non_vscode_term_program_does_not_force_low_motion` now clears SSH_CLIENT / SSH_TTY before iterating its negative-case fixtures so the suite still passes when run from a developer's actual SSH session. Detection logic mirrors the existing VS Code (#1356) and Ghostty (#1445) overrides: any of TERM_PROGRAM=Termius, SSH_CLIENT set, or SSH_TTY set unconditionally flips low_motion = true and fancy_animations = false. The 120 FPS cursor-repositioning that caused the cursor to cycle through input boxes over SSH is dropped to the 30 FPS cap the typewriter path already uses. Two new tests: termius_term_program_forces_low_motion_on and ssh_session_forces_low_motion_on. Both serialise through the existing term_program_test_guard / crate-wide test lock to avoid racing concurrent env-var-mutating tests in the suite. |
||
|
|
1f7cd9cc2f |
feat(mcp): custom HTTP headers per server for authenticated gateways
Closes #1454. Harvested from PR #1456 by @Oliver-ZPLiu. Adds `pub headers: HashMap<String, String>` to McpServerConfig, threaded through HttpTransport::new and StreamableHttpTransport so every outbound POST applies the user-configured headers after the fixed Accept / Content-Type framing. Mirrors the field shape that Claude Code, Codex, and OpenCode already accept in their MCP config formats — unblocks Hugging Face MCP, GitHub MCP, Atlassian Rovo MCP, and any other Streamable HTTP gateway that needs a Bearer token or API key. Defense-in-depth filter (`is_safe_custom_header`) drops: * empty / whitespace-only keys (would surface as a reqwest builder error mid-send and abort the connection); * `Accept` / `Content-Type` duplicates (the MCP Streamable HTTP protocol negotiates on these exact values; a stray override would silently break tool discovery); * values containing ASCII CR or LF (response-splitting defense against a misbehaving proxy). Skipped headers emit a `tracing::warn!` and the rest of the request still goes out, so a single bad entry can't take down a server. Scope-limited vs PR #1456: * SSE legacy transport intentionally not threaded (follow-up). The PR didn't cover it; modern MCP servers are Streamable HTTP. * No env-var interpolation in v0.8.31 — headers are sent literally, matching the PR. Documented in the field doc so users know tokens pasted directly into mcp.json live there as plain text. `${VAR}` substitution is a follow-up. 8 new tests (the original PR shipped without any): config round-trip with custom headers, empty headers omitted from serialized output, accept-normal-auth, reject empty key, reject CR/LF in value, reject Accept/Content-Type override (case- insensitive), and StreamableHttpTransport stores headers. Empty-headers MCP fixtures updated at 10 sites across mcp.rs and main.rs to match the new struct shape. |
||
|
|
b4158dcc1b |
feat(read_file): bounded chunked reads via start_line / max_lines
Harvested from PR #1451 by @Oliver-ZPLiu (closes part of #1450). Token-budget control for read_file: large files no longer drop their entire contents into the conversation context on every turn. Default window is 200 lines / ~16 KB; the hard cap is 500 lines. Small files (≤ 200 lines AND ≤ 16 KB, no explicit range params) keep the historical raw-contents return so existing prompts that read config files / single source files see no behavior change. Otherwise the response is wrapped in a `<file …>` tag with line-numbered content, `shown_lines`, `truncated`, and `next_start_line` attributes, plus a `[TRUNCATED]` hint so the model can page through in 16 KB slices. Cleanups from the original PR while harvesting: - shown_lines is now 1-based inclusive (e.g. "3-6"), matching start_line / next_start_line / the line-number prefix on each rendered row. The original PR mixed 0-based indices in attributes with 1-based numbers in body output, which was confusing. - The continuation hint mirrors that 1-based range so model reasoning over "what did I just see?" is unambiguous. - Added 6 unit tests covering: small-file fast path, explicit range wrap-in-file-tag with 1-based lines, out-of-range no-content sentinel, zero start_line / max_lines rejection, hard-cap clamp at 500 lines, and large-file no-range default window. The original PR shipped without tests. |
||
|
|
fd82f85800 |
fix(tui): auto-disable DEC 2026 sync output on Ptyxis to stop VTE 0.84 flicker
Ptyxis 50.x (the new default terminal on Ubuntu 26.04) ships with VTE 0.84.x, which parses the `\x1b[?2026h` / `\x1b[?2026l` synchronized- output begin/end pair but still flashes the entire viewport on every wrapped frame instead of deferring rendering. gnome-terminal 3.58 on the same VTE renders cleanly, so the heuristic stays narrow: trigger only on TERM_PROGRAM matching `ptyxis` (case-insensitive) or PTYXIS_VERSION non-empty. Add a new `synchronized_output` setting (`auto` | `on` | `off`, default `auto`) controlling whether the renderer wraps each frame in DEC 2026. `apply_env_overrides` flips `auto` → `off` when Ptyxis is detected; the four wrapping sites in ui.rs (`draw_app_frame_inner`, `reset_terminal_viewport`, `resume_terminal`, and the early-init viewport reset) now respect the resolved flag. Users on Ptyxis who upgrade past an upstream fix or want to confirm one landed can override with `/set synchronized_output on`. 8 new tests cover: default-auto resolves enabled, off disables, on stays enabled, set/aliases, Ptyxis via TERM_PROGRAM, Ptyxis via PTYXIS_VERSION alone, explicit `on` beats the heuristic, explicit `off` is preserved, and no non-Ptyxis TERM_PROGRAM (including Ghostty and VS Code, which both keep DEC 2026 on) regresses. Reported via WeChat by Cyrux on Ubuntu 26.04 with v0.8.30 npm install; analysis by Hunter pinpointed Ptyxis + VTE 0.84 as the cause. |
||
|
|
74fbc83b9e | docs(changelog): add missing v0.8.1–v0.8.30 comparison links, update Unreleased anchor | ||
|
|
1a73791e1d |
chore(release): bump to 0.8.30
- workspace.package.version: 0.8.29 → 0.8.30 - per-crate path-dependency version pins: 0.8.29 → 0.8.30 - npm/deepseek-tui: version + deepseekBinaryVersion → 0.8.30 - Cargo.lock refreshed via `cargo update --workspace --offline` - CHANGELOG: `[Unreleased]` → `[0.8.30] - 2026-05-11` with the full release-theme paragraph and the new "Changed" section for the Alt+<key> unification Verified with `./scripts/release/check-versions.sh`: Version state OK: workspace=0.8.30, npm=0.8.30, lockfile in sync. |
||
|
|
31198c11d0 |
fix(tui): unify transcript-nav shortcuts under Alt+<key>; stop eating first letters
v0.8.29 spot-fixed `g` with a gg double-tap (commit
|
||
|
|
a328344691 |
feat(tui): restore 🐳→🐋 cycling status indicator next to the effort chip
The whale was a 12-frame animated indicator (`🐳, 🐳., 🐳.., 🐳..., 🐳.., 🐳., 🐋, 🐋., 🐋.., 🐋..., 🐋.., 🐋.`) that shipped from v0.3.5 onward and rendered in the top-right status cluster of the header. Commit `1a04659a9` ("smoother TUI streaming") quietly swapped it for a 6-frame geometric ring (`◍ ◉ ◌ ◌ ◉ ◍`); `f4dbf828c` later deleted the function entirely. Nothing in the CHANGELOG mentioned either step, and the absence has been on the maintainer's mind ever since. This commit restores the whale as a configurable status indicator that sits immediately before the reasoning-effort chip ("next to max"): - `widgets/header.rs` gains a public `header_status_indicator_frame` helper and a `HeaderData::with_status_indicator(Option<&'static str>)` builder. The frame computation is pure (keyed off `turn_started_at` and the mode string) so the widget itself stays a stateless render. - The chip renders as the first item in the status cluster, before `provider` / `effort` / `Live` / context. Idle state shows a steady 🐳; an active turn cycles frames every 420 ms (same cadence as the original v0.3.5 implementation). New setting `status_indicator`: - `whale` (default) — restored historical cycling. - `dots` — the 6-frame geometric replacement, for users who came in during the dots era and prefer it. - `off` — hide the chip entirely. Settable via `/config status_indicator <whale|dots|off>`, persisted in `settings.toml`, mirrored in the typed `config_ui::SettingsSection` with a new `StatusIndicatorValue` enum so the web/JSON config surface sees it too. Default-to-whale rationale: this restores the historical behaviour for every user, including those who never realized the whale was gone, and keeps the "🐳 in /config" delight that the project's name has always implied. Regression-guarded by seven new tests in `widgets/header.rs::tests` covering idle frame, frame advancement, dots variant, off variant including aliases, unknown-mode fallback to whale, render placement before the effort label, and confirmation that `off` hides the chip without disturbing the effort chip layout. |