@hasna/recordings 0.2.10 → 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 +350 -35
  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 +1922 -77
  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 +1990 -29
  167. package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
@@ -0,0 +1,130 @@
1
+ import SwiftUI
2
+ import RecordingsLib
3
+
4
+ /// The recordings library list: a search field, then a flat list of transcripts on the white
5
+ /// canvas, separated by hairline dividers. Selecting a row shows it in the detail pane.
6
+ struct RecordingsListView: View {
7
+ @ObservedObject var store: RecordingsStore
8
+
9
+ var body: some View {
10
+ VStack(spacing: 0) {
11
+ searchRow
12
+ Divider().opacity(0.5)
13
+ list
14
+ }
15
+ }
16
+
17
+ private var searchRow: some View {
18
+ HStack(spacing: 8) {
19
+ Image(systemName: "magnifyingglass").foregroundStyle(.secondary)
20
+ TextField("Search transcripts", text: $store.searchText)
21
+ .textFieldStyle(.plain)
22
+ .font(.system(.body, design: .rounded))
23
+ if !store.searchText.isEmpty {
24
+ Button { store.searchText = "" } label: {
25
+ Image(systemName: "xmark.circle.fill").foregroundStyle(.tertiary)
26
+ }
27
+ .buttonStyle(.plain)
28
+ }
29
+ }
30
+ .padding(.horizontal, 16).padding(.vertical, 10)
31
+ }
32
+
33
+ @ViewBuilder
34
+ private var list: some View {
35
+ if store.isLoadingLibrary && store.library.isEmpty {
36
+ centered { ProgressView() }
37
+ } else if let error = store.loadError, store.library.isEmpty {
38
+ centered {
39
+ VStack(spacing: 8) {
40
+ Image(systemName: "exclamationmark.triangle").font(.largeTitle).foregroundStyle(.orange)
41
+ Text(error).font(.callout).foregroundStyle(.secondary).multilineTextAlignment(.center)
42
+ Button("Retry") { store.loadLibrary() }
43
+ }
44
+ .padding(24)
45
+ }
46
+ } else if store.visibleRecordings.isEmpty {
47
+ centered {
48
+ VStack(spacing: 8) {
49
+ Image(systemName: "waveform").font(.largeTitle).foregroundStyle(.quaternary)
50
+ Text(emptyMessage).foregroundStyle(.secondary).multilineTextAlignment(.center)
51
+ Button("Record") { store.pane = .record }.buttonStyle(.borderless)
52
+ }
53
+ }
54
+ } else {
55
+ ScrollView {
56
+ LazyVStack(spacing: 0) {
57
+ ForEach(store.visibleRecordings) { rec in
58
+ RecordingRow(rec: rec,
59
+ projectName: store.projectName(rec.projectId),
60
+ selected: store.selection == rec.id)
61
+ .contentShape(Rectangle())
62
+ .onTapGesture { store.selection = rec.id }
63
+ .contextMenu {
64
+ Button("Copy") {
65
+ let pb = NSPasteboard.general
66
+ pb.clearContents(); pb.setString(rec.displayText, forType: .string)
67
+ }
68
+ Button("Paste into front app") { store.engine.pasteIntoFrontApp(rec.displayText) }
69
+ Divider()
70
+ Button("Delete", role: .destructive) { store.delete(id: rec.id) }
71
+ }
72
+ Divider().opacity(0.35).padding(.leading, 16)
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ private var emptyMessage: String {
80
+ if !store.searchText.isEmpty { return "No matches for “\(store.searchText)”" }
81
+ switch store.filter {
82
+ case .project(let id):
83
+ return "No recordings in \(store.projectName(id) ?? "this project") yet"
84
+ case .noProject: return "No unassigned recordings"
85
+ case .mode(let m): return "No \(m) recordings yet"
86
+ case .thisMachine, .machine: return "No recordings from this machine"
87
+ case .all: return "No recordings yet"
88
+ }
89
+ }
90
+
91
+ @ViewBuilder
92
+ private func centered<Content: View>(@ViewBuilder _ content: () -> Content) -> some View {
93
+ VStack { Spacer(); content(); Spacer() }.frame(maxWidth: .infinity, maxHeight: .infinity)
94
+ }
95
+ }
96
+
97
+ private struct RecordingRow: View {
98
+ let rec: Recording
99
+ let projectName: String?
100
+ let selected: Bool
101
+
102
+ var body: some View {
103
+ VStack(alignment: .leading, spacing: 5) {
104
+ Text(rec.snippet)
105
+ .font(.system(.headline, design: .rounded)).lineLimit(2)
106
+ HStack(spacing: 6) {
107
+ if let date = rec.createdDate {
108
+ Text(date.relativeDescription).font(.caption2).foregroundStyle(.secondary)
109
+ }
110
+ if rec.durationMs > 0 {
111
+ Text("·").font(.caption2).foregroundStyle(.secondary)
112
+ Text(rec.durationLabel).font(.caption2).foregroundStyle(.secondary)
113
+ }
114
+ if rec.isEnhanced {
115
+ Text("·").font(.caption2).foregroundStyle(.secondary)
116
+ Label("Enhanced", systemImage: "wand.and.stars")
117
+ .labelStyle(.titleAndIcon).font(.caption2).foregroundStyle(Theme.accent.opacity(0.9))
118
+ }
119
+ if let projectName {
120
+ Text("·").font(.caption2).foregroundStyle(.secondary)
121
+ Label(projectName, systemImage: "folder").labelStyle(.titleAndIcon)
122
+ .font(.caption2).foregroundStyle(.secondary)
123
+ }
124
+ }
125
+ }
126
+ .padding(.horizontal, 16).padding(.vertical, 11)
127
+ .frame(maxWidth: .infinity, alignment: .leading)
128
+ .background { if selected { Theme.accent.opacity(0.12) } }
129
+ }
130
+ }
@@ -0,0 +1,206 @@
1
+ import Foundation
2
+ import SwiftUI
3
+ import Combine
4
+ import RecordingsLib
5
+
6
+ /// Which main pane the canvas shows.
7
+ enum Pane: Equatable {
8
+ case record // the recording workspace (default / first screen)
9
+ case library // browse past recordings
10
+ }
11
+
12
+ /// Sidebar filter selection for the library.
13
+ enum LibraryFilter: Hashable {
14
+ case all
15
+ case project(String) // project id
16
+ case noProject
17
+ case mode(String) // "raw" | "enhanced"
18
+ case thisMachine
19
+ case machine(String)
20
+ }
21
+
22
+ /// Observable application state for the full macOS app. Bridges the live `RecordingEngine`,
23
+ /// the `ProjectStore` / `VoiceShortcuts`, and the `recordings` CLI Store.
24
+ @MainActor
25
+ final class RecordingsStore: ObservableObject {
26
+ let engine: RecordingEngine
27
+ let projectStore: ProjectStore
28
+ let voiceShortcuts: VoiceShortcuts
29
+
30
+ @Published var pane: Pane = .record
31
+ @Published var library: [Recording] = []
32
+ @Published var selection: String?
33
+ @Published var filter: LibraryFilter = .all
34
+ @Published var searchText: String = ""
35
+ @Published var isLoadingLibrary = false
36
+ @Published var loadError: String?
37
+ @Published var operationError: String?
38
+
39
+ @Published var stats: RecordingStats?
40
+ let localMachineID: String
41
+ private let home: String
42
+ private var cancellables = Set<AnyCancellable>()
43
+
44
+ init(engine: RecordingEngine = RecordingEngine(),
45
+ projectStore: ProjectStore = ProjectStore(),
46
+ voiceShortcuts: VoiceShortcuts = VoiceShortcuts()) {
47
+ self.engine = engine
48
+ self.projectStore = projectStore
49
+ self.voiceShortcuts = voiceShortcuts
50
+ self.home = FileManager.default.homeDirectoryForCurrentUser.path
51
+ self.localMachineID = NativeMachineIdentity.current()
52
+ engine.projectStore = projectStore
53
+ engine.voiceShortcuts = voiceShortcuts
54
+
55
+ // Re-publish the wrapped ObservableObjects so views that observe only this store
56
+ // refresh on live recording changes (timer, live text) and project edits.
57
+ engine.objectWillChange
58
+ .sink { [weak self] in self?.objectWillChange.send() }
59
+ .store(in: &cancellables)
60
+ engine.$persistedRecordingRevision
61
+ .dropFirst()
62
+ .sink { [weak self] _ in self?.loadLibrary() }
63
+ .store(in: &cancellables)
64
+ projectStore.objectWillChange
65
+ .sink { [weak self] in self?.objectWillChange.send() }
66
+ .store(in: &cancellables)
67
+ voiceShortcuts.objectWillChange
68
+ .sink { [weak self] in self?.objectWillChange.send() }
69
+ .store(in: &cancellables)
70
+
71
+ reconcileProjects()
72
+ }
73
+
74
+ func reconcileProjects() {
75
+ let home = self.home
76
+ Task { [weak self] in
77
+ guard let self else { return }
78
+ do {
79
+ try await projectStore.reconcileWithCanonicalStore(home: home)
80
+ self.operationError = nil
81
+ } catch {
82
+ self.operationError = projectStore.persistenceError
83
+ ?? (error as? RecordingsCLI.Failure)?.message
84
+ ?? error.localizedDescription
85
+ }
86
+ }
87
+ }
88
+
89
+ // MARK: - Derived collections
90
+
91
+ var projects: [RecProject] { projectStore.settings.projects }
92
+
93
+ var machines: [String] {
94
+ Set(library.compactMap { $0.machineId }.filter { !$0.isEmpty })
95
+ .sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
96
+ }
97
+
98
+ var lastUpdated: Date? { library.compactMap(\.createdDate).max() }
99
+
100
+ var visibleRecordings: [Recording] {
101
+ library.filter { matchesFilter($0) && matchesSearch($0) }
102
+ }
103
+
104
+ private func matchesFilter(_ r: Recording) -> Bool {
105
+ switch filter {
106
+ case .all: return true
107
+ case .project(let id): return r.projectId == id
108
+ case .noProject: return r.projectId == nil
109
+ case .mode(let m): return r.processingMode == m
110
+ case .thisMachine: return r.machineId == localMachineID
111
+ case .machine(let m): return r.machineId == m
112
+ }
113
+ }
114
+
115
+ private func matchesSearch(_ r: Recording) -> Bool {
116
+ let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
117
+ guard !q.isEmpty else { return true }
118
+ if r.displayText.localizedCaseInsensitiveContains(q) { return true }
119
+ if r.tags.contains(where: { $0.localizedCaseInsensitiveContains(q) }) { return true }
120
+ return false
121
+ }
122
+
123
+ func count(for filter: LibraryFilter) -> Int {
124
+ library.filter {
125
+ switch filter {
126
+ case .all: return true
127
+ case .project(let id): return $0.projectId == id
128
+ case .noProject: return $0.projectId == nil
129
+ case .mode(let m): return $0.processingMode == m
130
+ case .thisMachine: return $0.machineId == localMachineID
131
+ case .machine(let m): return $0.machineId == m
132
+ }
133
+ }.count
134
+ }
135
+
136
+ var selectedRecording: Recording? {
137
+ guard let id = selection else { return nil }
138
+ return library.first { $0.id == id }
139
+ }
140
+
141
+ func projectName(_ id: String?) -> String? {
142
+ guard let id else { return nil }
143
+ return projects.first { $0.id == id }?.name
144
+ }
145
+
146
+ // MARK: - Library loading
147
+
148
+ private var reloadRequestedDuringLoad = false
149
+
150
+ func loadLibrary() {
151
+ guard !isLoadingLibrary else { reloadRequestedDuringLoad = true; return }
152
+ isLoadingLibrary = true
153
+ let home = home
154
+ Task.detached(priority: .userInitiated) {
155
+ let result: Result<([Recording], RecordingStats?), Error>
156
+ do {
157
+ let recs = try RecordingsCLI.listAll(home: home)
158
+ let stats = try? RecordingsCLI.stats(home: home)
159
+ result = .success((recs, stats))
160
+ } catch {
161
+ result = .failure(error)
162
+ }
163
+ await MainActor.run {
164
+ self.isLoadingLibrary = false
165
+ switch result {
166
+ case .success(let (recs, stats)):
167
+ self.library = recs
168
+ self.stats = stats
169
+ self.loadError = nil
170
+ if self.selection == nil || !recs.contains(where: { $0.id == self.selection }) {
171
+ self.selection = self.visibleRecordings.first?.id
172
+ }
173
+ case .failure(let error):
174
+ self.loadError = (error as? RecordingsCLI.Failure)?.message ?? error.localizedDescription
175
+ }
176
+ if self.reloadRequestedDuringLoad {
177
+ self.reloadRequestedDuringLoad = false
178
+ self.loadLibrary()
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ func delete(id: String) {
185
+ let home = home
186
+ Task.detached(priority: .userInitiated) {
187
+ let result: Result<Void, Error>
188
+ do {
189
+ try RecordingsCLI.delete(id: id, home: home)
190
+ result = .success(())
191
+ } catch {
192
+ result = .failure(error)
193
+ }
194
+ await MainActor.run {
195
+ switch result {
196
+ case .success:
197
+ self.library.removeAll { $0.id == id }
198
+ if self.selection == id { self.selection = self.visibleRecordings.first?.id }
199
+ case .failure(let error):
200
+ self.operationError = (error as? RecordingsCLI.Failure)?.message ?? error.localizedDescription
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ }
@@ -0,0 +1,158 @@
1
+ @preconcurrency import Cocoa
2
+ import SwiftUI
3
+ import RecordingsLib
4
+
5
+ @MainActor
6
+ final class RuntimeSmokeProbe: ObservableObject {
7
+ enum Phase: Int, CaseIterable {
8
+ case idle
9
+ case recording
10
+ case transcribing
11
+ }
12
+
13
+ @Published private(set) var phase: Phase = .idle
14
+ private(set) var surfaceAppearances = 0
15
+ private(set) var renderedLabels: [String] = []
16
+ private var completionScheduled = false
17
+ var completed: (() -> Void)? {
18
+ didSet { finishIfReady() }
19
+ }
20
+
21
+ var presentation: MenuBarPresentation {
22
+ MenuBarPresentation(
23
+ isRecording: phase == .recording,
24
+ canStartRecording: phase == .idle,
25
+ statusMessage: phase == .transcribing ? "Transcribing" : "Ready"
26
+ )
27
+ }
28
+
29
+ func surfaceAppeared() {
30
+ surfaceAppearances += 1
31
+ observeCurrentPresentation()
32
+ }
33
+
34
+ func presentationChanged() {
35
+ observeCurrentPresentation()
36
+ }
37
+
38
+ private func observeCurrentPresentation() {
39
+ let label = presentation.accessibilityLabel
40
+ if renderedLabels.last != label {
41
+ renderedLabels.append(label)
42
+ }
43
+ switch phase {
44
+ case .idle:
45
+ DispatchQueue.main.async { self.phase = .recording }
46
+ case .recording:
47
+ DispatchQueue.main.async { self.phase = .transcribing }
48
+ case .transcribing:
49
+ finishIfReady()
50
+ }
51
+ }
52
+
53
+ private func finishIfReady() {
54
+ guard phase == .transcribing,
55
+ renderedLabels.count == Phase.allCases.count,
56
+ !completionScheduled,
57
+ completed != nil else { return }
58
+ completionScheduled = true
59
+ DispatchQueue.main.async { self.completed?() }
60
+ }
61
+ }
62
+
63
+ struct RuntimeSmokeMenuBarLabel: View {
64
+ @ObservedObject var probe: RuntimeSmokeProbe
65
+
66
+ var body: some View {
67
+ Image(systemName: probe.presentation.iconName)
68
+ .accessibilityLabel(probe.presentation.accessibilityLabel)
69
+ .onAppear { probe.surfaceAppeared() }
70
+ .onChange(of: probe.phase) { _, _ in
71
+ probe.presentationChanged()
72
+ }
73
+ }
74
+ }
75
+
76
+ struct RuntimeSmokeResult: Codable {
77
+ let mode: String
78
+ let processIdentifier: Int32
79
+ let menuBarSurfaceCount: Int
80
+ let renderedStatusLabels: [String]
81
+ let accessibilityObservationStatus: String
82
+ let accessibilityMenuBarItemCount: Int
83
+ let accessibilityMenuBarLabels: [String]
84
+ let globalHandlersInstalled: Bool
85
+ let permissionRequestsStarted: Int
86
+ let windowCreationCount: Int
87
+ let windowActivationCount: Int
88
+ let retainedWindowReused: Bool
89
+ let applicationActivationPolicy: Int
90
+ let applicationIsActive: Bool
91
+ let mainWindowIsVisible: Bool
92
+ let mainWindowCanBecomeKey: Bool
93
+ let mainWindowIsKey: Bool
94
+ let resolvedCompanionPath: String?
95
+ let companionCapabilitiesPassed: Bool
96
+ }
97
+
98
+ enum RuntimeSmokeAccessibilityObservationStatus: String {
99
+ case available
100
+ case absent
101
+ case unavailable
102
+ }
103
+
104
+ struct RuntimeSmokeAccessibilitySnapshot {
105
+ let status: RuntimeSmokeAccessibilityObservationStatus
106
+ let itemCount: Int
107
+ let labels: [String]
108
+
109
+ static func processMenuBarExtras(processIdentifier: pid_t = getpid()) -> Self {
110
+ let application = AXUIElementCreateApplication(processIdentifier)
111
+ var menuBarValue: CFTypeRef?
112
+ let menuBarError = AXUIElementCopyAttributeValue(
113
+ application,
114
+ kAXExtrasMenuBarAttribute as CFString,
115
+ &menuBarValue
116
+ )
117
+ if menuBarError == .noValue || menuBarError == .attributeUnsupported {
118
+ return Self(status: .absent, itemCount: 0, labels: [])
119
+ }
120
+ guard menuBarError == .success,
121
+ let menuBarValue,
122
+ CFGetTypeID(menuBarValue) == AXUIElementGetTypeID() else {
123
+ return Self(status: .unavailable, itemCount: -1, labels: [])
124
+ }
125
+
126
+ let menuBar = menuBarValue as! AXUIElement
127
+ var childrenValue: CFTypeRef?
128
+ let childrenError = AXUIElementCopyAttributeValue(
129
+ menuBar,
130
+ kAXChildrenAttribute as CFString,
131
+ &childrenValue
132
+ )
133
+ if childrenError == .noValue || childrenError == .attributeUnsupported {
134
+ return Self(status: .unavailable, itemCount: -1, labels: [])
135
+ }
136
+ guard childrenError == .success,
137
+ let children = childrenValue as? [AXUIElement] else {
138
+ return Self(status: .unavailable, itemCount: -1, labels: [])
139
+ }
140
+
141
+ let labels = children.compactMap { child -> String? in
142
+ for attribute in [kAXDescriptionAttribute, kAXTitleAttribute, kAXHelpAttribute] {
143
+ var value: CFTypeRef?
144
+ if AXUIElementCopyAttributeValue(child, attribute as CFString, &value) == .success,
145
+ let label = value as? String,
146
+ !label.isEmpty {
147
+ return label
148
+ }
149
+ }
150
+ return nil
151
+ }
152
+ return Self(
153
+ status: children.isEmpty ? .absent : .available,
154
+ itemCount: children.count,
155
+ labels: labels
156
+ )
157
+ }
158
+ }