Better tui#26
Conversation
feat: update docs
acp_prompt was always using the global AgentState.llm regardless of the session's model override. Sessions created with a specific model were silently using the wrong client. Now builds a fresh client from the session config when provider or model differs from the default.
Opens a centered overlay listing all configured providers and models. Arrow keys navigate the list, Enter switches model mid-session (history preserved), Esc cancels. The status bar updates immediately on switch. Direct switch via :models <name> still works without opening the popup. Also fixes cursor visibility (hidden while picker is open to prevent input artifacts) and scroll bounds (saturating arithmetic to prevent panic on navigation).
…ceil_char_boundary
…nder ToolResult errors in red
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
📝 WalkthroughWalkthroughAdds a Ratatui TUI (types, rendering, app state, runtime), introduces AppConfig.theme_color and resolve_with_provider, enables per-session model switching (ACP + SessionHandle), implements streaming LLM across providers (OpenAI/Anthropic/Gemini) and agent loop, improves tool execution error handling, and bumps version/toolchain. ChangesTerminal User Interface Implementation
Sequence Diagram(s)sequenceDiagram
participant User
participant Terminal
participant TUI_App
participant Background_Task
participant AgentState
User->>Terminal: keyboard input
Terminal->>TUI_App: KeyEvent
TUI_App->>TUI_App: handle_key / handle_command
alt model switch
TUI_App->>Background_Task: send (provider,model) via channel
Background_Task->>AgentState: SessionHandle.switch_model(provider,model)
AgentState-->>Background_Task: (provider_name, model_name)
Background_Task->>TUI_App: AgentUpdate::ModelChanged
else session restore
TUI_App->>Background_Task: send (session_id,cwd)
Background_Task->>AgentState: SessionHandle.restore(session_id,cwd)
AgentState-->>Background_Task: restored SessionUpdate events
Background_Task->>TUI_App: AgentUpdate variants (TextChunk/ToolCall/ToolResult...)
else prompt
TUI_App->>Background_Task: send prompt text
Background_Task->>AgentState: prompt(text)
loop streaming
AgentState->>Background_Task: StreamEvent / LlmChunk
Background_Task->>TUI_App: AgentUpdate mapping
end
end
TUI_App->>Terminal: draw frame
Terminal->>User: rendered UI
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CHANGELOG.md`:
- Line 15: The release note "Added `CHANGELOG.md`" is inaccurate because
CHANGELOG.md already contains a 0.1.0 section; update the entry in CHANGELOG.md
to a correct phrasing such as "Updated changelog (added 0.1.0 release notes)" or
"Documented 0.1.0 release in changelog" so it no longer claims the file was
newly added; locate the line containing the exact string "Added `CHANGELOG.md`"
and replace it with the corrected wording.
In `@src/acp/mod.rs`:
- Around line 120-127: The session branch that rebuilds the per-session client
uses s.config directly (the block constructing llm and calling
build_http_client/create_client) but s.config may only contain partial overrides
(provider_name/model) and miss provider defaults like api_base/api_key/timeout;
hydrate/merge the full provider configuration into s.config before constructing
the HTTP client and calling create_client so the reconstructed client uses the
correct endpoint/credentials; specifically, locate the branch that sets llm (the
comparison of s.config vs self.config) and replace the direct use of s.config
with a merged/filled config (pulling defaults for the provider and applying
session overrides) prior to calling crate::config::build_http_client and
crate::config::create_client.
In `@src/tui/app.rs`:
- Around line 289-307: In handle_model_picker_key, when Enter is pressed you
currently extract only the model from picker_items and send model.clone() via
switch_model_tx, which drops the provider; instead send the full (provider,
model) pair so the receiver can disambiguate backends. Locate the match arm in
handle_model_picker_key that reads self.picker_items.get(self.picker_selected)
and replace the send of model.clone() with sending the cloned tuple
(provider.clone(), model.clone()) (or otherwise construct the same pair type
expected by switch_model_tx); ensure switch_model_tx’s message type and any
receivers are updated accordingly and keep the existing transition to
Screen::Chat and picker_selected usage unchanged.
- Around line 599-652: open_session currently appends restored messages to the
existing chat; before loading conv.messages you must clear the current
transcript and reset view state so the restored session replaces the old one. In
open_session (the RagContext::new().and_then(...) success branch) call the
component's chat-reset routine (e.g. clear self.items or invoke an existing
reset/clear method) and reset any scroll/pinned state fields before the for msg
in &conv.messages loop, then proceed to push the restored ChatItem entries and
the "─── session restored" SystemInfo.
In `@src/tui/mod.rs`:
- Around line 111-181: The function enables raw mode/alternate screen/keyboard
enhancement (enable_raw_mode, EnterAlternateScreen,
PushKeyboardEnhancementFlags) but only restores state in the panic hook, so any
? error path leaks the terminal; fix by creating a RAII guard type (e.g.,
TerminalGuard) that captures kbd_enhanced and a handle to the terminal/backend
and implements Drop to PopKeyboardEnhancementFlags, LeaveAlternateScreen,
disable_raw_mode, and show_cursor; instantiate this guard immediately after the
successful
enable_raw_mode/EnterAlternateScreen/PushKeyboardEnhancementFlags/Terminal::new
sequence (and remove duplicate cleanup in the panic hook or have the hook call
into the guard) so cleanup runs on all exit paths including normal returns and
early errors, and reference Terminal, EventStream, and update_rx usage remains
unchanged.
In `@src/tui/render.rs`:
- Around line 136-173: The bubble border can overflow because word_wrap only
breaks on whitespace, so in user_bubble the computed content_w can exceed the
viewport; fix by post-processing the wrapped lines (the Vec produced from
word_wrap) inside user_bubble to hard-split any run/token longer than
content_max into chunks of at most content_max characters before computing
content_w and rendering; specifically iterate each line from word_wrap, for each
contiguous run longer than content_max slice it into width-sized pieces and push
those pieces into the new wrapped list, then use that new wrapped list for
content_w calculation and rendering (same approach should also be applied to the
other similar function/section noted in the comment).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 8a712a69-b850-4891-8f31-e0014a0258be
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (13)
CHANGELOG.mdCargo.tomlsrc/acp/mod.rssrc/client.rssrc/config/client.rssrc/config/resolve.rssrc/config/types.rssrc/tools/execute_command.rssrc/transport/ws.rssrc/tui/app.rssrc/tui/mod.rssrc/tui/render.rssrc/tui/types.rs
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/acp/mod.rs`:
- Around line 248-261: The code silently ignores a restored
conversation.meta.provider that is missing from self.app_config.providers and
thus can restore a session with wrong defaults; update the branch in acp::mod
(the block handling if let Some(provider_name) = &conversation.meta.provider) to
fail fast when self.app_config.providers.get(provider_name) returns None by
returning an error (or propagating a Result) instead of falling through to the
else model branch, so that session_config is not populated with base defaults
for a non-existent provider; reference the provider_name lookup and ensure the
error includes the missing provider_name and occurs before any assignment to
session_config fields.
In `@src/config/resolve.rs`:
- Around line 56-74: resolve_with_provider currently accepts any model string
even if the chosen provider defines an allowed models list; add validation after
validate_provider to check the provider's models whitelist (e.g. inspect
provider.models or similar field) and return a config Error if the requested
model is not contained in that list, so invalid (provider, model) pairs are
rejected before constructing AgentConfig; update resolve_with_provider to
perform this membership check and only proceed to build AgentConfig when the
model is allowed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 8c725d3d-edb4-4387-a98b-855f14d7bf8b
📒 Files selected for processing (6)
src/acp/mod.rssrc/client.rssrc/config/resolve.rssrc/tui/app.rssrc/tui/mod.rssrc/tui/render.rs
| if let Some(provider_name) = &conversation.meta.provider { | ||
| if let Some(provider_cfg) = self.app_config.providers.get(provider_name) { | ||
| session_config.provider_name = provider_name.clone(); | ||
| session_config.api_base = provider_cfg.api_base.clone(); | ||
| session_config.api_key = provider_cfg.resolve_api_key(); | ||
| session_config.timeout_secs = provider_cfg.timeout_secs.unwrap_or(120); | ||
| session_config.max_tokens = provider_cfg.max_tokens; | ||
| session_config.model = conversation | ||
| .meta | ||
| .model | ||
| .clone() | ||
| .unwrap_or_else(|| provider_cfg.default_model.clone()); | ||
| } | ||
| } else if let Some(model) = &conversation.meta.model { |
There was a problem hiding this comment.
Fail fast when restored provider is missing from config.
If history metadata has a provider that no longer exists in app_config, this branch silently falls back to base defaults. That can restore a session onto the wrong provider/model without any signal.
💡 Proposed fix
let mut session_config = self.config.clone();
if let Some(provider_name) = &conversation.meta.provider {
- if let Some(provider_cfg) = self.app_config.providers.get(provider_name) {
- session_config.provider_name = provider_name.clone();
- session_config.api_base = provider_cfg.api_base.clone();
- session_config.api_key = provider_cfg.resolve_api_key();
- session_config.timeout_secs = provider_cfg.timeout_secs.unwrap_or(120);
- session_config.max_tokens = provider_cfg.max_tokens;
- session_config.model = conversation
- .meta
- .model
- .clone()
- .unwrap_or_else(|| provider_cfg.default_model.clone());
- }
+ let provider_cfg = self.app_config.providers.get(provider_name).ok_or_else(|| {
+ Error::config(format!(
+ "Provider '{}' referenced by session '{}' not found in config",
+ provider_name, session_id
+ ))
+ })?;
+ session_config.provider_name = provider_name.clone();
+ session_config.api_base = provider_cfg.api_base.clone();
+ session_config.api_key = provider_cfg.resolve_api_key();
+ session_config.timeout_secs = provider_cfg.timeout_secs.unwrap_or(120);
+ session_config.max_tokens = provider_cfg.max_tokens;
+ session_config.model = conversation
+ .meta
+ .model
+ .clone()
+ .unwrap_or_else(|| provider_cfg.default_model.clone());
} else if let Some(model) = &conversation.meta.model {
session_config.model = model.clone();
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/acp/mod.rs` around lines 248 - 261, The code silently ignores a restored
conversation.meta.provider that is missing from self.app_config.providers and
thus can restore a session with wrong defaults; update the branch in acp::mod
(the block handling if let Some(provider_name) = &conversation.meta.provider) to
fail fast when self.app_config.providers.get(provider_name) returns None by
returning an error (or propagating a Result) instead of falling through to the
else model branch, so that session_config is not populated with base defaults
for a non-existent provider; reference the provider_name lookup and ensure the
error includes the missing provider_name and occurs before any assignment to
session_config fields.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
src/core/llm/retry.rs (1)
49-56: 💤 Low valueStreaming bypasses retry logic.
Unlike
send(),send_streaming()delegates directly without any retry handling. This is likely intentional since retrying mid-stream would cause duplicate/inconsistent chunks. Consider adding a brief comment to clarify this design decision.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/core/llm/retry.rs` around lines 49 - 56, send_streaming currently delegates straight to self.inner.send_streaming without retry handling, which can be confusing; add a short clarifying comment above the async fn send_streaming (in retry.rs, function send_streaming) stating that streaming intentionally bypasses the retry logic because retrying mid-stream would produce duplicate or inconsistent chunks and thus retries are disabled for streaming paths. Ensure the comment mentions that send() retains retry behavior while send_streaming does not.src/core/llm/gemini.rs (1)
372-372: 💤 Low valueAvoid unnecessary String allocation.
&"sse".to_string()allocates a heap String. Use a static reference directly.♻️ Suggested fix
- .query(&[("key", &self.api_key), ("alt", &"sse".to_string())]) + .query(&[("key", &self.api_key), ("alt", "sse")])🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/core/llm/gemini.rs` at line 372, The .query call is creating an unnecessary heap allocation by using &"sse".to_string(); replace that allocation with a static &str literal (e.g., use "sse" or &"sse") in the .query argument so the tuple becomes ("alt", "sse") (located in the .query(...) call that includes ("key", &self.api_key)).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/core/agent.rs`:
- Around line 77-112: The loop can break when chunk_rx.recv() returns None
before the pinned streaming future (choice_fut) completes, leaving maybe_choice
None and causing a spurious "stream ended prematurely" error; to fix, after the
loop check if maybe_choice.is_none() and if so await the pinned choice_fut to
obtain its Result<Choice> (then set maybe_choice accordingly) before
unwrapping/propagating the value—adjust the logic around the existing
call_llm_streaming, choice_fut, maybe_choice, chunk_rx, LlmChunk and StreamEvent
handling to ensure the streaming future is awaited when the channel closes.
In `@src/core/llm/gemini.rs`:
- Around line 427-431: Run rustfmt (cargo fmt) to fix the formatting around the
let-chain in the if-condition that checks part.text and !text.is_empty(); ensure
the condition and block for the code that calls text_buf.push_str(&text) and
chunk_tx.send(LlmChunk::Text(text)) are formatted per rustfmt (or rewrite the
conditional to a rustfmt-friendly style, e.g., split into a simple if let
Some(text) = part.text { if !text.is_empty() { ... } } so cargo fmt --check
passes).
In `@src/core/llm/openai.rs`:
- Around line 174-183: The let-chain conditions using
delta["reasoning_content"].as_str() and delta["content"].as_str() are not
formatted to match cargo fmt; run `cargo fmt` (or apply rustfmt) to reformat the
if-let chains in openai.rs so the conditions and block indentation match the
project's style, specifically in the blocks that reference delta, chunk_tx,
text_buf and LlmChunk (the `if let Some(reasoning) =
delta["reasoning_content"].as_str() && !reasoning.is_empty()` and `if let
Some(content) = delta["content"].as_str() && !content.is_empty()` statements);
commit the formatted changes so `cargo fmt --check` passes.
In `@src/tui/mod.rs`:
- Around line 200-204: The formatting of the boolean assignment for is_thinking
(the chained calls on t.meta -> as_ref -> and_then -> get -> and_then -> as_str)
does not match rustfmt’s expectations; run cargo fmt to reformat src/tui/mod.rs
(or manually reflow the chained method calls on the is_thinking assignment to
match rustfmt output) so the expression is formatted correctly and the CI check
passes.
---
Nitpick comments:
In `@src/core/llm/gemini.rs`:
- Line 372: The .query call is creating an unnecessary heap allocation by using
&"sse".to_string(); replace that allocation with a static &str literal (e.g.,
use "sse" or &"sse") in the .query argument so the tuple becomes ("alt", "sse")
(located in the .query(...) call that includes ("key", &self.api_key)).
In `@src/core/llm/retry.rs`:
- Around line 49-56: send_streaming currently delegates straight to
self.inner.send_streaming without retry handling, which can be confusing; add a
short clarifying comment above the async fn send_streaming (in retry.rs,
function send_streaming) stating that streaming intentionally bypasses the retry
logic because retrying mid-stream would produce duplicate or inconsistent chunks
and thus retries are disabled for streaming paths. Ensure the comment mentions
that send() retains retry behavior while send_streaming does not.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 1e651fa5-1a76-4751-8328-b714b86cc0c8
📒 Files selected for processing (15)
src/acp/mod.rssrc/config/resolve.rssrc/core/agent.rssrc/core/llm/anthropic.rssrc/core/llm/gemini.rssrc/core/llm/mod.rssrc/core/llm/openai.rssrc/core/llm/openai_compatible.rssrc/core/llm/retry.rssrc/core/models.rssrc/mcp/client.rssrc/tui/app.rssrc/tui/mod.rssrc/tui/render.rssrc/tui/types.rs
🚧 Files skipped from review as they are similar to previous changes (4)
- src/config/resolve.rs
- src/tui/types.rs
- src/tui/app.rs
- src/tui/render.rs
| let choice = if callback.is_some() { | ||
| let (chunk_tx, mut chunk_rx) = mpsc::unbounded_channel::<LlmChunk>(); | ||
| let choice_fut = call_llm_streaming(llm, messages, &tools, prompt_builder, chunk_tx); | ||
| tokio::pin!(choice_fut); | ||
|
|
||
| let mut maybe_choice: Option<Result<Choice>> = None; | ||
| loop { | ||
| tokio::select! { | ||
| result = &mut choice_fut, if maybe_choice.is_none() => { | ||
| maybe_choice = Some(result); | ||
| } | ||
| maybe_chunk = chunk_rx.recv() => { | ||
| match maybe_chunk { | ||
| Some(LlmChunk::Text(text)) => { | ||
| if let Some(cb) = callback.as_mut() { | ||
| cb(StreamEvent::LlmResponse { content: text }); | ||
| } | ||
| } | ||
| Some(LlmChunk::Thinking(thought)) => { | ||
| if let Some(cb) = callback.as_mut() { | ||
| cb(StreamEvent::ThinkingContent { content: thought }); | ||
| } | ||
| } | ||
| None => break, | ||
| } | ||
| } | ||
| } | ||
| } | ||
| maybe_choice.unwrap_or_else(|| { | ||
| Err(crate::error::Error::Other( | ||
| "stream ended prematurely".into(), | ||
| )) | ||
| })? | ||
| } else { | ||
| call_llm(llm, messages, &tools, prompt_builder).await? | ||
| }; |
There was a problem hiding this comment.
Race between channel close and choice completion can spuriously error.
When send_streaming completes, it drops chunk_tx and returns Choice simultaneously. If tokio::select! picks recv() → None before the choice_fut arm in that final poll cycle, the loop breaks with maybe_choice still None, yielding a spurious "stream ended prematurely" error even though the choice is ready.
After the loop, await the pinned future if maybe_choice is None:
Proposed fix
- maybe_choice.unwrap_or_else(|| {
- Err(crate::error::Error::Other(
- "stream ended prematurely".into(),
- ))
- })?
+ match maybe_choice {
+ Some(result) => result?,
+ None => choice_fut.await?,
+ }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/core/agent.rs` around lines 77 - 112, The loop can break when
chunk_rx.recv() returns None before the pinned streaming future (choice_fut)
completes, leaving maybe_choice None and causing a spurious "stream ended
prematurely" error; to fix, after the loop check if maybe_choice.is_none() and
if so await the pinned choice_fut to obtain its Result<Choice> (then set
maybe_choice accordingly) before unwrapping/propagating the value—adjust the
logic around the existing call_llm_streaming, choice_fut, maybe_choice,
chunk_rx, LlmChunk and StreamEvent handling to ensure the streaming future is
awaited when the channel closes.
| if let Some(text) = part.text | ||
| && !text.is_empty() { | ||
| text_buf.push_str(&text); | ||
| let _ = chunk_tx.send(LlmChunk::Text(text)); | ||
| } |
There was a problem hiding this comment.
Fix formatting to pass CI.
The pipeline is failing due to cargo fmt --check differences around the let-chain condition. Run cargo fmt to apply the expected formatting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/core/llm/gemini.rs` around lines 427 - 431, Run rustfmt (cargo fmt) to
fix the formatting around the let-chain in the if-condition that checks
part.text and !text.is_empty(); ensure the condition and block for the code that
calls text_buf.push_str(&text) and chunk_tx.send(LlmChunk::Text(text)) are
formatted per rustfmt (or rewrite the conditional to a rustfmt-friendly style,
e.g., split into a simple if let Some(text) = part.text { if !text.is_empty() {
... } } so cargo fmt --check passes).
| if let Some(reasoning) = delta["reasoning_content"].as_str() | ||
| && !reasoning.is_empty() { | ||
| let _ = chunk_tx.send(LlmChunk::Thinking(reasoning.to_string())); | ||
| } | ||
|
|
||
| if let Some(content) = delta["content"].as_str() | ||
| && !content.is_empty() { | ||
| text_buf.push_str(content); | ||
| let _ = chunk_tx.send(LlmChunk::Text(content.to_string())); | ||
| } |
There was a problem hiding this comment.
Fix formatting to pass CI.
The pipeline is failing due to cargo fmt --check differences around the let-chain conditions. Run cargo fmt to apply the expected formatting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/core/llm/openai.rs` around lines 174 - 183, The let-chain conditions
using delta["reasoning_content"].as_str() and delta["content"].as_str() are not
formatted to match cargo fmt; run `cargo fmt` (or apply rustfmt) to reformat the
if-let chains in openai.rs so the conditions and block indentation match the
project's style, specifically in the blocks that reference delta, chunk_tx,
text_buf and LlmChunk (the `if let Some(reasoning) =
delta["reasoning_content"].as_str() && !reasoning.is_empty()` and `if let
Some(content) = delta["content"].as_str() && !content.is_empty()` statements);
commit the formatted changes so `cargo fmt --check` passes.
| let is_thinking = t | ||
| .meta | ||
| .as_ref() | ||
| .and_then(|m| m.get("kind")) | ||
| .and_then(|v| v.as_str()) == Some("thinking"); |
There was a problem hiding this comment.
Fix formatting to pass CI.
The pipeline is failing due to cargo fmt --check detecting formatting differences on this chain. Run cargo fmt to apply the expected formatting.
🧰 Tools
🪛 GitHub Actions: CI / Check
[error] 201-201: cargo fmt --check failed due to Rust formatting differences in this file (line break/indentation in .and_then(|v| v.as_str()) == Some("thinking")). Run cargo fmt to apply the formatting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/tui/mod.rs` around lines 200 - 204, The formatting of the boolean
assignment for is_thinking (the chained calls on t.meta -> as_ref -> and_then ->
get -> and_then -> as_str) does not match rustfmt’s expectations; run cargo fmt
to reformat src/tui/mod.rs (or manually reflow the chained method calls on the
is_thinking assignment to match rustfmt output) so the expression is formatted
correctly and the CI check passes.
Summary by CodeRabbit
New Features
Bug Fixes
Configuration
Chores
Documentation