mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-06-29 05:39:31 +00:00
- Fix notification preference tests to match default enabled: false - Fix PtyManager initialization in integration tests - Fix path splitting tests for macOS URL behavior - Add hour formatting to duration display (1h 23m 45s format) - Fix non-optional URL nil comparison warning - Fix force unwrapping warning in EventSource.swift - Apply SwiftFormat formatting fixes - Update test expectations to match actual behavior
385 lines
13 KiB
Swift
385 lines
13 KiB
Swift
import Foundation
|
|
|
|
/// Types of server events that can be received from the VibeTunnel server.
|
|
///
|
|
/// `ServerEventType` defines all possible event types that flow through the Server-Sent Events (SSE)
|
|
/// connection between the VibeTunnel server and the macOS app. Each event type corresponds to
|
|
/// a specific terminal session lifecycle event or user interaction.
|
|
///
|
|
/// ## Topics
|
|
///
|
|
/// ### Event Categories
|
|
///
|
|
/// - ``sessionStart``: Terminal session creation events
|
|
/// - ``sessionExit``: Terminal session termination events
|
|
/// - ``commandFinished``: Command completion events
|
|
/// - ``commandError``: Command failure events
|
|
/// - ``bell``: Terminal bell notifications
|
|
/// - ``claudeTurn``: AI assistant interaction events
|
|
/// - ``connected``: Connection establishment events
|
|
///
|
|
/// ### Event Properties
|
|
///
|
|
/// - ``description``: Human-readable event descriptions
|
|
/// - ``shouldNotify``: Notification eligibility
|
|
enum ServerEventType: String, Codable, CaseIterable {
|
|
/// Indicates a new terminal session has been started.
|
|
case sessionStart = "session-start"
|
|
|
|
/// Indicates a terminal session has ended.
|
|
case sessionExit = "session-exit"
|
|
|
|
/// Indicates a command has finished executing successfully.
|
|
case commandFinished = "command-finished"
|
|
|
|
/// Indicates a command has failed with an error.
|
|
case commandError = "command-error"
|
|
|
|
/// Indicates a terminal bell character was received.
|
|
case bell = "bell"
|
|
|
|
/// Indicates Claude (AI assistant) has finished responding and it's the user's turn.
|
|
case claudeTurn = "claude-turn"
|
|
|
|
/// Indicates the SSE connection has been established.
|
|
case connected = "connected"
|
|
|
|
/// Returns a human-readable description of the event type.
|
|
///
|
|
/// This property provides user-friendly labels suitable for display in
|
|
/// notifications and UI elements.
|
|
var description: String {
|
|
switch self {
|
|
case .sessionStart:
|
|
"Session Started"
|
|
case .sessionExit:
|
|
"Session Ended"
|
|
case .commandFinished:
|
|
"Command Completed"
|
|
case .commandError:
|
|
"Command Error"
|
|
case .bell:
|
|
"Terminal Bell"
|
|
case .claudeTurn:
|
|
"Your Turn"
|
|
case .connected:
|
|
"Connected"
|
|
}
|
|
}
|
|
|
|
/// Determines whether this event type should trigger a user notification.
|
|
///
|
|
/// This property helps filter which events should result in system notifications.
|
|
/// Currently, session lifecycle events and Claude turn events are eligible for
|
|
/// notifications, while command completion and system events are not.
|
|
///
|
|
/// - Returns: `true` if the event should trigger a notification, `false` otherwise.
|
|
var shouldNotify: Bool {
|
|
switch self {
|
|
case .sessionStart, .sessionExit, .claudeTurn:
|
|
true
|
|
case .commandFinished, .commandError, .bell, .connected:
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents a server event received via Server-Sent Events (SSE).
|
|
///
|
|
/// `ServerEvent` encapsulates all the information about terminal session events that flow
|
|
/// from the VibeTunnel server to the macOS app. Each event carries contextual information
|
|
/// about what happened, when it happened, and which session it relates to.
|
|
///
|
|
/// ## Overview
|
|
///
|
|
/// Server events are the primary communication mechanism for real-time updates about
|
|
/// terminal sessions. They enable the macOS app to:
|
|
/// - Track session lifecycle (creation, termination)
|
|
/// - Monitor command execution and completion
|
|
/// - Detect AI assistant interactions
|
|
/// - Handle system notifications like terminal bells
|
|
///
|
|
/// ## Topics
|
|
///
|
|
/// ### Creating Events
|
|
///
|
|
/// - ``init(type:sessionId:sessionName:command:exitCode:duration:processInfo:message:timestamp:)``
|
|
/// - ``sessionStart(sessionId:sessionName:command:)``
|
|
/// - ``sessionExit(sessionId:sessionName:exitCode:)``
|
|
/// - ``commandFinished(sessionId:command:duration:exitCode:)``
|
|
/// - ``claudeTurn(sessionId:sessionName:)``
|
|
/// - ``bell(sessionId:)``
|
|
///
|
|
/// ### Event Properties
|
|
///
|
|
/// - ``type``: The type of event
|
|
/// - ``sessionId``: Associated session identifier
|
|
/// - ``sessionName``: Human-readable session name
|
|
/// - ``command``: Command that was executed
|
|
/// - ``exitCode``: Process exit code
|
|
/// - ``duration``: Execution duration in milliseconds
|
|
/// - ``processInfo``: Additional process information
|
|
/// - ``message``: Event message
|
|
/// - ``timestamp``: When the event occurred
|
|
///
|
|
/// ### Computed Properties
|
|
///
|
|
/// - ``displayName``: User-friendly name for display
|
|
/// - ``shouldNotify``: Whether to show a notification
|
|
/// - ``formattedDuration``: Human-readable duration
|
|
/// - ``formattedTimestamp``: Formatted timestamp
|
|
struct ServerEvent: Codable, Identifiable, Equatable {
|
|
/// Unique identifier for the event instance.
|
|
let id = UUID()
|
|
|
|
/// The type of server event.
|
|
let type: ServerEventType
|
|
|
|
/// The terminal session identifier this event relates to.
|
|
let sessionId: String?
|
|
|
|
/// Human-readable name of the session.
|
|
let sessionName: String?
|
|
|
|
/// The command that was executed (for command-related events).
|
|
let command: String?
|
|
|
|
/// The process exit code (for exit and error events).
|
|
let exitCode: Int?
|
|
|
|
/// Duration in milliseconds (for command completion events).
|
|
let duration: Int?
|
|
|
|
/// Additional process information.
|
|
let processInfo: String?
|
|
|
|
/// Optional message providing additional context.
|
|
let message: String?
|
|
|
|
/// When the event occurred.
|
|
let timestamp: Date
|
|
|
|
/// Creates a new server event with the specified properties.
|
|
///
|
|
/// - Parameters:
|
|
/// - type: The type of event.
|
|
/// - sessionId: Optional session identifier.
|
|
/// - sessionName: Optional human-readable session name.
|
|
/// - command: Optional command that was executed.
|
|
/// - exitCode: Optional process exit code.
|
|
/// - duration: Optional duration in milliseconds.
|
|
/// - processInfo: Optional additional process information.
|
|
/// - message: Optional contextual message.
|
|
/// - timestamp: When the event occurred (defaults to current time).
|
|
init(
|
|
type: ServerEventType,
|
|
sessionId: String? = nil,
|
|
sessionName: String? = nil,
|
|
command: String? = nil,
|
|
exitCode: Int? = nil,
|
|
duration: Int? = nil,
|
|
processInfo: String? = nil,
|
|
message: String? = nil,
|
|
timestamp: Date = Date()
|
|
) {
|
|
self.type = type
|
|
self.sessionId = sessionId
|
|
self.sessionName = sessionName
|
|
self.command = command
|
|
self.exitCode = exitCode
|
|
self.duration = duration
|
|
self.processInfo = processInfo
|
|
self.message = message
|
|
self.timestamp = timestamp
|
|
}
|
|
|
|
// MARK: - Convenience Initializers
|
|
|
|
/// Creates a session start event.
|
|
///
|
|
/// Use this convenience method when a new terminal session is created.
|
|
///
|
|
/// - Parameters:
|
|
/// - sessionId: The unique identifier for the session.
|
|
/// - sessionName: Optional human-readable name for the session.
|
|
/// - command: Optional command that started the session.
|
|
/// - Returns: A configured `ServerEvent` of type ``ServerEventType/sessionStart``.
|
|
static func sessionStart(sessionId: String, sessionName: String? = nil, command: String? = nil) -> Self {
|
|
Self(
|
|
type: .sessionStart,
|
|
sessionId: sessionId,
|
|
sessionName: sessionName,
|
|
command: command
|
|
)
|
|
}
|
|
|
|
/// Creates a session exit event.
|
|
///
|
|
/// Use this convenience method when a terminal session ends.
|
|
///
|
|
/// - Parameters:
|
|
/// - sessionId: The unique identifier for the session.
|
|
/// - sessionName: Optional human-readable name for the session.
|
|
/// - exitCode: Optional process exit code.
|
|
/// - Returns: A configured `ServerEvent` of type ``ServerEventType/sessionExit``.
|
|
static func sessionExit(sessionId: String, sessionName: String? = nil, exitCode: Int? = nil) -> Self {
|
|
Self(
|
|
type: .sessionExit,
|
|
sessionId: sessionId,
|
|
sessionName: sessionName,
|
|
exitCode: exitCode
|
|
)
|
|
}
|
|
|
|
/// Creates a command finished event.
|
|
///
|
|
/// Use this convenience method when a command completes execution.
|
|
///
|
|
/// - Parameters:
|
|
/// - sessionId: The unique identifier for the session.
|
|
/// - command: The command that was executed.
|
|
/// - duration: Execution time in milliseconds.
|
|
/// - exitCode: Optional process exit code.
|
|
/// - Returns: A configured `ServerEvent` of type ``ServerEventType/commandFinished``.
|
|
static func commandFinished(
|
|
sessionId: String,
|
|
command: String,
|
|
duration: Int,
|
|
exitCode: Int? = nil
|
|
)
|
|
-> Self
|
|
{
|
|
Self(
|
|
type: .commandFinished,
|
|
sessionId: sessionId,
|
|
command: command,
|
|
exitCode: exitCode,
|
|
duration: duration
|
|
)
|
|
}
|
|
|
|
/// Creates a command error event.
|
|
///
|
|
/// Use this convenience method when a command fails with a non-zero exit code.
|
|
///
|
|
/// - Parameters:
|
|
/// - sessionId: The unique identifier for the session.
|
|
/// - command: The command that failed.
|
|
/// - exitCode: The process exit code.
|
|
/// - duration: Optional execution time in milliseconds.
|
|
/// - Returns: A configured `ServerEvent` of type ``ServerEventType/commandError``.
|
|
static func commandError(sessionId: String, command: String, exitCode: Int, duration: Int? = nil) -> Self {
|
|
Self(
|
|
type: .commandError,
|
|
sessionId: sessionId,
|
|
command: command,
|
|
exitCode: exitCode,
|
|
duration: duration
|
|
)
|
|
}
|
|
|
|
/// Creates a Claude turn event.
|
|
///
|
|
/// Use this convenience method when Claude (AI assistant) finishes responding
|
|
/// and it's the user's turn to interact.
|
|
///
|
|
/// - Parameters:
|
|
/// - sessionId: The unique identifier for the session.
|
|
/// - sessionName: Optional human-readable name for the session.
|
|
/// - Returns: A configured `ServerEvent` of type ``ServerEventType/claudeTurn``.
|
|
static func claudeTurn(sessionId: String, sessionName: String? = nil) -> Self {
|
|
Self(
|
|
type: .claudeTurn,
|
|
sessionId: sessionId,
|
|
sessionName: sessionName,
|
|
message: "Claude has finished responding"
|
|
)
|
|
}
|
|
|
|
/// Creates a bell event.
|
|
///
|
|
/// Use this convenience method when a terminal bell character is received.
|
|
///
|
|
/// - Parameter sessionId: The unique identifier for the session.
|
|
/// - Returns: A configured `ServerEvent` of type ``ServerEventType/bell``.
|
|
static func bell(sessionId: String) -> Self {
|
|
Self(
|
|
type: .bell,
|
|
sessionId: sessionId,
|
|
message: "Terminal bell"
|
|
)
|
|
}
|
|
|
|
// MARK: - Computed Properties
|
|
|
|
/// Returns a user-friendly display name for the event.
|
|
///
|
|
/// The display name is determined by the following priority:
|
|
/// 1. Session name (if available)
|
|
/// 2. Command (if available)
|
|
/// 3. Session ID (if available)
|
|
/// 4. "Unknown Session" as fallback
|
|
var displayName: String {
|
|
sessionName ?? command ?? sessionId ?? "Unknown Session"
|
|
}
|
|
|
|
/// Determines whether this event should trigger a user notification.
|
|
///
|
|
/// This delegates to the event type's ``ServerEventType/shouldNotify`` property.
|
|
var shouldNotify: Bool {
|
|
type.shouldNotify
|
|
}
|
|
|
|
/// Returns a human-readable formatted duration string.
|
|
///
|
|
/// The duration is formatted based on its length:
|
|
/// - Less than 1 second: Shows milliseconds (e.g., "500ms")
|
|
/// - Less than 1 minute: Shows seconds with one decimal (e.g., "2.5s")
|
|
/// - Less than 1 hour: Shows minutes and seconds (e.g., "2m 5s")
|
|
/// - 1 hour or more: Shows hours, minutes and seconds (e.g., "1h 2m 5s")
|
|
///
|
|
/// - Returns: A formatted duration string, or `nil` if no duration is set.
|
|
var formattedDuration: String? {
|
|
guard let duration else { return nil }
|
|
|
|
if duration < 1_000 {
|
|
return "\(duration)ms"
|
|
} else if duration < 60_000 {
|
|
return String(format: "%.1fs", Double(duration) / 1_000.0)
|
|
} else if duration < 3_600_000 {
|
|
let minutes = duration / 60_000
|
|
let seconds = (duration % 60_000) / 1_000
|
|
return "\(minutes)m \(seconds)s"
|
|
} else {
|
|
let hours = duration / 3_600_000
|
|
let minutes = (duration % 3_600_000) / 60_000
|
|
let seconds = (duration % 60_000) / 1_000
|
|
return "\(hours)h \(minutes)m \(seconds)s"
|
|
}
|
|
}
|
|
|
|
/// Returns a formatted timestamp string.
|
|
///
|
|
/// The timestamp is formatted using medium time style, which typically
|
|
/// shows hours, minutes, and seconds (e.g., "3:45:32 PM").
|
|
var formattedTimestamp: String {
|
|
let formatter = DateFormatter()
|
|
formatter.timeStyle = .medium
|
|
return formatter.string(from: timestamp)
|
|
}
|
|
|
|
// MARK: - Codable
|
|
|
|
/// Coding keys to exclude `id` from encoding/decoding since it's auto-generated
|
|
enum CodingKeys: String, CodingKey {
|
|
case type
|
|
case sessionId
|
|
case sessionName
|
|
case command
|
|
case exitCode
|
|
case duration
|
|
case processInfo
|
|
case message
|
|
case timestamp
|
|
}
|
|
}
|