Skip to main content

gpui_cn/composer/
prompt.rs

1use std::path::PathBuf;
2
3use gpui_kit::{
4    AnyElement, App, AppContext as _, Context, ElementId, Entity, EventEmitter, ExternalPaths,
5    Focusable as _, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce,
6    SharedString, StyleRefinement, Styled, Subscription, Window,
7    assets::IconName,
8    base::{
9        Disableable, StyledExt as _, TestSupportExt as _,
10        input::{InputEvent, TextareaState},
11    },
12    div,
13    prelude::FluentBuilder as _,
14    px,
15};
16
17use crate::{
18    ActiveTheme as _, Attachment, AttachmentStatus, AttachmentStrip, Button, ButtonSize,
19    EffortMenu, ModelPicker, ModelPickerEvent, ModelPickerState, PermissionEvent, PermissionMenu,
20    PermissionState, StatusSelectEvent, StatusSelectState, Textarea,
21    collapse::{self, Measured, Need},
22};
23
24/// The most lines the prompt grows to before it scrolls.
25const DEFAULT_MAX_LINES: usize = 10;
26
27/// What a [`ComposerState`] reports.
28#[derive(Clone)]
29#[non_exhaustive]
30pub enum ComposerEvent {
31    /// The prompt was submitted: Enter in the text, or the send button.
32    Submit {
33        /// The prompt's text.
34        text: SharedString,
35        /// The attachments sent with it.
36        attachments: Vec<Attachment>,
37    },
38    /// An attachment was added or removed, or its progress changed.
39    AttachmentsChanged,
40    /// The permission mode changed. The payload is its id.
41    PermissionChanged(SharedString),
42    /// The "Learn more" row of the permission menu was chosen.
43    PermissionLearnMore,
44    /// A status item was changed. `item` is the id the application gave
45    /// the state to [`ComposerState::watch_status`], and `value` the option.
46    StatusChanged {
47        /// The status item.
48        item: SharedString,
49        /// The id of the chosen option.
50        value: SharedString,
51    },
52    /// A model was chosen. The payload is its id.
53    ModelChanged(SharedString),
54    /// The effort level changed, from `0` to `5`.
55    EffortChanged(u8),
56    /// The add button was pressed.
57    AddClicked,
58    /// The microphone button was pressed.
59    MicClicked,
60    /// Files were dropped on the composer.
61    FilesDropped(Vec<PathBuf>),
62}
63
64/// The text, the attachments, the permission mode, and the model of a
65/// [`Composer`].
66///
67/// Owned by the view that shows the composer, which observes it so a change
68/// renders. It owns the prompt's `TextareaState` and holds the permission
69/// and model states, and reports what the user does as a
70/// [`ComposerEvent`]. The application changes what it shows through the
71/// methods here: add an attachment, move its upload progress, clear the
72/// text.
73///
74/// Enter submits, and Shift+Enter starts a new line. A prompt can be
75/// submitted when it has text or a ready attachment, no attachment is
76/// still uploading, and the composer is not disabled. A submit clears the
77/// text and the attachments unless
78/// [`set_clear_on_submit`](Self::set_clear_on_submit) turned that off.
79pub struct ComposerState {
80    input: Entity<TextareaState>,
81    attachments: Vec<Attachment>,
82    permission: Entity<PermissionState>,
83    models: Entity<ModelPickerState>,
84    disabled: bool,
85    max_lines: usize,
86    clear_on_submit: bool,
87    placeholder: SharedString,
88    _subscriptions: Vec<Subscription>,
89    _status: Vec<Subscription>,
90}
91
92impl EventEmitter<ComposerEvent> for ComposerState {}
93
94/// The prompt's focus, which holds the keyboard for the composer.
95impl gpui_kit::Focusable for ComposerState {
96    fn focus_handle(&self, cx: &App) -> gpui_kit::FocusHandle {
97        self.input.read(cx).focus_handle(cx)
98    }
99}
100
101impl ComposerState {
102    /// A composer with the default permission modes, an empty model
103    /// catalog, and the placeholder "Hi, what do you need today?".
104    pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
105        let placeholder = SharedString::from("Hi, what do you need today?");
106        let input = cx.new(|cx| {
107            TextareaState::new(window, cx)
108                .placeholder(placeholder.clone())
109                .auto_grow(1, DEFAULT_MAX_LINES)
110                .submit_on_enter(true)
111        });
112        let permission = cx.new(PermissionState::new);
113        let models = cx.new(|cx| ModelPickerState::new([], window, cx));
114        let mut state = Self {
115            input,
116            attachments: Vec::new(),
117            permission,
118            models,
119            disabled: false,
120            max_lines: DEFAULT_MAX_LINES,
121            clear_on_submit: true,
122            placeholder,
123            _subscriptions: Vec::new(),
124            _status: Vec::new(),
125        };
126        state.watch(window, cx);
127        state
128    }
129
130    /// Uses `models` for the model picker, in place of the empty one.
131    pub fn with_models(
132        mut self,
133        models: Entity<ModelPickerState>,
134        window: &mut Window,
135        cx: &mut Context<Self>,
136    ) -> Self {
137        self.models = models;
138        self.watch(window, cx);
139        self
140    }
141
142    /// Uses `permission` for the permission menu, in place of the default
143    /// modes.
144    pub fn with_permission(
145        mut self,
146        permission: Entity<PermissionState>,
147        window: &mut Window,
148        cx: &mut Context<Self>,
149    ) -> Self {
150        self.permission = permission;
151        self.watch(window, cx);
152        self
153    }
154
155    /// Subscribes to the entities this state owns or holds, replacing any
156    /// earlier subscriptions, so a change in one renders the composer and
157    /// is reported as a composer event.
158    fn watch(&mut self, window: &mut Window, cx: &mut Context<Self>) {
159        self._subscriptions = vec![
160            cx.subscribe_in(
161                &self.input,
162                window,
163                |this, _, event: &InputEvent, window, cx| match event {
164                    InputEvent::PressEnter { shift: false, .. } => {
165                        this.submit(window, cx);
166                    }
167                    InputEvent::Change => cx.notify(),
168                    _ => {}
169                },
170            ),
171            cx.subscribe(&self.permission, |_, _, event: &PermissionEvent, cx| {
172                match event {
173                    PermissionEvent::Changed(id) => {
174                        cx.emit(ComposerEvent::PermissionChanged(id.clone()))
175                    }
176                    PermissionEvent::LearnMore => cx.emit(ComposerEvent::PermissionLearnMore),
177                }
178                cx.notify();
179            }),
180            cx.subscribe(&self.models, |_, _, event: &ModelPickerEvent, cx| {
181                match event {
182                    ModelPickerEvent::Selected(id) => {
183                        cx.emit(ComposerEvent::ModelChanged(id.clone()))
184                    }
185                    ModelPickerEvent::EffortChanged(level) => {
186                        cx.emit(ComposerEvent::EffortChanged(*level))
187                    }
188                    _ => {}
189                }
190                cx.notify();
191            }),
192        ];
193    }
194
195    /// Forwards the changes of a status item as
196    /// [`ComposerEvent::StatusChanged`] under `item`, and renders the
197    /// composer when it changes.
198    pub fn watch_status(
199        &mut self,
200        item: impl Into<SharedString>,
201        state: &Entity<StatusSelectState>,
202        cx: &mut Context<Self>,
203    ) {
204        let item = item.into();
205        self._status.push(
206            cx.subscribe(state, move |_, _, event: &StatusSelectEvent, cx| {
207                let StatusSelectEvent::Changed(value) = event;
208                cx.emit(ComposerEvent::StatusChanged {
209                    item: item.clone(),
210                    value: value.clone(),
211                });
212                cx.notify();
213            }),
214        );
215    }
216
217    /// The prompt's text state.
218    pub fn input(&self) -> &Entity<TextareaState> {
219        &self.input
220    }
221
222    /// The permission state.
223    pub fn permission(&self) -> &Entity<PermissionState> {
224        &self.permission
225    }
226
227    /// The model picker state.
228    pub fn models(&self) -> &Entity<ModelPickerState> {
229        &self.models
230    }
231
232    /// The prompt's text.
233    pub fn text(&self, cx: &App) -> SharedString {
234        self.input.read(cx).value()
235    }
236
237    /// Replaces the prompt's text.
238    pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
239        let text = SharedString::from(text.to_string());
240        self.input
241            .update(cx, |input, cx| input.set_value(text, window, cx));
242        cx.notify();
243    }
244
245    /// Empties the prompt's text.
246    pub fn clear_text(&mut self, window: &mut Window, cx: &mut Context<Self>) {
247        self.set_text("", window, cx);
248    }
249
250    /// Puts the caret in the prompt.
251    pub fn focus(&self, window: &mut Window, cx: &mut App) {
252        self.input.update(cx, |input, cx| input.focus(window, cx));
253    }
254
255    /// The placeholder.
256    pub fn placeholder(&self) -> &SharedString {
257        &self.placeholder
258    }
259
260    /// Replaces the placeholder shown while the prompt is empty.
261    pub fn set_placeholder(
262        &mut self,
263        placeholder: impl Into<SharedString>,
264        window: &mut Window,
265        cx: &mut Context<Self>,
266    ) {
267        let placeholder = placeholder.into();
268        if placeholder == self.placeholder {
269            return;
270        }
271        self.placeholder = placeholder.clone();
272        self.input.update(cx, |input, cx| {
273            input.set_placeholder(placeholder, window, cx)
274        });
275    }
276
277    /// The most lines the prompt grows to before it scrolls.
278    pub fn max_lines(&self) -> usize {
279        self.max_lines
280    }
281
282    /// Sets the most lines the prompt grows to, at least one.
283    pub fn set_max_lines(&mut self, lines: usize, cx: &mut Context<Self>) {
284        let lines = lines.max(1);
285        if lines == self.max_lines {
286            return;
287        }
288        self.max_lines = lines;
289        self.input
290            .update(cx, |input, cx| input.set_auto_grow(1, lines, cx));
291    }
292
293    /// Whether the composer ignores input.
294    pub fn is_disabled(&self) -> bool {
295        self.disabled
296    }
297
298    /// Disables or enables the composer: the text, the send button, and
299    /// submitting.
300    pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
301        if disabled == self.disabled {
302            return;
303        }
304        self.disabled = disabled;
305        self.input
306            .update(cx, |input, cx| input.set_disabled(disabled, cx));
307        cx.notify();
308    }
309
310    /// Whether a submit clears the text and the attachments.
311    pub fn clears_on_submit(&self) -> bool {
312        self.clear_on_submit
313    }
314
315    /// Whether a submit clears the text and the attachments. On by default;
316    /// turn it off when the application clears them itself, once the
317    /// prompt is accepted.
318    pub fn set_clear_on_submit(&mut self, clear: bool) {
319        self.clear_on_submit = clear;
320    }
321
322    /// The attachments, in order.
323    pub fn attachments(&self) -> &[Attachment] {
324        &self.attachments
325    }
326
327    /// Adds an attachment at the end. An attachment with the id of one
328    /// already here replaces it in place.
329    pub fn add_attachment(&mut self, attachment: Attachment, cx: &mut Context<Self>) {
330        match self
331            .attachments
332            .iter_mut()
333            .find(|existing| existing.id() == attachment.id())
334        {
335            Some(existing) => *existing = attachment,
336            None => self.attachments.push(attachment),
337        }
338        cx.emit(ComposerEvent::AttachmentsChanged);
339        cx.notify();
340    }
341
342    /// Removes the attachment with `id` and returns it.
343    pub fn remove_attachment(&mut self, id: &str, cx: &mut Context<Self>) -> Option<Attachment> {
344        let ix = self.attachments.iter().position(|a| a.id() == id)?;
345        let removed = self.attachments.remove(ix);
346        cx.emit(ComposerEvent::AttachmentsChanged);
347        cx.notify();
348        Some(removed)
349    }
350
351    /// Sets the upload progress of the attachment with `id`: a percentage,
352    /// zero for queued, or `None` for done.
353    pub fn set_attachment_progress(
354        &mut self,
355        id: &str,
356        progress: Option<f32>,
357        cx: &mut Context<Self>,
358    ) {
359        if let Some(attachment) = self.attachments.iter_mut().find(|a| a.id() == id) {
360            attachment.set_progress(progress);
361            cx.emit(ComposerEvent::AttachmentsChanged);
362            cx.notify();
363        }
364    }
365
366    /// Removes every attachment.
367    pub fn clear_attachments(&mut self, cx: &mut Context<Self>) {
368        if !self.attachments.is_empty() {
369            self.attachments.clear();
370            cx.emit(ComposerEvent::AttachmentsChanged);
371            cx.notify();
372        }
373    }
374
375    /// Whether the prompt can be submitted now: it has text or an
376    /// attachment, nothing is still uploading, and the composer is enabled.
377    pub fn can_submit(&self, cx: &App) -> bool {
378        let busy = self
379            .attachments
380            .iter()
381            .any(|a| a.status() != AttachmentStatus::Ready);
382        !self.disabled
383            && !busy
384            && (!self.text(cx).trim().is_empty() || !self.attachments.is_empty())
385    }
386
387    /// Submits the prompt if it can be, and says whether it did.
388    pub fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
389        if !self.can_submit(cx) {
390            return false;
391        }
392        let text = self.text(cx);
393        let attachments = self.attachments.clone();
394        cx.emit(ComposerEvent::Submit { text, attachments });
395        if self.clear_on_submit {
396            self.set_text("", window, cx);
397            self.clear_attachments(cx);
398        }
399        true
400    }
401}
402
403/// What a control slot holds: the default control, nothing, or the
404/// application's own element.
405enum Slot {
406    Default,
407    Hidden,
408    Custom(AnyElement),
409}
410
411impl Slot {
412    fn custom(element: impl IntoElement) -> Self {
413        Self::Custom(element.into_any_element())
414    }
415}
416
417/// A prompt box: a card with the attachments above the text, and a row of
418/// controls under it. The row holds an add button, a permission menu, a
419/// space, a model picker, a microphone button, and a send button. An
420/// optional [status tab](crate::ComposerStatusTab) sits behind the top of
421/// the card.
422///
423/// Every default control can be replaced with the application's own, or
424/// left out, and [`toolbar`](Self::toolbar) adds controls after the
425/// permission menu. A file dropped on the card is reported as
426/// [`ComposerEvent::FilesDropped`].
427///
428/// ```no_run
429/// use gpui_cn::{Composer, ComposerState, ComposerStatusTab};
430/// use gpui_kit::{AppContext as _, Context, Window};
431///
432/// fn prompt(window: &mut Window, cx: &mut Context<()>) -> Composer {
433///     let state = cx.new(|cx| ComposerState::new(window, cx));
434///     Composer::new("composer", &state)
435///         .status(ComposerStatusTab::new("status").context(57.))
436/// }
437/// ```
438///
439/// The id derives the ids of the controls, so it must be stable across
440/// frames. `Styled` refines the card.
441///
442/// The builders `disabled`, `max_lines` and `placeholder` are applied to the
443/// state on every render, and each setter does nothing when the value is
444/// already the state's, so the frame that applies them again changes nothing
445/// and asks for no further frame.
446///
447/// The default icons (the mic, the shield, the file kinds, the git branch) are
448/// not in the default `gpui_kit::assets::Assets`: register
449/// [`ComposerAssets`](crate::ComposerAssets).
450#[derive(IntoElement)]
451#[non_exhaustive]
452pub struct Composer {
453    id: ElementId,
454    state: Entity<ComposerState>,
455    style: StyleRefinement,
456    status: Option<AnyElement>,
457    leading: Slot,
458    permission: Slot,
459    toolbar: Vec<AnyElement>,
460    models: Slot,
461    mic: Slot,
462    send: Slot,
463    show_mic: bool,
464    placeholder: Option<SharedString>,
465    max_lines: Option<usize>,
466    disabled: Option<bool>,
467}
468
469impl Composer {
470    /// A composer on `state`, with a stable id.
471    pub fn new(id: impl Into<ElementId>, state: &Entity<ComposerState>) -> Self {
472        Self {
473            id: id.into(),
474            state: state.clone(),
475            style: StyleRefinement::default(),
476            status: None,
477            leading: Slot::Default,
478            permission: Slot::Default,
479            toolbar: Vec::new(),
480            models: Slot::Default,
481            mic: Slot::Default,
482            send: Slot::Default,
483            show_mic: false,
484            placeholder: None,
485            max_lines: None,
486            disabled: None,
487        }
488    }
489
490    /// The strip behind the top of the card. By default there is none.
491    pub fn status(mut self, status: impl IntoElement) -> Self {
492        self.status = Some(status.into_any_element());
493        self
494    }
495
496    /// No strip behind the card: the bare card. This is the default.
497    pub fn no_status(mut self) -> Self {
498        self.status = None;
499        self
500    }
501
502    /// The placeholder shown while the prompt is empty.
503    pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
504        self.placeholder = Some(placeholder.into());
505        self
506    }
507
508    /// The most lines the prompt grows to before it scrolls. The default
509    /// is ten.
510    pub fn max_lines(mut self, lines: usize) -> Self {
511        self.max_lines = Some(lines);
512        self
513    }
514
515    /// Replaces the add button at the left of the controls.
516    pub fn leading(mut self, element: impl IntoElement) -> Self {
517        self.leading = Slot::custom(element);
518        self
519    }
520
521    /// Leaves out the add button.
522    pub fn no_leading(mut self) -> Self {
523        self.leading = Slot::Hidden;
524        self
525    }
526
527    /// Replaces the permission menu.
528    pub fn permission_menu(mut self, element: impl IntoElement) -> Self {
529        self.permission = Slot::custom(element);
530        self
531    }
532
533    /// Leaves out the permission menu.
534    pub fn no_permission_menu(mut self) -> Self {
535        self.permission = Slot::Hidden;
536        self
537    }
538
539    /// Adds a control after the permission menu. Each call adds one. The
540    /// control keeps the width it has: it takes no part in the plan that
541    /// shrinks the model and permission labels and turns controls into
542    /// icons when the row is narrow.
543    pub fn toolbar(mut self, element: impl IntoElement) -> Self {
544        self.toolbar.push(element.into_any_element());
545        self
546    }
547
548    /// Replaces the model picker.
549    pub fn model_picker(mut self, element: impl IntoElement) -> Self {
550        self.models = Slot::custom(element);
551        self
552    }
553
554    /// Leaves out the model picker.
555    pub fn no_model_picker(mut self) -> Self {
556        self.models = Slot::Hidden;
557        self
558    }
559
560    /// Whether the microphone button shows. Off by default.
561    pub fn show_mic(mut self, show: bool) -> Self {
562        self.show_mic = show;
563        self
564    }
565
566    /// Replaces the microphone button.
567    pub fn mic(mut self, element: impl IntoElement) -> Self {
568        self.mic = Slot::custom(element);
569        self
570    }
571
572    /// Replaces the send button. The replacement submits through
573    /// [`ComposerState::submit`].
574    pub fn send_button(mut self, element: impl IntoElement) -> Self {
575        self.send = Slot::custom(element);
576        self
577    }
578}
579
580impl Disableable for Composer {
581    /// Whether the composer ignores input. Pushes onto the state; leave it
582    /// unset to control the state directly.
583    fn disabled(mut self, disabled: bool) -> Self {
584        self.disabled = Some(disabled);
585        self
586    }
587}
588
589impl Styled for Composer {
590    fn style(&mut self) -> &mut StyleRefinement {
591        &mut self.style
592    }
593}
594
595fn child(parent: &ElementId, name: &'static str) -> ElementId {
596    ElementId::NamedChild(parent.clone().into(), name.into())
597}
598
599impl RenderOnce for Composer {
600    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
601        let id = self.id;
602        let state = self.state;
603        state.update(cx, |state, cx| {
604            if let Some(disabled) = self.disabled {
605                state.set_disabled(disabled, cx);
606            }
607            if let Some(lines) = self.max_lines {
608                state.set_max_lines(lines, cx);
609            }
610            if let Some(placeholder) = self.placeholder.clone() {
611                state.set_placeholder(placeholder, window, cx);
612            }
613        });
614        let (input, attachments, permission, models, disabled, can_submit) = {
615            let read = state.read(cx);
616            (
617                read.input.clone(),
618                read.attachments.clone(),
619                read.permission.clone(),
620                read.models.clone(),
621                read.disabled,
622                read.can_submit(cx),
623            )
624        };
625        let focused = input.read(cx).focus_handle(cx).contains_focused(window, cx);
626        let theme = cx.theme();
627        let (fill, border, focus_border, radius, accent, gap) = (
628            theme.field,
629            theme.field_border,
630            theme.field_focus_border,
631            theme.radius_xl(),
632            theme.field_focus_border,
633            theme.base.spacing.sm,
634        );
635        let (ink, muted) = (theme.foreground(), theme.muted_foreground());
636        let disabled_opacity = theme.disabled_opacity;
637        let radius_full = theme.radius_full();
638        let has_status = self.status.is_some();
639
640        let events = |state: &Entity<ComposerState>, event: ComposerEvent| {
641            let state = state.clone();
642            move |_: &gpui_kit::ClickEvent, _: &mut Window, cx: &mut App| {
643                let event = event.clone();
644                state.update(cx, |_, cx| cx.emit(event));
645            }
646        };
647
648        let has_leading = !matches!(self.leading, Slot::Hidden);
649        let has_mic = matches!(self.mic, Slot::Default if self.show_mic)
650            || matches!(self.mic, Slot::Custom(_));
651        let has_send = !matches!(self.send, Slot::Hidden);
652        let leading = match self.leading {
653            Slot::Default => Some(
654                Button::new(child(&id, "add"))
655                    .ghost()
656                    .size(ButtonSize::Default)
657                    .icon(IconName::Plus)
658                    .text_color(ink)
659                    .accessibility_label("Add")
660                    .tooltip("Add")
661                    .disabled(disabled)
662                    .on_click(events(&state, ComposerEvent::AddClicked))
663                    .into_any_element(),
664            ),
665            Slot::Hidden => None,
666            Slot::Custom(element) => Some(element),
667        };
668        let model_state = models.clone();
669        let has_default_models = matches!(self.models, Slot::Default);
670        let permission_menu = matches!(self.permission, Slot::Default)
671            .then(|| PermissionMenu::new(child(&id, "permission"), &permission));
672        let model_picker = has_default_models.then(|| {
673            ModelPicker::new(child(&id, "models"), &models).align(gpui_kit::base::Align::End)
674        });
675        let effort_menu = (has_default_models && model_state.read(cx).effort().is_some())
676            .then(|| EffortMenu::new(child(&id, "effort"), &model_state));
677        let toolbar_width = window
678            .use_keyed_state(child(&id, "toolbar-width"), cx, |_, _| Measured::default())
679            .read(cx)
680            .clone();
681        let fixed = {
682            let control = cx.theme().metrics.control_md;
683            Need {
684                natural: control,
685                min: control,
686                icon_only: None,
687            }
688        };
689        let mut needs = Vec::new();
690        let mut at = |need: Need| {
691            needs.push(need);
692            needs.len() - 1
693        };
694        if has_leading {
695            at(fixed);
696        }
697        at(Need {
698            natural: px(0.),
699            min: px(0.),
700            icon_only: None,
701        });
702        let permission_at = permission_menu
703            .as_ref()
704            .map(|menu| at(menu.need(window, cx)));
705        let model_need = model_picker.as_ref().map(|picker| picker.need(window, cx));
706        let model_at = model_need.map(&mut at);
707        let effort_at = effort_menu
708            .as_ref()
709            .and_then(|menu| menu.need(window, cx))
710            .map(&mut at);
711        if has_mic {
712            at(fixed);
713        }
714        if has_send {
715            at(fixed);
716        }
717        let order: Vec<usize> = [effort_at, permission_at, model_at]
718            .into_iter()
719            .flatten()
720            .collect();
721        let collapsed = collapse::plan(toolbar_width.width(), gap, gap, &needs, &order);
722        let is_collapsed = |index: Option<usize>| {
723            index.is_some_and(|index| collapsed.get(index).is_some_and(|fit| fit.is_icon()))
724        };
725        let permission = match self.permission {
726            Slot::Default => permission_menu.map(|menu| {
727                menu.icon_only(is_collapsed(permission_at))
728                    .flex_shrink(1.)
729                    .into_any_element()
730            }),
731            Slot::Hidden => None,
732            Slot::Custom(element) => Some(element),
733        };
734        let models = match self.models {
735            Slot::Default => model_picker.map(|picker| {
736                // The popover's own root takes no style, so a box around it
737                // is what shrinks in the row, down to the pill's minimum.
738                let collapsed = is_collapsed(model_at);
739                let min = model_need
740                    .map(|need| match (collapsed, need.icon_only) {
741                        (true, Some(icon_only)) => icon_only,
742                        _ => need.min,
743                    })
744                    .unwrap_or_default();
745                div()
746                    .flex()
747                    .flex_shrink(1.)
748                    .min_w(min)
749                    .child(picker.icon_only(collapsed))
750                    .into_any_element()
751            }),
752            Slot::Hidden => None,
753            Slot::Custom(element) => Some(element),
754        };
755        let effort = effort_menu.map(|menu| {
756            menu.icon_only(is_collapsed(effort_at))
757                .flex_shrink(1.)
758                .into_any_element()
759        });
760        let mic = match self.mic {
761            Slot::Default if self.show_mic => Some(
762                Button::new(child(&id, "mic"))
763                    .outline()
764                    .size(ButtonSize::Default)
765                    .icon(IconName::Mic)
766                    .text_color(muted)
767                    .rounded(radius_full)
768                    .accessibility_label("Dictate")
769                    .tooltip("Dictate")
770                    .disabled(disabled)
771                    .on_click(events(&state, ComposerEvent::MicClicked))
772                    .into_any_element(),
773            ),
774            Slot::Default | Slot::Hidden => None,
775            Slot::Custom(element) => Some(element),
776        };
777        let send = match self.send {
778            Slot::Default => Some(
779                Button::new(child(&id, "send"))
780                    .primary()
781                    .size(ButtonSize::Default)
782                    .icon(IconName::ArrowUp)
783                    .rounded(radius_full)
784                    .accessibility_label("Send")
785                    .tooltip("Send")
786                    .disabled(!can_submit)
787                    .on_click({
788                        let state = state.clone();
789                        move |_, window, cx| {
790                            state.update(cx, |state, cx| {
791                                state.submit(window, cx);
792                            });
793                        }
794                    })
795                    .into_any_element(),
796            ),
797            Slot::Hidden => None,
798            Slot::Custom(element) => Some(element),
799        };
800
801        let strip = (!attachments.is_empty()).then(|| {
802            let state = state.clone();
803            AttachmentStrip::new(child(&id, "attachments"))
804                .attachments(attachments)
805                .on_dismiss(move |attachment, _, cx| {
806                    let attachment = attachment.clone();
807                    state.update(cx, |state, cx| {
808                        state.remove_attachment(&attachment, cx);
809                    });
810                })
811        });
812
813        let drop_state = state.clone();
814        let card = div()
815            .id(child(&id, "card"))
816            .test_support()
817            .flex()
818            .flex_col()
819            .w_full()
820            .gap(gap)
821            .p_1()
822            .rounded(radius)
823            .bg(fill)
824            .border_1()
825            .border_color(if focused { focus_border } else { border })
826            .when(disabled, |this| this.opacity(disabled_opacity))
827            .when(has_status, |this| this.mt(-gpui_kit::px(1.)).relative())
828            .refine_style(&self.style)
829            .drag_over::<ExternalPaths>(move |style, _, _, _| style.border_color(accent))
830            .on_drop(move |paths: &ExternalPaths, _, cx| {
831                let paths = paths.paths().to_vec();
832                drop_state.update(cx, |_, cx| cx.emit(ComposerEvent::FilesDropped(paths)));
833            })
834            .children(strip)
835            .child(
836                Textarea::new(&input)
837                    .id(child(&id, "text"))
838                    .accessibility_label("Prompt")
839                    .bg(gpui_kit::transparent_black())
840                    .border_0()
841                    .rounded_none(),
842            )
843            .child(
844                div()
845                    .relative()
846                    .flex()
847                    .w_full()
848                    .min_w_0()
849                    .items_center()
850                    .gap(gap)
851                    .child(toolbar_width.probe())
852                    .children(leading)
853                    .children(permission)
854                    .children(self.toolbar)
855                    .child(div().flex_1())
856                    .children(models)
857                    .children(effort)
858                    .children(mic)
859                    .children(send),
860            );
861
862        div()
863            .id(id)
864            .test_support()
865            .flex()
866            .flex_col()
867            .w_full()
868            .children(self.status)
869            .child(card)
870    }
871}