Detailed changes
@@ -1132,6 +1132,7 @@
"ctrl-alt-r": "terminal::RerunTask",
"alt-t": "terminal::RerunTask",
"ctrl-shift-5": "pane::SplitRight",
+ "ctrl->": "agent::AddSelectionToThread",
},
},
{
@@ -1211,6 +1211,7 @@
"ctrl-alt-right": "pane::SplitRight",
"cmd-d": "pane::SplitRight",
"cmd-alt-r": "terminal::RerunTask",
+ "cmd->": "agent::AddSelectionToThread",
},
},
{
@@ -1150,6 +1150,7 @@
"ctrl-alt-r": "terminal::RerunTask",
"alt-t": "terminal::RerunTask",
"ctrl-shift-5": "pane::SplitRight",
+ "ctrl-shift-.": "agent::AddSelectionToThread",
},
},
{
@@ -54,6 +54,7 @@ pub enum MentionUri {
Fetch {
url: Url,
},
+ TerminalSelection,
}
impl MentionUri {
@@ -199,6 +200,8 @@ impl MentionUri {
abs_path: Some(path.into()),
line_range,
})
+ } else if path.starts_with("/agent/terminal-selection") {
+ Ok(Self::TerminalSelection)
} else {
bail!("invalid zed url: {:?}", input);
}
@@ -221,6 +224,7 @@ impl MentionUri {
MentionUri::TextThread { name, .. } => name.clone(),
MentionUri::Rule { name, .. } => name.clone(),
MentionUri::Diagnostics { .. } => "Diagnostics".to_string(),
+ MentionUri::TerminalSelection => "Terminal".to_string(),
MentionUri::Selection {
abs_path: path,
line_range,
@@ -243,6 +247,7 @@ impl MentionUri {
MentionUri::TextThread { .. } => IconName::Thread.path().into(),
MentionUri::Rule { .. } => IconName::Reader.path().into(),
MentionUri::Diagnostics { .. } => IconName::Warning.path().into(),
+ MentionUri::TerminalSelection => IconName::Terminal.path().into(),
MentionUri::Selection { .. } => IconName::Reader.path().into(),
MentionUri::Fetch { .. } => IconName::ToolWeb.path().into(),
}
@@ -337,6 +342,7 @@ impl MentionUri {
url
}
MentionUri::Fetch { url } => url.clone(),
+ MentionUri::TerminalSelection => Url::parse("zed:///agent/terminal-selection").unwrap(),
}
}
}
@@ -641,4 +647,16 @@ mod tests {
_ => panic!("Expected Selection variant"),
}
}
+
+ #[test]
+ fn test_parse_terminal_selection_uri() {
+ let terminal_uri = "zed:///agent/terminal-selection";
+ let parsed = MentionUri::parse(terminal_uri, PathStyle::local()).unwrap();
+ match &parsed {
+ MentionUri::TerminalSelection => {}
+ _ => panic!("Expected Terminal variant"),
+ }
+ assert_eq!(parsed.to_uri().to_string(), terminal_uri);
+ assert_eq!(parsed.name(), "Terminal");
+ }
}
@@ -317,6 +317,17 @@ impl UserMessage {
MentionUri::Diagnostics { .. } => {
write!(&mut diagnostics_context, "\n{}\n", content).ok();
}
+ MentionUri::TerminalSelection => {
+ write!(
+ &mut selection_context,
+ "\n{}",
+ MarkdownCodeBlock {
+ tag: "console",
+ text: content
+ }
+ )
+ .ok();
+ }
}
language_model::MessageContent::Text(uri.as_link().to_string())
@@ -1002,62 +1002,130 @@ impl MessageEditor {
creases: Vec<(String, String)>,
window: &mut Window,
cx: &mut Context<Self>,
+ ) {
+ self.editor.update(cx, |editor, cx| {
+ editor.insert("\n", window, cx);
+ });
+ for (text, crease_title) in creases {
+ self.insert_crease_impl(text, crease_title, IconName::TextSnippet, true, window, cx);
+ }
+ }
+
+ pub fn insert_terminal_crease(
+ &mut self,
+ text: String,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let mention_uri = MentionUri::TerminalSelection;
+ let mention_text = mention_uri.as_link().to_string();
+
+ let (excerpt_id, text_anchor, content_len) = self.editor.update(cx, |editor, cx| {
+ let buffer = editor.buffer().read(cx);
+ let snapshot = buffer.snapshot(cx);
+ let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
+ let text_anchor = editor
+ .selections
+ .newest_anchor()
+ .start
+ .text_anchor
+ .bias_left(&buffer_snapshot);
+
+ editor.insert(&mention_text, window, cx);
+ editor.insert(" ", window, cx);
+
+ (*excerpt_id, text_anchor, mention_text.len())
+ });
+
+ let Some((crease_id, tx)) = insert_crease_for_mention(
+ excerpt_id,
+ text_anchor,
+ content_len,
+ mention_uri.name().into(),
+ mention_uri.icon_path(cx),
+ None,
+ self.editor.clone(),
+ window,
+ cx,
+ ) else {
+ return;
+ };
+ drop(tx);
+
+ let mention_task = Task::ready(Ok(Mention::Text {
+ content: text,
+ tracked_buffers: vec![],
+ }))
+ .shared();
+
+ self.mention_set.update(cx, |mention_set, _| {
+ mention_set.insert_mention(crease_id, mention_uri, mention_task);
+ });
+ }
+
+ fn insert_crease_impl(
+ &mut self,
+ text: String,
+ title: String,
+ icon: IconName,
+ add_trailing_newline: bool,
+ window: &mut Window,
+ cx: &mut Context<Self>,
) {
use editor::display_map::{Crease, FoldPlaceholder};
use multi_buffer::MultiBufferRow;
use rope::Point;
self.editor.update(cx, |editor, cx| {
- editor.insert("\n", window, cx);
- for (text, crease_title) in creases {
- let point = editor
- .selections
- .newest::<Point>(&editor.display_snapshot(cx))
- .head();
- let start_row = MultiBufferRow(point.row);
-
- editor.insert(&text, window, cx);
-
- let snapshot = editor.buffer().read(cx).snapshot(cx);
- let anchor_before = snapshot.anchor_after(point);
- let anchor_after = editor
- .selections
- .newest_anchor()
- .head()
- .bias_left(&snapshot);
+ let point = editor
+ .selections
+ .newest::<Point>(&editor.display_snapshot(cx))
+ .head();
+ let start_row = MultiBufferRow(point.row);
- editor.insert("\n", window, cx);
+ editor.insert(&text, window, cx);
- let fold_placeholder = FoldPlaceholder {
- render: Arc::new({
- let title = crease_title.clone();
- move |_fold_id, _fold_range, _cx| {
- ButtonLike::new("code-crease")
- .style(ButtonStyle::Filled)
- .layer(ElevationIndex::ElevatedSurface)
- .child(Icon::new(IconName::TextSnippet))
- .child(Label::new(title.clone()).single_line())
- .into_any_element()
- }
- }),
- merge_adjacent: false,
- ..Default::default()
- };
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+ let anchor_before = snapshot.anchor_after(point);
+ let anchor_after = editor
+ .selections
+ .newest_anchor()
+ .head()
+ .bias_left(&snapshot);
- let crease = Crease::inline(
- anchor_before..anchor_after,
- fold_placeholder,
- |row, is_folded, fold, _window, _cx| {
- Disclosure::new(("code-crease-toggle", row.0 as u64), !is_folded)
- .toggle_state(is_folded)
- .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
- .into_any_element()
- },
- |_, _, _, _| gpui::Empty.into_any(),
- );
- editor.insert_creases(vec![crease], cx);
- editor.fold_at(start_row, window, cx);
+ if add_trailing_newline {
+ editor.insert("\n", window, cx);
}
+
+ let fold_placeholder = FoldPlaceholder {
+ render: Arc::new({
+ let title = title.clone();
+ move |_fold_id, _fold_range, _cx| {
+ ButtonLike::new("crease")
+ .style(ButtonStyle::Filled)
+ .layer(ElevationIndex::ElevatedSurface)
+ .child(Icon::new(icon))
+ .child(Label::new(title.clone()).single_line())
+ .into_any_element()
+ }
+ }),
+ merge_adjacent: false,
+ ..Default::default()
+ };
+
+ let crease = Crease::inline(
+ anchor_before..anchor_after,
+ fold_placeholder,
+ |row, is_folded, fold, _window, _cx| {
+ Disclosure::new(("crease-toggle", row.0 as u64), !is_folded)
+ .toggle_state(is_folded)
+ .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
+ .into_any_element()
+ },
+ |_, _, _, _| gpui::Empty.into_any(),
+ );
+ editor.insert_creases(vec![crease], cx);
+ editor.fold_at(start_row, window, cx);
});
}
@@ -6870,6 +6870,7 @@ impl AcpThreadView {
cx.open_url(url.as_str());
}
MentionUri::Diagnostics { .. } => {}
+ MentionUri::TerminalSelection => {}
})
} else {
cx.open_url(&url);
@@ -7651,6 +7652,18 @@ impl AcpThreadView {
});
}
+ /// Inserts terminal text as a crease into the message editor.
+ pub(crate) fn insert_terminal_text(
+ &self,
+ text: String,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.message_editor.update(cx, |message_editor, cx| {
+ message_editor.insert_terminal_crease(text, window, cx);
+ });
+ }
+
/// Inserts code snippets as creases into the message editor.
pub(crate) fn insert_code_crease(
&self,
@@ -2964,6 +2964,38 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
});
});
}
+
+ fn quote_terminal_text(
+ &self,
+ workspace: &mut Workspace,
+ text: String,
+ window: &mut Window,
+ cx: &mut Context<Workspace>,
+ ) {
+ let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
+ return;
+ };
+
+ if !panel.focus_handle(cx).contains_focused(window, cx) {
+ workspace.toggle_panel_focus::<AgentPanel>(window, cx);
+ }
+
+ panel.update(cx, |_, cx| {
+ // Wait to create a new context until the workspace is no longer
+ // being updated.
+ cx.defer_in(window, move |panel, window, cx| {
+ if let Some(thread_view) = panel.active_thread_view() {
+ thread_view.update(cx, |thread_view, cx| {
+ thread_view.insert_terminal_text(text, window, cx);
+ });
+ } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
+ text_thread_editor.update(cx, |text_thread_editor, cx| {
+ text_thread_editor.quote_terminal_text(text, window, cx)
+ });
+ }
+ });
+ });
+ }
}
struct OnboardingUpsell;
@@ -13,10 +13,12 @@ use editor::{
CompletionProvider, Editor, ExcerptId, code_context_menus::COMPLETION_MENU_MAX_WIDTH,
};
use feature_flags::{FeatureFlagAppExt as _, UserSlashCommandsFeatureFlag};
+use futures::FutureExt as _;
use fuzzy::{PathMatch, StringMatch, StringMatchCandidate};
use gpui::{App, BackgroundExecutor, Entity, SharedString, Task, WeakEntity};
use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId};
use lsp::CompletionContext;
+use multi_buffer::ToOffset as _;
use ordered_float::OrderedFloat;
use project::lsp_store::{CompletionDocumentation, SymbolLocation};
use project::{
@@ -25,7 +27,10 @@ use project::{
};
use prompt_store::{PromptStore, UserPromptId};
use rope::Point;
-use text::{Anchor, ToPoint as _};
+use settings::{Settings, TerminalDockPosition};
+use terminal::terminal_settings::TerminalSettings;
+use terminal_view::terminal_panel::TerminalPanel;
+use text::{Anchor, ToOffset as _, ToPoint as _};
use ui::IconName;
use ui::prelude::*;
use util::ResultExt as _;
@@ -33,6 +38,7 @@ 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;
@@ -554,48 +560,129 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
) -> Option<Completion> {
let (new_text, on_action) = match action {
PromptContextAction::AddSelections => {
- const PLACEHOLDER: &str = "selection ";
- let selections = selection_ranges(workspace, cx)
+ // 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_if_panel_open(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,
- (PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1),
+ (EDITOR_PLACEHOLDER.len() * ix)
+ ..(EDITOR_PLACEHOLDER.len() * (ix + 1) - 1),
)
})
.collect::<Vec<_>>();
- let new_text: String = PLACEHOLDER.repeat(selections.len());
+ 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 |_, window: &mut Window, cx: &mut App| {
+ 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() {
- mention_set
- .update(cx, |store, cx| {
- store.confirm_mention_for_selection(
- source_range,
- selections,
- editor,
- window,
- cx,
- )
- })
- .ok();
+ // 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.as_singleton_anchor(source_range.start)
+ else {
+ return;
+ };
+ let offset = start.to_offset(&snapshot);
+
+ let mention_uri = MentionUri::TerminalSelection;
+ 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),
+ 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)
+ (
+ new_text,
+ callback
+ as Arc<
+ dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync,
+ >,
+ )
}
};
@@ -1087,7 +1174,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
entries.push(PromptContextEntry::Mode(PromptContextType::Thread));
}
- let has_selection = workspace
+ let has_editor_selection = workspace
.read(cx)
.active_item(cx)
.and_then(|item| item.downcast::<Editor>())
@@ -1096,7 +1183,10 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
editor.has_non_empty_selection(&editor.display_snapshot(cx))
})
});
- if has_selection {
+
+ let has_terminal_selection = !terminal_selections_if_panel_open(workspace, cx).is_empty();
+
+ if has_editor_selection || has_terminal_selection {
entries.push(PromptContextEntry::Action(
PromptContextAction::AddSelections,
));
@@ -2106,6 +2196,30 @@ fn build_code_label_for_path(
label.build()
}
+/// Returns terminal selections from all terminal views if the terminal panel is open.
+fn terminal_selections_if_panel_open(workspace: &Entity<Workspace>, cx: &App) -> Vec<String> {
+ let Some(panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
+ return Vec::new();
+ };
+
+ // Check if the dock containing this panel is open
+ 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 {
+ return Vec::new();
+ }
+
+ panel.read(cx).terminal_selections(cx)
+}
+
fn selection_ranges(
workspace: &Entity<Workspace>,
cx: &mut App,
@@ -249,6 +249,10 @@ impl MentionSet {
debug_panic!("unexpected selection URI");
Task::ready(Err(anyhow!("unexpected selection URI")))
}
+ MentionUri::TerminalSelection => {
+ debug_panic!("unexpected terminal URI");
+ Task::ready(Err(anyhow!("unexpected terminal URI")))
+ }
};
let task = cx
.spawn(async move |_, _| task.await.map_err(|e| e.to_string()))
@@ -64,8 +64,11 @@ use workspace::{
CollaboratorId,
searchable::{Direction, SearchableItemHandle},
};
+
+use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use workspace::{
Save, Toast, Workspace,
+ dock::Panel,
item::{self, FollowableItem, Item},
notifications::NotificationId,
pane,
@@ -159,6 +162,14 @@ pub trait AgentPanelDelegate {
window: &mut Window,
cx: &mut Context<Workspace>,
);
+
+ fn quote_terminal_text(
+ &self,
+ workspace: &mut Workspace,
+ text: String,
+ window: &mut Window,
+ cx: &mut Context<Workspace>,
+ );
}
impl dyn AgentPanelDelegate {
@@ -1487,7 +1498,40 @@ impl TextThreadEditor {
return;
};
- let Some((selections, buffer)) = maybe!({
+ // Try terminal selection first (requires focus, so more specific)
+ if let Some(terminal_text) = maybe!({
+ let terminal_panel = workspace.panel::<TerminalPanel>(cx)?;
+
+ if !terminal_panel
+ .read(cx)
+ .focus_handle(cx)
+ .contains_focused(window, cx)
+ {
+ return None;
+ }
+
+ let terminal_view = terminal_panel.read(cx).pane().and_then(|pane| {
+ pane.read(cx)
+ .active_item()
+ .and_then(|t| t.downcast::<TerminalView>())
+ })?;
+
+ terminal_view
+ .read(cx)
+ .terminal()
+ .read(cx)
+ .last_content
+ .selection_text
+ .clone()
+ }) {
+ if !terminal_text.is_empty() {
+ agent_panel_delegate.quote_terminal_text(workspace, terminal_text, window, cx);
+ return;
+ }
+ }
+
+ // Try editor selection
+ if let Some((selections, buffer)) = maybe!({
let editor = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))?;
@@ -1506,15 +1550,11 @@ impl TextThreadEditor {
.collect::<Vec<_>>()
});
Some((selections, buffer))
- }) else {
- return;
- };
-
- if selections.is_empty() {
- return;
+ }) {
+ if !selections.is_empty() {
+ agent_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
+ }
}
-
- agent_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
}
/// Handles the SendReviewToAgent action from the ProjectDiff toolbar.
@@ -1714,6 +1754,54 @@ impl TextThreadEditor {
})
}
+ pub fn quote_terminal_text(
+ &mut self,
+ text: String,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let crease_title = "terminal".to_string();
+ let formatted_text = format!("```console\n{}\n```\n", text);
+
+ self.editor.update(cx, |editor, cx| {
+ // Insert newline first if not at the start of a line
+ let point = editor
+ .selections
+ .newest::<Point>(&editor.display_snapshot(cx))
+ .head();
+ if point.column > 0 {
+ editor.insert("\n", window, cx);
+ }
+
+ let point = editor
+ .selections
+ .newest::<Point>(&editor.display_snapshot(cx))
+ .head();
+ let start_row = MultiBufferRow(point.row);
+
+ editor.insert(&formatted_text, window, cx);
+
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+ let anchor_before = snapshot.anchor_after(point);
+ let anchor_after = editor
+ .selections
+ .newest_anchor()
+ .head()
+ .bias_left(&snapshot);
+
+ let fold_placeholder =
+ quote_selection_fold_placeholder(crease_title, cx.entity().downgrade());
+ let crease = Crease::inline(
+ anchor_before..anchor_after,
+ fold_placeholder,
+ render_quote_selection_output_toggle,
+ |_, _, _, _| Empty.into_any(),
+ );
+ editor.insert_creases(vec![crease], cx);
+ editor.fold_at(start_row, window, cx);
+ })
+ }
+
fn copy(&mut self, _: &editor::actions::Copy, _window: &mut Window, cx: &mut Context<Self>) {
if self.editor.read(cx).selections.count() == 1 {
let (copied_text, metadata, _) = self.get_clipboard_contents(cx);
@@ -3548,4 +3636,26 @@ mod tests {
theme::init(theme::LoadThemes::JustBase, cx);
}
+
+ #[gpui::test]
+ async fn test_quote_terminal_text(cx: &mut TestAppContext) {
+ let (_context, text_thread_editor, mut cx) =
+ setup_text_thread_editor_text(vec![(Role::User, "")], cx).await;
+
+ let terminal_output = "$ ls -la\ntotal 0\ndrwxr-xr-x 2 user user 40 Jan 1 00:00 .";
+
+ text_thread_editor.update_in(&mut cx, |text_thread_editor, window, cx| {
+ text_thread_editor.quote_terminal_text(terminal_output.to_string(), window, cx);
+
+ text_thread_editor.editor.update(cx, |editor, cx| {
+ let text = editor.text(cx);
+ // The text should contain the terminal output wrapped in a code block
+ assert!(
+ text.contains(&format!("```console\n{}\n```", terminal_output)),
+ "Terminal text should be wrapped in code block. Got: {}",
+ text
+ );
+ });
+ });
+ }
}
@@ -1084,6 +1084,32 @@ impl TerminalPanel {
self.assistant_enabled
}
+ /// Returns all panes in the terminal panel.
+ pub fn panes(&self) -> Vec<&Entity<Pane>> {
+ self.center.panes()
+ }
+
+ /// Returns all non-empty terminal selections from all terminal views in all panes.
+ pub fn terminal_selections(&self, cx: &App) -> Vec<String> {
+ self.center
+ .panes()
+ .iter()
+ .flat_map(|pane| {
+ pane.read(cx).items().filter_map(|item| {
+ let terminal_view = item.downcast::<crate::TerminalView>()?;
+ terminal_view
+ .read(cx)
+ .terminal()
+ .read(cx)
+ .last_content
+ .selection_text
+ .clone()
+ .filter(|text| !text.is_empty())
+ })
+ })
+ .collect()
+ }
+
fn is_enabled(&self, cx: &App) -> bool {
self.workspace
.upgrade()
@@ -19,6 +19,14 @@ use project::{Project, search::SearchQuery};
use schemars::JsonSchema;
use serde::Deserialize;
use settings::{Settings, SettingsStore, TerminalBlink, WorkingDirectory};
+use std::{
+ cmp,
+ ops::{Range, RangeInclusive},
+ path::{Path, PathBuf},
+ rc::Rc,
+ sync::Arc,
+ time::Duration,
+};
use task::TaskId;
use terminal::{
Clear, Copy, Event, HoveredWord, MaybeNavigationTarget, Paste, ScrollLineDown, ScrollLineUp,
@@ -50,16 +58,7 @@ use workspace::{
register_serializable_item,
searchable::{Direction, SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
};
-use zed_actions::assistant::InlineAssist;
-
-use std::{
- cmp,
- ops::{Range, RangeInclusive},
- path::{Path, PathBuf},
- rc::Rc,
- sync::Arc,
- time::Duration,
-};
+use zed_actions::{agent::AddSelectionToThread, assistant::InlineAssist};
struct ImeState {
marked_text: String,
@@ -496,6 +495,13 @@ impl TerminalView {
.upgrade()
.and_then(|workspace| workspace.read(cx).panel::<TerminalPanel>(cx))
.is_some_and(|terminal_panel| terminal_panel.read(cx).assistant_enabled());
+ let has_selection = self
+ .terminal
+ .read(cx)
+ .last_content
+ .selection_text
+ .as_ref()
+ .is_some_and(|text| !text.is_empty());
let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
menu.context(self.focus_handle.clone())
.action("New Terminal", Box::new(NewTerminal::default()))
@@ -507,6 +513,9 @@ impl TerminalView {
.when(assistant_enabled, |menu| {
menu.separator()
.action("Inline Assist", Box::new(InlineAssist::default()))
+ .when(has_selection, |menu| {
+ menu.action("Add to Agent Thread", Box::new(AddSelectionToThread))
+ })
})
.separator()
.action(