Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
99fcd3c
Improve file explorer search navigation
Jun 26, 2026
f980df9
Refresh Swift file length budget
Jun 26, 2026
3ad64ac
Merge origin/main into issue-5741
Jun 26, 2026
6324690
Address file preview navigation review feedback
Jun 27, 2026
a5a0b85
Clear stale navigation for dirty preview buffers
Jun 27, 2026
40ad797
Merge origin/main into issue-5741
Jun 27, 2026
4aa8871
Fix file preview navigation test enum calls
Jun 27, 2026
3c0b30e
Address file explorer preview review followups
Jun 27, 2026
f2323e5
Address file watcher and dirty navigation review
Jun 27, 2026
27ea5c0
Merge origin/main into issue 5741
Jun 29, 2026
e9e68c7
Merge remote-tracking branch 'origin/main' into issue-5741-hope-to-ha…
austinywang Jun 29, 2026
059d645
Coalesce file-explorer git status refreshes under watcher churn
austinywang Jun 29, 2026
40a7b28
Watch .git metadata so explorer badges refresh after commit/stage
austinywang Jun 30, 2026
06b9303
Scope git status coalescing to the explorer root/provider
austinywang Jun 30, 2026
c486277
Resolve .git gitdir pointer for worktree/submodule status watching
austinywang Jun 30, 2026
f9591a1
Validate resolved gitdir is a real Git metadata directory
austinywang Jun 30, 2026
a571ecc
Bound .git pointer file reads to avoid workspace-controlled hangs
austinywang Jun 30, 2026
f582bf3
Install git-state watcher when a repo appears after folder open
austinywang Jun 30, 2026
6b65424
Merge remote-tracking branch 'origin/main' into issue-5741-hope-to-ha…
austinywang Jul 2, 2026
666882c
Bundle file-preview open target into a single tuple parameter
austinywang Jul 2, 2026
8d3d56d
Harden .git pointer reads and document navigation/exclusion invariants
austinywang Jul 2, 2026
e1f5b2b
Merge remote-tracking branch 'origin/main' into issue-5741-hope-to-ha…
austinywang Jul 2, 2026
ecfd61e
Install git-state watcher via a .git-creation bootstrap watcher
austinywang Jul 2, 2026
be8a5a4
Clear pending text-position jump on a nil-line file open
austinywang Jul 2, 2026
550df11
Refresh git status when the git-state watcher installs post-open
austinywang Jul 2, 2026
486cad3
Make FIFO .git regression test fail cleanly instead of hanging
austinywang Jul 2, 2026
0b8b9e6
Reach git-state watcher internals via @testable, not DEBUG test seams
austinywang Jul 2, 2026
53c017c
Merge remote-tracking branch 'origin/main' into issue-5741-hope-to-ha…
austinywang Jul 2, 2026
e44630a
Merge remote-tracking branch 'origin/main' into issue-5741-hope-to-ha…
austinywang Jul 2, 2026
1eefac6
Merge remote-tracking branch 'origin/main' into issue-5741-hope-to-ha…
austinywang Jul 3, 2026
99f168c
Fix file preview text attach merge conflict
austinywang Jul 3, 2026
88a32b9
Merge remote-tracking branch 'origin/main' into issue-5741-hope-to-ha…
austinywang Jul 5, 2026
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
13 changes: 7 additions & 6 deletions .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
34499 CLI/cmux.swift
17954 Sources/AppDelegate.swift
16427 Sources/ContentView.swift
16431 Sources/ContentView.swift
14270 Sources/TerminalController.swift
13172 Sources/Workspace.swift
12348 cmuxTests/AppDelegateShortcutRoutingTests.swift
Expand All @@ -21,11 +21,11 @@
5915 cmuxTests/TerminalAndGhosttyTests.swift
5809 Sources/TextBoxInput.swift
5573 cmuxTests/BrowserConfigTests.swift
4587 Sources/Panels/FilePreviewPanel.swift
4483 Sources/cmuxApp.swift
4482 Sources/Panels/FilePreviewPanel.swift
4367 cmuxTests/BrowserPanelTests.swift
4283 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
4121 Sources/BrowserWindowPortal.swift
3964 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
3934 Sources/Feed/FeedPanelView.swift
3926 cmuxTests/TabManagerUnitTests.swift
3896 cmuxTests/WindowAndDragTests.swift
Expand Down Expand Up @@ -55,21 +55,22 @@
1732 cmuxTests/WorkspacePullRequestSidebarTests.swift
1687 cmuxTests/MarkdownPanelTests.swift
1680 cmuxUITests/BrowserPaneNavigationKeybindUITests.swift
1656 Sources/FileExplorerView.swift
1661 Sources/FileExplorerView.swift
1652 cmuxTests/CMUXCLIErrorOutputRegressionTests.swift
1649 cmuxTests/TerminalControllerSocketSecurityTests.swift
1640 Sources/FileExplorerStore.swift
1604 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift
1560 cmuxTests/TextBoxMentionCompletionTests.swift
1500 cmuxUITests/MultiWindowNotificationsUITests.swift
1499 cmuxTests/OmnibarAndToolsTests.swift
1481 cmuxTests/FileExplorerStoreTests.swift
1433 Sources/RemoteTmuxControlConnection.swift
1428 cmuxTests/AgentSessionAutoResumeSwiftTests.swift
1420 cmuxTests/AppDelegateIssue2907RoutingTests.swift
1384 cmuxTests/KeyboardShortcutSettingsFileStoreStartupTests.swift
1380 cmuxUITests/MenuKeyEquivalentRoutingUITests.swift
1363 Sources/CMUXInstalledExtensionSidebarHostView.swift
1360 Sources/Feed/FeedButtonStyleDebugWindowController.swift
1317 Sources/FileExplorerStore.swift
1295 cmuxTests/MobileHostAuthorizationTests.swift
1291 Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/Config/GhosttyConfig.swift
1290 cmuxTests/TextBoxSubmitActionTests.swift
Expand All @@ -79,7 +80,6 @@
1240 cmuxTests/SidebarOrderingTests.swift
1209 Packages/macOS/CmuxCommandPalette/Tests/CmuxCommandPaletteTests/CommandPaletteSearchEngineTests.swift
1205 Sources/RemoteTmuxController.swift
1204 cmuxTests/FileExplorerStoreTests.swift
1197 cmuxTests/CodexAppServerSessionTests.swift
1166 Sources/VaultAgentProcessScanner.swift
1147 cmuxTests/PiVaultAgentPersistenceTests.swift
Expand Down Expand Up @@ -215,6 +215,7 @@
547 Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/BackingUpPairedMacStore.swift
546 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AutomationSection.swift
544 cmuxUITests/DisplayResolutionRegressionUITests.swift
543 cmuxTests/FilePreviewTextEditorTextKitTests.swift
540 Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/MobileTerminalRenderGridReplay.swift
539 CLI/CMUXCLI+Themes.swift
539 CLI/CodexTeamsApprovalBridge.swift
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ import Foundation
/// after `FSEventStreamInvalidate`. No callback ever touches a freed instance,
/// so a separately retained context box is unnecessary.
final class FileSystemEventStream: @unchecked Sendable {
/// Upper bound on FSEvents exclusion paths. `FSEventStreamSetExclusionPaths`
/// accepts at most 8 entries and silently ignores the rest, so this stream and
/// its ``RecursivePathWatcher`` owner truncate to this single shared limit.
static let maximumExclusionPathCount = 8
private static let queueSpecificKey = DispatchSpecificKey<UInt8>()
private static let queue: DispatchQueue = {
let queue = DispatchQueue(label: "com.cmux.recursive-path-watcher", qos: .utility)
Expand Down Expand Up @@ -55,15 +59,23 @@ final class FileSystemEventStream: @unchecked Sendable {
///
/// - Parameters:
/// - paths: The files and directories to watch. Must be non-empty.
/// - excludedPaths: Descendant paths ignored by the stream. Use this for
/// high-churn subtrees that should not wake downstream reloads.
/// - latency: The FSEvents coalescing latency in seconds.
/// - onEvent: A non-blocking sink invoked on the shared queue for each
/// coalesced batch of filesystem events.
/// - Returns: `nil` if `paths` is empty or the underlying `FSEventStream`
/// could not be created or started.
init?(paths: [String], latency: TimeInterval, onEvent: @escaping @Sendable () -> Void) {
init?(
paths: [String],
excludedPaths: [String] = [],
latency: TimeInterval,
onEvent: @escaping @Sendable () -> Void
) {
guard !paths.isEmpty else { return nil }
self.onEvent = onEvent
self.stream = nil
let effectiveExcludedPaths = Array(excludedPaths.prefix(Self.maximumExclusionPathCount))

var context = FSEventStreamContext(
version: 0,
Expand All @@ -85,6 +97,12 @@ final class FileSystemEventStream: @unchecked Sendable {
return nil
}
self.stream = stream
if !effectiveExcludedPaths.isEmpty {
guard FSEventStreamSetExclusionPaths(stream, effectiveExcludedPaths as CFArray) else {
stop()
return nil
}
}
FSEventStreamSetDispatchQueue(stream, Self.queue)
guard FSEventStreamStart(stream) else {
stop()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ public actor RecursivePathWatcher {
/// recreating an equivalent watcher.
public nonisolated let watchedPaths: [String]

/// Descendant paths excluded by the underlying `FSEventStream`.
public nonisolated let excludedPaths: [String]

/// Stream of coalesced change events. Yields one element per throttle window
/// in which at least one filesystem event affected a watched path. Finishes
/// when ``stop()`` is called or the watcher is deallocated.
Expand All @@ -63,17 +66,22 @@ public actor RecursivePathWatcher {
///
/// - Parameters:
/// - paths: The files and directories to watch. Must be non-empty.
/// - excludedPaths: Descendant paths to suppress at the `FSEventStream`
/// level before they reach the watcher's throttle.
/// - clock: The clock driving the coalescing throttle. Defaults to
/// ``SystemFileWatchClock``.
/// - Returns: `nil` if `paths` is empty or the underlying `FSEventStream`
/// could not be created or started. On success the stream is already
/// listening.
public init?(
paths: [String],
excludedPaths: [String] = [],
clock: any FileWatchClock = SystemFileWatchClock()
) {
guard !paths.isEmpty else { return nil }
let effectiveExcludedPaths = Array(excludedPaths.prefix(FileSystemEventStream.maximumExclusionPathCount))
self.watchedPaths = paths
self.excludedPaths = effectiveExcludedPaths
self.clock = clock
let (events, eventsContinuation) = AsyncStream<Void>.makeStream()
self.events = events
Expand All @@ -86,6 +94,7 @@ public actor RecursivePathWatcher {
// actor mid-init.
guard let eventStream = FileSystemEventStream(
paths: paths,
excludedPaths: effectiveExcludedPaths,
latency: Self.streamLatency,
onEvent: { rawContinuation.yield(()) }
) else {
Expand All @@ -112,6 +121,7 @@ public actor RecursivePathWatcher {
/// with an injected clock and no real filesystem dependency.
init(testThrottleClock clock: any FileWatchClock) {
self.watchedPaths = []
self.excludedPaths = []
self.clock = clock
let (events, eventsContinuation) = AsyncStream<Void>.makeStream()
self.events = events
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ private actor GateClock: FileWatchClock {
await watcher?.stop()
}

@Test func exclusionPathsAreLimitedAndCanPointAtMissingSubtrees() async {
let directory = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-file-watch-exclusions-\(UUID().uuidString)", isDirectory: true)
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: directory) }
let exclusions = (0..<10).map {
directory.appendingPathComponent("ignored-\($0)", isDirectory: true).path
}

let watcher = RecursivePathWatcher(paths: [directory.path], excludedPaths: exclusions)
#expect(watcher != nil)
#expect(watcher?.excludedPaths == Array(exclusions.prefix(8)))
await watcher?.stop()
}

/// A burst of events inside one throttle window coalesces into a single
/// yield, a fresh event re-arms the throttle, and `stop()` finishes the
/// stream. This is the leading-edge behavior the watcher provides: react once
Expand Down
18 changes: 11 additions & 7 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1863,8 +1863,8 @@ struct ContentView: View {
onResumeSession: { entry in
resumeSession(entry: entry)
},
onOpenFilePreview: { filePath in
openFilePreviewFromSidebar(filePath: filePath)
onOpenFilePreview: { request in
openFilePreviewFromSidebar(filePath: request.path, lineNumber: request.lineNumber, columnNumber: request.columnNumber)
},
onOpenAsPane: { mode in
openRightSidebarToolPane(mode)
Expand Down Expand Up @@ -2264,7 +2264,7 @@ struct ContentView: View {
_ = workspace.openOrFocusRightSidebarToolSurface(inPane: paneId, mode: mode, focus: true)
}

private func openFilePreviewFromSidebar(filePath: String) {
private func openFilePreviewFromSidebar(filePath: String, lineNumber: Int?, columnNumber: Int?) {
guard let workspace = tabManager.selectedWorkspace else { return }
guard let paneId = workspace.bonsplitController.focusedPaneId ?? workspace.bonsplitController.allPaneIds.first else {
return
Expand All @@ -2276,9 +2276,11 @@ struct ContentView: View {
guard let workspace else { return }
do {
let localURL = try await fileExplorerStore.materializeRemoteFileForPreview(path: filePath)
_ = workspace.openFileSurfaces(
workspace.openFileSurfacesNavigatingTextPosition(
inPane: paneId,
filePaths: [localURL.path],
filePath: localURL.path,
lineNumber: lineNumber,
columnNumber: columnNumber,
focus: true,
reuseExisting: true
)
Expand All @@ -2288,9 +2290,11 @@ struct ContentView: View {
}
return
}
_ = workspace.openFileSurfaces(
workspace.openFileSurfacesNavigatingTextPosition(
inPane: paneId,
filePaths: [filePath],
filePath: filePath,
lineNumber: lineNumber,
columnNumber: columnNumber,
focus: true,
reuseExisting: true
)
Expand Down
13 changes: 10 additions & 3 deletions Sources/FileExplorerKeyboardShortcuts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@ import AppKit
import CmuxSettings
import CmuxWorkspaces

typealias FileExplorerPreviewOpenHandler = ((path: String, lineNumber: Int?, columnNumber: Int?)) -> Void
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// Perform the configured action for opening a local file from the file explorer.
@MainActor
func performFileExplorerFileOpen(path: String, onOpenFilePreview: (String) -> Void) {
func performFileExplorerFileOpen(
path: String,
lineNumber: Int? = nil,
columnNumber: Int? = nil,
onOpenFilePreview: FileExplorerPreviewOpenHandler
) {
let action = FileExplorerDoubleClickActionSettings.resolvedAction()
let hasPreferredEditor = PreferredEditorSettingsStore(defaults: .standard).resolvedCommand != nil
switch FileExplorerDoubleClickActionSettings.fileActivation(
action: action,
hasPreferredEditorCommand: hasPreferredEditor
) {
case .preview:
onOpenFilePreview(path)
onOpenFilePreview((path: path, lineNumber: lineNumber, columnNumber: columnNumber))
case .defaultEditor:
FileExternalOpenAction.openDefault(fileURL: URL(fileURLWithPath: path))
case .preferredEditor:
Expand Down Expand Up @@ -41,7 +48,7 @@ extension FileExplorerPanelView.Coordinator {
}

guard store.provider is LocalFileExplorerProvider else {
onOpenFilePreview(node.path)
onOpenFilePreview((path: node.path, lineNumber: nil, columnNumber: nil))
return
}
performFileExplorerFileOpen(path: node.path, onOpenFilePreview: onOpenFilePreview)
Expand Down
Loading
Loading