mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-25 09:25:50 +00:00
Merge branch 'main' - keep notification refactoring changes
This commit is contained in:
commit
75dd51883b
8 changed files with 702 additions and 8 deletions
|
|
@ -418,7 +418,6 @@ The agent will:
|
|||
- Don't check for "old format" vs "new format"
|
||||
- Don't add fallbacks for older versions
|
||||
- If you suggest backwards compatibility in any form, you have failed to understand this project
|
||||
|
||||
## Key Files Quick Reference
|
||||
|
||||
- Architecture Details: `docs/ARCHITECTURE.md`
|
||||
|
|
|
|||
501
docs/push-impl.md
Normal file
501
docs/push-impl.md
Normal file
|
|
@ -0,0 +1,501 @@
|
|||
# Push Notification Implementation Plan
|
||||
|
||||
This document outlines the comprehensive plan for improving VibeTunnel's notification system through two major initiatives:
|
||||
1. Creating a dedicated Notifications tab in macOS settings
|
||||
2. Migrating SessionMonitor from the Mac app to the server for unified notifications
|
||||
|
||||
## Overview
|
||||
|
||||
Currently, VibeTunnel has inconsistent notification implementations between the Mac and web clients. The Mac app has its own SessionMonitor while the web relies on server events. This leads to:
|
||||
- Different notification behaviors between platforms
|
||||
- Missing features (e.g., Claude Turn notifications not shown in web UI)
|
||||
- Duplicate code and maintenance burden
|
||||
- Inconsistent descriptions and thresholds
|
||||
|
||||
## Part 1: macOS Settings Redesign
|
||||
|
||||
### Current State
|
||||
- Notification settings are cramped in the General tab
|
||||
- No room for descriptive text explaining each notification type
|
||||
- Settings are already at 710px height (quite tall)
|
||||
- Missing helpful context that exists in the web UI
|
||||
|
||||
### Proposed Solution: Dedicated Notifications Tab
|
||||
|
||||
#### 1. Add Notifications Tab to SettingsTab enum
|
||||
|
||||
```swift
|
||||
// SettingsTab.swift
|
||||
enum SettingsTab: String, CaseIterable {
|
||||
case general
|
||||
case notifications // NEW
|
||||
case quickStart
|
||||
case dashboard
|
||||
// ... rest of tabs
|
||||
}
|
||||
|
||||
// Add display name and icon
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .notifications: "Notifications"
|
||||
// ... rest
|
||||
}
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .notifications: "bell.badge"
|
||||
// ... rest
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Create NotificationSettingsView.swift
|
||||
|
||||
```swift
|
||||
struct NotificationSettingsView: View {
|
||||
@ObservedObject private var configManager = ConfigManager.shared
|
||||
@ObservedObject private var notificationService = NotificationService.shared
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
// Master toggle section
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Toggle("Show Session Notifications", isOn: $showNotifications)
|
||||
Text("Display native macOS notifications for session and command events")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
// Notification types section
|
||||
Section {
|
||||
NotificationToggleRow(
|
||||
title: "Session starts",
|
||||
description: "When a new session starts (useful for shared terminals)",
|
||||
isOn: $configManager.notificationSessionStart,
|
||||
helpText: NotificationHelp.sessionStart
|
||||
)
|
||||
|
||||
NotificationToggleRow(
|
||||
title: "Session ends",
|
||||
description: "When a session terminates or crashes (shows exit code)",
|
||||
isOn: $configManager.notificationSessionExit,
|
||||
helpText: NotificationHelp.sessionExit
|
||||
)
|
||||
|
||||
// ... other notification types
|
||||
} header: {
|
||||
Text("Notification Types")
|
||||
}
|
||||
|
||||
// Behavior section
|
||||
Section {
|
||||
Toggle("Play sound", isOn: $configManager.notificationSoundEnabled)
|
||||
Toggle("Show in Notification Center", isOn: $configManager.showInNotificationCenter)
|
||||
} header: {
|
||||
Text("Notification Behavior")
|
||||
}
|
||||
|
||||
// Test section
|
||||
Section {
|
||||
Button("Test Notification") {
|
||||
notificationService.sendTestNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Create Reusable NotificationToggleRow Component
|
||||
|
||||
```swift
|
||||
struct NotificationToggleRow: View {
|
||||
let title: String
|
||||
let description: String
|
||||
@Binding var isOn: Bool
|
||||
let helpText: String
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Toggle(title, isOn: $isOn)
|
||||
.toggleStyle(.checkbox)
|
||||
HelpTooltip(text: helpText)
|
||||
}
|
||||
Text(description)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Update SettingsView.swift
|
||||
|
||||
```swift
|
||||
// Add the new tab
|
||||
NotificationSettingsView()
|
||||
.tabItem {
|
||||
Label(SettingsTab.notifications.displayName,
|
||||
systemImage: SettingsTab.notifications.icon)
|
||||
}
|
||||
.tag(SettingsTab.notifications)
|
||||
```
|
||||
|
||||
#### 5. Update GeneralSettingsView.swift
|
||||
|
||||
Remove all notification-related settings to free up space.
|
||||
|
||||
### Standardized Notification Descriptions
|
||||
|
||||
Use these descriptions consistently across Mac and web:
|
||||
|
||||
| Type | Title | Description |
|
||||
|------|-------|-------------|
|
||||
| Session Start | Session starts | When a new session starts (useful for shared terminals) |
|
||||
| Session Exit | Session ends | When a session terminates or crashes (shows exit code) |
|
||||
| Command Error | Commands fail | When commands fail with non-zero exit codes |
|
||||
| Command Completion | Commands complete (> 3 seconds) | When commands taking >3 seconds finish (builds, tests, etc.) |
|
||||
| Terminal Bell | Terminal bell (🔔) | Terminal bell (^G) from vim, IRC mentions, completion sounds |
|
||||
| Claude Turn | Claude turn notifications | When Claude AI finishes responding and awaits input |
|
||||
|
||||
## Part 2: Server-Side SessionMonitor Migration
|
||||
|
||||
### Current Architecture
|
||||
|
||||
```
|
||||
Mac App:
|
||||
SessionMonitor (Swift) → NotificationService → macOS notifications
|
||||
|
||||
Server:
|
||||
PtyManager → Basic events → SSE → Web notifications
|
||||
```
|
||||
|
||||
### Proposed Architecture
|
||||
|
||||
```
|
||||
Server:
|
||||
PtyManager → SessionMonitor (TypeScript) → Enhanced events → SSE/WebSocket
|
||||
↓
|
||||
Mac & Web clients
|
||||
```
|
||||
|
||||
### Implementation Steps
|
||||
|
||||
#### 1. Create Server-Side SessionMonitor
|
||||
|
||||
```typescript
|
||||
// web/src/server/services/session-monitor.ts
|
||||
|
||||
export interface SessionState {
|
||||
id: string;
|
||||
name: string;
|
||||
command: string[];
|
||||
isRunning: boolean;
|
||||
activityStatus?: {
|
||||
isActive: boolean;
|
||||
lastActivity?: Date;
|
||||
specificStatus?: {
|
||||
app: string;
|
||||
status: string;
|
||||
};
|
||||
};
|
||||
commandStartTime?: Date;
|
||||
lastCommand?: string;
|
||||
}
|
||||
|
||||
export class SessionMonitor {
|
||||
private sessions = new Map<string, SessionState>();
|
||||
private claudeIdleNotified = new Set<string>();
|
||||
private lastActivityState = new Map<string, boolean>();
|
||||
private commandThresholdMs = 3000; // 3 seconds
|
||||
|
||||
constructor(
|
||||
private ptyManager: PtyManager,
|
||||
private eventBus: EventEmitter
|
||||
) {
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
private detectClaudeSession(session: SessionState): boolean {
|
||||
const isClaudeCommand = session.command
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes('claude');
|
||||
|
||||
const isClaudeApp = session.activityStatus?.specificStatus?.app
|
||||
.toLowerCase()
|
||||
.includes('claude') ?? false;
|
||||
|
||||
return isClaudeCommand || isClaudeApp;
|
||||
}
|
||||
|
||||
private checkClaudeTurnNotification(sessionId: string, newState: SessionState) {
|
||||
if (!this.detectClaudeSession(newState)) return;
|
||||
|
||||
const currentActive = newState.activityStatus?.isActive ?? false;
|
||||
const previousActive = this.lastActivityState.get(sessionId) ?? false;
|
||||
|
||||
// Claude went from active to idle
|
||||
if (previousActive && !currentActive && !this.claudeIdleNotified.has(sessionId)) {
|
||||
this.eventBus.emit('notification', {
|
||||
type: ServerEventType.ClaudeTurn,
|
||||
sessionId,
|
||||
sessionName: newState.name,
|
||||
message: 'Claude has finished responding'
|
||||
});
|
||||
this.claudeIdleNotified.add(sessionId);
|
||||
}
|
||||
|
||||
// Reset when Claude becomes active again
|
||||
if (!previousActive && currentActive) {
|
||||
this.claudeIdleNotified.delete(sessionId);
|
||||
}
|
||||
|
||||
this.lastActivityState.set(sessionId, currentActive);
|
||||
}
|
||||
|
||||
// ... other monitoring methods
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Enhance Event Types
|
||||
|
||||
```typescript
|
||||
// web/src/shared/types.ts
|
||||
|
||||
export enum ServerEventType {
|
||||
SessionStart = 'session-start',
|
||||
SessionExit = 'session-exit',
|
||||
CommandFinished = 'command-finished',
|
||||
CommandError = 'command-error', // NEW - separate from finished
|
||||
Bell = 'bell', // NEW
|
||||
ClaudeTurn = 'claude-turn',
|
||||
Connected = 'connected'
|
||||
}
|
||||
|
||||
export interface ServerEvent {
|
||||
type: ServerEventType;
|
||||
timestamp: string;
|
||||
sessionId: string;
|
||||
sessionName?: string;
|
||||
|
||||
// Event-specific data
|
||||
exitCode?: number;
|
||||
command?: string;
|
||||
duration?: number;
|
||||
message?: string;
|
||||
|
||||
// Activity status for richer client UI
|
||||
activityStatus?: {
|
||||
isActive: boolean;
|
||||
app?: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Integrate with PtyManager
|
||||
|
||||
```typescript
|
||||
// web/src/server/pty/pty-manager.ts
|
||||
|
||||
class PtyManager {
|
||||
private sessionMonitor: SessionMonitor;
|
||||
|
||||
constructor() {
|
||||
this.sessionMonitor = new SessionMonitor(this, serverEventBus);
|
||||
}
|
||||
|
||||
// Feed data to SessionMonitor
|
||||
private handlePtyData(sessionId: string, data: string) {
|
||||
// Existing data handling...
|
||||
|
||||
// Detect bell character
|
||||
if (data.includes('\x07')) {
|
||||
serverEventBus.emit('notification', {
|
||||
type: ServerEventType.Bell,
|
||||
sessionId,
|
||||
sessionName: this.sessions.get(sessionId)?.name
|
||||
});
|
||||
}
|
||||
|
||||
// Update activity status
|
||||
this.sessionMonitor.updateActivity(sessionId, {
|
||||
isActive: true,
|
||||
lastActivity: new Date()
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Update Server Routes
|
||||
|
||||
```typescript
|
||||
// web/src/server/routes/events.ts
|
||||
|
||||
// Enhanced event handling
|
||||
serverEventBus.on('notification', (event: ServerEvent) => {
|
||||
// Send to all connected SSE clients
|
||||
broadcastEvent(event);
|
||||
|
||||
// Log for debugging
|
||||
logger.info(`📢 Notification event: ${event.type} for session ${event.sessionId}`);
|
||||
});
|
||||
```
|
||||
|
||||
#### 5. Update Mac NotificationService
|
||||
|
||||
```swift
|
||||
// NotificationService.swift
|
||||
|
||||
class NotificationService {
|
||||
// Remove local SessionMonitor dependency
|
||||
// Subscribe to server SSE events instead
|
||||
|
||||
private func connectToServerEvents() {
|
||||
eventSource = EventSource(url: "http://localhost:4020/api/events")
|
||||
|
||||
eventSource.onMessage { event in
|
||||
guard let data = event.data,
|
||||
let serverEvent = try? JSONDecoder().decode(ServerEvent.self, from: data) else {
|
||||
return
|
||||
}
|
||||
|
||||
Task { @MainActor in
|
||||
self.handleServerEvent(serverEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleServerEvent(_ event: ServerEvent) {
|
||||
// Map server events to notifications
|
||||
switch event.type {
|
||||
case .sessionStart:
|
||||
if preferences.sessionStart {
|
||||
sendNotification(for: event)
|
||||
}
|
||||
// ... handle other event types
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 6. Update Web Notification Service
|
||||
|
||||
```typescript
|
||||
// web/src/client/services/push-notification-service.ts
|
||||
|
||||
// Add Claude Turn to notification handling
|
||||
private handleServerEvent(event: ServerEvent) {
|
||||
if (!this.preferences[this.mapEventTypeToPreference(event.type)]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Send browser notification
|
||||
this.showNotification(event);
|
||||
}
|
||||
|
||||
private mapEventTypeToPreference(type: ServerEventType): keyof NotificationPreferences {
|
||||
const mapping = {
|
||||
[ServerEventType.SessionStart]: 'sessionStart',
|
||||
[ServerEventType.SessionExit]: 'sessionExit',
|
||||
[ServerEventType.CommandFinished]: 'commandCompletion',
|
||||
[ServerEventType.CommandError]: 'commandError',
|
||||
[ServerEventType.Bell]: 'bell',
|
||||
[ServerEventType.ClaudeTurn]: 'claudeTurn' // Now properly mapped
|
||||
};
|
||||
return mapping[type];
|
||||
}
|
||||
```
|
||||
|
||||
#### 7. Add Claude Turn to Web UI
|
||||
|
||||
```typescript
|
||||
// web/src/client/components/settings.ts
|
||||
|
||||
// In notification types section
|
||||
${this.renderNotificationToggle('claudeTurn', 'Claude Turn',
|
||||
'When Claude AI finishes responding and awaits input')}
|
||||
```
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Phase 1: Preparation (Non-breaking)
|
||||
1. Implement server-side SessionMonitor alongside existing system
|
||||
2. Add new event types to shared types
|
||||
3. Update web UI to show Claude Turn option
|
||||
|
||||
### Phase 2: Server Enhancement (Non-breaking)
|
||||
1. Deploy enhanced server with SessionMonitor
|
||||
2. Server emits both old and new event formats
|
||||
3. Test with web client to ensure compatibility
|
||||
|
||||
### Phase 3: Mac App Migration
|
||||
1. Update Mac app to consume server events
|
||||
2. Keep fallback to local monitoring if server unavailable
|
||||
3. Remove local SessionMonitor once stable
|
||||
|
||||
### Phase 4: Cleanup
|
||||
1. Remove old event formats from server
|
||||
2. Remove local SessionMonitor code from Mac
|
||||
3. Document new architecture
|
||||
|
||||
## Testing Plan
|
||||
|
||||
### Unit Tests
|
||||
- SessionMonitor Claude detection logic
|
||||
- Event threshold calculations
|
||||
- Activity state transitions
|
||||
|
||||
### Integration Tests
|
||||
- Server events reach both Mac and web clients
|
||||
- Notification preferences are respected
|
||||
- Claude Turn notifications work correctly
|
||||
- Bell character detection
|
||||
|
||||
### Manual Testing
|
||||
- Test each notification type on both platforms
|
||||
- Verify descriptions match
|
||||
- Test with multiple clients connected
|
||||
- Test offline Mac app behavior
|
||||
|
||||
## Success Metrics
|
||||
|
||||
1. **Consistency**: Same notifications appear on Mac and web for same events
|
||||
2. **Feature Parity**: Claude Turn available on both platforms
|
||||
3. **Performance**: No noticeable lag in notifications
|
||||
4. **Reliability**: No missed notifications
|
||||
5. **Maintainability**: Single codebase for monitoring logic
|
||||
|
||||
## Timeline Estimate
|
||||
|
||||
- **Week 1**: Implement macOS Notifications tab
|
||||
- **Week 2**: Create server-side SessionMonitor
|
||||
- **Week 3**: Integrate and test with web client
|
||||
- **Week 4**: Migrate Mac app and testing
|
||||
- **Week 5**: Polish, documentation, and deployment
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
|------|--------|------------|
|
||||
| Breaking existing notifications | High | Phased rollout, maintain backwards compatibility |
|
||||
| Performance impact on server | Medium | Efficient event handling, consider debouncing |
|
||||
| Mac app offline mode | Medium | Keep local fallback for critical notifications |
|
||||
| Complex migration | Medium | Detailed testing plan, feature flags |
|
||||
|
||||
## Conclusion
|
||||
|
||||
This two-part implementation will:
|
||||
1. Provide a better UI for notification settings on macOS
|
||||
2. Create a unified notification system across all platforms
|
||||
3. Reduce code duplication and maintenance burden
|
||||
4. Ensure consistent behavior for all users
|
||||
|
||||
The migration is designed to be non-breaking with careful phases to minimize risk.
|
||||
54
mac/VibeTunnel/Presentation/Components/HelpTooltip.swift
Normal file
54
mac/VibeTunnel/Presentation/Components/HelpTooltip.swift
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import SwiftUI
|
||||
|
||||
/// A help icon with tooltip for explaining settings
|
||||
struct HelpTooltip: View {
|
||||
let text: String
|
||||
@State private var isHovering = false
|
||||
|
||||
var body: some View {
|
||||
Image(systemName: "questionmark.circle")
|
||||
.foregroundColor(.secondary)
|
||||
.imageScale(.small)
|
||||
.help(text)
|
||||
.onHover { hovering in
|
||||
isHovering = hovering
|
||||
}
|
||||
.scaleEffect(isHovering ? 1.1 : 1.0)
|
||||
.animation(.easeInOut(duration: 0.1), value: isHovering)
|
||||
}
|
||||
}
|
||||
|
||||
/// Notification setting descriptions
|
||||
enum NotificationHelp {
|
||||
static let sessionStarts = "Get notified when a new terminal session begins. Useful for monitoring when someone starts using your shared terminal."
|
||||
|
||||
static let sessionEnds = "Get notified when a terminal session closes. Shows exit code if the session crashed or exited abnormally."
|
||||
|
||||
static let commandsComplete = "Get notified when commands that take longer than 3 seconds finish. Perfect for long builds, tests, or data processing tasks."
|
||||
|
||||
static let commandsFail = "Get notified when any command exits with an error (non-zero exit code). Helps you quickly spot and fix problems."
|
||||
|
||||
static let terminalBell = "Get notified when programs output the terminal bell character (^G). Common in vim alerts, IRC mentions, and completion notifications."
|
||||
|
||||
static let claudeTurn = "Get notified when Claude AI finishes responding and is waiting for your input. Automatically detects Claude CLI sessions and tracks activity transitions."
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
HStack {
|
||||
Text("Session starts")
|
||||
HelpTooltip(text: NotificationHelp.sessionStarts)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Commands complete (> 3 seconds)")
|
||||
HelpTooltip(text: NotificationHelp.commandsComplete)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Claude turn notifications")
|
||||
HelpTooltip(text: NotificationHelp.claudeTurn)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
|
@ -165,4 +165,3 @@ struct GeneralSettingsView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -540,6 +540,108 @@ private struct ErrorView: View {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Authentication Section
|
||||
|
||||
private 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("Remote Access Settings") {
|
||||
|
|
|
|||
|
|
@ -287,6 +287,10 @@ export class TerminalQuickKeys extends LitElement {
|
|||
/* 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;
|
||||
}
|
||||
|
||||
/* The actual bar with buttons */
|
||||
|
|
@ -299,6 +303,9 @@ export class TerminalQuickKeys extends LitElement {
|
|||
appearance: none;
|
||||
/* Add shadow for visibility */
|
||||
box-shadow: 0 -2px 10px rgb(var(--color-bg-secondary) / 0.5);
|
||||
/* Ensure proper width */
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Quick key buttons */
|
||||
|
|
@ -448,7 +455,7 @@ export class TerminalQuickKeys extends LitElement {
|
|||
>
|
||||
<div class="quick-keys-bar">
|
||||
<!-- Row 1 -->
|
||||
<div class="flex gap-0.5 justify-center mb-0.5">
|
||||
<div class="flex gap-0.5 justify-center mb-0.5 flex-wrap">
|
||||
${TERMINAL_QUICK_KEYS.filter((k) => k.row === 1).map(
|
||||
({ key, label, modifier, arrow, toggle }) => html`
|
||||
<button
|
||||
|
|
@ -500,7 +507,7 @@ export class TerminalQuickKeys extends LitElement {
|
|||
this.showCtrlKeys
|
||||
? html`
|
||||
<!-- Ctrl shortcuts row -->
|
||||
<div class="flex gap-0.5 justify-between flex-wrap mb-0.5">
|
||||
<div class="flex gap-0.5 justify-center flex-wrap mb-0.5">
|
||||
${CTRL_SHORTCUTS.map(
|
||||
({ key, label, combo, special }) => html`
|
||||
<button
|
||||
|
|
@ -535,7 +542,7 @@ export class TerminalQuickKeys extends LitElement {
|
|||
: this.showFunctionKeys
|
||||
? html`
|
||||
<!-- Function keys row -->
|
||||
<div class="flex gap-0.5 justify-between mb-0.5">
|
||||
<div class="flex gap-0.5 justify-center flex-wrap mb-0.5">
|
||||
${FUNCTION_KEYS.map(
|
||||
({ key, label }) => html`
|
||||
<button
|
||||
|
|
@ -569,7 +576,7 @@ export class TerminalQuickKeys extends LitElement {
|
|||
`
|
||||
: html`
|
||||
<!-- Regular row 2 -->
|
||||
<div class="flex gap-0.5 justify-center mb-0.5">
|
||||
<div class="flex gap-0.5 justify-center mb-0.5 flex-wrap">
|
||||
${TERMINAL_QUICK_KEYS.filter((k) => k.row === 2).map(
|
||||
({ key, label, modifier, combo, special, toggle }) => html`
|
||||
<button
|
||||
|
|
@ -608,7 +615,7 @@ export class TerminalQuickKeys extends LitElement {
|
|||
}
|
||||
|
||||
<!-- Row 3 - Additional special characters (always visible) -->
|
||||
<div class="flex gap-0.5 justify-center">
|
||||
<div class="flex gap-0.5 justify-center flex-wrap">
|
||||
${TERMINAL_QUICK_KEYS.filter((k) => k.row === 3).map(
|
||||
({ key, label, modifier, combo, special }) => html`
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1489,7 +1489,10 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
|
|||
|
||||
/* Position keyboard button */
|
||||
.keyboard-button {
|
||||
@apply fixed bottom-3 right-3 z-50;
|
||||
@apply fixed z-50;
|
||||
/* Account for safe areas on mobile devices */
|
||||
bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px));
|
||||
right: calc(0.75rem + env(safe-area-inset-right, 0px));
|
||||
/* Ensure button is always touchable */
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import type {
|
|||
} from '../../shared/types.js';
|
||||
import { TitleMode } from '../../shared/types.js';
|
||||
import { ProcessTreeAnalyzer } from '../services/process-tree-analyzer.js';
|
||||
import type { SessionMonitor } from '../services/session-monitor.js';
|
||||
import { ActivityDetector, type ActivityState } from '../utils/activity-detector.js';
|
||||
import { TitleSequenceFilter } from '../utils/ansi-title-filter.js';
|
||||
import { createLogger } from '../utils/logger.js';
|
||||
|
|
@ -137,6 +138,7 @@ export class PtyManager extends EventEmitter {
|
|||
private processTreeAnalyzer = new ProcessTreeAnalyzer(); // Process tree analysis for bell source identification
|
||||
private activityFileWarningsLogged = new Set<string>(); // Track which sessions we've logged warnings for
|
||||
private lastWrittenActivityState = new Map<string, string>(); // Track last written activity state to avoid unnecessary writes
|
||||
private sessionMonitor: SessionMonitor | null = null; // Reference to SessionMonitor for notification tracking
|
||||
|
||||
// Command tracking for notifications
|
||||
private commandTracking = new Map<
|
||||
|
|
@ -181,6 +183,13 @@ export class PtyManager extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the SessionMonitor instance for activity tracking
|
||||
*/
|
||||
public setSessionMonitor(monitor: SessionMonitor): void {
|
||||
this.sessionMonitor = monitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup terminal resize detection for when the hosting terminal is resized
|
||||
*/
|
||||
|
|
@ -708,6 +717,11 @@ export class PtyManager extends EventEmitter {
|
|||
ptyProcess.onData((data: string) => {
|
||||
let processedData = data;
|
||||
|
||||
// Track PTY output in SessionMonitor for activity and bell detection
|
||||
if (this.sessionMonitor) {
|
||||
this.sessionMonitor.trackPtyOutput(session.id, data);
|
||||
}
|
||||
|
||||
// If title mode is not NONE, filter out any title sequences the process might
|
||||
// have written to the stream.
|
||||
if (session.titleMode !== undefined && session.titleMode !== TitleMode.NONE) {
|
||||
|
|
@ -723,6 +737,16 @@ export class PtyManager extends EventEmitter {
|
|||
if (activity.specificStatus?.status !== session.lastActivityStatus) {
|
||||
session.lastActivityStatus = activity.specificStatus?.status;
|
||||
this.markTitleUpdateNeeded(session);
|
||||
|
||||
// Update SessionMonitor with activity change
|
||||
if (this.sessionMonitor) {
|
||||
const isActive = activity.specificStatus?.status === 'working';
|
||||
this.sessionMonitor.updateSessionActivity(
|
||||
session.id,
|
||||
isActive,
|
||||
activity.specificStatus?.app
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2456,6 +2480,11 @@ export class PtyManager extends EventEmitter {
|
|||
session.currentCommand = commandProc.command;
|
||||
session.commandStartTime = Date.now();
|
||||
|
||||
// Update SessionMonitor with new command
|
||||
if (this.sessionMonitor) {
|
||||
this.sessionMonitor.updateCommand(session.id, commandProc.command);
|
||||
}
|
||||
|
||||
// Special logging for Claude commands
|
||||
const isClaudeCommand = commandProc.command.toLowerCase().includes('claude');
|
||||
if (isClaudeCommand) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue