Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ jobs:
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'

runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write

steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand All @@ -43,12 +43,11 @@ jobs:
- Performance considerations
- Security concerns
- Test coverage

Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.

Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.

# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'

3 changes: 1 addition & 2 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}

# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
Expand All @@ -47,4 +47,3 @@ jobs:
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
# claude_args: '--model claude-opus-4-1-20250805 --allowed-tools Bash(gh pr:*)'

5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,8 @@ TextGrabber2/Secrets.plist
*.sqlite
*.sqlite3



# Agent
.agent/
_antigravity/
File renamed without changes
21 changes: 12 additions & 9 deletions ScreenScribe.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,18 @@
B9583CC52DBD8C8300F43550 /* NSMenu+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9583C812DBD8C8300F43550 /* NSMenu+Extension.swift */; };
B9583CC62DBD8C8300F43550 /* SMAppService+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9583C872DBD8C8300F43550 /* SMAppService+Extension.swift */; };
B9F54FE92F10866C0070D1C2 /* ScreenCapturePermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9F54FE82F10866C0070D1C2 /* ScreenCapturePermissionManager.swift */; };
C1000001000000000000000A /* Prompt.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1000001000000000000000B /* Prompt.swift */; };
C1000002000000000000000A /* PromptManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1000002000000000000000B /* PromptManager.swift */; };
C1000003000000000000000A /* PromptEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1000003000000000000000B /* PromptEditorView.swift */; };
C1000004000000000000000A /* PromptListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1000004000000000000000B /* PromptListView.swift */; };
C2000001000000000000000A /* OnboardingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2000001000000000000000B /* OnboardingManager.swift */; };
C2000002000000000000000A /* OnboardingWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2000002000000000000000B /* OnboardingWindowController.swift */; };
C2000003000000000000000A /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2000003000000000000000B /* OnboardingView.swift */; };
C2000004000000000000000A /* WelcomeStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2000004000000000000000B /* WelcomeStepView.swift */; };
C2000005000000000000000A /* PermissionStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2000005000000000000000B /* PermissionStepView.swift */; };
C2000006000000000000000A /* APIKeyStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2000006000000000000000B /* APIKeyStepView.swift */; };
C2000007000000000000000A /* CompletionStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2000007000000000000000B /* CompletionStepView.swift */; };
C1000001000000000000000A /* Prompt.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1000001000000000000000B /* Prompt.swift */; };
C1000002000000000000000A /* PromptManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1000002000000000000000B /* PromptManager.swift */; };
C1000003000000000000000A /* PromptEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1000003000000000000000B /* PromptEditorView.swift */; };
C1000004000000000000000A /* PromptListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1000004000000000000000B /* PromptListView.swift */; };
F5B0C7647A784F30BB1B71D9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C128894D5D084D2FBAADA65C /* Assets.xcassets */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand Down Expand Up @@ -83,17 +84,18 @@
B9583C9F2DBD8C8300F43550 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
B9583CCA2DBD935300F43550 /* ScreenScribe.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ScreenScribe.app; sourceTree = BUILT_PRODUCTS_DIR; };
B9F54FE82F10866C0070D1C2 /* ScreenCapturePermissionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenCapturePermissionManager.swift; sourceTree = "<group>"; };
C1000001000000000000000B /* Prompt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Prompt.swift; sourceTree = "<group>"; };
C1000002000000000000000B /* PromptManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromptManager.swift; sourceTree = "<group>"; };
C1000003000000000000000B /* PromptEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromptEditorView.swift; sourceTree = "<group>"; };
C1000004000000000000000B /* PromptListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromptListView.swift; sourceTree = "<group>"; };
C128894D5D084D2FBAADA65C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
C2000001000000000000000B /* OnboardingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingManager.swift; sourceTree = "<group>"; };
C2000002000000000000000B /* OnboardingWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingWindowController.swift; sourceTree = "<group>"; };
C2000003000000000000000B /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
C2000004000000000000000B /* WelcomeStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeStepView.swift; sourceTree = "<group>"; };
C2000005000000000000000B /* PermissionStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionStepView.swift; sourceTree = "<group>"; };
C2000006000000000000000B /* APIKeyStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeyStepView.swift; sourceTree = "<group>"; };
C2000007000000000000000B /* CompletionStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionStepView.swift; sourceTree = "<group>"; };
C1000001000000000000000B /* Prompt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Prompt.swift; sourceTree = "<group>"; };
C1000002000000000000000B /* PromptManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromptManager.swift; sourceTree = "<group>"; };
C1000003000000000000000B /* PromptEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromptEditorView.swift; sourceTree = "<group>"; };
C1000004000000000000000B /* PromptListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromptListView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -181,6 +183,7 @@
B9583C9D2DBD8C8300F43550 /* ScreenScribe */ = {
isa = PBXGroup;
children = (
C128894D5D084D2FBAADA65C /* Assets.xcassets */,
B9583C972DBD8C8300F43550 /* Sources */,
B9583C982DBD8C8300F43550 /* Info.entitlements */,
B9583C992DBD8C8300F43550 /* Info.plist */,
Expand Down Expand Up @@ -284,6 +287,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F5B0C7647A784F30BB1B71D9 /* Assets.xcassets in Resources */,
B9583CA72DBD8C8300F43550 /* Secrets.plist in Resources */,
B9583CA82DBD8C8300F43550 /* Build.xcconfig in Resources */,
B9583CAC2DBD8C8300F43550 /* Screen Capture.aif in Resources */,
Expand All @@ -310,7 +314,6 @@
B9583CB82DBD8C8300F43550 /* SettingsManager.swift in Sources */,
B9583CB92DBD8C8300F43550 /* ShortcutMonitor.swift in Sources */,
B9583CBA2DBD8C8300F43550 /* NSImage+Extension.swift in Sources */,
B9F54FED2F1089BD0070D1C2 /* APIKeyStepView.swift in Sources */,
B9F54FE92F10866C0070D1C2 /* ScreenCapturePermissionManager.swift in Sources */,
B9583CBB2DBD8C8300F43550 /* main.swift in Sources */,
B9583CBC2DBD8C8300F43550 /* App.swift in Sources */,
Expand Down
20 changes: 20 additions & 0 deletions ScreenScribe/Assets.xcassets/AppIcon.appiconset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"images": [
{
"filename": "Icon_512.png",
"idiom": "mac",
"scale": "1x",
"size": "512x512"
},
{
"filename": "Icon_512.png",
"idiom": "mac",
"scale": "2x",
"size": "512x512"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions ScreenScribe/Assets.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
12 changes: 12 additions & 0 deletions ScreenScribe/Sources/Onboarding/PermissionStepView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ struct PermissionStepView: View {
.buttonStyle(.bordered)
.controlSize(.large)

Button(action: {
Task {
await permissionManager.requestPermissionAndStartMonitoring()
}
}) {
Text("Recheck Permission")
.frame(width: 200)
}
.buttonStyle(.bordered)
.controlSize(.large)
.disabled(permissionManager.hasPermission)

Button(action: {
manager.nextStep()
}) {
Expand Down
48 changes: 38 additions & 10 deletions ScreenScribe/Sources/Services/ScreenCapturePermissionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,34 @@ final class ScreenCapturePermissionManager: ObservableObject {
/// Verify permission using ScreenCaptureKit with retry logic
/// Returns true if permission is confirmed, false if all attempts fail
/// On macOS Sequoia, ScreenCaptureKit can throw errors at app launch even when permission is granted
private func verifyPermissionViaScreenCaptureKit(maxAttempts: Int = 3, delaySeconds: Double = 0.5) async -> Bool {
/// This is a known issue where the system needs time to initialize screen capture subsystems
private func verifyPermissionViaScreenCaptureKit(maxAttempts: Int = 5, delaySeconds: Double = 1.0) async -> Bool {
for attempt in 1...maxAttempts {
do {
_ = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
hasPermission = true
stopPolling()
Logger.log(.info, "ScreenCaptureKit permission verified on attempt \(attempt)")
return true
} catch let error as NSError {
// Log known error codes that indicate permission is actually granted
// but system is not ready yet (common on macOS Sequoia after restart)
// Known transient errors: -3801 (userDeclined), -3802 (failedToStart),
// -3803 (missingEntitlements), -3805 (systemStoppedStream)

Logger.log(.info, "ScreenCaptureKit attempt \(attempt)/\(maxAttempts) failed: \(error.localizedDescription) (domain: \(error.domain), code: \(error.code))")

if attempt < maxAttempts {
// Use exponential backoff for better handling of slow system initialization
let delay = delaySeconds * Double(attempt)
Logger.log(.info, "Waiting \(delay)s before retry...")
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
}
} catch {
Logger.log(.info, "ScreenCaptureKit attempt \(attempt) failed: \(error.localizedDescription)")
Logger.log(.info, "ScreenCaptureKit attempt \(attempt)/\(maxAttempts) failed with unexpected error: \(error.localizedDescription)")
if attempt < maxAttempts {
try? await Task.sleep(nanoseconds: UInt64(delaySeconds * 1_000_000_000))
let delay = delaySeconds * Double(attempt)
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
}
}
}
Expand All @@ -49,18 +65,29 @@ final class ScreenCapturePermissionManager: ObservableObject {

/// Request permission and start monitoring for changes
func requestPermissionAndStartMonitoring() async {
Logger.log(.info, "Starting permission request and monitoring...")

// First, check via ScreenCaptureKit with retries (more reliable on macOS Sequoia)
// CGPreflightScreenCaptureAccess can return false even when permission is granted
// ScreenCaptureKit can also throw errors at app launch, so we retry a few times
let hasPermissionNow = await verifyPermissionViaScreenCaptureKit(maxAttempts: 3, delaySeconds: 0.5)
// ScreenCaptureKit can also throw errors after restart/cold boot, so we retry with exponential backoff
// Use more attempts (5) and longer base delay (1.0s) for initial check since we might be starting from cold boot
let hasPermissionNow = await verifyPermissionViaScreenCaptureKit(maxAttempts: 5, delaySeconds: 1.0)

// If already granted, no need to show dialog
if hasPermissionNow {
Logger.log(.info, "Permission already granted (verified via ScreenCaptureKit with retries)")
return
}

// Only trigger the system permission dialog if truly not granted after retries
// Also try CGPreflightScreenCaptureAccess as a final check before showing dialog
// Sometimes it works even when ScreenCaptureKit fails
if CGPreflightScreenCaptureAccess() {
Logger.log(.info, "Permission detected via CGPreflightScreenCaptureAccess after ScreenCaptureKit failed")
hasPermission = true
return
}

// Only trigger the system permission dialog if truly not granted after all retry methods
Logger.log(.info, "Permission not detected after retries, showing system dialog")
let result = CGRequestScreenCaptureAccess()
hasPermission = result
Expand Down Expand Up @@ -109,15 +136,16 @@ final class ScreenCapturePermissionManager: ObservableObject {
if !hasPermission {
hasPermission = true
stopPolling()
Logger.log(.info, "Screen capture permission granted")
Logger.log(.info, "Screen capture permission granted (via CGPreflightScreenCaptureAccess)")
}
return
}

// Fallback: check via ScreenCaptureKit (single attempt since we're polling)
let verified = await verifyPermissionViaScreenCaptureKit(maxAttempts: 1, delaySeconds: 0)
// Fallback: check via ScreenCaptureKit with a couple of attempts
// On macOS Sequoia, permission detection can be flaky even during polling
let verified = await verifyPermissionViaScreenCaptureKit(maxAttempts: 2, delaySeconds: 0.5)
if verified {
Logger.log(.info, "Screen capture permission granted (via ScreenCaptureKit)")
Logger.log(.info, "Screen capture permission granted (via ScreenCaptureKit polling)")
}
}

Expand Down
Loading