@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.
Files changed (167) hide show
  1. package/README.md +349 -69
  2. package/bun.lock +3 -0
  3. package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
  4. package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
  5. package/dist/cli/index.js +1728 -320
  6. package/dist/cli/macos-permissions.d.ts +13 -0
  7. package/dist/cli/macos-permissions.d.ts.map +1 -0
  8. package/dist/cli/options.d.ts +12 -0
  9. package/dist/cli/options.d.ts.map +1 -1
  10. package/dist/db/pg-migrations.d.ts.map +1 -1
  11. package/dist/db/recordings.d.ts +2 -1
  12. package/dist/db/recordings.d.ts.map +1 -1
  13. package/dist/db/remote-storage.d.ts +5 -1
  14. package/dist/db/remote-storage.d.ts.map +1 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +628 -147
  18. package/dist/lib/bun-runtime.d.ts +11 -0
  19. package/dist/lib/bun-runtime.d.ts.map +1 -0
  20. package/dist/lib/config.d.ts +8 -1
  21. package/dist/lib/config.d.ts.map +1 -1
  22. package/dist/lib/enhancer.d.ts +10 -2
  23. package/dist/lib/enhancer.d.ts.map +1 -1
  24. package/dist/lib/install-maintenance.d.ts +24 -0
  25. package/dist/lib/install-maintenance.d.ts.map +1 -0
  26. package/dist/lib/machine.d.ts +2 -0
  27. package/dist/lib/machine.d.ts.map +1 -0
  28. package/dist/lib/recorder.d.ts +2 -1
  29. package/dist/lib/recorder.d.ts.map +1 -1
  30. package/dist/lib/recording-create-identity.d.ts +13 -0
  31. package/dist/lib/recording-create-identity.d.ts.map +1 -0
  32. package/dist/lib/release-install-policy.d.ts +31 -0
  33. package/dist/lib/release-install-policy.d.ts.map +1 -0
  34. package/dist/lib/transcriber.d.ts.map +1 -1
  35. package/dist/mcp/index.d.ts.map +1 -1
  36. package/dist/mcp/index.js +977 -194
  37. package/dist/sdk/index.js +5 -1
  38. package/dist/sdk/v1.generated.d.ts +4 -0
  39. package/dist/sdk/v1.generated.d.ts.map +1 -1
  40. package/dist/server/cloud-config.d.ts +9 -0
  41. package/dist/server/cloud-config.d.ts.map +1 -0
  42. package/dist/server/cloud-readiness.d.ts +10 -0
  43. package/dist/server/cloud-readiness.d.ts.map +1 -0
  44. package/dist/server/cloud.d.ts +9 -18
  45. package/dist/server/cloud.d.ts.map +1 -1
  46. package/dist/server/index.js +1654 -304
  47. package/dist/server/migrate-command.d.ts +11 -0
  48. package/dist/server/migrate-command.d.ts.map +1 -0
  49. package/dist/server/openapi.d.ts +22 -0
  50. package/dist/server/openapi.d.ts.map +1 -1
  51. package/dist/server/repo.d.ts +8 -1
  52. package/dist/server/repo.d.ts.map +1 -1
  53. package/dist/server/serve.d.ts +6 -1
  54. package/dist/server/serve.d.ts.map +1 -1
  55. package/dist/server/v1.d.ts.map +1 -1
  56. package/dist/storage.js +515 -80
  57. package/dist/store.d.ts +3 -1
  58. package/dist/store.d.ts.map +1 -1
  59. package/dist/types/index.d.ts +10 -0
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/dist/version.d.ts +1 -1
  62. package/package.json +7 -4
  63. package/packaging/macos/Empty.entitlements +5 -0
  64. package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
  65. package/packaging/macos/Verifier.entitlements +10 -0
  66. package/packaging/macos/artifact-verifier.sb +33 -0
  67. package/packaging/macos/build_release_pkg.sh +872 -0
  68. package/packaging/macos/managed_bootstrap.sh +623 -0
  69. package/packaging/macos/pkgutil_fingerprint.awk +37 -0
  70. package/packaging/macos/release_lifecycle.ts +463 -0
  71. package/packaging/macos/scripts/postinstall +565 -0
  72. package/packaging/macos/scripts/preinstall +297 -0
  73. package/scripts/build_companion_cli.sh +337 -0
  74. package/scripts/build_native_fs_guard.sh +59 -0
  75. package/scripts/generate-sdk.ts +19 -1
  76. package/scripts/install_macos_app.sh +1919 -98
  77. package/scripts/macos_artifact.ts +5316 -0
  78. package/scripts/migrate.ts +38 -10
  79. package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
  80. package/scripts/native/recordings_fs_guard.c +1167 -0
  81. package/scripts/native_fs_guard.ts +158 -0
  82. package/scripts/release-guard.ts +20 -1
  83. package/scripts/resolve_tailscale_cli.sh +389 -0
  84. package/scripts/smoke_macos_app.sh +573 -0
  85. package/src/native/Recordings/App/ContentView.swift +83 -0
  86. package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
  87. package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
  88. package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
  89. package/src/native/Recordings/App/RecordingsApp.swift +367 -44
  90. package/src/native/Recordings/App/RecordingsListView.swift +130 -0
  91. package/src/native/Recordings/App/RecordingsStore.swift +206 -0
  92. package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
  93. package/src/native/Recordings/App/SidebarView.swift +212 -0
  94. package/src/native/Recordings/App/Theme.swift +87 -0
  95. package/src/native/Recordings/Package.resolved +5 -5
  96. package/src/native/Recordings/Package.swift +62 -2
  97. package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
  98. package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
  99. package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
  100. package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
  101. package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
  102. package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
  103. package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
  104. package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
  105. package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
  106. package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
  107. package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
  108. package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
  109. package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
  110. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
  111. package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
  112. package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
  113. package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
  114. package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
  115. package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
  116. package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
  117. package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
  118. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
  119. package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
  120. package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
  121. package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
  122. package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
  123. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
  124. package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
  125. package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
  126. package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
  127. package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
  128. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
  129. package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
  130. package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
  131. package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
  132. package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
  133. package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
  134. package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
  135. package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
  136. package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
  137. package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
  138. package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
  139. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
  140. package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
  141. package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
  142. package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
  143. package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
  144. package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
  145. package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
  146. package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
  147. package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
  148. package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
  149. package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
  150. package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
  151. package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
  152. package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
  153. package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
  154. package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
  155. package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
  156. package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
  157. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
  158. package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
  159. package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
  160. package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
  161. package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
  162. package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
  163. package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
  164. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
  165. package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
  166. package/src/native/Recordings/build.sh +1973 -236
  167. 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
- @StateObject private var engine = RecordingEngine()
10
- @StateObject private var shortcuts = VoiceShortcuts()
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
- Self.terminateDuplicateInstances()
15
- AXIsProcessTrustedWithOptions(
16
- [kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true] as CFDictionary
17
- )
18
- Self.handlePermissionRequestArguments()
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
- MenuBarPopover(engine: engine, shortcuts: shortcuts, projectStore: projectStore)
24
- .frame(width: 320, height: 420)
25
- .onAppear {
26
- engine.projectStore = projectStore
27
- engine.voiceShortcuts = shortcuts
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 engine.isRecording {
31
- Image(systemName: "waveform")
32
- .symbolEffect(.variableColor.iterative, isActive: true)
33
- } else if engine.isTranscribing {
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
- SettingsView(engine: engine, shortcuts: shortcuts, projectStore: projectStore)
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
- private static func terminateDuplicateInstances() {
48
- let currentPID = ProcessInfo.processInfo.processIdentifier
49
- for app in NSRunningApplication.runningApplications(withBundleIdentifier: "com.hasna.recordings")
50
- where app.processIdentifier != currentPID {
51
- app.terminate()
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 static func handlePermissionRequestArguments() {
56
- let arguments = CommandLine.arguments
57
- guard arguments.contains("--request-permissions") else { return }
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 trusted = AXIsProcessTrustedWithOptions(
65
- [kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true] as CFDictionary
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
- guard arguments.contains("--open-permission-settings") else { return }
70
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
71
- if let microphone = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone") {
72
- NSWorkspace.shared.open(microphone)
73
- }
74
- if let accessibility = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
75
- NSWorkspace.shared.open(accessibility)
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
  }