diff --git a/app/src/ai/execution_profiles/profiles.rs b/app/src/ai/execution_profiles/profiles.rs index 56b1fc006e1..89f8d58ec6a 100644 --- a/app/src/ai/execution_profiles/profiles.rs +++ b/app/src/ai/execution_profiles/profiles.rs @@ -2460,6 +2460,6 @@ impl Entity for AIExecutionProfilesModel { impl SingletonEntity for AIExecutionProfilesModel {} -#[cfg(test)] +#[cfg(all(test, not(feature = "agent_mode_evals")))] #[path = "profiles_tests.rs"] mod tests; diff --git a/app/src/lib.rs b/app/src/lib.rs index 697f0248d48..6bf288ffe8f 100644 --- a/app/src/lib.rs +++ b/app/src/lib.rs @@ -398,6 +398,7 @@ pub(crate) enum LaunchMode { /// Whether this CLI invocation is running in a sandboxed environment. is_sandboxed: bool, /// Override for computer use permission from CLI flags. If None, uses default behavior. + #[allow(dead_code)] computer_use_override: Option, }, /// Run a test - this may be an integration test or an eval. diff --git a/app/src/terminal/local_tty/terminal_manager.rs b/app/src/terminal/local_tty/terminal_manager.rs index 13d8cd2d23f..821e5e8cabb 100644 --- a/app/src/terminal/local_tty/terminal_manager.rs +++ b/app/src/terminal/local_tty/terminal_manager.rs @@ -94,6 +94,7 @@ pub struct TerminalManager { pty_controller: ModelHandle, /// The manager is responsible for managing the lifetime of the remote server controller. + #[allow(dead_code)] remote_server_controller: ModelHandle, /// The process ID of the PTY. Purely used for integration tests. None if the PTY has not yet @@ -194,6 +195,7 @@ impl Drop for TerminalManager { impl TerminalManager { /// Creates a local terminal manager model and terminal surface. #[allow(clippy::too_many_arguments)] + #[allow(dead_code)] pub(crate) fn create_model( startup_directory: Option, env_vars: HashMap, @@ -500,6 +502,7 @@ impl TerminalManager { } /// Returns the remote server controller owned by this manager. + #[allow(dead_code)] pub(super) fn remote_server_controller(&self) -> ModelHandle { self.remote_server_controller.clone() } diff --git a/app/src/terminal/local_tty/terminal_view_adaptor.rs b/app/src/terminal/local_tty/terminal_view_adaptor.rs index e69b90b2d22..cef2f4a8df8 100644 --- a/app/src/terminal/local_tty/terminal_view_adaptor.rs +++ b/app/src/terminal/local_tty/terminal_view_adaptor.rs @@ -71,6 +71,7 @@ use crate::terminal::writeable_pty::terminal_manager_util::wire_up_remote_server use crate::terminal::{TerminalManager as TerminalManagerTrait, TerminalModel, TerminalView}; use crate::view_components::ToastFlavor; +#[allow(dead_code)] const ACL_UPDATE_FAILURE_RESPONSE: &str = "Something went wrong. Please try again."; /// Whether the given CRDT operation should be dropped when broadcasting @@ -78,11 +79,13 @@ const ACL_UPDATE_FAILURE_RESPONSE: &str = "Something went wrong. Please try agai /// headless worker — forwarding its selection ops would produce a phantom /// cursor on the viewer side. Content ops (Edit / Undo) are kept so the /// buffer stays in sync. +#[allow(dead_code)] fn should_skip_sharer_op(is_ambient_session: bool, op: &CrdtOperation) -> bool { is_ambient_session && matches!(op, CrdtOperation::UpdateSelections(_)) } /// Configuration for constructing the GUI terminal surface. +#[allow(dead_code)] pub(crate) struct TerminalViewSurfaceConfig { pub(crate) resources: TerminalViewResources, pub(crate) model_event_sender: Option>, @@ -96,6 +99,7 @@ pub(crate) struct TerminalViewSurfaceConfig { } /// Resolves the block list used by the GUI `TerminalView` surface. +#[allow(dead_code)] pub(crate) fn terminal_view_restored_blocks( restored_blocks: Option<&Vec>, conversation_restoration: &Option, @@ -122,6 +126,7 @@ pub(crate) fn terminal_view_restored_blocks( } /// Creates the GUI terminal surface and its manager-owned post-wiring closure. +#[allow(dead_code)] pub(crate) fn create_terminal_view_surface( config: TerminalViewSurfaceConfig, surface_init: TerminalSurfaceInit, @@ -240,6 +245,7 @@ pub(crate) fn create_terminal_view_surface( /// remain unchanged; this helper groups the `TerminalView`-dependent wiring so it is /// easy to identify and work on separately from the generic manager. #[allow(clippy::too_many_arguments)] +#[allow(dead_code)] fn wire_up_terminal_view_session_sharing( view: &ViewHandle, current_prompt: ModelHandle, @@ -613,6 +619,7 @@ fn wire_up_terminal_view_session_sharing( session_sharer } +#[allow(dead_code)] impl TerminalManager { /// Streams all historical agent conversations from this terminal to viewers. /// This is called when starting a shared session mid-conversation so that viewers diff --git a/app/src/terminal/shared_session/presence_manager.rs b/app/src/terminal/shared_session/presence_manager.rs index ee6b612ecd7..62ebd6e3f92 100644 --- a/app/src/terminal/shared_session/presence_manager.rs +++ b/app/src/terminal/shared_session/presence_manager.rs @@ -777,6 +777,7 @@ impl PresenceManager { /// Like `single_distinct_present_viewer_uid`, but reads directly from a /// participant list before the presence manager finishes processing it. #[cfg(not(target_arch = "wasm32"))] + #[allow(dead_code)] pub(crate) fn single_distinct_present_viewer_uid_from_viewers<'a>( viewers: impl Iterator, ) -> Option<&'a str> { diff --git a/app/src/terminal/view.rs b/app/src/terminal/view.rs index 57ade30dd35..d5215c4f83e 100644 --- a/app/src/terminal/view.rs +++ b/app/src/terminal/view.rs @@ -1172,6 +1172,7 @@ impl SizeUpdateBuilder { } #[cfg(not(target_arch = "wasm32"))] + #[allow(dead_code)] fn for_viewer_size_report(last_size: SizeInfo, num_rows: usize, num_cols: usize) -> Self { // Viewer size reports don't change the sharer's actual pane size. Self { @@ -1439,6 +1440,12 @@ pub enum ContextMenuAction { exchange_id: AIAgentExchangeId, conversation_id: AIConversationId, }, + /// Fork the AI conversation from the selected text in this AI block. + ForkAIConversationFromSelectedText { + ai_block_view_id: EntityId, + exchange_id: AIAgentExchangeId, + conversation_id: AIConversationId, + }, /// Save the AI block prompt as an agent mode workflow (saved prompt) SavePromptAsAgentModeWorkflow { ai_block_view_id: EntityId, @@ -1524,6 +1531,9 @@ impl fmt::Debug for ContextMenuAction { ForkAIConversationFromExactExchange { .. } => { f.write_str("ForkAIConversationFromExactExchange") } + ForkAIConversationFromSelectedText { .. } => { + f.write_str("ForkAIConversationFromSelectedText") + } SavePromptAsAgentModeWorkflow { .. } => f.write_str("SavePromptAsAgentModeWorkflow"), } } @@ -16644,6 +16654,35 @@ impl TerminalView { .into_item(), ]); } + if !cfg!(target_family = "wasm") + && let BlockListMenuSource::RichContentTextRightClick { + rich_content_view_id, + .. + } = menu_source + && let Some(ai_metadata) = + self.ai_block_metadata_for_view_id(*rich_content_view_id) + && self + .fork_selected_text_params( + *rich_content_view_id, + ai_metadata.exchange_id, + ai_metadata.conversation_id, + ctx, + ) + .is_some() + { + fields.extend([ + MenuItem::Separator, + MenuItemFields::new("Fork selection in new pane") + .with_on_select_action(TerminalAction::ContextMenu( + ContextMenuAction::ForkAIConversationFromSelectedText { + ai_block_view_id: *rich_content_view_id, + exchange_id: ai_metadata.exchange_id, + conversation_id: ai_metadata.conversation_id, + }, + )) + .into_item(), + ]); + } fields } ( @@ -20671,6 +20710,42 @@ impl TerminalView { }) } + fn ai_block_metadata_for_view_id(&self, view_id: EntityId) -> Option<&AIBlockMetadata> { + self.rich_content_views.iter().find_map(|rich_content| { + let ai_metadata = rich_content.ai_block_metadata()?; + if ai_metadata.ai_block_handle.id() == view_id { + return Some(ai_metadata); + } + None + }) + } + + fn fork_selected_text_params( + &self, + ai_block_view_id: EntityId, + exchange_id: AIAgentExchangeId, + conversation_id: AIConversationId, + ctx: &AppContext, + ) -> Option { + let initial_prompt = self + .ai_block_handle_by_view_id(ai_block_view_id)? + .as_ref(ctx) + .selected_text(ctx) + .filter(|text| !text.trim().is_empty())?; + + Some(ForkAIConversationParams { + conversation_id, + fork_from_exchange: Some(ForkFromExchange { + exchange_id, + fork_from_exact_exchange: false, + }), + summarize_after_fork: false, + summarization_prompt: None, + initial_prompt: Some(initial_prompt), + destination: ForkedConversationDestination::SplitPane, + }) + } + /// Returns the last block's `EnvVarCollectionBlock` if it is uncompleted, scoped to the /// currently visible conversation. fn active_env_var_collection_block( @@ -24793,6 +24868,20 @@ impl TerminalView { ctx, ); } + ForkAIConversationFromSelectedText { + ai_block_view_id, + exchange_id, + conversation_id, + } => { + if let Some(params) = self.fork_selected_text_params( + *ai_block_view_id, + *exchange_id, + *conversation_id, + ctx, + ) { + ctx.dispatch_global_action("workspace:fork_ai_conversation", params); + } + } SavePromptAsAgentModeWorkflow { ai_block_view_id } => { for rich_content in self.rich_content_views.iter() { if let Some(ai_metadata) = rich_content.ai_block_metadata() diff --git a/app/src/terminal/view/shared_session/view_impl.rs b/app/src/terminal/view/shared_session/view_impl.rs index e98d81daac3..e8d529fd38f 100644 --- a/app/src/terminal/view/shared_session/view_impl.rs +++ b/app/src/terminal/view/shared_session/view_impl.rs @@ -2020,6 +2020,7 @@ impl TerminalView { /// Resizes the sharer's terminal to match the viewer's reported size, /// going through the normal view/model/PTY resize pipeline. #[cfg(not(target_arch = "wasm32"))] + #[allow(dead_code)] pub(crate) fn resize_from_viewer_report( &mut self, viewer_size: WindowSize, diff --git a/app/src/terminal/view_tests.rs b/app/src/terminal/view_tests.rs index 25ee0326cba..8a7ecde7296 100644 --- a/app/src/terminal/view_tests.rs +++ b/app/src/terminal/view_tests.rs @@ -8009,6 +8009,113 @@ fn cmd_k_does_not_clear_buffer_when_agent_is_driving_command() { }) } +#[test] +fn selected_ai_block_text_can_be_forked_in_new_pane() { + App::test((), |mut app| async move { + initialize_app_for_terminal_view(&mut app); + let _agent_view = FeatureFlag::AgentView.override_enabled(true); + let terminal = add_window_with_terminal(&mut app, None); + + terminal.update(&mut app, |view, ctx| { + append_exchange_and_handle_event( + view, + AIAgentInput::UserQuery { + query: "explain the selected text".to_owned(), + context: Default::default(), + static_query_type: None, + referenced_attachments: Default::default(), + user_query_mode: UserQueryMode::Normal, + running_command: None, + intended_agent: None, + }, + ctx, + ); + }); + + let (ai_block, exchange_id, conversation_id) = terminal.read(&app, |view, _| { + view.rich_content_views + .iter() + .find_map(|rich_content| { + let metadata = rich_content.ai_block_metadata()?; + Some(( + metadata.ai_block_handle.clone(), + metadata.exchange_id, + metadata.conversation_id, + )) + }) + .expect("an AI block should have been inserted") + }); + + ai_block.update(&mut app, |block, ctx| { + block.set_block_level_selected_text_for_test(Some("selected agent text".to_owned())); + block.handle_action(&AIBlockAction::SelectText, ctx); + }); + + terminal.update(&mut app, |view, ctx| { + let params = view + .fork_selected_text_params(ai_block.id(), exchange_id, conversation_id, ctx) + .expect("selected AI block text should produce fork params"); + + assert_eq!(params.conversation_id, conversation_id); + assert_eq!( + params.initial_prompt, + Some("selected agent text".to_owned()) + ); + assert!(matches!( + params.fork_from_exchange, + Some(ForkFromExchange { + exchange_id: actual_exchange_id, + fork_from_exact_exchange: false, + }) if actual_exchange_id == exchange_id + )); + assert_eq!(params.destination, ForkedConversationDestination::SplitPane); + + let menu_source = BlockListMenuSource::RichContentTextRightClick { + rich_content_view_id: ai_block.id(), + position_in_rich_content: Vector2F::zero(), + }; + let menu_items = view.context_menu_items(&menu_source, ctx); + let labels = menu_items + .iter() + .filter_map(|item| item.fields().map(|fields| fields.label())) + .collect::>(); + assert!( + labels.contains(&"Fork selection in new pane"), + "Expected selected AI text menu to include fork action, got {labels:?}" + ); + }); + }) +} + +#[test] +fn regular_selected_text_context_menu_does_not_include_fork_selection() { + App::test((), |mut app| async move { + initialize_app_for_terminal_view(&mut app); + + let terminal = add_window_with_terminal(&mut app, None); + terminal.update(&mut app, |view, ctx| { + { + let mut model = view.model.lock(); + model.simulate_block("ls", "foo"); + assert!(!model.is_block_list_empty()); + } + + let menu_source = BlockListMenuSource::RegularTextRightClick { + position_in_terminal_view: Vector2F::zero(), + }; + let menu_items = view.context_menu_items(&menu_source, ctx); + let labels = menu_items + .iter() + .filter_map(|item| item.fields().map(|fields| fields.label())) + .collect::>(); + assert!( + !labels.contains(&"Fork selection in new pane"), + "Did not expect regular selected-text menu to include fork action, got {labels:?}" + ); + }); + }) +} + #[test] fn cmd_k_in_agent_view_clears_active_block_not_full_buffer_when_agent_driving_command() { App::test((), |mut app| async move { diff --git a/app/src/terminal/writeable_pty/remote_server_controller.rs b/app/src/terminal/writeable_pty/remote_server_controller.rs index a4d1bc88d7b..6e2ccb3566b 100644 --- a/app/src/terminal/writeable_pty/remote_server_controller.rs +++ b/app/src/terminal/writeable_pty/remote_server_controller.rs @@ -29,6 +29,7 @@ use crate::{TelemetryEvent, send_telemetry_from_ctx}; /// /// Every active state carries `setup_start` so that the total setup duration /// can be measured when the flow reaches `SessionConnected`. +#[allow(dead_code)] enum SshInitState { Idle, /// Stash held, `check_binary` in flight. @@ -352,6 +353,7 @@ impl RemoteServerController { } } + #[allow(dead_code)] pub fn handle_ssh_remote_server_install( &mut self, session_id: SessionId, @@ -476,6 +478,7 @@ impl RemoteServerController { self.flush_stashed_bootstrap(session_info, ctx); } + #[allow(dead_code)] pub fn handle_ssh_remote_server_skip( &mut self, session_id: SessionId, diff --git a/app/src/terminal/writeable_pty/terminal_manager_util.rs b/app/src/terminal/writeable_pty/terminal_manager_util.rs index 6c83f206da4..172ef976128 100644 --- a/app/src/terminal/writeable_pty/terminal_manager_util.rs +++ b/app/src/terminal/writeable_pty/terminal_manager_util.rs @@ -149,6 +149,7 @@ pub fn wire_up_pty_controller_with_surface( remote_server_controller: &ModelHandle< super::remote_server_controller::RemoteServerController, diff --git a/crates/onboarding/src/bin/main.rs b/crates/onboarding/src/bin/main.rs index fb3e63b8f99..dfc8f6d8a02 100644 --- a/crates/onboarding/src/bin/main.rs +++ b/crates/onboarding/src/bin/main.rs @@ -165,6 +165,7 @@ impl OnboardingMainView { | AgentOnboardingEvent::UpgradeRequested | AgentOnboardingEvent::UpgradeCopyUrlRequested | AgentOnboardingEvent::UpgradePasteTokenFromClipboardRequested + | AgentOnboardingEvent::OfferSetUpLaterSelected { .. } | AgentOnboardingEvent::LoginFromWelcomeRequested | AgentOnboardingEvent::PrivacySettingsFromTerminalThemeSlideRequested | AgentOnboardingEvent::AppBecameActive => { diff --git a/crates/warp_completer/src/completer/engine/argument/v2.rs b/crates/warp_completer/src/completer/engine/argument/v2.rs index fb6faf1b4ef..9ab6d60cf1f 100644 --- a/crates/warp_completer/src/completer/engine/argument/v2.rs +++ b/crates/warp_completer/src/completer/engine/argument/v2.rs @@ -51,41 +51,41 @@ pub async fn complete( // True if and only if we called the complete function. let mut arg_has_spec = false; - if let Some(found_signature) = found_signature { - if let hir::Command::Classified(mut shell_command) = classified_command.command { - suggestions = match classified_command.error { - Some(error) => { - let (results, complete_called) = suggestions_for_parse_error( - input, - tokens_without_last_editing, - &location.span, - error, - &mut shell_command, - found_signature, - session_env_vars, - options, - ctx, - ) - .await; - arg_has_spec = complete_called; - results - } - None => { - let (results, complete_called) = suggestions_for_last_argument( - tokens_without_last_editing, - &mut shell_command, - &location.span, - found_signature, - session_env_vars, - options, - ctx, - ) - .await; - arg_has_spec = complete_called; - results - } + if let Some(found_signature) = found_signature + && let hir::Command::Classified(mut shell_command) = classified_command.command + { + suggestions = match classified_command.error { + Some(error) => { + let (results, complete_called) = suggestions_for_parse_error( + input, + tokens_without_last_editing, + &location.span, + error, + &mut shell_command, + found_signature, + session_env_vars, + options, + ctx, + ) + .await; + arg_has_spec = complete_called; + results } - } + None => { + let (results, complete_called) = suggestions_for_last_argument( + tokens_without_last_editing, + &mut shell_command, + &location.span, + found_signature, + session_env_vars, + options, + ctx, + ) + .await; + arg_has_spec = complete_called; + results + } + }; } // If we were unsuccessful in getting completions *and* there was not @@ -96,15 +96,14 @@ pub async fn complete( options.fallback_strategy, CompletionsFallbackStrategy::FilePaths ) + && let Some(path_completion_context) = ctx.path_completion_context() { - if let Some(path_completion_context) = ctx.path_completion_context() { - suggestions = sorted_paths_relative_to( - parsed_argument, - options.match_strategy, - path_completion_context, - ) - .await; - } + suggestions = sorted_paths_relative_to( + parsed_argument, + options.match_strategy, + path_completion_context, + ) + .await; } suggestions @@ -402,35 +401,35 @@ async fn complete_positional( // command. let mut should_suggest_subcommands = true; - if let Some(positionals) = &shell_command.args.positionals.as_ref() { - if !arguments.is_empty() { - let positional_index = positionals.len() - 1; - - let arg = match arguments - .iter() - .enumerate() - .find(|(idx, arg)| idx <= &positional_index && arg.is_variadic()) - { - None => arguments.get(positionals.len() - 1), - Some((_, arg)) => { - // If the argument is optional, it's possible the user is actually trying to - // enter a subcommand. - should_suggest_subcommands = arg.optional; - Some(arg) - } - }; - - if let Some(arg) = arg { - suggestions = generate_suggestions_for_argument( - arg, - parsed_token, - tokens_without_last_editing, - session_env_vars, - options, - ctx, - ) - .await; + if let Some(positionals) = shell_command.args.positionals.as_ref() + && !arguments.is_empty() + { + let positional_index = positionals.len() - 1; + + let arg = match arguments + .iter() + .enumerate() + .find(|(idx, arg)| idx <= &positional_index && arg.is_variadic()) + { + None => arguments.get(positionals.len() - 1), + Some((_, arg)) => { + // If the argument is optional, it's possible the user is actually trying to + // enter a subcommand. + should_suggest_subcommands = arg.optional; + Some(arg) } + }; + + if let Some(arg) = arg { + suggestions = generate_suggestions_for_argument( + arg, + parsed_token, + tokens_without_last_editing, + session_env_vars, + options, + ctx, + ) + .await; } } @@ -566,15 +565,13 @@ async fn generate_suggestions_for_argument_value( type_name: TemplateType::Files, .. } => { - let path_suggestions = match ctx.path_completion_context() { + // TODO(completions-v2): Implement template filter functions. + match ctx.path_completion_context() { Some(path_completion_context) => { sorted_paths_relative_to(parsed_token, matcher, path_completion_context).await } None => Vec::new(), - }; - - // TODO(completions-v2): Implement template filter functions. - path_suggestions + } } ArgumentValue::Template { type_name: TemplateType::Folders, @@ -583,7 +580,8 @@ async fn generate_suggestions_for_argument_value( let is_cd = tokens_without_last_editing .first() .is_some_and(|t| *t == "cd"); - let path_suggestions = match ctx.path_completion_context() { + // TODO(completions-v2): Implement template filter functions. + match ctx.path_completion_context() { Some(path_completion_context) => { if is_cd { sorted_cd_directories(parsed_token, matcher, path_completion_context).await @@ -597,10 +595,7 @@ async fn generate_suggestions_for_argument_value( } } None => Vec::new(), - }; - - // TODO(completions-v2): Implement template filter functions. - path_suggestions + } } ArgumentValue::Generator(GeneratorFn::Custom(js_fn)) => { let (Some(js_ctx), Some(path_ctx)) = (ctx.js_context(), ctx.path_completion_context())