mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-19 13:35:54 +00:00
Fix: Defer screen recording permission check until actual use (#238)
This commit is contained in:
parent
db6ac03bf5
commit
732210f333
12 changed files with 153 additions and 191 deletions
|
|
@ -41,7 +41,7 @@ struct ServerConfig: Codable, Equatable {
|
|||
// 1. Contain at least 2 colons
|
||||
// 2. Only contain valid IPv6 characters (hex digits, colons, and optionally dots for IPv4-mapped addresses)
|
||||
// 3. Not be a hostname with colons (which would contain other characters)
|
||||
let colonCount = formattedHost.count(where: { $0 == ":" })
|
||||
let colonCount = formattedHost.count { $0 == ":" }
|
||||
let validIPv6Chars = CharacterSet(charactersIn: "0123456789abcdefABCDEF:.%")
|
||||
let isIPv6 = colonCount >= 2 && formattedHost.unicodeScalars.allSatisfy { validIPv6Chars.contains($0) }
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import Foundation
|
|||
enum AppConstants {
|
||||
/// Current version of the welcome dialog
|
||||
/// Increment this when significant changes require re-showing the welcome flow
|
||||
static let currentWelcomeVersion = 3
|
||||
static let currentWelcomeVersion = 4
|
||||
|
||||
/// UserDefaults keys
|
||||
enum UserDefaultsKeys {
|
||||
|
|
|
|||
|
|
@ -2051,41 +2051,36 @@ extension ScreencapService: SCStreamOutput {
|
|||
return
|
||||
}
|
||||
|
||||
// Convert to CGImage in the nonisolated context
|
||||
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
|
||||
let context = CIContext()
|
||||
|
||||
// Check extent is valid
|
||||
guard !ciImage.extent.isEmpty else {
|
||||
// Skip frame with empty extent
|
||||
return
|
||||
}
|
||||
|
||||
guard let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else {
|
||||
// Failed to create CGImage
|
||||
return
|
||||
}
|
||||
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
await self.processFrame(ciImage: ciImage)
|
||||
await self.processFrameWithCGImage(cgImage)
|
||||
}
|
||||
}
|
||||
|
||||
/// Separate async function to handle frame processing
|
||||
@MainActor
|
||||
private func processFrame(ciImage: CIImage) async {
|
||||
private func processFrameWithCGImage(_ cgImage: CGImage) async {
|
||||
// Check if we're still capturing before processing
|
||||
guard isCapturing else {
|
||||
logger.debug("Skipping frame processing - capture stopped")
|
||||
return
|
||||
}
|
||||
|
||||
let context = CIContext()
|
||||
|
||||
// Check extent is valid
|
||||
guard !ciImage.extent.isEmpty else {
|
||||
logger.error("CIImage has empty extent, skipping frame")
|
||||
return
|
||||
}
|
||||
|
||||
guard let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else {
|
||||
logger.error("Failed to create CGImage from CIImage")
|
||||
return
|
||||
}
|
||||
|
||||
// Check again if we're still capturing before updating frame
|
||||
guard isCapturing else {
|
||||
logger.debug("Capture stopped during frame processing")
|
||||
return
|
||||
}
|
||||
|
||||
// Update current frame
|
||||
currentFrame = cgImage
|
||||
let frameCount = frameCounter
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Foundation
|
||||
import Observation
|
||||
import OSLog
|
||||
import ScreenCaptureKit
|
||||
@preconcurrency import ScreenCaptureKit
|
||||
import SwiftUI
|
||||
|
||||
/// Errors that can occur during server operations
|
||||
|
|
@ -252,17 +252,8 @@ class ServerManager {
|
|||
// Initialize ScreencapService singleton and ensure WebSocket is connected
|
||||
let screencapService = ScreencapService.shared
|
||||
|
||||
// Check permission status
|
||||
let hasPermission = await checkScreenRecordingPermission()
|
||||
if hasPermission {
|
||||
logger.info("✅ Screen recording permission granted")
|
||||
} else {
|
||||
logger.warning("⚠️ Screen recording permission not granted - some features will be limited")
|
||||
logger
|
||||
.warning(
|
||||
"💡 Please grant screen recording permission in System Settings > Privacy & Security > Screen Recording"
|
||||
)
|
||||
}
|
||||
// Skip permission check at startup - it will be checked when actually needed
|
||||
logger.info("📸 Deferring screen recording permission check until first use")
|
||||
|
||||
// Connect WebSocket regardless of permission status
|
||||
// This allows the API to respond with appropriate errors
|
||||
|
|
@ -623,19 +614,3 @@ enum ServerManagerError: LocalizedError {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ServerManager Extension
|
||||
|
||||
extension ServerManager {
|
||||
/// Check if we have screen recording permission
|
||||
private func checkScreenRecordingPermission() async -> Bool {
|
||||
do {
|
||||
// Try to get shareable content - this will fail if we don't have permission
|
||||
_ = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: false)
|
||||
return true
|
||||
} catch {
|
||||
logger.warning("Screen recording permission check failed: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
# Visual Indicator Styles for VibeTunnel Menu Bar
|
||||
|
||||
## Current Implementation
|
||||
|
||||
The menu bar now shows session status using visual indicators instead of cryptic numbers. Here are the available styles:
|
||||
|
||||
### 1. **Dots Style** (Default)
|
||||
```
|
||||
No sessions: [empty]
|
||||
Only idle: 3
|
||||
Only active: ●●●
|
||||
Mixed (2/5): ●● 5
|
||||
Many active: ●●●+ 8
|
||||
```
|
||||
- Filled dots (●) represent active sessions
|
||||
- Shows up to 3 dots, then adds "+"
|
||||
- Total count shown only when idle sessions exist
|
||||
|
||||
### 2. **Bars Style**
|
||||
```
|
||||
No sessions: [empty]
|
||||
Only idle: ▫︎▫︎▫︎
|
||||
Only active: ▪︎▪︎▪︎
|
||||
Mixed (2/5): ▪︎▪︎▫︎▫︎▫︎
|
||||
Many (3/7): ▪︎▪︎▪︎▫︎▫︎+
|
||||
```
|
||||
- Filled squares (▪︎) for active sessions
|
||||
- Empty squares (▫︎) for idle sessions
|
||||
- Shows up to 5 bars total
|
||||
|
||||
### 3. **Compact Style**
|
||||
```
|
||||
No sessions: [empty]
|
||||
Only idle: ◯3
|
||||
Only active: ◆2
|
||||
Mixed (2/5): 2◆5
|
||||
```
|
||||
- Diamond (◆) as separator/indicator
|
||||
- Most space-efficient option
|
||||
|
||||
### 4. **Minimalist Style**
|
||||
```
|
||||
No sessions: [empty]
|
||||
Only idle: 3
|
||||
Only active: ●2
|
||||
Mixed (2/5): 2|5
|
||||
```
|
||||
- Simple vertical bar separator
|
||||
- Dot prefix for active-only
|
||||
|
||||
### 5. **Meter Style**
|
||||
```
|
||||
No sessions: [empty]
|
||||
Only idle: [□□□□□]
|
||||
Only active: [■■■■■]
|
||||
Mixed (2/5): [■■□□□]
|
||||
Mixed (1/3): [■■□□□]
|
||||
```
|
||||
- Progress bar visualization
|
||||
- Shows active/total ratio
|
||||
|
||||
## Changing Styles
|
||||
|
||||
To change the indicator style, modify line 144 in `StatusBarController.swift`:
|
||||
|
||||
```swift
|
||||
let indicatorStyle: IndicatorStyle = .dots // Change to .bars, .compact, etc.
|
||||
```
|
||||
|
||||
## Button Highlighting
|
||||
|
||||
The menu bar button now properly highlights when the dropdown is open, providing clear visual feedback that the menu is active.
|
||||
|
|
@ -146,7 +146,7 @@ struct AdvancedSettingsView: View {
|
|||
)
|
||||
}
|
||||
))
|
||||
Text("Allows screen sharing and remote control features. Runs on port 4010.")
|
||||
Text("Allow screen sharing feature in the web interface.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
|
@ -393,39 +393,32 @@ private struct WindowHighlightSettingsSection: View {
|
|||
var body: some View {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// Enable/Disable toggle
|
||||
Toggle("Show window highlight effect", isOn: $highlightEnabled)
|
||||
.onChange(of: highlightEnabled) { _, newValue in
|
||||
if newValue {
|
||||
previewHighlightEffect()
|
||||
}
|
||||
}
|
||||
|
||||
if highlightEnabled {
|
||||
// Style picker
|
||||
Picker("Highlight style", selection: $highlightStyle) {
|
||||
// Window highlight style picker
|
||||
HStack {
|
||||
Text("Window highlight")
|
||||
Spacer()
|
||||
Picker("", selection: highlightStyleBinding) {
|
||||
Text("None").tag("none")
|
||||
Text("Default").tag("default")
|
||||
Text("Subtle").tag("subtle")
|
||||
Text("Neon").tag("neon")
|
||||
Text("Custom").tag("custom")
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.onChange(of: highlightStyle) { _, _ in
|
||||
previewHighlightEffect()
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.labelsHidden()
|
||||
}
|
||||
|
||||
// Custom color picker (only shown when custom is selected)
|
||||
if highlightStyle == "custom" {
|
||||
HStack {
|
||||
Text("Custom color")
|
||||
Spacer()
|
||||
ColorPicker("", selection: $customColor, supportsOpacity: false)
|
||||
.labelsHidden()
|
||||
.onChange(of: customColor) { _, newColor in
|
||||
saveCustomColor(newColor)
|
||||
previewHighlightEffect()
|
||||
}
|
||||
}
|
||||
// Custom color picker (only shown when custom is selected)
|
||||
if highlightStyle == "custom" && highlightEnabled {
|
||||
HStack {
|
||||
Text("Custom color")
|
||||
Spacer()
|
||||
ColorPicker("", selection: $customColor, supportsOpacity: false)
|
||||
.labelsHidden()
|
||||
.onChange(of: customColor) { _, newColor in
|
||||
saveCustomColor(newColor)
|
||||
previewHighlightEffect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -445,6 +438,24 @@ private struct WindowHighlightSettingsSection: View {
|
|||
}
|
||||
}
|
||||
|
||||
private var highlightStyleBinding: Binding<String> {
|
||||
Binding(
|
||||
get: {
|
||||
highlightEnabled ? highlightStyle : "none"
|
||||
},
|
||||
set: { newValue in
|
||||
if newValue == "none" {
|
||||
highlightEnabled = false
|
||||
highlightStyle = "default" // Keep a default style for when re-enabled
|
||||
} else {
|
||||
highlightEnabled = true
|
||||
highlightStyle = newValue
|
||||
previewHighlightEffect()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func saveCustomColor(_ color: Color) {
|
||||
let nsColor = NSColor(color)
|
||||
do {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ struct SettingsView: View {
|
|||
// MARK: - Constants
|
||||
|
||||
private enum Layout {
|
||||
static let defaultTabSize = CGSize(width: 500, height: 620)
|
||||
static let fallbackTabSize = CGSize(width: 500, height: 400)
|
||||
static let defaultTabSize = CGSize(width: 500, height: 670)
|
||||
static let fallbackTabSize = CGSize(width: 500, height: 450)
|
||||
}
|
||||
|
||||
/// Define ideal sizes for each tab
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
import SwiftUI
|
||||
|
||||
/// Fifth page explaining how to manage multiple AI agent sessions.
|
||||
///
|
||||
/// This view provides information about controlling multiple terminal sessions
|
||||
/// with AI agents, showing how to use the menu bar icon to see all instances,
|
||||
/// update session names, and use the magic wand feature for automatic naming.
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Overview
|
||||
/// The agent army control page includes:
|
||||
/// - Instructions for using the menu bar to view all sessions
|
||||
/// - Information about session activity tracking
|
||||
/// - Details about renaming sessions and using the magic wand
|
||||
/// - Explanation of session title display locations
|
||||
///
|
||||
/// ### Features
|
||||
/// - Menu bar session overview
|
||||
/// - Git change tracking per session
|
||||
/// - Manual and automatic session naming
|
||||
/// - Terminal window title management
|
||||
struct ControlAgentArmyPageView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 30) {
|
||||
VStack(spacing: 16) {
|
||||
Text("Control Your Agent Army")
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Text(
|
||||
"Click on the VibeTunnel icon in your menu bar to see all open terminal sessions. Track their activity, working paths, and Git changes."
|
||||
)
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: 480)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
// VT Title Command
|
||||
VStack(spacing: 12) {
|
||||
Text("Update titles from inside your terminal:")
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
HStack {
|
||||
Text("vt title \"Current action in project context\"")
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
.background(Color(NSColor.controlBackgroundColor))
|
||||
.cornerRadius(6)
|
||||
}
|
||||
|
||||
Text(
|
||||
"Session titles appear in the menu bar and terminal windows.\nUse the dashboard to rename sessions manually, or use the magic wand with Claude/Gemini."
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: 420)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview("Control Agent Army Page") {
|
||||
ControlAgentArmyPageView()
|
||||
.frame(width: 640, height: 480)
|
||||
.background(Color(NSColor.windowBackgroundColor))
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@ struct RequestPermissionsPageView: View {
|
|||
.fontWeight(.semibold)
|
||||
|
||||
Text(
|
||||
"VibeTunnel needs these permissions:\n• Automation to start terminal sessions\n• Accessibility to send commands\n• Screen Recording for screen capture"
|
||||
"VibeTunnel needs permissions for automation to start terminal sessions, accessibility to send commands, and screen recording for screen capture."
|
||||
)
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
|
|
@ -109,7 +109,7 @@ struct RequestPermissionsPageView: View {
|
|||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
Text("Screen Recording permission granted")
|
||||
Text("Screen Recording granted")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.font(.body)
|
||||
|
|
|
|||
|
|
@ -10,12 +10,13 @@ import SwiftUI
|
|||
/// ## Topics
|
||||
///
|
||||
/// ### Overview
|
||||
/// The welcome flow consists of six pages:
|
||||
/// The welcome flow consists of seven pages:
|
||||
/// - ``WelcomePageView`` - Introduction and app overview
|
||||
/// - ``VTCommandPageView`` - CLI tool installation
|
||||
/// - ``RequestPermissionsPageView`` - System permissions setup
|
||||
/// - ``SelectTerminalPageView`` - Terminal selection and testing
|
||||
/// - ``ProtectDashboardPageView`` - Dashboard security configuration
|
||||
/// - ``ControlAgentArmyPageView`` - Managing multiple AI agent sessions
|
||||
/// - ``AccessDashboardPageView`` - Remote access instructions
|
||||
struct WelcomeView: View {
|
||||
@State private var currentPage = 0
|
||||
|
|
@ -66,7 +67,11 @@ struct WelcomeView: View {
|
|||
ProtectDashboardPageView()
|
||||
.frame(width: pageWidth)
|
||||
|
||||
// Page 6: Accessing Dashboard
|
||||
// Page 6: Control Your Agent Army
|
||||
ControlAgentArmyPageView()
|
||||
.frame(width: pageWidth)
|
||||
|
||||
// Page 7: Accessing Dashboard
|
||||
AccessDashboardPageView()
|
||||
.frame(width: pageWidth)
|
||||
}
|
||||
|
|
@ -113,7 +118,7 @@ struct WelcomeView: View {
|
|||
|
||||
// Page indicators centered
|
||||
HStack(spacing: 8) {
|
||||
ForEach(0..<6) { index in
|
||||
ForEach(0..<7) { index in
|
||||
Button {
|
||||
withAnimation {
|
||||
currentPage = index
|
||||
|
|
@ -149,7 +154,7 @@ struct WelcomeView: View {
|
|||
}
|
||||
|
||||
private var buttonTitle: String {
|
||||
currentPage == 5 ? "Finish" : "Next"
|
||||
currentPage == 6 ? "Finish" : "Next"
|
||||
}
|
||||
|
||||
private func handleBackAction() {
|
||||
|
|
@ -159,7 +164,7 @@ struct WelcomeView: View {
|
|||
}
|
||||
|
||||
private func handleNextAction() {
|
||||
if currentPage < 5 {
|
||||
if currentPage < 6 {
|
||||
withAnimation {
|
||||
currentPage += 1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"configurations" : [
|
||||
{
|
||||
"id" : "9D7E1994-9FC3-4F5E-B761-5D212EC7615B",
|
||||
"name" : "Test Scheme Action",
|
||||
"options" : {
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"defaultOptions" : {
|
||||
"codeCoverage" : true,
|
||||
"performanceAntipatternCheckerEnabled" : true,
|
||||
"targetForVariableExpansion" : {
|
||||
"containerPath" : "container:VibeTunnel-Mac.xcodeproj",
|
||||
"identifier" : "788687F02DFF4FCB00B22C15",
|
||||
"name" : "VibeTunnel"
|
||||
}
|
||||
},
|
||||
"testTargets" : [
|
||||
{
|
||||
"target" : {
|
||||
"containerPath" : "container:VibeTunnel-Mac.xcodeproj",
|
||||
"identifier" : "788687FD2DFF4FCB00B22C15",
|
||||
"name" : "VibeTunnelTests"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 1
|
||||
}
|
||||
|
|
@ -115,7 +115,7 @@ struct VibeTunnelApp: App {
|
|||
@MainActor
|
||||
final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUserNotificationCenterDelegate {
|
||||
// Needed for some gross menu item highlight hack
|
||||
weak static var shared: AppDelegate?
|
||||
static weak var shared: AppDelegate?
|
||||
override init() {
|
||||
super.init()
|
||||
Self.shared = self
|
||||
|
|
|
|||
Loading…
Reference in a new issue