vibetunnel/VibeTunnel/Core/Services/SessionMonitor.swift
Peter Steinberger 70a8da5235 feat: enhance UI and automatic update handling
- Fix session count display to show on single line in menu bar
- Add conditional compilation to disable automatic updates in DEBUG mode
- Add "Open Dashboard" menu item that opens internal server URL
- Convert Help menu from popover to native macOS submenu style
- Enable automatic update downloads in Sparkle configuration
- Increase Advanced Settings tab height from 400 to 500 pixels
- Add Tailscale recommendation with clickable markdown link
- Fix Sendable protocol conformance issues throughout codebase
- Add ApplicationMover utility for app installation location management

These changes improve the overall user experience by making the UI more
intuitive and ensuring automatic updates work correctly in production
while being disabled during development.
2025-06-16 05:53:08 +02:00

139 lines
4.2 KiB
Swift

import Foundation
import Observation
/// Monitors tty-fwd sessions and provides real-time session count
@MainActor
@Observable
class SessionMonitor {
static let shared = SessionMonitor()
var sessionCount: Int = 0
var sessions: [String: SessionInfo] = [:]
var lastError: String?
private var monitoringTask: Task<Void, Never>?
private let refreshInterval: TimeInterval = 5.0 // Check every 5 seconds
private var serverPort: Int
/// Information about a terminal session
struct SessionInfo: Codable {
let cmdline: [String]
let cwd: String
let exitCode: Int?
let name: String
let pid: Int
let startedAt: String
let status: String
let stdin: String
let streamOut: String
enum CodingKeys: String, CodingKey {
case cmdline
case cwd
case name
case pid
case status
case stdin
case exitCode = "exit_code"
case startedAt = "started_at"
case streamOut = "stream-out"
}
var isRunning: Bool {
status == "running"
}
}
private init() {
let port = UserDefaults.standard.integer(forKey: "serverPort")
self.serverPort = port > 0 ? port : 4_020
}
func startMonitoring() {
stopMonitoring()
// Update port from UserDefaults in case it changed
let port = UserDefaults.standard.integer(forKey: "serverPort")
self.serverPort = port > 0 ? port : 4_020
// Start monitoring task
monitoringTask = Task {
// Initial fetch
await fetchSessions()
// Set up periodic fetching
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(refreshInterval))
if !Task.isCancelled {
await fetchSessions()
}
}
}
}
func stopMonitoring() {
monitoringTask?.cancel()
monitoringTask = nil
}
@MainActor
private func fetchSessions() async {
do {
// First check if server is running
let healthURL = URL(string: "http://127.0.0.1:\(serverPort)/health")!
let healthRequest = URLRequest(url: healthURL, timeoutInterval: 2.0)
do {
let (_, healthResponse) = try await URLSession.shared.data(for: healthRequest)
guard let httpResponse = healthResponse as? HTTPURLResponse,
httpResponse.statusCode == 200
else {
// Server not running
self.sessions = [:]
self.sessionCount = 0
self.lastError = nil
return
}
} catch {
// Server not reachable
self.sessions = [:]
self.sessionCount = 0
self.lastError = nil
return
}
// Server is running, fetch sessions
let url = URL(string: "http://127.0.0.1:\(serverPort)/sessions")!
let request = URLRequest(url: url, timeoutInterval: 5.0)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200
else {
self.lastError = "Failed to fetch sessions"
return
}
// Parse JSON response
let sessionsData = try JSONDecoder().decode([String: SessionInfo].self, from: data)
self.sessions = sessionsData
// Count only running sessions
self.sessionCount = sessionsData.values.count(where: { $0.isRunning })
self.lastError = nil
} catch {
// Don't set error for connection issues when server is likely not running
if !(error is URLError) {
self.lastError = "Error fetching sessions: \(error.localizedDescription)"
}
// Clear sessions on error
self.sessions = [:]
self.sessionCount = 0
}
}
func refreshNow() async {
await fetchSessions()
}
}