Fix: Defer screen recording permission check until actual use (#238)

This commit is contained in:
Peter Steinberger 2025-07-06 09:36:08 +01:00 committed by GitHub
parent db6ac03bf5
commit 732210f333
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 153 additions and 191 deletions

View file

@ -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) }

View file

@ -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 {

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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.

View file

@ -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 {

View file

@ -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

View file

@ -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))
}

View file

@ -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)

View file

@ -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
}

View file

@ -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
}

View file

@ -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