@hasna/recordings 0.2.11 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +349 -69
  2. package/bun.lock +3 -0
  3. package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
  4. package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
  5. package/dist/cli/index.js +1728 -320
  6. package/dist/cli/macos-permissions.d.ts +13 -0
  7. package/dist/cli/macos-permissions.d.ts.map +1 -0
  8. package/dist/cli/options.d.ts +12 -0
  9. package/dist/cli/options.d.ts.map +1 -1
  10. package/dist/db/pg-migrations.d.ts.map +1 -1
  11. package/dist/db/recordings.d.ts +2 -1
  12. package/dist/db/recordings.d.ts.map +1 -1
  13. package/dist/db/remote-storage.d.ts +5 -1
  14. package/dist/db/remote-storage.d.ts.map +1 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +628 -147
  18. package/dist/lib/bun-runtime.d.ts +11 -0
  19. package/dist/lib/bun-runtime.d.ts.map +1 -0
  20. package/dist/lib/config.d.ts +8 -1
  21. package/dist/lib/config.d.ts.map +1 -1
  22. package/dist/lib/enhancer.d.ts +10 -2
  23. package/dist/lib/enhancer.d.ts.map +1 -1
  24. package/dist/lib/install-maintenance.d.ts +24 -0
  25. package/dist/lib/install-maintenance.d.ts.map +1 -0
  26. package/dist/lib/machine.d.ts +2 -0
  27. package/dist/lib/machine.d.ts.map +1 -0
  28. package/dist/lib/recorder.d.ts +2 -1
  29. package/dist/lib/recorder.d.ts.map +1 -1
  30. package/dist/lib/recording-create-identity.d.ts +13 -0
  31. package/dist/lib/recording-create-identity.d.ts.map +1 -0
  32. package/dist/lib/release-install-policy.d.ts +31 -0
  33. package/dist/lib/release-install-policy.d.ts.map +1 -0
  34. package/dist/lib/transcriber.d.ts.map +1 -1
  35. package/dist/mcp/index.d.ts.map +1 -1
  36. package/dist/mcp/index.js +977 -194
  37. package/dist/sdk/index.js +5 -1
  38. package/dist/sdk/v1.generated.d.ts +4 -0
  39. package/dist/sdk/v1.generated.d.ts.map +1 -1
  40. package/dist/server/cloud-config.d.ts +9 -0
  41. package/dist/server/cloud-config.d.ts.map +1 -0
  42. package/dist/server/cloud-readiness.d.ts +10 -0
  43. package/dist/server/cloud-readiness.d.ts.map +1 -0
  44. package/dist/server/cloud.d.ts +9 -18
  45. package/dist/server/cloud.d.ts.map +1 -1
  46. package/dist/server/index.js +1654 -304
  47. package/dist/server/migrate-command.d.ts +11 -0
  48. package/dist/server/migrate-command.d.ts.map +1 -0
  49. package/dist/server/openapi.d.ts +22 -0
  50. package/dist/server/openapi.d.ts.map +1 -1
  51. package/dist/server/repo.d.ts +8 -1
  52. package/dist/server/repo.d.ts.map +1 -1
  53. package/dist/server/serve.d.ts +6 -1
  54. package/dist/server/serve.d.ts.map +1 -1
  55. package/dist/server/v1.d.ts.map +1 -1
  56. package/dist/storage.js +515 -80
  57. package/dist/store.d.ts +3 -1
  58. package/dist/store.d.ts.map +1 -1
  59. package/dist/types/index.d.ts +10 -0
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/dist/version.d.ts +1 -1
  62. package/package.json +7 -4
  63. package/packaging/macos/Empty.entitlements +5 -0
  64. package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
  65. package/packaging/macos/Verifier.entitlements +10 -0
  66. package/packaging/macos/artifact-verifier.sb +33 -0
  67. package/packaging/macos/build_release_pkg.sh +872 -0
  68. package/packaging/macos/managed_bootstrap.sh +623 -0
  69. package/packaging/macos/pkgutil_fingerprint.awk +37 -0
  70. package/packaging/macos/release_lifecycle.ts +463 -0
  71. package/packaging/macos/scripts/postinstall +565 -0
  72. package/packaging/macos/scripts/preinstall +297 -0
  73. package/scripts/build_companion_cli.sh +337 -0
  74. package/scripts/build_native_fs_guard.sh +59 -0
  75. package/scripts/generate-sdk.ts +19 -1
  76. package/scripts/install_macos_app.sh +1919 -98
  77. package/scripts/macos_artifact.ts +5316 -0
  78. package/scripts/migrate.ts +38 -10
  79. package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
  80. package/scripts/native/recordings_fs_guard.c +1167 -0
  81. package/scripts/native_fs_guard.ts +158 -0
  82. package/scripts/release-guard.ts +20 -1
  83. package/scripts/resolve_tailscale_cli.sh +389 -0
  84. package/scripts/smoke_macos_app.sh +573 -0
  85. package/src/native/Recordings/App/ContentView.swift +83 -0
  86. package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
  87. package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
  88. package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
  89. package/src/native/Recordings/App/RecordingsApp.swift +367 -44
  90. package/src/native/Recordings/App/RecordingsListView.swift +130 -0
  91. package/src/native/Recordings/App/RecordingsStore.swift +206 -0
  92. package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
  93. package/src/native/Recordings/App/SidebarView.swift +212 -0
  94. package/src/native/Recordings/App/Theme.swift +87 -0
  95. package/src/native/Recordings/Package.resolved +5 -5
  96. package/src/native/Recordings/Package.swift +62 -2
  97. package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
  98. package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
  99. package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
  100. package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
  101. package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
  102. package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
  103. package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
  104. package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
  105. package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
  106. package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
  107. package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
  108. package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
  109. package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
  110. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
  111. package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
  112. package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
  113. package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
  114. package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
  115. package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
  116. package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
  117. package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
  118. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
  119. package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
  120. package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
  121. package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
  122. package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
  123. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
  124. package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
  125. package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
  126. package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
  127. package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
  128. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
  129. package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
  130. package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
  131. package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
  132. package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
  133. package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
  134. package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
  135. package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
  136. package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
  137. package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
  138. package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
  139. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
  140. package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
  141. package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
  142. package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
  143. package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
  144. package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
  145. package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
  146. package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
  147. package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
  148. package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
  149. package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
  150. package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
  151. package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
  152. package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
  153. package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
  154. package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
  155. package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
  156. package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
  157. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
  158. package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
  159. package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
  160. package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
  161. package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
  162. package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
  163. package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
  164. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
  165. package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
  166. package/src/native/Recordings/build.sh +1973 -236
  167. package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
@@ -0,0 +1,29 @@
1
+ /// Shared presentation contract for every control that starts a new recording. Keeping the
2
+ /// labels and enabled state together prevents a ready or error surface from offering an action
3
+ /// that `RecordingEngine` would reject while clipboard delivery is still settling.
4
+ public struct RecordingStartControlPresentation: Equatable, Sendable {
5
+ public enum Kind: CaseIterable, Sendable {
6
+ case record
7
+ case recordAgain
8
+ case tryAgain
9
+ }
10
+
11
+ public let title: String
12
+ public let accessibilityLabel: String
13
+ public let isEnabled: Bool
14
+
15
+ public init(kind: Kind, canStartRecording: Bool) {
16
+ switch kind {
17
+ case .record:
18
+ title = "Record"
19
+ accessibilityLabel = "Start recording"
20
+ case .recordAgain:
21
+ title = "Record Again"
22
+ accessibilityLabel = "Start a new recording"
23
+ case .tryAgain:
24
+ title = "Try Again"
25
+ accessibilityLabel = "Try recording again"
26
+ }
27
+ isEnabled = canStartRecording
28
+ }
29
+ }
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>com.apple.security.cs.allow-jit</key>
6
+ <true/>
7
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
8
+ <true/>
9
+ </dict>
10
+ </plist>
@@ -0,0 +1,222 @@
1
+ import Foundation
2
+
3
+ /// Swift bridge over the `recordings` CLI. The macOS app uses this for the library,
4
+ /// search, stats, and active Store — reusing the exact same local/HTTP
5
+ /// store the CLI and MCP server write, instead of re-implementing persistence in Swift.
6
+ public enum RecordingsCLI {
7
+ public struct Failure: Error, CustomStringConvertible {
8
+ public let message: String
9
+ public var description: String { message }
10
+ }
11
+
12
+ public static var defaultHome: String {
13
+ FileManager.default.homeDirectoryForCurrentUser.path
14
+ }
15
+
16
+ public struct PackagedCompanionProbe: Sendable, Equatable {
17
+ public let executablePath: String
18
+ public let version: String
19
+
20
+ public init(executablePath: String, version: String) {
21
+ self.executablePath = executablePath
22
+ self.version = version
23
+ }
24
+ }
25
+
26
+ /// Exercise the resolver and the read/write capabilities the installed app depends on.
27
+ /// A packaged app must resolve only its embedded helper; PATH fallbacks are rejected.
28
+ public static func probePackagedCompanion(home: String) throws -> PackagedCompanionProbe {
29
+ let command = CLIRunner.resolveCommand(home: home)
30
+ let expected = Bundle.main.bundleURL.appendingPathComponent("Contents/Helpers/recordings").path
31
+ guard Bundle.main.bundleURL.pathExtension.caseInsensitiveCompare("app") == .orderedSame,
32
+ command.executable == expected,
33
+ command.argumentsPrefix.isEmpty else {
34
+ throw Failure(message: "Packaged companion resolver did not select the embedded helper")
35
+ }
36
+ let versionOutput = CLIRunner.run(["--version"], home: home)
37
+ .trimmingCharacters(in: .whitespacesAndNewlines)
38
+ if let error = CLIRunner.parseError(versionOutput) { throw Failure(message: error) }
39
+ let expectedVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
40
+ guard !versionOutput.isEmpty, versionOutput == expectedVersion else {
41
+ throw Failure(message: "Packaged companion version does not match the app")
42
+ }
43
+ let project = try registerProject(
44
+ name: "Activated Helper Contract",
45
+ path: "recordings-app://install/activated-helper-contract",
46
+ description: "Installed app resolver verification",
47
+ home: home
48
+ )
49
+ guard project.name == "Activated Helper Contract" else {
50
+ throw Failure(message: "Packaged companion project capability failed")
51
+ }
52
+ let saved = CLIRunner.run(
53
+ ["--json", "save-text", "Activated helper contract", "--source", "native_install_contract", "--post-processing", "off"],
54
+ home: home
55
+ )
56
+ struct SavedRecording: Decodable { let raw_text: String }
57
+ let recording = try decode(SavedRecording.self, from: saved)
58
+ guard recording.raw_text == "Activated helper contract" else {
59
+ throw Failure(message: "Packaged companion recording capability failed")
60
+ }
61
+ return PackagedCompanionProbe(executablePath: command.executable, version: versionOutput)
62
+ }
63
+
64
+ // MARK: - Library
65
+
66
+ public static func list(limit: Int = 100, offset: Int = 0, projectId: String? = nil, home: String = defaultHome) throws -> [Recording] {
67
+ var args = ["--json"]
68
+ if let projectId, !projectId.isEmpty { args += ["--project", projectId] }
69
+ args += ["list", "-n", String(limit), "--offset", String(offset)]
70
+ return try runDecoding([Recording].self, args, home: home)
71
+ }
72
+
73
+ /// Load the complete library in pages. The HTTP store caps each request at 500,
74
+ /// while the local SQLite store accepts the same offset/limit contract.
75
+ public static func listAll(pageSize: Int = 500, home: String = defaultHome) throws -> [Recording] {
76
+ try collectAllPages(pageSize: pageSize) { limit, offset in
77
+ try list(limit: limit, offset: offset, home: home)
78
+ }
79
+ }
80
+
81
+ static func collectAllPages(
82
+ pageSize: Int = 500,
83
+ loadPage: (_ limit: Int, _ offset: Int) throws -> [Recording]
84
+ ) rethrows -> [Recording] {
85
+ let size = max(1, min(pageSize, 500))
86
+ var recordings: [Recording] = []
87
+ var seen = Set<String>()
88
+ var offset = 0
89
+
90
+ while true {
91
+ let page = try loadPage(size, offset)
92
+ let unseen = page.filter { seen.insert($0.id).inserted }
93
+ recordings.append(contentsOf: unseen)
94
+
95
+ guard page.count == size, !unseen.isEmpty else { break }
96
+ offset += page.count
97
+ }
98
+
99
+ return recordings
100
+ }
101
+
102
+ public static func show(id: String, home: String = defaultHome) throws -> Recording {
103
+ try runDecoding(Recording.self, ["--json", "show", id], home: home)
104
+ }
105
+
106
+ public static func search(_ query: String, limit: Int = 100, home: String = defaultHome) throws -> [Recording] {
107
+ let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines)
108
+ guard !trimmed.isEmpty else { return try list(limit: limit, home: home) }
109
+ return try runDecoding([Recording].self, ["--json", "search", trimmed, "-n", String(limit)], home: home)
110
+ }
111
+
112
+ public static func stats(home: String = defaultHome) throws -> RecordingStats {
113
+ try runDecoding(RecordingStats.self, ["--json", "stats"], home: home)
114
+ }
115
+
116
+ public static func delete(id: String, home: String = defaultHome) throws {
117
+ let output = CLIRunner.run(["delete", id], home: home)
118
+ if let err = CLIRunner.parseError(output) { throw Failure(message: err) }
119
+ }
120
+
121
+ public struct CanonicalProject: Codable, Sendable {
122
+ public let id: String
123
+ public let name: String
124
+ public let path: String
125
+ }
126
+
127
+ public static func registerProject(
128
+ name: String,
129
+ path: String,
130
+ description: String = "Recordings macOS project",
131
+ home: String = defaultHome
132
+ ) throws -> CanonicalProject {
133
+ try runDecoding(
134
+ CanonicalProject.self,
135
+ ["--json", "project", "register", "--name", name, "--path", path, "--description", description],
136
+ home: home
137
+ )
138
+ }
139
+
140
+ // MARK: - Internals
141
+
142
+ private static func runDecoding<T: Decodable>(_ type: T.Type, _ args: [String], home: String) throws -> T {
143
+ let output = CLIRunner.run(args, home: home)
144
+ if let err = CLIRunner.parseError(output) { throw Failure(message: err) }
145
+ return try decode(type, from: output)
146
+ }
147
+
148
+ static func decode<T: Decodable>(_ type: T.Type, from output: String) throws -> T {
149
+ let candidates = Self.jsonCandidates(from: output)
150
+ guard !candidates.isEmpty else {
151
+ throw Failure(message: "No JSON in CLI output")
152
+ }
153
+ var lastError: Error?
154
+ for json in candidates.reversed() {
155
+ guard let data = json.data(using: .utf8) else { continue }
156
+ do {
157
+ return try JSONDecoder().decode(T.self, from: data)
158
+ } catch {
159
+ lastError = error
160
+ }
161
+ }
162
+ throw Failure(message: "Failed to decode \(T.self): \(lastError?.localizedDescription ?? "invalid JSON")")
163
+ }
164
+
165
+ /// Find a balanced, valid top-level JSON array or object while ignoring bracketed logs.
166
+ static func extractJSON(from output: String) -> String? {
167
+ jsonCandidates(from: output).last
168
+ }
169
+
170
+ static func jsonCandidates(from output: String) -> [String] {
171
+ let trimmed = output.trimmingCharacters(in: .whitespacesAndNewlines)
172
+ guard !trimmed.isEmpty else { return [] }
173
+ var candidates: [String] = []
174
+ var cursor = trimmed.startIndex
175
+
176
+ while cursor < trimmed.endIndex {
177
+ guard let start = trimmed[cursor...].firstIndex(where: { $0 == "[" || $0 == "{" }) else { break }
178
+ var stack: [Character] = []
179
+ var inString = false
180
+ var escaped = false
181
+ var index = start
182
+ var candidateEnd: String.Index?
183
+
184
+ scan: while index < trimmed.endIndex {
185
+ let character = trimmed[index]
186
+ if inString {
187
+ if escaped { escaped = false }
188
+ else if character == "\\" { escaped = true }
189
+ else if character == "\"" { inString = false }
190
+ } else {
191
+ switch character {
192
+ case "\"": inString = true
193
+ case "[": stack.append("]")
194
+ case "{": stack.append("}")
195
+ case "]", "}":
196
+ guard stack.last == character else { break scan }
197
+ stack.removeLast()
198
+ if stack.isEmpty {
199
+ candidateEnd = index
200
+ break scan
201
+ }
202
+ default: break
203
+ }
204
+ }
205
+ index = trimmed.index(after: index)
206
+ }
207
+
208
+ if let end = candidateEnd {
209
+ let candidate = String(trimmed[start...end])
210
+ if let data = candidate.data(using: .utf8),
211
+ let value = try? JSONSerialization.jsonObject(with: data),
212
+ value is [Any] || value is [String: Any] {
213
+ candidates.append(candidate)
214
+ }
215
+ cursor = trimmed.index(after: end)
216
+ } else {
217
+ cursor = trimmed.index(after: start)
218
+ }
219
+ }
220
+ return candidates
221
+ }
222
+ }
@@ -19,7 +19,15 @@ public struct SettingsView: View {
19
19
  projectsTab.tabItem { Label("Projects", systemImage: "folder") }
20
20
  shortcutsTab.tabItem { Label("Voice Shortcuts", systemImage: "text.badge.star") }
21
21
  }
22
- .frame(width: 520, height: 440)
22
+ .frame(width: 520, height: 500)
23
+ .alert("Project Settings Error", isPresented: Binding(
24
+ get: { projectStore.persistenceError != nil },
25
+ set: { if !$0 { projectStore.clearPersistenceError() } }
26
+ )) {
27
+ Button("OK", role: .cancel) {}
28
+ } message: {
29
+ Text(projectStore.persistenceError ?? "The project settings could not be saved.")
30
+ }
23
31
  }
24
32
 
25
33
  // MARK: - General
@@ -41,6 +49,12 @@ public struct SettingsView: View {
41
49
  .foregroundStyle(.secondary)
42
50
  }
43
51
 
52
+ Section("Intent") {
53
+ Toggle("Detect questions and edit commands", isOn: $engine.intentDetectionEnabled)
54
+ Text("When on, Recordings infers whether you dictated, asked a question, or asked it to edit the selected text. Anything uncertain is typed out literally. When off, every recording is typed out.")
55
+ .foregroundStyle(.secondary)
56
+ }
57
+
44
58
  Section("Recording Shortcut") {
45
59
  HStack {
46
60
  Text("Shortcut")
@@ -84,15 +98,25 @@ public struct SettingsView: View {
84
98
  }
85
99
  }
86
100
 
87
- Section("System Prompt") {
101
+ Section("Transcription Cleanup") {
102
+ Picker("Mode", selection: $projectStore.settings.postProcessingMode) {
103
+ ForEach(PostProcessingMode.allCases) { mode in
104
+ Text(mode.label).tag(mode.rawValue)
105
+ }
106
+ }
107
+ .pickerStyle(.segmented)
108
+ .onChange(of: projectStore.settings.postProcessingMode) {
109
+ try? projectStore.save()
110
+ }
88
111
  TextEditor(text: $projectStore.settings.globalSystemPrompt)
89
112
  .frame(height: 80)
90
113
  .onChange(of: projectStore.settings.globalSystemPrompt) {
91
- projectStore.save()
114
+ try? projectStore.save()
92
115
  }
93
- Text("Applied to all transcription enhancements.")
116
+ Text("Instructions for post-transcription cleanup and formatting.")
94
117
  .foregroundStyle(.secondary)
95
118
  }
119
+ .disabled(!projectStore.canMutateProjects)
96
120
  }
97
121
  .formStyle(.grouped).padding()
98
122
  }
@@ -109,8 +133,13 @@ public struct SettingsView: View {
109
133
  .textFieldStyle(.roundedBorder)
110
134
  Button("Add") {
111
135
  guard !newProjectName.isEmpty else { return }
112
- projectStore.addProject(name: newProjectName)
113
- newProjectName = ""
136
+ let name = newProjectName
137
+ Task {
138
+ do {
139
+ try await projectStore.addProject(name: name)
140
+ newProjectName = ""
141
+ } catch {}
142
+ }
114
143
  }
115
144
  .disabled(newProjectName.isEmpty)
116
145
  }
@@ -134,12 +163,13 @@ public struct SettingsView: View {
134
163
  }
135
164
  .onDelete { indexSet in
136
165
  for i in indexSet {
137
- projectStore.removeProject(id: projectStore.settings.projects[i].id)
166
+ try? projectStore.removeProject(id: projectStore.settings.projects[i].id)
138
167
  }
139
168
  }
140
169
  }
141
170
  }
142
171
  }
172
+ .disabled(!projectStore.canMutateProjects)
143
173
  .sheet(item: $editingProject) { project in
144
174
  ProjectEditView(project: project, store: projectStore) {
145
175
  editingProject = nil
@@ -230,25 +260,29 @@ struct ProjectEditView: View {
230
260
  .textFieldStyle(.roundedBorder)
231
261
  }
232
262
 
233
- Section("System Prompt") {
263
+ Section("Transcriber Instructions") {
234
264
  TextEditor(text: Binding(
235
265
  get: { project.systemPrompt ?? "" },
236
266
  set: { project.systemPrompt = $0.isEmpty ? nil : $0 }
237
267
  ))
238
268
  .frame(height: 100)
239
- Text("Additional context for transcription enhancement in this project.")
269
+ Text("Project-specific cleanup and formatting instructions.")
240
270
  .foregroundStyle(.secondary)
241
271
  }
242
272
  }
243
273
  .formStyle(.grouped)
274
+ .disabled(!store.canMutateProjects)
244
275
 
245
276
  HStack {
246
277
  Spacer()
247
278
  Button("Cancel") { onDismiss() }
248
279
  Button("Save") {
249
- store.updateProject(project)
250
- onDismiss()
280
+ do {
281
+ try store.updateProject(project)
282
+ onDismiss()
283
+ } catch {}
251
284
  }
285
+ .disabled(!store.canMutateProjects)
252
286
  }
253
287
  .padding()
254
288
  }