Revert "Merge pull request #239 from manaflow-ai/issue-151-ssh-remote-port-proxying"

This reverts commit 78e4bd32ba, reversing
changes made to cf75da8f8a.
This commit is contained in:
Lawrence Chen 2026-03-12 14:45:58 -07:00
parent 78e4bd32ba
commit f7cbbad434
60 changed files with 1250 additions and 17140 deletions

View file

@ -30,20 +30,11 @@ enum NewWorkspacePlacement: String, CaseIterable, Identifiable {
var description: String {
switch self {
case .top:
return String(
localized: "workspace.placement.top.description",
defaultValue: "Insert new workspaces at the top of the list."
)
return String(localized: "workspace.placement.top.description", defaultValue: "Insert new workspaces at the top of the list.")
case .afterCurrent:
return String(
localized: "workspace.placement.afterCurrent.description",
defaultValue: "Insert new workspaces directly after the active workspace."
)
return String(localized: "workspace.placement.afterCurrent.description", defaultValue: "Insert new workspaces directly after the active workspace.")
case .end:
return String(
localized: "workspace.placement.end.description",
defaultValue: "Append new workspaces to the bottom of the list."
)
return String(localized: "workspace.placement.end.description", defaultValue: "Append new workspaces to the bottom of the list.")
}
}
}
@ -81,9 +72,9 @@ enum SidebarActiveTabIndicatorStyle: String, CaseIterable, Identifiable {
var displayName: String {
switch self {
case .leftRail:
return "Left Rail"
return String(localized: "sidebar.indicator.leftRail", defaultValue: "Left Rail")
case .solidFill:
return "Solid Fill"
return String(localized: "sidebar.indicator.solidFill", defaultValue: "Solid Fill")
}
}
}
@ -741,25 +732,36 @@ class TabManager: ObservableObject {
}
var isFindVisible: Bool {
selectedTerminalPanel?.searchState != nil || focusedBrowserPanel?.searchState != nil
if selectedTerminalPanel?.searchState != nil { return true }
if focusedBrowserPanel?.searchState != nil { return true }
return false
}
var canUseSelectionForFind: Bool {
selectedTerminalPanel?.hasSelection() == true
if focusedBrowserPanel != nil { return false }
return selectedTerminalPanel?.hasSelection() == true
}
func startSearch() {
if let panel = selectedTerminalPanel {
if panel.searchState == nil {
panel.searchState = TerminalSurface.SearchState()
}
NSLog("Find: startSearch workspace=%@ panel=%@", panel.workspaceId.uuidString, panel.id.uuidString)
NotificationCenter.default.post(name: .ghosttySearchFocus, object: panel.surface)
_ = panel.performBindingAction("start_search")
if let browser = focusedBrowserPanel {
browser.startFind()
return
}
focusedBrowserPanel?.startFind()
guard let panel = selectedTerminalPanel else {
#if DEBUG
dlog("find.startSearch SKIPPED no selectedTerminalPanel")
#endif
return
}
let wasNil = panel.searchState == nil
if wasNil {
panel.searchState = TerminalSurface.SearchState()
}
#if DEBUG
dlog("find.startSearch workspace=\(panel.workspaceId.uuidString.prefix(5)) panel=\(panel.id.uuidString.prefix(5)) created=\(wasNil ? "yes" : "no(reuse)") firstResponder=\(String(describing: panel.surface.hostedView.window?.firstResponder))")
#endif
NotificationCenter.default.post(name: .ghosttySearchFocus, object: panel.surface)
_ = panel.performBindingAction("start_search")
}
func searchSelection() {
@ -767,27 +769,27 @@ class TabManager: ObservableObject {
if panel.searchState == nil {
panel.searchState = TerminalSurface.SearchState()
}
NSLog("Find: searchSelection workspace=%@ panel=%@", panel.workspaceId.uuidString, panel.id.uuidString)
#if DEBUG
dlog("find.searchSelection workspace=\(panel.workspaceId.uuidString.prefix(5)) panel=\(panel.id.uuidString.prefix(5))")
#endif
NotificationCenter.default.post(name: .ghosttySearchFocus, object: panel.surface)
_ = panel.performBindingAction("search_selection")
}
func findNext() {
if let panel = selectedTerminalPanel {
_ = panel.performBindingAction("search:next")
if let browser = focusedBrowserPanel, browser.searchState != nil {
browser.findNext()
return
}
focusedBrowserPanel?.findNext()
_ = selectedTerminalPanel?.performBindingAction("search:next")
}
func findPrevious() {
if let panel = selectedTerminalPanel {
_ = panel.performBindingAction("search:previous")
if let browser = focusedBrowserPanel, browser.searchState != nil {
browser.findPrevious()
return
}
focusedBrowserPanel?.findPrevious()
_ = selectedTerminalPanel?.performBindingAction("search:previous")
}
@discardableResult
@ -797,26 +799,27 @@ class TabManager: ObservableObject {
}
func hideFind() {
if let panel = selectedTerminalPanel {
panel.searchState = nil
if let browser = focusedBrowserPanel, browser.searchState != nil {
browser.hideFind()
return
}
focusedBrowserPanel?.hideFind()
#if DEBUG
dlog("find.hideFind panel=\(selectedTerminalPanel?.id.uuidString.prefix(5) ?? "nil")")
#endif
selectedTerminalPanel?.searchState = nil
}
@discardableResult
func addWorkspace(
workingDirectory overrideWorkingDirectory: String? = nil,
initialTerminalCommand: String? = nil,
initialTerminalEnvironment: [String: String] = [:],
select: Bool = true,
eagerLoadTerminal: Bool = false,
placementOverride: NewWorkspacePlacement? = nil,
autoWelcomeIfNeeded: Bool = true
) -> Workspace {
sentryBreadcrumb("workspace.create", data: ["tabCount": tabs.count + 1])
let workingDirectory = normalizedWorkingDirectory(overrideWorkingDirectory) ?? preferredWorkingDirectoryForNewTab()
let explicitWorkingDirectory = normalizedWorkingDirectory(overrideWorkingDirectory)
let workingDirectory = explicitWorkingDirectory ?? preferredWorkingDirectoryForNewTab()
let inheritedConfig = inheritedTerminalConfigForNewWorkspace()
let ordinal = Self.nextPortOrdinal
Self.nextPortOrdinal += 1
@ -824,9 +827,7 @@ class TabManager: ObservableObject {
title: "Terminal \(tabs.count + 1)",
workingDirectory: workingDirectory,
portOrdinal: ordinal,
configTemplate: inheritedConfig,
initialTerminalCommand: initialTerminalCommand,
initialTerminalEnvironment: initialTerminalEnvironment
configTemplate: inheritedConfig
)
wireClosedBrowserTracking(for: newWorkspace)
let insertIndex = newTabInsertIndex(placementOverride: placementOverride)
@ -835,8 +836,17 @@ class TabManager: ObservableObject {
} else {
tabs.append(newWorkspace)
}
if let explicitWorkingDirectory,
let terminalPanel = newWorkspace.focusedTerminalPanel {
scheduleInitialWorkspaceGitMetadataRefresh(
workspaceId: newWorkspace.id,
panelId: terminalPanel.id,
directory: explicitWorkingDirectory
)
}
if eagerLoadTerminal {
newWorkspace.focusedTerminalPanel?.surface.requestBackgroundSurfaceStartIfNeeded()
requestBackgroundWorkspaceLoad(for: newWorkspace.id)
newWorkspace.requestBackgroundTerminalSurfaceStartIfNeeded()
}
if select {
selectedTabId = newWorkspace.id
@ -1152,6 +1162,16 @@ class TabManager: ObservableObject {
tabs.insert(tab, at: insertIndex)
}
func moveTabToTopForNotification(_ tabId: UUID) {
guard let index = tabs.firstIndex(where: { $0.id == tabId }) else { return }
let pinnedCount = tabs.filter { $0.isPinned }.count
guard index != pinnedCount else { return }
let tab = tabs[index]
guard !tab.isPinned else { return }
tabs.remove(at: index)
tabs.insert(tab, at: pinnedCount)
}
func moveTabsToTop(_ tabIds: Set<UUID>) {
guard !tabIds.isEmpty else { return }
let selectedTabs = tabs.filter { tabIds.contains($0.id) }
@ -1164,16 +1184,6 @@ class TabManager: ObservableObject {
tabs = selectedPinned + remainingPinned + selectedUnpinned + remainingUnpinned
}
func moveTabToTopForNotification(_ tabId: UUID) {
guard let index = tabs.firstIndex(where: { $0.id == tabId }) else { return }
let pinnedCount = tabs.filter { $0.isPinned }.count
guard index != pinnedCount else { return }
let tab = tabs[index]
guard !tab.isPinned else { return }
tabs.remove(at: index)
tabs.insert(tab, at: pinnedCount)
}
@discardableResult
func reorderWorkspace(tabId: UUID, toIndex targetIndex: Int) -> Bool {
guard let currentIndex = tabs.firstIndex(where: { $0.id == tabId }) else { return false }
@ -1259,23 +1269,22 @@ class TabManager: ObservableObject {
func closeWorkspace(_ workspace: Workspace) {
guard tabs.count > 1 else { return }
guard let index = tabs.firstIndex(where: { $0.id == workspace.id }) else { return }
sentryBreadcrumb("workspace.close", data: ["tabCount": tabs.count - 1])
clearInitialWorkspaceGitProbe(workspaceId: workspace.id)
AppDelegate.shared?.notificationStore?.clearNotifications(forTabId: workspace.id)
workspace.teardownAllPanels()
workspace.teardownRemoteConnection()
unwireClosedBrowserTracking(for: workspace)
workspace.teardownAllPanels()
if let index = tabs.firstIndex(where: { $0.id == workspace.id }) {
tabs.remove(at: index)
tabs.remove(at: index)
if selectedTabId == workspace.id {
// Keep the "focused index" stable when possible:
// - If we closed workspace i and there is still a workspace at index i, focus it (the one that moved up).
// - Otherwise (we closed the last workspace), focus the new last workspace (i-1).
let newIndex = min(index, max(0, tabs.count - 1))
selectedTabId = tabs[newIndex].id
}
if selectedTabId == workspace.id {
// Keep the "focused index" stable when possible:
// - If we closed workspace i and there is still a workspace at index i, focus it (the one that moved up).
// - Otherwise (we closed the last workspace), focus the new last workspace (i-1).
let newIndex = min(index, max(0, tabs.count - 1))
selectedTabId = tabs[newIndex].id
}
}
@ -1284,6 +1293,7 @@ class TabManager: ObservableObject {
@discardableResult
func detachWorkspace(tabId: UUID) -> Workspace? {
guard let index = tabs.firstIndex(where: { $0.id == tabId }) else { return nil }
clearInitialWorkspaceGitProbe(workspaceId: tabId)
let removed = tabs.remove(at: index)
unwireClosedBrowserTracking(for: removed)
@ -1345,9 +1355,13 @@ class TabManager: ObservableObject {
let count = plan.panelIds.count
let titleLines = plan.titles.map { "\($0)" }.joined(separator: "\n")
let message = "This is about to close \(count) tab\(count == 1 ? "" : "s") in this pane:\n\(titleLines)"
let message = if count == 1 {
String(localized: "dialog.closeOtherTabs.message.one", defaultValue: "This will close 1 tab in this pane:\n\(titleLines)")
} else {
String(localized: "dialog.closeOtherTabs.message.other", defaultValue: "This will close \(count) tabs in this pane:\n\(titleLines)")
}
guard confirmClose(
title: "Close other tabs?",
title: String(localized: "dialog.closeOtherTabs.title", defaultValue: "Close other tabs?"),
message: message,
acceptCmdD: false
) else { return }
@ -1387,8 +1401,8 @@ class TabManager: ObservableObject {
alert.messageText = title
alert.informativeText = message
alert.alertStyle = .warning
alert.addButton(withTitle: "Close")
alert.addButton(withTitle: "Cancel")
alert.addButton(withTitle: String(localized: "common.close", defaultValue: "Close"))
alert.addButton(withTitle: String(localized: "common.cancel", defaultValue: "Cancel"))
// macOS convention: Cmd+D = confirm destructive close (e.g. "Don't Save").
// We only opt into this for the "close last workspace => close window" path to avoid
@ -1449,15 +1463,15 @@ class TabManager: ObservableObject {
if let collapsed, !collapsed.isEmpty {
return collapsed
}
return "Untitled Tab"
return String(localized: "tab.untitled", defaultValue: "Untitled Tab")
}
private func closeWorkspaceIfRunningProcess(_ workspace: Workspace) {
let willCloseWindow = tabs.count <= 1
if workspaceNeedsConfirmClose(workspace),
!confirmClose(
title: "Close workspace?",
message: "This will close the workspace and all of its panels.",
title: String(localized: "dialog.closeWorkspace.title", defaultValue: "Close workspace?"),
message: String(localized: "dialog.closeWorkspace.message", defaultValue: "This will close the workspace and all of its panels."),
acceptCmdD: willCloseWindow
) {
return
@ -1498,8 +1512,8 @@ class TabManager: ObservableObject {
let needsConfirm = workspaceNeedsConfirmClose(tab)
if needsConfirm {
let message = willCloseWindow
? "This will close the last tab and close the window."
: "This will close the last tab and close its workspace."
? String(localized: "dialog.closeLastTabWindow.message", defaultValue: "This will close the last tab and close the window.")
: String(localized: "dialog.closeLastTabWorkspace.message", defaultValue: "This will close the last tab and close its workspace.")
#if DEBUG
dlog(
"surface.close.shortcut.confirm tab=\(tab.id.uuidString.prefix(5)) " +
@ -1507,7 +1521,7 @@ class TabManager: ObservableObject {
)
#endif
guard confirmClose(
title: "Close tab?",
title: String(localized: "dialog.closeTab.title", defaultValue: "Close tab?"),
message: message,
acceptCmdD: willCloseWindow
) else {
@ -1539,8 +1553,8 @@ class TabManager: ObservableObject {
)
#endif
guard confirmClose(
title: "Close tab?",
message: "This will close the current tab.",
title: String(localized: "dialog.closeTab.title", defaultValue: "Close tab?"),
message: String(localized: "dialog.closeTab.message", defaultValue: "This will close the current tab."),
acceptCmdD: false
) else {
#if DEBUG
@ -1578,8 +1592,8 @@ class TabManager: ObservableObject {
if let terminalPanel = tab.terminalPanel(for: surfaceId),
terminalPanel.needsConfirmClose() {
guard confirmClose(
title: "Close tab?",
message: "This will close the current tab.",
title: String(localized: "dialog.closeTab.title", defaultValue: "Close tab?"),
message: String(localized: "dialog.closeTab.message", defaultValue: "This will close the current tab."),
acceptCmdD: false
) else { return }
}
@ -1846,28 +1860,32 @@ class TabManager: ObservableObject {
guard !shouldSuppressFlash else { return }
guard AppFocusState.isAppActive() else { return }
guard let panelId = focusedPanelId(for: tabId) else { return }
markPanelReadOnFocusIfActive(tabId: tabId, panelId: panelId)
_ = dismissNotificationIfActive(tabId: tabId, surfaceId: panelId, triggerFlash: true)
}
private func markPanelReadOnFocusIfActive(tabId: UUID, panelId: UUID) {
guard selectedTabId == tabId else { return }
guard !suppressFocusFlash else { return }
guard AppFocusState.isAppActive() else { return }
guard let notificationStore = AppDelegate.shared?.notificationStore else { return }
guard notificationStore.hasUnreadNotification(forTabId: tabId, surfaceId: panelId) else { return }
if let tab = tabs.first(where: { $0.id == tabId }) {
tab.triggerNotificationFocusFlash(panelId: panelId, requiresSplit: false, shouldFocus: false)
}
notificationStore.markRead(forTabId: tabId, surfaceId: panelId)
_ = dismissNotificationIfActive(tabId: tabId, surfaceId: panelId, triggerFlash: true)
}
@discardableResult
func dismissNotificationOnDirectInteraction(tabId: UUID, surfaceId: UUID?) -> Bool {
dismissNotificationIfActive(tabId: tabId, surfaceId: surfaceId, triggerFlash: true)
}
@discardableResult
private func dismissNotificationIfActive(
tabId: UUID,
surfaceId: UUID?,
triggerFlash: Bool
) -> Bool {
guard selectedTabId == tabId else { return false }
guard AppFocusState.isAppActive() else { return false }
guard let notificationStore = AppDelegate.shared?.notificationStore else { return false }
guard notificationStore.hasUnreadNotification(forTabId: tabId, surfaceId: surfaceId) else { return false }
if let panelId = surfaceId,
if triggerFlash,
let panelId = surfaceId,
let tab = tabs.first(where: { $0.id == tabId }) {
tab.triggerNotificationFocusFlash(panelId: panelId, requiresSplit: false, shouldFocus: false)
}
@ -2166,9 +2184,24 @@ class TabManager: ObservableObject {
guard let selectedTabId,
let tab = tabs.first(where: { $0.id == selectedTabId }),
let focusedPanelId = tab.focusedPanelId else { return }
#if DEBUG
let directionLabel = direction.debugLabel
dlog(
"split.create.request kind=terminal dir=\(directionLabel) " +
"tab=\(selectedTabId.uuidString.prefix(5)) panel=\(focusedPanelId.uuidString.prefix(5)) " +
"panels=\(tab.panels.count) panes=\(tab.bonsplitController.allPaneIds.count)"
)
#endif
tab.clearSplitZoom()
sentryBreadcrumb("split.create", data: ["direction": String(describing: direction)])
_ = newSplit(tabId: selectedTabId, surfaceId: focusedPanelId, direction: direction)
let createdPanelId = newSplit(tabId: selectedTabId, surfaceId: focusedPanelId, direction: direction)
#if DEBUG
dlog(
"split.create.result kind=terminal dir=\(directionLabel) " +
"created=\(createdPanelId?.uuidString.prefix(5) ?? "nil") " +
"panels=\(tab.panels.count) panes=\(tab.bonsplitController.allPaneIds.count)"
)
#endif
}
/// Create a new browser split from the currently focused panel.
@ -2177,14 +2210,30 @@ class TabManager: ObservableObject {
guard let selectedTabId,
let tab = tabs.first(where: { $0.id == selectedTabId }),
let focusedPanelId = tab.focusedPanelId else { return nil }
#if DEBUG
let directionLabel = direction.debugLabel
dlog(
"split.create.request kind=browser dir=\(directionLabel) " +
"tab=\(selectedTabId.uuidString.prefix(5)) panel=\(focusedPanelId.uuidString.prefix(5)) " +
"panels=\(tab.panels.count) panes=\(tab.bonsplitController.allPaneIds.count)"
)
#endif
tab.clearSplitZoom()
return newBrowserSplit(
let createdPanelId = newBrowserSplit(
tabId: selectedTabId,
fromPanelId: focusedPanelId,
orientation: direction.orientation,
insertFirst: direction.insertFirst,
url: url
)
#if DEBUG
dlog(
"split.create.result kind=browser dir=\(directionLabel) " +
"created=\(createdPanelId?.uuidString.prefix(5) ?? "nil") " +
"panels=\(tab.panels.count) panes=\(tab.bonsplitController.allPaneIds.count)"
)
#endif
return createdPanelId
}
/// Refresh Bonsplit right-side action button tooltips for all workspaces.
@ -2285,12 +2334,21 @@ class TabManager: ObservableObject {
/// Returns the new panel's ID (which is also the surface ID for terminals)
func newSplit(tabId: UUID, surfaceId: UUID, direction: SplitDirection, focus: Bool = true) -> UUID? {
guard let tab = tabs.first(where: { $0.id == tabId }) else { return nil }
return tab.newTerminalSplit(
let createdPanel = tab.newTerminalSplit(
from: surfaceId,
orientation: direction.orientation,
insertFirst: direction.insertFirst,
focus: focus
)?.id
#if DEBUG
let directionLabel = direction.debugLabel
dlog(
"split.newSurface result dir=\(directionLabel) " +
"tab=\(tabId.uuidString.prefix(5)) source=\(surfaceId.uuidString.prefix(5)) " +
"created=\(createdPanel?.uuidString.prefix(5) ?? "nil") focus=\(focus ? 1 : 0)"
)
#endif
return createdPanel
}
/// Move focus in the specified direction
@ -2891,7 +2949,7 @@ class TabManager: ObservableObject {
continue
}
terminal.hostedView.reconcileGeometryNow()
terminal.surface.forceRefresh()
terminal.surface.forceRefresh(reason: "tabManager.reconcileVisibleTerminalGeometry")
}
}
@ -3869,6 +3927,15 @@ enum SplitDirection {
var insertFirst: Bool {
self == .left || self == .up
}
var debugLabel: String {
switch self {
case .left: return "left"
case .right: return "right"
case .up: return "up"
case .down: return "down"
}
}
}
/// Resize direction for backwards compatibility