Commit Graph

245 Commits

Author SHA1 Message Date
Hunter Bown 5fa24733e9 chore(rebrand): update repository links for CodeWhale 2026-05-23 14:07:36 -05:00
Hunter Bown 6de8ba363f chore(release): prepare codewhale v0.8.41 test build 2026-05-23 13:19:01 -05:00
Hunter Bown a3acdbe70b docs(brand): rename to codewhale across READMEs and docs
Sweep brand mentions of `DeepSeek TUI` / `deepseek-tui` / bare
`deepseek` (the dispatcher binary) across all user-facing docs to
the new `codewhale` brand. The DeepSeek **provider** integration is
left untouched throughout: env vars (`DEEPSEEK_*`), model IDs
(`deepseek-v4-pro`, `deepseek-v4-flash`, `deepseek-chat`,
`deepseek-reasoner`), the `api.deepseek.com` host, the
`~/.deepseek/` config dir, and the `--provider deepseek` argument
value all keep the legacy spelling.

Anti-scope items deliberately left as the legacy `deepseek-tui`:

- Homebrew tap and formula (`Hmbown/homebrew-deepseek-tui`,
  `brew install deepseek-tui`, `scoop install deepseek-tui`). The
  tap rename ships separately.
- Docker image (`ghcr.io/hmbown/deepseek-tui`). Image-tag rename
  ships separately.
- CNB mirror namespace (`cnb.cool/deepseek-tui.com/DeepSeek-TUI`).
  Third-party hosted path.
- Security contact email (`security@deepseek-tui.com`).
- GitHub repo URL (`Hmbown/DeepSeek-TUI`).

New artifact:

- `docs/REBRAND.md` documents what changed, what didn't, the
  deprecation window, and migration commands for npm / Cargo /
  Homebrew / manual installs.

CHANGELOG entries:

- Root `CHANGELOG.md` and `crates/tui/CHANGELOG.md` both gain a
  new `[Unreleased]` section describing the rename and the one-
  release deprecation window. Historical entries are untouched.

Issue templates:

- `.github/ISSUE_TEMPLATE/bug_report.md` and `feature_request.md`
  refer to "codewhale" / `codewhale --version` instead of the old
  brand name in their environment fields.

The rebrand sweep was driven by a perl script with bulk patterns
(`deepseek-tui` -> `codewhale-tui`, `DeepSeek TUI` -> `codewhale`,
bare `deepseek` -> `codewhale` with provider/model/host/env-var/
config-path negative lookbehind/lookahead) followed by targeted
reverts for the anti-scope items above. Output was visually
reviewed file-by-file before committing.

Verified:

- `cargo check --workspace --all-targets --locked` — pass.
- `cargo test --workspace --all-features --locked` — pass (no
  test source touched here; suite stayed green to confirm no
  doc-from-string assertions broke).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 11:25:48 -05:00
Hunter Bown faf5e07adb feat(tui): add Fin tool-agent and screenshot OCR 2026-05-21 10:47:14 +08:00
Hunter Bown 396b0e822b fix(tui): persist model picker effort 2026-05-21 09:45:03 +08:00
Hunter Bown 26c66079ba fix(tui): persist selected model mode 2026-05-21 09:15:52 +08:00
Hunter Bown 67fb788110 fix(tui): harvest final v0.8.40 polish 2026-05-21 08:45:04 +08:00
Hunter Bown b639740b73 chore: bump CHANGELOG date to 2026-05-21 for v0.8.40 release 2026-05-21 06:52:48 +08:00
Hunter Bown 66b4a5fd59 fix(tui): rank exact slash aliases first 2026-05-21 01:00:18 +08:00
Hunter Bown 2c642ec375 feat(session): fork conversations inside the TUI 2026-05-21 00:24:52 +08:00
Hunter Bown 912da38cca chore(release): prepare v0.8.40 2026-05-18 23:29:20 +08:00
Hunter Bown 373fbd95a0 chore(release): prepare v0.8.39
Bump workspace, inter-crate, and npm package versions 0.8.38 -> 0.8.39.

Roll CHANGELOG [Unreleased] into [0.8.39] with all fixes:
- Revert v0.8.38 /model picker rework (back to instant curated picker)
- Restore approval grouping (lossy v0.8.37 logic for approvals, exact key for denials)
- Thinking-only turn surface fix (#1727)
- ACP server JSON-RPC id stringification (#1696)
- Chat client: reasoning_content for generic providers (#1673)
- Compaction: user text query preservation (#1704)
- Engine: system prompt override survival (#1688)
- Pager: G/End overshoot fix (#1706), mouse scroll (#1716)
- Composer: scroll with text (#1677), multiline arrows (#1721)
- macOS system theme detection (#1670)
- rlm_open blank source fields (#1712)
- Terminal resize paging fix (#1724)
- Docker first-run permission (#1684)
- README Rust 1.88+ requirement note (#1718)

Tests: 3149 passed, 0 failed (deepseek-tui crate)
clippy: clean on --all-targets --all-features
2026-05-17 16:36:21 +08:00
Claude 81bc2da069 fix(tui): revert v0.8.38 /model picker rework and restore approval grouping
The v0.8.38 upgrade dramatically changed two user-visible behaviors that
were not intended as regressions:

- The /model picker was reworked (#1201/#1632) to make a blocking network
  fetch on open and replace the curated tier list with the raw provider
  catalog. Revert model_picker.rs and the OpenModelPicker handler to the
  v0.8.37 instant curated picker. The /models command still lists the live
  catalog.

- #1617 rekeyed the approval cache to an exact full-argument fingerprint,
  which also dropped the v0.8.37 arity-aware command-family grouping for
  "approve for session". Reintroduce build_approval_grouping_key (the lossy
  v0.8.37 logic) for approvals while keeping the exact key for denials, so
  denying one call no longer over-blocks later differing calls.

https://claude.ai/code/session_01NDuRxM56o17SE7SDLcTFYT
2026-05-17 16:15:17 +08:00
Hunter Bown f0a7e15d30 fix(feishu): guard thread store startup order (#1700) 2026-05-16 18:25:23 -05:00
Hunter Bown 5401eaae08 chore(release): prepare v0.8.38 (#1698) 2026-05-15 18:08:58 -05:00
Hunter Bown b080891efa fix(tui): count loop guard blocks as failures (#1658) 2026-05-15 17:43:07 -05:00
Hunter Bown b834548897 fix(tui): calm legacy Windows console rendering (#1655) 2026-05-14 19:00:52 -05:00
Hunter Bown 7c8c71eb03 fix(tui): default Windows composer arrows to scroll 2026-05-14 16:39:51 -05:00
Hunter Bown 2a4022acbe docs: refresh README setup guidance 2026-05-14 16:38:11 -05:00
Hunter Bown f8a4dee173 fix(tui): preserve provider-selected models 2026-05-14 16:18:18 -05:00
Hunter Bown c9845465db docs: update unreleased bug-bash notes 2026-05-14 15:25:40 -05:00
Hunter Bown 4c32a316be chore(release): prepare v0.8.37 2026-05-14 14:37:14 -05:00
Hunter Bown 9483248a9f feat(feishu): carry Lighthouse bridge into v0.8.37
Add the Feishu/Lark long-connection bridge, Tencent Lighthouse runbooks, CNB mirror guidance, CNB tag release pipeline, and China-friendly update fallback documentation for the v0.8.37 line.
2026-05-14 03:56:03 -05:00
Hunter Bown a3f88bf6cf fix(search): default web search to bing (#1619)
Summary:
- add Bing as explicit default web_search provider
- keep explicit DuckDuckGo configuration supported
- update docs/help/config examples

Validation: CI green before merge.
2026-05-14 03:31:15 -05:00
Hunter Bown d5c45d962d chore(release): prepare v0.8.36
Squash merge of work/v0.8.36-cache-hygiene into main.

All preflight gates passed: version-drift/check/lint/test (3073 pass, 0 fail) / CodeQL / GitGuardian / npm-smoke. Preparing the v0.8.36 release tag.
2026-05-14 00:31:18 -05:00
Hunter Bown 3890ce0b82 docs(release): credit Horace Liu for nix package support
Horace Liu (@liuhq) contributed Nix package support and install
documentation in the v0.8.34 cycle but was inadvertently omitted from
that release's changelog and the README contributor list. This commit
adds them to both.
2026-05-13 17:44:15 -05:00
Hunter Bown 0ab95aea1c chore(release): start v0.8.35 branch
- Bump workspace, internal crate pins, npm wrapper metadata, generated facts, and docs from 0.8.34 to 0.8.35

- Clarify 60% manual compact guidance vs 80% opt-in automatic guardrail

- Expire completed live-tool rows and collapse stale running shell rows in the Tasks sidebar
2026-05-13 13:36:15 -05:00
Hunter Bown f0a4e25360 fix(prompt): trim first-turn context noise 2026-05-13 13:12:14 -05:00
Hunter Bown 16114f3020 fix(release): polish v0.8.34 review issues 2026-05-13 12:08:46 -05:00
Hunter Bown 06177155da feat(edit_file): fuzzy fallback tolerates typographic punctuation drift
When `edit_file` is called with `fuzz: true` and exact match fails,
the existing fallback strips leading whitespace and retries. That
catches indentation differences, but not the much more common
copy-paste failure mode where the search string came from a browser
or chat client that silently substituted Unicode punctuation:

  * U+201C / U+201D (`"` / `"`)  ↔  ASCII `"`
  * U+2018 / U+2019 (`'` / `'`)  ↔  ASCII `'`
  * U+2013 / U+2014 (en/em dash `–` / `—`)  ↔  ASCII `-`
  * U+00A0 (non-breaking space)  ↔  ASCII space

Add a second fallback after the indentation pass: when that yields
no matches, retry once more with both the file contents and the
search string punctuation-normalized to ASCII. A byte-map sized to
the normalized output recovers the original byte range, so the
replacement still goes back into the file untouched (the replacement
text is taken verbatim from the caller).

The fuzz note appended to the success message now distinguishes the
two cases:

  Replaced 1 occurrence in foo.txt (fuzzy indentation match)
  Replaced 1 occurrence in foo.rs (fuzzy punctuation match — typographic quotes/dashes normalized)

Adds two unit tests: one that recovers a smart-quote substitution,
one that handles em-dash + NBSP together.

Inspired by pi-agent's `edit-diff.ts` Unicode normalization step.
2026-05-13 02:03:25 -05:00
Hunter Bown cf45efa07b chore(release): bump workspace to v0.8.34 and clean repo root
- Bump workspace.package.version 0.8.33 -> 0.8.34 across all 14 crates
- Bump npm wrapper version + deepseekBinaryVersion pin
- Update docs/TOOL_SURFACE.md "Current surface" + docs/ARCHITECTURE.md
  current-surface references; historical "removed_in"/"v0.8.33 began
  moving" wording stays as fact
- Update web/lib/facts.generated.ts version pin
- Draft [0.8.34] CHANGELOG section covering the 135 commits since 0.8.33
  (prefix-cache stability, bundled skills, Kitty/Ghostty notifications,
  theme picker, chunked tool dispatch, MCP session-id persistence,
  cost-calc reasoning tokens, and the in-flight internal cleanup)
- Remove stale repo-root development artifacts:
  * TAKEOVER_PROMPT.md (v0.8.6 handoff, 3 minors stale)
  * PROMPT_ANALYSIS.md (v0.8.13-era prompt audit doc)
  * DEPENDENCY_GRAPH.md (claimed monolith layout, predates 14-crate split)
  docs/ARCHITECTURE.md already contains the live crate map.
- Update CONTRIBUTING.md to reference docs/ARCHITECTURE.md for build
  ordering instead of the removed DEPENDENCY_GRAPH.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 01:12:21 -05:00
Hunter Bown 485ba7bbd4 chore(release): finish v0.8.33 polish 2026-05-12 22:03:47 -05:00
Hunter Bown 99c6b22e83 chore(release): v0.8.33 — sub-agent and RLM renovation with persistent sessions
- Persistent RLM sessions (rlm_open/rlm_eval/rlm_close) with bounded REPL helpers
- Fork-aware sub-agent sessions (agent_open/agent_eval/agent_close) with handle_read
- Shared handle_read storage with slice/range/count/JSONPath projections
- Slash-command routing: /rlm, /agent, /relay (/接力) for handoff prompts
- Sidebar renamed to "Work" tab, consistent across Plan/Agent/YOLO modes
- Tool papercuts: file_search excludes, grep_files strings, fetch_url JSON,
  edit_file fuzz, exec_shell merged stdout/stderr, revert_turn no-op reject
- CLI reasoning-effort honoured on non-auto exec routes (#1511 @h3c-hexin)
- Edit-file replacement boundaries clarified (#1516)
- Pandoc output validated before probing (#1523)
- Running turns steerable/repaintable (#1533, #1537)
- Tasks/Activity Detail calmer under load
- npm retry timeout hint (#1538 @reidliu41)
- Issue templates improved (#1525 @reidliu41)
- Shell: kill process group to prevent UI freeze (#828 @CrepuscularIRIS)
- TUI: ignore leaked SGR mouse reports in composer (#1421 @reidliu41)
- Footer: keep chips within available width (#1417 @Wenjunyun123)
- Session picker: scope Ctrl+R to current workspace (#1395 @LinQ)
- Removed stale competitive-analysis doc
- Prompts/docs teach only new tool names
2026-05-12 19:54:08 -05:00
Hunter Bown ad70655bbb docs(release): note v0.8.32 selection issue 2026-05-12 15:08:21 -05:00
Hunter Bown 8a76916140 docs(changelog): finalize v0.8.32 notes 2026-05-12 12:10:44 -05:00
Hunter Bown 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>
2026-05-12 02:47:11 -05:00
Hunter Bown 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>
2026-05-12 02:38:37 -05:00
Hunter Bown 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>
2026-05-12 02:11:10 -05:00
Hunter Bown 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>
2026-05-12 01:39:44 -05:00
Hunter Bown 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>
2026-05-12 01:37:32 -05:00
Hunter Bown 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>
2026-05-12 01:36:57 -05:00
Hunter Bown 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>
2026-05-12 01:35:20 -05:00
Hunter Bown 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>
2026-05-12 01:33:17 -05:00
Hunter Bown 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>
2026-05-12 01:29:15 -05:00
Hunter Bown 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>
2026-05-12 01:28:21 -05:00
Hunter Bown 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>
2026-05-12 01:27:20 -05:00
Hunter Bown 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>
2026-05-12 01:25:16 -05:00
Hunter Bown 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>
2026-05-12 01:23:14 -05:00
Hunter Bown 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>
2026-05-12 01:04:47 -05:00
Hunter Bown 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>
2026-05-12 01:03:19 -05:00