Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -738,9 +738,17 @@ When a session's folder is a git repo, its terminal toolbar shows a chip β€” `
(branch, peach dirty dot, blue ahead/behind) β€” just left of β–Ά. Clicking it opens a menu:
a header line (`main β†’ origin/main Β· ↑2 ↓5 Β· 3 changed`), **Fetch**, **Pull (fast-forward
only)**, **Status…**, **Switch branch β–Έ** (local branches, loaded when the submenu opens),
**Copy branch name**, and **Open in VS Code** when `code.cmd` is on PATH. The UI is in
`MainWindow.Git.cs`; git calls are in `GitService`. Hidden for SSH sessions β€” an OSC 9001
branch is display-only, there is no local folder to run git in.
and **Copy branch name**. The UI is in `MainWindow.Git.cs`; git calls are in `GitService`.
Hidden for SSH sessions β€” an OSC 9001 branch is display-only, there is no local folder to run
git in.

**"Open in VS Code" is not in this menu** β€” it is not a git action and must work for a folder
that isn't a repo. It lives in `MainWindow.OpenIn.cs`: a `{ }` button beside πŸ“ / >_ on the
terminal toolbar, and an item after "Open PowerShell here" in the sidebar right-click menu.
Both appear only when VS Code was found (`code.cmd` on PATH β†’ its `Code.exe`, launched
directly with the folder as one argv element, never through `code.cmd`/`cmd.exe`) and the
session has a local folder. A configurable "Open with…" for other editors and installs is
tracked in #178.

**Deliberately read-mostly: no commit, push or merge.** A Claude session is usually editing
that same working tree, and a one-click history write from outside its conversation is both a
Expand Down
48 changes: 0 additions & 48 deletions src/CodeShellManager/MainWindow.Git.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ private sealed class GitPaneUi
/// </summary>
private readonly HashSet<string> _gitBusyRepos = new(StringComparer.OrdinalIgnoreCase);

private static Task<string?>? _vsCodeLookup;
private const int MaxChipBranchChars = 32;

private static readonly SolidColorBrush GitMuted = Frozen(0xa6, 0xad, 0xc8);
Expand All @@ -75,8 +74,6 @@ private static string GitRepoKey(SessionViewModel vm) =>
/// </summary>
private (WpfButton chip, Border panel) BuildGitControls(SessionViewModel vm)
{
_vsCodeLookup ??= Task.Run(FindVsCode);

var chipText = new TextBlock { VerticalAlignment = VerticalAlignment.Center, FontSize = 11 };
var chip = new WpfButton
{
Expand Down Expand Up @@ -279,28 +276,6 @@ private void ShowGitMenu(SessionViewModel vm, WpfButton anchor)
copy.Click += (_, _) => { try { Clipboard.SetText(vm.GitBranch ?? ""); } catch { } };
menu.Items.Add(copy);

if (_vsCodeLookup is { IsCompletedSuccessfully: true, Result: { } codeExe })
{
var code = new MenuItem { Header = "Open in VS Code" };
string folder = vm.WorkingFolder;
code.Click += (_, _) => _ = Task.Run(() =>
{
try
{
// Code.exe directly with the folder as one argv element β€” not code.cmd,
// which would put the folder through cmd.exe's parser.
var psi = new System.Diagnostics.ProcessStartInfo(codeExe) { UseShellExecute = false };
psi.ArgumentList.Add(folder);
System.Diagnostics.Process.Start(psi)?.Dispose();
}
catch (Exception ex)
{
Dispatcher.BeginInvoke(() => ToastHelper.Show("Couldn't open VS Code", ex.Message));
}
});
menu.Items.Add(code);
}

menu.IsOpen = true;
}

Expand Down Expand Up @@ -388,27 +363,4 @@ private void ShowGitPanel(SessionViewModel vm, string header, string body)
ui.PanelText.ScrollToHome();
ui.Panel.Visibility = Visibility.Visible;
}

/// <summary>
/// Finds VS Code's Code.exe via the <c>code.cmd</c> launcher on PATH (it lives in
/// <c>&lt;install&gt;\bin\</c>). File checks only β€” no process is spawned to find it.
/// </summary>
private static string? FindVsCode()
{
try
{
string path = Environment.GetEnvironmentVariable("PATH") ?? "";
foreach (string dir in path.Split(';', StringSplitOptions.RemoveEmptyEntries))
{
string d = dir.Trim().Trim('"');
if (d.Length == 0 || d.StartsWith(@"\\", StringComparison.Ordinal)) continue; // skip network dirs
string cmd = System.IO.Path.Combine(d, "code.cmd");
if (!System.IO.File.Exists(cmd)) continue;
string exe = System.IO.Path.GetFullPath(System.IO.Path.Combine(d, "..", "Code.exe"));
if (System.IO.File.Exists(exe)) return exe;
}
}
catch { }
return null;
}
}
119 changes: 119 additions & 0 deletions src/CodeShellManager/MainWindow.OpenIn.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using CodeShellManager.Services;
using CodeShellManager.ViewModels;
using WpfButton = System.Windows.Controls.Button;
using Brushes = System.Windows.Media.Brushes;
using Color = System.Windows.Media.Color;
using FontFamily = System.Windows.Media.FontFamily;

namespace CodeShellManager;

/// <summary>
/// "Open in VS Code" β€” on the terminal toolbar next to πŸ“ / >_, and in the sidebar right-click
/// menu next to "Open in Explorer" / "Open PowerShell here". It used to live in the git menu,
/// which was the wrong home: opening a folder in an editor has nothing to do with git, and
/// should work for a folder that isn't a repo. A configurable "Open with…" for other editors
/// (and VS Code installs this lookup misses) is tracked in #178.
/// </summary>
public partial class MainWindow
{
/// <summary>
/// Resolved once per process, off the UI thread (it walks PATH and touches the disk).
/// Null result = not found; the button and menu item then simply don't appear.
/// </summary>
private static Task<string?>? _vsCodeLookup;

private static Task<string?> VsCodeLookup() => _vsCodeLookup ??= Task.Run(FindVsCode);

/// <summary>Only a session with a local folder can be opened β€” not SSH.</summary>
private static bool CanOpenInEditor(SessionViewModel vm) =>
!vm.Session.IsRemote && !string.IsNullOrEmpty(vm.WorkingFolder);

/// <summary>
/// The toolbar button. Starts collapsed and appears once the lookup finds VS Code, so a
/// machine without it never shows a button that can't work.
/// </summary>
private WpfButton BuildVsCodeButton(SessionViewModel vm)
{
var btn = new WpfButton
{
Content = "{ }",
ToolTip = "Open folder in VS Code",
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
Foreground = new SolidColorBrush(Color.FromRgb(0xa6, 0xad, 0xc8)),
FontSize = 11,
FontFamily = new FontFamily("Consolas"),
Cursor = System.Windows.Input.Cursors.Hand,
Padding = new Thickness(5, 2, 5, 2),
Margin = new Thickness(0, 0, 4, 0),
Visibility = Visibility.Collapsed,
};
btn.Click += (_, _) => OpenInVsCode(vm.WorkingFolder);

if (CanOpenInEditor(vm))
{
_ = VsCodeLookup().ContinueWith(t =>
{
if (t.IsCompletedSuccessfully && t.Result != null)
Dispatcher.BeginInvoke(() => btn.Visibility = Visibility.Visible);
}, TaskScheduler.Default);
}
return btn;
}

/// <summary>True when VS Code has been found (the lookup has finished and succeeded).</summary>
private static bool IsVsCodeAvailable() =>
VsCodeLookup() is { IsCompletedSuccessfully: true, Result: not null };

private void OpenInVsCode(string folder)
{
_ = Task.Run(async () =>
{
try
{
string? codeExe = await VsCodeLookup().ConfigureAwait(false);
if (codeExe == null) return;
// Code.exe directly with the folder as one argv element β€” not code.cmd, which
// would put the folder through cmd.exe's parser (a folder named with & or %
// could then be read as a command).
var psi = new ProcessStartInfo(codeExe) { UseShellExecute = false };
psi.ArgumentList.Add(folder);
Process.Start(psi)?.Dispose();
}
catch (Exception ex)
{
_ = Dispatcher.BeginInvoke(() => ToastHelper.Show("Couldn't open VS Code", ex.Message));
}
});
}

/// <summary>
/// Finds VS Code's Code.exe via the <c>code.cmd</c> launcher on PATH (it lives in
/// <c>&lt;install&gt;\bin\</c>). File checks only β€” no process is spawned to find it.
/// Misses installs not on PATH, Insiders, portable copies and other editors; see #178.
/// </summary>
private static string? FindVsCode()
{
try
{
string path = Environment.GetEnvironmentVariable("PATH") ?? "";
foreach (string dir in path.Split(';', StringSplitOptions.RemoveEmptyEntries))
{
string d = dir.Trim().Trim('"');
if (d.Length == 0 || d.StartsWith(@"\\", StringComparison.Ordinal)) continue; // skip network dirs
string cmd = Path.Combine(d, "code.cmd");
if (!File.Exists(cmd)) continue;
string exe = Path.GetFullPath(Path.Combine(d, "..", "Code.exe"));
if (File.Exists(exe)) return exe;
}
}
catch { }
return null;
}
}
12 changes: 12 additions & 0 deletions src/CodeShellManager/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3464,6 +3464,13 @@ private System.Windows.Controls.ContextMenu BuildSessionContextMenu(SessionViewM
psItem.Click += (_, _) => LaunchPowerShellInFolder(vm.WorkingFolder, vm.GroupId);
menu.Items.Add(psItem);

if (IsVsCodeAvailable())
{
var codeItem = new System.Windows.Controls.MenuItem { Header = "Open in VS Code" };
codeItem.Click += (_, _) => OpenInVsCode(vm.WorkingFolder);
menu.Items.Add(codeItem);
}

if (vm.Session.IsWsl)
{
var wslConsoleItem = new System.Windows.Controls.MenuItem { Header = "Open WSL console here" };
Expand Down Expand Up @@ -4524,6 +4531,9 @@ private Border BuildTerminalWrapper(SessionViewModel vm, WebView2 webView)
};
explorerBtn.Click += (_, _) => vm.OpenInExplorerCommand.Execute(null);

// { } β€” open the folder in VS Code (MainWindow.OpenIn.cs). Hidden unless found.
var vsCodeBtn = BuildVsCodeButton(vm);

// PowerShell quick-launch button
var toolbarPsBtn = new WpfButton
{
Expand Down Expand Up @@ -4658,6 +4668,7 @@ private Border BuildTerminalWrapper(SessionViewModel vm, WebView2 webView)
DockPanel.SetDock(closeBtn, Dock.Right);
DockPanel.SetDock(termStatusDot, Dock.Right);
DockPanel.SetDock(explorerBtn, Dock.Right);
DockPanel.SetDock(vsCodeBtn, Dock.Right);
DockPanel.SetDock(toolbarPsBtn, Dock.Right);
DockPanel.SetDock(notesBtn, Dock.Right);
DockPanel.SetDock(sleepBtn, Dock.Right);
Expand All @@ -4673,6 +4684,7 @@ private Border BuildTerminalWrapper(SessionViewModel vm, WebView2 webView)
toolbarContent.Children.Add(closeBtn);
toolbarContent.Children.Add(termStatusDot);
toolbarContent.Children.Add(explorerBtn);
toolbarContent.Children.Add(vsCodeBtn);
toolbarContent.Children.Add(toolbarPsBtn);
toolbarContent.Children.Add(notesBtn);
toolbarContent.Children.Add(sleepBtn);
Expand Down