@hasna/recordings 0.2.13 → 0.3.1
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/Dockerfile.package +4 -5
- package/README.md +113 -8
- package/bun.lock +2 -2
- package/dist/__tests__/helpers/installer-guard-execution.d.ts +22 -0
- package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +1 -0
- package/dist/__tests__/helpers/installer-preflight.d.ts +22 -0
- package/dist/__tests__/helpers/installer-preflight.d.ts.map +1 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -1
- package/dist/__tests__/helpers/source-assertions.d.ts +169 -0
- package/dist/__tests__/helpers/source-assertions.d.ts.map +1 -0
- package/dist/__tests__/preload.d.ts +1 -4
- package/dist/__tests__/preload.d.ts.map +1 -1
- package/dist/cli/desktop-snapshot.d.ts +20 -0
- package/dist/cli/desktop-snapshot.d.ts.map +1 -0
- package/dist/cli/index.js +2078 -277
- package/dist/cli/macos-permissions.d.ts +72 -0
- package/dist/cli/macos-permissions.d.ts.map +1 -1
- package/dist/cli/macos-shortcut.d.ts +139 -0
- package/dist/cli/macos-shortcut.d.ts.map +1 -0
- package/dist/cli/trigger-probe.d.ts +172 -0
- package/dist/cli/trigger-probe.d.ts.map +1 -0
- package/dist/db/database.d.ts +9 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/http/client.d.ts +10 -7
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +634 -49
- package/dist/lib/capture-probe.d.ts +216 -0
- package/dist/lib/capture-probe.d.ts.map +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/macos-bundle.d.ts +50 -0
- package/dist/lib/macos-bundle.d.ts.map +1 -0
- package/dist/lib/persistence-probe.d.ts +208 -0
- package/dist/lib/persistence-probe.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/retired-deployment-modes.d.ts +24 -0
- package/dist/lib/retired-deployment-modes.d.ts.map +1 -0
- package/dist/lib/transcriber.d.ts +60 -0
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.js +140 -56
- package/dist/server/cloud-config.d.ts +25 -3
- package/dist/server/cloud-config.d.ts.map +1 -1
- package/dist/server/cloud-readiness.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +8 -7
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +187 -79
- package/dist/server/migrate-command.d.ts.map +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +108 -49
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/package.json +25 -6
- package/scripts/build_native_fs_guard.sh +15 -2
- package/scripts/ci-linux-suite.ts +403 -0
- package/scripts/ci-native-build.ts +402 -0
- package/scripts/enforce_identity_migration.sh +110 -0
- package/scripts/generate-sdk.ts +1 -1
- package/scripts/install_macos_app.sh +138 -24
- package/scripts/macos_artifact.ts +205 -33
- package/scripts/native/realtime-settle-probe/README.md +39 -0
- package/scripts/native/realtime-settle-probe/main.swift +135 -0
- package/scripts/policy/local-only-approved-targets.txt +21 -0
- package/scripts/read_local_only_targets.sh +203 -0
- package/scripts/scan-artifact.ts +40 -0
- package/scripts/set-version.ts +145 -0
- package/scripts/vacuity-manifest-gen.ts +96 -0
- package/scripts/vacuity-manifests/enhancement-screen-mirror.tsv +44 -0
- package/scripts/vacuity-manifests/install-chain.tsv +13 -0
- package/scripts/vacuity-manifests/must-survive-controls.tsv +10 -0
- package/scripts/vacuity-manifests/reorder.tsv +4 -0
- package/scripts/vacuity-manifests/source-side.tsv +15 -0
- package/scripts/vacuity-manifests/variable-operands.tsv +16 -0
- package/scripts/vacuity-manifests/version-sites.tsv +61 -0
- package/scripts/vacuity-mutation-battery.ts +266 -0
- package/src/native/Recordings/App/MenuBarStatusView.swift +14 -7
- package/src/native/Recordings/App/RuntimeSmoke.swift +5 -1
- package/src/native/Recordings/App/SidebarView.swift +1 -1
- package/src/native/Recordings/Package.swift +24 -5
- package/src/native/Recordings/RecordingsLib/EnhancementScreen.swift +91 -0
- package/src/native/Recordings/RecordingsLib/FnKeyMonitor.swift +27 -4
- package/src/native/Recordings/RecordingsLib/Info.plist +2 -2
- package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +56 -3
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +13 -0
- package/src/native/Recordings/RecordingsLib/PasteDeliveryVerification.swift +460 -0
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +41 -7
- package/src/native/Recordings/RecordingsLib/RecordingAttemptAlert.swift +36 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +1071 -75
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +47 -3
- package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +11 -6
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +3 -3
- package/src/native/Recordings/RecordingsTests/EnhancementScreenTests.swift +191 -0
- package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +144 -4
- package/src/native/Recordings/RecordingsTests/PasteDeliveryVerificationTests.swift +463 -0
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +33 -20
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +1 -1
- package/src/native/Recordings/RecordingsTests/RealtimeSettleBenchmark.swift +200 -0
- package/src/native/Recordings/RecordingsTests/RealtimeSettleBudgetTests.swift +80 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +22 -4
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +12 -6
- package/src/native/Recordings/RecordingsTests/RecordingEngineHomeIsolationTests.swift +67 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +12 -3
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +207 -11
- package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +17 -6
- package/src/native/Recordings/RecordingsTests/TestHomeDirectory.swift +23 -0
- package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +7 -1
- package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +10 -10
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +5 -1
- package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +1 -0
- package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +59 -3
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +21 -2
- package/src/native/Recordings/Updater/ProtocolTests/ExpectationIntegrityTests.swift +60 -0
- package/src/native/Recordings/Updater/ProtocolTests/UpdateXPCInterfaceTests.swift +113 -0
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +106 -5
- package/src/native/Recordings/build.sh +75 -9
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
|
@@ -20,10 +20,24 @@ let package = Package(
|
|
|
20
20
|
],
|
|
21
21
|
dependencies: [
|
|
22
22
|
.package(url: "https://github.com/sindresorhus/KeyboardShortcuts", exact: "1.12.0"),
|
|
23
|
-
// Swift Testing
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// corrupted `#expect` Bool-comparison evaluation, passing false assertions.
|
|
23
|
+
// Swift Testing. The stations build with CommandLineTools (no bundled Testing
|
|
24
|
+
// module), so the package is required. The original rule here was "the pin must
|
|
25
|
+
// track the compiler", because an archived 0.99 shim under the 6.2 compiler once
|
|
26
|
+
// silently corrupted `#expect` Bool-comparison evaluation, passing false assertions.
|
|
27
|
+
//
|
|
28
|
+
// Measured on station06 (Apple Swift 6.3.2, swiftlang-6.3.2.1.108): the 6.3.x line
|
|
29
|
+
// CANNOT be used here. From swift-6.3-RELEASE onward swift-testing's own manifest adds
|
|
30
|
+
// `.linkedLibrary("_TestingInterop")` under `#if compiler(>=6.3)`. That library ships
|
|
31
|
+
// in the toolchain at `$(xcode-select -p)/Library/Developer/usr/lib`, which is not on
|
|
32
|
+
// SwiftPM's default search path, so `swift build --build-tests` fails with
|
|
33
|
+
// `ld: library '_TestingInterop' not found`. It links only if every invocation adds
|
|
34
|
+
// `-Xlinker -L$(xcode-select -p)/Library/Developer/usr/lib`, which nothing in this repo
|
|
35
|
+
// supplies. Repinning to 6.3.2 would therefore break `swift test` for every caller.
|
|
36
|
+
//
|
|
37
|
+
// So the pin stays on the 6.2 line, and the rule it was standing in for is now
|
|
38
|
+
// enforced directly instead of by version matching: ExpectationIntegrityTests asserts
|
|
39
|
+
// that `#expect` both admits true Bool comparisons and *records issues* for false ones.
|
|
40
|
+
// That turns the silent failure mode into a red test, which a version pin never could.
|
|
27
41
|
.package(url: "https://github.com/apple/swift-testing.git", revision: "swift-6.2-RELEASE"),
|
|
28
42
|
],
|
|
29
43
|
targets: [
|
|
@@ -46,7 +60,12 @@ let package = Package(
|
|
|
46
60
|
.executableTarget(
|
|
47
61
|
name: "RecordingsUpdateBroker",
|
|
48
62
|
dependencies: ["RecordingsUpdateProtocol", "RecordingsVerifierLauncher"],
|
|
49
|
-
path: "Updater/Broker"
|
|
63
|
+
path: "Updater/Broker",
|
|
64
|
+
// PeerIdentity resolves the XPC peer's audit token into a PID and EUID with
|
|
65
|
+
// audit_token_to_pid/_euid, which live in libbsm. Without this the broker
|
|
66
|
+
// compiles and then fails at link, which `swift build --target` does not
|
|
67
|
+
// surface because it stops short of linking the executable product.
|
|
68
|
+
linkerSettings: [.linkedLibrary("bsm")]
|
|
50
69
|
),
|
|
51
70
|
.target(
|
|
52
71
|
name: "RecordingsVerifierLauncher",
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Deterministic local pre-screen for the CLI's enhancement decision.
|
|
4
|
+
///
|
|
5
|
+
/// The helper CLI owns the real decision (`needsEnhancement` in `src/lib/enhancer.ts`):
|
|
6
|
+
/// in `auto` post-processing mode it rewrites transcripts that carry an explicit
|
|
7
|
+
/// configured trigger phrase or an instruction-shaped opening. Delivery may only run
|
|
8
|
+
/// ahead of persistence when that rewrite provably cannot happen — the user asked for
|
|
9
|
+
/// the rewritten text, so an eligible transcript must keep pasting the helper's output.
|
|
10
|
+
///
|
|
11
|
+
/// This screen therefore mirrors the CLI's detection *conservatively*: it answers
|
|
12
|
+
/// "may the helper enhance this?" and fails closed (true) whenever it cannot decide.
|
|
13
|
+
/// The trigger list is config-owned and arrives as the same frozen JSON array the
|
|
14
|
+
/// engine already forwards to the helper via `--enhance-triggers-json`, so both sides
|
|
15
|
+
/// read one source of truth for the configurable half. The instruction patterns mirror
|
|
16
|
+
/// the CLI's built-in regexes; `EnhancementScreenTests` and the `enhancer.test.ts`
|
|
17
|
+
/// "cross-language fixture" suite pin both implementations to one shared fixture table
|
|
18
|
+
/// so they cannot drift apart silently.
|
|
19
|
+
public enum EnhancementScreen {
|
|
20
|
+
/// Built-in instruction shapes, mirrored from `instructionPatterns` in
|
|
21
|
+
/// `src/lib/enhancer.ts`. Order and wording must match the CLI; the shared fixture
|
|
22
|
+
/// tests fail when either side changes alone.
|
|
23
|
+
static let instructionPatternSources: [String] = [
|
|
24
|
+
#"(?:write|draft|compose|create)\s+(?:an?\s+)?(?:email|message|response|reply|letter|note|text|slack|dm)"#,
|
|
25
|
+
#"(?:give|provide|send)\s+(?:them|him|her|it|the\s+agent|the\s+team)\s+(?:full\s+)?instructions"#,
|
|
26
|
+
#"(?:tell|ask)\s+(?:them|him|her|it|the\s+agent)\s+(?:to|that)"#,
|
|
27
|
+
#"(?:make\s+it|make\s+this)\s+(?:sound|look|read)\s+(?:more\s+)?(?:professional|formal|casual|friendly|better)"#,
|
|
28
|
+
#"(?:ok\s+so|okay\s+so|alright\s+so)\s+(?:say|write|tell|put)"#,
|
|
29
|
+
#"(?:i\s+need|i\s+want)\s+(?:the\s+agent|it|them|you)\s+to\s+(?:build|create|implement|design|make)"#,
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
private static let instructionPatterns: [NSRegularExpression]? = {
|
|
33
|
+
var patterns: [NSRegularExpression] = []
|
|
34
|
+
for source in instructionPatternSources {
|
|
35
|
+
guard let pattern = try? NSRegularExpression(
|
|
36
|
+
pattern: source,
|
|
37
|
+
options: [.caseInsensitive]
|
|
38
|
+
) else { return nil }
|
|
39
|
+
patterns.append(pattern)
|
|
40
|
+
}
|
|
41
|
+
return patterns
|
|
42
|
+
}()
|
|
43
|
+
|
|
44
|
+
/// True when the helper CLI could rewrite `text` in `auto` mode — or when this
|
|
45
|
+
/// screen cannot prove it will not (malformed trigger JSON, an unbuildable
|
|
46
|
+
/// pattern): enhancement-eligible speech must never be pasted raw ahead of the
|
|
47
|
+
/// helper, so every unprovable case persists first.
|
|
48
|
+
public static func mayRequireEnhancement(text: String, enhanceTriggersJSON: String) -> Bool {
|
|
49
|
+
guard let triggers = decodeTriggers(enhanceTriggersJSON) else { return true }
|
|
50
|
+
let lowered = text.lowercased()
|
|
51
|
+
for trigger in triggers {
|
|
52
|
+
let loweredTrigger = trigger.lowercased()
|
|
53
|
+
// An EMPTY configured trigger matches EVERYTHING in the CLI:
|
|
54
|
+
// `needsEnhancement` tests `lower.includes(trigger.toLowerCase())` and
|
|
55
|
+
// JavaScript's `includes("")` is true for every string, so one empty row
|
|
56
|
+
// makes the helper rewrite every transcript (MEASURED on the CLI:
|
|
57
|
+
// needs=true, reason `Explicit trigger: ""`). This screen must answer
|
|
58
|
+
// may-enhance for the same input or it admits that speech to
|
|
59
|
+
// paste-before-persistence and pastes raw text where the user asked for
|
|
60
|
+
// the rewrite.
|
|
61
|
+
//
|
|
62
|
+
// Special-cased rather than left to `contains`, because Swift's answer for
|
|
63
|
+
// an empty needle depends on which overload resolves — Foundation's
|
|
64
|
+
// `range(of:)`-based one answers false, the stdlib `Collection` one answers
|
|
65
|
+
// true — and NO machine has ever executed this file, so neither answer may
|
|
66
|
+
// be assumed. That overload claim is an UNVERIFIED INFERENCE from the
|
|
67
|
+
// language, not a measurement; `return true` is the fail-closed answer
|
|
68
|
+
// under either.
|
|
69
|
+
guard !loweredTrigger.isEmpty else { return true }
|
|
70
|
+
if lowered.contains(loweredTrigger) { return true }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
guard let instructionPatterns else { return true }
|
|
74
|
+
let range = NSRange(text.startIndex..., in: text)
|
|
75
|
+
return instructionPatterns.contains { pattern in
|
|
76
|
+
pattern.firstMatch(in: text, range: range) != nil
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Decodes the frozen trigger array the engine forwards to the helper. An empty
|
|
81
|
+
/// string means "no configured triggers" (the built-in patterns still apply);
|
|
82
|
+
/// anything else must parse as a JSON string array or the screen fails closed.
|
|
83
|
+
private static func decodeTriggers(_ json: String) -> [String]? {
|
|
84
|
+
let trimmed = json.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
85
|
+
guard !trimmed.isEmpty else { return [] }
|
|
86
|
+
guard let data = trimmed.data(using: .utf8),
|
|
87
|
+
let decoded = try? JSONDecoder().decode([String].self, from: data)
|
|
88
|
+
else { return nil }
|
|
89
|
+
return decoded
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -12,15 +12,31 @@ final class FnKeyMonitor: @unchecked Sendable {
|
|
|
12
12
|
private var healthCheckTimer: Timer?
|
|
13
13
|
private var fnIsDown = false
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
/// Whether the tap exists *and* is still enabled.
|
|
16
|
+
///
|
|
17
|
+
/// `eventTap != nil` was not enough: macOS disables a tap when Accessibility is revoked
|
|
18
|
+
/// at runtime, and only `stop()` ever cleared the handle — so a dead tap reported itself
|
|
19
|
+
/// as running, the engine saw no blocker, and the `!isRunning` retry guard prevented
|
|
20
|
+
/// recovery. Ask the tap, not the handle.
|
|
21
|
+
var isRunning: Bool {
|
|
22
|
+
guard let eventTap else { return false }
|
|
23
|
+
return CGEvent.tapIsEnabled(tap: eventTap)
|
|
24
|
+
}
|
|
16
25
|
|
|
17
26
|
private static let fnKeyCode: UInt16 = 63
|
|
18
27
|
|
|
19
28
|
/// Start monitoring. Returns true if successful.
|
|
20
29
|
func start() -> Bool {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
if eventTap != nil {
|
|
31
|
+
guard !isRunning else {
|
|
32
|
+
fputs("[FnKeyMonitor] Already running\n", stderr)
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
35
|
+
// A handle we still hold but that the system has disabled cannot be revived by
|
|
36
|
+
// re-enabling it here — the grant that backed it is gone. Tear it down so the
|
|
37
|
+
// code below can build a fresh one once trust returns.
|
|
38
|
+
fputs("[FnKeyMonitor] Existing tap is disabled — discarding it before retry\n", stderr)
|
|
39
|
+
stop()
|
|
24
40
|
}
|
|
25
41
|
|
|
26
42
|
let eventMask: CGEventMask = 1 << CGEventType.flagsChanged.rawValue
|
|
@@ -63,6 +79,13 @@ final class FnKeyMonitor: @unchecked Sendable {
|
|
|
63
79
|
if !CGEvent.tapIsEnabled(tap: tap) {
|
|
64
80
|
fputs("[FnKeyMonitor] Tap was disabled, re-enabling\n", stderr)
|
|
65
81
|
CGEvent.tapEnable(tap: tap, enable: true)
|
|
82
|
+
// If it will not come back, the permission behind it is gone. Drop the
|
|
83
|
+
// tap so `isRunning` turns false and the engine can report a blocker and
|
|
84
|
+
// rebuild, instead of polling a corpse every three seconds forever.
|
|
85
|
+
if !CGEvent.tapIsEnabled(tap: tap) {
|
|
86
|
+
fputs("[FnKeyMonitor] Re-enable failed — tearing down\n", stderr)
|
|
87
|
+
self.stop()
|
|
88
|
+
}
|
|
66
89
|
}
|
|
67
90
|
}
|
|
68
91
|
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
<key>CFBundleIdentifier</key>
|
|
18
18
|
<string>com.hasna.recordings</string>
|
|
19
19
|
<key>CFBundleVersion</key>
|
|
20
|
-
<string>0.
|
|
20
|
+
<string>0.3.1</string>
|
|
21
21
|
<key>CFBundlePackageType</key>
|
|
22
22
|
<string>APPL</string>
|
|
23
23
|
<key>CFBundleShortVersionString</key>
|
|
24
|
-
<string>0.
|
|
24
|
+
<string>0.3.1</string>
|
|
25
25
|
<key>LSMinimumSystemVersion</key>
|
|
26
26
|
<string>26.0</string>
|
|
27
27
|
<key>NSMicrophoneUsageDescription</key>
|
|
@@ -7,6 +7,12 @@ import Foundation
|
|
|
7
7
|
/// present a busy state and a disabled Start affordance, never an idle state it would
|
|
8
8
|
/// then refuse.
|
|
9
9
|
public struct MenuBarPresentation: Equatable, Sendable {
|
|
10
|
+
/// Symbol for idle-but-blocked. `exclamationmark.triangle.fill` on purpose: it has shipped
|
|
11
|
+
/// since macOS 11, so it cannot resolve to nothing. An invalid `Image(systemName:)` renders
|
|
12
|
+
/// an EMPTY image, which would make the blocked state *less* visible than the bug it fixes.
|
|
13
|
+
public static let blockedIconName = "exclamationmark.triangle.fill"
|
|
14
|
+
public static let idleIconName = "mic.fill"
|
|
15
|
+
|
|
10
16
|
public let iconName: String
|
|
11
17
|
public let accessibilityLabel: String
|
|
12
18
|
public let statusText: String
|
|
@@ -14,24 +20,71 @@ public struct MenuBarPresentation: Equatable, Sendable {
|
|
|
14
20
|
/// (always enabled) Stop affordance; otherwise it is Start and must match
|
|
15
21
|
/// `RecordingEngine.canStartRecording` exactly.
|
|
16
22
|
public let primaryActionEnabled: Bool
|
|
23
|
+
/// Whether this is the idle-but-blocked state. Published so a view can tint or badge without
|
|
24
|
+
/// string-matching `statusText` to guess.
|
|
25
|
+
public let isBlocked: Bool
|
|
17
26
|
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
/// - Parameter blockedReason: `RecordingEngine.blockedReason` — why the app cannot record or
|
|
28
|
+
/// deliver, when the reason outlives one status write.
|
|
29
|
+
///
|
|
30
|
+
/// `blockedReason` is a parameter rather than something inferred from `statusMessage` because
|
|
31
|
+
/// the always-visible menu-bar item renders **only** `iconName` and `accessibilityLabel`
|
|
32
|
+
/// (`MenuBarStatusView.swift`: `Image(systemName:).accessibilityLabel(...)`). Until this
|
|
33
|
+
/// existed, the idle branch set `mic.fill` / "Recordings" and discarded `statusMessage`
|
|
34
|
+
/// entirely — so a blocked app was **byte-identical to Ready** in that surface, for sighted
|
|
35
|
+
/// and VoiceOver users alike, and the only channel carrying "press Cmd-V" was a popover the
|
|
36
|
+
/// user had to click open. A reason with no view consumer is not a disclosure.
|
|
37
|
+
/// - Parameter isWarmingUpCapture: `RecordingEngine.isWarmingUpCapture` — the microphone is
|
|
38
|
+
/// open but has not delivered a sample yet. No default: `false` is the *invisible* value,
|
|
39
|
+
/// and a surface that forgot it would compile and drop the glyph to the busy ellipsis for
|
|
40
|
+
/// ~100 ms in the middle of a hold.
|
|
41
|
+
///
|
|
42
|
+
/// `blockedReason` has no default either, for exactly the same reason and after exactly the
|
|
43
|
+
/// same regression: `nil` is its invisible value, and while it was defaulted
|
|
44
|
+
/// `App/RuntimeSmoke.swift` omitted it and compiled — a surface silently rendering every
|
|
45
|
+
/// blocked state as plain idle. An argument whose absent value *is* the failure mode does
|
|
46
|
+
/// not get to be optional.
|
|
47
|
+
public init(
|
|
48
|
+
isRecording: Bool,
|
|
49
|
+
isWarmingUpCapture: Bool,
|
|
50
|
+
canStartRecording: Bool,
|
|
51
|
+
statusMessage: String,
|
|
52
|
+
blockedReason: String?
|
|
53
|
+
) {
|
|
54
|
+
// Warm-up presents as recording. The user is holding the key and Stop is live; anything
|
|
55
|
+
// else would read as a dead app for the ~100 ms between `recorder.start()` returning and
|
|
56
|
+
// the first PCM chunk.
|
|
57
|
+
if isRecording || isWarmingUpCapture {
|
|
20
58
|
iconName = "waveform"
|
|
21
59
|
accessibilityLabel = "Recordings, recording"
|
|
22
60
|
statusText = "Recording"
|
|
23
61
|
primaryActionEnabled = true
|
|
62
|
+
isBlocked = false
|
|
24
63
|
} else if !canStartRecording {
|
|
25
64
|
let normalizedBusyStatus = statusMessage.trimmingCharacters(in: .punctuationCharacters)
|
|
26
65
|
iconName = "ellipsis.circle"
|
|
27
66
|
accessibilityLabel = "Recordings, \(normalizedBusyStatus.lowercased())"
|
|
28
67
|
statusText = normalizedBusyStatus
|
|
29
68
|
primaryActionEnabled = false
|
|
69
|
+
isBlocked = false
|
|
70
|
+
} else if let blockedReason, !blockedReason.isEmpty {
|
|
71
|
+
// Idle and startable, but something the owner has to act on is wrong. Distinct icon
|
|
72
|
+
// AND distinct label: the icon is the only signal a sighted user gets from the menu
|
|
73
|
+
// bar, and the label is the only signal VoiceOver gets. Fixing one and not the other
|
|
74
|
+
// leaves half the users exactly where they were.
|
|
75
|
+
iconName = Self.blockedIconName
|
|
76
|
+
accessibilityLabel = "Recordings, blocked: \(blockedReason)"
|
|
77
|
+
statusText = blockedReason
|
|
78
|
+
// Still true, and still this type's other contract: a blocked trigger or a blocked
|
|
79
|
+
// paste does not make Start unavailable, and `canStartRecording` is what governs.
|
|
80
|
+
primaryActionEnabled = true
|
|
81
|
+
isBlocked = true
|
|
30
82
|
} else {
|
|
31
|
-
iconName =
|
|
83
|
+
iconName = Self.idleIconName
|
|
32
84
|
accessibilityLabel = "Recordings"
|
|
33
85
|
statusText = statusMessage
|
|
34
86
|
primaryActionEnabled = true
|
|
87
|
+
isBlocked = false
|
|
35
88
|
}
|
|
36
89
|
}
|
|
37
90
|
}
|
|
@@ -127,6 +127,19 @@ final class NativePCMRecorder: @unchecked Sendable {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
do {
|
|
130
|
+
// These two calls are the ~520 ms a cold start costs, and they are why a short tap
|
|
131
|
+
// cannot capture anything: `RecordingEngine` only reports a live capture once the
|
|
132
|
+
// tap above delivers its first buffer, ~100 ms after `start()` returns.
|
|
133
|
+
//
|
|
134
|
+
// Pre-warming (holding a started engine between recordings, or hoisting
|
|
135
|
+
// `prepare()` to app launch) is the only way to make a sub-500 ms press record.
|
|
136
|
+
// It is deliberately NOT done here: a running engine with an installed input tap
|
|
137
|
+
// holds the input device open, which lights the macOS microphone-in-use indicator
|
|
138
|
+
// and lists the app under Control Center's microphone recents for as long as it
|
|
139
|
+
// runs. A permanently lit indicator is a worse regression than the bug. Whether
|
|
140
|
+
// `prepare()` alone — resource allocation without starting the IO thread — both
|
|
141
|
+
// absorbs a useful share of the 520 ms and leaves the indicator dark has to be
|
|
142
|
+
// measured on real hardware before anyone acts on it.
|
|
130
143
|
engine.prepare()
|
|
131
144
|
try engine.start()
|
|
132
145
|
finishStart()
|