@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,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:
|
|
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("
|
|
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("
|
|
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
|
-
|
|
113
|
-
|
|
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("
|
|
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("
|
|
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
|
-
|
|
250
|
-
|
|
280
|
+
do {
|
|
281
|
+
try store.updateProject(project)
|
|
282
|
+
onDismiss()
|
|
283
|
+
} catch {}
|
|
251
284
|
}
|
|
285
|
+
.disabled(!store.canMutateProjects)
|
|
252
286
|
}
|
|
253
287
|
.padding()
|
|
254
288
|
}
|