Skip to content
25 changes: 25 additions & 0 deletions app/src/search/slash_command_menu/static_commands/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ pub static RENAME_TAB: LazyLock<StaticCommand> = LazyLock::new(|| StaticCommand
argument: Some(Argument::required().with_hint_text("<tab name>")),
});

pub static RENAME_PANE: LazyLock<StaticCommand> = LazyLock::new(|| StaticCommand {
name: "/rename-pane",
description: "Rename the current pane",
icon_path: "bundled/svg/pencil-line.svg",
availability: Availability::ALWAYS,
auto_enter_ai_mode: false,
argument: Some(Argument::required().with_hint_text("<pane name>")),
});

Comment thread
psh4607 marked this conversation as resolved.
pub static FORK: LazyLock<StaticCommand> = LazyLock::new(|| {
let hint_text = "<optional prompt to send in forked conversation>";
StaticCommand {
Expand Down Expand Up @@ -526,6 +535,7 @@ fn all_commands() -> Vec<StaticCommand> {
NEW.clone(),
PLAN.clone(),
RENAME_TAB.clone(),
RENAME_PANE.clone(),
USAGE,
CONVERSATIONS,
EXPORT_TO_CLIPBOARD,
Expand Down Expand Up @@ -650,6 +660,21 @@ mod tests {
assert_eq!(argument.hint_text, Some("<tab name>"));
}

#[test]
fn rename_pane_command_requires_argument() {
let command = COMMAND_REGISTRY
.get_command_with_name(RENAME_PANE.name)
.expect("expected /rename-pane to be registered");
let argument = command
.argument
.as_ref()
.expect("expected /rename-pane to require an argument");

assert!(!argument.is_optional);
assert!(!argument.should_execute_on_selection);
assert_eq!(argument.hint_text, Some("<pane name>"));
}

#[test]
fn strip_command_prefix_matches_orchestrate() {
let result = strip_command_prefix("/orchestrate deploy services", "/orchestrate");
Expand Down
14 changes: 14 additions & 0 deletions app/src/terminal/input/slash_commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,20 @@ impl Input {

ctx.dispatch_typed_action(&WorkspaceAction::SetActiveTabName(name.to_owned()));
}
rename_pane if command.name == commands::RENAME_PANE.name => {
let Some(name) = argument
.map(|name| name.trim())
.filter(|name| !name.is_empty())
else {
show_error_toast(
"Please provide a pane name after /rename-pane".to_owned(),
ctx,
);
return true;
};

ctx.dispatch_typed_action(&WorkspaceAction::SetActivePaneName(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
Expand Down
2 changes: 2 additions & 0 deletions app/src/util/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ pub enum CustomAction {
GoToLine,
ToggleGlobalSearch,
ToggleConversationListView,
RenamePane,
}

lazy_static! {
Expand Down Expand Up @@ -441,6 +442,7 @@ pub fn custom_tag_to_keystroke(custom: CustomTag) -> Option<Keystroke> {
| CustomAction::SplitPaneUp
| CustomAction::ConfigureKeybindings
| CustomAction::RenameTab
| CustomAction::RenamePane
| CustomAction::CloseTab
| CustomAction::CloseOtherTabs
| CustomAction::CloseTabsRight
Expand Down
4 changes: 4 additions & 0 deletions app/src/workspace/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ pub enum WorkspaceAction {
RenamePane(PaneViewLocator),
ResetPaneName(PaneViewLocator),
RenameActiveTab,
RenameActivePane,
SetActiveTabName(String),
SetActivePaneName(String),
ToggleTabRightClickMenu {
tab_index: usize,
anchor: TabContextMenuAnchor,
Expand Down Expand Up @@ -720,7 +722,9 @@ impl WorkspaceAction {
| RenamePane(_)
| ResetPaneName(_)
| RenameActiveTab
| RenameActivePane
| SetActiveTabName(_)
| SetActivePaneName(_)
| CloseTab(_)
| CloseActiveTab
| CloseOtherTabs(_)
Expand Down
9 changes: 9 additions & 0 deletions app/src/workspace/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,15 @@ pub fn init(app: &mut AppContext) {
.with_custom_action(CustomAction::RenameTab)
.with_context_predicate(id!("Workspace"))]);

app.register_editable_bindings([EditableBinding::new(
"workspace:rename_active_pane",
"Rename the current pane",
WorkspaceAction::RenameActivePane,
)
.with_group(bindings::BindingGroup::Settings.as_str())
.with_custom_action(CustomAction::RenamePane)
.with_context_predicate(id!("Workspace"))]);

app.register_editable_bindings([
EditableBinding::new(
"workspace:terminate_app",
Expand Down
82 changes: 80 additions & 2 deletions app/src/workspace/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ mod wasm_view;

use self::vertical_tabs::telemetry::{VerticalTabsDisplayOption, VerticalTabsTelemetryEvent};
use self::vertical_tabs::{
render_detail_sidecar, render_settings_popup, VerticalTabsPanelState,
VERTICAL_TABS_SETTINGS_BUTTON_POSITION_ID,
render_detail_sidecar, render_settings_popup, resolve_vertical_tabs_mode,
VerticalTabsPanelState, VerticalTabsResolvedMode, VERTICAL_TABS_SETTINGS_BUTTON_POSITION_ID,
};
pub(crate) use onboarding::OnboardingTutorial;

Expand Down Expand Up @@ -5242,6 +5242,82 @@ impl Workspace {
ctx.notify();
}

pub fn rename_active_pane(&mut self, ctx: &mut ViewContext<Self>) {
// The inline pane-rename editor is rendered whenever the vertical-tabs
// panel enters its pane-row loop — i.e. in every resolved mode EXCEPT
// Summary. In Summary mode the panel emits a single
// `render_summary_tab_item` row with `pane_rename_editor: None`, so
// focusing the editor would leave the user in an invisible edit state.
// Both Panes and FocusedSession resolve into the row loop and thread
// the editor through `PaneProps::new` → `render_title_override`, so the
// inline path is correct there. (`resolve_vertical_tabs_mode` also
// collapses Summary to FocusedSession when the
// `VerticalTabsSummaryMode` feature flag is off, keeping the gate
// consistent with the renderer.)
let inline_editor_visible = FeatureFlag::VerticalTabs.is_enabled()
&& *TabSettings::as_ref(ctx).use_vertical_tabs
&& !matches!(
resolve_vertical_tabs_mode(ctx),
VerticalTabsResolvedMode::Summary,
);

if inline_editor_visible {
if !self.vertical_tabs_panel_open {
self.vertical_tabs_panel_open = true;
}
let active_pane_group = self.active_tab_pane_group().clone();
let pane_group_id = active_pane_group.id();
let pane_id = active_pane_group.as_ref(ctx).focused_pane_id(ctx);
self.rename_pane(
PaneViewLocator {
pane_group_id,
pane_id,
},
ctx,
);
return;
}

if let Some(input_handle) = self.get_active_input_view_handle(ctx) {
input_handle.update(ctx, |input, input_ctx| {
input.replace_buffer_content("/rename-pane ", input_ctx);
input.focus_input_box(input_ctx);
});
} else {
let message = "Open a terminal to rename a pane.".to_string();
self.toast_stack.update(ctx, |view, ctx| {
view.add_ephemeral_toast(DismissibleToast::default(message), ctx);
});
}
}

fn set_active_pane_name(&mut self, name: &str, ctx: &mut ViewContext<Self>) {
Comment thread
psh4607 marked this conversation as resolved.
let active_pane_group = self.active_tab_pane_group().clone();
let pane_group_id = active_pane_group.id();
let pane_id = active_pane_group.as_ref(ctx).focused_pane_id(ctx);

if self.current_workspace_state.is_any_pane_being_renamed() {
self.current_workspace_state.clear_pane_being_renamed();
self.clear_pane_name_editor(ctx);
}

let title = name.trim();
if title.is_empty() {
ctx.notify();
return;
}
self.set_custom_pane_name(
PaneViewLocator {
pane_group_id,
pane_id,
},
title.to_owned(),
ctx,
);
Comment thread
psh4607 marked this conversation as resolved.
ctx.dispatch_global_action("workspace:save_app", ());
ctx.notify();
}

pub fn list_tab_pane_groups(&self, app: &AppContext) -> Vec<TabPaneGroupIdentifiers> {
self.tabs
.iter()
Expand Down Expand Up @@ -19676,7 +19752,9 @@ impl TypedActionView for Workspace {
RenamePane(locator) => self.rename_pane(*locator, ctx),
ResetPaneName(locator) => self.clear_pane_name(*locator, ctx),
RenameActiveTab => self.rename_tab(self.active_tab_index, ctx),
RenameActivePane => self.rename_active_pane(ctx),
SetActiveTabName(name) => self.set_active_tab_name(name, ctx),
SetActivePaneName(name) => self.set_active_pane_name(name, ctx),
ToggleTabRightClickMenu { tab_index, anchor } => {
self.toggle_tab_right_click_menu(*tab_index, *anchor, ctx)
}
Expand Down
4 changes: 2 additions & 2 deletions app/src/workspace/view/vertical_tabs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,7 @@ enum TabGroupColorMode {
}

#[derive(Clone, Copy, PartialEq, Eq)]
enum VerticalTabsResolvedMode {
pub(super) enum VerticalTabsResolvedMode {
Panes,
FocusedSession,
Summary,
Expand Down Expand Up @@ -821,7 +821,7 @@ struct TabGroupDragState {
insert_after_index: Option<usize>,
}

fn resolve_vertical_tabs_mode(app: &AppContext) -> VerticalTabsResolvedMode {
pub(super) fn resolve_vertical_tabs_mode(app: &AppContext) -> VerticalTabsResolvedMode {
let settings = TabSettings::as_ref(app);
match *settings.vertical_tabs_display_granularity.value() {
VerticalTabsDisplayGranularity::Panes => VerticalTabsResolvedMode::Panes,
Expand Down