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
When an OpenAI-compatible backend (vLLM, Ollama, LM Studio, Together AI,
self-hosted vLLM/SGLang, etc.) streams an assistant message containing
multiple tool_calls in a single round, only the **last** tool's
`Event::ToolCallStarted` was firing. The preceding N-1 tool calls
executed and produced tool_result events, but never announced their
start to consumers (TUI / runtime API / embedder bridges), leaving them
with N orphan tool_result blocks and no matching tool_use blocks in the
assistant history.
## Reproduction
```text
backend dispatches: 7 × write_file + 1 × exec_shell
log shows: 7 × ApprovalRequired events ✓
listeners receive: 1 × chat:tool_start, 7 × chat:tool_end
session history: 1 tool_use + 7 tool_result (6 orphans)
```
Tested against vLLM 0.7 + Qwen3.6-35B-A3B with a "scaffold 7-file Tauri
template" prompt. Any model+backend combo that emits batch tool_calls
trips this — typical when a single LLM round asks for multiple parallel
file writes or edits.
## Root cause
`run_turn` tracked the currently-streaming tool block with a single
`current_tool_index: Option<usize>`. The Anthropic-style adapter
(non-streaming response → events at `chat.rs::L1807`) emits
Start/Stop pairs in lockstep so the slot never overlaps. But the
OpenAI streaming parser (`chat.rs::L1954-2064`) emits every
`ContentBlockStart::ToolUse` as soon as a tool_call delta lands, then
batches every `ContentBlockStop` at `finish_reason`:
```text
Start { index: 0 } // tool #1
Delta { index: 0, .. }
Start { index: 1 } // tool #2 — overwrites current_tool_index
Delta { index: 1, .. }
…
Start { index: 6 } // current_tool_index = Some(6)
Delta { index: 6, .. }
Stop { index: 0 } // take() returns Some(6) ← wrong tool!
Stop { index: 1 } // take() returns None
Stop { index: 2 } // take() returns None
…
```
The first `Stop` consumes the last index and emits `ToolCallStarted`
for the wrong `tool_uses` entry; every subsequent `Stop` finds the
slot already `None` and skips the entire `if let Some(index) = …`
branch, dropping the announcement.
## Fix
Replace the single slot with `HashMap<u32 block_index, usize
tool_uses_idx>`:
- `ContentBlockStart::ToolUse` and `::ServerToolUse` insert the
`(event.index → tool_uses.len())` mapping.
- `InputJsonDelta` looks up by the `ContentBlockDelta` outer index.
- `ContentBlockStop` removes by the stop's index, so each Stop routes
to its own `tool_uses` entry regardless of arrival order.
Routing no longer depends on `current_block_kind` (which has the same
single-slot overwrite problem); `current_tool_indices.remove(&index)`
returning `Some(_)` already proves the Stop belongs to a tool block.
## Tests
Added `batch_tool_calls_preserve_all_tool_use_indices` in
`core/engine/turn_loop.rs::tests` — feeds 7 Starts and 7 Stops through
the same `HashMap` API used by `run_turn`, asserts every index round-trips.
Manual end-to-end verification: vLLM + Qwen3.6-35B + 7-file Tauri
template prompt → frontend `messages` history now contains all 7
`write_file` tool_use blocks paired with their tool_result blocks.
Co-authored-by: hexin <he.xin@h3c.com>
Map legacy DeepSeek CN provider names back to the canonical Deepseek provider in both manual parsing and TOML deserialization.
Co-authored-by: qiyan233 <qiyan233@users.noreply.github.com>
Hard-wrap overlong CJK/no-whitespace runs in diff and pager text wrappers so they do not overflow the right edge.
Fixes#1571.
Co-authored-by: Aitensa <1900013029@pku.edu.cn>
Deduplicate official DeepSeek model completions and normalize known prefixed aliases to the bare model IDs expected by official DeepSeek providers, while preserving provider-specific IDs for compatible backends.\n\nFixes #1594.\n\nCo-authored-by: reidliu41 <reid201711@gmail.com>
Make the translation client optional so missing or invalid API configuration does not crash startup before onboarding can render.\n\nCo-authored-by: Crvena <kuragectl@gmail.com>
Capture and replay Mcp-Session-Id for Streamable HTTP transports, and apply configured custom headers to the GET preflight.\n\nCloses #1629.\n\nCo-authored-by: Zhiping <2716057626@qq.com>
Write the Kitty keyboard protocol probe (ESC[>0u) on Windows instead of enabling disambiguation flags that crossterm does not decode there. Fixes#1599.
Make the sidebar expiry test avoid subtracting from a fresh Instant on Windows runners, falling back to a short sleep only when an older Instant cannot be represented.
Hard-break oversized streaming tokens so CJK runs, URLs, and other no-whitespace content stay within the target width. Includes regression coverage for long CJK text and first-token overflow.
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.
Summary:
- restore auto sidebar focus when Ctrl+Alt+0 is pressed from hidden state
- preserve existing hide behavior from visible sidebar states
- add regression coverage
Validation: CI green before merge.
Summary:
- include generic tool input in approval-cache fingerprints
- keep exact repeat denials stable
- prevent one denied generic tool call from blocking future distinct calls
Validation: CI green before merge.
Summary:
- concise live shell/tool labels
- collapsed pending CI polling rows
- hardened stale task-panel timing test
Validation: CI green before merge.
The workspace ships two CHANGELOG files — the repo-root one and the
crate-local `crates/tui/CHANGELOG.md`. The `prompts::tests::
changelog_entry_exists_for_current_package_version` gate scans the
nearest CHANGELOG to the manifest, so the crate-local copy needs the
same `## [<version>]` section before tagging.
Copy the [0.8.34] section over from the root CHANGELOG, including the
edit_file fuzzy-punctuation bullet added later in the session. No new
content; the two files now agree.
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.
Two more cohesive seams move out of ui.rs:
* `tui::format_helpers` (new) — the three pure builders for the
cache-warmup status message, the prefix-stability footer chip, and
the `/models` listing. Renamed `format_cache_warmup_result` →
`cache_warmup_result`, `format_prefix_stability_chip` →
`prefix_stability_chip`, `format_available_models_message` →
`available_models_message` (the `format_` prefix was redundant once
the module name is `format_helpers`). Adds two unit tests.
* `tui::key_shortcuts` (new) — the 10 cross-platform key-event
predicates and labels: `is_copy_shortcut`,
`is_file_tree_toggle_shortcut`, `tool_details_shortcut_label`,
`activity_shortcut_label`, `alt_nav_modifiers`,
`is_macos_option_v_legacy_key` (+ the test-only platform variant),
`is_paste_shortcut`, `is_text_input_key`, `is_ctrl_h_backspace`.
These were the cross-platform glue around `crossterm`'s `KeyEvent`
that normalises Ctrl-vs-Cmd, the macOS Option-Latin escape, and
legacy Ctrl+H-as-backspace behavior.
ui.rs is now **8916 lines** — under 9000 for the first time, down
from the 10,025-line starting point (a ~11% reduction across the
session). All 956 tui:: unit tests still pass.
The `/files` picker ranks workspace files by three signals harvested
from the session: which files git reports as modified, which the user
@-mentioned in the composer, and which recent tool calls touched. The
scoring code — `open_file_picker` plus 9 helpers (build_relevance,
modified_workspace_paths, parse_git_status_path,
mark_tool_detail_paths / from_value / from_text, workspace_file_candidate,
clean_path_token, workspace_path_to_picker_string) — was ~218 lines
of self-contained logic mid-ui.rs.
Moved to `tui/file_picker_relevance.rs`. Same behavior; the picker
view file (`tui/file_picker.rs`) keeps the rendering layer, and the
new module owns the per-session ranking that fed it.
ui.rs is now 9073 lines (down ~950 from the pre-refactor 10025).
`crates/tui/src/modules/` was never registered with `mod modules;` in
the binary and nothing in the workspace called `run_deepseek_chat` /
`run_official_chat`. The file is a 23 KB text-REPL workflow from a
much earlier iteration of the project — superseded by the actual
TUI runtime in `crates/tui/src/tui/` long ago, but never deleted.
Removing it cuts ~580 lines of unreachable code and 44 `println!`
calls from the workspace audit, with zero behavior change (the
compiler confirms nothing imports from this path).