diff --git a/mac/CHANGELOG.md b/mac/CHANGELOG.md index 7a71342f..3e1f6265 100644 --- a/mac/CHANGELOG.md +++ b/mac/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [1.0.0-beta.6] - 2025-01-01 +## [1.0.0-beta.6] - 2025-07-02 ### ✨ New Features - **Sleep Prevention** - Mac now stays awake when VibeTunnel is running terminal sessions diff --git a/mac/VibeTunnel/Core/Services/WindowTracker.swift b/mac/VibeTunnel/Core/Services/WindowTracker.swift index 5b99ebe9..016088c4 100644 --- a/mac/VibeTunnel/Core/Services/WindowTracker.swift +++ b/mac/VibeTunnel/Core/Services/WindowTracker.swift @@ -138,7 +138,7 @@ final class WindowTracker { guard let windowList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] else { return [] } - + let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "WindowTracker") return windowList.compactMap { windowDict in @@ -149,9 +149,9 @@ final class WindowTracker { else { return nil } - + // Log suspicious window IDs for debugging - if windowID < 1000 && windowID == CGWindowID(ownerPID) { + if windowID < 1_000 && windowID == CGWindowID(ownerPID) { logger.warning("Suspicious window ID \(windowID) matches PID for \(ownerName)") } @@ -215,7 +215,7 @@ final class WindowTracker { // First try to find window by process PID traversal if let sessionInfo = getSessionInfo(for: sessionID), let sessionPID = sessionInfo.pid { logger.debug("Attempting to find window by process PID: \(sessionPID)") - + // For debugging: log the process tree logProcessTree(for: pid_t(sessionPID)) @@ -248,7 +248,10 @@ final class WindowTracker { if let matchingWindow = terminalWindows.first(where: { window in window.ownerPID == grandParentPID }) { - logger.info("Found window by ancestor process match: PID \(grandParentPID) at depth \(depth + 2)") + logger + .info( + "Found window by ancestor process match: PID \(grandParentPID) at depth \(depth + 2)" + ) return createWindowInfo( from: matchingWindow, sessionID: sessionID, @@ -391,7 +394,7 @@ final class WindowTracker { return nil } - + /// Get process info including name private func getProcessInfo(for pid: pid_t) -> (name: String, ppid: pid_t)? { var info = kinfo_proc() @@ -409,13 +412,13 @@ final class WindowTracker { return nil } - + /// Log the process tree for debugging private func logProcessTree(for pid: pid_t) { var currentPID = pid var depth = 0 var processPath: [String] = [] - + while depth < 15 { if let processInfo = getProcessInfo(for: currentPID) { processPath.append("\(processInfo.name):\(currentPID)") @@ -428,7 +431,7 @@ final class WindowTracker { break } } - + logger.debug("Process tree: \(processPath.joined(separator: " <- "))") } @@ -673,8 +676,11 @@ final class WindowTracker { /// Focuses a Ghostty window with macOS standard tabs. private func focusGhosttyWindow(_ windowInfo: WindowInfo) { - logger.info("Attempting to focus Ghostty window - windowID: \(windowInfo.windowID), ownerPID: \(windowInfo.ownerPID), sessionID: \(windowInfo.sessionID)") - + logger + .info( + "Attempting to focus Ghostty window - windowID: \(windowInfo.windowID), ownerPID: \(windowInfo.ownerPID), sessionID: \(windowInfo.sessionID)" + ) + // First bring the application to front if let app = NSRunningApplication(processIdentifier: windowInfo.ownerPID) { app.activate() @@ -683,13 +689,13 @@ final class WindowTracker { // Ghostty uses macOS standard tabs, so we need to: // 1. Focus the window // 2. Find and select the correct tab - + // Use Accessibility API to handle tab selection let axApp = AXUIElementCreateApplication(windowInfo.ownerPID) - + var windowsValue: CFTypeRef? let result = AXUIElementCopyAttributeValue(axApp, kAXWindowsAttribute as CFString, &windowsValue) - + guard result == .success, let windows = windowsValue as? [AXUIElement], !windows.isEmpty @@ -698,17 +704,17 @@ final class WindowTracker { focusWindowUsingAccessibility(windowInfo) return } - + // Find the matching window logger.debug("Looking for Ghostty window with ID \(windowInfo.windowID) among \(windows.count) windows") - + // If we have a very low window ID that matches PID, it's likely wrong - if windowInfo.windowID < 1000 && windowInfo.windowID == CGWindowID(windowInfo.ownerPID) { + if windowInfo.windowID < 1_000 && windowInfo.windowID == CGWindowID(windowInfo.ownerPID) { logger.warning("Window ID \(windowInfo.windowID) suspiciously matches PID, will try alternative matching") - + // In this case, we need to find the correct window by tab content let sessionInfo = getSessionInfo(for: windowInfo.sessionID) - + for (windowIndex, window) in windows.enumerated() { // Check tabs in this window var tabsValue: CFTypeRef? @@ -721,40 +727,40 @@ final class WindowTracker { // Found the window with matching tab AXUIElementSetAttributeValue(window, kAXMainAttribute as CFString, true as CFTypeRef) AXUIElementSetAttributeValue(window, kAXFocusedAttribute as CFString, true as CFTypeRef) - + // Select the correct tab selectGhosttyTab(tabs: tabs, windowInfo: windowInfo, sessionInfo: sessionInfo) - + logger.info("Focused Ghostty window \(windowIndex) by tab content match") return } } } - + // If no matching tab found, use first window as fallback if !windows.isEmpty { let window = windows[0] AXUIElementSetAttributeValue(window, kAXMainAttribute as CFString, true as CFTypeRef) AXUIElementSetAttributeValue(window, kAXFocusedAttribute as CFString, true as CFTypeRef) - + logger.info("Focused first Ghostty window as final fallback") return } } - + // Try matching by window ID, but also prepare for tab-based matching var windowWithMatchingTab: (window: AXUIElement, tabs: [AXUIElement])? let sessionInfo = getSessionInfo(for: windowInfo.sessionID) - + for (windowIndex, window) in windows.enumerated() { var windowIDValue: CFTypeRef? let windowIDResult = AXUIElementCopyAttributeValue(window, kAXWindowAttribute as CFString, &windowIDValue) - + // Also get window title for debugging var titleValue: CFTypeRef? let titleResult = AXUIElementCopyAttributeValue(window, kAXTitleAttribute as CFString, &titleValue) let windowTitle = titleResult == .success ? (titleValue as? String ?? "no title") : "failed to get title" - + // Check tabs in this window for content matching var tabsValue: CFTypeRef? if AXUIElementCopyAttributeValue(window, kAXTabsAttribute as CFString, &tabsValue) == .success, @@ -767,59 +773,66 @@ final class WindowTracker { logger.debug("Window \(windowIndex) has matching tab for session \(windowInfo.sessionID)") } } - + if windowIDResult == .success { if let windowNumber = windowIDValue as? Int { - logger.debug("Window \(windowIndex): AX window ID = \(windowNumber), title = '\(windowTitle)', looking for \(windowInfo.windowID)") - + logger + .debug( + "Window \(windowIndex): AX window ID = \(windowNumber), title = '\(windowTitle)', looking for \(windowInfo.windowID)" + ) + if windowNumber == windowInfo.windowID { // Found the window by ID, make it main and focused AXUIElementSetAttributeValue(window, kAXMainAttribute as CFString, true as CFTypeRef) AXUIElementSetAttributeValue(window, kAXFocusedAttribute as CFString, true as CFTypeRef) - - // Now select the correct tab - var tabsValue2: CFTypeRef? - if AXUIElementCopyAttributeValue(window, kAXTabsAttribute as CFString, &tabsValue2) == .success, - let tabs = tabsValue2 as? [AXUIElement], - !tabs.isEmpty - { - // Use the helper method to select the correct tab - selectGhosttyTab(tabs: tabs, windowInfo: windowInfo, sessionInfo: sessionInfo) + + // Now select the correct tab + var tabsValue2: CFTypeRef? + if AXUIElementCopyAttributeValue(window, kAXTabsAttribute as CFString, &tabsValue2) == .success, + let tabs = tabsValue2 as? [AXUIElement], + !tabs.isEmpty + { + // Use the helper method to select the correct tab + selectGhosttyTab(tabs: tabs, windowInfo: windowInfo, sessionInfo: sessionInfo) + } + + logger.info("Focused Ghostty window by ID match") + return } - - logger.info("Focused Ghostty window by ID match") - return - } } else { - logger.debug("Window \(windowIndex): AX window ID value is not an Int: \(String(describing: windowIDValue))") + logger + .debug( + "Window \(windowIndex): AX window ID value is not an Int: \(String(describing: windowIDValue))" + ) } } else { - logger.debug("Window \(windowIndex): Failed to get AX window ID, error code: \(windowIDResult.rawValue)") + logger + .debug("Window \(windowIndex): Failed to get AX window ID, error code: \(windowIDResult.rawValue)") } } - + // If we couldn't find by window ID but found a window with matching tab content, use that if let matchingWindow = windowWithMatchingTab { AXUIElementSetAttributeValue(matchingWindow.window, kAXMainAttribute as CFString, true as CFTypeRef) AXUIElementSetAttributeValue(matchingWindow.window, kAXFocusedAttribute as CFString, true as CFTypeRef) - + // Select the correct tab selectGhosttyTab(tabs: matchingWindow.tabs, windowInfo: windowInfo, sessionInfo: sessionInfo) - + logger.info("Focused Ghostty window by tab content match (no window ID match)") return } - + // Fallback if we couldn't find the specific window logger.warning("Could not find matching Ghostty window with ID \(windowInfo.windowID), using fallback") - + // Log additional debugging info if windows.isEmpty { logger.error("No Ghostty windows found at all") } else { logger.debug("Ghostty windows found but none matched ID \(windowInfo.windowID)") } - + focusWindowUsingAccessibility(windowInfo) } @@ -855,7 +868,7 @@ final class WindowTracker { // Found the matching window, make it main and focused AXUIElementSetAttributeValue(window, kAXMainAttribute as CFString, true as CFTypeRef) AXUIElementSetAttributeValue(window, kAXFocusedAttribute as CFString, true as CFTypeRef) - + // For terminals that use macOS standard tabs, try to select the correct tab var tabsValue: CFTypeRef? if AXUIElementCopyAttributeValue(window, kAXTabsAttribute as CFString, &tabsValue) == .success, @@ -863,45 +876,55 @@ final class WindowTracker { !tabs.isEmpty { logger.info("Terminal has \(tabs.count) tabs, attempting to find correct one") - + // Try to find the tab with matching session info if let sessionInfo = getSessionInfo(for: windowInfo.sessionID) { let workingDir = sessionInfo.workingDir let dirName = (workingDir as NSString).lastPathComponent let sessionID = windowInfo.sessionID let activityStatus = sessionInfo.activityStatus?.specificStatus?.status - + // Try multiple matching strategies for (index, tab) in tabs.enumerated() { var titleValue: CFTypeRef? - if AXUIElementCopyAttributeValue(tab, kAXTitleAttribute as CFString, &titleValue) == .success, - let title = titleValue as? String + if AXUIElementCopyAttributeValue(tab, kAXTitleAttribute as CFString, &titleValue) == + .success, + let title = titleValue as? String { // Check for session ID match first (most precise) if title.contains(sessionID) || title.contains("TTY_SESSION_ID=\(sessionID)") { AXUIElementPerformAction(tab, kAXPressAction as CFString) - logger.info("Selected tab \(index) by session ID for terminal \(windowInfo.terminalApp.rawValue)") + logger + .info( + "Selected tab \(index) by session ID for terminal \(windowInfo.terminalApp.rawValue)" + ) return } - + // Check for activity status match (unique for dynamic activities) if let activity = activityStatus, !activity.isEmpty, title.contains(activity) { AXUIElementPerformAction(tab, kAXPressAction as CFString) - logger.info("Selected tab \(index) by activity '\(activity)' for terminal \(windowInfo.terminalApp.rawValue)") + logger + .info( + "Selected tab \(index) by activity '\(activity)' for terminal \(windowInfo.terminalApp.rawValue)" + ) return } - + // Check for directory match if title.contains(dirName) || title.contains(workingDir) { AXUIElementPerformAction(tab, kAXPressAction as CFString) - logger.info("Selected tab \(index) by directory for terminal \(windowInfo.terminalApp.rawValue)") + logger + .info( + "Selected tab \(index) by directory for terminal \(windowInfo.terminalApp.rawValue)" + ) return } } } } } - + logger.info("Focused window using Accessibility API") return } @@ -1162,16 +1185,16 @@ final class WindowTracker { SystemPermissionManager.shared.requestPermission(.accessibility) } } - + /// Find a tab that matches the session private func findMatchingTab(tabs: [AXUIElement], sessionInfo: ServerSessionInfo?) -> AXUIElement? { - guard let sessionInfo = sessionInfo else { return nil } - + guard let sessionInfo else { return nil } + let workingDir = sessionInfo.workingDir let dirName = (workingDir as NSString).lastPathComponent let sessionID = sessionInfo.id let activityStatus = sessionInfo.activityStatus?.specificStatus?.status - + for tab in tabs { var titleValue: CFTypeRef? if AXUIElementCopyAttributeValue(tab, kAXTitleAttribute as CFString, &titleValue) == .success, @@ -1181,34 +1204,34 @@ final class WindowTracker { if title.contains(sessionID) || title.contains("TTY_SESSION_ID=\(sessionID)") { return tab } - + // Priority 2: Activity status with directory if let activity = activityStatus, !activity.isEmpty, title.contains(activity) { if title.contains(dirName) || title.contains(workingDir) { return tab } } - + // Priority 3: Command with directory if let command = sessionInfo.command.first, !command.isEmpty, title.contains(command) { if title.contains(dirName) || title.contains(workingDir) { return tab } } - + // Priority 4: Directory only if title.contains(dirName) || title.contains(workingDir) { return tab } } } - + return nil } - + /// Helper to select the correct Ghostty tab private func selectGhosttyTab(tabs: [AXUIElement], windowInfo: WindowInfo, sessionInfo: ServerSessionInfo?) { - guard let sessionInfo = sessionInfo else { + guard let sessionInfo else { // No session info, select last tab as fallback if let lastTab = tabs.last { AXUIElementPerformAction(lastTab, kAXPressAction as CFString) @@ -1216,12 +1239,12 @@ final class WindowTracker { } return } - + let workingDir = sessionInfo.workingDir let dirName = (workingDir as NSString).lastPathComponent let sessionID = windowInfo.sessionID let activityStatus = sessionInfo.activityStatus?.specificStatus?.status - + // Try multiple matching strategies as in the main method for (index, tab) in tabs.enumerated() { var titleValue: CFTypeRef? @@ -1234,7 +1257,7 @@ final class WindowTracker { logger.info("Selected Ghostty tab \(index) by session ID") return } - + // Priority 2: Activity status if let activity = activityStatus, !activity.isEmpty, title.contains(activity) { if title.contains(dirName) || title.contains(workingDir) { @@ -1243,7 +1266,7 @@ final class WindowTracker { return } } - + // Priority 3: Command if let command = sessionInfo.command.first, !command.isEmpty, title.contains(command) { if title.contains(dirName) || title.contains(workingDir) { @@ -1252,7 +1275,7 @@ final class WindowTracker { return } } - + // Priority 4: Directory only if title.contains(dirName) || title.contains(workingDir) { AXUIElementPerformAction(tab, kAXPressAction as CFString) @@ -1261,7 +1284,7 @@ final class WindowTracker { } } } - + // Fallback: select last tab if let lastTab = tabs.last { AXUIElementPerformAction(lastTab, kAXPressAction as CFString) diff --git a/mac/VibeTunnel/Presentation/Components/CustomMenuWindow.swift b/mac/VibeTunnel/Presentation/Components/CustomMenuWindow.swift index e4e3333b..93aacbec 100644 --- a/mac/VibeTunnel/Presentation/Components/CustomMenuWindow.swift +++ b/mac/VibeTunnel/Presentation/Components/CustomMenuWindow.swift @@ -6,6 +6,10 @@ import SwiftUI /// Provides a dropdown-style window for the menu bar application /// without the standard macOS popover arrow. Handles automatic positioning below /// the status item, click-outside dismissal, and proper window management. +private enum DesignConstants { + static let menuCornerRadius: CGFloat = 12 +} + @MainActor final class CustomMenuWindow: NSPanel { private var eventMonitor: Any? @@ -15,6 +19,8 @@ final class CustomMenuWindow: NSPanel { private var targetFrame: NSRect? private weak var statusBarButton: NSStatusBarButton? private var _isWindowVisible = false + private var frameObserver: Any? + private var lastBounds: CGRect = .zero /// Closure to be called when window hides var onHide: (() -> Void)? @@ -49,7 +55,7 @@ final class CustomMenuWindow: NSPanel { isMovableByWindowBackground = false hidesOnDeactivate = false isReleasedWhenClosed = false - + // Allow the window to become key but not main // This helps maintain button highlight state acceptsMouseMovedEvents = false @@ -66,19 +72,29 @@ final class CustomMenuWindow: NSPanel { // Create a custom mask layer for side-rounded corners let maskLayer = CAShapeLayer() - maskLayer.path = createSideRoundedPath(in: contentView.bounds, cornerRadius: 12) + maskLayer.path = createSideRoundedPath( + in: contentView.bounds, + cornerRadius: DesignConstants.menuCornerRadius + ) contentView.layer?.mask = maskLayer + lastBounds = contentView.bounds // Update mask when bounds change contentView.postsFrameChangedNotifications = true - NotificationCenter.default.addObserver( + self.frameObserver = NotificationCenter.default.addObserver( forName: NSView.frameDidChangeNotification, object: contentView, queue: .main ) { [weak self, weak contentView] _ in - guard let self = self, let contentView = contentView else { return } Task { @MainActor in - maskLayer.path = self.createSideRoundedPath(in: contentView.bounds, cornerRadius: 12) + guard let self, let contentView else { return } + let currentBounds = contentView.bounds + guard currentBounds != self.lastBounds else { return } + self.lastBounds = currentBounds + maskLayer.path = self.createSideRoundedPath( + in: currentBounds, + cornerRadius: DesignConstants.menuCornerRadius + ) } } @@ -91,9 +107,8 @@ final class CustomMenuWindow: NSPanel { } func show(relativeTo statusItemButton: NSStatusBarButton) { - // Store button reference and ensure it stays highlighted + // Store button reference (state should already be set by StatusBarMenuManager) self.statusBarButton = statusItemButton - statusItemButton.highlight(true) // First, make sure the SwiftUI hierarchy has laid itself out hostingController.view.layoutSubtreeIfNeeded() @@ -154,18 +169,14 @@ final class CustomMenuWindow: NSPanel { // Set all visual properties at once alphaValue = 1.0 - // Ensure button remains highlighted - statusBarButton?.highlight(true) + // Button state is managed by StatusBarMenuManager, don't change it here // Show window without activating the app aggressively // This helps maintain the button's highlight state orderFront(nil) makeKey() - // Force button highlight update again after window is shown - DispatchQueue.main.async { [weak self] in - self?.statusBarButton?.highlight(true) - } + // Button state is managed by StatusBarMenuManager // Set first responder after window is visible makeFirstResponder(self) @@ -259,8 +270,7 @@ final class CustomMenuWindow: NSPanel { // Mark window as not visible _isWindowVisible = false - // Reset button highlight when hiding - statusBarButton?.highlight(false) + // Button state will be reset by StatusBarMenuManager via onHide callback orderOut(nil) teardownEventMonitoring() onHide?() @@ -272,8 +282,7 @@ final class CustomMenuWindow: NSPanel { // Mark window as not visible _isWindowVisible = false - // Reset button highlight when window is ordered out - statusBarButton?.highlight(false) + // Button state will be reset by StatusBarMenuManager via onHide callback onHide?() } @@ -325,6 +334,9 @@ final class CustomMenuWindow: NSPanel { deinit { MainActor.assumeIsolated { teardownEventMonitoring() + if let observer = frameObserver { + NotificationCenter.default.removeObserver(observer) + } } } @@ -384,7 +396,6 @@ final class CustomMenuWindow: NSPanel { } } - /// A wrapper view that applies modern SwiftUI material background to menu content. struct CustomMenuContainer: View { @ViewBuilder let content: Content @@ -395,9 +406,9 @@ struct CustomMenuContainer: View { var body: some View { content .fixedSize() - .background(backgroundMaterial, in: SideRoundedRectangle(cornerRadius: 12)) + .background(backgroundMaterial, in: SideRoundedRectangle(cornerRadius: DesignConstants.menuCornerRadius)) .overlay( - SideRoundedRectangle(cornerRadius: 12) + SideRoundedRectangle(cornerRadius: DesignConstants.menuCornerRadius) .stroke(borderColor, lineWidth: 1) ) } diff --git a/mac/VibeTunnel/Presentation/Components/NewSessionForm.swift b/mac/VibeTunnel/Presentation/Components/NewSessionForm.swift index 77e86179..dfc32aad 100644 --- a/mac/VibeTunnel/Presentation/Components/NewSessionForm.swift +++ b/mac/VibeTunnel/Presentation/Components/NewSessionForm.swift @@ -292,7 +292,11 @@ struct NewSessionForm: View { .foregroundColor(.white) .background( RoundedRectangle(cornerRadius: 6) - .fill(command.isEmpty || workingDirectory.isEmpty ? Color.gray.opacity(0.4) : Color(red: 0.2, green: 0.6, blue: 0.3)) + .fill(command.isEmpty || workingDirectory.isEmpty ? Color.gray.opacity(0.4) : Color( + red: 0.2, + green: 0.6, + blue: 0.3 + )) ) .disabled(isCreating || command.isEmpty || workingDirectory.isEmpty) } diff --git a/mac/VibeTunnel/Presentation/Components/StatusBarController.swift b/mac/VibeTunnel/Presentation/Components/StatusBarController.swift index c6ee4839..13ea467b 100644 --- a/mac/VibeTunnel/Presentation/Components/StatusBarController.swift +++ b/mac/VibeTunnel/Presentation/Components/StatusBarController.swift @@ -63,10 +63,10 @@ final class StatusBarController: NSObject { button.action = #selector(handleClick(_:)) button.target = self button.sendAction(on: [.leftMouseUp, .rightMouseUp]) - + // Use pushOnPushOff for proper state management button.setButtonType(.pushOnPushOff) - + // Accessibility button.setAccessibilityTitle("VibeTunnel") button.setAccessibilityRole(.button) diff --git a/mac/VibeTunnel/Presentation/Components/StatusBarMenuManager.swift b/mac/VibeTunnel/Presentation/Components/StatusBarMenuManager.swift index 3fb53825..95e4feca 100644 --- a/mac/VibeTunnel/Presentation/Components/StatusBarMenuManager.swift +++ b/mac/VibeTunnel/Presentation/Components/StatusBarMenuManager.swift @@ -69,7 +69,10 @@ final class StatusBarMenuManager: NSObject { statusBarButton = button } - // No need to manage highlight here since we're using button state + // Reset button state when no menu is active + if newState == .none { + statusBarButton?.state = .off + } } // MARK: - Left-Click Custom Window Management @@ -144,20 +147,11 @@ final class StatusBarMenuManager: NSObject { // Show the custom window customWindow?.show(relativeTo: button) - - // Force immediate button highlight update after showing window - // This ensures the button stays highlighted even if there's a timing issue - Task { @MainActor in - try? await Task.sleep(for: .milliseconds(10)) - button.highlight(true) - } } func hideCustomWindow() { customWindow?.hide() - // Reset button state - statusBarButton?.state = .off - // Note: state will be reset by the onHide callback + // Button state will be reset by updateMenuState(.none) in the onHide callback } var isCustomWindowVisible: Bool { @@ -189,7 +183,7 @@ final class StatusBarMenuManager: NSObject { // Store status item reference currentStatusItem = statusItem - + // Set the button's state to on for context menu button.state = .on @@ -356,7 +350,7 @@ extension StatusBarMenuManager: NSMenuDelegate { func menuDidClose(_ menu: NSMenu) { // Reset button state statusBarButton?.state = .off - + // Reset menu state when context menu closes updateMenuState(.none) diff --git a/mac/VibeTunnel/Presentation/Components/VibeTunnelMenuView.swift b/mac/VibeTunnel/Presentation/Components/VibeTunnelMenuView.swift index a8831e5d..0d2e480d 100644 --- a/mac/VibeTunnel/Presentation/Components/VibeTunnelMenuView.swift +++ b/mac/VibeTunnel/Presentation/Components/VibeTunnelMenuView.swift @@ -469,13 +469,13 @@ struct SessionRow: View { .foregroundColor(.primary) .lineLimit(1) .truncationMode(.tail) - + // Show session name if available if let name = session.value.name, !name.isEmpty { Text("–") .font(.system(size: 12)) .foregroundColor(.secondary.opacity(0.6)) - + Text(name) .font(.system(size: 12)) .foregroundColor(.secondary) @@ -657,10 +657,10 @@ struct SessionRow: View { guard let firstCommand = session.value.command.first else { return "Unknown" } - + // Extract just the executable name from the path let executableName = (firstCommand as NSString).lastPathComponent - + // Special handling for common commands switch executableName { case "zsh", "bash", "sh": @@ -668,7 +668,8 @@ struct SessionRow: View { if session.value.command.count > 2, session.value.command.contains("-c"), let cIndex = session.value.command.firstIndex(of: "-c"), - cIndex + 1 < session.value.command.count { + cIndex + 1 < session.value.command.count + { let actualCommand = session.value.command[cIndex + 1] return (actualCommand as NSString).lastPathComponent } @@ -677,7 +678,7 @@ struct SessionRow: View { return executableName } } - + private var sessionName: String { // Use the session name if available, otherwise fall back to directory name if let name = session.value.name, !name.isEmpty { diff --git a/mac/VibeTunnel/Presentation/Shapes/SideRoundedRectangle.swift b/mac/VibeTunnel/Presentation/Shapes/SideRoundedRectangle.swift index 3d87e8c6..dee9d735 100644 --- a/mac/VibeTunnel/Presentation/Shapes/SideRoundedRectangle.swift +++ b/mac/VibeTunnel/Presentation/Shapes/SideRoundedRectangle.swift @@ -15,9 +15,6 @@ struct SideRoundedRectangle: Shape { // Top edge (flat) path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY)) - // Top-right corner (flat) - path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY)) - // Right edge with rounded corners path.addArc( center: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY + cornerRadius), @@ -40,9 +37,6 @@ struct SideRoundedRectangle: Shape { // Bottom edge (flat) path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) - // Bottom-left corner (flat) - path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) - // Left edge with rounded corners path.addArc( center: CGPoint(x: rect.minX + cornerRadius, y: rect.maxY - cornerRadius), @@ -66,4 +60,4 @@ struct SideRoundedRectangle: Shape { return path } -} \ No newline at end of file +} diff --git a/mac/VibeTunnel/Presentation/Views/MenuBarView.swift b/mac/VibeTunnel/Presentation/Views/MenuBarView.swift index 2c60df7e..c1951bdc 100644 --- a/mac/VibeTunnel/Presentation/Views/MenuBarView.swift +++ b/mac/VibeTunnel/Presentation/Views/MenuBarView.swift @@ -349,13 +349,13 @@ struct SessionRowView: View { .foregroundColor(.primary) .lineLimit(1) .truncationMode(.tail) - + // Show session name if available if let name = session.value.name, !name.isEmpty { Text("–") .font(.system(size: 12)) .foregroundColor(.secondary.opacity(0.6)) - + Text(name) .font(.system(size: 12)) .foregroundColor(.secondary) @@ -435,10 +435,10 @@ struct SessionRowView: View { guard let firstCommand = session.value.command.first else { return "Unknown" } - + // Extract just the executable name from the path let executableName = (firstCommand as NSString).lastPathComponent - + // Special handling for common commands switch executableName { case "zsh", "bash", "sh": @@ -446,7 +446,8 @@ struct SessionRowView: View { if session.value.command.count > 2, session.value.command.contains("-c"), let cIndex = session.value.command.firstIndex(of: "-c"), - cIndex + 1 < session.value.command.count { + cIndex + 1 < session.value.command.count + { let actualCommand = session.value.command[cIndex + 1] return (actualCommand as NSString).lastPathComponent } @@ -455,7 +456,7 @@ struct SessionRowView: View { return executableName } } - + private var sessionName: String { // Extract the working directory name as the session name let workingDir = session.value.workingDir diff --git a/mac/VibeTunnel/Utilities/CLIInstaller.swift b/mac/VibeTunnel/Utilities/CLIInstaller.swift index 905fb384..7fbd14cb 100644 --- a/mac/VibeTunnel/Utilities/CLIInstaller.swift +++ b/mac/VibeTunnel/Utilities/CLIInstaller.swift @@ -59,15 +59,16 @@ final class CLIInstaller { vtTargetPath, "/opt/homebrew/bin/vt" ] - + for path in pathsToCheck { if FileManager.default.fileExists(atPath: path) { // Check if it contains the correct app path reference if let content = try? String(contentsOfFile: path, encoding: .utf8) { // Verify it's our wrapper script with all expected components if content.contains("VibeTunnel CLI wrapper") && - content.contains("$TRY_PATH/Contents/Resources/vibetunnel") && - content.contains("exec \"$VIBETUNNEL_BIN\" fwd") { + content.contains("$TRY_PATH/Contents/Resources/vibetunnel") && + content.contains("exec \"$VIBETUNNEL_BIN\" fwd") + { isCorrectlyInstalled = true logger.info("CLIInstaller: Found valid vt script at \(path)") break