Vectrum
Sign In

Rest the composer when the conversation scrolls up, and a Scroll to end pill

Scrolling up through a thread now rests the composer on one line (when
Settings → Collapse composer on scroll is on, as it is by default in T3
Code); the scroll used to be swallowed by the list. The resting composer
shows its line in full, and the jump back down is T3's "Scroll to end"
pill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JjohannSocommitted 1 parent ef6a6cccommit25a21cd

Showing 1 changed file with 17 additions and 9 deletions.

+17−9src/ui/chat.rsView File
@@ -204,6 +204,8 @@ pub struct ChatView {
204204 folded: std::cell::Cell<(bool, bool)>,
205205 /// The composer rests on one line while the conversation scrolls.
206206 collapsed: bool,
207+ /// Whether the conversation was scrolled away from its end last frame.
208+ was_scrolled_up: bool,
207209 _subscriptions: Vec<Subscription>,
208210 _ticker: Task<()>,
209211 }
@@ -331,6 +333,7 @@ impl ChatView {
331333 setup_details: false,
332334 folded: Default::default(),
333335 collapsed: false,
336+ was_scrolled_up: false,
334337 _subscriptions: subscriptions,
335338 _ticker: ticker,
336339 };
@@ -1414,18 +1417,12 @@ impl ChatView {
14141417 .flex()
14151418 .flex_col()
14161419 // Scrolling rests the composer, when set to.
1417- .on_scroll_wheel(cx.listener(|this, _, _, cx| {
1418- let empty = this.composer.read(cx).value().is_empty() && this.attachments.is_empty();
1419- if !this.collapsed && empty && settings::get(|settings| settings.collapse_composer) {
1420- this.collapsed = true;
1421- cx.notify();
1422- }
1423- }))
14241420 .child(
14251421 MessageScroller::new("transcript", self.scroller.clone(), move |ix, window, cx| {
14261422 timeline::render_item(&session, &scroller, &chat, ix, window, cx)
14271423 })
14281424 .with_jump_button_label("Scroll to end")
1425+ .with_jump_button_renderer(|button| button.icon(IconName::ChevronDown).label("Scroll to end"))
14291426 .flex_1()
14301427 .w_full()
14311428 .with_row_style(StyleRefinement::default().pb_0())
@@ -1686,10 +1683,11 @@ impl ChatView {
16861683 .size_full()
16871684 };
16881685
1686+ let rows = if collapsed { (1, 1) } else if approval { (2, 9) } else { (3, 9) };
16891687 let textarea = div()
16901688 .text_size(px(14.))
16911689 .text_color(p.ink)
1692- .child(MarkdownInput::new(&self.composer).rows(if approval { 2 } else { 3 }, 9));
1690+ .child(MarkdownInput::new(&self.composer).rows(rows.0, rows.1));
16931691 let card = if collapsed {
16941692 // Resting: one line, a click (or typing) away from the full composer.
16951693 h_flex()
@@ -1709,7 +1707,7 @@ impl ChatView {
17091707 this.focus_composer(window, cx);
17101708 }))
17111709 .child(measure)
1712- .child(textarea.flex_1().min_w_0().h(px(22.)).overflow_hidden())
1710+ .child(textarea.flex_1().min_w_0().overflow_hidden())
17131711 .child(self.render_actions(start, cx))
17141712 .into_any_element()
17151713 } else {
@@ -1871,6 +1869,16 @@ impl ChatView {
18711869 impl Render for ChatView {
18721870 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
18731871 let is_empty = self.session.read(cx).items.is_empty();
1872+ // Scrolling up through the conversation rests the composer, when set
1873+ // to, as in T3 Code; a click or typing wakes it.
1874+ let scrolled_up = self.scroller.read(cx).is_scrolled_up();
1875+ if scrolled_up && !self.was_scrolled_up {
1876+ let empty = self.composer.read(cx).value().is_empty() && self.attachments.is_empty();
1877+ if empty && settings::get(|settings| settings.collapse_composer) {
1878+ self.collapsed = true;
1879+ }
1880+ }
1881+ self.was_scrolled_up = scrolled_up;
18741882 let on_diff = {
18751883 let chat = cx.entity().downgrade();
18761884 move |action: DiffAction, window: &mut Window, cx: &mut App| {