mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-25 09:25:50 +00:00
Add SSE connection status indicator to notification settings
- Show real-time connection status with colored indicator - Display 'Event Stream: Connected/Disconnected' on separate row - Add warning message when notifications enabled but connection is down - Update connection status via NotificationCenter events - Add debugging logs to EventSource for better troubleshooting
This commit is contained in:
parent
5083cb5c1e
commit
dad2f3380e
13 changed files with 261 additions and 116 deletions
|
|
@ -2,7 +2,9 @@
|
|||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(pnpm test:*)",
|
||||
"Bash(rg:*)"
|
||||
"Bash(rg:*)",
|
||||
"Bash(./scripts/vtlog.sh:*)",
|
||||
"Bash(ls:*)"
|
||||
],
|
||||
"deny": []
|
||||
},
|
||||
|
|
|
|||
|
|
@ -282,13 +282,19 @@ VibeTunnel includes a powerful log viewing utility for debugging and monitoring:
|
|||
|
||||
**Common usage**:
|
||||
```bash
|
||||
./scripts/vtlog.sh -f # Follow logs in real-time
|
||||
# View recent logs (RECOMMENDED for debugging):
|
||||
./scripts/vtlog.sh -n 200 # Show last 200 lines
|
||||
./scripts/vtlog.sh -n 500 -s "error" # Search last 500 lines for "error"
|
||||
./scripts/vtlog.sh -e # Show only errors
|
||||
./scripts/vtlog.sh -c ServerManager # Show logs from specific component
|
||||
./scripts/vtlog.sh --server -e # Show server errors
|
||||
|
||||
# Follow logs in real-time (AVOID in Claude Code - causes timeouts):
|
||||
./scripts/vtlog.sh -f # Follow logs in real-time - DO NOT USE for checking existing logs
|
||||
```
|
||||
|
||||
**Important for Claude Code**: Always use `-n` to check a specific number of recent log lines. Do NOT use `-f` (follow mode) as it will block and timeout after 2 minutes. Follow mode is only useful when monitoring logs in a separate terminal while performing actions.
|
||||
|
||||
**Log prefixes**:
|
||||
- `[FE]` - Frontend (browser) logs
|
||||
- `[SRV]` - Server-side logs from Node.js/Bun
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ extension Notification.Name {
|
|||
// MARK: - Welcome
|
||||
|
||||
static let showWelcomeScreen = Notification.Name("showWelcomeScreen")
|
||||
|
||||
// MARK: - Services
|
||||
|
||||
static let notificationServiceConnectionChanged = Notification.Name("notificationServiceConnectionChanged")
|
||||
}
|
||||
|
||||
/// Notification categories for user notifications.
|
||||
|
|
|
|||
|
|
@ -72,6 +72,11 @@ final class BunServer {
|
|||
return localAuthToken
|
||||
}
|
||||
|
||||
/// Get the current authentication mode
|
||||
var authMode: String {
|
||||
AuthConfig.current().mode
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init() {
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ final class EventSource: NSObject {
|
|||
}
|
||||
|
||||
// Dispatch event
|
||||
logger.debug("🎯 Dispatching event - type: \(event.event ?? "default"), data: \(event.data ?? "none")")
|
||||
DispatchQueue.main.async {
|
||||
self.onMessage?(event)
|
||||
}
|
||||
|
|
@ -201,8 +202,12 @@ extension EventSource: URLSessionDataDelegate {
|
|||
}
|
||||
|
||||
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
||||
guard let text = String(data: data, encoding: .utf8) else { return }
|
||||
guard let text = String(data: data, encoding: .utf8) else {
|
||||
logger.error("Failed to decode data as UTF-8")
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug("📨 EventSource received data: \(text)")
|
||||
buffer += text
|
||||
processBuffer()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ final class NotificationService: NSObject {
|
|||
private var recentlyNotifiedSessions = Set<String>()
|
||||
private var notificationCleanupTimer: Timer?
|
||||
|
||||
/// Public property to check SSE connection status
|
||||
var isSSEConnected: Bool { isConnected }
|
||||
|
||||
/// Notification types that can be enabled/disabled
|
||||
struct NotificationPreferences {
|
||||
var sessionStart: Bool
|
||||
|
|
@ -63,14 +66,50 @@ final class NotificationService: NSObject {
|
|||
|
||||
/// Start monitoring server events
|
||||
func start() async {
|
||||
logger.info("🚀 NotificationService.start() called")
|
||||
guard serverManager.isRunning else {
|
||||
logger.warning("🔴 Server not running, cannot start notification service")
|
||||
return
|
||||
}
|
||||
|
||||
logger.info("🔔 Starting notification service...")
|
||||
logger.info("🔔 Starting notification service - server is running on port \(self.serverManager.port)")
|
||||
|
||||
connect()
|
||||
// Wait for Unix socket to be ready before connecting SSE
|
||||
// This ensures the server is fully ready to accept connections
|
||||
await MainActor.run {
|
||||
waitForUnixSocketAndConnect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for Unix socket ready notification then connect
|
||||
private func waitForUnixSocketAndConnect() {
|
||||
logger.info("⏳ Waiting for Unix socket ready notification...")
|
||||
|
||||
// Check if Unix socket is already connected
|
||||
if SharedUnixSocketManager.shared.isConnected {
|
||||
logger.info("✅ Unix socket already connected, connecting to SSE immediately")
|
||||
connect()
|
||||
return
|
||||
}
|
||||
|
||||
// Listen for Unix socket ready notification
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: SharedUnixSocketManager.unixSocketReadyNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
Task { @MainActor [weak self] in
|
||||
self?.logger.info("✅ Unix socket ready notification received, connecting to SSE")
|
||||
self?.connect()
|
||||
|
||||
// Remove observer after first notification to prevent duplicate connections
|
||||
NotificationCenter.default.removeObserver(
|
||||
self as Any,
|
||||
name: SharedUnixSocketManager.unixSocketReadyNotification,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop monitoring server events
|
||||
|
|
@ -372,60 +411,49 @@ final class NotificationService: NSObject {
|
|||
// MARK: - Private Methods
|
||||
|
||||
private func setupNotifications() {
|
||||
// Listen for server state changes
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(serverStateChanged),
|
||||
name: .serverStateChanged,
|
||||
object: nil
|
||||
)
|
||||
// Note: We do NOT listen for server state changes here
|
||||
// Connection is managed explicitly via start() and stop() methods
|
||||
// This prevents dual-path connection attempts
|
||||
}
|
||||
|
||||
@objc
|
||||
private func serverStateChanged(_ notification: Notification) {
|
||||
if serverManager.isRunning {
|
||||
logger.info("🔔 Server started, initializing notification service...")
|
||||
// Delay connection to ensure server is ready
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds (increased delay)
|
||||
await MainActor.run {
|
||||
if serverManager.isRunning {
|
||||
logger.info("🔔 Server ready, connecting notification service...")
|
||||
connect()
|
||||
} else {
|
||||
logger.warning("🔴 Server stopped before notification service could connect")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.info("🔔 Server stopped, disconnecting notification service...")
|
||||
disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private func connect() {
|
||||
logger.info("🔌 NotificationService.connect() called - isConnected: \(self.isConnected)")
|
||||
guard !isConnected else {
|
||||
logger.info("Already connected to notification service")
|
||||
return
|
||||
}
|
||||
|
||||
guard let authToken = serverManager.localAuthToken else {
|
||||
logger.error("No auth token available for notification service")
|
||||
// When auth mode is "none", we can connect without a token.
|
||||
// In any other auth mode, a token is required for the local Mac app to connect.
|
||||
if serverManager.authMode != "none", serverManager.localAuthToken == nil {
|
||||
logger.error("No auth token available for notification service in auth mode '\(self.serverManager.authMode)'")
|
||||
return
|
||||
}
|
||||
|
||||
guard let url = URL(string: "http://localhost:\(serverManager.port)/api/events") else {
|
||||
logger.error("Invalid events URL")
|
||||
let eventsURL = "http://localhost:\(self.serverManager.port)/api/events"
|
||||
logger.info("📡 Attempting to connect to SSE endpoint: \(eventsURL)")
|
||||
|
||||
guard let url = URL(string: eventsURL) else {
|
||||
logger.error("Invalid events URL: \(eventsURL)")
|
||||
return
|
||||
}
|
||||
|
||||
// Create headers
|
||||
var headers: [String: String] = [
|
||||
"Authorization": "Bearer \(authToken)",
|
||||
"Accept": "text/event-stream",
|
||||
"Cache-Control": "no-cache"
|
||||
]
|
||||
|
||||
// Add authorization header if auth token is available.
|
||||
// When auth mode is "none", there's no token, and that's okay.
|
||||
if let authToken = serverManager.localAuthToken {
|
||||
headers["Authorization"] = "Bearer \(authToken)"
|
||||
logger.info("🔑 Using auth token for SSE connection")
|
||||
} else {
|
||||
logger.info("🔓 Connecting to SSE without an auth token (auth mode: '\(self.serverManager.authMode)')")
|
||||
}
|
||||
|
||||
// Add custom header to indicate this is the Mac app
|
||||
headers["X-VibeTunnel-Client"] = "mac-app"
|
||||
|
||||
|
|
@ -435,6 +463,8 @@ final class NotificationService: NSObject {
|
|||
Task { @MainActor in
|
||||
self?.logger.info("✅ Connected to notification event stream")
|
||||
self?.isConnected = true
|
||||
// Post notification for UI update
|
||||
NotificationCenter.default.post(name: .notificationServiceConnectionChanged, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -444,12 +474,15 @@ final class NotificationService: NSObject {
|
|||
self?.logger.error("❌ EventSource error: \(error)")
|
||||
}
|
||||
self?.isConnected = false
|
||||
// Post notification for UI update
|
||||
NotificationCenter.default.post(name: .notificationServiceConnectionChanged, object: nil)
|
||||
// Don't reconnect here - let server state changes trigger reconnection
|
||||
}
|
||||
}
|
||||
|
||||
eventSource?.onMessage = { [weak self] event in
|
||||
Task { @MainActor in
|
||||
self?.logger.info("🎯 EventSource onMessage fired! Event type: \(event.event ?? "default"), Has data: \(event.data != nil)")
|
||||
self?.handleEvent(event)
|
||||
}
|
||||
}
|
||||
|
|
@ -462,12 +495,19 @@ final class NotificationService: NSObject {
|
|||
eventSource = nil
|
||||
isConnected = false
|
||||
logger.info("Disconnected from notification service")
|
||||
// Post notification for UI update
|
||||
NotificationCenter.default.post(name: .notificationServiceConnectionChanged, object: nil)
|
||||
}
|
||||
|
||||
private func handleEvent(_ event: Event) {
|
||||
guard let data = event.data else { return }
|
||||
guard let data = event.data else {
|
||||
logger.warning("Received event with no data")
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug("📨 Received event: \(data)")
|
||||
// Log event details for debugging
|
||||
logger.debug("📨 Received SSE event - Type: \(event.event ?? "message"), ID: \(event.id ?? "none")")
|
||||
logger.debug("📨 Event data: \(data)")
|
||||
|
||||
do {
|
||||
guard let jsonData = data.data(using: .utf8) else {
|
||||
|
|
@ -757,27 +797,63 @@ final class NotificationService: NSObject {
|
|||
func sendServerTestNotification() async {
|
||||
logger.info("🧪 Sending test notification through server...")
|
||||
|
||||
guard let url = serverManager.buildURL(endpoint: "/api/test-notification") else {
|
||||
logger.error("Failed to build test notification URL")
|
||||
// Check if server is running
|
||||
guard serverManager.isRunning else {
|
||||
logger.error("❌ Cannot send test notification - server is not running")
|
||||
return
|
||||
}
|
||||
|
||||
// If not connected to SSE, try to connect first
|
||||
if !isConnected {
|
||||
logger.warning("⚠️ Not connected to SSE endpoint, attempting to connect...")
|
||||
await MainActor.run {
|
||||
connect()
|
||||
}
|
||||
// Give it a moment to connect
|
||||
try? await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
|
||||
}
|
||||
|
||||
// Log server info
|
||||
logger.info("Server info - Port: \(self.serverManager.port), Running: \(self.serverManager.isRunning), SSE Connected: \(self.isConnected)")
|
||||
|
||||
guard let url = serverManager.buildURL(endpoint: "/api/test-notification") else {
|
||||
logger.error("❌ Failed to build test notification URL")
|
||||
return
|
||||
}
|
||||
|
||||
logger.info("📤 Sending POST request to: \(url)")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
// Add auth token if available
|
||||
if let authToken = serverManager.localAuthToken {
|
||||
request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization")
|
||||
logger.debug("Added auth token to request")
|
||||
}
|
||||
|
||||
do {
|
||||
let (_, response) = try await URLSession.shared.data(for: request)
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
logger.info("📥 Received response - Status: \(httpResponse.statusCode)")
|
||||
|
||||
if httpResponse.statusCode == 200 {
|
||||
logger.info("✅ Server test notification sent successfully")
|
||||
if let responseData = String(data: data, encoding: .utf8) {
|
||||
logger.debug("Response data: \(responseData)")
|
||||
}
|
||||
} else {
|
||||
logger.error("❌ Server test notification failed with status: \(httpResponse.statusCode)")
|
||||
if let errorData = String(data: data, encoding: .utf8) {
|
||||
logger.error("Error response: \(errorData)")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
logger.error("❌ Failed to send server test notification: \(error)")
|
||||
logger.error("Error details: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -788,8 +864,3 @@ final class NotificationService: NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Extensions
|
||||
|
||||
extension Notification.Name {
|
||||
static let serverStateChanged = Notification.Name("ServerStateChanged")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,11 @@ class ServerManager {
|
|||
bunServer?.localToken
|
||||
}
|
||||
|
||||
/// The current authentication mode of the server
|
||||
var authMode: String {
|
||||
bunServer?.authMode ?? "os"
|
||||
}
|
||||
|
||||
var bindAddress: String {
|
||||
get {
|
||||
// Get the raw value from UserDefaults, defaulting to the app default
|
||||
|
|
@ -309,8 +314,7 @@ class ServerManager {
|
|||
|
||||
isRunning = false
|
||||
|
||||
// Post notification that server state has changed
|
||||
NotificationCenter.default.post(name: .serverStateChanged, object: nil)
|
||||
// Notification service connection is now handled explicitly via start() method
|
||||
|
||||
// Clear the auth token from SessionMonitor
|
||||
SessionMonitor.shared.setLocalAuthToken(nil)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ final class SharedUnixSocketManager {
|
|||
logger.info("🚀 SharedUnixSocketManager initialized")
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
static let unixSocketReadyNotification = Notification.Name("unixSocketReady")
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
/// Get or create the shared Unix socket connection
|
||||
|
|
@ -42,10 +46,36 @@ final class SharedUnixSocketManager {
|
|||
}
|
||||
}
|
||||
|
||||
// Set up state change handler to notify when socket is ready
|
||||
socket.onStateChange = { [weak self] state in
|
||||
Task { @MainActor [weak self] in
|
||||
self?.handleSocketStateChange(state)
|
||||
}
|
||||
}
|
||||
|
||||
unixSocket = socket
|
||||
return socket
|
||||
}
|
||||
|
||||
/// Handle socket state changes and notify when ready
|
||||
private func handleSocketStateChange(_ state: UnixSocketConnection.ConnectionState) {
|
||||
switch state {
|
||||
case .ready:
|
||||
logger.info("🚀 Unix socket is ready, posting notification")
|
||||
NotificationCenter.default.post(name: Self.unixSocketReadyNotification, object: nil)
|
||||
case .failed(let error):
|
||||
logger.error("❌ Unix socket connection failed: \(error)")
|
||||
case .cancelled:
|
||||
logger.info("🛑 Unix socket connection cancelled")
|
||||
case .preparing:
|
||||
logger.debug("🔄 Unix socket is preparing connection")
|
||||
case .setup:
|
||||
logger.debug("🔧 Unix socket is in setup state")
|
||||
case .waiting(let error):
|
||||
logger.warning("⏳ Unix socket is waiting: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the shared connection is connected
|
||||
var isConnected: Bool {
|
||||
unixSocket?.isConnected ?? false
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ struct NotificationSettingsView: View {
|
|||
|
||||
@State private var isTestingNotification = false
|
||||
@State private var showingPermissionAlert = false
|
||||
@State private var sseConnectionStatus = false
|
||||
|
||||
private func updateNotificationPreferences() {
|
||||
// Load current preferences from ConfigManager and notify the service
|
||||
|
|
@ -62,6 +63,36 @@ struct NotificationSettingsView: View {
|
|||
Text("Display native macOS notifications for session and command events")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
// SSE Connection Status Row
|
||||
HStack(spacing: 6) {
|
||||
Circle()
|
||||
.fill(sseConnectionStatus ? Color.green : Color.red)
|
||||
.frame(width: 8, height: 8)
|
||||
Text("Event Stream:")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(sseConnectionStatus ? "Connected" : "Disconnected")
|
||||
.font(.caption)
|
||||
.foregroundStyle(sseConnectionStatus ? .green : .red)
|
||||
.fontWeight(.medium)
|
||||
Spacer()
|
||||
}
|
||||
.help(sseConnectionStatus
|
||||
? "Real-time notification stream is connected"
|
||||
: "Real-time notification stream is disconnected. Check if the server is running.")
|
||||
|
||||
// Show warning when disconnected
|
||||
if showNotifications && !sseConnectionStatus {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.yellow)
|
||||
.font(.caption)
|
||||
Text("Real-time notifications are unavailable. The server connection may be down.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
|
@ -192,6 +223,14 @@ struct NotificationSettingsView: View {
|
|||
.onAppear {
|
||||
// Sync the AppStorage value with ConfigManager on first load
|
||||
showNotifications = configManager.notificationsEnabled
|
||||
|
||||
// Update initial connection status
|
||||
sseConnectionStatus = notificationService.isSSEConnected
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .notificationServiceConnectionChanged)) { _ in
|
||||
// Update connection status when it changes
|
||||
sseConnectionStatus = notificationService.isSSEConnected
|
||||
logger.debug("SSE connection status changed: \(sseConnectionStatus)")
|
||||
}
|
||||
}
|
||||
.alert("Notification Permission Required", isPresented: $showingPermissionAlert) {
|
||||
|
|
|
|||
|
|
@ -204,9 +204,19 @@ export class DirectKeyboardManager {
|
|||
// Focus synchronously - critical for iOS Safari
|
||||
this.hiddenInput.focus();
|
||||
|
||||
// Also click synchronously to help trigger keyboard
|
||||
this.hiddenInput.click();
|
||||
logger.log('Focused and clicked hidden input synchronously');
|
||||
// Set a dummy value and select it to help trigger iOS keyboard
|
||||
// This helps iOS recognize that we want to show the keyboard
|
||||
this.hiddenInput.value = ' ';
|
||||
this.hiddenInput.setSelectionRange(0, 1);
|
||||
|
||||
// Clear the dummy value after a short delay
|
||||
setTimeout(() => {
|
||||
if (this.hiddenInput) {
|
||||
this.hiddenInput.value = '';
|
||||
}
|
||||
}, 50);
|
||||
|
||||
logger.log('Focused hidden input with dummy value trick');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -109,17 +109,9 @@ export class TerminalQuickKeys extends LitElement {
|
|||
this.isLandscape = window.innerWidth > window.innerHeight && window.innerWidth > 600;
|
||||
}
|
||||
|
||||
private getButtonSizeClass(label: string): string {
|
||||
if (label.length >= 4) {
|
||||
// Long text: compact with max-width constraint
|
||||
return this.isLandscape ? 'px-0.5 py-1 flex-1 max-w-14' : 'px-0.5 py-1.5 flex-1 max-w-16';
|
||||
} else if (label.length === 3) {
|
||||
// Medium text: slightly more padding, larger max-width
|
||||
return this.isLandscape ? 'px-1 py-1 flex-1 max-w-16' : 'px-1 py-1.5 flex-1 max-w-18';
|
||||
} else {
|
||||
// Short text: can grow freely
|
||||
return this.isLandscape ? 'px-1 py-1 flex-1' : 'px-1 py-1.5 flex-1';
|
||||
}
|
||||
private getButtonSizeClass(_label: string): string {
|
||||
// Use flexible sizing without constraining width
|
||||
return this.isLandscape ? 'px-1 py-1' : 'px-1.5 py-1.5';
|
||||
}
|
||||
|
||||
private getButtonFontClass(label: string): string {
|
||||
|
|
@ -210,7 +202,7 @@ export class TerminalQuickKeys extends LitElement {
|
|||
}
|
||||
|
||||
if (this.onKeyPress) {
|
||||
this.onKeyPress(key, isModifier, isSpecial);
|
||||
this.onKeyPress(key, isModifier, isSpecial, isToggle);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -233,7 +225,7 @@ export class TerminalQuickKeys extends LitElement {
|
|||
|
||||
// Send first key immediately
|
||||
if (this.onKeyPress) {
|
||||
this.onKeyPress(key, isModifier, isSpecial);
|
||||
this.onKeyPress(key, isModifier, isSpecial, false);
|
||||
}
|
||||
|
||||
// Start repeat after 500ms initial delay
|
||||
|
|
@ -279,31 +271,20 @@ export class TerminalQuickKeys extends LitElement {
|
|||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
/* Default to bottom of screen */
|
||||
bottom: 0;
|
||||
z-index: 999999;
|
||||
/* Ensure it stays on top */
|
||||
isolation: isolate;
|
||||
/* Smooth transition when keyboard appears/disappears */
|
||||
transition: bottom 0.3s ease-out;
|
||||
background-color: rgb(var(--color-bg-secondary));
|
||||
/* Prevent horizontal overflow */
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
/* Properly handle safe areas */
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
}
|
||||
|
||||
/* The actual bar with buttons */
|
||||
.quick-keys-bar {
|
||||
background: rgb(var(--color-bg-secondary));
|
||||
border-top: 1px solid rgb(var(--color-border-base));
|
||||
padding: 0.25rem 0.25rem;
|
||||
/* Prevent iOS from adding its own styling */
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
/* Add shadow for visibility */
|
||||
box-shadow: 0 -2px 10px rgb(var(--color-bg-secondary) / 0.5);
|
||||
/* Ensure proper width */
|
||||
padding: 0.25rem 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
@ -314,8 +295,6 @@ export class TerminalQuickKeys extends LitElement {
|
|||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
/* Ensure buttons are clickable */
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* Modifier key styling */
|
||||
|
|
@ -360,20 +339,6 @@ export class TerminalQuickKeys extends LitElement {
|
|||
font-size: 8px;
|
||||
}
|
||||
|
||||
/* Max width constraints for buttons */
|
||||
.max-w-14 {
|
||||
max-width: 3.5rem; /* 56px */
|
||||
}
|
||||
|
||||
.max-w-16 {
|
||||
max-width: 4rem; /* 64px */
|
||||
}
|
||||
|
||||
.max-w-18 {
|
||||
max-width: 4.5rem; /* 72px */
|
||||
}
|
||||
|
||||
|
||||
/* Combo key styling (like ^C, ^Z) */
|
||||
.combo-key {
|
||||
background-color: rgb(var(--color-bg-tertiary));
|
||||
|
|
@ -401,7 +366,6 @@ export class TerminalQuickKeys extends LitElement {
|
|||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* Toggle button styling */
|
||||
|
|
@ -430,15 +394,8 @@ export class TerminalQuickKeys extends LitElement {
|
|||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* Landscape mode adjustments */
|
||||
@media (orientation: landscape) and (max-width: 926px) {
|
||||
.quick-keys-bar {
|
||||
padding: 0.2rem 0.2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
|
@ -455,7 +412,7 @@ export class TerminalQuickKeys extends LitElement {
|
|||
>
|
||||
<div class="quick-keys-bar">
|
||||
<!-- Row 1 -->
|
||||
<div class="flex gap-0.5 justify-center mb-0.5 flex-wrap">
|
||||
<div class="flex gap-0.5 mb-0.5 overflow-x-auto scrollbar-hide px-0.5">
|
||||
${TERMINAL_QUICK_KEYS.filter((k) => k.row === 1).map(
|
||||
({ key, label, modifier, arrow, toggle }) => html`
|
||||
<button
|
||||
|
|
@ -507,7 +464,7 @@ export class TerminalQuickKeys extends LitElement {
|
|||
this.showCtrlKeys
|
||||
? html`
|
||||
<!-- Ctrl shortcuts row -->
|
||||
<div class="flex gap-0.5 justify-center flex-wrap mb-0.5">
|
||||
<div class="flex gap-0.5 mb-0.5 overflow-x-auto scrollbar-hide px-0.5">
|
||||
${CTRL_SHORTCUTS.map(
|
||||
({ key, label, combo, special }) => html`
|
||||
<button
|
||||
|
|
@ -542,7 +499,7 @@ export class TerminalQuickKeys extends LitElement {
|
|||
: this.showFunctionKeys
|
||||
? html`
|
||||
<!-- Function keys row -->
|
||||
<div class="flex gap-0.5 justify-center flex-wrap mb-0.5">
|
||||
<div class="flex gap-0.5 mb-0.5 overflow-x-auto scrollbar-hide px-0.5">
|
||||
${FUNCTION_KEYS.map(
|
||||
({ key, label }) => html`
|
||||
<button
|
||||
|
|
@ -576,7 +533,7 @@ export class TerminalQuickKeys extends LitElement {
|
|||
`
|
||||
: html`
|
||||
<!-- Regular row 2 -->
|
||||
<div class="flex gap-0.5 justify-center mb-0.5 flex-wrap">
|
||||
<div class="flex gap-0.5 mb-0.5 overflow-x-auto scrollbar-hide px-0.5">
|
||||
${TERMINAL_QUICK_KEYS.filter((k) => k.row === 2).map(
|
||||
({ key, label, modifier, combo, special, toggle }) => html`
|
||||
<button
|
||||
|
|
@ -597,12 +554,12 @@ export class TerminalQuickKeys extends LitElement {
|
|||
if (key === 'Paste') {
|
||||
this.handlePasteImmediate(e);
|
||||
} else {
|
||||
this.handleKeyPress(key, modifier || combo, special, toggle, e);
|
||||
this.handleKeyPress(key, modifier || combo, special, false, e);
|
||||
}
|
||||
}}
|
||||
@click=${(e: MouseEvent) => {
|
||||
if (e.detail !== 0) {
|
||||
this.handleKeyPress(key, modifier || combo, special, toggle, e);
|
||||
this.handleKeyPress(key, modifier || combo, special, false, e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -615,7 +572,7 @@ export class TerminalQuickKeys extends LitElement {
|
|||
}
|
||||
|
||||
<!-- Row 3 - Additional special characters (always visible) -->
|
||||
<div class="flex gap-0.5 justify-center flex-wrap">
|
||||
<div class="flex gap-0.5 overflow-x-auto scrollbar-hide px-0.5">
|
||||
${TERMINAL_QUICK_KEYS.filter((k) => k.row === 3).map(
|
||||
({ key, label, modifier, combo, special }) => html`
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -473,6 +473,17 @@
|
|||
|
||||
/* Micro-interactions and animations */
|
||||
@layer utilities {
|
||||
/* Hide scrollbar for webkit browsers */
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for Firefox */
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
/* Smooth transitions for interactive elements */
|
||||
.interactive {
|
||||
@apply transition-all duration-200 ease-out;
|
||||
|
|
|
|||
|
|
@ -40,9 +40,9 @@ export function createEventsRouter(sessionMonitor?: SessionMonitor): Router {
|
|||
}
|
||||
};
|
||||
|
||||
// Send initial connection event
|
||||
// Send initial connection event as default message event
|
||||
try {
|
||||
res.write('event: connected\ndata: {"type": "connected"}\n\n');
|
||||
res.write('data: {"type": "connected"}\n\n');
|
||||
} catch (error) {
|
||||
logger.debug('Failed to send initial connection event:', error);
|
||||
return;
|
||||
|
|
@ -69,8 +69,9 @@ export function createEventsRouter(sessionMonitor?: SessionMonitor): Router {
|
|||
logger.info('🧪 Forwarding test notification through SSE:', event);
|
||||
}
|
||||
|
||||
// Proper SSE format with id, event, and data fields
|
||||
const sseMessage = `id: ${++eventId}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`;
|
||||
// Send as default message event (not named event) for compatibility with Mac EventSource
|
||||
// The event type is already included in the data payload
|
||||
const sseMessage = `id: ${++eventId}\ndata: ${JSON.stringify(event)}\n\n`;
|
||||
|
||||
try {
|
||||
res.write(sseMessage);
|
||||
|
|
|
|||
Loading…
Reference in a new issue