Skip to content
Open
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
24 changes: 22 additions & 2 deletions GhosttyTabs.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,12 @@
F6000000A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6000001A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift */; };
F7000000A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */; };
F8000000A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8000001A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift */; };
/* End PBXBuildFile section */
A5003000 /* SidebarContentMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5003010 /* SidebarContentMode.swift */; };
A5003001 /* FileTreeNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5003011 /* FileTreeNode.swift */; };
A5003002 /* FileTreeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5003012 /* FileTreeModel.swift */; };
A5003003 /* FileTreeRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5003013 /* FileTreeRow.swift */; };
A5003004 /* FileTreeSidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5003014 /* FileTreeSidebar.swift */; };
/* End PBXBuildFile section */

/* Begin PBXCopyFilesBuildPhase section */
A5001020 /* Embed Frameworks */ = {
Expand Down Expand Up @@ -215,7 +220,12 @@
F6000001A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegateShortcutRoutingTests.swift; sourceTree = "<group>"; };
F7000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceContentViewVisibilityTests.swift; sourceTree = "<group>"; };
F8000001A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketControlPasswordStoreTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
A5003010 /* SidebarContentMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTree/SidebarContentMode.swift; sourceTree = "<group>"; };
A5003011 /* FileTreeNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTree/FileTreeNode.swift; sourceTree = "<group>"; };
A5003012 /* FileTreeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTree/FileTreeModel.swift; sourceTree = "<group>"; };
A5003013 /* FileTreeRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTree/FileTreeRow.swift; sourceTree = "<group>"; };
A5003014 /* FileTreeSidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTree/FileTreeSidebar.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
A5001030 /* Frameworks */ = {
Expand Down Expand Up @@ -322,6 +332,11 @@
children = (
A5001011 /* cmuxApp.swift */,
A5001012 /* ContentView.swift */,
A5003010 /* SidebarContentMode.swift */,
A5003011 /* FileTreeNode.swift */,
A5003012 /* FileTreeModel.swift */,
A5003013 /* FileTreeRow.swift */,
A5003014 /* FileTreeSidebar.swift */,
9AD52285508B1D6A9875E7B3 /* SidebarSelectionState.swift */,
B9000017A1B2C3D4E5F60719 /* WindowDragHandleView.swift */,
A50012F0 /* Backport.swift */,
Expand Down Expand Up @@ -561,6 +576,11 @@
files = (
A5001001 /* cmuxApp.swift in Sources */,
A5001002 /* ContentView.swift in Sources */,
A5003000 /* SidebarContentMode.swift in Sources */,
A5003001 /* FileTreeNode.swift in Sources */,
A5003002 /* FileTreeModel.swift in Sources */,
A5003003 /* FileTreeRow.swift in Sources */,
A5003004 /* FileTreeSidebar.swift in Sources */,
E62155868BB29FEB5DAAAF25 /* SidebarSelectionState.swift in Sources */,
B9000018A1B2C3D4E5F60719 /* WindowDragHandleView.swift in Sources */,
A50012F1 /* Backport.swift in Sources */,
Expand Down
15 changes: 15 additions & 0 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3310,6 +3310,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
tabManager = context.tabManager
sidebarState = context.sidebarState
sidebarSelectionState = context.sidebarSelectionState

TerminalController.shared.setActiveTabManager(context.tabManager)
}
#if DEBUG
Expand Down Expand Up @@ -3757,6 +3758,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
tabManager = context.tabManager
sidebarState = context.sidebarState
sidebarSelectionState = context.sidebarSelectionState

TerminalController.shared.setActiveTabManager(context.tabManager)
}

Expand Down Expand Up @@ -5205,6 +5207,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
return true
}

if matchShortcut(event: event, shortcut: KeyboardShortcutSettings.shortcut(for: .toggleFileTree)) {
let current = UserDefaults.standard.string(forKey: "sidebarContentMode") ?? SidebarContentMode.tabs.rawValue
if current == SidebarContentMode.fileTree.rawValue {
UserDefaults.standard.set(SidebarContentMode.tabs.rawValue, forKey: "sidebarContentMode")
} else {
UserDefaults.standard.set(SidebarContentMode.fileTree.rawValue, forKey: "sidebarContentMode")
if sidebarState?.isVisible == false {
sidebarState?.toggle()
}
}
return true
}

if matchShortcut(event: event, shortcut: KeyboardShortcutSettings.shortcut(for: .newTab)) {
#if DEBUG
dlog("shortcut.action name=newWorkspace \(debugShortcutRouteSnapshot(event: event))")
Expand Down
63 changes: 59 additions & 4 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1225,6 +1225,8 @@ struct ContentView: View {
@EnvironmentObject var notificationStore: TerminalNotificationStore
@EnvironmentObject var sidebarState: SidebarState
@EnvironmentObject var sidebarSelectionState: SidebarSelectionState
@AppStorage("sidebarContentMode") private var sidebarContentMode = SidebarContentMode.tabs.rawValue
@StateObject private var fileTreeModel = FileTreeModel()
@State private var sidebarWidth: CGFloat = 200
@State private var hoveredResizerHandles: Set<SidebarResizerHandle> = []
@State private var isResizerDragging = false
Expand Down Expand Up @@ -1773,14 +1775,57 @@ struct ContentView: View {
}
}

private var sidebarView: some View {
private var tabsSidebarContent: some View {
VerticalTabsSidebar(
updateViewModel: updateViewModel,
selection: $sidebarSelectionState.selection,
selectedTabIds: $selectedTabIds,
lastSidebarSelectionIndex: $lastSidebarSelectionIndex
)
}

private var fileTreeSidebarContent: some View {
Group {
if let workspace = tabManager.selectedTab {
FileTreeSidebar(
model: fileTreeModel,
workspace: workspace,
onComposePath: { path in
sendPathToFocusedTerminal(path)
}
)
} else {
Text("No workspace")
.font(.system(size: 12))
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}

private var sidebarView: some View {
VStack(spacing: 0) {
if sidebarContentMode == SidebarContentMode.fileTree.rawValue {
fileTreeSidebarContent
} else {
tabsSidebarContent
Comment on lines +1803 to +1811

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete shell escaping - missing glob chars (*, ?, [, ]), brace expansion ({, }), command substitution (`), tilde (~), hash (#), backslash (\), and control characters. A file named test*.txt or $(dangerous) would be sent unquoted and cause shell expansion/execution.

Safer approach: always wrap in single quotes:

Suggested change
}
}
private var sidebarView: some View {
VStack(spacing: 0) {
if sidebarContentMode == SidebarContentMode.fileTree.rawValue {
fileTreeSidebarContent
} else {
tabsSidebarContent
private func shellEscapePath(_ path: String) -> String {
// Always wrap in single quotes and escape internal single quotes
let escaped = path.replacingOccurrences(of: "'", with: "'\\''")
return "'\(escaped)'"
}

}
}
.frame(width: sidebarWidth)
.background(SidebarBackdrop().ignoresSafeArea())
}

private func sendPathToFocusedTerminal(_ path: String) {
guard let workspace = tabManager.selectedTab,
let terminalPanel = workspace.focusedTerminalPanel else { return }
// Shell-escape the path so spaces and special chars don't break commands
let escaped = shellEscapePath(path)
terminalPanel.sendText(escaped)
}
Comment on lines +1818 to +1824

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

鈿狅笍 Potential issue | 馃煛 Minor

馃З Analysis chain

馃弫 Script executed:

#!/bin/bash
# Verify delimiter logic in onComposePath call sites and sendPathToFocusedTerminal usage
rg -n -C5 'onComposePath|sendPathToFocusedTerminal|shellEscapePath' Sources --type swift | head -200

Repository: manaflow-ai/cmux

Length of output: 7141


馃弫 Script executed:

#!/bin/bash
# Find terminalPanel.sendText implementation and other call sites
rg -n 'func sendText|\.sendText\(' Sources --type swift -A2 | head -100

Repository: manaflow-ai/cmux

Length of output: 3789


馃弫 Script executed:

#!/bin/bash
# Read the complete sendText implementation in GhosttyTerminalView
sed -n '2100,2120p' Sources/GhosttyTerminalView.swift

Repository: manaflow-ai/cmux

Length of output: 748


Add trailing space to sendPathToFocusedTerminal to support multiple cmd-click path insertion.

When users cmd-click multiple file paths sequentially, each call to sendText() concatenates without spacing, resulting in invalid shell syntax like '/path/one''/path/two'. Append a space after the escaped path to ensure proper token separation:

private func sendPathToFocusedTerminal(_ path: String) {
    guard let workspace = tabManager.selectedTab,
          let terminalPanel = workspace.focusedTerminalPanel else { return }
    // Shell-escape the path so spaces and special chars don't break commands
    let escaped = shellEscapePath(path)
    terminalPanel.sendText(escaped + " ")
}
馃 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ContentView.swift` around lines 1818 - 1824, The
sendPathToFocusedTerminal function currently sends the escaped path without a
trailing space, causing concatenated tokens when cmd-clicking multiple paths;
update the code in sendPathToFocusedTerminal so that after calling
shellEscapePath(path) you append a single space to the string passed to
terminalPanel.sendText (i.e., send escaped + " ") so each inserted path is
separated; reference sendPathToFocusedTerminal, shellEscapePath, and
terminalPanel.sendText when making the change.


private func shellEscapePath(_ path: String) -> String {
let escaped = path.replacingOccurrences(of: "'", with: "'\\''")
return "'\(escaped)'"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment on lines +1826 to 1829

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent with existing shell escaping in GhosttyTerminalView.escapeDropForShell which uses backslash escaping for \\ ()[]{}<>\"'\!#$&;|*?\t`. File drop and file tree should use the same escaping strategy.

Extract escapeDropForShell to a shared utility or use the same character set and approach here.


/// Space at top of content area for the titlebar. This must be at least the actual titlebar
Expand Down Expand Up @@ -1868,6 +1913,17 @@ struct ContentView: View {
notificationStore: TerminalNotificationStore.shared,
viewModel: fullscreenControlsViewModel,
onToggleSidebar: { sidebarState.toggle() },
onToggleFileTree: {
let current = UserDefaults.standard.string(forKey: "sidebarContentMode") ?? SidebarContentMode.tabs.rawValue
if current == SidebarContentMode.fileTree.rawValue {
UserDefaults.standard.set(SidebarContentMode.tabs.rawValue, forKey: "sidebarContentMode")
} else {
UserDefaults.standard.set(SidebarContentMode.fileTree.rawValue, forKey: "sidebarContentMode")
if !sidebarState.isVisible {
sidebarState.toggle()
}
}
},
onToggleNotifications: { [fullscreenControlsViewModel] in
AppDelegate.shared?.toggleNotificationsPopover(
animated: true,
Expand Down Expand Up @@ -2493,7 +2549,7 @@ struct ContentView: View {
windowId: windowId,
tabManager: tabManager,
sidebarState: sidebarState,
sidebarSelectionState: sidebarSelectionState
sidebarSelectionState: sidebarSelectionState,
)
installFileDropOverlay(on: window, tabManager: tabManager)
}))
Expand Down Expand Up @@ -5665,7 +5721,6 @@ struct VerticalTabsSidebar: View {
}
.accessibilityIdentifier("Sidebar")
.ignoresSafeArea()
.background(SidebarBackdrop().ignoresSafeArea())
.background(
WindowAccessor { window in
commandKeyMonitor.setHostWindow(window)
Expand Down Expand Up @@ -8374,7 +8429,7 @@ enum SidebarSelection {
case notifications
}

private struct ClearScrollBackground: ViewModifier {
struct ClearScrollBackground: ViewModifier {
func body(content: Content) -> some View {
if #available(macOS 13.0, *) {
content
Expand Down
Loading