Closes#2074
Adds FauxStep::Factory(Box<dyn Fn(&MessageRequest) -> CannedTurn + Send + Sync>)
to MockLlmClient. When a Factory step is dequeued, its closure runs
against the real outgoing MessageRequest before the response stream is
built, so any assert! panic surfaces directly from the client call
instead of later in stream polling.
Internal storage moves from VecDeque<CannedTurn> to VecDeque<FauxStep>,
but every existing public method keeps working:
- MockLlmClient::new(Vec<CannedTurn>) wraps each turn in FauxStep::Canned.
- push_turn(CannedTurn) appends as FauxStep::Canned.
Adds push_factory(closure) for tests that want the Factory branch.
Doc comment on the Factory variant captures the DeepSeek V4
thinking-mode tool-call invariant (the v0.4.9-v0.5.1 reasoning_content
drop that produced HTTP 400 on follow-up turns).
Adds:
- crates/tui/tests/reasoning_content_replayed_after_tool_call.rs — a
regression test whose factory asserts the assistant tool-call turn
carries a Thinking content block after a thinking + tool-call round.
- An additional unit test in mock.rs covering create_message_synthesizes_from_factory_turn.
All 20 tests in the new file pass, and the existing
integration_mock_llm suite (27 tests) is unchanged.
Clearing active_turn immediately breaks is_interrupt_requested detection
in monitor_turn, causing turn status to be Completed instead of Interrupted.
Let monitor_turn handle the cleanup after it detects the interrupt flag
and performs full finalization with correct status, usage, and error.
- Change 'not loaded' to 'not found' in submit_user_input and
cancel_user_input so map_thread_err correctly maps to 404
- Use map_thread_err in submit_user_input API endpoint for
consistent error response (404 for missing thread, 409 for
conflict, etc.) instead of always returning 500
The monitor_turn loop already handles full turn finalization when the
engine shuts down after cancellation, including saving turn status,
usage, error, emitting turn.completed, and clearing active_turn.
Having interrupt_turn also save turn status and emit turn.completed
causes duplicate SSE events and loses usage/error data that
monitor_turn would have captured from TurnComplete.
Keep only the active_turn cleanup so the 409 error is resolved while
monitor_turn remains the single source of truth for turn completion.
Add SSE event forwarding for UserInputRequired, REST endpoint for submitting user input responses, timeout protection for await_user_input, and fix interrupt_turn to clear active_turn immediately.
The DNS-resolution SSRF guard in `validate_dns_resolved_ip` rejects any
hostname that resolves into a restricted IP range. Under a transparent-proxy
/ TUN setup running in `fake-ip` mode, the local resolver maps *every*
hostname to a reserved placeholder range (commonly `198.18.0.0/15`), so the
guard rejects all outbound `fetch_url` requests even though the proxy will
route them correctly.
Add a narrowly-scoped, opt-in trust list on `NetworkPolicyDecider`:
- `with_trusted_fakeip_cidrs(&["198.18.0.0/15"])` registers IPv4 CIDR ranges
to treat as benign fake-ip placeholders (default: empty — no behavior
change unless the embedder configures it).
- `is_trusted_fakeip_addr(ip)` matches only those configured ranges.
`validate_dns_resolved_ip` now allows a resolved IP if it is either in a
configured fake-ip range or the host is on the existing trusted-proxy list.
Real private/loopback/link-local/cloud-metadata IPs match neither and stay
blocked, so this does not widen SSRF exposure for default configurations.
Includes CIDR parsing/containment helpers and a unit test covering the
placeholder-allowed / real-private-blocked / nothing-configured cases.
Bing wraps every SERP result URL in a `/ck/a?...&u=<base64>` click-tracking
redirect, and in the raw HTML the separators are `&` entities.
normalize_bing_url parsed the href without decoding entities first, so
extract_query_param looked for `u` while the actual key was `amp;u`. The
base64 redirect target was never recovered: every result collapsed to a
`bing.com` root domain, is_likely_spam_results rejected the whole batch,
and Bing — the default backend — returned zero results.
Decode HTML entities before extracting the redirect target. Adds a
regression test.
The header bar was reported to appear vertically centered on macOS
Terminal.app with large blank space above and below. While the existing
Constraint::Min(1) layout with default Flex::Start should place the
header at row 0, some ratatui/flex interactions can produce unexpected
centering on certain terminal sizes.
Restructure the render layout into a defensive two-pass system:
1. First pass: split the terminal into header (Length(1)) + body (Min(1))
with explicit Flex::Start, pinning the header to the absolute top.
2. Second pass: split the body area for chat, preview, composer, footer.
This guarantees the header is never vertically centered regardless of
ratatui Flex defaults or terminal dimensions.
Fixes#1834
EngineConfig.instructions was Vec<PathBuf>, which forces embedders that
compute instructions at runtime to stage content to a disk file just to
satisfy the path API. That has two awkward side-effects:
1. The disk file looks like editable config but gets overwritten on
every launch, confusing for users browsing the install dir.
2. Multi-engine setups need per-engine paths to avoid `rehydrate`
re-reading the wrong session's content across concurrent engines.
Add an `InstructionSource` enum (`File(PathBuf)` / `Inline { name,
content }`) covering both shapes. `render_instructions_block` dispatches:
File sources read disk (original behavior preserved), Inline sources use
the content directly with `name` becoming the `<instructions source=...>`
attribute.
`From<PathBuf> for InstructionSource` is provided so the existing CLI /
runtime call sites upgrade with a single `.into_iter().map(Into::into).
collect()` chain — no behavior change for callers passing paths.
New test `render_instructions_block_handles_inline_source` covers Inline
empty / oversize-truncate / File+Inline mixed-ordering. The 5 existing
`render_instructions_block_*` tests are updated to use `.into()` on
`PathBuf` and continue to assert File-variant behavior.
Deduplicate failures per tool name, clear stale failures when
the same tool succeeds, and cap visible failures at 2 so old
failures don't crowd the sidebar after the task moves on.
Adopt Gemini code-assist review on PR #1831:
- mode_plan: Yellow -> Magenta. Plan chip now contrasts with
status_warning (still Yellow) so the two never visually collide
in the status row.
- status_ready: Reset -> DarkGray. Ready chip now reads as a
distinct subdued accent instead of blending into body text
(which also resolves to Reset on this theme).
No surface change otherwise -- backgrounds and body text still use
Color::Reset to inherit the host terminal's color scheme.
Adds a new selectable theme `terminal` (alongside System / Whale / Whale
Light / Grayscale / Catppuccin / Tokyo Night / Dracula / Gruvbox) that
paints every surface with `Color::Reset` instead of any RGB so the
host terminal's own background, foreground, and palette show through.
The existing `system` theme only chose between two RGB themes (Whale dark
or Whale Light) based on COLORFGBG / macOS appearance — useful, but it
still painted brand-colored RGB surfaces. Users with custom terminal
themes (Solarized, Nord, transparent backgrounds, custom Ghostty/iTerm
schemes) had no way to make the TUI respect their terminal palette.
Implementation:
- New `TERMINAL_UI_THEME` const where every `*_bg` and most text slots
are `Color::Reset`, and accents (mode_agent/yolo/plan, status_working,
status_warning) use ANSI named colors so they also inherit the user's
terminal palette rather than DeepSeek brand RGB.
- `ThemeId::Terminal` plumbed through `from_name` / `name` /
`display_name` / `tagline` / `ui_theme` / `SELECTABLE_THEMES`, and
registered in `normalize_theme_name` with aliases `term`,
`transparent`, `follow-terminal`, `inherit` so existing
user-friendly config strings just work.
- `theme_remap_active(Terminal) → true` so the existing per-cell remap
in `ColorCompatBackend` rewrites every hard-coded palette constant
(`DEEPSEEK_INK`, `DEEPSEEK_SLATE`, `BORDER_COLOR`, `TEXT_BODY`, …) to
`Color::Reset`. Without this, the many render sites that reach for
the named palette constants directly would still paint brand RGB.
- `theme_green` / `theme_red` return `Color::Green` / `Color::Red`
for Terminal so diff "+"/"−" stay green/red but follow the user's
terminal palette.
- `theme_diff_added_bg` / `theme_diff_deleted_bg` return `Color::Reset`
for Terminal — diff highlight is conveyed by foreground color only.
- The new theme is the second entry in `SELECTABLE_THEMES` (right after
System) so it surfaces prominently in the `/theme` picker.
theme_picker tests: the new theme is inserted in row 2 of
`SELECTABLE_THEMES`, which shifts the indices three existing tests
relied on — `arrow_down_previews_next_theme`,
`enter_commits_with_persist_true`, and `digit_jumps_to_row` — so those
expectations are updated to match the new ordering. No production
behavior change in those tests, just index arithmetic.
Default (`theme = "system"`) is unchanged; existing users see no
difference. Users who want full terminal pass-through opt in via
`/theme` or `theme = "terminal"` in settings.toml.