Detailed changes
@@ -1106,6 +1106,10 @@
//
// Default: true
"expand_terminal_card": true,
+ // How thinking blocks should be displayed by default in the agent panel.
+ //
+ // Default: automatic
+ "thinking_display": "automatic",
// Whether clicking the stop button on a running terminal tool should also cancel the agent's generation.
// Note that this only applies to the stop button, not to ctrl+c inside the terminal.
//
@@ -597,6 +597,7 @@ mod tests {
show_turn_stats: false,
new_thread_location: Default::default(),
sidebar_side: Default::default(),
+ thinking_display: Default::default(),
}
}
@@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize};
use settings::{
DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection,
NewThreadLocation, NotifyWhenAgentWaiting, RegisterSetting, Settings, SidebarDockPosition,
- SidebarSide, ToolPermissionMode,
+ SidebarSide, ThinkingBlockDisplay, ToolPermissionMode,
};
pub use crate::agent_profile::*;
@@ -48,6 +48,7 @@ pub struct AgentSettings {
pub enable_feedback: bool,
pub expand_edit_card: bool,
pub expand_terminal_card: bool,
+ pub thinking_display: ThinkingBlockDisplay,
pub cancel_generation_on_terminal_stop: bool,
pub use_modifier_to_send: bool,
pub message_editor_min_lines: usize,
@@ -448,6 +449,7 @@ impl Settings for AgentSettings {
enable_feedback: agent.enable_feedback.unwrap(),
expand_edit_card: agent.expand_edit_card.unwrap(),
expand_terminal_card: agent.expand_terminal_card.unwrap(),
+ thinking_display: agent.thinking_display.unwrap(),
cancel_generation_on_terminal_stop: agent.cancel_generation_on_terminal_stop.unwrap(),
use_modifier_to_send: agent.use_modifier_to_send.unwrap(),
message_editor_min_lines: agent.message_editor_min_lines.unwrap(),
@@ -673,6 +673,7 @@ mod tests {
show_turn_stats: false,
new_thread_location: Default::default(),
sidebar_side: Default::default(),
+ thinking_display: Default::default(),
};
cx.update(|cx| {
@@ -43,7 +43,7 @@ use prompt_store::{PromptId, PromptStore};
use crate::DEFAULT_THREAD_TITLE;
use crate::message_editor::SessionCapabilities;
use rope::Point;
-use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
+use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore, ThinkingBlockDisplay};
use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
@@ -253,6 +253,7 @@ pub struct ThreadView {
pub expanded_tool_call_raw_inputs: HashSet<agent_client_protocol::ToolCallId>,
pub expanded_thinking_blocks: HashSet<(usize, usize)>,
auto_expanded_thinking_block: Option<(usize, usize)>,
+ user_toggled_thinking_blocks: HashSet<(usize, usize)>,
pub subagent_scroll_handles: RefCell<HashMap<agent_client_protocol::SessionId, ScrollHandle>>,
pub edits_expanded: bool,
pub plan_expanded: bool,
@@ -495,6 +496,7 @@ impl ThreadView {
expanded_tool_call_raw_inputs: HashSet::default(),
expanded_thinking_blocks: HashSet::default(),
auto_expanded_thinking_block: None,
+ user_toggled_thinking_blocks: HashSet::default(),
subagent_scroll_handles: RefCell::new(HashMap::default()),
edits_expanded: false,
plan_expanded: false,
@@ -5155,9 +5157,13 @@ impl ThreadView {
.into_any_element()
}
- /// If the last entry's last chunk is a streaming thought block, auto-expand it.
- /// Also collapses the previously auto-expanded block when a new one starts.
pub(crate) fn auto_expand_streaming_thought(&mut self, cx: &mut Context<Self>) {
+ // Only auto-expand thinking blocks in Automatic mode.
+ // AlwaysExpanded shows them open by default; AlwaysCollapsed keeps them closed.
+ if AgentSettings::get_global(cx).thinking_display != ThinkingBlockDisplay::Automatic {
+ return;
+ }
+
let key = {
let thread = self.thread.read(cx);
if thread.status() != ThreadStatus::Generating {
@@ -5178,30 +5184,59 @@ impl ThreadView {
if let Some(key) = key {
if self.auto_expanded_thinking_block != Some(key) {
- if let Some(old_key) = self.auto_expanded_thinking_block.replace(key) {
- self.expanded_thinking_blocks.remove(&old_key);
- }
+ self.auto_expanded_thinking_block = Some(key);
self.expanded_thinking_blocks.insert(key);
cx.notify();
}
} else if self.auto_expanded_thinking_block.is_some() {
- // The last chunk is no longer a thought (model transitioned to responding),
- // so collapse the previously auto-expanded block.
- self.collapse_auto_expanded_thinking_block();
+ self.auto_expanded_thinking_block = None;
cx.notify();
}
}
- fn collapse_auto_expanded_thinking_block(&mut self) {
- if let Some(key) = self.auto_expanded_thinking_block.take() {
- self.expanded_thinking_blocks.remove(&key);
- }
- }
-
pub(crate) fn clear_auto_expand_tracking(&mut self) {
self.auto_expanded_thinking_block = None;
}
+ fn toggle_thinking_block_expansion(&mut self, key: (usize, usize), cx: &mut Context<Self>) {
+ let thinking_display = AgentSettings::get_global(cx).thinking_display;
+
+ match thinking_display {
+ ThinkingBlockDisplay::Automatic => {
+ let is_user_expanded = self.user_toggled_thinking_blocks.contains(&key);
+ let is_in_expanded_set = self.expanded_thinking_blocks.contains(&key);
+
+ if is_user_expanded {
+ self.user_toggled_thinking_blocks.remove(&key);
+ self.expanded_thinking_blocks.remove(&key);
+ } else if is_in_expanded_set {
+ self.user_toggled_thinking_blocks.insert(key);
+ } else {
+ self.expanded_thinking_blocks.insert(key);
+ self.user_toggled_thinking_blocks.insert(key);
+ }
+ }
+ ThinkingBlockDisplay::AlwaysExpanded => {
+ if self.user_toggled_thinking_blocks.contains(&key) {
+ self.user_toggled_thinking_blocks.remove(&key);
+ } else {
+ self.user_toggled_thinking_blocks.insert(key);
+ }
+ }
+ ThinkingBlockDisplay::AlwaysCollapsed => {
+ if self.user_toggled_thinking_blocks.contains(&key) {
+ self.user_toggled_thinking_blocks.remove(&key);
+ self.expanded_thinking_blocks.remove(&key);
+ } else {
+ self.expanded_thinking_blocks.insert(key);
+ self.user_toggled_thinking_blocks.insert(key);
+ }
+ }
+ }
+
+ cx.notify();
+ }
+
fn render_thinking_block(
&self,
entry_ix: usize,
@@ -5215,7 +5250,21 @@ impl ThreadView {
let key = (entry_ix, chunk_ix);
- let is_open = self.expanded_thinking_blocks.contains(&key);
+ let thinking_display = AgentSettings::get_global(cx).thinking_display;
+ let is_user_toggled = self.user_toggled_thinking_blocks.contains(&key);
+ let is_in_expanded_set = self.expanded_thinking_blocks.contains(&key);
+
+ let (is_open, is_constrained) = match thinking_display {
+ ThinkingBlockDisplay::Automatic => {
+ let is_open = is_user_toggled || is_in_expanded_set;
+ let is_constrained = is_in_expanded_set && !is_user_toggled;
+ (is_open, is_constrained)
+ }
+ ThinkingBlockDisplay::AlwaysExpanded => (!is_user_toggled, false),
+ ThinkingBlockDisplay::AlwaysCollapsed => (is_user_toggled, false),
+ };
+
+ let should_auto_scroll = self.auto_expanded_thinking_block == Some(key);
let scroll_handle = self
.entry_view_state
@@ -5223,6 +5272,14 @@ impl ThreadView {
.entry(entry_ix)
.and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix));
+ if should_auto_scroll {
+ if let Some(ref handle) = scroll_handle {
+ handle.scroll_to_bottom();
+ }
+ }
+
+ let panel_bg = cx.theme().colors().panel_background;
+
v_flex()
.gap_1()
.child(
@@ -5255,42 +5312,51 @@ impl ThreadView {
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.visible_on_hover(&card_header_id)
- .on_click(cx.listener({
- move |this, _event, _window, cx| {
- if is_open {
- this.expanded_thinking_blocks.remove(&key);
- } else {
- this.expanded_thinking_blocks.insert(key);
- }
- cx.notify();
- }
- })),
+ .on_click(cx.listener(
+ move |this, _event: &ClickEvent, _window, cx| {
+ this.toggle_thinking_block_expansion(key, cx);
+ },
+ )),
)
- .on_click(cx.listener(move |this, _event, _window, cx| {
- if is_open {
- this.expanded_thinking_blocks.remove(&key);
- } else {
- this.expanded_thinking_blocks.insert(key);
- }
- cx.notify();
+ .on_click(cx.listener(move |this, _event: &ClickEvent, _window, cx| {
+ this.toggle_thinking_block_expansion(key, cx);
})),
)
.when(is_open, |this| {
this.child(
div()
- .id(("thinking-content", chunk_ix))
- .ml_1p5()
- .pl_3p5()
- .border_l_1()
- .border_color(self.tool_card_border_color(cx))
- .when_some(scroll_handle, |this, scroll_handle| {
- this.track_scroll(&scroll_handle)
- })
- .overflow_hidden()
- .child(self.render_markdown(
- chunk,
- MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
- )),
+ .when(is_constrained, |this| this.relative())
+ .child(
+ div()
+ .id(("thinking-content", chunk_ix))
+ .ml_1p5()
+ .pl_3p5()
+ .border_l_1()
+ .border_color(self.tool_card_border_color(cx))
+ .when(is_constrained, |this| this.max_h_64())
+ .when_some(scroll_handle, |this, scroll_handle| {
+ this.track_scroll(&scroll_handle)
+ })
+ .overflow_hidden()
+ .child(self.render_markdown(
+ chunk,
+ MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
+ )),
+ )
+ .when(is_constrained, |this| {
+ this.child(
+ div()
+ .absolute()
+ .inset_0()
+ .size_full()
+ .bg(linear_gradient(
+ 180.,
+ linear_color_stop(panel_bg.opacity(0.8), 0.),
+ linear_color_stop(panel_bg.opacity(0.), 0.1),
+ ))
+ .block_mouse_except_scroll(),
+ )
+ }),
)
})
.into_any_element()
@@ -66,6 +66,34 @@ pub enum SidebarSide {
Right,
}
+/// How thinking blocks should be displayed by default in the agent panel.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Default,
+ PartialEq,
+ Eq,
+ Serialize,
+ Deserialize,
+ JsonSchema,
+ MergeFrom,
+ strum::VariantArray,
+ strum::VariantNames,
+)]
+#[serde(rename_all = "snake_case")]
+pub enum ThinkingBlockDisplay {
+ /// Thinking blocks auto-expand with a height constraint during streaming,
+ /// then remain in their constrained state when complete. Users can click
+ /// to fully expand or collapse.
+ #[default]
+ Automatic,
+ /// Thinking blocks are always fully expanded by default (no height constraint).
+ AlwaysExpanded,
+ /// Thinking blocks are always collapsed by default.
+ AlwaysCollapsed,
+}
+
#[with_fallible_options]
#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)]
pub struct AgentSettingsContent {
@@ -159,6 +187,10 @@ pub struct AgentSettingsContent {
///
/// Default: true
pub expand_terminal_card: Option<bool>,
+ /// How thinking blocks should be displayed by default in the agent panel.
+ ///
+ /// Default: automatic
+ pub thinking_display: Option<ThinkingBlockDisplay>,
/// Whether clicking the stop button on a running terminal tool should also cancel the agent's generation.
/// Note that this only applies to the stop button, not to ctrl+c inside the terminal.
///
@@ -7323,6 +7323,28 @@ fn ai_page(cx: &App) -> SettingsPage {
metadata: None,
files: USER,
}),
+ SettingsPageItem::SettingItem(SettingItem {
+ title: "Thinking Display",
+ description: "How thinking blocks should be displayed by default. 'Automatic' auto-expands with a height constraint during streaming. 'Always Expanded' shows full content. 'Always Collapsed' keeps them collapsed.",
+ field: Box::new(SettingField {
+ json_path: Some("agent.thinking_display"),
+ pick: |settings_content| {
+ settings_content
+ .agent
+ .as_ref()?
+ .thinking_display
+ .as_ref()
+ },
+ write: |settings_content, value| {
+ settings_content
+ .agent
+ .get_or_insert_default()
+ .thinking_display = value;
+ },
+ }),
+ metadata: None,
+ files: USER,
+ }),
SettingsPageItem::SettingItem(SettingItem {
title: "Cancel Generation On Terminal Stop",
description: "Whether clicking the stop button on a running terminal tool should also cancel the agent's generation. Note that this only applies to the stop button, not to ctrl+c inside the terminal.",
@@ -524,6 +524,7 @@ fn init_renderers(cx: &mut App) {
.add_basic_renderer::<settings::SteppingGranularity>(render_dropdown)
.add_basic_renderer::<settings::NotifyWhenAgentWaiting>(render_dropdown)
.add_basic_renderer::<settings::NewThreadLocation>(render_dropdown)
+ .add_basic_renderer::<settings::ThinkingBlockDisplay>(render_dropdown)
.add_basic_renderer::<settings::ImageFileSizeUnit>(render_dropdown)
.add_basic_renderer::<settings::StatusStyle>(render_dropdown)
.add_basic_renderer::<settings::EncodingDisplayOptions>(render_dropdown)