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
16 changes: 12 additions & 4 deletions GhosttyTabs.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
1A1B2C3D4E5F607180000003 /* WorkspacePromptSubmitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A1B2C3D4E5F607180000004 /* WorkspacePromptSubmitTests.swift */; };
1F14445B9627DE9D3AF4FD2E /* BrowserPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C7B1B978620BE162CC057E /* BrowserPanelTests.swift */; };
2BB56A710BB1FC50367E5BCF /* TabManagerSessionSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10D684CFFB8CDEF89CE2D9E1 /* TabManagerSessionSnapshotTests.swift */; };
C2B6A97D1F2E4C71A8B9D001 /* BrowserOmnibarPerformanceSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B6A97D1F2E4C71A8B9D002 /* BrowserOmnibarPerformanceSupportTests.swift */; };
3023A1003023A1003023A100 /* ConfigSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3023B1003023B1003023B100 /* ConfigSource.swift */; };
3023A1013023A1013023A101 /* ConfigSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3023B1013023B1013023B101 /* ConfigSettingsView.swift */; };
350DAC5EBD38642A3E81471A /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 312DE7503B4658DD173121B8 /* AuthManager.swift */; };
Expand Down Expand Up @@ -139,6 +140,7 @@
D7AB00000000000000000007 /* BrowserPanel+MoveTabToNewWorkspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB00000000000000000008 /* BrowserPanel+MoveTabToNewWorkspace.swift */; };
A5001403 /* TerminalPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001413 /* TerminalPanelView.swift */; };
A5001404 /* BrowserPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001414 /* BrowserPanelView.swift */; };
B0A500000000000000000001 /* BrowserOmnibarPerformanceSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0A500000000000000000002 /* BrowserOmnibarPerformanceSupport.swift */; };
A5001405 /* PanelContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001415 /* PanelContentView.swift */; };
A5001406 /* Workspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001416 /* Workspace.swift */; };
C0DE32470000000000000003 /* WorkspaceSurfaceIdentifierClipboardText.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE32470000000000000004 /* WorkspaceSurfaceIdentifierClipboardText.swift */; };
Expand Down Expand Up @@ -479,6 +481,7 @@
D7AB00000000000000000008 /* BrowserPanel+MoveTabToNewWorkspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Panels/BrowserPanel+MoveTabToNewWorkspace.swift"; sourceTree = "<group>"; };
A5001413 /* TerminalPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/TerminalPanelView.swift; sourceTree = "<group>"; };
A5001414 /* BrowserPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserPanelView.swift; sourceTree = "<group>"; };
B0A500000000000000000002 /* BrowserOmnibarPerformanceSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserOmnibarPerformanceSupport.swift; sourceTree = "<group>"; };
A5001415 /* PanelContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/PanelContentView.swift; sourceTree = "<group>"; };
A5001416 /* Workspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Workspace.swift; sourceTree = "<group>"; };
D7AB00000000000000000014 /* TabManager+DetachedWorkspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+DetachedWorkspace.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -530,6 +533,7 @@
A5008382 /* CommandPaletteSearchEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteSearchEngineTests.swift; sourceTree = "<group>"; };
A500RG00 /* ReactGrab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/ReactGrab.swift; sourceTree = "<group>"; };
A5F10010A1B2C3D4E5F60719 /* KeyboardShortcutSettingsFileStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardShortcutSettingsFileStore.swift; sourceTree = "<group>"; };
C2B6A97D1F2E4C71A8B9D002 /* BrowserOmnibarPerformanceSupportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserOmnibarPerformanceSupportTests.swift; sourceTree = "<group>"; };
C0DEF0B10000000000000002 /* JSONCParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONCParser.swift; sourceTree = "<group>"; };
AA1B2C3D4E5F60719 /* BonsplitTabDragUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BonsplitTabDragUITests.swift; sourceTree = "<group>"; };
AA1B2C3D4E5F60721 /* RightSidebarChromeHeightUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RightSidebarChromeHeightUITests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -858,6 +862,7 @@
A500RG00 /* ReactGrab.swift */,
A5001413 /* TerminalPanelView.swift */,
A5001414 /* BrowserPanelView.swift */,
B0A500000000000000000002 /* BrowserOmnibarPerformanceSupport.swift */,
A5007421 /* BrowserPopupWindowController.swift */,
A5007423 /* BrowserWebAuthnSupport.swift */,
A5001418 /* MarkdownPanel.swift */,
Expand Down Expand Up @@ -962,8 +967,9 @@
F9000001A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift */,
F1C1AA20B7E84D10A1C10001 /* InactivePaneFirstClickFocusTests.swift */,
FA000001A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift */,
A5008380 /* BrowserFindJavaScriptTests.swift */,
A5008382 /* CommandPaletteSearchEngineTests.swift */,
A5008380 /* BrowserFindJavaScriptTests.swift */,
C2B6A97D1F2E4C71A8B9D002 /* BrowserOmnibarPerformanceSupportTests.swift */,
A5008382 /* CommandPaletteSearchEngineTests.swift */,
A50019B3 /* SettingsSearchIndexTests.swift */,
970226F3C99D0D937CD00539 /* BrowserConfigTests.swift */,
58C7B1B978620BE162CC057E /* BrowserPanelTests.swift */,
Expand Down Expand Up @@ -1335,6 +1341,7 @@
A500RG01 /* ReactGrab.swift in Sources */,
A5001403 /* TerminalPanelView.swift in Sources */,
A5001404 /* BrowserPanelView.swift in Sources */,
B0A500000000000000000001 /* BrowserOmnibarPerformanceSupport.swift in Sources */,
A5007420 /* BrowserPopupWindowController.swift in Sources */,
A5007422 /* BrowserWebAuthnSupport.swift in Sources */,
A5001420 /* MarkdownPanel.swift in Sources */,
Expand Down Expand Up @@ -1498,8 +1505,9 @@
1521D55DC63D5E5FC4955E31 /* ShortcutAndCommandPaletteTests.swift in Sources */,
C3408A000000000000000003 /* RightSidebarCommandPaletteTests.swift in Sources */,
CB23911D7E131E8FBC9B82B6 /* SidebarOrderingTests.swift in Sources */,
4378399A7C0245EF8186F306 /* OmnibarAndToolsTests.swift in Sources */,
734F49D37E543DD01C2F4FEF /* NotificationAndMenuBarTests.swift in Sources */,
4378399A7C0245EF8186F306 /* OmnibarAndToolsTests.swift in Sources */,
C2B6A97D1F2E4C71A8B9D001 /* BrowserOmnibarPerformanceSupportTests.swift in Sources */,
734F49D37E543DD01C2F4FEF /* NotificationAndMenuBarTests.swift in Sources */,
B6BF3DC98DB1495E57900199 /* TabManagerUnitTests.swift in Sources */,
DCC935C5F55C1DCB33E25521 /* WorkspacePullRequestSidebarTests.swift in Sources */,
FEEDC0DEC0DEC0DEC0DE0001 /* FeedCoordinatorTests.swift in Sources */,
Expand Down
4 changes: 2 additions & 2 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4456,8 +4456,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent

func setCommandPaletteVisible(_ visible: Bool, for window: NSWindow) {
guard let windowId = mainWindowId(for: window) else { return }
let wasVisible = commandPaletteVisibilityByWindowId[windowId] ?? false
commandPaletteVisibilityByWindowId[windowId] = visible
let wasVisible = commandPaletteVisibilityByWindowId.updateValue(visible, forKey: windowId) ?? false
postCommandPaletteVisibilityDidChangeIfNeeded(wasVisible: wasVisible, visible: visible, window: window, windowId: windowId)
// Opening (false -> true) always resolves pending-open.
// Closing (true -> false) also clears stale pending state.
// Ignore repeated false updates so a stale sync cannot erase an in-flight open request.
Expand Down
251 changes: 251 additions & 0 deletions Sources/Panels/BrowserOmnibarPerformanceSupport.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import AppKit
import Foundation

struct BrowserOpenTabSuggestionSnapshot: Equatable {
let workspaceId: UUID
let panelId: UUID
let url: String
let title: String?
let lowercasedURL: String
let lowercasedTitle: String

init?(workspaceId: UUID, panelId: UUID, url: String?, title: String?) {
guard let normalizedURL = url?.trimmingCharacters(in: .whitespacesAndNewlines),
!normalizedURL.isEmpty else { return nil }
let normalizedTitle = title?.trimmingCharacters(in: .whitespacesAndNewlines)
self.workspaceId = workspaceId
self.panelId = panelId
self.url = normalizedURL
self.title = normalizedTitle?.isEmpty == false ? normalizedTitle : nil
self.lowercasedURL = normalizedURL.lowercased()
self.lowercasedTitle = self.title?.lowercased() ?? ""
}
}

struct OmnibarOpenTabMatch: Equatable {
let tabId: UUID
let panelId: UUID
let url: String
let title: String?
let isKnownOpenTab: Bool

init(tabId: UUID, panelId: UUID, url: String, title: String?, isKnownOpenTab: Bool = true) {
self.tabId = tabId
self.panelId = panelId
self.url = url
self.title = title
self.isKnownOpenTab = isKnownOpenTab
}
}

extension BrowserHistoryStore {
static func uiTestSeedEntriesIfConfigured() -> [Entry]? {
let env = ProcessInfo.processInfo.environment
guard env["CMUX_UI_TEST_MODE"] == "1",
let rawSeed = env["CMUX_UI_TEST_BROWSER_HISTORY_JSON"],
let data = rawSeed.data(using: .utf8) else {
return nil
}
return try? JSONDecoder().decode([Entry].self, from: data)
}
}

final class BrowserOpenTabSuggestionIndex {
private var suggestionsByPanelId: [UUID: BrowserOpenTabSuggestionSnapshot] = [:]
private var suggestionOrder: [UUID] = []
private var isSeeded = false

func upsert(_ snapshot: BrowserOpenTabSuggestionSnapshot) {
let existing = suggestionsByPanelId[snapshot.panelId]
guard existing != snapshot else { return }
suggestionsByPanelId[snapshot.panelId] = snapshot
if existing == nil {
suggestionOrder.append(snapshot.panelId)
}
}

func remove(panelId: UUID) {
guard suggestionsByPanelId.removeValue(forKey: panelId) != nil else { return }
suggestionOrder.removeAll { $0 == panelId }
}

func matching(
for query: String,
currentWorkspaceId: UUID,
currentPanelId: UUID,
currentPanelSnapshot: BrowserOpenTabSuggestionSnapshot?,
includeCurrentPanelForSingleCharacterQuery: Bool,
limit: Int,
seedSnapshots: () -> [BrowserOpenTabSuggestionSnapshot]
) -> [OmnibarOpenTabMatch] {
let trimmedQuery = query.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedQuery.isEmpty, limit > 0 else { return [] }

seedIfNeeded(seedSnapshots)

let loweredQuery = trimmedQuery.lowercased()
let singleCharacterQuery = omnibarSingleCharacterQuery(for: trimmedQuery)
var matches: [OmnibarOpenTabMatch] = []
matches.reserveCapacity(min(limit, suggestionOrder.count + 1))
var seenKeys = Set<String>()

func snapshotMatches(_ snapshot: BrowserOpenTabSuggestionSnapshot) -> Bool {
if let singleCharacterQuery {
return omnibarHasSingleCharacterPrefixMatch(
query: singleCharacterQuery,
url: snapshot.url,
title: snapshot.title
)
}
return snapshot.lowercasedURL.contains(loweredQuery) ||
snapshot.lowercasedTitle.contains(loweredQuery)
}

func append(_ snapshot: BrowserOpenTabSuggestionSnapshot, isKnownOpenTab: Bool) {
guard matches.count < limit else { return }
let key = [
snapshot.workspaceId.uuidString.lowercased(),
snapshot.panelId.uuidString.lowercased(),
snapshot.lowercasedURL,
].joined(separator: "|")
guard snapshotMatches(snapshot) else { return }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
guard seenKeys.insert(key).inserted else { return }
matches.append(
OmnibarOpenTabMatch(
tabId: snapshot.workspaceId,
panelId: snapshot.panelId,
url: snapshot.url,
title: snapshot.title,
isKnownOpenTab: isKnownOpenTab
)
)
}

if includeCurrentPanelForSingleCharacterQuery, let currentPanelSnapshot {
append(currentPanelSnapshot, isKnownOpenTab: true)
}

for panelId in suggestionOrder {
guard matches.count < limit else { break }
guard let snapshot = suggestionsByPanelId[panelId] else { continue }
let isCurrentPanel = snapshot.workspaceId == currentWorkspaceId && snapshot.panelId == currentPanelId
if isCurrentPanel && !includeCurrentPanelForSingleCharacterQuery {
continue
}
append(snapshot, isKnownOpenTab: true)
}

return matches
}

private func seedIfNeeded(_ snapshots: () -> [BrowserOpenTabSuggestionSnapshot]) {
guard !isSeeded else { return }
isSeeded = true
suggestionsByPanelId.removeAll(keepingCapacity: true)
suggestionOrder.removeAll(keepingCapacity: true)
for snapshot in snapshots() {
suggestionsByPanelId[snapshot.panelId] = snapshot
suggestionOrder.append(snapshot.panelId)
}
}
}

private var browserOpenTabSuggestionIndexesByManagerId: [ObjectIdentifier: BrowserOpenTabSuggestionIndex] = [:]

extension TabManager {
private var browserOpenTabSuggestionIndex: BrowserOpenTabSuggestionIndex {
let managerId = ObjectIdentifier(self)
if let index = browserOpenTabSuggestionIndexesByManagerId[managerId] {
return index
}
let index = BrowserOpenTabSuggestionIndex()
browserOpenTabSuggestionIndexesByManagerId[managerId] = index
return index
Comment on lines +153 to +163

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 | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Swift, is ObjectIdentifier guaranteed unique only for an object's lifetime, and can the same identifier be reused after the object is deallocated?

💡 Result:

Yes, ObjectIdentifier is guaranteed to be unique only during the lifetime of the object. The same identifier can be reused after the object is deallocated, as it is based on the object's memory address, which can be recycled by the runtime.

Citations:


🏁 Script executed:

find . -type f -name "BrowserOmnibarPerformanceSupport.swift" | head -5

Repository: manaflow-ai/cmux

Length of output: 116


🏁 Script executed:

wc -l Sources/Panels/BrowserOmnibarPerformanceSupport.swift

Repository: manaflow-ai/cmux

Length of output: 118


🏁 Script executed:

cat -n Sources/Panels/BrowserOmnibarPerformanceSupport.swift

Repository: manaflow-ai/cmux

Length of output: 10563


🏁 Script executed:

find . -type f -name "*.swift" | xargs rg "class TabManager" -A 5 | head -30

Repository: manaflow-ai/cmux

Length of output: 2540


🏁 Script executed:

rg "browserOpenTabSuggestionIndexesByManagerId" --type swift

Repository: manaflow-ai/cmux

Length of output: 483


🏁 Script executed:

find . -type f -name "*.swift" | xargs rg "^class TabManager" -A 30 | head -50

Repository: manaflow-ai/cmux

Length of output: 1790


🏁 Script executed:

rg "deinit" Sources -A 5 | grep -A 5 "TabManager\|browserOpenTab" | head -30

Repository: manaflow-ai/cmux

Length of output: 679


🏁 Script executed:

sed -n '1,200p' Sources/TabManager.swift | tail -100

Repository: manaflow-ai/cmux

Length of output: 3163


🏁 Script executed:

rg "deinit" Sources/TabManager.swift -A 20

Repository: manaflow-ai/cmux

Length of output: 1010


🏁 Script executed:

rg "browserOpenTabSuggestionIndexesByManagerId\[" Sources -B 3 -A 3

Repository: manaflow-ai/cmux

Length of output: 1067


🏁 Script executed:

rg "removeValue\|removeAll\|removeValue.*browserOpenTab" Sources -B 5 -A 5

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

rg "ObjectIdentifier" Sources -B 2 -A 2

Repository: manaflow-ai/cmux

Length of output: 50372


🏁 Script executed:

grep -r "browserOpenTabSuggestion" Sources --include="*.swift" | grep -v "BrowserOmnibarPerformanceSupport"

Repository: manaflow-ai/cmux

Length of output: 42


Add cleanup in TabManager.deinit to remove closed manager's suggestion index.

The global browserOpenTabSuggestionIndexesByManagerId dictionary keeps BrowserOpenTabSuggestionIndex entries for closed TabManager instances indefinitely—this is a memory leak. Additionally, since ObjectIdentifier is based on object memory address and can be reused after deallocation, a future TabManager may acquire the same identifier as a deallocated one and receive stale suggestion snapshots.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Panels/BrowserOmnibarPerformanceSupport.swift` around lines 143 -
153, Add cleanup in TabManager.deinit to remove its entry from the global
browserOpenTabSuggestionIndexesByManagerId map: in the TabManager type implement
a deinit that computes let managerId = ObjectIdentifier(self) and calls
browserOpenTabSuggestionIndexesByManagerId.removeValue(forKey: managerId) so
closed managers don't leak BrowserOpenTabSuggestionIndex instances or yield
stale snapshots; place the deinit alongside the browserOpenTabSuggestionIndex
computed property to ensure symmetry.

}

func upsertBrowserOpenTabSuggestion(_ snapshot: BrowserOpenTabSuggestionSnapshot) {
browserOpenTabSuggestionIndex.upsert(snapshot)
}

func removeBrowserOpenTabSuggestion(panelId: UUID) {
browserOpenTabSuggestionIndex.remove(panelId: panelId)
}

func matchingOpenBrowserTabSuggestions(
for query: String,
currentWorkspaceId: UUID,
currentPanelId: UUID,
currentPanelSnapshot: BrowserOpenTabSuggestionSnapshot?,
includeCurrentPanelForSingleCharacterQuery: Bool,
limit: Int
) -> [OmnibarOpenTabMatch] {
browserOpenTabSuggestionIndex.matching(
for: query,
currentWorkspaceId: currentWorkspaceId,
currentPanelId: currentPanelId,
currentPanelSnapshot: currentPanelSnapshot,
includeCurrentPanelForSingleCharacterQuery: includeCurrentPanelForSingleCharacterQuery,
limit: limit,
seedSnapshots: browserOpenTabSuggestionSeedSnapshots
)
}

private func browserOpenTabSuggestionSeedSnapshots() -> [BrowserOpenTabSuggestionSnapshot] {
tabs.flatMap { workspace in
workspace.panels.compactMap { _, panel in
guard let browserPanel = panel as? BrowserPanel else { return nil }
return BrowserOpenTabSuggestionSnapshot(
workspaceId: workspace.id,
panelId: browserPanel.id,
url: browserPanel.preferredURLStringForOmnibar(),
title: browserPanel.pageTitle
)
}
}
}
}

extension Workspace {
func publishBrowserOpenTabSuggestion(for browserPanel: BrowserPanel) {
guard let snapshot = BrowserOpenTabSuggestionSnapshot(
workspaceId: id,
panelId: browserPanel.id,
url: browserPanel.preferredURLStringForOmnibar(),
title: browserPanel.pageTitle
) else {
owningTabManager?.removeBrowserOpenTabSuggestion(panelId: browserPanel.id)
return
}
owningTabManager?.upsertBrowserOpenTabSuggestion(snapshot)
}

func removeBrowserOpenTabSuggestion(panelId: UUID) {
owningTabManager?.removeBrowserOpenTabSuggestion(panelId: panelId)
}

func removeBrowserOpenTabSuggestionIfNeeded(panel: (any Panel)?, panelId: UUID) {
guard panel is BrowserPanel else { return }
removeBrowserOpenTabSuggestion(panelId: panelId)
}
}

extension Notification.Name {
static let commandPaletteVisibilityDidChange = Notification.Name("cmux.commandPaletteVisibilityDidChange")
}

func postCommandPaletteVisibilityDidChangeIfNeeded(
wasVisible: Bool,
visible: Bool,
window: NSWindow,
windowId: UUID
) {
guard wasVisible != visible else { return }
NotificationCenter.default.post(
name: .commandPaletteVisibilityDidChange,
object: window,
userInfo: [
"windowId": windowId,
"visible": visible,
]
)
}
2 changes: 1 addition & 1 deletion Sources/Panels/BrowserPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1051,7 +1051,7 @@ final class BrowserHistoryStore: ObservableObject {

func loadIfNeeded() {
guard !didLoad else { return }
didLoad = true
didLoad = true; if let seededEntries = BrowserHistoryStore.uiTestSeedEntriesIfConfigured() { entries = seededEntries.sorted { $0.lastVisited > $1.lastVisited }; return }
guard let fileURL else { return }
migrateLegacyTaggedHistoryFileIfNeeded(to: fileURL)

Expand Down
Loading