@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,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
|
+
}
|