agent_ui: Restore `Ctrl + >` behavior for whole lines (#54698)

Neel created

Restores current line fallback when using `Ctrl + >` to add context to
the agent.

Release Notes:

- N/A

Change summary

crates/agent_ui/src/agent_panel.rs         |  65 -
crates/agent_ui/src/completion_provider.rs | 609 +++++++++++++++--------
crates/agent_ui/src/conversation_view.rs   |  29 
crates/agent_ui/src/message_editor.rs      |  33 
crates/terminal_view/src/terminal_panel.rs |   6 
crates/workspace/src/dock.rs               |  12 
6 files changed, 474 insertions(+), 280 deletions(-)

Detailed changes

crates/agent_ui/src/agent_panel.rs 🔗

@@ -32,6 +32,7 @@ use zed_actions::{
 use crate::ExpandMessageEditor;
 use crate::ManageProfiles;
 use crate::agent_connection_store::AgentConnectionStore;
+use crate::completion_provider::AgentContextSource;
 use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore, ThreadMetadataStoreEvent};
 use crate::{
     AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, Follow,
@@ -67,10 +68,7 @@ use language_model::LanguageModelRegistry;
 use project::{Project, ProjectPath, Worktree};
 use prompt_store::{PromptStore, UserPromptId};
 use rules_library::{RulesLibrary, open_rules_library};
-use settings::TerminalDockPosition;
 use settings::{Settings, update_settings_file};
-use terminal::terminal_settings::TerminalSettings;
-use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
 use theme_settings::ThemeSettings;
 use ui::{
     Button, ContextMenu, ContextMenuEntry, IconButton, PopoverMenu, PopoverMenuHandle, Tab,
@@ -413,61 +411,36 @@ pub fn init(cx: &mut App) {
                 )
                 .register_action(
                     |workspace: &mut Workspace, _: &AddSelectionToThread, window, cx| {
-                        let active_editor = workspace
-                            .active_item(cx)
-                            .and_then(|item| item.act_as::<Editor>(cx));
-                        let has_editor_selection = active_editor.is_some_and(|editor| {
-                            editor.update(cx, |editor, cx| {
-                                editor.has_non_empty_selection(&editor.display_snapshot(cx))
-                            })
-                        });
-
-                        let has_terminal_selection = workspace
-                            .active_item(cx)
-                            .and_then(|item| item.act_as::<TerminalView>(cx))
-                            .is_some_and(|terminal_view| {
-                                terminal_view
-                                    .read(cx)
-                                    .terminal()
-                                    .read(cx)
-                                    .last_content
-                                    .selection_text
-                                    .as_ref()
-                                    .is_some_and(|text| !text.is_empty())
-                            });
+                        let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) else {
+                            return;
+                        };
 
-                        let has_terminal_panel_selection =
-                            workspace.panel::<TerminalPanel>(cx).is_some_and(|panel| {
-                                let position = match TerminalSettings::get_global(cx).dock {
-                                    TerminalDockPosition::Left => DockPosition::Left,
-                                    TerminalDockPosition::Bottom => DockPosition::Bottom,
-                                    TerminalDockPosition::Right => DockPosition::Right,
-                                };
-                                let dock_is_open =
-                                    workspace.dock_at_position(position).read(cx).is_open();
-                                dock_is_open && !panel.read(cx).terminal_selections(cx).is_empty()
-                            });
+                        let source = AgentContextSource::from_focused(workspace, window, cx);
+                        let source = source.or_else(|| {
+                            let cached = agent_panel.read(cx).last_context_source.clone()?;
+                            cached.exists(workspace, cx).then_some(cached)
+                        });
+                        let source =
+                            source.or_else(|| AgentContextSource::from_active(workspace, cx));
 
-                        if !has_editor_selection
-                            && !has_terminal_selection
-                            && !has_terminal_panel_selection
-                        {
+                        let Some(source) = source else {
                             return;
-                        }
+                        };
 
-                        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
+                        let Some(selection) = source.read_selection(workspace, true, cx) else {
                             return;
                         };
 
-                        if !panel.focus_handle(cx).contains_focused(window, cx) {
+                        if !agent_panel.focus_handle(cx).contains_focused(window, cx) {
                             workspace.toggle_panel_focus::<AgentPanel>(window, cx);
                         }
 
-                        panel.update(cx, |_, cx| {
+                        agent_panel.update(cx, |panel, cx| {
+                            panel.last_context_source = Some(source);
                             cx.defer_in(window, move |panel, window, cx| {
                                 if let Some(conversation_view) = panel.active_conversation_view() {
                                     conversation_view.update(cx, |conversation_view, cx| {
-                                        conversation_view.insert_selections(window, cx);
+                                        conversation_view.insert_selection(selection, window, cx);
                                     });
                                 }
                             });
@@ -707,6 +680,7 @@ pub struct AgentPanel {
     _base_view_observation: Option<Subscription>,
     _draft_editor_observation: Option<Subscription>,
     _thread_metadata_store_subscription: Subscription,
+    last_context_source: Option<AgentContextSource>,
 }
 
 impl AgentPanel {
@@ -1065,6 +1039,7 @@ impl AgentPanel {
             _base_view_observation: None,
             _draft_editor_observation: None,
             _thread_metadata_store_subscription,
+            last_context_source: None,
         };
 
         // Initial sync of agent servers from extensions

crates/agent_ui/src/completion_provider.rs 🔗

@@ -12,7 +12,7 @@ use anyhow::Result;
 use editor::{CompletionProvider, Editor, code_context_menus::COMPLETION_MENU_MAX_WIDTH};
 use futures::FutureExt as _;
 use fuzzy::{PathMatch, StringMatch, StringMatchCandidate};
-use gpui::{App, BackgroundExecutor, Entity, SharedString, Task, WeakEntity};
+use gpui::{App, BackgroundExecutor, Entity, Focusable, SharedString, Task, WeakEntity, Window};
 use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId};
 use lsp::CompletionContext;
 use multi_buffer::ToOffset as _;
@@ -24,7 +24,7 @@ use project::{
 };
 use prompt_store::{PromptStore, UserPromptId};
 use rope::Point;
-use settings::{Settings, TerminalDockPosition};
+use settings::Settings;
 use terminal::terminal_settings::TerminalSettings;
 use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
 use text::{Anchor, ToOffset as _, ToPoint as _};
@@ -35,11 +35,108 @@ use util::paths::PathStyle;
 use util::rel_path::RelPath;
 use util::truncate_and_remove_front;
 use workspace::Workspace;
-use workspace::dock::DockPosition;
 
 use crate::AgentPanel;
 use crate::mention_set::MentionSet;
 
+#[derive(Clone)]
+pub(crate) enum AgentContextSelection {
+    Editor(Vec<(Entity<Buffer>, Range<text::Anchor>)>),
+    Terminal(Vec<String>),
+}
+
+#[derive(Clone)]
+pub(crate) enum AgentContextSource {
+    Editor(WeakEntity<Editor>),
+    TerminalView(WeakEntity<TerminalView>),
+    TerminalPanel,
+}
+
+impl AgentContextSource {
+    pub(crate) fn read_selection(
+        &self,
+        workspace: &Workspace,
+        include_current_line: bool,
+        cx: &mut App,
+    ) -> Option<AgentContextSelection> {
+        match self {
+            Self::Editor(handle) => {
+                let editor = handle.upgrade()?;
+                let ranges = editor_selection_ranges(&editor, include_current_line, cx);
+                (!ranges.is_empty()).then_some(AgentContextSelection::Editor(ranges))
+            }
+            Self::TerminalView(handle) => {
+                let terminal_view = handle.upgrade()?;
+                terminal_view_selection(&terminal_view, cx)
+                    .map(|text| AgentContextSelection::Terminal(vec![text]))
+            }
+            Self::TerminalPanel => {
+                let panel = workspace.panel::<TerminalPanel>(cx)?;
+                let selections = panel.read(cx).terminal_selections(cx);
+                (!selections.is_empty()).then_some(AgentContextSelection::Terminal(selections))
+            }
+        }
+    }
+
+    pub(crate) fn from_focused(workspace: &Workspace, window: &Window, cx: &App) -> Option<Self> {
+        if let Some(agent_panel) = workspace.panel::<AgentPanel>(cx)
+            && agent_panel.focus_handle(cx).contains_focused(window, cx)
+        {
+            return None;
+        }
+
+        if let Some(active_item) = workspace.active_item(cx) {
+            if let Some(editor) = active_item.act_as::<Editor>(cx) {
+                if editor.focus_handle(cx).is_focused(window) {
+                    return Some(Self::Editor(editor.downgrade()));
+                }
+            } else if let Some(terminal_view) = active_item.act_as::<TerminalView>(cx)
+                && terminal_view.focus_handle(cx).is_focused(window)
+            {
+                return Some(Self::TerminalView(terminal_view.downgrade()));
+            }
+        }
+
+        if let Some(panel) = workspace.panel::<TerminalPanel>(cx)
+            && panel.focus_handle(cx).contains_focused(window, cx)
+        {
+            return Some(Self::TerminalPanel);
+        }
+
+        None
+    }
+
+    pub(crate) fn from_active(workspace: &Workspace, cx: &App) -> Option<Self> {
+        if let Some(active_item) = workspace.active_item(cx) {
+            if let Some(editor) = active_item.act_as::<Editor>(cx) {
+                return Some(Self::Editor(editor.downgrade()));
+            } else if let Some(terminal_view) = active_item.act_as::<TerminalView>(cx) {
+                return Some(Self::TerminalView(terminal_view.downgrade()));
+            }
+        }
+        if terminal_panel_dock_is_open(workspace, cx) {
+            return Some(Self::TerminalPanel);
+        }
+        None
+    }
+
+    pub(crate) fn exists(&self, workspace: &Workspace, cx: &App) -> bool {
+        match self {
+            Self::Editor(handle) => handle.upgrade().is_some(),
+            Self::TerminalView(handle) => handle.upgrade().is_some(),
+            Self::TerminalPanel => terminal_panel_dock_is_open(workspace, cx),
+        }
+    }
+}
+
+fn terminal_panel_dock_is_open(workspace: &Workspace, cx: &App) -> bool {
+    if workspace.panel::<TerminalPanel>(cx).is_none() {
+        return false;
+    }
+    let position = TerminalSettings::get_global(cx).dock.into();
+    workspace.dock_at_position(position).read(cx).is_open()
+}
+
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub(crate) enum PromptContextEntry {
     Mode(PromptContextType),
@@ -267,14 +364,13 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
                 // inserted
                 confirm: Some(Arc::new(|_, _, _| true)),
             }),
-            PromptContextEntry::Action(action) => Self::completion_for_action(
-                action,
-                source_range,
-                editor,
-                mention_set,
-                workspace,
-                cx,
-            ),
+            PromptContextEntry::Action(action) => {
+                let selection = workspace.update(cx, |workspace, cx| {
+                    AgentContextSource::from_active(workspace, cx)?
+                        .read_selection(workspace, false, cx)
+                });
+                Self::completion_for_action(action, source_range, editor, mention_set, selection)
+            }
         }
     }
 
@@ -542,136 +638,27 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
         source_range: Range<Anchor>,
         editor: WeakEntity<Editor>,
         mention_set: WeakEntity<MentionSet>,
-        workspace: &Entity<Workspace>,
-        cx: &mut App,
+        selection: Option<AgentContextSelection>,
     ) -> Option<Completion> {
         let (new_text, on_action) = match action {
-            PromptContextAction::AddSelections => {
-                // Collect non-empty editor selections
-                let editor_selections: Vec<_> = selection_ranges(workspace, cx)
-                    .into_iter()
-                    .filter(|(buffer, range)| {
-                        let snapshot = buffer.read(cx).snapshot();
-                        range.start.to_offset(&snapshot) != range.end.to_offset(&snapshot)
-                    })
-                    .collect();
-
-                // Collect terminal selections from all terminal views if the terminal panel is visible
-                let terminal_selections: Vec<String> = terminal_selections(workspace, cx);
-
-                const EDITOR_PLACEHOLDER: &str = "selection ";
-                const TERMINAL_PLACEHOLDER: &str = "terminal ";
-
-                let selections = editor_selections
-                    .into_iter()
-                    .enumerate()
-                    .map(|(ix, (buffer, range))| {
-                        (
-                            buffer,
-                            range,
-                            (EDITOR_PLACEHOLDER.len() * ix)
-                                ..(EDITOR_PLACEHOLDER.len() * (ix + 1) - 1),
-                        )
-                    })
-                    .collect::<Vec<_>>();
-
-                let mut new_text: String = EDITOR_PLACEHOLDER.repeat(selections.len());
-
-                // Add terminal placeholders for each terminal selection
-                let terminal_ranges: Vec<(String, std::ops::Range<usize>)> = terminal_selections
-                    .into_iter()
-                    .map(|text| {
-                        let start = new_text.len();
-                        new_text.push_str(TERMINAL_PLACEHOLDER);
-                        (text, start..(new_text.len() - 1))
-                    })
-                    .collect();
-
-                let callback = Arc::new({
-                    let source_range = source_range.clone();
-                    move |_: CompletionIntent, window: &mut Window, cx: &mut App| {
-                        let editor = editor.clone();
-                        let selections = selections.clone();
-                        let mention_set = mention_set.clone();
-                        let source_range = source_range.clone();
-                        let terminal_ranges = terminal_ranges.clone();
-                        window.defer(cx, move |window, cx| {
-                            if let Some(editor) = editor.upgrade() {
-                                // Insert editor selections
-                                if !selections.is_empty() {
-                                    mention_set
-                                        .update(cx, |store, cx| {
-                                            store.confirm_mention_for_selection(
-                                                source_range.clone(),
-                                                selections,
-                                                editor.clone(),
-                                                window,
-                                                cx,
-                                            )
-                                        })
-                                        .ok();
-                                }
-
-                                // Insert terminal selections
-                                for (terminal_text, terminal_range) in terminal_ranges {
-                                    let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
-                                    let Some(start) =
-                                        snapshot.anchor_in_excerpt(source_range.start)
-                                    else {
-                                        return;
-                                    };
-                                    let offset = start.to_offset(&snapshot);
-
-                                    let line_count = terminal_text.lines().count() as u32;
-                                    let mention_uri = MentionUri::TerminalSelection { line_count };
-                                    let range = snapshot.anchor_after(offset + terminal_range.start)
-                                        ..snapshot.anchor_after(offset + terminal_range.end);
-
-                                    let crease = crate::mention_set::crease_for_mention(
-                                        mention_uri.name().into(),
-                                        mention_uri.icon_path(cx),
-                                        None,
-                                        range,
-                                        editor.downgrade(),
-                                    );
-
-                                    let crease_id = editor.update(cx, |editor, cx| {
-                                        let crease_ids =
-                                            editor.insert_creases(vec![crease.clone()], cx);
-                                        editor.fold_creases(vec![crease], false, window, cx);
-                                        crease_ids.first().copied().unwrap()
-                                    });
-
-                                    mention_set
-                                        .update(cx, |mention_set, _| {
-                                            mention_set.insert_mention(
-                                                crease_id,
-                                                mention_uri.clone(),
-                                                gpui::Task::ready(Ok(
-                                                    crate::mention_set::Mention::Text {
-                                                        content: terminal_text,
-                                                        tracked_buffers: vec![],
-                                                    },
-                                                ))
-                                                .shared(),
-                                            );
-                                        })
-                                        .ok();
-                                }
-                            }
-                        });
-                        false
-                    }
-                });
-
-                (
-                    new_text,
-                    callback
-                        as Arc<
-                            dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync,
-                        >,
-                )
-            }
+            PromptContextAction::AddSelections => match selection? {
+                AgentContextSelection::Editor(editor_selections) => {
+                    completion_text_for_editor_selections(
+                        source_range.clone(),
+                        editor,
+                        mention_set,
+                        editor_selections,
+                    )
+                }
+                AgentContextSelection::Terminal(terminal_selections) => {
+                    completion_text_for_terminal_selections(
+                        source_range.clone(),
+                        editor,
+                        mention_set,
+                        terminal_selections,
+                    )
+                }
+            },
         };
 
         Some(Completion {
@@ -1166,19 +1153,12 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
             entries.push(PromptContextEntry::Mode(PromptContextType::Thread));
         }
 
-        let has_editor_selection = workspace
-            .read(cx)
-            .active_item(cx)
-            .and_then(|item| item.downcast::<Editor>())
-            .is_some_and(|editor| {
-                editor.update(cx, |editor, cx| {
-                    editor.has_non_empty_selection(&editor.display_snapshot(cx))
-                })
-            });
-
-        let has_terminal_selection = !terminal_selections(workspace, cx).is_empty();
-
-        if has_editor_selection || has_terminal_selection {
+        let has_active_selection = workspace.update(cx, |workspace, cx| {
+            AgentContextSource::from_active(workspace, cx)
+                .and_then(|source| source.read_selection(workspace, false, cx))
+                .is_some()
+        });
+        if has_active_selection {
             entries.push(PromptContextEntry::Action(
                 PromptContextAction::AddSelections,
             ));
@@ -2168,81 +2148,219 @@ fn build_code_label_for_path(
     label.build()
 }
 
-fn terminal_selections(workspace: &Entity<Workspace>, cx: &App) -> Vec<String> {
-    let mut selections = Vec::new();
-
-    // Check if the active item is a terminal (in a panel or not)
-    if let Some(terminal_view) = workspace
+fn terminal_view_selection(terminal_view: &Entity<TerminalView>, cx: &App) -> Option<String> {
+    terminal_view
         .read(cx)
-        .active_item(cx)
-        .and_then(|item| item.act_as::<TerminalView>(cx))
-    {
-        if let Some(text) = terminal_view
-            .read(cx)
-            .terminal()
-            .read(cx)
-            .last_content
-            .selection_text
-            .clone()
-            .filter(|text| !text.is_empty())
-        {
-            selections.push(text);
-        }
-    }
-
-    if let Some(panel) = workspace.read(cx).panel::<TerminalPanel>(cx) {
-        let position = match TerminalSettings::get_global(cx).dock {
-            TerminalDockPosition::Left => DockPosition::Left,
-            TerminalDockPosition::Bottom => DockPosition::Bottom,
-            TerminalDockPosition::Right => DockPosition::Right,
-        };
-        let dock_is_open = workspace
-            .read(cx)
-            .dock_at_position(position)
-            .read(cx)
-            .is_open();
-        if dock_is_open {
-            selections.extend(panel.read(cx).terminal_selections(cx));
-        }
-    }
-
-    selections
+        .terminal()
+        .read(cx)
+        .last_content
+        .selection_text
+        .clone()
+        .filter(|text| !text.is_empty())
 }
 
-fn selection_ranges(
-    workspace: &Entity<Workspace>,
+fn editor_selection_ranges(
+    editor: &Entity<Editor>,
+    include_current_line: bool,
     cx: &mut App,
 ) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
-    let Some(editor) = workspace
-        .read(cx)
-        .active_item(cx)
-        .and_then(|item| item.act_as::<Editor>(cx))
-    else {
-        return Vec::new();
-    };
-
     editor.update(cx, |editor, cx| {
         let selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
 
-        let buffer = editor.buffer().clone().read(cx);
-        let snapshot = buffer.snapshot(cx);
+        let multi_buffer = editor.buffer().read(cx);
+        let multi_buffer_snapshot = multi_buffer.snapshot(cx);
 
-        selections
-            .into_iter()
+        let non_empty_rows: collections::HashSet<u32> = selections
+            .iter()
             .filter(|s| !s.is_empty())
-            .map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
-            .flat_map(|range| {
-                let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
-                let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
+            .flat_map(|s| s.start.row..=s.end.row)
+            .collect();
+
+        let mut seen_current_line_rows = collections::HashSet::default();
+        let mut results = Vec::new();
+
+        for s in selections {
+            if s.is_empty() {
+                if !include_current_line
+                    || non_empty_rows.contains(&s.start.row)
+                    || !seen_current_line_rows.insert(s.start.row)
+                {
+                    continue;
+                }
+                let Some((buffer, anchor)) = multi_buffer.text_anchor_for_position(s.start, cx)
+                else {
+                    continue;
+                };
+                let buffer_snapshot = buffer.read(cx).snapshot();
+                let row = anchor.to_point(&buffer_snapshot).row;
+                let line_start = text::Point::new(row, 0);
+                let line_end = text::Point::new(row, buffer_snapshot.line_len(row));
+                let start = buffer_snapshot.anchor_after(line_start);
+                let end = buffer_snapshot.anchor_before(line_end);
+                if start.to_offset(&buffer_snapshot) == end.to_offset(&buffer_snapshot) {
+                    continue;
+                }
+                results.push((buffer, start..end));
+            } else {
+                let mb_start = multi_buffer_snapshot.anchor_after(s.start);
+                let mb_end = multi_buffer_snapshot.anchor_before(s.end);
+                let Some((start_buffer, start)) =
+                    multi_buffer.text_anchor_for_position(mb_start, cx)
+                else {
+                    continue;
+                };
+                let Some((end_buffer, end)) = multi_buffer.text_anchor_for_position(mb_end, cx)
+                else {
+                    continue;
+                };
                 if start_buffer != end_buffer {
-                    return None;
+                    continue;
                 }
-                Some((start_buffer, start..end))
-            })
-            .collect::<Vec<_>>()
+                let buffer_snapshot = start_buffer.read(cx).snapshot();
+                if start.to_offset(&buffer_snapshot) == end.to_offset(&buffer_snapshot) {
+                    continue;
+                }
+                results.push((start_buffer, start..end));
+            }
+        }
+
+        results
     })
 }
 
+type ConfirmCallback = Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync>;
+
+fn completion_text_for_editor_selections(
+    source_range: Range<Anchor>,
+    editor: WeakEntity<Editor>,
+    mention_set: WeakEntity<MentionSet>,
+    editor_selections: Vec<(Entity<Buffer>, Range<text::Anchor>)>,
+) -> (String, ConfirmCallback) {
+    const EDITOR_PLACEHOLDER: &str = "selection ";
+
+    let selections = editor_selections
+        .into_iter()
+        .enumerate()
+        .map(|(ix, (buffer, range))| {
+            (
+                buffer,
+                range,
+                (EDITOR_PLACEHOLDER.len() * ix)..(EDITOR_PLACEHOLDER.len() * (ix + 1) - 1),
+            )
+        })
+        .collect::<Vec<_>>();
+
+    let new_text = EDITOR_PLACEHOLDER.repeat(selections.len());
+
+    let callback: ConfirmCallback = Arc::new({
+        move |_: CompletionIntent, window: &mut Window, cx: &mut App| {
+            let editor = editor.clone();
+            let selections = selections.clone();
+            let mention_set = mention_set.clone();
+            let source_range = source_range.clone();
+            window.defer(cx, move |window, cx| {
+                if let Some(editor) = editor.upgrade()
+                    && !selections.is_empty()
+                {
+                    mention_set
+                        .update(cx, |store, cx| {
+                            store.confirm_mention_for_selection(
+                                source_range.clone(),
+                                selections,
+                                editor.clone(),
+                                window,
+                                cx,
+                            )
+                        })
+                        .ok();
+                }
+            });
+            false
+        }
+    });
+
+    (new_text, callback)
+}
+
+fn completion_text_for_terminal_selections(
+    source_range: Range<Anchor>,
+    editor: WeakEntity<Editor>,
+    mention_set: WeakEntity<MentionSet>,
+    terminal_selections: Vec<String>,
+) -> (String, ConfirmCallback) {
+    const TERMINAL_PLACEHOLDER: &str = "terminal ";
+
+    let mut new_text = String::new();
+    let terminal_ranges: Vec<(String, std::ops::Range<usize>)> = terminal_selections
+        .into_iter()
+        .map(|text| {
+            let start = new_text.len();
+            new_text.push_str(TERMINAL_PLACEHOLDER);
+            (text, start..(new_text.len() - 1))
+        })
+        .collect();
+
+    let callback: ConfirmCallback = Arc::new({
+        move |_: CompletionIntent, window: &mut Window, cx: &mut App| {
+            let editor = editor.clone();
+            let mention_set = mention_set.clone();
+            let source_range = source_range.clone();
+            let terminal_ranges = terminal_ranges.clone();
+            window.defer(cx, move |window, cx| {
+                let Some(editor) = editor.upgrade() else {
+                    return;
+                };
+                for (terminal_text, terminal_range) in terminal_ranges {
+                    let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
+                    let Some(start) = snapshot.anchor_in_excerpt(source_range.start) else {
+                        return;
+                    };
+                    let offset = start.to_offset(&snapshot);
+
+                    let line_count = terminal_text.lines().count() as u32;
+                    let mention_uri = MentionUri::TerminalSelection { line_count };
+                    let range = snapshot.anchor_after(offset + terminal_range.start)
+                        ..snapshot.anchor_after(offset + terminal_range.end);
+
+                    let crease = crate::mention_set::crease_for_mention(
+                        mention_uri.name().into(),
+                        mention_uri.icon_path(cx),
+                        None,
+                        range,
+                        editor.downgrade(),
+                    );
+
+                    let Some(crease_id) = editor.update(cx, |editor, cx| {
+                        let crease_ids = editor.insert_creases(vec![crease.clone()], cx);
+                        editor.fold_creases(vec![crease], false, window, cx);
+                        crease_ids.first().copied()
+                    }) else {
+                        log::error!("insert_creases returned no ids for terminal selection");
+                        continue;
+                    };
+
+                    mention_set
+                        .update(cx, |mention_set, _| {
+                            mention_set.insert_mention(
+                                crease_id,
+                                mention_uri.clone(),
+                                Task::ready(Ok(crate::mention_set::Mention::Text {
+                                    content: terminal_text,
+                                    tracked_buffers: vec![],
+                                }))
+                                .shared(),
+                            );
+                        })
+                        .ok();
+                }
+            });
+            false
+        }
+    });
+
+    (new_text, callback)
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -2652,4 +2770,71 @@ mod tests {
             "dir1/a.txt should be second"
         );
     }
+
+    #[gpui::test]
+    async fn test_source_read_selection_editor_whole_line(cx: &mut TestAppContext) {
+        use editor::Editor;
+        use project::Project;
+        use serde_json::json;
+        use text::ToOffset as _;
+        use util::path;
+        use workspace::{AppState, MultiWorkspace};
+
+        crate::conversation_view::tests::init_test(cx);
+
+        let app_state = cx.update(AppState::test);
+
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(path!("/root"), json!({ "a.txt": "" }))
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
+        let (multi_workspace, cx) =
+            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
+        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
+
+        let buffer = cx.new(|cx| language::Buffer::local("abc\ndef\nghi", cx));
+        let editor =
+            cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
+
+        editor.update_in(cx, |editor, window, cx| {
+            editor.change_selections(Default::default(), window, cx, |selections| {
+                selections.select_ranges([text::Point::new(1, 1)..text::Point::new(1, 1)]);
+            });
+        });
+
+        let source = AgentContextSource::Editor(editor.downgrade());
+
+        workspace.update(cx, |workspace, cx| {
+            let selection = source
+                .read_selection(workspace, true, cx)
+                .expect("editor source with cursor on a line should yield a selection");
+            assert!(
+                matches!(selection, AgentContextSelection::Editor(_)),
+                "expected Editor variant"
+            );
+            if let AgentContextSelection::Editor(ranges) = selection {
+                assert_eq!(
+                    ranges.len(),
+                    1,
+                    "expected exactly one range for whole-line fallback"
+                );
+                let (range_buffer, range) = &ranges[0];
+                let snapshot = range_buffer.read(cx).snapshot();
+                let start_offset = range.start.to_offset(&snapshot);
+                let end_offset = range.end.to_offset(&snapshot);
+                assert_eq!(
+                    &snapshot.text()[start_offset..end_offset],
+                    "def",
+                    "whole-line fallback should capture the current row"
+                );
+            }
+
+            // With include_current_line = false and no non-empty selection, the
+            // fallback is suppressed and read_selection should return None.
+            assert!(source.read_selection(workspace, false, cx).is_none());
+        });
+    }
 }

crates/agent_ui/src/conversation_view.rs 🔗

@@ -80,6 +80,7 @@ use crate::agent_connection_store::{
     AgentConnectedState, AgentConnectionEntryEvent, AgentConnectionStore,
 };
 use crate::agent_diff::AgentDiff;
+use crate::completion_provider::AgentContextSelection;
 use crate::entry_view_state::{EntryViewEvent, ViewEvent};
 use crate::message_editor::{InputAttempt, MessageEditor, MessageEditorEvent};
 use crate::profile_selector::{ProfileProvider, ProfileSelector};
@@ -2760,11 +2761,16 @@ impl ConversationView {
 
     /// Inserts the selected text into the message editor or the message being
     /// edited, if any.
-    pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context<Self>) {
+    pub(crate) fn insert_selection(
+        &self,
+        selection: AgentContextSelection,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
         if let Some(active_thread) = self.active_thread() {
             active_thread.update(cx, |thread, cx| {
                 thread.active_editor(cx).update(cx, |editor, cx| {
-                    editor.insert_selections(window, cx);
+                    editor.insert_selections(selection, window, cx);
                 })
             });
         }
@@ -2974,6 +2980,7 @@ pub(crate) mod tests {
     use workspace::{Item, MultiWorkspace};
 
     use crate::agent_panel;
+    use crate::completion_provider::AgentContextSource;
     use crate::thread_metadata_store::ThreadMetadataStore;
 
     use super::*;
@@ -5903,7 +5910,14 @@ pub(crate) mod tests {
                     .and_then(|active| active.read(cx).editing_message),
                 Some(0)
             );
-            view.insert_selections(window, cx);
+            let workspace = workspace.upgrade().unwrap();
+            let selection = workspace
+                .update(cx, |workspace, cx| {
+                    AgentContextSource::from_active(workspace, cx)?
+                        .read_selection(workspace, false, cx)
+                })
+                .unwrap();
+            view.insert_selection(selection, window, cx);
         });
 
         user_message_editor.read_with(cx, |editor, cx| {
@@ -5966,7 +5980,14 @@ pub(crate) mod tests {
                     .and_then(|active| active.read(cx).editing_message),
                 None
             );
-            view.insert_selections(window, cx);
+            let workspace = view.workspace.upgrade().unwrap();
+            let selection = workspace
+                .update(cx, |workspace, cx| {
+                    AgentContextSource::from_active(workspace, cx)?
+                        .read_selection(workspace, false, cx)
+                })
+                .unwrap();
+            view.insert_selection(selection, window, cx);
         });
 
         message_editor.read_with(cx, |editor, cx| {

crates/agent_ui/src/message_editor.rs 🔗

@@ -3,8 +3,8 @@ use crate::SendImmediately;
 use crate::{
     ChatWithFollow,
     completion_provider::{
-        PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextAction,
-        PromptContextType, SlashCommandCompletion,
+        AgentContextSelection, PromptCompletionProvider, PromptCompletionProviderDelegate,
+        PromptContextAction, PromptContextType, SlashCommandCompletion,
     },
     mention_set::{Mention, MentionImage, MentionSet, insert_crease_for_mention},
 };
@@ -1365,7 +1365,12 @@ impl MessageEditor {
             .detach_and_log_err(cx);
     }
 
-    pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+    pub(crate) fn insert_selections(
+        &mut self,
+        selection: AgentContextSelection,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
         let editor = self.editor.read(cx);
         let editor_buffer = editor.buffer().read(cx);
         let Some(buffer) = editor_buffer.as_singleton() else {
@@ -1376,17 +1381,13 @@ impl MessageEditor {
         let anchor = buffer.update(cx, |buffer, _cx| {
             buffer.anchor_before(cursor_offset.0.min(buffer.len()))
         });
-        let Some(workspace) = self.workspace.upgrade() else {
-            return;
-        };
         let Some(completion) =
             PromptCompletionProvider::<MessageEditorCompletionDelegate>::completion_for_action(
                 PromptContextAction::AddSelections,
                 anchor..anchor,
                 self.editor.downgrade(),
                 self.mention_set.downgrade(),
-                &workspace,
-                cx,
+                Some(selection),
             )
         else {
             return;
@@ -2010,7 +2011,7 @@ mod tests {
     use util::{path, paths::PathStyle, rel_path::rel_path};
     use workspace::{AppState, Item, MultiWorkspace};
 
-    use crate::completion_provider::PromptContextType;
+    use crate::completion_provider::{AgentContextSelection, PromptContextType};
     use crate::{
         conversation_view::tests::init_test,
         mention_set::insert_crease_for_mention,
@@ -3731,11 +3732,17 @@ mod tests {
             })
         });
 
-        // Now let's insert the selection in the Agent Panel's editor and
-        // confirm that, after the insertion, the cursor is now in the visible
-        // range.
+        let text_editor_selection = editor.update(&mut cx, |editor, cx| {
+            let multibuffer = editor.buffer().read(cx);
+            let buffer = multibuffer.as_singleton().unwrap();
+            let buffer_snapshot = buffer.read(cx).snapshot();
+            let start = buffer_snapshot.anchor_before(0);
+            let end = buffer_snapshot.anchor_after(5);
+            AgentContextSelection::Editor(vec![(buffer, start..end)])
+        });
+
         message_editor.update_in(&mut cx, |message_editor, window, cx| {
-            message_editor.insert_selections(window, cx);
+            message_editor.insert_selections(text_editor_selection, window, cx);
         });
 
         cx.run_until_parked();

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -1539,11 +1539,7 @@ impl Focusable for TerminalPanel {
 
 impl Panel for TerminalPanel {
     fn position(&self, _window: &Window, cx: &App) -> DockPosition {
-        match TerminalSettings::get_global(cx).dock {
-            TerminalDockPosition::Left => DockPosition::Left,
-            TerminalDockPosition::Bottom => DockPosition::Bottom,
-            TerminalDockPosition::Right => DockPosition::Right,
-        }
+        TerminalSettings::get_global(cx).dock.into()
     }
 
     fn position_is_valid(&self, _: DockPosition) -> bool {

crates/workspace/src/dock.rs 🔗

@@ -13,7 +13,7 @@ use gpui::{
     px,
 };
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore};
+use settings::{Settings, SettingsStore, TerminalDockPosition};
 use std::sync::Arc;
 use ui::{
     ContextMenu, CountBadge, Divider, DividerColor, IconButton, Tooltip, prelude::*,
@@ -301,6 +301,16 @@ impl Into<settings::DockPosition> for DockPosition {
     }
 }
 
+impl From<TerminalDockPosition> for DockPosition {
+    fn from(value: TerminalDockPosition) -> Self {
+        match value {
+            TerminalDockPosition::Left => DockPosition::Left,
+            TerminalDockPosition::Bottom => DockPosition::Bottom,
+            TerminalDockPosition::Right => DockPosition::Right,
+        }
+    }
+}
+
 impl DockPosition {
     fn label(&self) -> &'static str {
         match self {