@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,212 @@
1
+ import SwiftUI
2
+ import RecordingsLib
3
+
4
+ /// Narrow violet sidebar: Record · Library · Projects · Modes · Machines. White text/tint
5
+ /// over the gradient; selected rows get a translucent white highlight (Liquid Glass refracts
6
+ /// the violet beneath).
7
+ struct SidebarView: View {
8
+ @ObservedObject var store: RecordingsStore
9
+ @Environment(\.colorScheme) private var colorScheme
10
+
11
+ @State private var addingProject = false
12
+ @State private var newProjectName = ""
13
+
14
+ var body: some View {
15
+ ScrollView {
16
+ VStack(alignment: .leading, spacing: 18) {
17
+ workspaceSection
18
+ librarySection
19
+ projectsSection
20
+ if hasAnyMode { modesSection }
21
+ if !store.machines.isEmpty { machinesSection }
22
+ Spacer(minLength: 8)
23
+ }
24
+ .padding(14)
25
+ .padding(.top, 24)
26
+ }
27
+ .scrollContentBackground(.hidden)
28
+ .foregroundStyle(.white)
29
+ .tint(.white)
30
+ }
31
+
32
+ // MARK: - Workspace
33
+
34
+ private var workspaceSection: some View {
35
+ section(title: "Workspace") {
36
+ Button {
37
+ withAnimation(.spring(response: 0.3, dampingFraction: 0.85)) { store.pane = .record }
38
+ } label: {
39
+ rowLabel(icon: "mic.fill", label: "Record", count: nil,
40
+ selected: store.pane == .record, accentDot: store.engine.isRecording)
41
+ }
42
+ .buttonStyle(.plain)
43
+ }
44
+ }
45
+
46
+ // MARK: - Library
47
+
48
+ private var librarySection: some View {
49
+ section(title: "Library") {
50
+ filterRow(.all, icon: "waveform", label: "All Recordings")
51
+ if store.count(for: .noProject) > 0 {
52
+ filterRow(.noProject, icon: "tray", label: "No Project")
53
+ }
54
+ }
55
+ }
56
+
57
+ // MARK: - Projects
58
+
59
+ private var projectsSection: some View {
60
+ section(title: "Projects", trailing: {
61
+ Button { addingProject = true; newProjectName = "" } label: {
62
+ Image(systemName: "plus").font(.system(size: 11, weight: .semibold))
63
+ }
64
+ .buttonStyle(.plain)
65
+ .disabled(!store.projectStore.canMutateProjects)
66
+ .help("Add Project")
67
+ .popover(isPresented: $addingProject, arrowEdge: .trailing) {
68
+ VStack(alignment: .leading, spacing: 10) {
69
+ Text("New Project").font(.headline)
70
+ TextField("Project name", text: $newProjectName)
71
+ .textFieldStyle(.roundedBorder).frame(width: 200)
72
+ .onSubmit(commitNewProject)
73
+ HStack {
74
+ Spacer()
75
+ Button("Cancel") { addingProject = false }
76
+ Button("Add") { commitNewProject() }
77
+ .keyboardShortcut(.defaultAction)
78
+ .disabled(newProjectName.trimmingCharacters(in: .whitespaces).isEmpty)
79
+ }
80
+ }
81
+ .padding(14)
82
+ }
83
+ }) {
84
+ if store.projects.isEmpty {
85
+ Text("No projects")
86
+ .font(.caption).foregroundStyle(.white.opacity(0.5)).padding(.leading, 6)
87
+ }
88
+ ForEach(store.projects) { project in
89
+ filterRow(.project(project.id), icon: "folder", label: project.name)
90
+ .disabled(!store.projectStore.canMutateProjects)
91
+ .contextMenu {
92
+ Button("Delete", role: .destructive) {
93
+ do {
94
+ try store.projectStore.removeProject(id: project.id)
95
+ } catch {
96
+ store.operationError = store.projectStore.persistenceError ?? error.localizedDescription
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ private func commitNewProject() {
105
+ let name = newProjectName.trimmingCharacters(in: .whitespacesAndNewlines)
106
+ guard !name.isEmpty else { return }
107
+ Task {
108
+ do {
109
+ try await store.projectStore.addProject(name: name)
110
+ addingProject = false
111
+ newProjectName = ""
112
+ } catch {
113
+ store.operationError = store.projectStore.persistenceError
114
+ ?? (error as? RecordingsCLI.Failure)?.message
115
+ ?? error.localizedDescription
116
+ }
117
+ }
118
+ }
119
+
120
+ // MARK: - Modes
121
+
122
+ private var hasAnyMode: Bool {
123
+ store.count(for: .mode("enhanced")) > 0 || store.count(for: .mode("raw")) > 0
124
+ }
125
+
126
+ private var modesSection: some View {
127
+ section(title: "Modes") {
128
+ if store.count(for: .mode("enhanced")) > 0 {
129
+ filterRow(.mode("enhanced"), icon: "wand.and.stars", label: "Enhanced")
130
+ }
131
+ if store.count(for: .mode("raw")) > 0 {
132
+ filterRow(.mode("raw"), icon: "text.quote", label: "Raw")
133
+ }
134
+ }
135
+ }
136
+
137
+ // MARK: - Machines
138
+
139
+ private var machinesSection: some View {
140
+ section(title: "Machines") {
141
+ if store.count(for: .thisMachine) > 0 {
142
+ filterRow(.thisMachine, icon: "desktopcomputer", label: "This Mac")
143
+ }
144
+ ForEach(otherMachines, id: \.self) { machine in
145
+ filterRow(.machine(machine), icon: "macpro.gen3.server", label: machine)
146
+ }
147
+ }
148
+ }
149
+
150
+ private var otherMachines: [String] {
151
+ store.machines.filter { $0 != store.localMachineID }
152
+ }
153
+
154
+ // MARK: - Building blocks
155
+
156
+ @ViewBuilder
157
+ private func section<Trailing: View, Content: View>(
158
+ title: String,
159
+ @ViewBuilder trailing: () -> Trailing = { EmptyView() },
160
+ @ViewBuilder content: () -> Content
161
+ ) -> some View {
162
+ VStack(alignment: .leading, spacing: 6) {
163
+ HStack {
164
+ Text(title.uppercased())
165
+ .font(.caption2.weight(.semibold))
166
+ .foregroundStyle(.white.opacity(0.6))
167
+ .padding(.leading, 6)
168
+ Spacer()
169
+ trailing().foregroundStyle(.white.opacity(0.85)).padding(.trailing, 6)
170
+ }
171
+ content()
172
+ }
173
+ }
174
+
175
+ private func filterRow(_ target: LibraryFilter, icon: String, label: String) -> some View {
176
+ let selected = store.pane == .library && store.filter == target
177
+ let count = store.count(for: target)
178
+ return Button {
179
+ withAnimation(.spring(response: 0.32, dampingFraction: 0.82)) {
180
+ store.filter = target
181
+ store.pane = .library
182
+ if !store.visibleRecordings.contains(where: { $0.id == store.selection }) {
183
+ store.selection = store.visibleRecordings.first?.id
184
+ }
185
+ }
186
+ } label: {
187
+ rowLabel(icon: icon, label: label, count: count > 0 ? count : nil, selected: selected, accentDot: false)
188
+ }
189
+ .buttonStyle(.plain)
190
+ }
191
+
192
+ private func rowLabel(icon: String, label: String, count: Int?, selected: Bool, accentDot: Bool) -> some View {
193
+ HStack(spacing: 9) {
194
+ Image(systemName: icon).font(.system(size: 12, weight: .medium)).frame(width: 16)
195
+ Text(label).font(.system(.subheadline, design: .rounded)).lineLimit(1)
196
+ Spacer(minLength: 4)
197
+ if accentDot {
198
+ Circle().fill(Theme.recordRed).frame(width: 7, height: 7)
199
+ } else if let count {
200
+ Text("\(count)").font(.caption2.monospacedDigit()).foregroundStyle(.white.opacity(0.55))
201
+ }
202
+ }
203
+ .foregroundStyle(.white.opacity(selected ? 1 : 0.85))
204
+ .padding(.horizontal, 9).padding(.vertical, 7)
205
+ .contentShape(Rectangle())
206
+ .background {
207
+ if selected {
208
+ RoundedRectangle(cornerRadius: Theme.cornerSmall, style: .continuous).fill(.white.opacity(0.22))
209
+ }
210
+ }
211
+ }
212
+ }
@@ -0,0 +1,87 @@
1
+ import RecordingsLib
2
+ import SwiftUI
3
+
4
+ /// Design tokens and Liquid Glass helpers for the Recordings app. Mirrors the Hasna Notes
5
+ /// visual system (narrow colored Liquid-Glass sidebar + one continuous canvas, hairline
6
+ /// dividers, rounded type) with a Recordings identity: an "infinity violet→indigo" sidebar.
7
+ enum Theme {
8
+ /// Accent used for selection highlights and small affordances.
9
+ static let accent = Color(red: 0.42, green: 0.34, blue: 0.92)
10
+ static let recordRed = Color(red: 0.92, green: 0.26, blue: 0.30)
11
+
12
+ static let cornerLarge: CGFloat = 22
13
+ static let cornerMedium: CGFloat = 14
14
+ static let cornerSmall: CGFloat = 9
15
+
16
+ /// Deliberately narrow, Apple-style sidebar.
17
+ static let sidebarWidth: CGFloat = 204
18
+
19
+ /// Continuous main canvas color: pure white in light mode, system window background in dark.
20
+ static func canvas(_ scheme: ColorScheme) -> Color {
21
+ scheme == .dark ? Color(NSColor.windowBackgroundColor) : .white
22
+ }
23
+
24
+ /// "Infinity violet" sidebar gradient rendered behind the Liquid Glass rows so the glass
25
+ /// refracts the color. Darkened in dark mode so white text stays legible.
26
+ static func sidebarGradient(_ scheme: ColorScheme) -> LinearGradient {
27
+ let colors: [Color] = scheme == .dark
28
+ ? [Color(red: 0.16, green: 0.09, blue: 0.34),
29
+ Color(red: 0.20, green: 0.12, blue: 0.44),
30
+ Color(red: 0.28, green: 0.12, blue: 0.46)]
31
+ : [Color(red: 0.30, green: 0.18, blue: 0.66),
32
+ Color(red: 0.28, green: 0.20, blue: 0.76),
33
+ Color(red: 0.44, green: 0.18, blue: 0.70)]
34
+ return LinearGradient(colors: colors, startPoint: .top, endPoint: .bottom)
35
+ }
36
+ }
37
+
38
+ extension View {
39
+ /// Liquid Glass surface on macOS 26, honoring reduce-transparency. Used sparingly —
40
+ /// chiefly the sidebar rows and the record hero — never as boxed panels in the canvas.
41
+ @ViewBuilder
42
+ func glassSurface(cornerRadius: CGFloat, tint: Color? = nil, interactive: Bool = false) -> some View {
43
+ modifier(GlassSurface(cornerRadius: cornerRadius, tint: tint, interactive: interactive))
44
+ }
45
+ }
46
+
47
+ private struct GlassSurface: ViewModifier {
48
+ let cornerRadius: CGFloat
49
+ let tint: Color?
50
+ let interactive: Bool
51
+ @Environment(\.accessibilityReduceTransparency) private var reduceTransparency
52
+
53
+ @ViewBuilder
54
+ func body(content: Content) -> some View {
55
+ switch ChromeSurface.forReducedTransparency(reduceTransparency) {
56
+ case .opaque:
57
+ // Reduce Transparency: opaque system background, never a translucent material.
58
+ content
59
+ .background(
60
+ Color(NSColor.windowBackgroundColor),
61
+ in: RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
62
+ )
63
+ .overlay(
64
+ RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
65
+ .strokeBorder(.separator, lineWidth: 1)
66
+ )
67
+ case .liquidGlass:
68
+ content.glassEffect(makeGlass(), in: .rect(cornerRadius: cornerRadius))
69
+ }
70
+ }
71
+
72
+ private func makeGlass() -> Glass {
73
+ var glass: Glass = .regular
74
+ if let tint { glass = glass.tint(tint) }
75
+ if interactive { glass = glass.interactive() }
76
+ return glass
77
+ }
78
+ }
79
+
80
+ extension Date {
81
+ /// Compact relative description for list rows, e.g. "2h ago".
82
+ var relativeDescription: String {
83
+ let f = RelativeDateTimeFormatter()
84
+ f.unitsStyle = .abbreviated
85
+ return f.localizedString(for: self, relativeTo: Date())
86
+ }
87
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "originHash" : "a63dc0aa4ab0839e19cd7644cfd2bb42729aafbf9ef920adeb5c3ac856b7701e",
2
+ "originHash" : "687b5e4737064bbc8cae591b1e3177f27923cff756de7a7c0dd0b5bfb13ab55a",
3
3
  "pins" : [
4
4
  {
5
5
  "identity" : "keyboardshortcuts",
@@ -15,8 +15,8 @@
15
15
  "kind" : "remoteSourceControl",
16
16
  "location" : "https://github.com/swiftlang/swift-syntax.git",
17
17
  "state" : {
18
- "revision" : "0687f71944021d616d34d922343dcef086855920",
19
- "version" : "600.0.1"
18
+ "revision" : "4799286537280063c85a32f09884cfbca301b1a1",
19
+ "version" : "602.0.0"
20
20
  }
21
21
  },
22
22
  {
@@ -24,8 +24,8 @@
24
24
  "kind" : "remoteSourceControl",
25
25
  "location" : "https://github.com/apple/swift-testing.git",
26
26
  "state" : {
27
- "revision" : "399f76dcd91e4c688ca2301fa24a8cc6d9927211",
28
- "version" : "0.99.0"
27
+ "branch" : "swift-6.2-RELEASE",
28
+ "revision" : "3fdabe5392108d874abae1c1e58e1328ab46f681"
29
29
  }
30
30
  }
31
31
  ],
@@ -7,16 +7,31 @@ let package = Package(
7
7
  platforms: [
8
8
  .macOS(.v26)
9
9
  ],
10
+ products: [
11
+ .executable(name: "App", targets: ["App"]),
12
+ .library(name: "RecordingsUpdateProtocol", targets: ["RecordingsUpdateProtocol"]),
13
+ .executable(name: "recordings-update-broker", targets: ["RecordingsUpdateBroker"]),
14
+ .executable(name: "recordings-update-client", targets: ["RecordingsUpdateClient"]),
15
+ .executable(name: "recordings-envelope-signer", targets: ["RecordingsEnvelopeSigner"]),
16
+ .executable(
17
+ name: "recordings-bootstrap-preflight",
18
+ targets: ["RecordingsBootstrapPreflight"]
19
+ ),
20
+ ],
10
21
  dependencies: [
11
22
  .package(url: "https://github.com/sindresorhus/KeyboardShortcuts", exact: "1.12.0"),
12
- .package(url: "https://github.com/apple/swift-testing.git", .upToNextMinor(from: "0.99.0")),
23
+ // Swift Testing pinned to the toolchain-matched release. The stations build with
24
+ // CommandLineTools (no bundled Testing module), so the package is required — but it
25
+ // must track the compiler: the archived 0.99 shim under the 6.2 compiler silently
26
+ // corrupted `#expect` Bool-comparison evaluation, passing false assertions.
27
+ .package(url: "https://github.com/apple/swift-testing.git", revision: "swift-6.2-RELEASE"),
13
28
  ],
14
29
  targets: [
15
30
  .target(
16
31
  name: "RecordingsLib",
17
32
  dependencies: ["KeyboardShortcuts"],
18
33
  path: "RecordingsLib",
19
- exclude: ["Info.plist", "Recordings.entitlements"],
34
+ exclude: ["Info.plist", "Recordings.entitlements", "RecordingsCLI.entitlements"],
20
35
  resources: [.process("Resources")]
21
36
  ),
22
37
  .executableTarget(
@@ -24,10 +39,55 @@ let package = Package(
24
39
  dependencies: ["RecordingsLib"],
25
40
  path: "App"
26
41
  ),
42
+ .target(
43
+ name: "RecordingsUpdateProtocol",
44
+ path: "Updater/Protocol"
45
+ ),
46
+ .executableTarget(
47
+ name: "RecordingsUpdateBroker",
48
+ dependencies: ["RecordingsUpdateProtocol", "RecordingsVerifierLauncher"],
49
+ path: "Updater/Broker"
50
+ ),
51
+ .target(
52
+ name: "RecordingsVerifierLauncher",
53
+ path: "Updater/VerifierLauncher",
54
+ publicHeadersPath: "include"
55
+ ),
56
+ .executableTarget(
57
+ name: "RecordingsUpdateClient",
58
+ dependencies: ["RecordingsUpdateProtocol"],
59
+ path: "Updater/Client"
60
+ ),
61
+ .executableTarget(
62
+ name: "RecordingsEnvelopeSigner",
63
+ dependencies: ["RecordingsUpdateProtocol"],
64
+ path: "Updater/Signer"
65
+ ),
66
+ .executableTarget(
67
+ name: "RecordingsBootstrapPreflight",
68
+ dependencies: ["RecordingsUpdateProtocol"],
69
+ path: "Updater/BootstrapPreflight"
70
+ ),
27
71
  .testTarget(
28
72
  name: "RecordingsTests",
29
73
  dependencies: ["RecordingsLib", .product(name: "Testing", package: "swift-testing")],
30
74
  path: "RecordingsTests"
31
75
  ),
76
+ .testTarget(
77
+ name: "RecordingsUpdateProtocolTests",
78
+ dependencies: [
79
+ "RecordingsUpdateProtocol",
80
+ .product(name: "Testing", package: "swift-testing"),
81
+ ],
82
+ path: "Updater/ProtocolTests"
83
+ ),
84
+ .testTarget(
85
+ name: "RecordingsUpdateBrokerTests",
86
+ dependencies: [
87
+ "RecordingsUpdateBroker",
88
+ .product(name: "Testing", package: "swift-testing"),
89
+ ],
90
+ path: "Updater/BrokerTests"
91
+ ),
32
92
  ]
33
93
  )
@@ -0,0 +1,104 @@
1
+ import Foundation
2
+ @preconcurrency import ApplicationServices
3
+
4
+ public struct AccessibilityTrustResult: Sendable, Equatable {
5
+ public let trusted: Bool
6
+ public let didPrompt: Bool
7
+ }
8
+
9
+ /// Limits automatic Accessibility prompting to one attempt for the lifetime of a process.
10
+ /// Explicit user actions may request again, while ordinary trust checks never prompt.
11
+ public final class AccessibilityPromptGate: @unchecked Sendable {
12
+ public static let processShared = AccessibilityPromptGate()
13
+
14
+ private let lock = NSLock()
15
+ private var automaticPromptAttempted = false
16
+ private var promptRequests = 0
17
+
18
+ public init() {}
19
+
20
+ public var promptRequestCount: Int {
21
+ lock.withLock { promptRequests }
22
+ }
23
+
24
+ public func trustForProtectedOperation() -> AccessibilityTrustResult {
25
+ trustForProtectedOperation(
26
+ isTrusted: { AXIsProcessTrusted() },
27
+ requestPrompt: { AccessibilityPromptGate.requestSystemPrompt() }
28
+ )
29
+ }
30
+
31
+ public func trustForProtectedOperation(
32
+ isTrusted: () -> Bool,
33
+ requestPrompt: () -> Bool
34
+ ) -> AccessibilityTrustResult {
35
+ if isTrusted() {
36
+ return AccessibilityTrustResult(trusted: true, didPrompt: false)
37
+ }
38
+
39
+ let shouldPrompt = lock.withLock {
40
+ guard !automaticPromptAttempted else { return false }
41
+ automaticPromptAttempted = true
42
+ promptRequests += 1
43
+ return true
44
+ }
45
+ guard shouldPrompt else {
46
+ return AccessibilityTrustResult(trusted: false, didPrompt: false)
47
+ }
48
+ return AccessibilityTrustResult(trusted: requestPrompt(), didPrompt: true)
49
+ }
50
+
51
+ public func requestExplicitly() -> AccessibilityTrustResult {
52
+ requestExplicitly(
53
+ isTrusted: { AXIsProcessTrusted() },
54
+ requestPrompt: { AccessibilityPromptGate.requestSystemPrompt() }
55
+ )
56
+ }
57
+
58
+ public func requestExplicitly(_ requestPrompt: () -> Bool) -> AccessibilityTrustResult {
59
+ requestExplicitly(isTrusted: { AXIsProcessTrusted() }, requestPrompt: requestPrompt)
60
+ }
61
+
62
+ public func requestExplicitly(
63
+ isTrusted: () -> Bool,
64
+ requestPrompt: () -> Bool
65
+ ) -> AccessibilityTrustResult {
66
+ lock.withLock {
67
+ automaticPromptAttempted = true
68
+ promptRequests += 1
69
+ }
70
+ _ = requestPrompt()
71
+ return AccessibilityTrustResult(trusted: isTrusted(), didPrompt: true)
72
+ }
73
+
74
+ public func recheckExplicitRequest(
75
+ _ promptResult: AccessibilityTrustResult,
76
+ isTrusted: () -> Bool
77
+ ) -> AccessibilityTrustResult {
78
+ AccessibilityTrustResult(trusted: isTrusted(), didPrompt: promptResult.didPrompt)
79
+ }
80
+
81
+ public func waitForExplicitRequestCompletion(
82
+ _ promptResult: AccessibilityTrustResult,
83
+ attempts: Int = 20,
84
+ waitBetweenAttempts: () -> Void = { Thread.sleep(forTimeInterval: 0.25) },
85
+ isTrusted: () -> Bool = { AXIsProcessTrusted() }
86
+ ) -> AccessibilityTrustResult {
87
+ let boundedAttempts = max(attempts, 1)
88
+ for attempt in 0..<boundedAttempts {
89
+ if isTrusted() {
90
+ return AccessibilityTrustResult(trusted: true, didPrompt: promptResult.didPrompt)
91
+ }
92
+ if attempt + 1 < boundedAttempts {
93
+ waitBetweenAttempts()
94
+ }
95
+ }
96
+ return AccessibilityTrustResult(trusted: false, didPrompt: promptResult.didPrompt)
97
+ }
98
+
99
+ private static func requestSystemPrompt() -> Bool {
100
+ AXIsProcessTrustedWithOptions(
101
+ [kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true] as CFDictionary
102
+ )
103
+ }
104
+ }
@@ -16,35 +16,3 @@ public struct HasnaLogoMark: View {
16
16
  .clipShape(RoundedRectangle(cornerRadius: 4, style: .continuous))
17
17
  }
18
18
  }
19
-
20
- public struct HasnaMenuBarIcon: View {
21
- private let isRecording: Bool
22
- private let isTranscribing: Bool
23
-
24
- public init(isRecording: Bool, isTranscribing: Bool) {
25
- self.isRecording = isRecording
26
- self.isTranscribing = isTranscribing
27
- }
28
-
29
- public var body: some View {
30
- ZStack(alignment: .bottomTrailing) {
31
- Image("HasnaLogo", bundle: .module)
32
- .resizable()
33
- .renderingMode(.template)
34
- .interpolation(.high)
35
- .scaledToFit()
36
- .frame(width: 18, height: 18)
37
-
38
- if isRecording {
39
- Circle()
40
- .fill(.red)
41
- .frame(width: 6, height: 6)
42
- } else if isTranscribing {
43
- Circle()
44
- .fill(.tint)
45
- .frame(width: 6, height: 6)
46
- }
47
- }
48
- .frame(width: 22, height: 18)
49
- }
50
- }
@@ -0,0 +1,17 @@
1
+ import Foundation
2
+
3
+ /// Deterministic surface contract for glass-capable chrome (the record hero, glass panels).
4
+ /// When the user enables Reduce Transparency, chrome must render on an opaque, fully
5
+ /// readable surface — never a translucent material, which still composites content from
6
+ /// behind the window. Views resolve their surface through this single function so the
7
+ /// accessibility behavior is a testable state contract rather than per-view convention.
8
+ public enum ChromeSurface: Equatable, Sendable {
9
+ /// Liquid Glass — only while transparency effects are allowed.
10
+ case liquidGlass
11
+ /// Opaque window-background surface with a hairline border, honoring Reduce Transparency.
12
+ case opaque
13
+
14
+ public static func forReducedTransparency(_ reduceTransparency: Bool) -> ChromeSurface {
15
+ reduceTransparency ? .opaque : .liquidGlass
16
+ }
17
+ }
@@ -2,7 +2,11 @@
2
2
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
3
  <plist version="1.0">
4
4
  <dict>
5
- <key>LSUIElement</key>
5
+ <key>NSPrincipalClass</key>
6
+ <string>NSApplication</string>
7
+ <key>NSHighResolutionCapable</key>
8
+ <true/>
9
+ <key>NSSupportsAutomaticGraphicsSwitching</key>
6
10
  <true/>
7
11
  <key>CFBundleName</key>
8
12
  <string>Recordings</string>
@@ -13,11 +17,11 @@
13
17
  <key>CFBundleIdentifier</key>
14
18
  <string>com.hasna.recordings</string>
15
19
  <key>CFBundleVersion</key>
16
- <string>1</string>
20
+ <string>0.2.13</string>
17
21
  <key>CFBundlePackageType</key>
18
22
  <string>APPL</string>
19
23
  <key>CFBundleShortVersionString</key>
20
- <string>0.1.0</string>
24
+ <string>0.2.13</string>
21
25
  <key>LSMinimumSystemVersion</key>
22
26
  <string>26.0</string>
23
27
  <key>NSMicrophoneUsageDescription</key>
@@ -0,0 +1,37 @@
1
+ import Foundation
2
+
3
+ /// State contract for the menu-bar surface. The visual vocabulary is deliberately tiny and
4
+ /// unchanged (mic / waveform / ellipsis, one status line, one primary button); what this
5
+ /// type guarantees is truthfulness: whenever the engine would reject `startRecording` —
6
+ /// including while an intent decision, answer, or rewrite is pending — the menu bar must
7
+ /// present a busy state and a disabled Start affordance, never an idle state it would
8
+ /// then refuse.
9
+ public struct MenuBarPresentation: Equatable, Sendable {
10
+ public let iconName: String
11
+ public let accessibilityLabel: String
12
+ public let statusText: String
13
+ /// Whether the primary button is available. While recording the same button is the
14
+ /// (always enabled) Stop affordance; otherwise it is Start and must match
15
+ /// `RecordingEngine.canStartRecording` exactly.
16
+ public let primaryActionEnabled: Bool
17
+
18
+ public init(isRecording: Bool, canStartRecording: Bool, statusMessage: String) {
19
+ if isRecording {
20
+ iconName = "waveform"
21
+ accessibilityLabel = "Recordings, recording"
22
+ statusText = "Recording"
23
+ primaryActionEnabled = true
24
+ } else if !canStartRecording {
25
+ let normalizedBusyStatus = statusMessage.trimmingCharacters(in: .punctuationCharacters)
26
+ iconName = "ellipsis.circle"
27
+ accessibilityLabel = "Recordings, \(normalizedBusyStatus.lowercased())"
28
+ statusText = normalizedBusyStatus
29
+ primaryActionEnabled = false
30
+ } else {
31
+ iconName = "mic.fill"
32
+ accessibilityLabel = "Recordings"
33
+ statusText = statusMessage
34
+ primaryActionEnabled = true
35
+ }
36
+ }
37
+ }