Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
@@ -0,0 +1,981 @@
mod data_source;
mod search_item;
mod view;
pub use data_source::*;
pub use view::*;
use ai::skills::SkillReference;
use warp_core::features::FeatureFlag;
use warp_core::send_telemetry_from_ctx;
use warp_core::ui::appearance::Appearance;
use warpui::clipboard::ClipboardContent;
use warpui::{SingletonEntity, ViewContext};
use crate::ai::blocklist::agent_view::{
AgentViewEntryOrigin, DismissalStrategy, EphemeralMessage, ENTER_OR_EXIT_CONFIRMATION_WINDOW,
};
use crate::ai::blocklist::{BlocklistAIHistoryModel, SlashCommandRequest};
use crate::cloud_object::model::persistence::CloudModel;
use crate::code_review::telemetry_event::CodeReviewPaneEntrypoint;
use crate::search::slash_command_menu::static_commands::commands::{self, COMMAND_REGISTRY};
use crate::search::slash_command_menu::static_commands::Availability;
use crate::search::slash_command_menu::{SlashCommandId, StaticCommand};
use crate::server::ids::SyncId;
use crate::server::telemetry::SlashCommandAcceptedDetails;
use crate::settings::AISettings;
use crate::terminal::input::decorations::InputBackgroundJobOptions;
use crate::terminal::input::inline_menu::{InlineMenuAction, InlineMenuType};
use crate::terminal::input::message_bar::Message;
use crate::terminal::input::slash_command_model::{
SlashCommandEntryState, UpdatedSlashCommandModel,
};
use crate::terminal::input::{
CompletionsTrigger, Event, Input, InputSuggestionsMode, UserQueryMenuAction,
};
use crate::terminal::view::TerminalAction;
use crate::view_components::DismissibleToast;
use crate::workflows::{WorkflowSelectionSource, WorkflowSource, WorkflowType};
use crate::workspace::{ForkedConversationDestination, ToastStack, WorkspaceAction};
use crate::TelemetryEvent;
#[derive(Debug, Clone)]
pub enum AcceptSlashCommandOrSavedPrompt {
SlashCommand {
id: SlashCommandId,
},
SavedPrompt {
id: SyncId,
},
/// A skill selected from browse or search. Contains name (for display/insertion) and path/bundled_skill_id (for execution).
Skill {
reference: SkillReference,
name: String,
},
}
impl InlineMenuAction for AcceptSlashCommandOrSavedPrompt {
const MENU_TYPE: InlineMenuType = InlineMenuType::SlashCommands;
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum SlashCommandTrigger {
Input { cmd_or_ctrl_enter: bool },
Keybinding,
}
impl SlashCommandTrigger {
fn cmd_or_ctrl_enter() -> Self {
Self::Input {
cmd_or_ctrl_enter: true,
}
}
pub fn input() -> Self {
Self::Input {
cmd_or_ctrl_enter: false,
}
}
pub(super) fn keybinding() -> Self {
Self::Keybinding
}
pub fn is_keybinding(&self) -> bool {
matches!(self, Self::Keybinding)
}
fn is_cmd_or_ctrl_enter(&self) -> bool {
matches!(
self,
Self::Input {
cmd_or_ctrl_enter: true
}
)
}
}
impl Input {
pub(super) fn select_slash_command(
&mut self,
command: &StaticCommand,
trigger: SlashCommandTrigger,
ctx: &mut ViewContext<Self>,
) {
if command.argument.as_ref().is_none() {
self.execute_slash_command(
command, None, trigger, /*is_queued_prompt*/ false, ctx,
);
} else if command
.argument
.as_ref()
.is_some_and(|arg| arg.should_execute_on_selection)
{
// TODO (zachbai): this is a hack for Oz launch. Caller
// should probably be invoking `execute_slash_command` in this case.
let argument = if !self.suggestions_mode_model.as_ref(ctx).is_slash_commands() {
let trimmed = self.buffer_text(ctx).trim().to_owned();
(!trimmed.is_empty()).then_some(trimmed)
} else {
None
};
self.execute_slash_command(
command,
argument.as_ref(),
trigger,
/*is_queued_prompt*/ false,
ctx,
);
} else {
self.editor.update(ctx, |editor, ctx| {
editor.set_buffer_text(&format!("{} ", command.name), ctx);
});
}
}
pub(super) fn close_slash_commands_menu(&mut self, ctx: &mut ViewContext<Self>) {
self.suggestions_mode_model.update(ctx, |model, ctx| {
model.set_mode(InputSuggestionsMode::Closed, ctx);
});
ctx.notify();
}
pub(super) fn handle_slash_command_model_event(
&mut self,
event: &UpdatedSlashCommandModel,
ctx: &mut ViewContext<Self>,
) {
// Refresh decorations if the slash command detection state changed, since
// detected commands affect syntax highlighting.
let new_state = self.slash_command_model.as_ref(ctx).state();
if event.old_state.is_detected_command() != new_state.is_detected_command() {
let _ = self
.debounce_input_background_tx
.try_send(InputBackgroundJobOptions::default().with_command_decoration());
}
match self.slash_command_model.as_ref(ctx).state().clone() {
SlashCommandEntryState::None | SlashCommandEntryState::DisabledUntilEmptyBuffer => {
if self.suggestions_mode_model.as_ref(ctx).is_slash_commands() {
self.close_slash_commands_menu(ctx);
}
}
SlashCommandEntryState::Composing { .. } => {
if self.suggestions_mode_model.as_ref(ctx).is_closed() {
self.open_slash_commands_menu(ctx);
} else if !self.suggestions_mode_model.as_ref(ctx).is_slash_commands() {
self.slash_command_model.update(ctx, |model, ctx| {
model.disable(ctx);
});
}
}
SlashCommandEntryState::SlashCommand(detected_command) => {
// If there is only one result (or zero, but that should be impossible if there is
// a valid command in the input) OR if the user has started typing arguments, hide
// the menu.
if self.suggestions_mode_model.as_ref(ctx).is_slash_commands()
&& (self
.inline_slash_commands_view
.as_ref(ctx)
.result_count(ctx)
< 2
|| detected_command.argument.is_some())
{
self.close_slash_commands_menu(ctx);
}
if detected_command.command.auto_enter_ai_mode
|| !FeatureFlag::AgentView.is_enabled()
{
self.enter_ai_mode(ctx);
}
if detected_command.command.name == commands::EDIT.name
&& detected_command
.argument
.as_ref()
.is_some_and(|argument| argument.is_empty())
&& self.suggestions_mode_model.as_ref(ctx).is_closed()
{
self.open_completion_suggestions(CompletionsTrigger::Keybinding, ctx);
}
}
SlashCommandEntryState::SkillCommand(detected_skill) => {
// Hide the menu once the user has started typing the prompt
if self.suggestions_mode_model.as_ref(ctx).is_slash_commands()
&& (self
.inline_slash_commands_view
.as_ref(ctx)
.result_count(ctx)
< 2
|| detected_skill.argument.is_some())
{
self.close_slash_commands_menu(ctx);
}
// Skill commands always require AI mode
self.enter_ai_mode(ctx);
}
}
}
pub(crate) fn handle_slash_commands_menu_event(
&mut self,
event: &SlashCommandsEvent,
ctx: &mut ViewContext<Self>,
) {
match event {
SlashCommandsEvent::Close(reason) => {
if reason.is_manual_dismissal() {
self.slash_command_model.update(ctx, |model, ctx| {
model.disable(ctx);
});
}
self.suggestions_mode_model.update(ctx, |model, ctx| {
model.set_mode(InputSuggestionsMode::Closed, ctx);
});
ctx.notify();
}
SlashCommandsEvent::SelectedSavedPrompt { id } => {
let Some(workflow) = CloudModel::as_ref(ctx).get_workflow(id).cloned() else {
log::warn!("Tried to execute workflow for id {id:?} but it does not exist");
return;
};
let is_in_agent_view = FeatureFlag::AgentView.is_enabled()
&& self.agent_view_controller.as_ref(ctx).is_fullscreen();
send_telemetry_from_ctx!(
TelemetryEvent::SlashCommandAccepted {
command_details: SlashCommandAcceptedDetails::SavedPrompt,
is_in_agent_view,
},
ctx
);
self.show_workflows_info_box_on_workflow_selection(
WorkflowType::Cloud(Box::new(workflow)),
WorkflowSource::WarpAI,
WorkflowSelectionSource::SlashMenu,
None,
ctx,
);
}
SlashCommandsEvent::SelectedStaticCommand {
id,
cmd_or_ctrl_enter,
} => {
let Some(command) = COMMAND_REGISTRY.get_command(id) else {
return;
};
self.select_slash_command(
command,
SlashCommandTrigger::Input {
cmd_or_ctrl_enter: *cmd_or_ctrl_enter,
},
ctx,
);
}
SlashCommandsEvent::SelectedSkill { name, reference: _ } => {
// Insert /{skill-name} into the buffer
self.editor.update(ctx, |editor, ctx| {
editor.set_buffer_text(format!("/{name} ").as_str(), ctx);
});
self.close_slash_commands_menu(ctx);
}
}
}
/// Executes the given `command` with `argument`, if any.
///
/// When `is_queued_prompt` is true, this is the first send of a previously queued prompt:
/// the input buffer is left alone so the user doesn't lose anything they've typed while
/// the agent was busy.
///
/// Returns `true` if execution was 'handled' (whether or not it resulted in success or failure).
pub(super) fn execute_slash_command(
&mut self,
command: &StaticCommand,
argument: Option<&String>,
trigger: SlashCommandTrigger,
is_queued_prompt: bool,
ctx: &mut ViewContext<Self>,
) -> bool {
fn show_error_toast(message: String, ctx: &mut ViewContext<Input>) {
let window_id = ctx.window_id();
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
toast_stack.add_ephemeral_toast(DismissibleToast::error(message), window_id, ctx);
});
}
// Safety net: commands whose availability requires AI should not execute when AI is
// globally disabled. They're normally filtered out of the slash command menu, but this
// protects keybinding-triggered execution where a bound key may still address the command.
if command.availability.contains(Availability::AI_ENABLED)
&& !AISettings::as_ref(ctx).is_any_ai_enabled(ctx)
{
show_error_toast(format!("{} requires AI to be enabled", command.name), ctx);
return true;
}
// Handle the slash command action based on its kind
match command.name {
add_mcp if command.name == commands::ADD_MCP.name => {
ctx.dispatch_typed_action(&TerminalAction::OpenAddMCPPane);
}
add_prompt if command.name == commands::ADD_PROMPT.name => {
ctx.dispatch_typed_action(&TerminalAction::OpenAddPromptPane);
}
add_rule if command.name == commands::ADD_RULE.name => {
ctx.dispatch_typed_action(&TerminalAction::OpenAddRulePane);
}
agent_or_new
if command.name == commands::NEW.name || command.name == commands::AGENT.name =>
{
if !self
.ai_context_model
.as_ref(ctx)
.can_start_new_conversation()
{
self.ephemeral_message_model.update(ctx, |model, ctx| {
let appearance = Appearance::handle(ctx).as_ref(ctx);
let message = Message::from_text(
"cannot start new conversation while terminal command is running",
)
.with_text_color(appearance.theme().ansi_fg_red());
model.show_ephemeral_message(
EphemeralMessage::new(
message,
DismissalStrategy::Timer(ENTER_OR_EXIT_CONFIRMATION_WINDOW),
),
ctx,
);
});
return true;
}
// Keybindings can be triggered reflexively while users are already in an active
// conversation, so we gate only this path behind a second-press confirmation.
// Typed `/agent`/`/new` and slash-menu execution stay single-step by design.
if trigger.is_keybinding() && self.agent_view_controller.as_ref(ctx).is_active() {
let should_start_new_conversation =
self.agent_view_controller.update(ctx, |controller, ctx| {
controller
.should_start_new_conversation_for_keybinding(command.name, ctx)
});
if !should_start_new_conversation {
// Keep the current input/conversation untouched on first press; only the
// ephemeral confirmation prompt should change.
return true;
}
}
let prompt = argument.and_then(|argument| {
let trimmed = argument.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_owned())
}
});
ctx.emit(Event::EnterAgentView {
initial_prompt: prompt,
conversation_id: None,
origin: AgentViewEntryOrigin::SlashCommand { trigger },
});
}
cloud_agent if command.name == commands::CLOUD_AGENT.name => {
let prompt = argument.and_then(|argument| {
let trimmed = argument.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_owned())
}
});
ctx.emit(Event::EnterCloudAgentView {
initial_prompt: prompt,
});
}
create_docker_sandbox if command.name == commands::CREATE_DOCKER_SANDBOX.name => {
ctx.emit(Event::CreateDockerSandbox);
}
conversations if command.name == commands::CONVERSATIONS.name => {
if FeatureFlag::AgentView.is_enabled() {
self.open_conversation_menu(ctx);
} else {
ctx.dispatch_typed_action(&TerminalAction::OpenConversationsPalette);
}
}
rename_tab if command.name == commands::RENAME_TAB.name => {
let Some(name) = argument
.map(|name| name.trim())
.filter(|name| !name.is_empty())
else {
show_error_toast(
"Please provide a tab name after /rename-tab".to_owned(),
ctx,
);
return true;
};
ctx.dispatch_typed_action(&WorkspaceAction::SetActiveTabName(name.to_owned()));
}
create_env if command.name == commands::CREATE_ENVIRONMENT.name => {
// If the user included args after the slash command, treat them as repo paths/URLs.
let repos = argument
.map(|arg| {
arg.split_whitespace()
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect()
})
.unwrap_or_default();
ctx.emit(Event::TriggerEnvironmentSetup { repos });
}
create_project if command.name == commands::CREATE_NEW_PROJECT.name => {
if argument.is_none_or(|args| args.is_empty()) {
show_error_toast(
"Please describe the project you want to create after /create-new-project"
.to_owned(),
ctx,
);
return true;
}
let args = argument.expect("args are Some()");
self.initiate_create_new_project(args.to_owned(), ctx);
}
edit if command.name == commands::EDIT.name => {
#[cfg(feature = "local_fs")]
match argument {
Some(args) if !args.is_empty() => {
use shellexpand::tilde;
use warp_util::path::CleanPathResult;
let Some(session_id) = self.active_block_session_id() else {
return false;
};
let Some(session) = self.sessions.as_ref(ctx).get(session_id) else {
return false;
};
if !session.is_local() {
let window_id = ctx.window_id();
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
toast_stack.add_ephemeral_toast(
DismissibleToast::error(
"The /open-file command is only available for local sessions"
.to_owned(),
),
window_id,
ctx,
);
});
return false;
}
let current_dir = self
.active_block_metadata
.as_ref()
.and_then(|metadata| metadata.current_working_directory())
.map(std::path::PathBuf::from);
let Some(current_dir) = current_dir else {
return false;
};
let parsed_path = CleanPathResult::with_line_and_column_number(args.trim());
// The argument may contain shell-escaped characters (e.g. `\ ` for
// spaces) from auto-suggest. Unescape them so the path matches the
// actual filesystem entry.
let unescaped_path = session.shell_family().unescape(&parsed_path.path);
// Expand `~` to the user's home directory.
let expanded_path = tilde(&unescaped_path);
let file_path = current_dir.join(&*expanded_path);
match std::fs::metadata(&file_path) {
Ok(metadata) if metadata.is_file() => {
use crate::util::file::external_editor;
ctx.dispatch_typed_action(&TerminalAction::OpenCodeInWarp {
path: file_path,
layout: external_editor::settings::EditorLayout::SplitPane,
line_col: parsed_path.line_and_column_num,
});
}
Ok(_) => {
show_error_toast(
"The /open-file command only works for files, not directories"
.to_owned(),
ctx,
);
return true;
}
Err(_) => {
show_error_toast(
format!("File not found: {}", file_path.display()),
ctx,
);
return true;
}
}
}
_ => {
use crate::server::telemetry::PaletteSource;
ctx.emit(Event::OpenFilesPalette {
source: PaletteSource::Keybinding,
});
}
}
#[cfg(not(feature = "local_fs"))]
{
show_error_toast(
"The /open-file command is not supported in this build".to_owned(),
ctx,
);
return true;
}
}
export_to_clipboard if command.name == commands::EXPORT_TO_CLIPBOARD.name => {
let history = BlocklistAIHistoryModel::handle(ctx);
let Some(conversation) = history
.as_ref(ctx)
.active_conversation(self.terminal_view_id)
else {
show_error_toast("No active conversation to export".to_owned(), ctx);
return true;
};
let action_model = self.ai_action_model.as_ref(ctx);
let conversation_text = conversation.export_to_markdown(Some(action_model));
ctx.clipboard()
.write(ClipboardContent::plain_text(conversation_text));
// Show a toast to confirm the export
let window_id = ctx.window_id();
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
let toast = DismissibleToast::default(String::from(
"Conversation exported to clipboard",
));
toast_stack.add_ephemeral_toast(toast, window_id, ctx);
});
}
export_to_file if command.name == commands::EXPORT_TO_FILE.name => {
#[cfg(not(target_family = "wasm"))]
{
self.export_conversation_to_file(
argument.map(|filename| filename.to_owned()),
ctx,
);
}
#[cfg(target_family = "wasm")]
{
show_error_toast(
"Export conversation to file unsupported in web".to_owned(),
ctx,
);
return true;
}
}
index if command.name == commands::INDEX.name => {
ctx.dispatch_typed_action(&TerminalAction::IndexProjectSpeedbump);
}
init if command.name == commands::INIT.name => {
ctx.dispatch_typed_action(&TerminalAction::InitProject);
}
changelog if command.name == commands::CHANGELOG.name => {
if !FeatureFlag::Changelog.is_enabled() {
return false;
}
ctx.dispatch_typed_action(&WorkspaceAction::ViewLatestChangelog);
}
feedback if command.name == commands::FEEDBACK.name => {
ctx.dispatch_typed_action(&WorkspaceAction::SendFeedback);
}
open_code_review if command.name == commands::OPEN_CODE_REVIEW.name => {
ctx.dispatch_typed_action(&TerminalAction::ToggleCodeReviewPane {
entrypoint: CodeReviewPaneEntrypoint::SlashCommand,
});
}
open_mcp_servers if command.name == commands::OPEN_MCP_SERVERS.name => {
ctx.dispatch_typed_action(&TerminalAction::OpenViewMCPPane);
}
open_settings_file if command.name == commands::OPEN_SETTINGS_FILE.name => {
if !FeatureFlag::SettingsFile.is_enabled() || !cfg!(feature = "local_fs") {
return false;
}
ctx.dispatch_typed_action(&WorkspaceAction::OpenSettingsFile);
}
open_project_rules if command.name == commands::OPEN_PROJECT_RULES.name => {
ctx.dispatch_typed_action(&TerminalAction::OpenProjectRulesPane);
}
open_rules if command.name == commands::OPEN_RULES.name => {
ctx.dispatch_typed_action(&TerminalAction::OpenRulesPane);
}
edit_skill if command.name == commands::EDIT_SKILL.name => {
if !FeatureFlag::ListSkills.is_enabled() {
return false;
}
// Open the skill selector menu - user will select a skill from the inline menu
self.open_skill_selector(ctx);
}
invoke_skill if command.name == commands::INVOKE_SKILL.name => {
if !FeatureFlag::ListSkills.is_enabled() {
return false;
}
// Open the skill selector menu for invocation - skill command will be inserted into buffer
self.open_invoke_skill_selector(ctx);
}
models if command.name == commands::MODEL.name => {
self.open_model_selector(ctx);
}
profiles if command.name == commands::PROFILE.name => {
if !FeatureFlag::InlineProfileSelector.is_enabled() {
return false;
}
self.open_profile_selector(ctx);
}
prompts if command.name == commands::PROMPTS.name => {
if FeatureFlag::AgentView.is_enabled() {
self.open_prompts_menu(ctx);
} else {
return false;
}
}
rewind if command.name == commands::REWIND.name => {
self.open_rewind_menu(ctx);
}
pr_comments if command.name == commands::PR_COMMENTS.name => {
if !FeatureFlag::PRCommentsSlashCommand.is_enabled() {
return false;
}
let Some(repo_path) = self
.active_session_path_if_local(ctx)
.map(|path| path.to_path_buf())
.map(|path| path.to_string_lossy().to_string())
else {
log::error!("Expected a valid working directory since /pr-comments is only available from the terminal");
return false;
};
self.ai_controller.update(ctx, move |controller, ctx| {
controller.send_slash_command_request(
SlashCommandRequest::FetchReviewComments { repo_path },
ctx,
)
});
}
usage if command.name == commands::USAGE.name => {
ctx.dispatch_typed_action(&TerminalAction::OpenBillingAndUsagePane);
}
remote_control if command.name == commands::REMOTE_CONTROL.name => {
if !FeatureFlag::CreatingSharedSessions.is_enabled()
|| !FeatureFlag::HOARemoteControl.is_enabled()
{
return false;
}
if self
.model
.lock()
.shared_session_status()
.is_sharer_or_viewer()
{
show_error_toast("Session is already being shared".to_owned(), ctx);
return true;
}
ctx.emit(Event::StartRemoteControl);
}
cost if command.name == commands::COST.name => {
let history = BlocklistAIHistoryModel::handle(ctx);
let conversation = history
.as_ref(ctx)
.active_conversation(self.terminal_view_id);
if conversation.is_none() {
show_error_toast(
"Cannot show conversation cost: no active conversation".to_owned(),
ctx,
);
} else if conversation.is_some_and(|c| c.is_empty()) {
show_error_toast(
"Cannot show conversation cost: conversation is empty".to_owned(),
ctx,
);
} else if conversation.is_some_and(|c| !c.status().is_done()) {
show_error_toast(
"Cannot show conversation cost: conversation is in progress".to_owned(),
ctx,
);
} else {
ctx.dispatch_typed_action(&TerminalAction::ToggleUsageFooter);
}
}
fork if command.name == commands::FORK.name => {
let Some(conversation_id) = self
.ai_context_model
.as_ref(ctx)
.selected_conversation_id(ctx)
else {
show_error_toast("/fork requires an active conversation".to_owned(), ctx);
return true;
};
let destination = if trigger.is_cmd_or_ctrl_enter() {
ForkedConversationDestination::NewTab
} else {
ForkedConversationDestination::SplitPane
};
ctx.dispatch_typed_action(&WorkspaceAction::ForkAIConversation {
conversation_id,
fork_from_exchange: None,
summarize_after_fork: false,
summarization_prompt: None,
initial_prompt: argument.cloned(),
destination,
});
}
fork_from if command.name == commands::FORK_FROM.name => {
self.open_user_query_menu(UserQueryMenuAction::ForkFrom, ctx);
return true;
}
fork_and_compact if command.name == commands::FORK_AND_COMPACT.name => {
let Some(conversation_id) = self
.ai_context_model
.as_ref(ctx)
.selected_conversation_id(ctx)
else {
show_error_toast(
"/fork-and-compact requires an active conversation".to_owned(),
ctx,
);
return true;
};
let destination = if trigger.is_cmd_or_ctrl_enter() {
ForkedConversationDestination::SplitPane
} else {
ForkedConversationDestination::CurrentPane
};
ctx.dispatch_typed_action(&WorkspaceAction::ForkAIConversation {
conversation_id,
fork_from_exchange: None,
summarize_after_fork: true,
summarization_prompt: None,
initial_prompt: argument.cloned(),
destination,
});
}
compact_and if command.name == commands::COMPACT_AND.name => {
if self
.ai_context_model
.as_ref(ctx)
.selected_conversation_id(ctx)
.is_none()
{
show_error_toast(
"/compact-and requires an active conversation".to_owned(),
ctx,
);
return true;
};
ctx.dispatch_typed_action(&WorkspaceAction::SummarizeAIConversation {
prompt: None,
initial_prompt: argument.cloned(),
});
}
queue if command.name == commands::QUEUE.name => {
let Some(conversation_id) = self
.ai_context_model
.as_ref(ctx)
.selected_conversation_id(ctx)
else {
show_error_toast("/queue requires an active conversation".to_owned(), ctx);
return true;
};
let Some(prompt) = argument.filter(|a| !a.is_empty()).cloned() else {
show_error_toast("/queue requires a prompt argument".to_owned(), ctx);
return true;
};
let history = BlocklistAIHistoryModel::handle(ctx);
let is_in_progress = history
.as_ref(ctx)
.conversation(&conversation_id)
.is_some_and(|c| c.status().is_in_progress() || c.status().is_blocked());
if is_in_progress {
ctx.dispatch_typed_action(&WorkspaceAction::QueuePromptForConversation {
prompt,
});
} else {
self.submit_queued_prompt(prompt, ctx);
}
}
open_repo if command.name == commands::OPEN_REPO.name => {
if !FeatureFlag::InlineRepoMenu.is_enabled() {
return false;
}
self.open_repos_menu(ctx);
}
command_that_just_sends_ai_request_with_prefix
if command.name == commands::COMPACT.name
|| command.name == commands::PLAN.name
|| command.name == commands::ORCHESTRATE.name =>
{
// These slash commands just send AI requests with the slash command text as a
// prefix, and special handling is done downstream as an implementation detail
// of handling user queries with specific slash command prefixes.
return false;
}
_ => {
debug_assert!(
false,
"Attempted to execute slash command with no handler: {}",
command.name
);
return false;
}
}
// Leave the buffer alone when re-sending a queued prompt (the user may have typed
// new input while the agent was busy).
if !is_queued_prompt {
self.editor.update(ctx, |editor, ctx| {
editor.clear_buffer(ctx);
});
}
// If the command must be executed in AI mode, and we're not already in an agent view,
// enter the agent view.
if FeatureFlag::AgentView.is_enabled()
&& command.auto_enter_ai_mode
&& !self.agent_view_controller.as_ref(ctx).is_active()
{
self.agent_view_controller.update(ctx, |controller, ctx| {
let _ = controller.try_enter_agent_view(
None,
AgentViewEntryOrigin::SlashCommand {
trigger: SlashCommandTrigger::input(),
},
ctx,
);
});
}
let is_in_agent_view = FeatureFlag::AgentView.is_enabled()
&& self.agent_view_controller.as_ref(ctx).is_active();
send_telemetry_from_ctx!(
TelemetryEvent::SlashCommandAccepted {
command_details: SlashCommandAcceptedDetails::StaticCommand {
command_name: command.name.to_owned(),
},
is_in_agent_view,
},
ctx
);
true
}
/// Handles cmd+enter (Mac) / ctrl+enter (Linux/Windows) for slash commands.
///
/// Returns `true` if the keypress was handled.
pub(super) fn maybe_handle_cmd_or_ctrl_shift_enter_for_slash_command(
&mut self,
ctx: &mut ViewContext<Self>,
) -> bool {
// If slash command menu is open, accept the selected item with cmd_or_ctrl_enter=true.
if matches!(
self.suggestions_mode_model.as_ref(ctx).mode(),
InputSuggestionsMode::SlashCommands
) {
self.inline_slash_commands_view.update(ctx, |view, ctx| {
view.accept_selected_item(true, ctx);
});
return true;
}
// If no menu but slash command detected in buffer, execute with cmd_or_ctrl_enter=true
match self.slash_command_model.as_ref(ctx).state() {
SlashCommandEntryState::SlashCommand(detected_command) => {
let command = detected_command.command.clone();
let argument = detected_command.argument.clone();
self.execute_slash_command(
&command,
argument.as_ref(),
SlashCommandTrigger::cmd_or_ctrl_enter(),
/*is_queued_prompt*/ false,
ctx,
)
}
SlashCommandEntryState::SkillCommand(detected_skill) => {
let reference = detected_skill.reference.clone();
let user_query = detected_skill.argument.clone();
self.execute_skill_command(
reference, user_query, /*is_queued_prompt*/ false, ctx,
)
}
SlashCommandEntryState::None
| SlashCommandEntryState::Composing { .. }
| SlashCommandEntryState::DisabledUntilEmptyBuffer => false,
}
}
/// Executes a slash command on `enter` keypress.
///
/// If the slash command menu is open, then "accepts" the slash command:
/// * If the slash command does not take arguments, executes it
/// * If the slash command does take arguments, inserts it into the input.
///
/// If the slash command menu is not open, then "executes" the slash command in the input, if
/// there is one.
///
/// Returns `true` if the enter keypress was 'handled', else upstream enter keypress handling
/// logic should continue.
pub(super) fn maybe_handle_enter_for_slash_command(
&mut self,
ctx: &mut ViewContext<Self>,
) -> bool {
if matches!(
self.suggestions_mode_model.as_ref(ctx).mode(),
InputSuggestionsMode::SlashCommands
) {
self.inline_slash_commands_view.update(ctx, |view, ctx| {
view.accept_selected_item(false, ctx);
});
return true;
}
match self.slash_command_model.as_ref(ctx).state() {
SlashCommandEntryState::SlashCommand(detected_command) => {
let command = detected_command.command.clone();
let argument = detected_command.argument.clone();
self.execute_slash_command(
&command,
argument.as_ref(),
SlashCommandTrigger::input(),
/*is_queued_prompt*/ false,
ctx,
)
}
SlashCommandEntryState::SkillCommand(detected_skill) => {
let reference = detected_skill.reference.clone();
let user_query = detected_skill.argument.clone();
self.execute_skill_command(
reference, user_query, /*is_queued_prompt*/ false, ctx,
)
}
SlashCommandEntryState::None
| SlashCommandEntryState::Composing { .. }
| SlashCommandEntryState::DisabledUntilEmptyBuffer => false,
}
}
}