From 4004cf63a5617a49932cf267a1a5af8ec7aebba5 Mon Sep 17 00:00:00 2001 From: Niduank Date: Tue, 16 Jun 2026 10:37:57 +0200 Subject: [PATCH] Remove Secrets.xcconfig + SecretsManager system Remove the Secrets.xcconfig build configuration, the SecretsManager helper, and the internal non-accessibility-trigger dataset collection feature that depended on it (Azure upload): NonAXTriggerDatasetUploader, AzureBlobUploader, the debug labeling UI/window controller, the false-negative report button, and their hooks in the trigger service and hint view. Drop the now-unused 'hide bug report emoji' demo toggle. Clean up the project baseConfiguration references, the Secrets Check build phase step, git hooks and docs. --- .githooks/install.sh | 20 +- .githooks/post-checkout | 15 - .githooks/pre-commit | 15 - .gitignore | 3 - CLAUDE.md | 9 +- macos/OnitQuickEdit.xcodeproj/project.pbxproj | 11 +- .../Data/Persistence/Defaults.swift | 3 - .../Helpers/SecretsManager.swift | 55 -- ...ckEditNonAccessibilityTriggerService.swift | 71 -- .../QuickEdit/UI/Hint/QuickEditHintView.swift | 23 - macos/OnitQuickEdit/Secrets.xcconfig.sample | 23 - .../Settings/Pages/SettingsQuickEditDev.swift | 17 - .../UI/Debug/AzureBlobUploader.swift | 114 --- .../UI/Debug/FalseNegativeReportButton.swift | 185 ---- .../Debug/NonAXTriggerDatasetUploader.swift | 75 -- .../UI/Debug/NonAXTriggerLabelingView.swift | 820 ------------------ ...NonAXTriggerLabelingWindowController.swift | 72 -- macos/com.synth.environment.plist.sample | 44 - 18 files changed, 3 insertions(+), 1572 deletions(-) delete mode 100755 .githooks/pre-commit delete mode 100644 macos/OnitQuickEdit/Helpers/SecretsManager.swift delete mode 100644 macos/OnitQuickEdit/Secrets.xcconfig.sample delete mode 100644 macos/OnitQuickEdit/UI/Debug/AzureBlobUploader.swift delete mode 100644 macos/OnitQuickEdit/UI/Debug/FalseNegativeReportButton.swift delete mode 100644 macos/OnitQuickEdit/UI/Debug/NonAXTriggerDatasetUploader.swift delete mode 100644 macos/OnitQuickEdit/UI/Debug/NonAXTriggerLabelingView.swift delete mode 100644 macos/OnitQuickEdit/UI/Debug/NonAXTriggerLabelingWindowController.swift delete mode 100644 macos/com.synth.environment.plist.sample diff --git a/.githooks/install.sh b/.githooks/install.sh index 4d5d165..01873c4 100755 --- a/.githooks/install.sh +++ b/.githooks/install.sh @@ -19,27 +19,9 @@ echo "Setting up git hooks..." # Configure git to use the shared hooks directory git config core.hooksPath .githooks -# Create Secrets.xcconfig from sample if it doesn't exist -SECRETS_FILE="macos/OnitQuickEdit/Secrets.xcconfig" -SECRETS_SAMPLE="macos/OnitQuickEdit/Secrets.xcconfig.sample" - -if [ ! -f "$SECRETS_FILE" ] && [ -f "$SECRETS_SAMPLE" ]; then - cp "$SECRETS_SAMPLE" "$SECRETS_FILE" - echo "✓ Created Secrets.xcconfig from sample" - echo " → Please edit $SECRETS_FILE with your API keys" -fi - -# Create backup if Secrets exists but backup doesn't -SECRETS_BACKUP="macos/OnitQuickEdit/Secrets.xcconfig.backup" -if [ -f "$SECRETS_FILE" ] && [ ! -f "$SECRETS_BACKUP" ]; then - cp "$SECRETS_FILE" "$SECRETS_BACKUP" - echo "✓ Created Secrets.xcconfig.backup" -fi - echo "" echo "✓ Git hooks installed successfully!" echo "" echo "What this does:" -echo " • post-checkout: Restores Secrets.xcconfig + updates submodules + cleans SPM cache" +echo " • post-checkout: Updates submodules + cleans SPM cache" echo " • post-merge: Updates submodules after pull + cleans SPM cache" -echo " • pre-commit: Prevents accidentally committing secrets" diff --git a/.githooks/post-checkout b/.githooks/post-checkout index 9ac9224..293a907 100755 --- a/.githooks/post-checkout +++ b/.githooks/post-checkout @@ -6,21 +6,6 @@ # Created by Kévin Naudin on 2026-01-23. # -SECRETS_FILE="macos/OnitQuickEdit/Secrets.xcconfig" -SECRETS_BACKUP="macos/OnitQuickEdit/Secrets.xcconfig.backup" -SECRETS_SAMPLE="macos/OnitQuickEdit/Secrets.xcconfig.sample" - -# If Secrets.xcconfig was deleted during checkout, restore it -if [ ! -f "$SECRETS_FILE" ]; then - if [ -f "$SECRETS_BACKUP" ]; then - cp "$SECRETS_BACKUP" "$SECRETS_FILE" - echo "✓ Secrets.xcconfig restored from backup" - elif [ -f "$SECRETS_SAMPLE" ]; then - cp "$SECRETS_SAMPLE" "$SECRETS_FILE" - echo "⚠ Secrets.xcconfig created from sample - please fill in your values" - fi -fi - # Update submodules after checkout # --init: Initialize any submodules not yet initialized # --recursive: Also update nested submodules diff --git a/.githooks/pre-commit b/.githooks/pre-commit deleted file mode 100755 index 0a62764..0000000 --- a/.githooks/pre-commit +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -# -# pre-commit -# Onit -# -# Created by Kévin Naudin on 2026-01-23. -# - -# Prevent committing Secrets.xcconfig (but allow .sample) -if git diff --cached --name-only | grep -q "Secrets.xcconfig$"; then - echo "❌ ERROR: Attempting to commit Secrets.xcconfig" - echo " This file contains sensitive data and should never be committed." - echo " Please unstage it with: git reset HEAD macos/OnitQuickEdit/Secrets.xcconfig" - exit 1 -fi diff --git a/.gitignore b/.gitignore index 8870323..c5f4c55 100644 --- a/.gitignore +++ b/.gitignore @@ -250,9 +250,6 @@ dist __pycache__/ .venv/ -# Secrets and sensitive configuration -macos/OnitQuickEdit/Secrets.xcconfig -macos/OnitQuickEdit/Secrets.xcconfig.backup .gstack/ # Former submodules / private tooling kept on disk only (quickedit-only) diff --git a/CLAUDE.md b/CLAUDE.md index d7a1460..56580b8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,18 +25,11 @@ is the QuickEdit-only strip of onit-beacon, headed to its own private repo **After creating a new worktree or switching to a new branch, always run:** ```bash -# 1. Copy Secrets.xcconfig from the main onit-beacon repo (not tracked in git; -# the main repo still keeps it under macos/Onit/) -cp /Users/kevinnaudin/SynthInc/onit-beacon/macos/Onit/Secrets.xcconfig macos/OnitQuickEdit/Secrets.xcconfig - -# 2. Install git hooks (cleans SPM cache) +# Install git hooks (cleans SPM cache) ./.githooks/install.sh ``` ## File Locations -### Configuration -- `macos/OnitQuickEdit/Secrets.xcconfig` - API keys (base64 encoded) - ### Documentation - `macos/OnitQuickEdit/PERMISSIONS.md` - Permissions model (Accessibility, Screen Recording) diff --git a/macos/OnitQuickEdit.xcodeproj/project.pbxproj b/macos/OnitQuickEdit.xcodeproj/project.pbxproj index 77a92c9..2268b08 100644 --- a/macos/OnitQuickEdit.xcodeproj/project.pbxproj +++ b/macos/OnitQuickEdit.xcodeproj/project.pbxproj @@ -270,7 +270,6 @@ inputFileListPaths = ( ); inputPaths = ( - "$(SRCROOT)/Onit/Secrets.xcconfig", ); name = "Environment Checks"; outputFileListPaths = ( @@ -279,7 +278,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -e\n\n# ============================================\n# Git Hooks Check\n# ============================================\necho \"🔧 Checking git hooks configuration...\"\n\nREPO_ROOT=$(cd \"${SRCROOT}/..\" && git rev-parse --show-toplevel 2>/dev/null || echo \"\")\n\nif [ -n \"$REPO_ROOT\" ]; then\n HOOKS_PATH=$(cd \"$REPO_ROOT\" && git config core.hooksPath 2>/dev/null || echo \"\")\n \n if [ \"$HOOKS_PATH\" != \".githooks\" ]; then\n echo \"error: \"\n echo \"error: ════════════════════════════════════════════════════════════\"\n echo \"error: 👋 Welcome to the Onit team!\"\n echo \"error: ════════════════════════════════════════════════════════════\"\n echo \"error: \"\n echo \"error: One last step before you can build:\"\n echo \"error: \"\n echo \"error: Run this command from the repository root:\"\n echo \"error: \"\n echo \"error: ./.githooks/install.sh\"\n echo \"error: \"\n echo \"error: This configures git hooks for the team workflow.\"\n echo \"error: \"\n echo \"error: ════════════════════════════════════════════════════════════\"\n echo \"error: \"\n exit 1\n else\n echo \"✓ Git hooks configured\"\n fi\nelse\n echo \"error: ❌ Not in a git repository\"\n exit 1\nfi\n\n# ============================================\n# Secrets Check\n# ============================================\necho \"🔐 Checking secrets configuration...\"\n\n# Check if GH_READONLY_TOKEN is available in build settings\nif [ -z \"$GH_READONLY_TOKEN\" ]; then\n echo \"error: ❌ GH_READONLY_TOKEN not found in build settings\"\n echo \"error: 📝 Make sure Secrets.xcconfig is:\"\n echo \"error: 1. Created at ${SRCROOT}/Onit/Secrets.xcconfig\"\n echo \"error: 2. Contains: GH_READONLY_TOKEN = \"\n echo \"error: 3. Assigned in Project Settings → Info → Configurations\"\n exit 1\nfi\n\necho \"✓ GH_READONLY_TOKEN is configured\"\n\n# Check if GITHUB_REPO is available in build settings\nif [ -z \"$GITHUB_REPO\" ]; then\n echo \"error: ❌ GITHUB_REPO not found in build settings\"\n echo \"error: 📝 Make sure Secrets.xcconfig contains:\"\n echo \"error: GITHUB_REPO = synth-inc/onit-brain\"\n exit 1\nfi\n\necho \"✓ GITHUB_REPO is configured: $GITHUB_REPO\"\n\n# Verify that Secrets.xcconfig file exists\nSECRETS_FILE=\"${SRCROOT}/Onit/Secrets.xcconfig\"\nif [ ! -f \"$SECRETS_FILE\" ]; then\n echo \"warning: ⚠️ Secrets.xcconfig file not found at expected location\"\n echo \"warning: 💡 But build settings are configured, so build will proceed\"\nfi\n\necho \"✅ All environment checks passed!\"\n"; + shellScript = "set -e\n\n# ============================================\n# Git Hooks Check\n# ============================================\necho \"🔧 Checking git hooks configuration...\"\n\nREPO_ROOT=$(cd \"${SRCROOT}/..\" && git rev-parse --show-toplevel 2>/dev/null || echo \"\")\n\nif [ -n \"$REPO_ROOT\" ]; then\n HOOKS_PATH=$(cd \"$REPO_ROOT\" && git config core.hooksPath 2>/dev/null || echo \"\")\n \n if [ \"$HOOKS_PATH\" != \".githooks\" ]; then\n echo \"error: \"\n echo \"error: ════════════════════════════════════════════════════════════\"\n echo \"error: 👋 Welcome to the Onit team!\"\n echo \"error: ════════════════════════════════════════════════════════════\"\n echo \"error: \"\n echo \"error: One last step before you can build:\"\n echo \"error: \"\n echo \"error: Run this command from the repository root:\"\n echo \"error: \"\n echo \"error: ./.githooks/install.sh\"\n echo \"error: \"\n echo \"error: This configures git hooks for the team workflow.\"\n echo \"error: \"\n echo \"error: ════════════════════════════════════════════════════════════\"\n echo \"error: \"\n exit 1\n else\n echo \"✓ Git hooks configured\"\n fi\nelse\n echo \"error: ❌ Not in a git repository\"\n exit 1\nfi\n\necho \"✅ All environment checks passed!\"\n"; }; 864FE87F2E97AD56004BFC07 /* Download and Copy ripgrep Binary */ = { isa = PBXShellScriptBuildPhase; @@ -354,8 +353,6 @@ /* Begin XCBuildConfiguration section */ 2883E4C02CA5ECBC00F79A48 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReferenceAnchor = 2883E4B52CA5ECBB00F79A48 /* Onit */; - baseConfigurationReferenceRelativePath = Secrets.xcconfig; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -421,8 +418,6 @@ }; 2883E4C12CA5ECBC00F79A48 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReferenceAnchor = 2883E4B52CA5ECBB00F79A48 /* Onit */; - baseConfigurationReferenceRelativePath = Secrets.xcconfig; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -481,8 +476,6 @@ }; 2883E4C32CA5ECBC00F79A48 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReferenceAnchor = 2883E4B52CA5ECBB00F79A48 /* Onit */; - baseConfigurationReferenceRelativePath = Secrets.xcconfig; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -535,8 +528,6 @@ }; 2883E4C42CA5ECBC00F79A48 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReferenceAnchor = 2883E4B52CA5ECBB00F79A48 /* Onit */; - baseConfigurationReferenceRelativePath = Secrets.xcconfig; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; diff --git a/macos/OnitQuickEdit/Data/Persistence/Defaults.swift b/macos/OnitQuickEdit/Data/Persistence/Defaults.swift index 43562d6..5a9a9c9 100644 --- a/macos/OnitQuickEdit/Data/Persistence/Defaults.swift +++ b/macos/OnitQuickEdit/Data/Persistence/Defaults.swift @@ -117,9 +117,6 @@ extension Defaults.Keys { static let stopModeUserConfigured = Key("stopModeUserConfigured", default: false) // QuickEdit - #if DEBUG || ONIT_BETA - static let hideBugReportEmoji = Key("hideBugReportEmoji", default: false) - #endif static let quickEditConfig = Key("quickEditConfig", default: .default) static let quickEditMode = Key("quickEditMode", default: .remote) static let quickEditRemoteModel = Key("quickEditRemoteModel", default: nil) diff --git a/macos/OnitQuickEdit/Helpers/SecretsManager.swift b/macos/OnitQuickEdit/Helpers/SecretsManager.swift deleted file mode 100644 index a8cb2fb..0000000 --- a/macos/OnitQuickEdit/Helpers/SecretsManager.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// SecretsManager.swift -// Onit -// -// Created by Kévin Naudin on 10/10/2025. -// - -import Foundation - -/// Manages access to application secrets. -/// -/// All secrets are read from environment variables at runtime — never bundled. -/// -/// The Azure key must be set with launchctl (required by GUI apps): -/// -/// launchctl setenv AZURE_NON_AX_TRIGGER_SAS_KEY "..." -/// -/// See macos/com.synth.environment.plist.sample for a LaunchAgent that sets -/// it automatically on login (recommended for team development). -enum SecretsManager { - - // MARK: - Secret Keys - - private enum SecretKey: String { - case azureNonAXTriggerSasKey = "AZURE_NON_AX_TRIGGER_SAS_KEY" - } - - // MARK: - Azure Blob Storage - - /// Retrieves the Azure Blob Storage base URL. - static func getAzureBlobBaseUrl() -> String? { - return "https://syntheticco.blob.core.windows.net" - } - - /// Retrieves the Azure non-AX trigger container name. - static func getAzureNonAXTriggerContainer() -> String? { - return "non-ax-trigger" - } - - /// Retrieves the Azure non-AX trigger SAS key from environment. - static func getAzureNonAXTriggerSasKey() -> String? { - return getFromEnvironment(.azureNonAXTriggerSasKey) - } - - // MARK: - Private - - /// Reads a secret from process environment variables (plain text, not Base64). - private static func getFromEnvironment(_ key: SecretKey) -> String? { - guard let value = ProcessInfo.processInfo.environment[key.rawValue], - !value.isEmpty else { - return nil - } - return value - } -} diff --git a/macos/OnitQuickEdit/QuickEdit/Triggers/QuickEditNonAccessibilityTriggerService.swift b/macos/OnitQuickEdit/QuickEdit/Triggers/QuickEditNonAccessibilityTriggerService.swift index 481506e..d1d9188 100644 --- a/macos/OnitQuickEdit/QuickEdit/Triggers/QuickEditNonAccessibilityTriggerService.swift +++ b/macos/OnitQuickEdit/QuickEdit/Triggers/QuickEditNonAccessibilityTriggerService.swift @@ -13,18 +13,6 @@ import Defaults import Foundation import UniformTypeIdentifiers -// MARK: - Debug Capture (DEBUG only) - -#if DEBUG || ONIT_BETA -struct NonAXTriggerDebugCapture { - let beforeImage: CGImage - let afterImage: CGImage - let changedRegions: [CGRect] - let mouseLocation: CGPoint - let timestamp: Date -} -#endif - // MARK: - Configuration struct NonAccessibilityTriggerConfig { @@ -84,10 +72,6 @@ final class QuickEditNonAccessibilityTriggerService: NSObject { static let shared = QuickEditNonAccessibilityTriggerService() - #if DEBUG || ONIT_BETA - static var latestDebugCapture: NonAXTriggerDebugCapture? - #endif - // MARK: - Properties weak var delegate: QuickEditTriggerServiceDelegate? @@ -108,7 +92,6 @@ final class QuickEditNonAccessibilityTriggerService: NSObject { private var beforeScreenshotWindowFrame: CGRect? private var captureTask: Task? - // Cleanup task for screenshots that aren't used (e.g., simple click with no follow-up action) private var screenshotCleanupTask: Task? @@ -458,13 +441,6 @@ final class QuickEditNonAccessibilityTriggerService: NSObject { } guard let validRegions = analysis.validRegions else { - #if DEBUG || ONIT_BETA - showFalseNegativeReportButton( - beforeImage: beforeImage, - afterImage: afterImage, - groupedRegions: analysis.groupedRegions - ) - #endif return } @@ -476,17 +452,6 @@ final class QuickEditNonAccessibilityTriggerService: NSObject { let aspectRatio = selectedRegion.width / selectedRegion.height print("[NonAccessibilityTrigger] Selected region: \(Int(selectedRegion.width))x\(Int(selectedRegion.height)) | aspect: \(String(format: "%.2f", aspectRatio)) (closest to mouse at \(mouseLocation))") - // Store debug capture for labeling UI (DEBUG only) - #if DEBUG || ONIT_BETA - QuickEditNonAccessibilityTriggerService.latestDebugCapture = NonAXTriggerDebugCapture( - beforeImage: beforeImage, - afterImage: afterImage, - changedRegions: analysis.groupedRegions, - mouseLocation: mouseLocation, - timestamp: Date() - ) - #endif - // Step 5: Convert to screen coordinates and trigger QuickEdit await triggerQuickEdit(with: selectedRegion, appName: appName, reason: reason, checkpoint: checkpoint) checkpoint("Hint displayed ✅") @@ -568,28 +533,6 @@ final class QuickEditNonAccessibilityTriggerService: NSObject { } } - // MARK: - Debug False Negative Reporting - - #if DEBUG || ONIT_BETA - /// Shows the scream emoji button near mouse when detection rejects regions. - /// User can click it to save a false negative test case. - private func showFalseNegativeReportButton( - beforeImage: CGImage, - afterImage: CGImage, - groupedRegions: [CGRect] - ) { - let mouseLocation = NSEvent.mouseLocation - let capture = NonAXTriggerDebugCapture( - beforeImage: beforeImage, - afterImage: afterImage, - changedRegions: groupedRegions, - mouseLocation: mouseLocation, - timestamp: Date() - ) - FalseNegativeReportButtonPresenter.shared.show(near: mouseLocation, capture: capture) - } - #endif - // MARK: - Debug Screenshot Saving private func saveDebugScreenshots(before: CGImage, after: CGImage, reason: String) { @@ -880,9 +823,6 @@ final class QuickEditNonAccessibilityTriggerService: NSObject { // Track shift key state if modifiers.shift && !isShiftDown { isShiftDown = true - #if DEBUG || ONIT_BETA - FalseNegativeReportButtonPresenter.shared.dismiss() - #endif captureBeforeScreenshotIfNeeded(reason: "Shift down") } else if !modifiers.shift && isShiftDown { isShiftDown = false @@ -891,9 +831,6 @@ final class QuickEditNonAccessibilityTriggerService: NSObject { // Track command key state if modifiers.command && !isCommandDown { isCommandDown = true - #if DEBUG || ONIT_BETA - FalseNegativeReportButtonPresenter.shared.dismiss() - #endif captureBeforeScreenshotIfNeeded(reason: "Cmd down") } else if !modifiers.command && isCommandDown { isCommandDown = false @@ -905,10 +842,6 @@ final class QuickEditNonAccessibilityTriggerService: NSObject { if modifiers.command && !modifiers.shift && !modifiers.control && !modifiers.option { let character = event.event.charactersIgnoringModifiers?.lowercased() if character == "a" && isKeyDown { - // Dismiss the false negative report button on new selection - #if DEBUG || ONIT_BETA - FalseNegativeReportButtonPresenter.shared.dismiss() - #endif // Before screenshot already captured on Cmd down // Use 300ms additional delay for instant selection to render scheduleTrigger(reason: "Cmd+A (Select All)", afterDelay: 300) @@ -1005,10 +938,6 @@ extension QuickEditNonAccessibilityTriggerService: MouseNotificationDelegate { } func mouseNotificationManager(_ manager: MouseNotificationManager, didReceiveSingleClick event: NSEvent) { - // Dismiss the false negative report button if click is outside it - #if DEBUG || ONIT_BETA - FalseNegativeReportButtonPresenter.shared.dismissIfClickOutside(at: NSEvent.mouseLocation) - #endif let isShiftClick = event.modifierFlags.contains(.shift) diff --git a/macos/OnitQuickEdit/QuickEdit/UI/Hint/QuickEditHintView.swift b/macos/OnitQuickEdit/QuickEdit/UI/Hint/QuickEditHintView.swift index 4cfc98b..91c67be 100644 --- a/macos/OnitQuickEdit/QuickEdit/UI/Hint/QuickEditHintView.swift +++ b/macos/OnitQuickEdit/QuickEdit/UI/Hint/QuickEditHintView.swift @@ -182,12 +182,6 @@ struct QuickEditHintView: View { ActionButton(action: action) } } - - #if DEBUG || ONIT_BETA - if !Defaults[.hideBugReportEmoji] { - debugLabelingButton - } - #endif } .padding(.leading, 5) .padding([.vertical, .trailing], 3) @@ -312,21 +306,4 @@ struct QuickEditHintView: View { } } } - - #if DEBUG || ONIT_BETA - private var debugLabelingButton: some View { - Button { - if let capture = QuickEditNonAccessibilityTriggerService.latestDebugCapture { - NonAXTriggerLabelingWindowController.shared.show(capture: capture) - } - } label: { - Text("😱") - .font(.system(size: 14)) - } - .buttonStyle(.plain) - .padding(.horizontal, 4) - .opacity(QuickEditNonAccessibilityTriggerService.latestDebugCapture != nil ? 1 : 0.3) - .disabled(QuickEditNonAccessibilityTriggerService.latestDebugCapture == nil) - } - #endif } diff --git a/macos/OnitQuickEdit/Secrets.xcconfig.sample b/macos/OnitQuickEdit/Secrets.xcconfig.sample deleted file mode 100644 index dc10918..0000000 --- a/macos/OnitQuickEdit/Secrets.xcconfig.sample +++ /dev/null @@ -1,23 +0,0 @@ -// -// Secrets.xcconfig.sample -// Onit -// -// This file is a template for BUILD-TIME configuration values only. -// Copy this file to Secrets.xcconfig and fill in your actual values. -// DO NOT commit Secrets.xcconfig to version control. -// -// ───────────────────────────────────────────────────────────── -// SECRETS ARE NO LONGER STORED HERE -// ───────────────────────────────────────────────────────────── -// All secrets are read from environment variables at runtime. -// They are never embedded in the app bundle. -// -// The Azure key is read by the built .app and Xcode, which are GUI apps -// and do NOT read ~/.zshenv or ~/.zshrc — set it with launchctl: -// See com.synth.environment.plist.sample for the recommended setup. -// Or set it manually: -// launchctl setenv AZURE_NON_AX_TRIGGER_SAS_KEY "..." -// (then relaunch the app) -// ───────────────────────────────────────────────────────────── - -// Azure non-secret config is hardcoded in SecretsManager.swift — nothing needed here. diff --git a/macos/OnitQuickEdit/Settings/Pages/SettingsQuickEditDev.swift b/macos/OnitQuickEdit/Settings/Pages/SettingsQuickEditDev.swift index da577ca..daaab6c 100644 --- a/macos/OnitQuickEdit/Settings/Pages/SettingsQuickEditDev.swift +++ b/macos/OnitQuickEdit/Settings/Pages/SettingsQuickEditDev.swift @@ -30,7 +30,6 @@ struct SettingsQuickEditDev: View { #if DEBUG || ONIT_BETA experimentalTriggersSection - demoModeSection trainingDataSection #endif } @@ -114,8 +113,6 @@ struct SettingsQuickEditDev: View { // MARK: - Child Components: Experimental Triggers Section #if DEBUG || ONIT_BETA - @Default(.hideBugReportEmoji) private var hideBugReportEmoji - private var experimentalTriggersSection: some View { SettingsPageSection(title: .init(text: String.localized("Experimental Triggers", table: "QuickEdit"))) { SettingsPageSubsection( @@ -128,20 +125,6 @@ struct SettingsQuickEditDev: View { } } - // MARK: - Child Components: Demo Mode Section - - private var demoModeSection: some View { - SettingsPageSection(title: .init(text: String.localized("Demo Mode", table: "QuickEdit"))) { - SettingsPageSubsection( - header: .init( - title: String.localized("Hide bug report emoji", table: "QuickEdit"), - subtitle: String.localized("Hides the 😱 emoji button used for reporting errors. Useful for recording demo videos.", table: "QuickEdit") - ), - isOn: self.$hideBugReportEmoji - ) - } - } - // MARK: - Child Components: Training Data Section private var trainingDataSection: some View { diff --git a/macos/OnitQuickEdit/UI/Debug/AzureBlobUploader.swift b/macos/OnitQuickEdit/UI/Debug/AzureBlobUploader.swift deleted file mode 100644 index 4a94021..0000000 --- a/macos/OnitQuickEdit/UI/Debug/AzureBlobUploader.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// AzureBlobUploader.swift -// Onit -// -// Created by Kévin Naudin on 21/01/2026. -// - -import Foundation - -/// Configuration for Azure Blob Storage connection -struct AzureConfig: Sendable { - let baseURL: String - let container: String - let sasKey: String - - var isConfigured: Bool { - !baseURL.isEmpty && !container.isEmpty && !sasKey.isEmpty - } -} - -/// Error types for Azure Blob upload operations -enum AzureUploadError: Error { - case missingConfiguration - case invalidURL - case uploadFailed(Error) - case fileNotFound -} - -/// Base actor for uploading files to Azure Blob Storage. -/// Provides common upload functionality for dataset collection. -actor AzureBlobUploader { - - // MARK: - Properties - - private let session: URLSession - let config: AzureConfig - private let logPrefix: String - - // MARK: - Initialization - - init(config: AzureConfig, logPrefix: String) { - self.config = config - self.logPrefix = logPrefix - - let sessionConfig = URLSessionConfiguration.default - sessionConfig.timeoutIntervalForRequest = 60 - sessionConfig.timeoutIntervalForResource = 300 - self.session = URLSession(configuration: sessionConfig) - } - - // MARK: - Public Methods - - /// Upload a file to Azure Blob Storage - /// - Parameters: - /// - localURL: Local file URL to upload - /// - remotePath: Remote path in the container (e.g., "case_001/audio.wav") - /// - contentType: MIME type of the file - func uploadFile(localURL: URL, remotePath: String, contentType: String) async throws { - guard FileManager.default.fileExists(atPath: localURL.path) else { - throw AzureUploadError.fileNotFound - } - - let encodedSasKey = config.sasKey.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? config.sasKey - let urlString = "\(config.baseURL)/\(config.container)/\(remotePath)?\(encodedSasKey)" - - guard let url = URL(string: urlString) else { - throw AzureUploadError.invalidURL - } - - var request = URLRequest(url: url) - request.httpMethod = "PUT" - request.setValue(contentType, forHTTPHeaderField: "Content-Type") - request.setValue("BlockBlob", forHTTPHeaderField: "x-ms-blob-type") - request.setValue("2023-11-03", forHTTPHeaderField: "x-ms-version") - request.setValue(formattedDate, forHTTPHeaderField: "x-ms-date") - - do { - let (_, response) = try await session.upload(for: request, fromFile: localURL) - - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { - let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1 - throw AzureUploadError.uploadFailed(NSError(domain: "HTTP", code: statusCode)) - } - } catch let error as AzureUploadError { - throw error - } catch { - throw AzureUploadError.uploadFailed(error) - } - } - - /// Upload metadata.json file from a case directory - /// - Parameters: - /// - caseDirectory: Local directory containing metadata.json - /// - remotePath: Remote base path for the case - func uploadMetadata(from caseDirectory: URL, remotePath: String) async throws { - let metadataURL = caseDirectory.appendingPathComponent("metadata.json") - try await uploadFile( - localURL: metadataURL, - remotePath: "\(remotePath)/metadata.json", - contentType: "application/json" - ) - } - - // MARK: - Private Methods - - private var formattedDate: String { - let formatter = DateFormatter() - formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss 'GMT'" - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.timeZone = TimeZone(secondsFromGMT: 0) - return formatter.string(from: Date()) - } -} diff --git a/macos/OnitQuickEdit/UI/Debug/FalseNegativeReportButton.swift b/macos/OnitQuickEdit/UI/Debug/FalseNegativeReportButton.swift deleted file mode 100644 index e54bc01..0000000 --- a/macos/OnitQuickEdit/UI/Debug/FalseNegativeReportButton.swift +++ /dev/null @@ -1,185 +0,0 @@ -// -// FalseNegativeReportButton.swift -// Onit -// -// Debug-only floating button that appears near the mouse when non-AX trigger -// detection rejects regions. Clicking opens the labeling UI for the test case. -// - -#if DEBUG || ONIT_BETA -import Defaults -import SwiftUI - -struct FalseNegativeReportButtonView: View { - let onTap: () -> Void - - var body: some View { - Button(action: onTap) { - HStack(spacing: 4) { - Text("😱") - .font(.system(size: 14)) - Text("is text highlighted?") - .font(.system(size: 11, weight: .medium)) - .foregroundColor(.primary.opacity(0.8)) - } - } - .buttonStyle(.plain) - .padding(.vertical, 3) - .padding(.horizontal, 6) - .background(Color(NSColor.windowBackgroundColor).opacity(0.9)) - .background(.ultraThinMaterial) - .cornerRadius(9) - .overlay( - RoundedRectangle(cornerRadius: 9) - .stroke(Color.primary.opacity(0.15), lineWidth: 1) - ) - } -} - -@MainActor -final class FalseNegativeReportButtonPresenter { - static let shared = FalseNegativeReportButtonPresenter() - - private var window: NSWindow? - private var dismissTask: Task? - private var pendingCapture: NonAXTriggerDebugCapture? - - /// Size of the button view (approximate, will be measured) - private let buttonSize = CGSize(width: 130, height: 24) - - func show(near mouseLocation: CGPoint, capture: NonAXTriggerDebugCapture) { - guard !Defaults[.hideBugReportEmoji] else { return } - - // Cancel any existing dismiss task - dismissTask?.cancel() - - // Store capture for when button is clicked - pendingCapture = capture - - // Close existing window if any - if let window { - window.orderOut(nil) - self.window = nil - } - - // Calculate anchor bounds from changed regions - // Convert from image coordinates (pixels) to screen coordinates (points) - let anchorBounds: CGRect? = { - guard !capture.changedRegions.isEmpty else { return nil } - - // Get window frame for coordinate conversion - guard let pid = NSWorkspace.shared.frontmostApplication?.processIdentifier, - let mainWindow = pid.firstMainWindow, - let windowFrame = mainWindow.getFrame(convertedToGlobalCoordinateSpace: true) else { - return nil - } - - // Union all changed regions - var bounds = capture.changedRegions[0] - for region in capture.changedRegions.dropFirst() { - bounds = bounds.union(region) - } - - // Convert from image pixels to screen points - let scaleFactor = NSScreen.main?.backingScaleFactor ?? 2.0 - return CGRect( - x: windowFrame.minX + bounds.minX / scaleFactor, - y: windowFrame.minY + bounds.minY / scaleFactor, - width: bounds.width / scaleFactor, - height: bounds.height / scaleFactor - ) - }() - - // Use SmartUIPositioner for placement - Task { @MainActor in - let config = UIPositioningConfig( - uiSize: buttonSize, - searchPaddingX: 100, - searchPaddingY: 50, - useComplexityAnalysis: true, - horizontalBias: 0.05, - proximityBias: 0.1, - hintPadding: 4 - ) - - let position: CGPoint - if let result = await SmartUIPositioner.shared.findOptimalPosition( - anchorPoint: mouseLocation, - anchorBounds: anchorBounds, - config: config - ) { - position = result.displayArea.origin - } else { - // Fallback: position near mouse - position = CGPoint(x: mouseLocation.x + 20, y: mouseLocation.y - buttonSize.height / 2) - } - - self.showWindow(at: position) - } - } - - private func showWindow(at position: CGPoint) { - let window = NSWindow( - contentRect: NSRect(origin: .zero, size: buttonSize), - styleMask: [.borderless], - backing: .buffered, - defer: false - ) - window.isOpaque = false - window.backgroundColor = .clear - window.level = .floating - window.ignoresMouseEvents = false - window.hasShadow = false - - let hostingView = NSHostingView(rootView: FalseNegativeReportButtonView(onTap: { [weak self] in - self?.handleTap() - })) - hostingView.frame = NSRect(origin: .zero, size: buttonSize) - window.contentView = hostingView - - window.setFrameOrigin(position) - window.makeKeyAndOrderFront(nil) - self.window = window - - // Auto-dismiss after 5 seconds - dismissTask = Task { @MainActor in - do { - try await Task.sleep(nanoseconds: 5_000_000_000) - self.dismiss() - } catch { - // Cancelled - } - } - } - - func dismiss() { - dismissTask?.cancel() - dismissTask = nil - window?.orderOut(nil) - window = nil - pendingCapture = nil - } - - /// Dismisses the button only if the given screen location is outside the button window. - /// Use this for global click handlers to avoid dismissing when clicking on the button itself. - func dismissIfClickOutside(at screenLocation: CGPoint) { - guard let window = window else { return } - if !window.frame.contains(screenLocation) { - dismiss() - } - } - - private func handleTap() { - guard let capture = pendingCapture else { - dismiss() - return - } - - // Dismiss the button first - dismiss() - - // Open the labeling UI with the captured data - NonAXTriggerLabelingWindowController.shared.show(capture: capture) - } -} -#endif diff --git a/macos/OnitQuickEdit/UI/Debug/NonAXTriggerDatasetUploader.swift b/macos/OnitQuickEdit/UI/Debug/NonAXTriggerDatasetUploader.swift deleted file mode 100644 index f39e2cf..0000000 --- a/macos/OnitQuickEdit/UI/Debug/NonAXTriggerDatasetUploader.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// NonAXTriggerDatasetUploader.swift -// Onit -// -// Created on 16/01/2026. -// - -#if DEBUG || ONIT_BETA -import Foundation - -/// Uploads non-AX trigger dataset cases to Azure Blob Storage for collection from beta testers. -actor NonAXTriggerDatasetUploader { - - // MARK: - Singleton - - static let shared = NonAXTriggerDatasetUploader() - - // MARK: - Private Properties - - private let uploader: AzureBlobUploader - - // MARK: - Initialization - - private init() { - let config = AzureConfig( - baseURL: SecretsManager.getAzureBlobBaseUrl() ?? "", - container: SecretsManager.getAzureNonAXTriggerContainer() ?? "", - sasKey: SecretsManager.getAzureNonAXTriggerSasKey() ?? "" - ) - self.uploader = AzureBlobUploader(config: config, logPrefix: "NonAXTriggerDatasetUploader") - } - - // MARK: - Public Methods - - /// Upload a non-AX trigger dataset case (images + metadata) to Azure - /// - Parameters: - /// - caseDirectory: Local directory containing before.png, after.png, and metadata.json - /// - caseName: Name of the case (e.g., "case_001") - func uploadCase(from caseDirectory: URL, caseName: String) async { - let isConfigured = await uploader.config.isConfigured - guard isConfigured else { - log.debug("NonAXTriggerDatasetUploader: Azure not configured, skipping upload") - return - } - - let beforeURL = caseDirectory.appendingPathComponent("before.png") - let afterURL = caseDirectory.appendingPathComponent("after.png") - let remotePath = caseName - - // Upload all files in parallel - async let beforeUpload: () = uploader.uploadFile( - localURL: beforeURL, - remotePath: "\(remotePath)/before.png", - contentType: "image/png" - ) - async let afterUpload: () = uploader.uploadFile( - localURL: afterURL, - remotePath: "\(remotePath)/after.png", - contentType: "image/png" - ) - async let metadataUpload: () = uploader.uploadMetadata( - from: caseDirectory, - remotePath: remotePath - ) - - do { - _ = try await (beforeUpload, afterUpload, metadataUpload) - log.info("NonAXTriggerDatasetUploader: Successfully uploaded case \(caseName)") - } catch { - log.error("NonAXTriggerDatasetUploader: Failed to upload case \(caseName): \(error)") - } - } -} - -#endif diff --git a/macos/OnitQuickEdit/UI/Debug/NonAXTriggerLabelingView.swift b/macos/OnitQuickEdit/UI/Debug/NonAXTriggerLabelingView.swift deleted file mode 100644 index c9ad8c7..0000000 --- a/macos/OnitQuickEdit/UI/Debug/NonAXTriggerLabelingView.swift +++ /dev/null @@ -1,820 +0,0 @@ -// -// NonAXTriggerLabelingView.swift -// Onit -// -// Debug UI for labeling non-accessibility trigger test cases. -// - -#if DEBUG || ONIT_BETA -import SwiftUI -import UniformTypeIdentifiers - -// MARK: - Main View - -struct NonAXTriggerLabelingView: View { - let capture: NonAXTriggerDebugCapture - let onDismiss: () -> Void - - @State private var containsHighlight: Bool? = true - @State private var selectedRegionIndices: Set = [] - @State private var showSideBySide: Bool = false - @State private var showMLDebug: Bool = false - @State private var saveError: String? = nil - @State private var saveSuccess: Bool = false - - private var canSave: Bool { - guard let containsHighlight else { return false } - if containsHighlight { - return !selectedRegionIndices.isEmpty - } - return true - } - - var body: some View { - VStack(spacing: 16) { - // Header - HStack { - Text("Non-AX Trigger Test Case Labeling") - .font(.headline) - Spacer() - } - - // Image display - if showSideBySide { - HStack(spacing: 8) { - ScrollableImageView( - image: capture.beforeImage, - label: "Before", - showOverlays: false, - capture: capture, - selectedRegionIndices: $selectedRegionIndices, - containsHighlight: containsHighlight - ) - ScrollableImageView( - image: capture.afterImage, - label: "After", - showOverlays: true, - capture: capture, - selectedRegionIndices: $selectedRegionIndices, - containsHighlight: containsHighlight - ) - } - } else { - ScrollableImageView( - image: capture.afterImage, - label: "After", - showOverlays: true, - capture: capture, - selectedRegionIndices: $selectedRegionIndices, - containsHighlight: containsHighlight - ) - } - - // Controls - VStack(alignment: .leading, spacing: 12) { - Toggle("Show side-by-side with Before image", isOn: $showSideBySide) - - Button("Show ML Debug Info") { - showMLDebug = true - } - .buttonStyle(.bordered) - - Divider() - - // Contains highlight question - Text("Does this image contain highlighted text?") - .font(.subheadline.weight(.medium)) - - HStack(spacing: 16) { - RadioButton(label: "Yes", isSelected: containsHighlight == true) { - containsHighlight = true - } - RadioButton(label: "No", isSelected: containsHighlight == false) { - containsHighlight = false - selectedRegionIndices.removeAll() - } - } - - if containsHighlight == true { - Text("Select all regions that containing highlighted text (click again to deselect)") - .font(.subheadline.weight(.medium)) - .foregroundColor(.secondary) - - if selectedRegionIndices.isEmpty { - Text("No regions selected") - .font(.caption) - .foregroundColor(.orange) - } else { - let sortedIndices = selectedRegionIndices.sorted().map { "Region \($0 + 1)" }.joined(separator: ", ") - Text("Selected: \(sortedIndices)") - .font(.caption) - .foregroundColor(.blue) - } - } - - if let error = saveError { - Text(error) - .font(.caption) - .foregroundColor(.red) - } - - if saveSuccess { - Text("Test case saved successfully!") - .font(.caption) - .foregroundColor(.green) - } - } - .padding(.horizontal) - - Divider() - - // Buttons - HStack { - Text("Pinch to zoom, scroll to pan") - .font(.caption) - .foregroundColor(.secondary) - - Spacer() - - Button("Cancel") { - onDismiss() - } - .keyboardShortcut(.escape) - - Button("Save Test Case") { - saveTestCase() - } - .disabled(!canSave) - .keyboardShortcut(.return) - } - .padding(.horizontal) - } - .padding() - .frame(minWidth: 800, minHeight: 600) - .background(Color(NSColor.windowBackgroundColor)) - .sheet(isPresented: $showMLDebug) { - MLDebugPanel(capture: capture) - } - } - - // MARK: - Save Test Case - - private func saveTestCase() { - saveError = nil - saveSuccess = false - - guard let containsHighlight else { return } - - let fileManager = FileManager.default - - // Use a temp directory for saving files before uploading to Azure - // This avoids permission issues in external beta builds - let tempDir = fileManager.temporaryDirectory - let caseDirName = "case_\(Int(Date().timeIntervalSince1970 * 1000))" - let caseDir = tempDir.appendingPathComponent("non-ax-trigger-\(caseDirName)") - - do { - try fileManager.createDirectory(at: caseDir, withIntermediateDirectories: true) - } catch { - saveError = "Failed to create temp directory: \(error.localizedDescription)" - return - } - - let beforeURL = caseDir.appendingPathComponent("before.png") - if !saveImage(capture.beforeImage, to: beforeURL) { - saveError = "Failed to save before.png" - return - } - - let afterURL = caseDir.appendingPathComponent("after.png") - if !saveImage(capture.afterImage, to: afterURL) { - saveError = "Failed to save after.png" - return - } - - let metadata = TestCaseMetadata( - containsHighlight: containsHighlight, - selectedRegionIndices: selectedRegionIndices.sorted(), - regions: capture.changedRegions.map { RegionRect(x: $0.origin.x, y: $0.origin.y, width: $0.width, height: $0.height) }, - mouseLocation: PointData(x: capture.mouseLocation.x, y: capture.mouseLocation.y), - accountEmail: AuthManager.shared.account?.email - ) - - let metadataURL = caseDir.appendingPathComponent("metadata.json") - do { - let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted, .sortedKeys] - let data = try encoder.encode(metadata) - try data.write(to: metadataURL) - } catch { - saveError = "Failed to save metadata: \(error.localizedDescription)" - return - } - - saveSuccess = true - print("[NonAXTriggerLabeling] Saved test case to temp: \(caseDir.path)") - - // Upload to Azure and clean up temp files when done - Task { - await NonAXTriggerDatasetUploader.shared.uploadCase(from: caseDir, caseName: caseDirName) - // Clean up temp directory after upload - try? fileManager.removeItem(at: caseDir) - } - - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - onDismiss() - } - } - - private func saveImage(_ image: CGImage, to url: URL) -> Bool { - guard let destination = CGImageDestinationCreateWithURL(url as CFURL, UTType.png.identifier as CFString, 1, nil) else { - return false - } - CGImageDestinationAddImage(destination, image, nil) - return CGImageDestinationFinalize(destination) - } - -} - -// MARK: - ML Debug Panel - -private struct MLDebugPanel: View { - let capture: NonAXTriggerDebugCapture - @Environment(\.dismiss) private var dismiss - @State private var results: [HighlightDetectionDebugResult] = [] - @State private var selectedResult: HighlightDetectionDebugResult? - @State private var isLoading: Bool = true - - var body: some View { - VStack(spacing: 0) { - // Header - HStack { - Text("ML Model Results") - .font(.headline) - Spacer() - if isLoading { - ProgressView() - .scaleEffect(0.7) - Text("Loading...") - .font(.subheadline) - .foregroundColor(.secondary) - } else { - Text("\(results.filter { $0.isHighlight }.count)/\(results.count) detected as highlights") - .font(.subheadline) - .foregroundColor(.secondary) - } - Spacer() - Button("Close") { - dismiss() - } - .keyboardShortcut(.escape) - } - .padding() - .background(Color(NSColor.windowBackgroundColor)) - - Divider() - - if isLoading { - Spacer() - ProgressView("Analyzing regions...") - Spacer() - } else if results.isEmpty { - Spacer() - Text("No regions to analyze") - .foregroundColor(.secondary) - Spacer() - } else { - // Full-width scrollable list of regions, sorted by probability (highest first) - ScrollView(.vertical, showsIndicators: true) { - VStack(spacing: 12) { - let sortedResults = results.enumerated().sorted { $0.element.probability > $1.element.probability } - ForEach(sortedResults, id: \.offset) { index, result in - MLRegionDebugRow(index: index, result: result, selectedResult: $selectedResult) - } - } - .padding() - } - } - } - .frame(minWidth: 800, minHeight: 500, maxHeight: 700) - .background(Color(NSColor.controlBackgroundColor)) - .sheet(item: $selectedResult) { result in - MLRegionZoomView(result: result) - } - .task { - await loadResultsAsync() - } - } - - /// Loads ML detection results asynchronously to keep the UI responsive. - /// Uses Swift's structured concurrency to allow UI updates between operations. - @MainActor - private func loadResultsAsync() async { - isLoading = true - - // Yield to allow the loading UI to render before starting heavy work - await Task.yield() - - // Call the ML service (thread-safe, can run from any context) - results = HighlightDetectorService.shared.detectHighlightsWithDebugInfo( - beforeImage: capture.beforeImage, - afterImage: capture.afterImage, - regions: capture.changedRegions - ) - - isLoading = false - } -} - -// MARK: - ML Region Debug Row (Full Width) - -private struct MLRegionDebugRow: View { - let index: Int - let result: HighlightDetectionDebugResult - @Binding var selectedResult: HighlightDetectionDebugResult? - - var body: some View { - HStack(spacing: 16) { - // Status indicator - VStack(spacing: 4) { - Text(result.isHighlight ? "✅" : "❌") - .font(.title2) - Text("Region \(index + 1)") - .font(.caption.weight(.medium)) - } - .frame(width: 70) - - // Before image - VStack(spacing: 2) { - Text("Before") - .font(.system(size: 10)) - .foregroundColor(.secondary) - Image(nsImage: NSImage(cgImage: result.beforeCrop, size: NSSize(width: result.beforeCrop.width, height: result.beforeCrop.height))) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(height: 80) - .background(Color.black.opacity(0.1)) - .cornerRadius(4) - } - .frame(maxWidth: .infinity) - - // After image - VStack(spacing: 2) { - Text("After") - .font(.system(size: 10)) - .foregroundColor(.secondary) - Image(nsImage: NSImage(cgImage: result.afterCrop, size: NSSize(width: result.afterCrop.width, height: result.afterCrop.height))) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(height: 80) - .background(Color.black.opacity(0.1)) - .cornerRadius(4) - } - .frame(maxWidth: .infinity) - - // Stats - VStack(alignment: .trailing, spacing: 4) { - Text("P: \(String(format: "%.2f", result.probability))") - .font(.system(size: 13, weight: .semibold, design: .monospaced)) - .foregroundColor(result.isHighlight ? .green : .red) - Text("Threshold: 0.90") - .font(.system(size: 9)) - .foregroundColor(.secondary) - Text("\(Int(result.region.width))×\(Int(result.region.height)) px") - .font(.system(size: 10)) - .foregroundColor(.secondary) - - Button("Zoom") { - selectedResult = result - } - .font(.caption) - .buttonStyle(.bordered) - } - .frame(width: 100) - } - .padding(10) - .background(result.isHighlight ? Color.green.opacity(0.08) : Color.red.opacity(0.08)) - .cornerRadius(8) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(result.isHighlight ? Color.green.opacity(0.3) : Color.red.opacity(0.3), lineWidth: 1) - ) - .onTapGesture { - selectedResult = result - } - } -} - -// MARK: - ML Region Zoom View (Sheet) - -private struct MLRegionZoomView: View { - let result: HighlightDetectionDebugResult - @Environment(\.dismiss) private var dismiss - - var body: some View { - VStack(spacing: 16) { - // Header - HStack { - Text("Region Detail") - .font(.headline) - Spacer() - Text(result.isHighlight ? "✅ Highlight" : "❌ Not Highlight") - .font(.subheadline.weight(.medium)) - .foregroundColor(result.isHighlight ? .green : .red) - Spacer() - Button("Close") { - dismiss() - } - .keyboardShortcut(.escape) - } - .padding(.horizontal) - - // Stats bar - HStack(spacing: 24) { - HStack(spacing: 4) { - Text("Probability:") - .foregroundColor(.secondary) - Text(String(format: "%.3f", result.probability)) - .font(.system(.body, design: .monospaced).weight(.semibold)) - .foregroundColor(result.isHighlight ? .green : .red) - } - HStack(spacing: 4) { - Text("Threshold:") - .foregroundColor(.secondary) - Text("0.90") - .font(.system(.body, design: .monospaced)) - } - HStack(spacing: 4) { - Text("Size:") - .foregroundColor(.secondary) - Text("\(Int(result.region.width))×\(Int(result.region.height)) px") - .font(.system(.body, design: .monospaced)) - } - } - .font(.subheadline) - .padding(.horizontal) - - // Zoomable images side by side - HStack(spacing: 16) { - VStack(spacing: 4) { - Text("Before") - .font(.subheadline.weight(.medium)) - .foregroundColor(.secondary) - ZoomableImageView(image: result.beforeCrop) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.black.opacity(0.05)) - .cornerRadius(8) - } - - VStack(spacing: 4) { - Text("After") - .font(.subheadline.weight(.medium)) - .foregroundColor(.secondary) - ZoomableImageView(image: result.afterCrop) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.black.opacity(0.05)) - .cornerRadius(8) - } - } - .padding(.horizontal) - - Text("Pinch to zoom, scroll to pan") - .font(.caption) - .foregroundColor(.secondary) - } - .padding() - .frame(minWidth: 700, minHeight: 500) - } -} - -// MARK: - Zoomable Image View - -private struct ZoomableImageView: NSViewRepresentable { - typealias NSViewType = NSScrollView - - let image: CGImage - - func makeNSView(context: NSViewRepresentableContext) -> NSScrollView { - let scrollView = NSScrollView() - scrollView.hasVerticalScroller = true - scrollView.hasHorizontalScroller = true - scrollView.autohidesScrollers = true - scrollView.borderType = .noBorder - scrollView.backgroundColor = .clear - scrollView.drawsBackground = false - - // Allow magnification with pinch gesture - scrollView.allowsMagnification = true - scrollView.minMagnification = 1.0 - scrollView.maxMagnification = 20.0 - - let imageView = NSImageView() - imageView.image = NSImage(cgImage: image, size: NSSize(width: image.width, height: image.height)) - imageView.imageScaling = .scaleProportionallyUpOrDown - - scrollView.documentView = imageView - - return scrollView - } - - func updateNSView(_ scrollView: NSScrollView, context: NSViewRepresentableContext) { - if let imageView = scrollView.documentView as? NSImageView { - imageView.image = NSImage(cgImage: image, size: NSSize(width: image.width, height: image.height)) - - // Size the image view to fill the scroll view initially - if let clipView = scrollView.contentView as? NSClipView { - imageView.frame = clipView.bounds - } - } - } -} - -// MARK: - Identifiable conformance for sheet - -extension HighlightDetectionDebugResult: Identifiable { - var id: String { - "\(region.origin.x)-\(region.origin.y)-\(region.width)-\(region.height)" - } -} - -// MARK: - Scrollable Image View - -private struct ScrollableImageView: View { - let image: CGImage - let label: String - let showOverlays: Bool - let capture: NonAXTriggerDebugCapture - @Binding var selectedRegionIndices: Set - let containsHighlight: Bool? - - var body: some View { - VStack(spacing: 4) { - Text(label) - .font(.caption) - .foregroundColor(.secondary) - - GeometryReader { geometry in - ImageScrollView( - image: image, - showOverlays: showOverlays, - capture: capture, - selectedRegionIndices: $selectedRegionIndices, - containsHighlight: containsHighlight, - containerSize: geometry.size - ) - .background(Color.black.opacity(0.1)) - .cornerRadius(8) - } - } - } -} - -// MARK: - NSScrollView Wrapper - -private struct ImageScrollView: NSViewRepresentable { - typealias NSViewType = NSScrollView - - let image: CGImage - let showOverlays: Bool - let capture: NonAXTriggerDebugCapture - @Binding var selectedRegionIndices: Set - let containsHighlight: Bool? - let containerSize: CGSize - - func makeNSView(context: NSViewRepresentableContext) -> NSScrollView { - let scrollView = NSScrollView() - scrollView.hasVerticalScroller = true - scrollView.hasHorizontalScroller = true - scrollView.autohidesScrollers = true - scrollView.borderType = .noBorder - scrollView.backgroundColor = .clear - scrollView.drawsBackground = false - - // Allow magnification with pinch gesture - scrollView.allowsMagnification = true - scrollView.minMagnification = 1.0 - scrollView.maxMagnification = 10.0 - - // Create the content view - let contentView = ImageContentView( - image: image, - showOverlays: showOverlays, - capture: capture, - selectedRegionIndices: $selectedRegionIndices, - containsHighlight: containsHighlight - ) - contentView.frame = NSRect(origin: .zero, size: containerSize) - - scrollView.documentView = contentView - - return scrollView - } - - func updateNSView(_ scrollView: NSScrollView, context: NSViewRepresentableContext) { - // Update content view size based on magnification - if let contentView = scrollView.documentView as? ImageContentView { - let scaledSize = NSSize( - width: containerSize.width * scrollView.magnification, - height: containerSize.height * scrollView.magnification - ) - if contentView.frame.size != scaledSize { - contentView.frame.size = scaledSize - contentView.needsDisplay = true - } - contentView.updateBindings(selectedRegionIndices: $selectedRegionIndices, containsHighlight: containsHighlight) - } - } -} - -// MARK: - Image Content View (NSView) - -private class ImageContentView: NSView { - let cgImage: CGImage - let showOverlays: Bool - let capture: NonAXTriggerDebugCapture - private var selectedRegionIndices: Binding> - private var containsHighlight: Bool? - - init(image: CGImage, showOverlays: Bool, capture: NonAXTriggerDebugCapture, selectedRegionIndices: Binding>, containsHighlight: Bool?) { - self.cgImage = image - self.showOverlays = showOverlays - self.capture = capture - self.selectedRegionIndices = selectedRegionIndices - self.containsHighlight = containsHighlight - super.init(frame: .zero) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func updateBindings(selectedRegionIndices: Binding>, containsHighlight: Bool?) { - self.selectedRegionIndices = selectedRegionIndices - self.containsHighlight = containsHighlight - needsDisplay = true - } - - /// Calculate the aspect-fit rect for drawing the image within the view bounds - private func calculateDrawRect() -> CGRect { - let imageSize = CGSize(width: cgImage.width, height: cgImage.height) - let viewSize = bounds.size - - let imageAspect = imageSize.width / imageSize.height - let viewAspect = viewSize.width / viewSize.height - - if imageAspect > viewAspect { - // Image is wider - fit to width - let height = viewSize.width / imageAspect - let y = (viewSize.height - height) / 2 - return CGRect(x: 0, y: y, width: viewSize.width, height: height) - } else { - // Image is taller - fit to height - let width = viewSize.height * imageAspect - let x = (viewSize.width - width) / 2 - return CGRect(x: x, y: 0, width: width, height: viewSize.height) - } - } - - override func draw(_ dirtyRect: NSRect) { - guard let context = NSGraphicsContext.current?.cgContext else { return } - - let drawRect = calculateDrawRect() - - // Draw image using NSImage to handle orientation correctly - let nsImage = NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height)) - nsImage.draw(in: drawRect) - - // Draw overlays if enabled - if showOverlays { - let scaleFactor = drawRect.width / CGFloat(cgImage.width) - - for (index, region) in capture.changedRegions.enumerated() { - let isSelected = selectedRegionIndices.wrappedValue.contains(index) - - // Convert region coordinates to view coordinates - let scaledX = drawRect.origin.x + region.origin.x * scaleFactor - let scaledY = drawRect.origin.y + region.origin.y * scaleFactor - let scaledWidth = region.width * scaleFactor - let scaledHeight = region.height * scaleFactor - - let overlayRect = CGRect(x: scaledX, y: scaledY, width: scaledWidth, height: scaledHeight) - - // Draw fill - let fillColor = isSelected ? NSColor.green.withAlphaComponent(0.4) : NSColor.blue.withAlphaComponent(0.3) - fillColor.setFill() - NSBezierPath(rect: overlayRect).fill() - - // Draw border - let borderColor = isSelected ? NSColor.green : NSColor.blue - borderColor.setStroke() - let path = NSBezierPath(rect: overlayRect) - path.lineWidth = isSelected ? 3 : 1 - path.stroke() - } - - // Draw crosshair at mouse location - let mouseX = drawRect.origin.x + capture.mouseLocation.x * scaleFactor - let mouseY = drawRect.origin.y + capture.mouseLocation.y * scaleFactor - let crosshairSize: CGFloat = 20 - - NSColor.red.setStroke() - let horizontalPath = NSBezierPath() - horizontalPath.move(to: NSPoint(x: mouseX - crosshairSize/2, y: mouseY)) - horizontalPath.line(to: NSPoint(x: mouseX + crosshairSize/2, y: mouseY)) - horizontalPath.lineWidth = 2 - horizontalPath.stroke() - - let verticalPath = NSBezierPath() - verticalPath.move(to: NSPoint(x: mouseX, y: mouseY - crosshairSize/2)) - verticalPath.line(to: NSPoint(x: mouseX, y: mouseY + crosshairSize/2)) - verticalPath.lineWidth = 2 - verticalPath.stroke() - } - } - - override func mouseDown(with event: NSEvent) { - guard showOverlays, containsHighlight == true else { - super.mouseDown(with: event) - return - } - - let location = convert(event.locationInWindow, from: nil) - let drawRect = calculateDrawRect() - let scaleFactor = drawRect.width / CGFloat(cgImage.width) - - // Check if click is within any region - for (index, region) in capture.changedRegions.enumerated() { - let scaledX = drawRect.origin.x + region.origin.x * scaleFactor - let scaledY = drawRect.origin.y + region.origin.y * scaleFactor - let scaledWidth = region.width * scaleFactor - let scaledHeight = region.height * scaleFactor - - let overlayRect = CGRect(x: scaledX, y: scaledY, width: scaledWidth, height: scaledHeight) - - if overlayRect.contains(location) { - if selectedRegionIndices.wrappedValue.contains(index) { - selectedRegionIndices.wrappedValue.remove(index) - } else { - selectedRegionIndices.wrappedValue.insert(index) - } - needsDisplay = true - return - } - } - - super.mouseDown(with: event) - } -} - -// MARK: - Supporting Views - -private struct RadioButton: View { - let label: String - let isSelected: Bool - let action: () -> Void - - var body: some View { - Button(action: action) { - HStack(spacing: 6) { - Circle() - .strokeBorder(isSelected ? Color.blue : Color.gray, lineWidth: 2) - .background(Circle().fill(isSelected ? Color.blue : Color.clear)) - .frame(width: 16, height: 16) - Text(label) - } - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - } -} - -// MARK: - Data Models - -private struct TestCaseMetadata: Codable { - let containsHighlight: Bool - let selectedRegionIndices: [Int] - let regions: [RegionRect] - let mouseLocation: PointData - let accountEmail: String? - - enum CodingKeys: String, CodingKey { - case containsHighlight = "contains_highlight" - case selectedRegionIndices = "selected_region_indices" - case regions - case mouseLocation = "mouse_location" - case accountEmail = "account_email" - } -} - -private struct RegionRect: Codable { - let x: CGFloat - let y: CGFloat - let width: CGFloat - let height: CGFloat -} - -private struct PointData: Codable { - let x: CGFloat - let y: CGFloat -} -#endif diff --git a/macos/OnitQuickEdit/UI/Debug/NonAXTriggerLabelingWindowController.swift b/macos/OnitQuickEdit/UI/Debug/NonAXTriggerLabelingWindowController.swift deleted file mode 100644 index 20d0d30..0000000 --- a/macos/OnitQuickEdit/UI/Debug/NonAXTriggerLabelingWindowController.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// NonAXTriggerLabelingWindowController.swift -// Onit -// -// Window controller for the non-accessibility trigger labeling interface. -// - -#if DEBUG || ONIT_BETA -import AppKit -import SwiftUI - -@MainActor -final class NonAXTriggerLabelingWindowController: NSObject, NSWindowDelegate { - // MARK: - Singleton - - static let shared = NonAXTriggerLabelingWindowController() - - // MARK: - Properties - - private var window: CenteredWindow? - - // MARK: - Public Functions - - func show(capture: NonAXTriggerDebugCapture) { - // Close existing window if any - if let existingWindow = window { - existingWindow.close() - window = nil - } - - let labelingView = NonAXTriggerLabelingView(capture: capture) { [weak self] in - self?.closeWindow() - } - - let newWindow = CenteredWindow( - rootView: labelingView, - windowLevel: .floating, - hideTitleBar: false, - canResize: true, - canDrag: true, - canCloseWithEsc: true, - windowSize: (width: 800, height: 700) - ) - - newWindow.delegate = self - newWindow.title = "Non-AX Trigger Test Case Labeling" - newWindow.isReleasedWhenClosed = false - - NSApp.activate(ignoringOtherApps: true) - newWindow.makeKeyAndOrderFront(nil) - - window = newWindow - } - - func closeWindow() { - window?.close() - } - - // MARK: - NSWindowDelegate - - func windowWillClose(_ notification: Notification) { - guard let closingWindow = notification.object as? NSWindow, - closingWindow === window else { - return - } - - window?.cleanupObservers() - window?.delegate = nil - window = nil - } -} -#endif diff --git a/macos/com.synth.environment.plist.sample b/macos/com.synth.environment.plist.sample deleted file mode 100644 index 13435b8..0000000 --- a/macos/com.synth.environment.plist.sample +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - Label - com.synth.environment - ProgramArguments - - /bin/sh - -c - - launchctl setenv AZURE_NON_AX_TRIGGER_SAS_KEY "YOUR_AZURE_NON_AX_TRIGGER_SAS_KEY"; - - - RunAtLoad - - -