Rework Settings to give Notifications its own Tab

This commit is contained in:
Peter Steinberger 2025-07-28 13:43:44 +02:00
parent 8945dd5d66
commit 414a2160e7
10 changed files with 573 additions and 133 deletions

View file

@ -47,6 +47,7 @@ final class ConfigManager {
var notificationClaudeTurn: Bool = false
var notificationSoundEnabled: Bool = true
var notificationVibrationEnabled: Bool = true
var showInNotificationCenter: Bool = true
// Remote access
var ngrokEnabled: Bool = false
@ -107,6 +108,7 @@ final class ConfigManager {
var claudeTurn: Bool
var soundEnabled: Bool
var vibrationEnabled: Bool
var showInNotificationCenter: Bool?
}
private struct RemoteAccessConfig: Codable {
@ -193,6 +195,9 @@ final class ConfigManager {
self.notificationClaudeTurn = notif.claudeTurn
self.notificationSoundEnabled = notif.soundEnabled
self.notificationVibrationEnabled = notif.vibrationEnabled
if let showInCenter = notif.showInNotificationCenter {
self.showInNotificationCenter = showInCenter
}
}
}
@ -236,6 +241,7 @@ final class ConfigManager {
self.notificationClaudeTurn = false
self.notificationSoundEnabled = true
self.notificationVibrationEnabled = true
self.showInNotificationCenter = true
saveConfiguration()
}
@ -281,7 +287,8 @@ final class ConfigManager {
bell: notificationBell,
claudeTurn: notificationClaudeTurn,
soundEnabled: notificationSoundEnabled,
vibrationEnabled: notificationVibrationEnabled
vibrationEnabled: notificationVibrationEnabled,
showInNotificationCenter: showInNotificationCenter
)
)

View file

@ -1,5 +1,6 @@
import AppKit
import Foundation
import Observation
import os.log
@preconcurrency import UserNotifications
@ -8,6 +9,7 @@ import os.log
/// Connects to the VibeTunnel server to receive real-time events like session starts,
/// command completions, and errors, then displays them as native macOS notifications.
@MainActor
@Observable
final class NotificationService: NSObject {
@MainActor
static let shared = NotificationService()
@ -730,9 +732,7 @@ final class NotificationService: NSObject {
deinit {
// Note: We can't call disconnect() here because it's @MainActor isolated
// The cleanup will happen when the EventSource is deallocated
eventSource?.disconnect()
eventSource = nil
NotificationCenter.default.removeObserver(self)
// NotificationCenter observers are automatically removed on deinit in modern Swift
}
}

View file

@ -0,0 +1,122 @@
import SwiftUI
import AppKit
import os.log
/// Authentication configuration section for remote access settings
struct AuthenticationSection: View {
@Binding var authMode: AuthenticationMode
@Binding var enableSSHKeys: Bool
let logger: Logger
let serverManager: ServerManager
var body: some View {
Section {
VStack(alignment: .leading, spacing: 16) {
// Authentication mode picker
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("Authentication Method")
.font(.callout)
Spacer()
Picker("", selection: $authMode) {
ForEach(AuthenticationMode.allCases, id: \.self) { mode in
Text(mode.displayName)
.tag(mode)
}
}
.labelsHidden()
.pickerStyle(.menu)
.frame(alignment: .trailing)
.onChange(of: authMode) { _, newValue in
// Save the authentication mode
UserDefaults.standard.set(
newValue.rawValue,
forKey: AppConstants.UserDefaultsKeys.authenticationMode
)
Task {
logger.info("Authentication mode changed to: \(newValue.rawValue)")
await serverManager.restart()
}
}
}
Text(authMode.description)
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
}
// Additional info based on selected mode
if authMode == .osAuth || authMode == .both {
HStack(alignment: .center, spacing: 6) {
Image(systemName: "info.circle")
.foregroundColor(.blue)
.font(.system(size: 12))
.frame(width: 16, height: 16)
Text("Uses your macOS username: \(NSUserName())")
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
}
}
if authMode == .sshKeys || authMode == .both {
HStack(alignment: .center, spacing: 6) {
Image(systemName: "key.fill")
.foregroundColor(.blue)
.font(.system(size: 12))
.frame(width: 16, height: 16)
Text("SSH keys from ~/.ssh/authorized_keys")
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
Button("Open folder") {
let sshPath = NSHomeDirectory() + "/.ssh"
if FileManager.default.fileExists(atPath: sshPath) {
NSWorkspace.shared.open(URL(fileURLWithPath: sshPath))
} else {
// Create .ssh directory if it doesn't exist
try? FileManager.default.createDirectory(
atPath: sshPath,
withIntermediateDirectories: true,
attributes: [.posixPermissions: 0o700]
)
NSWorkspace.shared.open(URL(fileURLWithPath: sshPath))
}
}
.buttonStyle(.link)
.font(.caption)
}
}
}
} header: {
Text("Authentication")
.font(.headline)
} footer: {
Text("Localhost connections are always accessible without authentication.")
.font(.caption)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
}
}
}
#Preview {
struct PreviewWrapper: View {
@State var authMode = AuthenticationMode.osAuth
@State var enableSSHKeys = false
var body: some View {
AuthenticationSection(
authMode: $authMode,
enableSSHKeys: $enableSSHKeys,
logger: Logger(subsystem: "preview", category: "auth"),
serverManager: ServerManager.shared
)
.frame(width: 500)
.padding()
}
}
return PreviewWrapper()
}

View file

@ -6,8 +6,6 @@ import SwiftUI
struct GeneralSettingsView: View {
@AppStorage("autostart")
private var autostart = false
@AppStorage("showNotifications")
private var showNotifications = true
@AppStorage(AppConstants.UserDefaultsKeys.updateChannel)
private var updateChannelRaw = UpdateChannel.stable.rawValue
@AppStorage(AppConstants.UserDefaultsKeys.showInDock)
@ -16,8 +14,10 @@ struct GeneralSettingsView: View {
private var preventSleepWhenRunning = true
@Environment(ConfigManager.self) private var configManager
@Environment(SystemPermissionManager.self) private var permissionManager
@State private var isCheckingForUpdates = false
@State private var permissionUpdateTrigger = 0
private let startupManager = StartupManager()
private let logger = Logger(subsystem: BundleIdentifiers.loggerSubsystem, category: "GeneralSettings")
@ -26,10 +26,20 @@ struct GeneralSettingsView: View {
UpdateChannel(rawValue: updateChannelRaw) ?? .stable
}
private func updateNotificationPreferences() {
// Load current preferences from ConfigManager and notify the service
let prefs = NotificationService.NotificationPreferences(fromConfig: configManager)
NotificationService.shared.updatePreferences(prefs)
// MARK: - Helper Properties
// IMPORTANT: These computed properties ensure the UI always shows current permission state.
// The permissionUpdateTrigger dependency forces SwiftUI to re-evaluate these properties
// when permissions change. Without this, the UI would not update when permissions are
// granted in System Settings while this view is visible.
private var hasAppleScriptPermission: Bool {
_ = permissionUpdateTrigger
return permissionManager.hasPermission(.appleScript)
}
private var hasAccessibilityPermission: Bool {
_ = permissionUpdateTrigger
return permissionManager.hasPermission(.accessibility)
}
var body: some View {
@ -66,114 +76,6 @@ struct GeneralSettingsView: View {
}
}
// Show Session Notifications
VStack(alignment: .leading, spacing: 4) {
Toggle("Show Session Notifications", isOn: $showNotifications)
.onChange(of: showNotifications) { _, newValue in
// Ensure NotificationService starts/stops based on the toggle
if newValue {
Task {
// Request permissions and show test notification
let granted = await NotificationService.shared
.requestPermissionAndShowTestNotification()
if granted {
await NotificationService.shared.start()
} else {
// If permission denied, turn toggle back off
await MainActor.run {
showNotifications = false
// Show alert explaining the situation
let alert = NSAlert()
alert.messageText = "Notification Permission Required"
alert.informativeText = "VibeTunnel needs permission to show notifications. Please enable notifications for VibeTunnel in System Settings."
alert.alertStyle = .informational
alert.addButton(withTitle: "Open System Settings")
alert.addButton(withTitle: "Cancel")
if alert.runModal() == .alertFirstButtonReturn {
// Settings will already be open from the service
}
}
}
}
} else {
NotificationService.shared.stop()
}
}
Text("Display native macOS notifications for session and command events.")
.font(.caption)
.foregroundStyle(.secondary)
if showNotifications {
VStack(alignment: .leading, spacing: 6) {
Text("Notify me for:")
.font(.caption)
.foregroundStyle(.secondary)
.padding(.leading, 20)
.padding(.top, 4)
VStack(alignment: .leading, spacing: 4) {
Toggle("Session starts", isOn: Binding(
get: { configManager.notificationSessionStart },
set: { newValue in
configManager.notificationSessionStart = newValue
updateNotificationPreferences()
}
))
.toggleStyle(.checkbox)
Toggle("Session ends", isOn: Binding(
get: { configManager.notificationSessionExit },
set: { newValue in
configManager.notificationSessionExit = newValue
updateNotificationPreferences()
}
))
.toggleStyle(.checkbox)
Toggle("Commands complete (> 3 seconds)", isOn: Binding(
get: { configManager.notificationCommandCompletion },
set: { newValue in
configManager.notificationCommandCompletion = newValue
updateNotificationPreferences()
}
))
.toggleStyle(.checkbox)
Toggle("Commands fail", isOn: Binding(
get: { configManager.notificationCommandError },
set: { newValue in
configManager.notificationCommandError = newValue
updateNotificationPreferences()
}
))
.toggleStyle(.checkbox)
Toggle("Terminal bell (\u{0007})", isOn: Binding(
get: { configManager.notificationBell },
set: { newValue in
configManager.notificationBell = newValue
updateNotificationPreferences()
}
))
.toggleStyle(.checkbox)
Toggle("Claude turn notifications", isOn: Binding(
get: { configManager.notificationClaudeTurn },
set: { newValue in
configManager.notificationClaudeTurn = newValue
updateNotificationPreferences()
}
))
.toggleStyle(.checkbox)
}
.padding(.leading, 20)
}
}
}
// Prevent Sleep
VStack(alignment: .leading, spacing: 4) {
Toggle("Prevent Sleep When Running", isOn: $preventSleepWhenRunning)
@ -185,6 +87,13 @@ struct GeneralSettingsView: View {
Text("Application")
.font(.headline)
}
// System Permissions section (moved from Security)
PermissionsSection(
hasAppleScriptPermission: hasAppleScriptPermission,
hasAccessibilityPermission: hasAccessibilityPermission,
permissionManager: permissionManager
)
}
.formStyle(.grouped)
.scrollContentBackground(.hidden)
@ -193,6 +102,19 @@ struct GeneralSettingsView: View {
.task {
// Sync launch at login status
autostart = startupManager.isLaunchAtLoginEnabled
// Check permissions before first render to avoid UI flashing
await permissionManager.checkAllPermissions()
}
.onAppear {
// Register for continuous monitoring
permissionManager.registerForMonitoring()
}
.onDisappear {
permissionManager.unregisterFromMonitoring()
}
.onReceive(NotificationCenter.default.publisher(for: .permissionsUpdated)) { _ in
// Increment trigger to force computed property re-evaluation
permissionUpdateTrigger += 1
}
}
@ -243,3 +165,4 @@ struct GeneralSettingsView: View {
}
}
}

View file

@ -0,0 +1,241 @@
import AppKit
import os.log
import SwiftUI
private let logger = Logger(subsystem: BundleIdentifiers.loggerSubsystem, category: "NotificationSettings")
/// Settings view for managing notification preferences
struct NotificationSettingsView: View {
@AppStorage("showNotifications")
private var showNotifications = true
@Environment(ConfigManager.self) private var configManager
@Environment(NotificationService.self) private var notificationService
@State private var isTestingNotification = false
@State private var showingPermissionAlert = false
private func updateNotificationPreferences() {
// Load current preferences from ConfigManager and notify the service
let prefs = NotificationService.NotificationPreferences(fromConfig: configManager)
notificationService.updatePreferences(prefs)
// Also update the enabled state in ConfigManager
configManager.notificationsEnabled = showNotifications
}
var body: some View {
NavigationStack {
@Bindable var bindableConfig = configManager
Form {
// Master toggle section
Section {
VStack(alignment: .leading, spacing: 12) {
Toggle("Show Session Notifications", isOn: $showNotifications)
.controlSize(.large)
.onChange(of: showNotifications) { _, newValue in
// Update ConfigManager's notificationsEnabled to match
configManager.notificationsEnabled = newValue
// Ensure NotificationService starts/stops based on the toggle
if newValue {
Task {
// Request permissions and show test notification
let granted = await notificationService
.requestPermissionAndShowTestNotification()
if granted {
await notificationService.start()
} else {
// If permission denied, turn toggle back off
await MainActor.run {
showNotifications = false
configManager.notificationsEnabled = false
showingPermissionAlert = true
}
}
}
} else {
notificationService.stop()
}
}
Text("Display native macOS notifications for session and command events")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(.vertical, 8)
}
// Notification types section
if showNotifications {
Section {
NotificationToggleRow(
title: "Session starts",
description: "When a new session starts (useful for shared terminals)",
isOn: $bindableConfig.notificationSessionStart
)
.onChange(of: bindableConfig.notificationSessionStart) { _, _ in
updateNotificationPreferences()
}
NotificationToggleRow(
title: "Session ends",
description: "When a session terminates or crashes (shows exit code)",
isOn: $bindableConfig.notificationSessionExit
)
.onChange(of: bindableConfig.notificationSessionExit) { _, _ in
updateNotificationPreferences()
}
NotificationToggleRow(
title: "Commands fail",
description: "When commands fail with non-zero exit codes",
isOn: $bindableConfig.notificationCommandError
)
.onChange(of: bindableConfig.notificationCommandError) { _, _ in
updateNotificationPreferences()
}
NotificationToggleRow(
title: "Commands complete (> 3 seconds)",
description: "When commands taking >3 seconds finish (builds, tests, etc.)",
isOn: $bindableConfig.notificationCommandCompletion
)
.onChange(of: bindableConfig.notificationCommandCompletion) { _, _ in
updateNotificationPreferences()
}
NotificationToggleRow(
title: "Terminal bell (🔔)",
description: "Terminal bell (^G) from vim, IRC mentions, completion sounds",
isOn: $bindableConfig.notificationBell
)
.onChange(of: bindableConfig.notificationBell) { _, _ in
updateNotificationPreferences()
}
NotificationToggleRow(
title: "Claude turn notifications",
description: "When Claude AI finishes responding and awaits input",
isOn: $bindableConfig.notificationClaudeTurn
)
.onChange(of: bindableConfig.notificationClaudeTurn) { _, _ in
updateNotificationPreferences()
}
} header: {
Text("Notification Types")
.font(.headline)
}
// Behavior section
Section {
VStack(spacing: 12) {
Toggle("Play sound", isOn: $bindableConfig.notificationSoundEnabled)
.onChange(of: bindableConfig.notificationSoundEnabled) { _, _ in
updateNotificationPreferences()
}
Toggle("Show in Notification Center", isOn: $bindableConfig.showInNotificationCenter)
.onChange(of: bindableConfig.showInNotificationCenter) { _, _ in
updateNotificationPreferences()
}
}
} header: {
Text("Notification Behavior")
.font(.headline)
}
// Test section
Section {
VStack(alignment: .leading, spacing: 12) {
HStack {
Button("Test Notification") {
Task {
isTestingNotification = true
await notificationService.sendGenericNotification(
title: "VibeTunnel Test",
body: "This is a test notification to verify your settings are working correctly."
)
// Reset button state after a delay
try? await Task.sleep(nanoseconds: 1_000_000_000)
isTestingNotification = false
}
}
.buttonStyle(.bordered)
.disabled(!showNotifications || isTestingNotification)
if isTestingNotification {
ProgressView()
.scaleEffect(0.7)
.frame(width: 16, height: 16)
}
Spacer()
}
HStack {
Button("Open System Settings") {
notificationService.openNotificationSettings()
}
.buttonStyle(.link)
Spacer()
}
}
} header: {
Text("Actions")
.font(.headline)
}
}
}
.formStyle(.grouped)
.scrollContentBackground(.hidden)
.navigationTitle("Notification Settings")
.onAppear {
// Sync the AppStorage value with ConfigManager on first load
showNotifications = configManager.notificationsEnabled
}
}
.alert("Notification Permission Required", isPresented: $showingPermissionAlert) {
Button("Open System Settings") {
notificationService.openNotificationSettings()
}
Button("Cancel", role: .cancel) {}
} message: {
Text(
"VibeTunnel needs permission to show notifications. Please enable notifications for VibeTunnel in System Settings."
)
}
}
}
/// Reusable component for notification toggle rows with descriptions
struct NotificationToggleRow: View {
let title: String
let description: String
@Binding var isOn: Bool
var body: some View {
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.body)
Text(description)
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
Toggle("", isOn: $isOn)
.labelsHidden()
}
.padding(.vertical, 6)
}
}
#Preview {
NotificationSettingsView()
.environment(ConfigManager.shared)
.environment(NotificationService.shared)
.frame(width: 560, height: 700)
}

View file

@ -0,0 +1,131 @@
import SwiftUI
/// System permissions configuration section
struct PermissionsSection: View {
let hasAppleScriptPermission: Bool
let hasAccessibilityPermission: Bool
let permissionManager: SystemPermissionManager
var body: some View {
Section {
// Automation permission
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("Terminal Automation")
.font(.body)
Text("Required to launch and control terminal applications.")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
if hasAppleScriptPermission {
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text("Granted")
.foregroundColor(.secondary)
}
.font(.caption)
.padding(.horizontal, 10)
.padding(.vertical, 2)
.frame(height: 22) // Match small button height
.contextMenu {
Button("Refresh Status") {
permissionManager.forcePermissionRecheck()
}
Button("Open System Settings...") {
permissionManager.requestPermission(.appleScript)
}
}
} else {
Button("Grant Permission") {
permissionManager.requestPermission(.appleScript)
}
.buttonStyle(.bordered)
.controlSize(.small)
}
}
// Accessibility permission
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("Accessibility")
.font(.body)
Text("Required to enter terminal startup commands.")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
if hasAccessibilityPermission {
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text("Granted")
.foregroundColor(.secondary)
}
.font(.caption)
.padding(.horizontal, 10)
.padding(.vertical, 2)
.frame(height: 22) // Match small button height
.contextMenu {
Button("Refresh Status") {
permissionManager.forcePermissionRecheck()
}
Button("Open System Settings...") {
permissionManager.requestPermission(.accessibility)
}
}
} else {
Button("Grant Permission") {
permissionManager.requestPermission(.accessibility)
}
.buttonStyle(.bordered)
.controlSize(.small)
}
}
} header: {
Text("System Permissions")
.font(.headline)
} footer: {
if hasAppleScriptPermission && hasAccessibilityPermission {
Text(
"All permissions granted. VibeTunnel has full functionality."
)
.font(.caption)
.frame(maxWidth: .infinity)
.multilineTextAlignment(.center)
.foregroundColor(.green)
} else {
Text(
"Terminals can be captured without permissions, however new sessions won't load."
)
.font(.caption)
.frame(maxWidth: .infinity)
.multilineTextAlignment(.center)
}
}
}
}
#Preview {
struct PreviewWrapper: View {
@State var hasAppleScript = true
@State var hasAccessibility = false
var body: some View {
PermissionsSection(
hasAppleScriptPermission: hasAppleScript,
hasAccessibilityPermission: hasAccessibility,
permissionManager: SystemPermissionManager.shared
)
.frame(width: 500)
.padding()
}
}
return PreviewWrapper()
}

View file

@ -12,6 +12,10 @@ struct RemoteAccessSettingsView: View {
private var serverPort = "4020"
@AppStorage(AppConstants.UserDefaultsKeys.dashboardAccessMode)
private var accessModeString = AppConstants.Defaults.dashboardAccessMode
@AppStorage(AppConstants.UserDefaultsKeys.authenticationMode)
private var authModeString = "os"
@State private var authMode: AuthenticationMode = .osAuth
@Environment(NgrokService.self)
private var ngrokService
@ -43,6 +47,14 @@ struct RemoteAccessSettingsView: View {
var body: some View {
NavigationStack {
Form {
// Authentication section (moved from Security)
AuthenticationSection(
authMode: $authMode,
enableSSHKeys: .constant(authMode == .sshKeys || authMode == .both),
logger: logger,
serverManager: serverManager
)
TailscaleIntegrationSection(
tailscaleService: tailscaleService,
serverPort: serverPort,
@ -78,6 +90,9 @@ struct RemoteAccessSettingsView: View {
.onAppear {
onAppearSetup()
updateLocalIPAddress()
// Initialize authentication mode from stored value
let storedMode = UserDefaults.standard.string(forKey: AppConstants.UserDefaultsKeys.authenticationMode) ?? "os"
authMode = AuthenticationMode(rawValue: storedMode) ?? .osAuth
}
}
.alert("ngrok Authentication Required", isPresented: $showingAuthTokenAlert) {

View file

@ -6,10 +6,10 @@ import Foundation
/// with associated display names and SF Symbol icons for the tab bar.
enum SettingsTab: String, CaseIterable {
case general
case notifications
case quickStart
case dashboard
case remoteAccess
case securityPermissions
case advanced
case debug
case about
@ -17,10 +17,10 @@ enum SettingsTab: String, CaseIterable {
var displayName: String {
switch self {
case .general: "General"
case .notifications: "Notifications"
case .quickStart: "Quick Start"
case .dashboard: "Dashboard"
case .remoteAccess: "Remote"
case .securityPermissions: "Security"
case .advanced: "Advanced"
case .debug: "Debug"
case .about: "About"
@ -30,10 +30,10 @@ enum SettingsTab: String, CaseIterable {
var icon: String {
switch self {
case .general: "gear"
case .notifications: "bell.badge"
case .quickStart: "bolt.fill"
case .dashboard: "server.rack"
case .remoteAccess: "network"
case .securityPermissions: "lock.shield"
case .advanced: "gearshape.2"
case .debug: "hammer"
case .about: "info.circle"

View file

@ -14,17 +14,17 @@ struct SettingsView: View {
// MARK: - Constants
private enum Layout {
static let defaultTabSize = CGSize(width: 520, height: 710)
static let fallbackTabSize = CGSize(width: 520, height: 450)
static let defaultTabSize = CGSize(width: 550, height: 710)
static let fallbackTabSize = CGSize(width: 550, height: 450)
}
/// Define ideal sizes for each tab
private let tabSizes: [SettingsTab: CGSize] = [
.general: Layout.defaultTabSize,
.notifications: Layout.defaultTabSize,
.quickStart: Layout.defaultTabSize,
.dashboard: Layout.defaultTabSize,
.remoteAccess: Layout.defaultTabSize,
.securityPermissions: Layout.defaultTabSize,
.advanced: Layout.defaultTabSize,
.debug: Layout.defaultTabSize,
.about: Layout.defaultTabSize
@ -38,6 +38,12 @@ struct SettingsView: View {
}
.tag(SettingsTab.general)
NotificationSettingsView()
.tabItem {
Label(SettingsTab.notifications.displayName, systemImage: SettingsTab.notifications.icon)
}
.tag(SettingsTab.notifications)
QuickStartSettingsView()
.tabItem {
Label(SettingsTab.quickStart.displayName, systemImage: SettingsTab.quickStart.icon)
@ -56,15 +62,6 @@ struct SettingsView: View {
}
.tag(SettingsTab.remoteAccess)
SecurityPermissionsSettingsView()
.tabItem {
Label(
SettingsTab.securityPermissions.displayName,
systemImage: SettingsTab.securityPermissions.icon
)
}
.tag(SettingsTab.securityPermissions)
AdvancedSettingsView()
.tabItem {
Label(SettingsTab.advanced.displayName, systemImage: SettingsTab.advanced.icon)

View file

@ -27,6 +27,7 @@ struct VibeTunnelApp: App {
@State var sessionService: SessionService?
@State var worktreeService = WorktreeService(serverManager: ServerManager.shared)
@State var configManager = ConfigManager.shared
@State var notificationService = NotificationService.shared
init() {
// Connect the app delegate to this app instance
@ -57,6 +58,7 @@ struct VibeTunnelApp: App {
.environment(repositoryDiscoveryService)
.environment(configManager)
.environment(worktreeService)
.environment(notificationService)
}
.windowResizability(.contentSize)
.defaultSize(width: 580, height: 480)
@ -83,6 +85,7 @@ struct VibeTunnelApp: App {
sessionMonitor: sessionMonitor
))
.environment(worktreeService)
.environment(notificationService)
} else {
Text("Session not found")
.frame(width: 400, height: 300)
@ -109,6 +112,7 @@ struct VibeTunnelApp: App {
sessionMonitor: sessionMonitor
))
.environment(worktreeService)
.environment(notificationService)
}
.commands {
CommandGroup(after: .appInfo) {