From 72e47a3095c6445eaf972aac6df3becf8dcb5ed4 Mon Sep 17 00:00:00 2001 From: Adrian Schmidt-Foehre Date: Thu, 12 Mar 2026 16:43:04 +0100 Subject: [PATCH 1/2] Added configurable working directory --- ClaudeCodeControl.ProviderManagement.cs | 183 ++++++++++++++++++++++++ ClaudeCodeControl.Workspace.cs | 53 ++++++- ClaudeCodeControl.xaml | 2 + ClaudeCodeModels.cs | 7 + 4 files changed, 244 insertions(+), 1 deletion(-) diff --git a/ClaudeCodeControl.ProviderManagement.cs b/ClaudeCodeControl.ProviderManagement.cs index af631c2..f4d9f31 100644 --- a/ClaudeCodeControl.ProviderManagement.cs +++ b/ClaudeCodeControl.ProviderManagement.cs @@ -1644,6 +1644,46 @@ private void ProviderContextMenu_Opened(object sender, RoutedEventArgs e) AutoOpenChangesMenuItem.IsChecked = _settings.AutoOpenChangesOnPrompt; ClaudeDangerouslySkipPermissionsMenuItem.IsChecked = _settings.ClaudeDangerouslySkipPermissions; CodexFullAutoMenuItem.IsChecked = _settings.CodexFullAuto; + + // Update working directory menu item to show current value, with red text if path doesn't exist + if (!string.IsNullOrWhiteSpace(_settings.CustomWorkingDirectory)) + { + string customDir = _settings.CustomWorkingDirectory.Trim(); + bool directoryExists = false; + try + { + if (Path.IsPathRooted(customDir)) + { + directoryExists = Directory.Exists(customDir); + } + else + { + // Resolve relative path against base workspace directory + string baseDir = ThreadHelper.JoinableTaskFactory.Run(async () => await GetBaseWorkspaceDirectoryAsync()); + string resolved = Path.GetFullPath(Path.Combine(baseDir, customDir)); + directoryExists = Directory.Exists(resolved); + } + } + catch + { + directoryExists = false; + } + + var headerBlock = new System.Windows.Controls.TextBlock(); + headerBlock.Inlines.Add("Set Working Directory ("); + var pathRun = new System.Windows.Documents.Run(customDir); + if (!directoryExists) + { + pathRun.Foreground = System.Windows.Media.Brushes.Red; + } + headerBlock.Inlines.Add(pathRun); + headerBlock.Inlines.Add(")"); + SetWorkingDirectoryMenuItem.Header = headerBlock; + } + else + { + SetWorkingDirectoryMenuItem.Header = "Set Working Directory..."; + } } } @@ -1724,6 +1764,149 @@ private void CodexFullAutoMenuItem_Click(object sender, RoutedEventArgs e) } } + /// + /// Handles set working directory menu item click - prompts user for a custom working directory + /// + private void SetWorkingDirectoryMenuItem_Click(object sender, RoutedEventArgs e) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (_settings == null) return; + + string currentValue = _settings.CustomWorkingDirectory ?? ""; + + // Show input dialog; returns null on Cancel, or the entered string on OK + string input = ShowWorkingDirectoryInputDialog(currentValue); + if (input == null) + { + // User cancelled - no change + return; + } + + string trimmed = input.Trim(); + if (trimmed != currentValue) + { + _settings.CustomWorkingDirectory = trimmed; + SaveSettings(); + + // Restart terminal to apply the new working directory + ThreadHelper.JoinableTaskFactory.Run(async delegate + { + try + { + await RestartTerminalWithSelectedProviderAsync(); + } + catch (Exception ex) + { + Debug.WriteLine($"Error restarting terminal after working directory change: {ex.Message}"); + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + MessageBox.Show($"Failed to restart terminal: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + } + }); + } + } + + /// + /// Shows a WPF input dialog for the custom working directory setting + /// + /// The current value to pre-populate + /// The entered string on OK, or null if the user cancelled + private string ShowWorkingDirectoryInputDialog(string currentValue) + { + // Build dialog window programmatically + var dialog = new Window + { + Title = "Set Working Directory", + Width = 500, + Height = 200, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + ResizeMode = ResizeMode.NoResize, + Background = System.Windows.SystemColors.WindowBrush, + ShowInTaskbar = false + }; + + // Try to set owner to VS main window + try + { + dialog.Owner = Application.Current?.MainWindow; + } + catch + { + // Ignore if owner cannot be set + } + + var grid = new System.Windows.Controls.Grid(); + grid.Margin = new Thickness(12); + grid.RowDefinitions.Add(new System.Windows.Controls.RowDefinition { Height = GridLength.Auto }); + grid.RowDefinitions.Add(new System.Windows.Controls.RowDefinition { Height = GridLength.Auto }); + grid.RowDefinitions.Add(new System.Windows.Controls.RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); + grid.RowDefinitions.Add(new System.Windows.Controls.RowDefinition { Height = GridLength.Auto }); + + // Label + var label = new System.Windows.Controls.TextBlock + { + Text = "Enter a custom working directory for the terminal:\n" + + " - Absolute path (e.g. C:\\Projects\\MyRepo)\n" + + " - Relative path to solution directory (e.g. ..\\OtherRepo)\n" + + " - Leave empty to use the default solution directory", + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 0, 0, 8) + }; + System.Windows.Controls.Grid.SetRow(label, 0); + grid.Children.Add(label); + + // TextBox + var textBox = new System.Windows.Controls.TextBox + { + Text = currentValue, + Margin = new Thickness(0, 0, 0, 12) + }; + textBox.SelectAll(); + System.Windows.Controls.Grid.SetRow(textBox, 1); + grid.Children.Add(textBox); + + // Button panel + var buttonPanel = new System.Windows.Controls.StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Right + }; + System.Windows.Controls.Grid.SetRow(buttonPanel, 3); + + var okButton = new System.Windows.Controls.Button + { + Content = "OK", + Width = 75, + Height = 25, + Margin = new Thickness(0, 0, 8, 0), + IsDefault = true + }; + okButton.Click += (s, args) => { dialog.DialogResult = true; }; + buttonPanel.Children.Add(okButton); + + var cancelButton = new System.Windows.Controls.Button + { + Content = "Cancel", + Width = 75, + Height = 25, + IsCancel = true + }; + buttonPanel.Children.Add(cancelButton); + + grid.Children.Add(buttonPanel); + dialog.Content = grid; + + // Focus the text box when loaded + dialog.Loaded += (s, args) => { textBox.Focus(); }; + + if (dialog.ShowDialog() == true) + { + return textBox.Text; + } + + return null; + } + #endregion } } diff --git a/ClaudeCodeControl.Workspace.cs b/ClaudeCodeControl.Workspace.cs index d58727e..1f306ac 100644 --- a/ClaudeCodeControl.Workspace.cs +++ b/ClaudeCodeControl.Workspace.cs @@ -73,10 +73,61 @@ private void SetupSolutionEvents() #region Workspace Directory Management /// - /// Gets the current workspace directory (solution or project directory) + /// Gets the current workspace directory (solution or project directory), + /// applying the custom working directory setting if configured /// /// The workspace directory path, or My Documents as fallback private async Task GetWorkspaceDirectoryAsync() + { + string baseDir = await GetBaseWorkspaceDirectoryAsync(); + + // Apply custom working directory if configured + if (_settings != null && !string.IsNullOrWhiteSpace(_settings.CustomWorkingDirectory)) + { + try + { + string customDir = _settings.CustomWorkingDirectory.Trim(); + + if (Path.IsPathRooted(customDir)) + { + // Absolute path: use as-is if it exists + if (Directory.Exists(customDir)) + { + return customDir; + } + else + { + Debug.WriteLine($"Custom working directory does not exist: {customDir}"); + } + } + else + { + // Relative path: resolve against the base workspace directory + string resolved = Path.GetFullPath(Path.Combine(baseDir, customDir)); + if (Directory.Exists(resolved)) + { + return resolved; + } + else + { + Debug.WriteLine($"Custom working directory (resolved) does not exist: {resolved}"); + } + } + } + catch (Exception ex) + { + Debug.WriteLine($"Error resolving custom working directory: {ex.Message}"); + } + } + + return baseDir; + } + + /// + /// Gets the base workspace directory from the solution or project, before applying custom directory overrides + /// + /// The base workspace directory path, or My Documents as fallback + private async Task GetBaseWorkspaceDirectoryAsync() { try { diff --git a/ClaudeCodeControl.xaml b/ClaudeCodeControl.xaml index 18aab2e..d41eeb8 100644 --- a/ClaudeCodeControl.xaml +++ b/ClaudeCodeControl.xaml @@ -278,6 +278,8 @@ + + diff --git a/ClaudeCodeModels.cs b/ClaudeCodeModels.cs index f7790a7..61758f0 100644 --- a/ClaudeCodeModels.cs +++ b/ClaudeCodeModels.cs @@ -85,5 +85,12 @@ public class ClaudeCodeSettings /// Applies to Codex (Windows native) and Codex (WSL) /// public bool CodexFullAuto { get; set; } = false; + + /// + /// Custom working directory for the terminal. + /// Can be an absolute path or a path relative to the solution directory. + /// When empty or null, the default solution/project directory is used. + /// + public string CustomWorkingDirectory { get; set; } = ""; } } From da5cccc6166015bd0378eeef868c58e4443210d0 Mon Sep 17 00:00:00 2001 From: Adrian Schmidt-Foehre Date: Thu, 12 Mar 2026 16:49:26 +0100 Subject: [PATCH 2/2] evaluate path validity during edit time --- ClaudeCodeControl.ProviderManagement.cs | 48 ++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/ClaudeCodeControl.ProviderManagement.cs b/ClaudeCodeControl.ProviderManagement.cs index f4d9f31..53d616e 100644 --- a/ClaudeCodeControl.ProviderManagement.cs +++ b/ClaudeCodeControl.ProviderManagement.cs @@ -1775,8 +1775,11 @@ private void SetWorkingDirectoryMenuItem_Click(object sender, RoutedEventArgs e) string currentValue = _settings.CustomWorkingDirectory ?? ""; + // Resolve base workspace directory for relative path validation in the dialog + string baseDir = ThreadHelper.JoinableTaskFactory.Run(async () => await GetBaseWorkspaceDirectoryAsync()); + // Show input dialog; returns null on Cancel, or the entered string on OK - string input = ShowWorkingDirectoryInputDialog(currentValue); + string input = ShowWorkingDirectoryInputDialog(currentValue, baseDir); if (input == null) { // User cancelled - no change @@ -1807,11 +1810,13 @@ private void SetWorkingDirectoryMenuItem_Click(object sender, RoutedEventArgs e) } /// - /// Shows a WPF input dialog for the custom working directory setting + /// Shows a WPF input dialog for the custom working directory setting. + /// Validates the path in real-time, coloring the text red when the directory does not exist. /// /// The current value to pre-populate + /// The base workspace directory used to resolve relative paths /// The entered string on OK, or null if the user cancelled - private string ShowWorkingDirectoryInputDialog(string currentValue) + private string ShowWorkingDirectoryInputDialog(string currentValue, string baseDir) { // Build dialog window programmatically var dialog = new Window @@ -1855,6 +1860,9 @@ private string ShowWorkingDirectoryInputDialog(string currentValue) System.Windows.Controls.Grid.SetRow(label, 0); grid.Children.Add(label); + // Default foreground for restoring after validation + var defaultForeground = System.Windows.SystemColors.WindowTextBrush; + // TextBox var textBox = new System.Windows.Controls.TextBox { @@ -1865,6 +1873,38 @@ private string ShowWorkingDirectoryInputDialog(string currentValue) System.Windows.Controls.Grid.SetRow(textBox, 1); grid.Children.Add(textBox); + // Real-time path validation on text change + textBox.TextChanged += (s, args) => + { + string path = textBox.Text.Trim(); + if (string.IsNullOrEmpty(path)) + { + // Empty means default directory - always valid + textBox.Foreground = defaultForeground; + return; + } + + bool exists = false; + try + { + if (Path.IsPathRooted(path)) + { + exists = Directory.Exists(path); + } + else + { + string resolved = Path.GetFullPath(Path.Combine(baseDir, path)); + exists = Directory.Exists(resolved); + } + } + catch + { + exists = false; + } + + textBox.Foreground = exists ? defaultForeground : System.Windows.Media.Brushes.Red; + }; + // Button panel var buttonPanel = new System.Windows.Controls.StackPanel { @@ -1896,7 +1936,7 @@ private string ShowWorkingDirectoryInputDialog(string currentValue) grid.Children.Add(buttonPanel); dialog.Content = grid; - // Focus the text box when loaded + // Focus the text box and trigger initial validation when loaded dialog.Loaded += (s, args) => { textBox.Focus(); }; if (dialog.ShowDialog() == true)