@hasna/recordings 0.2.11 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +349 -69
- package/bun.lock +3 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
- package/dist/cli/index.js +1728 -320
- package/dist/cli/macos-permissions.d.ts +13 -0
- package/dist/cli/macos-permissions.d.ts.map +1 -0
- package/dist/cli/options.d.ts +12 -0
- package/dist/cli/options.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/recordings.d.ts +2 -1
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/db/remote-storage.d.ts +5 -1
- package/dist/db/remote-storage.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +628 -147
- package/dist/lib/bun-runtime.d.ts +11 -0
- package/dist/lib/bun-runtime.d.ts.map +1 -0
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/enhancer.d.ts +10 -2
- package/dist/lib/enhancer.d.ts.map +1 -1
- package/dist/lib/install-maintenance.d.ts +24 -0
- package/dist/lib/install-maintenance.d.ts.map +1 -0
- package/dist/lib/machine.d.ts +2 -0
- package/dist/lib/machine.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts +2 -1
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/recording-create-identity.d.ts +13 -0
- package/dist/lib/recording-create-identity.d.ts.map +1 -0
- package/dist/lib/release-install-policy.d.ts +31 -0
- package/dist/lib/release-install-policy.d.ts.map +1 -0
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +977 -194
- package/dist/sdk/index.js +5 -1
- package/dist/sdk/v1.generated.d.ts +4 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud-config.d.ts +9 -0
- package/dist/server/cloud-config.d.ts.map +1 -0
- package/dist/server/cloud-readiness.d.ts +10 -0
- package/dist/server/cloud-readiness.d.ts.map +1 -0
- package/dist/server/cloud.d.ts +9 -18
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +1654 -304
- package/dist/server/migrate-command.d.ts +11 -0
- package/dist/server/migrate-command.d.ts.map +1 -0
- package/dist/server/openapi.d.ts +22 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/repo.d.ts +8 -1
- package/dist/server/repo.d.ts.map +1 -1
- package/dist/server/serve.d.ts +6 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage.js +515 -80
- package/dist/store.d.ts +3 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +7 -4
- package/packaging/macos/Empty.entitlements +5 -0
- package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
- package/packaging/macos/Verifier.entitlements +10 -0
- package/packaging/macos/artifact-verifier.sb +33 -0
- package/packaging/macos/build_release_pkg.sh +872 -0
- package/packaging/macos/managed_bootstrap.sh +623 -0
- package/packaging/macos/pkgutil_fingerprint.awk +37 -0
- package/packaging/macos/release_lifecycle.ts +463 -0
- package/packaging/macos/scripts/postinstall +565 -0
- package/packaging/macos/scripts/preinstall +297 -0
- package/scripts/build_companion_cli.sh +337 -0
- package/scripts/build_native_fs_guard.sh +59 -0
- package/scripts/generate-sdk.ts +19 -1
- package/scripts/install_macos_app.sh +1919 -98
- package/scripts/macos_artifact.ts +5316 -0
- package/scripts/migrate.ts +38 -10
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
- package/scripts/native/recordings_fs_guard.c +1167 -0
- package/scripts/native_fs_guard.ts +158 -0
- package/scripts/release-guard.ts +20 -1
- package/scripts/resolve_tailscale_cli.sh +389 -0
- package/scripts/smoke_macos_app.sh +573 -0
- package/src/native/Recordings/App/ContentView.swift +83 -0
- package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
- package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
- package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
- package/src/native/Recordings/App/RecordingsApp.swift +367 -44
- package/src/native/Recordings/App/RecordingsListView.swift +130 -0
- package/src/native/Recordings/App/RecordingsStore.swift +206 -0
- package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
- package/src/native/Recordings/App/SidebarView.swift +212 -0
- package/src/native/Recordings/App/Theme.swift +87 -0
- package/src/native/Recordings/Package.resolved +5 -5
- package/src/native/Recordings/Package.swift +62 -2
- package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
- package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
- package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
- package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
- package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
- package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
- package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
- package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
- package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
- package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
- package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
- package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
- package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
- package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
- package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
- package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
- package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
- package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
- package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
- package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
- package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
- package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
- package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
- package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
- package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
- package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
- package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
- package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
- package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
- package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
- package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
- package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
- package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
- package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
- package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
- package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
- package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
- package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
- package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
- package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
- package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
- package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
- package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
- package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
- package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
- package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
- package/src/native/Recordings/build.sh +1973 -236
- package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import AVFoundation
|
|
3
|
+
import RecordingsLib
|
|
4
|
+
|
|
5
|
+
/// Detail pane for a selected recording: the full transcript on the white canvas, a slim
|
|
6
|
+
/// action toolbar (copy / paste / play / delete), and a metadata strip. No boxed panels.
|
|
7
|
+
struct RecordingDetailView: View {
|
|
8
|
+
@ObservedObject var store: RecordingsStore
|
|
9
|
+
@State private var player: AVAudioPlayer?
|
|
10
|
+
@State private var isPlaying = false
|
|
11
|
+
@State private var copied = false
|
|
12
|
+
|
|
13
|
+
var body: some View {
|
|
14
|
+
if let rec = store.selectedRecording {
|
|
15
|
+
detail(rec)
|
|
16
|
+
} else {
|
|
17
|
+
VStack(spacing: 10) {
|
|
18
|
+
Image(systemName: "waveform.and.mic").font(.system(size: 40)).foregroundStyle(.quaternary)
|
|
19
|
+
Text("Select a recording").foregroundStyle(.secondary)
|
|
20
|
+
}
|
|
21
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private func detail(_ rec: Recording) -> some View {
|
|
26
|
+
VStack(alignment: .leading, spacing: 0) {
|
|
27
|
+
HStack(spacing: 10) {
|
|
28
|
+
Text(rec.createdDate.map { dateLabel($0) } ?? "Recording")
|
|
29
|
+
.font(.system(.title2, design: .rounded).weight(.semibold))
|
|
30
|
+
Spacer()
|
|
31
|
+
toolbar(rec)
|
|
32
|
+
}
|
|
33
|
+
.padding(.horizontal, 18).padding(.top, 16).padding(.bottom, 8)
|
|
34
|
+
Divider().opacity(0.4)
|
|
35
|
+
|
|
36
|
+
ScrollView {
|
|
37
|
+
Text(rec.displayText.isEmpty ? "No transcript" : rec.displayText)
|
|
38
|
+
.font(.system(.title3, design: .rounded))
|
|
39
|
+
.textSelection(.enabled)
|
|
40
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
41
|
+
.padding(.horizontal, 18).padding(.top, 14)
|
|
42
|
+
|
|
43
|
+
if rec.isEnhanced, !rec.rawText.isEmpty, rec.rawText != rec.displayText {
|
|
44
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
45
|
+
Text("RAW TRANSCRIPT").font(.caption2.weight(.semibold)).foregroundStyle(.secondary)
|
|
46
|
+
Text(rec.rawText).font(.callout).foregroundStyle(.secondary).textSelection(.enabled)
|
|
47
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
48
|
+
}
|
|
49
|
+
.padding(.horizontal, 18).padding(.top, 18)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Divider().opacity(0.4)
|
|
54
|
+
metadata(rec).padding(.horizontal, 18).padding(.vertical, 10)
|
|
55
|
+
}
|
|
56
|
+
.onChange(of: store.selection) { _, _ in stopPlayback() }
|
|
57
|
+
.onDisappear { stopPlayback() }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private func toolbar(_ rec: Recording) -> some View {
|
|
61
|
+
HStack(spacing: 4) {
|
|
62
|
+
if hasAudio(rec) {
|
|
63
|
+
iconButton(isPlaying ? "stop.circle" : "play.circle", help: isPlaying ? "Stop" : "Play audio") {
|
|
64
|
+
togglePlayback(rec)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
iconButton(copied ? "checkmark" : "doc.on.doc", help: "Copy transcript") {
|
|
68
|
+
let pb = NSPasteboard.general
|
|
69
|
+
pb.clearContents(); pb.setString(rec.displayText, forType: .string)
|
|
70
|
+
withAnimation { copied = true }
|
|
71
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { withAnimation { copied = false } }
|
|
72
|
+
}
|
|
73
|
+
iconButton("arrow.up.right.square", help: "Paste into front app") {
|
|
74
|
+
store.engine.pasteIntoFrontApp(rec.displayText)
|
|
75
|
+
}
|
|
76
|
+
iconButton("trash", help: "Delete") { store.delete(id: rec.id) }
|
|
77
|
+
}
|
|
78
|
+
.foregroundStyle(.secondary)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private func iconButton(_ name: String, help: String, action: @escaping () -> Void) -> some View {
|
|
82
|
+
Button(action: action) { Image(systemName: name).frame(width: 24, height: 24) }
|
|
83
|
+
.buttonStyle(.plain).help(help)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private func metadata(_ rec: Recording) -> some View {
|
|
87
|
+
HStack(spacing: 14) {
|
|
88
|
+
if rec.durationMs > 0 { metaItem("clock", rec.durationLabel) }
|
|
89
|
+
if let model = rec.modelUsed { metaItem("cpu", model) }
|
|
90
|
+
if let lang = rec.language, !lang.isEmpty { metaItem("globe", lang) }
|
|
91
|
+
if let name = store.projectName(rec.projectId) { metaItem("folder", name) }
|
|
92
|
+
if let machine = rec.machineId, !machine.isEmpty { metaItem("desktopcomputer", machine) }
|
|
93
|
+
if !rec.tags.isEmpty { metaItem("number", rec.tags.joined(separator: ", ")) }
|
|
94
|
+
Spacer()
|
|
95
|
+
}
|
|
96
|
+
.font(.caption2).foregroundStyle(.tertiary).lineLimit(1)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private func metaItem(_ icon: String, _ text: String) -> some View {
|
|
100
|
+
Label(text, systemImage: icon).labelStyle(.titleAndIcon)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private func dateLabel(_ date: Date) -> String {
|
|
104
|
+
let f = DateFormatter()
|
|
105
|
+
f.dateStyle = .medium; f.timeStyle = .short
|
|
106
|
+
return f.string(from: date)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// MARK: - Audio playback
|
|
110
|
+
|
|
111
|
+
private func hasAudio(_ rec: Recording) -> Bool {
|
|
112
|
+
guard let path = rec.audioPath, !path.isEmpty else { return false }
|
|
113
|
+
return FileManager.default.fileExists(atPath: path)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private func togglePlayback(_ rec: Recording) {
|
|
117
|
+
if isPlaying { stopPlayback(); return }
|
|
118
|
+
guard let path = rec.audioPath else { return }
|
|
119
|
+
do {
|
|
120
|
+
let p = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
|
|
121
|
+
p.play()
|
|
122
|
+
player = p
|
|
123
|
+
isPlaying = true
|
|
124
|
+
// Reset the button when playback ends.
|
|
125
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + p.duration + 0.1) {
|
|
126
|
+
if player === p { stopPlayback() }
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
isPlaying = false
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private func stopPlayback() {
|
|
134
|
+
player?.stop()
|
|
135
|
+
player = nil
|
|
136
|
+
isPlaying = false
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -1,79 +1,402 @@
|
|
|
1
1
|
@preconcurrency import Cocoa
|
|
2
2
|
import AVFoundation
|
|
3
|
+
import Darwin
|
|
3
4
|
import SwiftUI
|
|
4
5
|
import KeyboardShortcuts
|
|
5
6
|
import RecordingsLib
|
|
6
7
|
|
|
8
|
+
private final class PermissionRequestResultBox: @unchecked Sendable {
|
|
9
|
+
private let lock = NSLock()
|
|
10
|
+
private var microphoneGranted = false
|
|
11
|
+
|
|
12
|
+
func setMicrophoneGranted(_ granted: Bool) {
|
|
13
|
+
lock.withLock { microphoneGranted = granted }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func outcome(accessibilityTrusted: Bool) -> PermissionRequestOutcome {
|
|
17
|
+
lock.withLock {
|
|
18
|
+
PermissionRequestOutcome(
|
|
19
|
+
microphoneGranted: microphoneGranted,
|
|
20
|
+
accessibilityTrusted: accessibilityTrusted
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private struct RuntimeSmokeCompletionResponse: Encodable, Sendable {
|
|
27
|
+
let challenge: String
|
|
28
|
+
let mode: String
|
|
29
|
+
let processIdentifier: Int32
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Recordings — a full native macOS app. The main window is the Recordings workspace
|
|
33
|
+
/// (record + library); the menu-bar surface, global shortcuts, and the intent-routed
|
|
34
|
+
/// recording flow keep working while the window is in the background.
|
|
35
|
+
/// Keeps the app (and therefore the RecordingEngine + global shortcuts) alive after the
|
|
36
|
+
/// last window is closed, so background recording shortcuts keep working.
|
|
37
|
+
@MainActor
|
|
38
|
+
final class RecordingsAppDelegate: NSObject, NSApplicationDelegate {
|
|
39
|
+
weak var state: RecordingsAppState?
|
|
40
|
+
|
|
41
|
+
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { false }
|
|
42
|
+
|
|
43
|
+
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
44
|
+
state?.startRuntimeSmokeIfNeeded()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
|
48
|
+
if !flag {
|
|
49
|
+
state?.openRecordings()
|
|
50
|
+
}
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@MainActor
|
|
56
|
+
final class RecordingsAppState: ObservableObject {
|
|
57
|
+
let store: RecordingsStore?
|
|
58
|
+
let declaresMenuBar: Bool
|
|
59
|
+
let runtimeSmokeProbe: RuntimeSmokeProbe?
|
|
60
|
+
private let runtimeSmokeMode: String?
|
|
61
|
+
private let runtimeSmokeOutputPath: String?
|
|
62
|
+
private let runtimeSmokeAcknowledgementPath: String?
|
|
63
|
+
private let runtimeSmokeCompletionPath: String?
|
|
64
|
+
private var mainWindow: NSWindow?
|
|
65
|
+
private(set) var windowCreationCount = 0
|
|
66
|
+
private(set) var windowActivationCount = 0
|
|
67
|
+
|
|
68
|
+
init(plan: PermissionRequestLaunchPlan) {
|
|
69
|
+
declaresMenuBar = plan.declaresMenuBar
|
|
70
|
+
runtimeSmokeMode = plan.runtimeSmokeMode
|
|
71
|
+
runtimeSmokeOutputPath = plan.runtimeSmokeOutputPath
|
|
72
|
+
runtimeSmokeAcknowledgementPath = plan.runtimeSmokeAcknowledgementPath
|
|
73
|
+
runtimeSmokeCompletionPath = plan.runtimeSmokeCompletionPath
|
|
74
|
+
runtimeSmokeProbe = plan.runtimeSmokeMode == "normal" ? RuntimeSmokeProbe() : nil
|
|
75
|
+
if plan.installsGlobalHandlers {
|
|
76
|
+
let store = RecordingsStore()
|
|
77
|
+
self.store = store
|
|
78
|
+
if plan.declaresMainWindow {
|
|
79
|
+
Task { @MainActor [weak self] in
|
|
80
|
+
self?.openRecordings()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
store = nil
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func openRecordings() {
|
|
89
|
+
if let store {
|
|
90
|
+
showWindow(contentView: NSHostingView(rootView: ContentView(store: store)))
|
|
91
|
+
} else if runtimeSmokeMode == "normal" {
|
|
92
|
+
showWindow(contentView: NSHostingView(rootView: Text("Recordings runtime smoke")))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private func showWindow(contentView: NSView) {
|
|
97
|
+
windowActivationCount += 1
|
|
98
|
+
NSApplication.shared.setActivationPolicy(.regular)
|
|
99
|
+
if let mainWindow {
|
|
100
|
+
NSApplication.shared.activate()
|
|
101
|
+
NSRunningApplication.current.activate(options: [.activateAllWindows])
|
|
102
|
+
mainWindow.makeKeyAndOrderFront(nil)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let window = NSWindow(
|
|
107
|
+
contentRect: NSRect(x: 0, y: 0, width: 1180, height: 760),
|
|
108
|
+
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
|
109
|
+
backing: .buffered,
|
|
110
|
+
defer: false
|
|
111
|
+
)
|
|
112
|
+
window.title = "Recordings"
|
|
113
|
+
window.titleVisibility = .hidden
|
|
114
|
+
window.titlebarAppearsTransparent = true
|
|
115
|
+
window.isReleasedWhenClosed = false
|
|
116
|
+
window.contentView = contentView
|
|
117
|
+
window.center()
|
|
118
|
+
mainWindow = window
|
|
119
|
+
windowCreationCount += 1
|
|
120
|
+
NSApplication.shared.activate()
|
|
121
|
+
NSRunningApplication.current.activate(options: [.activateAllWindows])
|
|
122
|
+
window.makeKeyAndOrderFront(nil)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func startRuntimeSmokeIfNeeded() {
|
|
126
|
+
guard let mode = runtimeSmokeMode, runtimeSmokeOutputPath != nil else { return }
|
|
127
|
+
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 15) {
|
|
128
|
+
Darwin._exit(124)
|
|
129
|
+
}
|
|
130
|
+
if mode == "permission-helper" {
|
|
131
|
+
NSApplication.shared.setActivationPolicy(.accessory)
|
|
132
|
+
DispatchQueue.main.async { [weak self] in
|
|
133
|
+
DispatchQueue.main.async {
|
|
134
|
+
self?.finishRuntimeSmoke(
|
|
135
|
+
mode: mode,
|
|
136
|
+
surfaceCount: 0,
|
|
137
|
+
labels: [],
|
|
138
|
+
accessibility: RuntimeSmokeAccessibilitySnapshot.processMenuBarExtras()
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
if mode == "resolver" {
|
|
145
|
+
let probeHome = FileManager.default.temporaryDirectory
|
|
146
|
+
.appendingPathComponent("recordings-resolver-smoke-\(UUID().uuidString)")
|
|
147
|
+
do {
|
|
148
|
+
try FileManager.default.createDirectory(at: probeHome, withIntermediateDirectories: true)
|
|
149
|
+
let probe = try RecordingsCLI.probePackagedCompanion(home: probeHome.path)
|
|
150
|
+
try? FileManager.default.removeItem(at: probeHome)
|
|
151
|
+
finishRuntimeSmoke(
|
|
152
|
+
mode: mode,
|
|
153
|
+
surfaceCount: 0,
|
|
154
|
+
labels: [],
|
|
155
|
+
accessibility: RuntimeSmokeAccessibilitySnapshot.processMenuBarExtras(),
|
|
156
|
+
resolvedCompanionPath: probe.executablePath,
|
|
157
|
+
companionCapabilitiesPassed: true
|
|
158
|
+
)
|
|
159
|
+
} catch {
|
|
160
|
+
try? FileManager.default.removeItem(at: probeHome)
|
|
161
|
+
fputs("Packaged companion resolver smoke failed: \(error)\n", stderr)
|
|
162
|
+
finishRuntimeSmoke(
|
|
163
|
+
mode: mode,
|
|
164
|
+
surfaceCount: 0,
|
|
165
|
+
labels: [],
|
|
166
|
+
accessibility: RuntimeSmokeAccessibilitySnapshot.processMenuBarExtras(),
|
|
167
|
+
companionCapabilitiesPassed: false
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
guard mode == "normal", let runtimeSmokeProbe else {
|
|
173
|
+
finishRuntimeSmoke(
|
|
174
|
+
mode: mode,
|
|
175
|
+
surfaceCount: 0,
|
|
176
|
+
labels: [],
|
|
177
|
+
accessibility: RuntimeSmokeAccessibilitySnapshot.processMenuBarExtras()
|
|
178
|
+
)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
runtimeSmokeProbe.completed = { [weak self, weak runtimeSmokeProbe] in
|
|
182
|
+
guard let self, let runtimeSmokeProbe else { return }
|
|
183
|
+
let accessibility = RuntimeSmokeAccessibilitySnapshot.processMenuBarExtras()
|
|
184
|
+
self.openRecordings()
|
|
185
|
+
let firstWindow = self.mainWindow
|
|
186
|
+
self.openRecordings()
|
|
187
|
+
self.finishRuntimeSmokeWhenWindowSettles(
|
|
188
|
+
mode: mode,
|
|
189
|
+
surfaceCount: runtimeSmokeProbe.surfaceAppearances,
|
|
190
|
+
labels: runtimeSmokeProbe.renderedLabels,
|
|
191
|
+
accessibility: accessibility,
|
|
192
|
+
retainedWindowReused: firstWindow === self.mainWindow
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private func finishRuntimeSmokeWhenWindowSettles(
|
|
198
|
+
mode: String,
|
|
199
|
+
surfaceCount: Int,
|
|
200
|
+
labels: [String],
|
|
201
|
+
accessibility: RuntimeSmokeAccessibilitySnapshot,
|
|
202
|
+
retainedWindowReused: Bool,
|
|
203
|
+
attempt: Int = 0
|
|
204
|
+
) {
|
|
205
|
+
let windowSettled = NSApplication.shared.isActive && (mainWindow?.isKeyWindow ?? false)
|
|
206
|
+
guard windowSettled || attempt >= 60 else {
|
|
207
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
|
|
208
|
+
self?.finishRuntimeSmokeWhenWindowSettles(
|
|
209
|
+
mode: mode,
|
|
210
|
+
surfaceCount: surfaceCount,
|
|
211
|
+
labels: labels,
|
|
212
|
+
accessibility: accessibility,
|
|
213
|
+
retainedWindowReused: retainedWindowReused,
|
|
214
|
+
attempt: attempt + 1
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
finishRuntimeSmoke(
|
|
220
|
+
mode: mode,
|
|
221
|
+
surfaceCount: surfaceCount,
|
|
222
|
+
labels: labels,
|
|
223
|
+
accessibility: accessibility,
|
|
224
|
+
retainedWindowReused: retainedWindowReused
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private func finishRuntimeSmoke(
|
|
229
|
+
mode: String,
|
|
230
|
+
surfaceCount: Int,
|
|
231
|
+
labels: [String],
|
|
232
|
+
accessibility: RuntimeSmokeAccessibilitySnapshot,
|
|
233
|
+
retainedWindowReused: Bool = false,
|
|
234
|
+
resolvedCompanionPath: String? = nil,
|
|
235
|
+
companionCapabilitiesPassed: Bool = false
|
|
236
|
+
) {
|
|
237
|
+
guard let runtimeSmokeOutputPath else { return }
|
|
238
|
+
let result = RuntimeSmokeResult(
|
|
239
|
+
mode: mode,
|
|
240
|
+
processIdentifier: getpid(),
|
|
241
|
+
menuBarSurfaceCount: surfaceCount,
|
|
242
|
+
renderedStatusLabels: labels,
|
|
243
|
+
accessibilityObservationStatus: accessibility.status.rawValue,
|
|
244
|
+
accessibilityMenuBarItemCount: accessibility.itemCount,
|
|
245
|
+
accessibilityMenuBarLabels: accessibility.labels,
|
|
246
|
+
globalHandlersInstalled: store != nil,
|
|
247
|
+
permissionRequestsStarted: AccessibilityPromptGate.processShared.promptRequestCount,
|
|
248
|
+
windowCreationCount: windowCreationCount,
|
|
249
|
+
windowActivationCount: windowActivationCount,
|
|
250
|
+
retainedWindowReused: retainedWindowReused,
|
|
251
|
+
applicationActivationPolicy: NSApplication.shared.activationPolicy().rawValue,
|
|
252
|
+
applicationIsActive: NSApplication.shared.isActive,
|
|
253
|
+
mainWindowIsVisible: mainWindow?.isVisible ?? false,
|
|
254
|
+
mainWindowCanBecomeKey: mainWindow?.canBecomeKey ?? false,
|
|
255
|
+
mainWindowIsKey: mainWindow?.isKeyWindow ?? false,
|
|
256
|
+
resolvedCompanionPath: resolvedCompanionPath,
|
|
257
|
+
companionCapabilitiesPassed: companionCapabilitiesPassed
|
|
258
|
+
)
|
|
259
|
+
do {
|
|
260
|
+
let data = try JSONEncoder().encode(result)
|
|
261
|
+
try data.write(to: URL(fileURLWithPath: runtimeSmokeOutputPath), options: .atomic)
|
|
262
|
+
if let acknowledgementPath = runtimeSmokeAcknowledgementPath,
|
|
263
|
+
let completionPath = runtimeSmokeCompletionPath {
|
|
264
|
+
let processIdentifier = getpid()
|
|
265
|
+
DispatchQueue.global(qos: .utility).async {
|
|
266
|
+
for _ in 0..<300 {
|
|
267
|
+
if let challenge = try? String(
|
|
268
|
+
contentsOfFile: acknowledgementPath,
|
|
269
|
+
encoding: .utf8
|
|
270
|
+
).trimmingCharacters(in: .whitespacesAndNewlines),
|
|
271
|
+
!challenge.isEmpty {
|
|
272
|
+
do {
|
|
273
|
+
let response = RuntimeSmokeCompletionResponse(
|
|
274
|
+
challenge: challenge,
|
|
275
|
+
mode: mode,
|
|
276
|
+
processIdentifier: processIdentifier
|
|
277
|
+
)
|
|
278
|
+
let responseData = try JSONEncoder().encode(response)
|
|
279
|
+
try responseData.write(
|
|
280
|
+
to: URL(fileURLWithPath: completionPath),
|
|
281
|
+
options: .atomic
|
|
282
|
+
)
|
|
283
|
+
} catch {
|
|
284
|
+
NativeAppLog.write("Runtime smoke completion failed: \(error)")
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
Darwin._exit(0)
|
|
288
|
+
}
|
|
289
|
+
usleep(50_000)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
fputs("Runtime smoke result failed: \(error)\n", stderr)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
7
299
|
@main
|
|
8
300
|
struct RecordingsApp: App {
|
|
9
|
-
@
|
|
10
|
-
@StateObject private var
|
|
11
|
-
@StateObject private var projectStore = ProjectStore()
|
|
301
|
+
@NSApplicationDelegateAdaptor(RecordingsAppDelegate.self) private var appDelegate
|
|
302
|
+
@StateObject private var state: RecordingsAppState
|
|
12
303
|
|
|
13
304
|
init() {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
305
|
+
let plan = PermissionRequestLaunchPlan(arguments: CommandLine.arguments)
|
|
306
|
+
let state = RecordingsAppState(plan: plan)
|
|
307
|
+
_state = StateObject(wrappedValue: state)
|
|
308
|
+
appDelegate.state = state
|
|
309
|
+
if plan.isRuntimeSmoke {
|
|
310
|
+
// Runtime smoke retains the real launch classification but never invokes TCC APIs.
|
|
311
|
+
} else if plan.requestsAccessibilityPrompt {
|
|
312
|
+
Self.handlePermissionRequest(plan)
|
|
313
|
+
}
|
|
19
314
|
}
|
|
20
315
|
|
|
21
|
-
var body: some Scene {
|
|
22
|
-
MenuBarExtra {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
316
|
+
@SceneBuilder var body: some Scene {
|
|
317
|
+
MenuBarExtra(isInserted: menuBarInsertion) {
|
|
318
|
+
if let store = state.store {
|
|
319
|
+
MenuBarStatusView(store: store, openRecordings: state.openRecordings)
|
|
320
|
+
} else if state.runtimeSmokeProbe != nil {
|
|
321
|
+
EmptyView()
|
|
322
|
+
}
|
|
29
323
|
} label: {
|
|
30
|
-
if
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Image(systemName: "ellipsis.circle")
|
|
35
|
-
.symbolEffect(.pulse, isActive: true)
|
|
324
|
+
if let store = state.store {
|
|
325
|
+
MenuBarStatusLabel(store: store)
|
|
326
|
+
} else if let probe = state.runtimeSmokeProbe {
|
|
327
|
+
RuntimeSmokeMenuBarLabel(probe: probe)
|
|
36
328
|
} else {
|
|
37
329
|
Image(systemName: "mic.fill")
|
|
330
|
+
.accessibilityLabel("Recordings")
|
|
38
331
|
}
|
|
39
332
|
}
|
|
40
333
|
.menuBarExtraStyle(.window)
|
|
41
334
|
|
|
42
335
|
Settings {
|
|
43
|
-
|
|
336
|
+
if let store = state.store {
|
|
337
|
+
SettingsView(engine: store.engine, shortcuts: store.voiceShortcuts, projectStore: store.projectStore)
|
|
338
|
+
} else {
|
|
339
|
+
EmptyView()
|
|
340
|
+
}
|
|
44
341
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
342
|
+
.commands {
|
|
343
|
+
if let store = state.store {
|
|
344
|
+
CommandGroup(replacing: .newItem) {
|
|
345
|
+
Button("New Recording") {
|
|
346
|
+
store.pane = .record
|
|
347
|
+
store.engine.startRecording()
|
|
348
|
+
}
|
|
349
|
+
.keyboardShortcut("n", modifiers: .command)
|
|
350
|
+
}
|
|
351
|
+
CommandGroup(after: .toolbar) {
|
|
352
|
+
Button("Recordings Library") { store.pane = .library }
|
|
353
|
+
.keyboardShortcut("l", modifiers: .command)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
52
356
|
}
|
|
53
357
|
}
|
|
54
358
|
|
|
55
|
-
private
|
|
56
|
-
|
|
57
|
-
|
|
359
|
+
private var menuBarInsertion: Binding<Bool> {
|
|
360
|
+
Binding(
|
|
361
|
+
get: { state.declaresMenuBar && (state.store != nil || state.runtimeSmokeProbe != nil) },
|
|
362
|
+
set: { _ in }
|
|
363
|
+
)
|
|
364
|
+
}
|
|
58
365
|
|
|
366
|
+
private static func handlePermissionRequest(_ plan: PermissionRequestLaunchPlan) {
|
|
367
|
+
NSApplication.shared.setActivationPolicy(.accessory)
|
|
59
368
|
NativeAppLog.write("request-permissions argument received")
|
|
369
|
+
let work = DispatchGroup()
|
|
370
|
+
let resultBox = PermissionRequestResultBox()
|
|
371
|
+
work.enter()
|
|
60
372
|
AVCaptureDevice.requestAccess(for: .audio) { granted in
|
|
373
|
+
resultBox.setMicrophoneGranted(granted)
|
|
61
374
|
NativeAppLog.write("request-permissions microphone granted=\(granted)")
|
|
375
|
+
work.leave()
|
|
62
376
|
}
|
|
63
377
|
|
|
64
|
-
let
|
|
65
|
-
|
|
66
|
-
)
|
|
67
|
-
NativeAppLog.write("request-permissions accessibility trusted=\(trusted)")
|
|
378
|
+
let accessibility = AccessibilityPromptGate.processShared.requestExplicitly()
|
|
379
|
+
NativeAppLog.write("request-permissions accessibility trusted=\(accessibility.trusted)")
|
|
68
380
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
381
|
+
if plan.opensPermissionSettings {
|
|
382
|
+
work.enter()
|
|
383
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
|
384
|
+
if let microphone = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone") {
|
|
385
|
+
NSWorkspace.shared.open(microphone)
|
|
386
|
+
}
|
|
387
|
+
if let accessibility = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
|
|
388
|
+
NSWorkspace.shared.open(accessibility)
|
|
389
|
+
}
|
|
390
|
+
work.leave()
|
|
76
391
|
}
|
|
77
392
|
}
|
|
393
|
+
work.notify(queue: .global(qos: .userInitiated)) {
|
|
394
|
+
let completedAccessibility = AccessibilityPromptGate.processShared.waitForExplicitRequestCompletion(
|
|
395
|
+
accessibility
|
|
396
|
+
)
|
|
397
|
+
let outcome = resultBox.outcome(accessibilityTrusted: completedAccessibility.trusted)
|
|
398
|
+
NativeAppLog.write("request-permissions helper completed success=\(outcome.succeeded)")
|
|
399
|
+
exit(outcome.succeeded ? EXIT_SUCCESS : EXIT_FAILURE)
|
|
400
|
+
}
|
|
78
401
|
}
|
|
79
402
|
}
|