From 05dab44eeb46e8168e8351bfa83850b064ba07f0 Mon Sep 17 00:00:00 2001 From: oleksiiovdiienko Date: Sun, 7 Jun 2026 19:38:04 +0300 Subject: [PATCH 1/4] Preserve nonexistent cwd in tab configuration tests --- app/src/pane_group/mod.rs | 3 +-- app/src/pane_group/mod_tests.rs | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/app/src/pane_group/mod.rs b/app/src/pane_group/mod.rs index 7c51ca45f6e..b722a97d612 100644 --- a/app/src/pane_group/mod.rs +++ b/app/src/pane_group/mod.rs @@ -1333,9 +1333,8 @@ impl PaneGroup { Self::create_ambient_agent_terminal(resources, view_size, ctx) } PaneMode::Terminal | PaneMode::Agent => PaneGroup::create_session( - // Use cwd from the template iff such path exists, otherwise None // TODO(CORE-3187): On Windows, support WSL directory restoration. - Some(cwd).filter(|p| p.exists()), + Some(cwd), HashMap::new(), uuid.as_bytes(), IsSharedSessionCreator::No, diff --git a/app/src/pane_group/mod_tests.rs b/app/src/pane_group/mod_tests.rs index a8d61153b69..9c1f32db954 100644 --- a/app/src/pane_group/mod_tests.rs +++ b/app/src/pane_group/mod_tests.rs @@ -2508,6 +2508,39 @@ fn test_initial_widths_are_computed_correctly() { }); } +#[test] +fn test_tab_config_preserves_nonexistent_cwd() { + App::test((), |mut app| async move { + initialize_app(&mut app); + + let cwd = std::env::temp_dir().join(format!( + "warp-tab-config-nonexistent-cwd-{}", + Uuid::new_v4() + )); + assert!(!cwd.exists()); + + let pane_group = mock_pane_group( + &mut app, + MockOptions { + layout: PanesLayout::Template(PaneTemplateType::PaneTemplate { + is_focused: None, + cwd: cwd.clone(), + commands: vec![], + pane_mode: PaneMode::Terminal, + shell: None, + }), + ..Default::default() + }, + ); + + pane_group.read(&app, |pane_group, ctx| { + let terminal_view = pane_group.terminal_view_at_pane_index(0, ctx).unwrap(); + let model = terminal_view.as_ref(ctx).model.lock(); + assert_eq!(model.session_startup_path(), Some(cwd)); + }); + }); +} + #[test] fn test_is_terminal_pane_being_shared() { App::test((), |mut app| async move { From 6d3f2931898893ef9d6feee0f1c9a13248893541 Mon Sep 17 00:00:00 2001 From: oleksiiovdiienko Date: Sun, 7 Jun 2026 20:23:25 +0300 Subject: [PATCH 2/4] Show persistent tab config cwd errors --- app/src/pane_group/mod_tests.rs | 33 ----- app/src/tab_configs/tab_config.rs | 42 ++++++ app/src/user_config/mod_tests.rs | 148 +++++++++++++++++++ app/src/user_config/util.rs | 6 + app/src/view_components/dismissible_toast.rs | 5 + app/src/workspace/view.rs | 94 ++++++++++++ app/src/workspace/view_tests.rs | 108 +++++++++++++- 7 files changed, 399 insertions(+), 37 deletions(-) diff --git a/app/src/pane_group/mod_tests.rs b/app/src/pane_group/mod_tests.rs index 9c1f32db954..a8d61153b69 100644 --- a/app/src/pane_group/mod_tests.rs +++ b/app/src/pane_group/mod_tests.rs @@ -2508,39 +2508,6 @@ fn test_initial_widths_are_computed_correctly() { }); } -#[test] -fn test_tab_config_preserves_nonexistent_cwd() { - App::test((), |mut app| async move { - initialize_app(&mut app); - - let cwd = std::env::temp_dir().join(format!( - "warp-tab-config-nonexistent-cwd-{}", - Uuid::new_v4() - )); - assert!(!cwd.exists()); - - let pane_group = mock_pane_group( - &mut app, - MockOptions { - layout: PanesLayout::Template(PaneTemplateType::PaneTemplate { - is_focused: None, - cwd: cwd.clone(), - commands: vec![], - pane_mode: PaneMode::Terminal, - shell: None, - }), - ..Default::default() - }, - ); - - pane_group.read(&app, |pane_group, ctx| { - let terminal_view = pane_group.terminal_view_at_pane_index(0, ctx).unwrap(); - let model = terminal_view.as_ref(ctx).model.lock(); - assert_eq!(model.session_startup_path(), Some(cwd)); - }); - }); -} - #[test] fn test_is_terminal_pane_being_shared() { App::test((), |mut app| async move { diff --git a/app/src/tab_configs/tab_config.rs b/app/src/tab_configs/tab_config.rs index 57ffec971f9..0b6e5c80ae1 100644 --- a/app/src/tab_configs/tab_config.rs +++ b/app/src/tab_configs/tab_config.rs @@ -203,6 +203,48 @@ impl TabConfig { } } +pub(crate) fn validate_load_time_tab_config_directories(config: &TabConfig) -> Result<(), String> { + let mut invalid_directories = Vec::new(); + + for pane in &config.panes { + let Some(directory) = pane.directory.as_deref() else { + continue; + }; + + if directory.is_empty() || contains_template_placeholder(directory) { + continue; + } + + let expanded = shellexpand::tilde(directory).into_owned(); + let path = PathBuf::from(&expanded); + if path.is_dir() { + continue; + } + + let reason = if path.exists() { + "path exists but is not a directory" + } else { + "directory does not exist" + }; + invalid_directories.push(format!( + "pane '{}' references invalid directory '{}': {}", + pane.id, expanded, reason + )); + } + + if invalid_directories.is_empty() { + Ok(()) + } else { + Err(invalid_directories.join("; ")) + } +} + +fn contains_template_placeholder(value: &str) -> bool { + value + .split_once("{{") + .is_some_and(|(_, rest)| rest.contains("}}")) +} + /// Renders a [`TabConfig`] with the given param values into a [`PaneTemplateType`] /// and an optional rendered title. /// diff --git a/app/src/user_config/mod_tests.rs b/app/src/user_config/mod_tests.rs index 5640dd6e652..d6d6f6f7f3b 100644 --- a/app/src/user_config/mod_tests.rs +++ b/app/src/user_config/mod_tests.rs @@ -174,3 +174,151 @@ fn test_load_tab_configs_skips_non_toml_files() { assert_eq!(configs.len(), 1); assert_eq!(configs[0].name, "Real"); } + +#[cfg(feature = "local_fs")] +fn write_tab_config_toml_with_directory( + dir: &Path, + file_name: &str, + config_name: &str, + directory: &str, +) { + let path = dir.join(file_name); + let toml = format!( + r#"name = "{config_name}" + +[[panes]] +id = "main" +type = "terminal" +directory = "{directory}" +"# + ); + std::fs::write(path, toml).unwrap(); +} + +#[cfg(feature = "local_fs")] +#[test] +fn test_load_tab_configs_rejects_nonexistent_literal_directory() { + let dir = tempfile::tempdir().unwrap(); + let nonexistent_dir = dir.path().join("does-not-exist"); + write_tab_config_toml_with_directory( + dir.path(), + "invalid.toml", + "Invalid", + &nonexistent_dir.display().to_string(), + ); + + let (configs, errors) = load_tab_configs(dir.path()); + + assert!(configs.is_empty()); + assert_eq!(errors.len(), 1); + assert_eq!(errors[0].file_name, "invalid.toml"); + assert!(errors[0].file_path.ends_with("invalid.toml")); + assert!(errors[0].error_message.contains("pane 'main'")); + assert!(errors[0].error_message.contains("does-not-exist")); + assert!(errors[0].error_message.contains("directory does not exist")); +} + +#[cfg(feature = "local_fs")] +#[test] +fn test_load_tab_configs_rejects_literal_directory_that_is_file() { + let dir = tempfile::tempdir().unwrap(); + let file_path = dir.path().join("not-a-directory"); + std::fs::write(&file_path, "not a directory").unwrap(); + write_tab_config_toml_with_directory( + dir.path(), + "invalid.toml", + "Invalid", + &file_path.display().to_string(), + ); + + let (configs, errors) = load_tab_configs(dir.path()); + + assert!(configs.is_empty()); + assert_eq!(errors.len(), 1); + assert!(errors[0].error_message.contains("pane 'main'")); + assert!(errors[0].error_message.contains("not-a-directory")); + assert!( + errors[0] + .error_message + .contains("path exists but is not a directory") + ); +} + +#[cfg(feature = "local_fs")] +#[test] +fn test_load_tab_configs_accepts_existing_literal_directory() { + let dir = tempfile::tempdir().unwrap(); + let existing_dir = tempfile::tempdir().unwrap(); + write_tab_config_toml_with_directory( + dir.path(), + "valid.toml", + "Valid", + &existing_dir.path().display().to_string(), + ); + + let (configs, errors) = load_tab_configs(dir.path()); + + assert!(errors.is_empty()); + assert_eq!(configs.len(), 1); + assert_eq!(configs[0].name, "Valid"); +} + +#[cfg(feature = "local_fs")] +#[test] +fn test_load_tab_configs_expands_tilde_in_literal_directory() { + if !dirs::home_dir().is_some_and(|home_dir| home_dir.is_dir()) { + return; + } + let dir = tempfile::tempdir().unwrap(); + write_tab_config_toml_with_directory(dir.path(), "valid.toml", "Valid", "~"); + + let (configs, errors) = load_tab_configs(dir.path()); + + assert!(errors.is_empty()); + assert_eq!(configs.len(), 1); + assert_eq!(configs[0].name, "Valid"); +} + +#[cfg(feature = "local_fs")] +#[test] +fn test_load_tab_configs_allows_empty_directory() { + let dir = tempfile::tempdir().unwrap(); + write_tab_config_toml_with_directory(dir.path(), "valid.toml", "Valid", ""); + + let (configs, errors) = load_tab_configs(dir.path()); + + assert!(errors.is_empty()); + assert_eq!(configs.len(), 1); + assert_eq!(configs[0].name, "Valid"); +} + +#[cfg(feature = "local_fs")] +#[test] +fn test_load_tab_configs_allows_missing_directory() { + let dir = tempfile::tempdir().unwrap(); + write_tab_config_toml(dir.path(), "valid.toml", "Valid"); + + let (configs, errors) = load_tab_configs(dir.path()); + + assert!(errors.is_empty()); + assert_eq!(configs.len(), 1); + assert_eq!(configs[0].name, "Valid"); +} + +#[cfg(feature = "local_fs")] +#[test] +fn test_load_tab_configs_allows_parameterized_directory() { + let dir = tempfile::tempdir().unwrap(); + write_tab_config_toml_with_directory( + dir.path(), + "parameterized.toml", + "Parameterized", + "{{repo}}/does-not-need-to-exist-yet", + ); + + let (configs, errors) = load_tab_configs(dir.path()); + + assert!(errors.is_empty()); + assert_eq!(configs.len(), 1); + assert_eq!(configs[0].name, "Parameterized"); +} diff --git a/app/src/user_config/util.rs b/app/src/user_config/util.rs index baff692c8c2..6b8cec698d2 100644 --- a/app/src/user_config/util.rs +++ b/app/src/user_config/util.rs @@ -10,6 +10,7 @@ use serde::de::DeserializeOwned; use walkdir::{DirEntry, WalkDir}; use crate::launch_configs::launch_config::LaunchConfig; +use crate::tab_configs::tab_config::validate_load_time_tab_config_directories; use crate::tab_configs::{TabConfig, TabConfigError}; use crate::themes::theme::{ThemeKind, WarpTheme, WarpThemeConfig}; use crate::workflows::workflow::Workflow; @@ -178,6 +179,11 @@ pub(super) fn parse_tab_config_dir_entry( config.source_path = Some(item.path().into()); config }) + .and_then(|config| { + validate_load_time_tab_config_directories(&config) + .map_err(anyhow::Error::msg) + .map(|()| config) + }) .map_err(|e| TabConfigError { file_name, file_path: item.path().into(), diff --git a/app/src/view_components/dismissible_toast.rs b/app/src/view_components/dismissible_toast.rs index 8e8ed4d5f4a..39e6ed01a12 100644 --- a/app/src/view_components/dismissible_toast.rs +++ b/app/src/view_components/dismissible_toast.rs @@ -164,6 +164,11 @@ impl DismissibleToastStack { pub fn has_toasts(&self) -> bool { !self.toasts.is_empty() } + + #[cfg(test)] + pub fn toast_count(&self) -> usize { + self.toasts.len() + } } impl View for DismissibleToastStack { diff --git a/app/src/workspace/view.rs b/app/src/workspace/view.rs index c267c181156..3c0d0f47a21 100644 --- a/app/src/workspace/view.rs +++ b/app/src/workspace/view.rs @@ -6639,6 +6639,12 @@ impl Workspace { let tab_color = tab_config.color; let (rendered_title, pane_template) = crate::tab_configs::render_tab_config(&tab_config, ¶m_values, worktree_branch_name); + + if let Some(invalid_cwd) = find_invalid_tab_config_cwd(&pane_template) { + self.show_invalid_tab_config_cwd_toast(&tab_config, invalid_cwd, ctx); + return; + } + self.add_tab_with_pane_layout( PanesLayout::Template(pane_template), Arc::new(HashMap::new()), @@ -6653,6 +6659,37 @@ impl Workspace { } } + fn show_invalid_tab_config_cwd_toast( + &mut self, + tab_config: &crate::tab_configs::TabConfig, + invalid_cwd: &Path, + ctx: &mut ViewContext, + ) { + let message = format!( + "Tab config '{}' references a directory that does not exist or is not a directory: {}", + tab_config.name, + invalid_cwd.display() + ); + + let object_id = tab_config.source_path.as_ref().map_or_else( + || format!("tab_config_error:{}", tab_config.name), + |path| format!("tab_config_error:{}", path.display()), + ); + let mut toast = DismissibleToast::error(message).with_object_id(object_id.clone()); + if let Some(path) = &tab_config.source_path { + toast = toast.with_link(ToastLink::new("Open file".to_string()).with_onclick_action( + WorkspaceAction::OpenTabConfigErrorFile { + path: path.clone(), + toast_object_id: object_id, + }, + )); + } + + self.toast_stack.update(ctx, |toast_stack, ctx| { + toast_stack.add_persistent_toast(toast, ctx); + }); + } + /// Opens a tab config, showing the param-fill modal when the config has parameters, /// or opening the tab directly when there are no parameters. pub(crate) fn open_tab_config( @@ -8692,6 +8729,48 @@ impl Workspace { ctx.notify(); } + /// Auto-opens the conversation list on first app start. + /// Once we've done this once, we persist a preference so subsequent restarts + /// will respect the user's visibility preference (restored from workspace state). + fn maybe_auto_open_conversation_list(&mut self, ctx: &mut ViewContext) { + if !FeatureFlag::AgentViewConversationListView.is_enabled() + || !AISettings::as_ref(ctx).is_any_ai_enabled(ctx) + { + return; + } + + let has_auto_opened = *AISettings::as_ref(ctx).has_auto_opened_conversation_list; + if has_auto_opened { + return; + } + + // Only auto-open on terminal tabs, not on settings or other non-terminal tabs + let has_terminal = self + .active_tab_pane_group() + .as_ref(ctx) + .has_terminal_panes(); + if !has_terminal { + return; + } + + // For first-time-users, auto-open the conversation list for discoverability + if !self.active_tab_pane_group().as_ref(ctx).left_panel_open { + self.open_left_panel(ctx); + } + self.left_panel_view.update(ctx, |lp, ctx| { + lp.restore_active_view_from_snapshot(ToolPanelView::ConversationListView, ctx); + }); + + // Mark that we've done the one-time auto-open + AISettings::handle(ctx).update(ctx, |settings, ctx| { + report_if_error!( + settings + .has_auto_opened_conversation_list + .set_value(true, ctx) + ); + }); + } + fn close_left_panel(&mut self, ctx: &mut ViewContext) { self.left_panel_open = false; @@ -22568,6 +22647,21 @@ impl Workspace { } } +fn find_invalid_tab_config_cwd(pane_template: &PaneTemplateType) -> Option<&Path> { + match pane_template { + PaneTemplateType::PaneTemplate { cwd, .. } => { + if cwd.as_os_str().is_empty() || cwd.is_dir() { + None + } else { + Some(cwd.as_path()) + } + } + PaneTemplateType::PaneBranchTemplate { panes, .. } => { + panes.iter().find_map(find_invalid_tab_config_cwd) + } + } +} + impl Entity for Workspace { type Event = (); } diff --git a/app/src/workspace/view_tests.rs b/app/src/workspace/view_tests.rs index 048fa5610de..ec5494eee0e 100644 --- a/app/src/workspace/view_tests.rs +++ b/app/src/workspace/view_tests.rs @@ -2812,10 +2812,110 @@ fn test_open_tab_config_with_params_uses_explicit_title_template() { Some("mesa-coyote".to_string()) ); }); - }); -} -#[test] -fn test_toggle_tab_configs_menu_does_not_change_vertical_tabs_panel_in_horizontal_mode() { + }); + } + + #[test] + fn test_open_tab_config_with_params_rejects_nonexistent_cwd() { + App::test((), |mut app| async move { + initialize_app(&mut app); + + let workspace = mock_workspace(&mut app); + let cwd = std::env::temp_dir().join(format!( + "warp-tab-config-nonexistent-cwd-{}", + uuid::Uuid::new_v4() + )); + assert!(!cwd.exists()); + + let tab_config = crate::tab_configs::TabConfig { + name: "Missing cwd".to_string(), + title: None, + color: Some(AnsiColorIdentifier::Magenta), + panes: vec![TabConfigPaneNode { + id: "main".to_string(), + pane_type: Some(TabConfigPaneType::Terminal), + split: None, + children: None, + is_focused: Some(true), + directory: Some(cwd.display().to_string()), + commands: None, + shell: None, + }], + params: HashMap::new(), + source_path: Some(std::env::temp_dir().join("missing-cwd-tab-config.toml")), + }; + + workspace.update(&mut app, |workspace, ctx| { + workspace.open_tab_config_with_params(tab_config, HashMap::new(), None, ctx); + }); + + workspace.read(&app, |workspace, _| { + assert_eq!(workspace.tab_count(), 1); + assert_eq!(workspace.get_tab_color(0), None); + }); + workspace.read(&app, |workspace, ctx| { + assert_eq!(workspace.toast_stack.as_ref(ctx).toast_count(), 1); + }); + }); + } + + #[test] + fn test_find_invalid_tab_config_cwd_checks_nested_panes() { + use crate::launch_configs::launch_config::{PaneMode, PaneTemplateType, SplitDirection}; + + let valid_cwd = std::env::temp_dir(); + let invalid_cwd = valid_cwd.join(format!( + "warp-tab-config-nested-nonexistent-cwd-{}", + uuid::Uuid::new_v4() + )); + assert!(!invalid_cwd.exists()); + + let pane_template = PaneTemplateType::PaneBranchTemplate { + split_direction: SplitDirection::Horizontal, + panes: vec![ + PaneTemplateType::PaneTemplate { + cwd: valid_cwd, + commands: vec![], + is_focused: Some(true), + pane_mode: PaneMode::Terminal, + shell: None, + }, + PaneTemplateType::PaneBranchTemplate { + split_direction: SplitDirection::Vertical, + panes: vec![PaneTemplateType::PaneTemplate { + cwd: invalid_cwd.clone(), + commands: vec![], + is_focused: Some(false), + pane_mode: PaneMode::Terminal, + shell: None, + }], + }, + ], + }; + + assert_eq!( + find_invalid_tab_config_cwd(&pane_template), + Some(invalid_cwd.as_path()) + ); + } + + #[test] + fn test_find_invalid_tab_config_cwd_allows_empty_cwd() { + use crate::launch_configs::launch_config::{PaneMode, PaneTemplateType}; + + let pane_template = PaneTemplateType::PaneTemplate { + cwd: std::path::PathBuf::new(), + commands: vec![], + is_focused: Some(true), + pane_mode: PaneMode::Terminal, + shell: None, + }; + + assert_eq!(find_invalid_tab_config_cwd(&pane_template), None); + } + + #[test] + fn test_toggle_tab_configs_menu_does_not_change_vertical_tabs_panel_in_horizontal_mode() { let _vertical_tabs_guard = FeatureFlag::VerticalTabs.override_enabled(true); App::test((), |mut app| async move { From ec87948d0d471c98e82609a15327897c269e1b5a Mon Sep 17 00:00:00 2001 From: oleksiiovdiienko Date: Sun, 7 Jun 2026 20:40:19 +0300 Subject: [PATCH 3/4] Fix build errors after rebase: add missing import and remove obsolete function --- app/src/workspace/view.rs | 44 +-------------------------------------- 1 file changed, 1 insertion(+), 43 deletions(-) diff --git a/app/src/workspace/view.rs b/app/src/workspace/view.rs index 3c0d0f47a21..d940bb4646f 100644 --- a/app/src/workspace/view.rs +++ b/app/src/workspace/view.rs @@ -264,7 +264,7 @@ use crate::editor::{ use crate::env_vars::manager::{EnvVarCollectionManager, EnvVarCollectionSource}; use crate::env_vars::CloudEnvVarCollection; use crate::experiments::{BlockOnboarding, Experiment}; -use crate::launch_configs::launch_config::WindowTemplate; +use crate::launch_configs::launch_config::{PaneTemplateType, WindowTemplate}; use crate::launch_configs::save_modal::{LaunchConfigModalEvent, LaunchConfigSaveModal}; use crate::menu::{Event as MenuEvent, Menu, MenuItem, MenuItemFields, MenuSelectionSource}; use crate::modal::{Modal, ModalEvent, ModalViewState}; @@ -8729,48 +8729,6 @@ impl Workspace { ctx.notify(); } - /// Auto-opens the conversation list on first app start. - /// Once we've done this once, we persist a preference so subsequent restarts - /// will respect the user's visibility preference (restored from workspace state). - fn maybe_auto_open_conversation_list(&mut self, ctx: &mut ViewContext) { - if !FeatureFlag::AgentViewConversationListView.is_enabled() - || !AISettings::as_ref(ctx).is_any_ai_enabled(ctx) - { - return; - } - - let has_auto_opened = *AISettings::as_ref(ctx).has_auto_opened_conversation_list; - if has_auto_opened { - return; - } - - // Only auto-open on terminal tabs, not on settings or other non-terminal tabs - let has_terminal = self - .active_tab_pane_group() - .as_ref(ctx) - .has_terminal_panes(); - if !has_terminal { - return; - } - - // For first-time-users, auto-open the conversation list for discoverability - if !self.active_tab_pane_group().as_ref(ctx).left_panel_open { - self.open_left_panel(ctx); - } - self.left_panel_view.update(ctx, |lp, ctx| { - lp.restore_active_view_from_snapshot(ToolPanelView::ConversationListView, ctx); - }); - - // Mark that we've done the one-time auto-open - AISettings::handle(ctx).update(ctx, |settings, ctx| { - report_if_error!( - settings - .has_auto_opened_conversation_list - .set_value(true, ctx) - ); - }); - } - fn close_left_panel(&mut self, ctx: &mut ViewContext) { self.left_panel_open = false; From 31f0bb0f7e22dce50cb4c86917ff750ef66341b0 Mon Sep 17 00:00:00 2001 From: Oleksii Date: Wed, 8 Jul 2026 22:38:28 +0300 Subject: [PATCH 4/4] Surface missing tab config directories as an auto-dismissing toast on open A non-existent directory is a runtime issue, not a config-file parse error, so stop rejecting tab configs at load time when a literal directory is missing or isn't a folder. When opening a tab config, fall back to the home directory for any pane whose directory is missing or not a folder, open the tab as normal, and show a toast per affected pane. The toast uses the original directory text from the config (preserving `~`) and is ephemeral so it auto-dismisses after the toast timeout. Drop the now-unused validate_load_time_tab_config_directories, contains_template_placeholder, and find_invalid_tab_config_cwd helpers, and update tests accordingly. --- Cargo.lock | 186 +++++++++++++++++++----------- app/src/tab_configs/tab_config.rs | 42 ------- app/src/user_config/mod_tests.rs | 26 ++--- app/src/user_config/util.rs | 6 - app/src/workspace/view.rs | 58 ++++++---- app/src/workspace/view_tests.rs | 69 ++--------- 6 files changed, 168 insertions(+), 219 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e3f39488ae..e46eebe2359 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -428,9 +428,9 @@ checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" [[package]] name = "alphanumeric-sort" -version = "1.5.7" +version = "1.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44ed2d38c0ddba387b4e25a9314fbce6a805f9988e66896e454fed1f8432e4e5" +checksum = "a97de3ad85c4425a2c267a5e91adbe49f9f14e650bd9c78e96b6bc00e5f6673a" [[package]] name = "alsa" @@ -609,7 +609,7 @@ dependencies = [ "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", - "windows-sys 0.52.0", + "windows-sys 0.59.0", "wl-clipboard-rs", "x11rb", ] @@ -866,9 +866,9 @@ dependencies = [ [[package]] name = "arborium-nix" -version = "2.13.0" +version = "2.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08a703d5502526e68f7ad8dbffd303537d6828005bed0ef0fc7d37e45bf7078" +checksum = "b56328852b01793707818ed971ae42129693ad0026d330a0fd42a600a54bd7aa" dependencies = [ "arborium-sysroot", "cc", @@ -1000,9 +1000,9 @@ dependencies = [ [[package]] name = "arborium-sysroot" -version = "2.13.0" +version = "2.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ce9624bea89e41ab45de48c1e94d4a8587f9ed429801314c91255149a852cc9" +checksum = "59d99d80550b726f9dec7ee6d07118c31e08b10e729ac488eabd4c10603dc841" dependencies = [ "cc", "dlmalloc", @@ -2614,7 +2614,7 @@ checksum = "3281776daed25d20f2a7846151561bac63091d41115ba9a5d8ac50891fb749d6" dependencies = [ "candle-core", "candle-nn", - "prost", + "prost 0.14.3", "prost-build", ] @@ -3110,7 +3110,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4445,7 +4445,7 @@ checksum = "6738d2e996274e499bc7b0d693c858b7720b9cd2543a0643a3087e6cb0a4fa16" dependencies = [ "cfg-if", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4750,7 +4750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4922,7 +4922,7 @@ name = "field_mask" version = "0.0.0" dependencies = [ "itertools 0.14.0", - "prost", + "prost 0.14.3", "prost-reflect", "prost-types", "thiserror 2.0.17", @@ -6404,7 +6404,7 @@ dependencies = [ "log", "oauth2", "prevent_sleep", - "prost", + "prost 0.14.3", "reqwest", "serde", "serde_json", @@ -6875,7 +6875,7 @@ dependencies = [ "ndarray", "ort", "parking_lot", - "prost", + "prost 0.14.3", "rust-embed 8.7.2", "serde", "smol_str", @@ -7085,7 +7085,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi 0.5.2", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -7252,9 +7252,9 @@ dependencies = [ [[package]] name = "jemalloc_pprof" -version = "0.8.2" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d44c349cfe2654897fadcb9de4f0bfbf48288ec344f700b2bd59f152dd209" +checksum = "74ff642505c7ce8d31c0d43ec0e235c6fd4585d9b8172d8f9dd04d36590200b5" dependencies = [ "anyhow", "libc", @@ -7279,7 +7279,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -9463,9 +9463,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.80" +version = "0.10.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" dependencies = [ "bitflags 2.13.0", "cfg-if", @@ -9494,9 +9494,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.116" +version = "0.9.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" dependencies = [ "cc", "libc", @@ -9542,7 +9542,7 @@ dependencies = [ "opentelemetry-http", "opentelemetry-proto", "opentelemetry_sdk", - "prost", + "prost 0.14.3", "thiserror 2.0.17", "zstd", ] @@ -9555,7 +9555,7 @@ checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" dependencies = [ "opentelemetry", "opentelemetry_sdk", - "prost", + "prost 0.14.3", ] [[package]] @@ -9677,7 +9677,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.45.0", + "windows-sys 0.61.2", ] [[package]] @@ -9916,9 +9916,9 @@ dependencies = [ [[package]] name = "pest" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9" dependencies = [ "memchr", "ucd-trie", @@ -9926,9 +9926,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +checksum = "4b4254325ecad416ab689e27ba51da03ba01a9632bc6e108f5fe7c3c4ad29d58" dependencies = [ "pest", "pest_generator", @@ -9936,9 +9936,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +checksum = "6c4c0e91ead7a8f7acecbca6f003fc2e8282b1dbe2dd9c9d2f16aba42995e0a7" dependencies = [ "pest", "pest_meta", @@ -9949,12 +9949,11 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +checksum = "f9744bc48116fee06334924bb5f2bad41eed5e89bd26e29b0b799f9a3f82c210" dependencies = [ "pest", - "sha2 0.10.9", ] [[package]] @@ -10251,15 +10250,15 @@ dependencies = [ [[package]] name = "pprof_util" -version = "0.8.2" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eea0cc524de808a6d98d192a3d99fe95617031ad4a52ec0a0f987ef4432e8fe1" +checksum = "4429d44e5e2c8a69399fc0070379201eed018e3df61e04eb7432811df073c224" dependencies = [ "anyhow", "flate2", "num", "paste", - "prost", + "prost 0.13.5", ] [[package]] @@ -10476,6 +10475,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive 0.13.5", +] + [[package]] name = "prost" version = "0.14.3" @@ -10483,7 +10492,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.14.3", ] [[package]] @@ -10492,19 +10501,32 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" dependencies = [ - "heck 0.4.1", - "itertools 0.10.5", + "heck 0.5.0", + "itertools 0.14.0", "log", "multimap", "petgraph", "prettyplease", - "prost", + "prost 0.14.3", "prost-types", "regex", "syn 2.0.117", "tempfile", ] +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "prost-derive" version = "0.14.3" @@ -10512,7 +10534,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.117", @@ -10524,7 +10546,7 @@ version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89455ef41ed200cafc47c76c552ee7792370ac420497e551f16123a9135f76e" dependencies = [ - "prost", + "prost 0.14.3", "prost-reflect-derive", "prost-types", ] @@ -10556,7 +10578,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" dependencies = [ - "prost", + "prost 0.14.3", ] [[package]] @@ -10757,7 +10779,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -10921,14 +10943,15 @@ checksum = "977b1e897f9d764566891689e642653e5ed90c6895106acd005eb4c1d0203991" [[package]] name = "ratatui" -version = "0.30.1" +version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1695748e3a735b34968c887ceea5a380b43545903868ae8f5b666593100f6b68" +checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" dependencies = [ "instability", "ratatui-core", "ratatui-crossterm", "ratatui-macros", + "ratatui-termina", "ratatui-termwiz", "ratatui-widgets", "serde", @@ -10936,15 +10959,14 @@ dependencies = [ [[package]] name = "ratatui-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3603f354bba8c595fa47860e60142d7372b7210c27044c6a7d0e1a4336b44" +checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" dependencies = [ "bitflags 2.13.0", "compact_str", "critical-section", "hashbrown 0.17.1", - "indoc", "itertools 0.14.0", "kasuari", "lru 0.18.0", @@ -10959,9 +10981,9 @@ dependencies = [ [[package]] name = "ratatui-crossterm" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2867bedcbd6a690ca4f8672a687b730ec07660c79844517b084311b529980c" +checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0" dependencies = [ "cfg-if", "crossterm", @@ -10971,19 +10993,30 @@ dependencies = [ [[package]] name = "ratatui-macros" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80fac59720679490d89d200df411faa249be728681adcabed3d047ae72c48f1d" +checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814" dependencies = [ "ratatui-core", "ratatui-widgets", ] +[[package]] +name = "ratatui-termina" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2" +dependencies = [ + "instability", + "ratatui-core", + "termina", +] + [[package]] name = "ratatui-termwiz" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "386b8ff8f74ed749509391c56d549761a2fcdb408e1f42e467286bcb7dac8967" +checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977" dependencies = [ "ratatui-core", "termwiz", @@ -10991,9 +11024,9 @@ dependencies = [ [[package]] name = "ratatui-widgets" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef4f17dd7ac3abf5adc2b920a03c61eee4bfe6a88fa5191936895525371d79c" +checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" dependencies = [ "bitflags 2.13.0", "hashbrown 0.17.1", @@ -11294,7 +11327,7 @@ dependencies = [ "futures-lite 1.13.0", "getrandom 0.2.16", "log", - "prost", + "prost 0.14.3", "prost-build", "repo_metadata", "serde", @@ -11737,7 +11770,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -11750,7 +11783,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -11818,7 +11851,7 @@ dependencies = [ "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs 1.0.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -13453,9 +13486,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.46" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", @@ -13490,7 +13523,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.2", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -13513,6 +13546,19 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termina" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" +dependencies = [ + "bitflags 2.13.0", + "parking_lot", + "rustix 1.1.2", + "signal-hook", + "windows-sys 0.61.2", +] + [[package]] name = "terminfo" version = "0.9.0" @@ -13813,7 +13859,7 @@ version = "0.3.0" source = "git+https://github.com/warpdotdev/tink-rust?rev=54b9ac9af93b0c08b446a7bc0582836c9403a71b#54b9ac9af93b0c08b446a7bc0582836c9403a71b" dependencies = [ "base64 0.22.1", - "prost", + "prost 0.14.3", "prost-build", "serde", ] @@ -15164,7 +15210,7 @@ dependencies = [ "pin-project", "plist", "pprof", - "prost", + "prost 0.14.3", "prost-build", "prost-types", "qrcode", @@ -15675,7 +15721,7 @@ name = "warp_multi_agent_api" version = "0.0.0" source = "git+https://github.com/warpdotdev/warp-proto-apis.git?rev=4de32f256901e34905817e5d88081130a54b6384#4de32f256901e34905817e5d88081130a54b6384" dependencies = [ - "prost", + "prost 0.14.3", "prost-reflect", "prost-reflect-build", "prost-types", @@ -15691,7 +15737,7 @@ dependencies = [ "base64 0.22.1", "cfg-if", "futures", - "prost", + "prost 0.14.3", "thiserror 2.0.17", "tracing", "tracing-futures", @@ -16824,7 +16870,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/app/src/tab_configs/tab_config.rs b/app/src/tab_configs/tab_config.rs index 0b6e5c80ae1..57ffec971f9 100644 --- a/app/src/tab_configs/tab_config.rs +++ b/app/src/tab_configs/tab_config.rs @@ -203,48 +203,6 @@ impl TabConfig { } } -pub(crate) fn validate_load_time_tab_config_directories(config: &TabConfig) -> Result<(), String> { - let mut invalid_directories = Vec::new(); - - for pane in &config.panes { - let Some(directory) = pane.directory.as_deref() else { - continue; - }; - - if directory.is_empty() || contains_template_placeholder(directory) { - continue; - } - - let expanded = shellexpand::tilde(directory).into_owned(); - let path = PathBuf::from(&expanded); - if path.is_dir() { - continue; - } - - let reason = if path.exists() { - "path exists but is not a directory" - } else { - "directory does not exist" - }; - invalid_directories.push(format!( - "pane '{}' references invalid directory '{}': {}", - pane.id, expanded, reason - )); - } - - if invalid_directories.is_empty() { - Ok(()) - } else { - Err(invalid_directories.join("; ")) - } -} - -fn contains_template_placeholder(value: &str) -> bool { - value - .split_once("{{") - .is_some_and(|(_, rest)| rest.contains("}}")) -} - /// Renders a [`TabConfig`] with the given param values into a [`PaneTemplateType`] /// and an optional rendered title. /// diff --git a/app/src/user_config/mod_tests.rs b/app/src/user_config/mod_tests.rs index d6d6f6f7f3b..82e62164c6c 100644 --- a/app/src/user_config/mod_tests.rs +++ b/app/src/user_config/mod_tests.rs @@ -197,7 +197,7 @@ directory = "{directory}" #[cfg(feature = "local_fs")] #[test] -fn test_load_tab_configs_rejects_nonexistent_literal_directory() { +fn test_load_tab_configs_allows_nonexistent_literal_directory() { let dir = tempfile::tempdir().unwrap(); let nonexistent_dir = dir.path().join("does-not-exist"); write_tab_config_toml_with_directory( @@ -209,18 +209,14 @@ fn test_load_tab_configs_rejects_nonexistent_literal_directory() { let (configs, errors) = load_tab_configs(dir.path()); - assert!(configs.is_empty()); - assert_eq!(errors.len(), 1); - assert_eq!(errors[0].file_name, "invalid.toml"); - assert!(errors[0].file_path.ends_with("invalid.toml")); - assert!(errors[0].error_message.contains("pane 'main'")); - assert!(errors[0].error_message.contains("does-not-exist")); - assert!(errors[0].error_message.contains("directory does not exist")); + assert!(errors.is_empty()); + assert_eq!(configs.len(), 1); + assert_eq!(configs[0].name, "Invalid"); } #[cfg(feature = "local_fs")] #[test] -fn test_load_tab_configs_rejects_literal_directory_that_is_file() { +fn test_load_tab_configs_allows_literal_directory_that_is_file() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("not-a-directory"); std::fs::write(&file_path, "not a directory").unwrap(); @@ -233,15 +229,9 @@ fn test_load_tab_configs_rejects_literal_directory_that_is_file() { let (configs, errors) = load_tab_configs(dir.path()); - assert!(configs.is_empty()); - assert_eq!(errors.len(), 1); - assert!(errors[0].error_message.contains("pane 'main'")); - assert!(errors[0].error_message.contains("not-a-directory")); - assert!( - errors[0] - .error_message - .contains("path exists but is not a directory") - ); + assert!(errors.is_empty()); + assert_eq!(configs.len(), 1); + assert_eq!(configs[0].name, "Invalid"); } #[cfg(feature = "local_fs")] diff --git a/app/src/user_config/util.rs b/app/src/user_config/util.rs index de7d81b153f..c98a93d8450 100644 --- a/app/src/user_config/util.rs +++ b/app/src/user_config/util.rs @@ -13,7 +13,6 @@ use crate::ai::custom_model_routers::{ parse_model_config_yaml, CustomModelRouter, ModelConfigError, }; use crate::launch_configs::launch_config::LaunchConfig; -use crate::tab_configs::tab_config::validate_load_time_tab_config_directories; use crate::tab_configs::{TabConfig, TabConfigError}; use crate::themes::theme::{ThemeKind, WarpTheme, WarpThemeConfig}; use crate::workflows::workflow::Workflow; @@ -182,11 +181,6 @@ pub(super) fn parse_tab_config_dir_entry( config.source_path = Some(item.path().into()); config }) - .and_then(|config| { - validate_load_time_tab_config_directories(&config) - .map_err(anyhow::Error::msg) - .map(|()| config) - }) .map_err(|e| TabConfigError { file_name, file_path: item.path().into(), diff --git a/app/src/workspace/view.rs b/app/src/workspace/view.rs index 3853467cefd..10a02ba06fc 100644 --- a/app/src/workspace/view.rs +++ b/app/src/workspace/view.rs @@ -275,7 +275,7 @@ use crate::editor::{ use crate::env_vars::manager::{EnvVarCollectionManager, EnvVarCollectionSource}; use crate::env_vars::CloudEnvVarCollection; use crate::experiments::{BlockOnboarding, Experiment}; -use crate::launch_configs::launch_config::{PaneTemplateType, WindowTemplate}; +use crate::launch_configs::launch_config::WindowTemplate; use crate::launch_configs::save_modal::{LaunchConfigModalEvent, LaunchConfigSaveModal}; use crate::menu::{Event as MenuEvent, Menu, MenuItem, MenuItemFields, MenuSelectionSource}; use crate::modal::{Modal, ModalEvent, ModalViewState}; @@ -6849,15 +6849,35 @@ impl Workspace { worktree_branch_name: Option<&str>, ctx: &mut ViewContext, ) { + let mut tab_config = tab_config; + let mut invalid_pane_directories = Vec::new(); + + let mut unquoted_context = param_values.clone(); + if let Some(name) = worktree_branch_name { + unquoted_context.insert( + crate::tab_configs::tab_config::AUTOGENERATED_BRANCH_NAME_PARAM.to_string(), + name.to_string(), + ); + } + + for pane in &mut tab_config.panes { + if let Some(ref original_dir_text) = pane.directory { + if !original_dir_text.is_empty() { + let rendered = handlebars::render_template(original_dir_text, &unquoted_context); + let expanded = shellexpand::tilde(&rendered).into_owned(); + let path = PathBuf::from(&expanded); + if !expanded.is_empty() && (!path.exists() || !path.is_dir()) { + invalid_pane_directories.push(original_dir_text.clone()); + pane.directory = dirs::home_dir().map(|p| p.display().to_string()); + } + } + } + } + let tab_color = tab_config.color; let (rendered_title, pane_template) = crate::tab_configs::render_tab_config(&tab_config, ¶m_values, worktree_branch_name); - if let Some(invalid_cwd) = find_invalid_tab_config_cwd(&pane_template) { - self.show_invalid_tab_config_cwd_toast(&tab_config, invalid_cwd, ctx); - return; - } - self.add_tab_with_pane_layout( PanesLayout::Template(pane_template), Arc::new(HashMap::new()), @@ -6870,18 +6890,22 @@ impl Workspace { tab.selected_color = SelectedTabColor::Color(color); } } + + for original_directory_text in invalid_pane_directories { + self.show_invalid_tab_config_cwd_toast(&tab_config, &original_directory_text, ctx); + } } fn show_invalid_tab_config_cwd_toast( &mut self, tab_config: &crate::tab_configs::TabConfig, - invalid_cwd: &Path, + original_directory_text: &str, ctx: &mut ViewContext, ) { let message = format!( - "Tab config '{}' references a directory that does not exist or is not a directory: {}", + "Tab config '{}' couldn't open '{}': directory is missing or not a folder", tab_config.name, - invalid_cwd.display() + original_directory_text ); let object_id = tab_config.source_path.as_ref().map_or_else( @@ -6899,7 +6923,7 @@ impl Workspace { } self.toast_stack.update(ctx, |toast_stack, ctx| { - toast_stack.add_persistent_toast(toast, ctx); + toast_stack.add_ephemeral_toast(toast, ctx); }); } @@ -23657,20 +23681,6 @@ impl Workspace { } } -fn find_invalid_tab_config_cwd(pane_template: &PaneTemplateType) -> Option<&Path> { - match pane_template { - PaneTemplateType::PaneTemplate { cwd, .. } => { - if cwd.as_os_str().is_empty() || cwd.is_dir() { - None - } else { - Some(cwd.as_path()) - } - } - PaneTemplateType::PaneBranchTemplate { panes, .. } => { - panes.iter().find_map(find_invalid_tab_config_cwd) - } - } -} impl Entity for Workspace { type Event = (); diff --git a/app/src/workspace/view_tests.rs b/app/src/workspace/view_tests.rs index 762dcac067e..005a371f443 100644 --- a/app/src/workspace/view_tests.rs +++ b/app/src/workspace/view_tests.rs @@ -3220,71 +3220,22 @@ fn test_open_tab_config_with_params_uses_explicit_title_template() { workspace.open_tab_config_with_params(tab_config, HashMap::new(), None, ctx); }); - workspace.read(&app, |workspace, _| { - assert_eq!(workspace.tab_count(), 1); - assert_eq!(workspace.get_tab_color(0), None); - }); workspace.read(&app, |workspace, ctx| { + assert_eq!(workspace.tab_count(), 2); + assert_eq!(workspace.get_tab_color(1), Some(AnsiColorIdentifier::Magenta)); + + let active_pane_group = workspace.active_tab_pane_group(); + active_pane_group.read(ctx, |pane_group, ctx| { + let terminal_view = pane_group.terminal_view_at_pane_index(0, ctx).unwrap(); + let model = terminal_view.as_ref(ctx).model.lock(); + assert_eq!(model.session_startup_path(), dirs::home_dir()); + }); + assert_eq!(workspace.toast_stack.as_ref(ctx).toast_count(), 1); }); }); } - #[test] - fn test_find_invalid_tab_config_cwd_checks_nested_panes() { - use crate::launch_configs::launch_config::{PaneMode, PaneTemplateType, SplitDirection}; - - let valid_cwd = std::env::temp_dir(); - let invalid_cwd = valid_cwd.join(format!( - "warp-tab-config-nested-nonexistent-cwd-{}", - uuid::Uuid::new_v4() - )); - assert!(!invalid_cwd.exists()); - - let pane_template = PaneTemplateType::PaneBranchTemplate { - split_direction: SplitDirection::Horizontal, - panes: vec![ - PaneTemplateType::PaneTemplate { - cwd: valid_cwd, - commands: vec![], - is_focused: Some(true), - pane_mode: PaneMode::Terminal, - shell: None, - }, - PaneTemplateType::PaneBranchTemplate { - split_direction: SplitDirection::Vertical, - panes: vec![PaneTemplateType::PaneTemplate { - cwd: invalid_cwd.clone(), - commands: vec![], - is_focused: Some(false), - pane_mode: PaneMode::Terminal, - shell: None, - }], - }, - ], - }; - - assert_eq!( - find_invalid_tab_config_cwd(&pane_template), - Some(invalid_cwd.as_path()) - ); - } - - #[test] - fn test_find_invalid_tab_config_cwd_allows_empty_cwd() { - use crate::launch_configs::launch_config::{PaneMode, PaneTemplateType}; - - let pane_template = PaneTemplateType::PaneTemplate { - cwd: std::path::PathBuf::new(), - commands: vec![], - is_focused: Some(true), - pane_mode: PaneMode::Terminal, - shell: None, - }; - - assert_eq!(find_invalid_tab_config_cwd(&pane_template), None); - } - #[test] fn test_toggle_tab_configs_menu_does_not_change_vertical_tabs_panel_in_horizontal_mode() { let _vertical_tabs_guard = FeatureFlag::VerticalTabs.override_enabled(true);