@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.
- package/README.md +350 -35
- package/bun.lock +3 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
- package/dist/cli/index.js +1728 -320
- package/dist/cli/macos-permissions.d.ts +13 -0
- package/dist/cli/macos-permissions.d.ts.map +1 -0
- package/dist/cli/options.d.ts +12 -0
- package/dist/cli/options.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/recordings.d.ts +2 -1
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/db/remote-storage.d.ts +5 -1
- package/dist/db/remote-storage.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +628 -147
- package/dist/lib/bun-runtime.d.ts +11 -0
- package/dist/lib/bun-runtime.d.ts.map +1 -0
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/enhancer.d.ts +10 -2
- package/dist/lib/enhancer.d.ts.map +1 -1
- package/dist/lib/install-maintenance.d.ts +24 -0
- package/dist/lib/install-maintenance.d.ts.map +1 -0
- package/dist/lib/machine.d.ts +2 -0
- package/dist/lib/machine.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts +2 -1
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/recording-create-identity.d.ts +13 -0
- package/dist/lib/recording-create-identity.d.ts.map +1 -0
- package/dist/lib/release-install-policy.d.ts +31 -0
- package/dist/lib/release-install-policy.d.ts.map +1 -0
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +977 -194
- package/dist/sdk/index.js +5 -1
- package/dist/sdk/v1.generated.d.ts +4 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud-config.d.ts +9 -0
- package/dist/server/cloud-config.d.ts.map +1 -0
- package/dist/server/cloud-readiness.d.ts +10 -0
- package/dist/server/cloud-readiness.d.ts.map +1 -0
- package/dist/server/cloud.d.ts +9 -18
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +1654 -304
- package/dist/server/migrate-command.d.ts +11 -0
- package/dist/server/migrate-command.d.ts.map +1 -0
- package/dist/server/openapi.d.ts +22 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/repo.d.ts +8 -1
- package/dist/server/repo.d.ts.map +1 -1
- package/dist/server/serve.d.ts +6 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage.js +515 -80
- package/dist/store.d.ts +3 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +7 -4
- package/packaging/macos/Empty.entitlements +5 -0
- package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
- package/packaging/macos/Verifier.entitlements +10 -0
- package/packaging/macos/artifact-verifier.sb +33 -0
- package/packaging/macos/build_release_pkg.sh +872 -0
- package/packaging/macos/managed_bootstrap.sh +623 -0
- package/packaging/macos/pkgutil_fingerprint.awk +37 -0
- package/packaging/macos/release_lifecycle.ts +463 -0
- package/packaging/macos/scripts/postinstall +565 -0
- package/packaging/macos/scripts/preinstall +297 -0
- package/scripts/build_companion_cli.sh +337 -0
- package/scripts/build_native_fs_guard.sh +59 -0
- package/scripts/generate-sdk.ts +19 -1
- package/scripts/install_macos_app.sh +1922 -77
- package/scripts/macos_artifact.ts +5316 -0
- package/scripts/migrate.ts +38 -10
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
- package/scripts/native/recordings_fs_guard.c +1167 -0
- package/scripts/native_fs_guard.ts +158 -0
- package/scripts/release-guard.ts +20 -1
- package/scripts/resolve_tailscale_cli.sh +389 -0
- package/scripts/smoke_macos_app.sh +573 -0
- package/src/native/Recordings/App/ContentView.swift +83 -0
- package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
- package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
- package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
- package/src/native/Recordings/App/RecordingsApp.swift +367 -44
- package/src/native/Recordings/App/RecordingsListView.swift +130 -0
- package/src/native/Recordings/App/RecordingsStore.swift +206 -0
- package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
- package/src/native/Recordings/App/SidebarView.swift +212 -0
- package/src/native/Recordings/App/Theme.swift +87 -0
- package/src/native/Recordings/Package.resolved +5 -5
- package/src/native/Recordings/Package.swift +62 -2
- package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
- package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
- package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
- package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
- package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
- package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
- package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
- package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
- package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
- package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
- package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
- package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
- package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
- package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
- package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
- package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
- package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
- package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
- package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
- package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
- package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
- package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
- package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
- package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
- package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
- package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
- package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
- package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
- package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
- package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
- package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
- package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
- package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
- package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
- package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
- package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
- package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
- package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
- package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
- package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
- package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
- package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
- package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
- package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
- package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
- package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
- package/src/native/Recordings/build.sh +1990 -29
- 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" : "
|
|
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" : "
|
|
19
|
-
"version" : "
|
|
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
|
-
"
|
|
28
|
-
"
|
|
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
|
-
|
|
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>
|
|
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>
|
|
20
|
+
<string>0.2.13</string>
|
|
17
21
|
<key>CFBundlePackageType</key>
|
|
18
22
|
<string>APPL</string>
|
|
19
23
|
<key>CFBundleShortVersionString</key>
|
|
20
|
-
<string>0.
|
|
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
|
+
}
|