mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Fix various SessionRow menu bugs (#196)
This commit is contained in:
parent
f602f2936e
commit
dab2c6056d
8 changed files with 52 additions and 973 deletions
|
|
@ -539,6 +539,16 @@ class ServerManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Authentication
|
||||||
|
|
||||||
|
/// Add authentication headers to a request
|
||||||
|
func authenticate(request: inout URLRequest) throws {
|
||||||
|
guard let server = bunServer else {
|
||||||
|
throw ServerError.startupFailed("Server not running")
|
||||||
|
}
|
||||||
|
request.setValue(server.localToken, forHTTPHeaderField: "X-VibeTunnel-Local")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Server Manager Error
|
// MARK: - Server Manager Error
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ final class SessionService {
|
||||||
request.httpMethod = "PATCH"
|
request.httpMethod = "PATCH"
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
request.setValue("localhost", forHTTPHeaderField: "Host")
|
request.setValue("localhost", forHTTPHeaderField: "Host")
|
||||||
|
try serverManager.authenticate(request: &request)
|
||||||
|
|
||||||
let body = ["name": trimmedName]
|
let body = ["name": trimmedName]
|
||||||
request.httpBody = try JSONEncoder().encode(body)
|
request.httpBody = try JSONEncoder().encode(body)
|
||||||
|
|
@ -53,6 +54,7 @@ final class SessionService {
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpMethod = "DELETE"
|
request.httpMethod = "DELETE"
|
||||||
request.setValue("localhost", forHTTPHeaderField: "Host")
|
request.setValue("localhost", forHTTPHeaderField: "Host")
|
||||||
|
try serverManager.authenticate(request: &request)
|
||||||
|
|
||||||
let (_, response) = try await URLSession.shared.data(for: request)
|
let (_, response) = try await URLSession.shared.data(for: request)
|
||||||
|
|
||||||
|
|
@ -91,7 +93,7 @@ final class SessionService {
|
||||||
"titleMode": titleMode
|
"titleMode": titleMode
|
||||||
]
|
]
|
||||||
|
|
||||||
if let name, !name.isEmpty {
|
if let name = name?.trimmingCharacters(in: .whitespacesAndNewlines), !name.isEmpty {
|
||||||
body["name"] = name
|
body["name"] = name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,6 +109,7 @@ final class SessionService {
|
||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
request.setValue("localhost", forHTTPHeaderField: "Host")
|
request.setValue("localhost", forHTTPHeaderField: "Host")
|
||||||
|
try serverManager.authenticate(request: &request)
|
||||||
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||||
|
|
||||||
let (data, response) = try await URLSession.shared.data(for: request)
|
let (data, response) = try await URLSession.shared.data(for: request)
|
||||||
|
|
|
||||||
14
mac/VibeTunnel/Core/Utilities/DashboardURLBuilder.swift
Normal file
14
mac/VibeTunnel/Core/Utilities/DashboardURLBuilder.swift
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Utility for building VibeTunnel dashboard URLs
|
||||||
|
enum DashboardURLBuilder {
|
||||||
|
/// Builds the base dashboard URL
|
||||||
|
/// - Parameters:
|
||||||
|
/// - port: The server port\
|
||||||
|
/// - sessionId: The session ID to open
|
||||||
|
/// - Returns: The base dashboard URL
|
||||||
|
static func dashboardURL(port: String, sessionId: String? = nil) -> URL? {
|
||||||
|
let sessionIDQueryParameter = sessionId.map { "/?session=\($0)" } ?? ""
|
||||||
|
return URL(string: "http://127.0.0.1:\(port)\(sessionIDQueryParameter)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -362,7 +362,7 @@ struct NewSessionForm: View {
|
||||||
|
|
||||||
// If not spawning window, open in browser
|
// If not spawning window, open in browser
|
||||||
if !spawnWindow {
|
if !spawnWindow {
|
||||||
if let webURL = URL(string: "http://127.0.0.1:\(serverManager.port)/?sessionId=\(sessionId)") {
|
if let webURL = DashboardURLBuilder.dashboardURL(port: serverManager.port, sessionId: sessionId) {
|
||||||
NSWorkspace.shared.open(webURL)
|
NSWorkspace.shared.open(webURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,7 @@ final class StatusBarMenuManager: NSObject {
|
||||||
@objc
|
@objc
|
||||||
private func openDashboard() {
|
private func openDashboard() {
|
||||||
guard let serverManager else { return }
|
guard let serverManager else { return }
|
||||||
if let url = URL(string: "http://127.0.0.1:\(serverManager.port)") {
|
if let url = DashboardURLBuilder.dashboardURL(port: serverManager.port) {
|
||||||
NSWorkspace.shared.open(url)
|
NSWorkspace.shared.open(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -433,6 +433,13 @@ struct SessionRow: View {
|
||||||
@FocusState private var isEditFieldFocused: Bool
|
@FocusState private var isEditFieldFocused: Bool
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
Button(action: handleTap) {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
var content: some View {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
// Activity indicator with subtle glow
|
// Activity indicator with subtle glow
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|
@ -568,18 +575,6 @@ struct SessionRow: View {
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
|
||||||
guard !isEditing else { return }
|
|
||||||
|
|
||||||
if hasWindow {
|
|
||||||
WindowTracker.shared.focusWindow(for: session.key)
|
|
||||||
} else {
|
|
||||||
// Open browser for sessions without windows
|
|
||||||
if let url = URL(string: "http://127.0.0.1:\(serverManager.port)/?sessionId=\(session.key)") {
|
|
||||||
NSWorkspace.shared.open(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 6)
|
RoundedRectangle(cornerRadius: 6)
|
||||||
.fill(isHovered ? hoverBackgroundColor : Color.clear)
|
.fill(isHovered ? hoverBackgroundColor : Color.clear)
|
||||||
|
|
@ -601,7 +596,7 @@ struct SessionRow: View {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Button("Open in Browser") {
|
Button("Open in Browser") {
|
||||||
if let url = URL(string: "http://127.0.0.1:\(serverManager.port)/?sessionId=\(session.key)") {
|
if let url = DashboardURLBuilder.dashboardURL(port: serverManager.port, sessionId: session.key) {
|
||||||
NSWorkspace.shared.open(url)
|
NSWorkspace.shared.open(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -634,6 +629,19 @@ struct SessionRow: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handleTap() {
|
||||||
|
guard !isEditing else { return }
|
||||||
|
|
||||||
|
if hasWindow {
|
||||||
|
WindowTracker.shared.focusWindow(for: session.key)
|
||||||
|
} else {
|
||||||
|
// Open browser for sessions without windows
|
||||||
|
if let url = DashboardURLBuilder.dashboardURL(port: serverManager.port, sessionId: session.key) {
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func terminateSession() {
|
private func terminateSession() {
|
||||||
isTerminating = true
|
isTerminating = true
|
||||||
|
|
||||||
|
|
@ -821,7 +829,7 @@ struct EmptySessionsView: View {
|
||||||
|
|
||||||
if serverManager.isRunning {
|
if serverManager.isRunning {
|
||||||
Button("Open Dashboard") {
|
Button("Open Dashboard") {
|
||||||
if let url = URL(string: "http://127.0.0.1:\(serverManager.port)") {
|
if let url = DashboardURLBuilder.dashboardURL(port: serverManager.port) {
|
||||||
NSWorkspace.shared.open(url)
|
NSWorkspace.shared.open(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,522 +0,0 @@
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
/// Main menu bar view displaying session status and app controls.
|
|
||||||
///
|
|
||||||
/// Appears in the macOS menu bar and provides quick access to VibeTunnel's
|
|
||||||
/// key features including server status, dashboard access, session monitoring,
|
|
||||||
/// and application preferences. Updates in real-time to reflect server state.
|
|
||||||
struct MenuBarView: View {
|
|
||||||
@Environment(SessionMonitor.self)
|
|
||||||
var sessionMonitor
|
|
||||||
@Environment(ServerManager.self)
|
|
||||||
var serverManager
|
|
||||||
@AppStorage("showInDock")
|
|
||||||
private var showInDock = false
|
|
||||||
@Environment(\.openWindow)
|
|
||||||
private var openWindow
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
// Server status header
|
|
||||||
ServerStatusView(isRunning: serverManager.isRunning, port: Int(serverManager.port) ?? 4_020)
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
|
|
||||||
// Open Dashboard button
|
|
||||||
Button(action: {
|
|
||||||
if let dashboardURL = URL(string: "http://127.0.0.1:\(serverManager.port)") {
|
|
||||||
NSWorkspace.shared.open(dashboardURL)
|
|
||||||
}
|
|
||||||
}, label: {
|
|
||||||
Label("Open Dashboard", systemImage: "safari")
|
|
||||||
})
|
|
||||||
.buttonStyle(MenuButtonStyle())
|
|
||||||
.disabled(!serverManager.isRunning)
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
|
|
||||||
// Session count header
|
|
||||||
SessionCountView(count: sessionMonitor.sessionCount)
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
|
|
||||||
// Session list with clickable items
|
|
||||||
if !sessionMonitor.sessions.isEmpty {
|
|
||||||
SessionListView(sessions: sessionMonitor.sessions)
|
|
||||||
.padding(.horizontal, 4)
|
|
||||||
} else {
|
|
||||||
Text("No sessions")
|
|
||||||
.font(.system(size: 11))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
|
|
||||||
// Help menu with submenu indicator
|
|
||||||
HStack {
|
|
||||||
Menu {
|
|
||||||
// Show Tutorial
|
|
||||||
Button(action: {
|
|
||||||
#if !SWIFT_PACKAGE
|
|
||||||
AppDelegate.showWelcomeScreen()
|
|
||||||
#endif
|
|
||||||
}, label: {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "book")
|
|
||||||
Text("Show Tutorial")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
// Website
|
|
||||||
Button(action: {
|
|
||||||
if let url = URL(string: "http://vibetunnel.sh") {
|
|
||||||
NSWorkspace.shared.open(url)
|
|
||||||
}
|
|
||||||
}, label: {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "globe")
|
|
||||||
Text("Website")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Report Issue
|
|
||||||
Button(action: {
|
|
||||||
if let url = URL(string: "https://github.com/amantus-ai/vibetunnel/issues") {
|
|
||||||
NSWorkspace.shared.open(url)
|
|
||||||
}
|
|
||||||
}, label: {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "exclamationmark.triangle")
|
|
||||||
Text("Report Issue")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
// Check for Updates
|
|
||||||
Button(action: {
|
|
||||||
SparkleUpdaterManager.shared.checkForUpdates()
|
|
||||||
}, label: {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "arrow.down.circle")
|
|
||||||
Text("Check for Updates…")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Version (non-interactive)
|
|
||||||
HStack {
|
|
||||||
Color.clear
|
|
||||||
.frame(width: 16, height: 16) // Match the typical SF Symbol size
|
|
||||||
Text("Version \(appVersion)")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
// About
|
|
||||||
Button(
|
|
||||||
action: {
|
|
||||||
SettingsOpener.openSettings()
|
|
||||||
// Navigate to About tab after settings opens
|
|
||||||
Task {
|
|
||||||
try? await Task.sleep(for: .milliseconds(100))
|
|
||||||
NotificationCenter.default.post(
|
|
||||||
name: .openSettingsTab,
|
|
||||||
object: SettingsTab.about
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "info.circle")
|
|
||||||
Text("About VibeTunnel")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} label: {
|
|
||||||
Label("Help", systemImage: "questionmark.circle")
|
|
||||||
}
|
|
||||||
.menuStyle(BorderlessButtonMenuStyle())
|
|
||||||
.fixedSize()
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 4)
|
|
||||||
.fill(Color.accentColor.opacity(0.001))
|
|
||||||
)
|
|
||||||
|
|
||||||
// New Session button
|
|
||||||
Button(
|
|
||||||
action: {
|
|
||||||
// Close menu and show custom window with new session form
|
|
||||||
NSApp.sendAction(#selector(NSWindow.performClose(_:)), to: nil, from: nil)
|
|
||||||
if let statusBarController = (NSApp.delegate as? AppDelegate)?.statusBarController {
|
|
||||||
statusBarController.showCustomWindow()
|
|
||||||
// Navigate to new session form
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
Label("New Session…", systemImage: "plus.square")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.buttonStyle(MenuButtonStyle())
|
|
||||||
.keyboardShortcut("n", modifiers: .command)
|
|
||||||
|
|
||||||
// Settings button
|
|
||||||
Button(
|
|
||||||
action: {
|
|
||||||
SettingsOpener.openSettings()
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
Label("Settings…", systemImage: "gear")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.buttonStyle(MenuButtonStyle())
|
|
||||||
.keyboardShortcut(",", modifiers: .command)
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
|
|
||||||
// Quit button
|
|
||||||
Button(action: {
|
|
||||||
NSApplication.shared.terminate(nil)
|
|
||||||
}, label: {
|
|
||||||
Label("Quit", systemImage: "power")
|
|
||||||
})
|
|
||||||
.buttonStyle(MenuButtonStyle())
|
|
||||||
.keyboardShortcut("q", modifiers: .command)
|
|
||||||
}
|
|
||||||
.frame(minWidth: 200)
|
|
||||||
.task {
|
|
||||||
// Wait for server to be running before fetching sessions
|
|
||||||
while !serverManager.isRunning {
|
|
||||||
try? await Task.sleep(for: .milliseconds(500))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Give the server a moment to fully initialize after starting
|
|
||||||
try? await Task.sleep(for: .milliseconds(100))
|
|
||||||
|
|
||||||
// Force initial refresh
|
|
||||||
await sessionMonitor.refresh()
|
|
||||||
|
|
||||||
// Update sessions periodically while view is visible
|
|
||||||
while true {
|
|
||||||
_ = await sessionMonitor.getSessions()
|
|
||||||
try? await Task.sleep(for: .seconds(1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var appVersion: String {
|
|
||||||
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Server Status View
|
|
||||||
|
|
||||||
/// Displays the HTTP server status
|
|
||||||
struct ServerStatusView: View {
|
|
||||||
let isRunning: Bool
|
|
||||||
let port: Int
|
|
||||||
@Environment(ServerManager.self)
|
|
||||||
var serverManager
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
HStack(spacing: 6) {
|
|
||||||
Circle()
|
|
||||||
.fill(isRunning ? Color.green : Color.red)
|
|
||||||
.frame(width: 8, height: 8)
|
|
||||||
|
|
||||||
Text(statusText)
|
|
||||||
.font(.system(size: 13))
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.lineLimit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isRunning {
|
|
||||||
Text(accessText)
|
|
||||||
.font(.system(size: 11))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
.padding(.leading, 14) // Align with the status text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var statusText: String {
|
|
||||||
if isRunning {
|
|
||||||
"Server running"
|
|
||||||
} else {
|
|
||||||
"Server stopped"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var accessText: String {
|
|
||||||
let bindAddress = serverManager.bindAddress
|
|
||||||
if bindAddress == "127.0.0.1" {
|
|
||||||
return "127.0.0.1:\(port)"
|
|
||||||
} else {
|
|
||||||
// Network mode - show local IP if available
|
|
||||||
if let localIP = NetworkUtility.getLocalIPAddress() {
|
|
||||||
return "\(localIP):\(port)"
|
|
||||||
} else {
|
|
||||||
return "0.0.0.0:\(port)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Session Count View
|
|
||||||
|
|
||||||
/// Displays the count of active SSH sessions
|
|
||||||
struct SessionCountView: View {
|
|
||||||
let count: Int
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 6) {
|
|
||||||
Text(sessionText)
|
|
||||||
.font(.system(size: 13))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
}
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var sessionText: String {
|
|
||||||
count == 1 ? "1 active session" : "\(count) active sessions"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Session List View
|
|
||||||
|
|
||||||
/// Lists active SSH sessions with truncation for large lists
|
|
||||||
struct SessionListView: View {
|
|
||||||
let sessions: [String: ServerSessionInfo]
|
|
||||||
@Environment(\.openWindow)
|
|
||||||
private var openWindow
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
ForEach(Array(activeSessions.prefix(5)), id: \.key) { session in
|
|
||||||
SessionRowView(session: session, openWindow: openWindow)
|
|
||||||
}
|
|
||||||
|
|
||||||
if activeSessions.count > 5 {
|
|
||||||
HStack {
|
|
||||||
Text(" • ...")
|
|
||||||
.font(.system(size: 12))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.padding(.vertical, 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var activeSessions: [(key: String, value: ServerSessionInfo)] {
|
|
||||||
sessions.filter(\.value.isRunning)
|
|
||||||
.sorted { $0.value.startedAt > $1.value.startedAt }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Session Row View
|
|
||||||
|
|
||||||
/// Individual row displaying session information
|
|
||||||
struct SessionRowView: View {
|
|
||||||
let session: (key: String, value: ServerSessionInfo)
|
|
||||||
let openWindow: OpenWindowAction
|
|
||||||
@State private var isHovered = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: {
|
|
||||||
// Focus the terminal window for this session
|
|
||||||
WindowTracker.shared.focusWindow(for: session.key)
|
|
||||||
}, label: {
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
// Main session row
|
|
||||||
HStack {
|
|
||||||
Text(" • \(commandName)")
|
|
||||||
.font(.system(size: 12))
|
|
||||||
.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)
|
|
||||||
.lineLimit(1)
|
|
||||||
.truncationMode(.middle)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if let pid = session.value.pid {
|
|
||||||
Text("PID: \(pid)")
|
|
||||||
.font(.system(size: 11))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activity status and path row
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Text(" ")
|
|
||||||
.font(.system(size: 11))
|
|
||||||
|
|
||||||
if let activityStatus {
|
|
||||||
Text(activityStatus)
|
|
||||||
.font(.system(size: 11))
|
|
||||||
.foregroundColor(Color(red: 0.8, green: 0.4, blue: 0.0))
|
|
||||||
|
|
||||||
Text("·")
|
|
||||||
.font(.system(size: 11))
|
|
||||||
.foregroundColor(.secondary.opacity(0.5))
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(compactPath)
|
|
||||||
.font(.system(size: 11))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
.truncationMode(.middle)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.vertical, 2)
|
|
||||||
.padding(.horizontal, 8)
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
})
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 4)
|
|
||||||
.fill(isHovered ? Color.accentColor.opacity(0.1) : Color.clear)
|
|
||||||
)
|
|
||||||
.onHover { hovering in
|
|
||||||
isHovered = hovering
|
|
||||||
}
|
|
||||||
.contextMenu {
|
|
||||||
Button("Focus Terminal Window") {
|
|
||||||
WindowTracker.shared.focusWindow(for: session.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button("View Session Details") {
|
|
||||||
openWindow(id: "session-detail", value: session.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button("Show in Finder") {
|
|
||||||
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: session.value.workingDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
Button("Copy Session ID") {
|
|
||||||
NSPasteboard.general.clearContents()
|
|
||||||
NSPasteboard.general.setString(session.key, forType: .string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var commandName: String {
|
|
||||||
// Extract the process name from the command
|
|
||||||
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":
|
|
||||||
// For shells, check if there's a -c argument with the actual command
|
|
||||||
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
|
|
||||||
{
|
|
||||||
let actualCommand = session.value.command[cIndex + 1]
|
|
||||||
return (actualCommand as NSString).lastPathComponent
|
|
||||||
}
|
|
||||||
return executableName
|
|
||||||
default:
|
|
||||||
return executableName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var sessionName: String {
|
|
||||||
// Extract the working directory name as the session name
|
|
||||||
let workingDir = session.value.workingDir
|
|
||||||
let name = (workingDir as NSString).lastPathComponent
|
|
||||||
|
|
||||||
// Truncate long session names
|
|
||||||
if name.count > 30 {
|
|
||||||
let prefix = String(name.prefix(15))
|
|
||||||
let suffix = String(name.suffix(10))
|
|
||||||
return "\(prefix)...\(suffix)"
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
private var activityStatus: String? {
|
|
||||||
if let specificStatus = session.value.activityStatus?.specificStatus {
|
|
||||||
return specificStatus.status
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private var compactPath: String {
|
|
||||||
let path = session.value.workingDir
|
|
||||||
let homeDir = NSHomeDirectory()
|
|
||||||
|
|
||||||
// Replace home directory with ~
|
|
||||||
if path.hasPrefix(homeDir) {
|
|
||||||
let relativePath = String(path.dropFirst(homeDir.count))
|
|
||||||
return "~" + relativePath
|
|
||||||
}
|
|
||||||
|
|
||||||
// For other paths, show last two components
|
|
||||||
let components = (path as NSString).pathComponents
|
|
||||||
if components.count > 2 {
|
|
||||||
let lastTwo = components.suffix(2).joined(separator: "/")
|
|
||||||
return ".../" + lastTwo
|
|
||||||
}
|
|
||||||
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Menu Button Style
|
|
||||||
|
|
||||||
/// Custom button style for menu items with hover effects
|
|
||||||
struct MenuButtonStyle: ButtonStyle {
|
|
||||||
@State private var isHovered = false
|
|
||||||
|
|
||||||
func makeBody(configuration: ButtonStyle.Configuration) -> some View {
|
|
||||||
configuration.label
|
|
||||||
.font(.system(size: 13))
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 4)
|
|
||||||
.fill(isHovered ? Color.accentColor.opacity(0.1) : Color.clear)
|
|
||||||
)
|
|
||||||
.onHover { hovering in
|
|
||||||
isHovered = hovering
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,434 +0,0 @@
|
||||||
import AppKit
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
/// Native macOS dialog for creating new terminal sessions
|
|
||||||
struct NewSessionView: View {
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
@Environment(ServerManager.self) private var serverManager
|
|
||||||
|
|
||||||
// Form fields
|
|
||||||
@State private var sessionName = ""
|
|
||||||
@State private var command = "zsh"
|
|
||||||
@State private var workingDirectory = "~/"
|
|
||||||
@State private var spawnWindow = true
|
|
||||||
@State private var titleMode: TitleMode = .dynamic
|
|
||||||
|
|
||||||
// UI state
|
|
||||||
@State private var isCreating = false
|
|
||||||
@State private var showError = false
|
|
||||||
@State private var errorMessage = ""
|
|
||||||
@FocusState private var focusedField: Field?
|
|
||||||
|
|
||||||
/// Quick commands
|
|
||||||
private let quickCommands = [
|
|
||||||
("claude", "✨"),
|
|
||||||
("aider", "✨"),
|
|
||||||
("zsh", nil),
|
|
||||||
("python3", nil),
|
|
||||||
("node", nil),
|
|
||||||
("pnpm run dev", "▶️")
|
|
||||||
]
|
|
||||||
|
|
||||||
enum Field: Hashable {
|
|
||||||
case name
|
|
||||||
case command
|
|
||||||
case directory
|
|
||||||
}
|
|
||||||
|
|
||||||
enum TitleMode: String, CaseIterable {
|
|
||||||
case none = "none"
|
|
||||||
case filter = "filter"
|
|
||||||
case `static` = "static"
|
|
||||||
case dynamic = "dynamic"
|
|
||||||
|
|
||||||
var displayName: String {
|
|
||||||
switch self {
|
|
||||||
case .none: "None"
|
|
||||||
case .filter: "Filter"
|
|
||||||
case .static: "Static"
|
|
||||||
case .dynamic: "Dynamic"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var description: String {
|
|
||||||
switch self {
|
|
||||||
case .none: "Apps control their own titles"
|
|
||||||
case .filter: "Block all title changes"
|
|
||||||
case .static: "Show path and command"
|
|
||||||
case .dynamic: "Show activity indicators"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
// Form content
|
|
||||||
Form {
|
|
||||||
// Command Section
|
|
||||||
Section {
|
|
||||||
// Command field with integrated name
|
|
||||||
HStack(alignment: .center, spacing: 12) {
|
|
||||||
TextField("Command", text: $command)
|
|
||||||
.textFieldStyle(.squareBorder)
|
|
||||||
.focused($focusedField, equals: .command)
|
|
||||||
.onChange(of: command) { _, newValue in
|
|
||||||
// Auto-select dynamic title mode for AI tools
|
|
||||||
if newValue.lowercased().contains("claude") ||
|
|
||||||
newValue.lowercased().contains("aider")
|
|
||||||
{
|
|
||||||
titleMode = .dynamic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional session name
|
|
||||||
TextField("Session Name (optional)", text: $sessionName)
|
|
||||||
.textFieldStyle(.squareBorder)
|
|
||||||
.focused($focusedField, equals: .name)
|
|
||||||
.frame(width: 160)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Working Directory
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
Text("Working Directory")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.frame(width: 120, alignment: .trailing)
|
|
||||||
|
|
||||||
TextField("", text: $workingDirectory)
|
|
||||||
.textFieldStyle(.squareBorder)
|
|
||||||
.focused($focusedField, equals: .directory)
|
|
||||||
|
|
||||||
Button(action: selectDirectory) {
|
|
||||||
Image(systemName: "folder")
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderless)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quick Start Grid
|
|
||||||
Section("Quick Start") {
|
|
||||||
LazyVGrid(columns: [
|
|
||||||
GridItem(.flexible()),
|
|
||||||
GridItem(.flexible()),
|
|
||||||
GridItem(.flexible())
|
|
||||||
], spacing: 8) {
|
|
||||||
ForEach(quickCommands, id: \.0) { cmd in
|
|
||||||
Button(action: {
|
|
||||||
command = cmd.0
|
|
||||||
// Clear session name when selecting quick command
|
|
||||||
sessionName = ""
|
|
||||||
}) {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
if let emoji = cmd.1 {
|
|
||||||
Text(emoji)
|
|
||||||
.font(.system(size: 13))
|
|
||||||
}
|
|
||||||
Text(cmd.0)
|
|
||||||
.font(.system(size: 12))
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding(.horizontal, 10)
|
|
||||||
.padding(.vertical, 6)
|
|
||||||
.background(Color.secondary.opacity(0.1))
|
|
||||||
.cornerRadius(6)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
|
|
||||||
// Options Section
|
|
||||||
Section {
|
|
||||||
// Terminal Title Mode - Single line
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
Text("Terminal Title Mode")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.frame(width: 120, alignment: .trailing)
|
|
||||||
|
|
||||||
Picker("", selection: $titleMode) {
|
|
||||||
ForEach(TitleMode.allCases, id: \.self) { mode in
|
|
||||||
Text(mode.displayName)
|
|
||||||
.tag(mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pickerStyle(.segmented)
|
|
||||||
.frame(width: 200)
|
|
||||||
|
|
||||||
Text(titleMode.description)
|
|
||||||
.font(.system(size: 11))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open in Terminal Window - Toggle on right
|
|
||||||
HStack {
|
|
||||||
Text("Open in Terminal Window")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.frame(width: 120, alignment: .trailing)
|
|
||||||
|
|
||||||
Text("Launch session in native terminal app")
|
|
||||||
.font(.system(size: 11))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Toggle("", isOn: $spawnWindow)
|
|
||||||
.toggleStyle(.switch)
|
|
||||||
.labelsHidden()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.formStyle(.grouped)
|
|
||||||
.scrollDisabled(true)
|
|
||||||
.padding(.top, 12)
|
|
||||||
|
|
||||||
// Buttons
|
|
||||||
HStack {
|
|
||||||
Button("Cancel") {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
.keyboardShortcut(.escape, modifiers: [])
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button(action: createSession) {
|
|
||||||
if isCreating {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
ProgressView()
|
|
||||||
.scaleEffect(0.7)
|
|
||||||
.controlSize(.small)
|
|
||||||
Text("Creating...")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text("Create")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.keyboardShortcut(.return, modifiers: [])
|
|
||||||
.disabled(isCreating || !isFormValid)
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.controlSize(.large)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.vertical, 16)
|
|
||||||
.background(Color(NSColor.controlBackgroundColor))
|
|
||||||
}
|
|
||||||
.frame(width: 620, height: 380)
|
|
||||||
.background(Color(NSColor.windowBackgroundColor))
|
|
||||||
.onAppear {
|
|
||||||
loadPreferences()
|
|
||||||
focusedField = .command
|
|
||||||
}
|
|
||||||
.alert("Error", isPresented: $showError) {
|
|
||||||
Button("OK") {}
|
|
||||||
} message: {
|
|
||||||
Text(errorMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Computed Properties
|
|
||||||
|
|
||||||
private var isFormValid: Bool {
|
|
||||||
!command.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
|
|
||||||
!workingDirectory.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Actions
|
|
||||||
|
|
||||||
private func selectDirectory() {
|
|
||||||
let panel = NSOpenPanel()
|
|
||||||
panel.canChooseFiles = false
|
|
||||||
panel.canChooseDirectories = true
|
|
||||||
panel.allowsMultipleSelection = false
|
|
||||||
panel.directoryURL = URL(fileURLWithPath: NSString(string: workingDirectory).expandingTildeInPath)
|
|
||||||
|
|
||||||
if panel.runModal() == .OK, let url = panel.url {
|
|
||||||
let path = url.path
|
|
||||||
let homeDir = NSHomeDirectory()
|
|
||||||
if path.hasPrefix(homeDir) {
|
|
||||||
workingDirectory = "~" + path.dropFirst(homeDir.count)
|
|
||||||
} else {
|
|
||||||
workingDirectory = path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func createSession() {
|
|
||||||
guard isFormValid else { return }
|
|
||||||
|
|
||||||
// Check if server is running
|
|
||||||
guard serverManager.isRunning else {
|
|
||||||
errorMessage = "Server is not running. Please start the server first."
|
|
||||||
showError = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isCreating = true
|
|
||||||
savePreferences()
|
|
||||||
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
// Parse command into array
|
|
||||||
let commandArray = parseCommand(command.trimmingCharacters(in: .whitespacesAndNewlines))
|
|
||||||
|
|
||||||
// Expand tilde in working directory
|
|
||||||
let expandedWorkingDir = NSString(string: workingDirectory).expandingTildeInPath
|
|
||||||
|
|
||||||
// Prepare request body
|
|
||||||
var body: [String: Any] = [
|
|
||||||
"command": commandArray,
|
|
||||||
"workingDir": expandedWorkingDir,
|
|
||||||
"titleMode": titleMode.rawValue
|
|
||||||
]
|
|
||||||
|
|
||||||
if !sessionName.isEmpty {
|
|
||||||
body["name"] = sessionName.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
}
|
|
||||||
|
|
||||||
if spawnWindow {
|
|
||||||
body["spawn_terminal"] = true
|
|
||||||
} else {
|
|
||||||
// Web sessions need terminal dimensions
|
|
||||||
body["cols"] = 120
|
|
||||||
body["rows"] = 30
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create session
|
|
||||||
let url = URL(string: "http://127.0.0.1:\(serverManager.port)/api/sessions")!
|
|
||||||
var request = URLRequest(url: url)
|
|
||||||
request.httpMethod = "POST"
|
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
||||||
request.setValue("localhost", forHTTPHeaderField: "Host")
|
|
||||||
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
||||||
|
|
||||||
let (data, response) = try await URLSession.shared.data(for: request)
|
|
||||||
|
|
||||||
if let httpResponse = response as? HTTPURLResponse {
|
|
||||||
if httpResponse.statusCode == 201 {
|
|
||||||
// Success
|
|
||||||
if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
||||||
let sessionId = json["id"] as? String
|
|
||||||
{
|
|
||||||
// If not spawning window, open in browser
|
|
||||||
if !spawnWindow {
|
|
||||||
if let webURL =
|
|
||||||
URL(string: "http://127.0.0.1:\(serverManager.port)/?sessionId=\(sessionId)")
|
|
||||||
{
|
|
||||||
NSWorkspace.shared.open(webURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Parse error response
|
|
||||||
var errorMessage = "Failed to create session"
|
|
||||||
if let errorData = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
||||||
let error = errorData["error"] as? String
|
|
||||||
{
|
|
||||||
errorMessage = error
|
|
||||||
}
|
|
||||||
|
|
||||||
throw NSError(domain: "VibeTunnel", code: httpResponse.statusCode, userInfo: [
|
|
||||||
NSLocalizedDescriptionKey: errorMessage
|
|
||||||
])
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw NSError(domain: "VibeTunnel", code: 1, userInfo: [
|
|
||||||
NSLocalizedDescriptionKey: "Invalid server response"
|
|
||||||
])
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
await MainActor.run {
|
|
||||||
isCreating = false
|
|
||||||
errorMessage = error.localizedDescription
|
|
||||||
showError = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func parseCommand(_ cmd: String) -> [String] {
|
|
||||||
// Simple command parsing that respects quotes
|
|
||||||
var result: [String] = []
|
|
||||||
var current = ""
|
|
||||||
var inQuotes = false
|
|
||||||
var quoteChar: Character?
|
|
||||||
|
|
||||||
for char in cmd {
|
|
||||||
if !inQuotes && (char == "\"" || char == "'") {
|
|
||||||
inQuotes = true
|
|
||||||
quoteChar = char
|
|
||||||
} else if inQuotes && char == quoteChar {
|
|
||||||
inQuotes = false
|
|
||||||
quoteChar = nil
|
|
||||||
} else if !inQuotes && char == " " {
|
|
||||||
if !current.isEmpty {
|
|
||||||
result.append(current)
|
|
||||||
current = ""
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
current.append(char)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !current.isEmpty {
|
|
||||||
result.append(current)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.isEmpty ? ["zsh"] : result
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Preferences
|
|
||||||
|
|
||||||
private func loadPreferences() {
|
|
||||||
if let savedCommand = UserDefaults.standard.string(forKey: "NewSession.command") {
|
|
||||||
command = savedCommand
|
|
||||||
}
|
|
||||||
if let savedDir = UserDefaults.standard.string(forKey: "NewSession.workingDirectory") {
|
|
||||||
workingDirectory = savedDir
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if spawn window preference has been explicitly set
|
|
||||||
if UserDefaults.standard.object(forKey: "NewSession.spawnWindow") != nil {
|
|
||||||
spawnWindow = UserDefaults.standard.bool(forKey: "NewSession.spawnWindow")
|
|
||||||
} else {
|
|
||||||
// Default to true if never set
|
|
||||||
spawnWindow = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if let savedMode = UserDefaults.standard.string(forKey: "NewSession.titleMode"),
|
|
||||||
let mode = TitleMode(rawValue: savedMode)
|
|
||||||
{
|
|
||||||
titleMode = mode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func savePreferences() {
|
|
||||||
UserDefaults.standard.set(command, forKey: "NewSession.command")
|
|
||||||
UserDefaults.standard.set(workingDirectory, forKey: "NewSession.workingDirectory")
|
|
||||||
UserDefaults.standard.set(spawnWindow, forKey: "NewSession.spawnWindow")
|
|
||||||
UserDefaults.standard.set(true, forKey: "NewSession.hasSetSpawnWindow")
|
|
||||||
UserDefaults.standard.set(titleMode.rawValue, forKey: "NewSession.titleMode")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Window Scene
|
|
||||||
|
|
||||||
struct NewSessionWindow: Scene {
|
|
||||||
var body: some Scene {
|
|
||||||
Window("New Session", id: "new-session") {
|
|
||||||
NewSessionView()
|
|
||||||
.environment(ServerManager.shared)
|
|
||||||
}
|
|
||||||
.windowStyle(.titleBar)
|
|
||||||
.windowToolbarStyle(.unified(showsTitle: true))
|
|
||||||
.windowResizability(.contentSize)
|
|
||||||
.defaultPosition(.center)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue