@hasna/recordings 0.3.13 → 0.5.0
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 +279 -43
- package/bun.lock +37 -38
- package/contracts/v1/fixtures.json +1206 -0
- package/dist/cli/index.js +1909 -619
- package/dist/cli/macos-shortcut.d.ts +2 -2
- package/dist/contracts/hosted-v1.d.ts +105 -0
- package/dist/contracts/hosted-v1.d.ts.map +1 -0
- package/dist/contracts/hosted-v1.js +41 -0
- package/dist/contracts/stream-v1.d.ts +117 -0
- package/dist/contracts/stream-v1.d.ts.map +1 -0
- package/dist/contracts/stream-v1.js +35 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/recordings.d.ts +6 -0
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/hosted/index.d.ts +58 -0
- package/dist/hosted/index.d.ts.map +1 -0
- package/dist/hosted/index.js +266 -0
- package/dist/hosted/transport.d.ts +36 -0
- package/dist/hosted/transport.d.ts.map +1 -0
- package/dist/hosted-v1-aavn7ktb.js +4114 -0
- package/dist/hosted-v1-gdr9extc.js +84 -0
- package/dist/http/client.d.ts +183 -22
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2879 -2288
- package/dist/lib/audio-artifact-storage.d.ts +98 -0
- package/dist/lib/audio-artifact-storage.d.ts.map +1 -0
- package/dist/lib/capture-probe.d.ts +3 -3
- package/dist/lib/capture-probe.d.ts.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/local-opt-in.d.ts +132 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -0
- package/dist/lib/macos-bundle.d.ts +1 -1
- package/dist/lib/persistence-probe.d.ts +56 -17
- package/dist/lib/persistence-probe.d.ts.map +1 -1
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1713 -459
- package/dist/sdk/index.d.ts +13 -5
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +904 -1
- package/dist/sdk/resolve.d.ts +53 -0
- package/dist/sdk/resolve.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +6 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/client-ip.d.ts +39 -0
- package/dist/server/client-ip.d.ts.map +1 -0
- package/dist/server/cloud-config.d.ts +12 -0
- 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 +20 -2
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +590 -287
- package/dist/server/migrate-command.d.ts +1 -1
- package/dist/server/migrate-command.d.ts.map +1 -1
- package/dist/server/openapi.d.ts +21 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/repo.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 +1358 -874
- package/dist/store.d.ts +12 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/types/index.d.ts +15 -0
- package/dist/types/index.d.ts.map +1 -1
- package/docs/hosted-sdk.md +71 -0
- package/docs/wire-contracts.md +24 -0
- package/package.json +33 -11
- package/packaging/macos/build_release_pkg.sh +3 -3
- package/packaging/macos/managed_bootstrap.sh +4 -4
- package/packaging/macos/scripts/postinstall +2 -2
- package/packaging/macos/scripts/preinstall +2 -2
- package/scripts/build_companion_cli.sh +7 -1
- package/scripts/ci-linux-suite.ts +23 -13
- package/scripts/install_macos_app.sh +27 -24
- package/scripts/macos_artifact.ts +60 -49
- package/scripts/migrate.ts +4 -20
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
- package/scripts/native/recordings_fs_guard.c +36 -4
- package/scripts/native-core-receipt.py +171 -0
- package/scripts/native_fs_guard.ts +2 -0
- package/scripts/release-suite-gate.ts +227 -150
- package/scripts/resolve_tailscale_cli.sh +24 -3
- package/scripts/smoke_macos_app.sh +21 -21
- package/src/native/Recordings/App/ContentView.swift +53 -68
- package/src/native/Recordings/App/MenuBarStatusView.swift +171 -61
- package/src/native/Recordings/App/RecordWorkspaceView.swift +47 -449
- package/src/native/Recordings/App/RecorderSettingsView.swift +75 -0
- package/src/native/Recordings/App/RecordingDetailView.swift +3 -4
- package/src/native/Recordings/App/RecordingsApp.swift +185 -9
- package/src/native/Recordings/App/RecordingsListView.swift +60 -112
- package/src/native/Recordings/App/RecordingsStore.swift +124 -109
- package/src/native/Recordings/App/Theme.swift +73 -61
- package/src/native/Recordings/Package.swift +1 -0
- package/src/native/Recordings/RecordingsLib/BlockingOperation.swift +29 -0
- package/src/native/Recordings/RecordingsLib/CaptureMonitor.swift +65 -0
- package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +75 -21
- package/src/native/Recordings/RecordingsLib/ProjectStore.swift +4 -4
- package/src/native/Recordings/RecordingsLib/Recording.swift +14 -2
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +539 -89
- package/src/native/Recordings/RecordingsLib/RecordingGlassBackground.swift +34 -0
- package/src/native/Recordings/RecordingsLib/RecordingPasteTarget.swift +114 -0
- package/src/native/Recordings/RecordingsLib/RecordingProvider.swift +112 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +17 -10
- package/src/native/Recordings/RecordingsLib/ServiceAPIConfiguration.swift +107 -0
- package/src/native/Recordings/RecordingsLib/ServiceConnectionView.swift +76 -0
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -149
- package/src/native/Recordings/RecordingsLib/SettingsWindowController.swift +48 -0
- package/src/native/Recordings/RecordingsTests/BlockingOperationTests.swift +85 -0
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +454 -76
- package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +43 -38
- package/src/native/Recordings/RecordingsTests/PipeClosureFixture.swift +215 -0
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +10 -10
- package/src/native/Recordings/RecordingsTests/RecorderSurfaceTests.swift +60 -0
- package/src/native/Recordings/RecordingsTests/RecordingDateTests.swift +26 -0
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +1 -1
- package/src/native/Recordings/RecordingsTests/RecordingFrozenPasteTargetTests.swift +50 -0
- package/src/native/Recordings/RecordingsTests/RecordingPasteTargetTrackerTests.swift +48 -0
- package/src/native/Recordings/RecordingsTests/RecordingProviderTests.swift +308 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +123 -21
- package/src/native/Recordings/RecordingsTests/ServiceAPIConfigurationTests.swift +75 -0
- package/src/native/Recordings/RecordingsTests/SettingsWindowControllerTests.swift +28 -0
- package/src/native/Recordings/RecordingsTests/TestHomeDirectory.swift +5 -1
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
- package/src/native/Recordings/build.sh +8 -7
- package/dist/__tests__/helpers/installer-guard-execution.d.ts +0 -22
- package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +0 -1
- package/dist/__tests__/helpers/installer-preflight.d.ts +0 -22
- package/dist/__tests__/helpers/installer-preflight.d.ts.map +0 -1
- package/dist/__tests__/helpers/native-fs-guard.d.ts +0 -2
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +0 -1
- package/dist/__tests__/helpers/source-assertions.d.ts +0 -171
- package/dist/__tests__/helpers/source-assertions.d.ts.map +0 -1
- package/dist/__tests__/preload.d.ts +0 -2
- package/dist/__tests__/preload.d.ts.map +0 -1
- package/src/native/Recordings/App/SidebarView.swift +0 -212
|
@@ -77,6 +77,27 @@ recordings_tailscale_stat_owner_and_mode() {
|
|
|
77
77
|
esac
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
# macOS does not ship /usr/bin/realpath on every supported host. Resolve with
|
|
81
|
+
# Bash's physical-directory builtins, without a PATH tool or interpreter. The
|
|
82
|
+
# callers still reject leaf symlinks and compare this physical path exactly.
|
|
83
|
+
recordings_tailscale_physical_path() {
|
|
84
|
+
local candidate="$1"
|
|
85
|
+
local parent
|
|
86
|
+
local leaf
|
|
87
|
+
|
|
88
|
+
recordings_validate_tailscale_cli_shape "$candidate" || return 1
|
|
89
|
+
if [ -d "$candidate" ]; then
|
|
90
|
+
(unset CDPATH; builtin cd -P -- "$candidate" && builtin pwd -P)
|
|
91
|
+
return
|
|
92
|
+
fi
|
|
93
|
+
[ -e "$candidate" ] && [ ! -L "$candidate" ] || return 1
|
|
94
|
+
parent="${candidate%/*}"
|
|
95
|
+
leaf="${candidate##*/}"
|
|
96
|
+
[ -n "$parent" ] || parent=/
|
|
97
|
+
parent="$(unset CDPATH; builtin cd -P -- "$parent" && builtin pwd -P)" || return 1
|
|
98
|
+
printf '%s/%s\n' "${parent%/}" "$leaf"
|
|
99
|
+
}
|
|
100
|
+
|
|
80
101
|
recordings_validate_trusted_tailscale_app_cli() {
|
|
81
102
|
local candidate="$1"
|
|
82
103
|
local real_host_kernel="$2"
|
|
@@ -96,7 +117,7 @@ recordings_validate_trusted_tailscale_app_cli() {
|
|
|
96
117
|
echo "Trusted Tailscale CLI must be a canonical non-symlink trusted executable." >&2
|
|
97
118
|
return 1
|
|
98
119
|
fi
|
|
99
|
-
canonical="$(
|
|
120
|
+
canonical="$(recordings_tailscale_physical_path "$candidate" 2>/dev/null)" || {
|
|
100
121
|
echo "Trusted Tailscale CLI could not be resolved canonically." >&2
|
|
101
122
|
return 1
|
|
102
123
|
}
|
|
@@ -145,7 +166,7 @@ recordings_validate_private_tailscale_snapshot_parent() {
|
|
|
145
166
|
echo "Tailscale snapshot parent must be an existing private directory." >&2
|
|
146
167
|
return 1
|
|
147
168
|
fi
|
|
148
|
-
canonical="$(
|
|
169
|
+
canonical="$(recordings_tailscale_physical_path "$snapshot_parent" 2>/dev/null)" || {
|
|
149
170
|
echo "Tailscale snapshot parent could not be resolved canonically." >&2
|
|
150
171
|
return 1
|
|
151
172
|
}
|
|
@@ -270,7 +291,7 @@ recordings_verify_official_tailscale_app_and_cli() {
|
|
|
270
291
|
echo "Trusted Tailscale app must be a canonical non-symlink directory." >&2
|
|
271
292
|
return 1
|
|
272
293
|
fi
|
|
273
|
-
canonical_app="$(
|
|
294
|
+
canonical_app="$(recordings_tailscale_physical_path "$app" 2>/dev/null)" || {
|
|
274
295
|
echo "Trusted Tailscale app could not be resolved canonically." >&2
|
|
275
296
|
return 1
|
|
276
297
|
}
|
|
@@ -9,7 +9,7 @@ if [ "$#" -ge 3 ]; then
|
|
|
9
9
|
SMOKE_VARIANT="${4:-full}"
|
|
10
10
|
;;
|
|
11
11
|
*)
|
|
12
|
-
echo "Usage: $0 <
|
|
12
|
+
echo "Usage: $0 <Hasna Recordings.app> <explicit absolute Bun executable path> [--variant full|bar]" >&2
|
|
13
13
|
exit 2
|
|
14
14
|
;;
|
|
15
15
|
esac
|
|
@@ -51,7 +51,7 @@ HOST_PLATFORM="$("$HOST_UNAME_EXECUTABLE" -s)"
|
|
|
51
51
|
readonly HOST_PLATFORM
|
|
52
52
|
|
|
53
53
|
if [ "$HOST_PLATFORM" != "Darwin" ] && [ "${RECORDINGS_TEST_SMOKE_ALLOW_NON_DARWIN:-0}" != "1" ]; then
|
|
54
|
-
echo "
|
|
54
|
+
echo "Hasna Recordings.app runtime smoke is only supported on macOS." >&2
|
|
55
55
|
exit 1
|
|
56
56
|
fi
|
|
57
57
|
|
|
@@ -123,12 +123,12 @@ HOME_DIRECTORY="${HOME:-}"
|
|
|
123
123
|
case "$HOME_DIRECTORY" in
|
|
124
124
|
/*) ;;
|
|
125
125
|
*)
|
|
126
|
-
echo "
|
|
126
|
+
echo "Hasna Recordings.app runtime smoke requires an absolute HOME path." >&2
|
|
127
127
|
exit 1
|
|
128
128
|
;;
|
|
129
129
|
esac
|
|
130
130
|
if [ ! -d "$HOME_DIRECTORY" ]; then
|
|
131
|
-
echo "
|
|
131
|
+
echo "Hasna Recordings.app runtime smoke HOME is not a directory: $HOME_DIRECTORY" >&2
|
|
132
132
|
exit 1
|
|
133
133
|
fi
|
|
134
134
|
|
|
@@ -136,7 +136,7 @@ APP_PARENT="$(cd -P "$("$DIRNAME_EXECUTABLE" "$1")" && "$PWD_EXECUTABLE" -P)"
|
|
|
136
136
|
APP_PATH="$APP_PARENT/$("$BASENAME_EXECUTABLE" "$1")"
|
|
137
137
|
EXECUTABLE="$APP_PATH/Contents/MacOS/Recordings"
|
|
138
138
|
if [ ! -x "$EXECUTABLE" ]; then
|
|
139
|
-
echo "
|
|
139
|
+
echo "Hasna Recordings.app runtime smoke executable is missing: $EXECUTABLE" >&2
|
|
140
140
|
exit 1
|
|
141
141
|
fi
|
|
142
142
|
|
|
@@ -317,7 +317,7 @@ cleanup() {
|
|
|
317
317
|
fi
|
|
318
318
|
if [ -n "$SMOKE_APP_PID" ] && [ -n "$SMOKE_APP_PID_START_IDENTITY" ]; then
|
|
319
319
|
terminate_verified_process \
|
|
320
|
-
"
|
|
320
|
+
"Hasna Recordings.app" \
|
|
321
321
|
"$SMOKE_APP_PID" \
|
|
322
322
|
"$EXECUTABLE" \
|
|
323
323
|
"$SMOKE_APP_PID_START_IDENTITY" || true
|
|
@@ -377,7 +377,7 @@ run_smoke() {
|
|
|
377
377
|
if ! SMOKE_APP_IDENTITIES_BEFORE_LAUNCH="$(
|
|
378
378
|
process_records_for_exact_executable "$EXECUTABLE"
|
|
379
379
|
)"; then
|
|
380
|
-
echo "
|
|
380
|
+
echo "Hasna Recordings.app runtime smoke ${mode} could not inventory exact app processes before launch." >&2
|
|
381
381
|
return 1
|
|
382
382
|
fi
|
|
383
383
|
|
|
@@ -399,24 +399,24 @@ run_smoke() {
|
|
|
399
399
|
result_pid="$("$ENV_EXECUTABLE" -i HOME="$HOME_DIRECTORY" PATH="$SANITIZED_PATH" TMPDIR="$WORK_DIR" \
|
|
400
400
|
"$BUN_EXECUTABLE" -e 'const value = await Bun.file(process.argv[1]).json(); console.log(value.processIdentifier)' "$output")"
|
|
401
401
|
if ! [[ "$result_pid" =~ ^[1-9][0-9]*$ ]] || ! kill -0 "$result_pid" 2>/dev/null; then
|
|
402
|
-
echo "
|
|
402
|
+
echo "Hasna Recordings.app runtime smoke ${mode} reported a process that is not running." >&2
|
|
403
403
|
return 1
|
|
404
404
|
fi
|
|
405
405
|
if ! result_pid_start_identity="$(
|
|
406
406
|
capture_process_start_identity "$result_pid" "$EXECUTABLE"
|
|
407
407
|
)"; then
|
|
408
|
-
echo "
|
|
408
|
+
echo "Hasna Recordings.app runtime smoke ${mode} reported a process outside the exact app path." >&2
|
|
409
409
|
return 1
|
|
410
410
|
fi
|
|
411
411
|
if process_identity_existed_before_launch "$result_pid" "$result_pid_start_identity"; then
|
|
412
|
-
echo "
|
|
412
|
+
echo "Hasna Recordings.app runtime smoke ${mode} reported an app process that predated this launch." >&2
|
|
413
413
|
return 1
|
|
414
414
|
fi
|
|
415
415
|
if [ -n "$SMOKE_APP_PID" ] && {
|
|
416
416
|
[ "$result_pid" != "$SMOKE_APP_PID" ] ||
|
|
417
417
|
[ "$result_pid_start_identity" != "$SMOKE_APP_PID_START_IDENTITY" ];
|
|
418
418
|
}; then
|
|
419
|
-
echo "
|
|
419
|
+
echo "Hasna Recordings.app runtime smoke ${mode} evidence did not match the exact launched app process." >&2
|
|
420
420
|
return 1
|
|
421
421
|
fi
|
|
422
422
|
SMOKE_APP_PID="$result_pid"
|
|
@@ -429,19 +429,19 @@ run_smoke() {
|
|
|
429
429
|
break
|
|
430
430
|
fi
|
|
431
431
|
if ! kill -0 "$SMOKE_PID" 2>/dev/null; then
|
|
432
|
-
echo "
|
|
432
|
+
echo "Hasna Recordings.app runtime smoke ${mode} exited without evidence." >&2
|
|
433
433
|
"$SED_EXECUTABLE" -n '1,120p' "$log" >&2
|
|
434
434
|
return 1
|
|
435
435
|
fi
|
|
436
436
|
"$SLEEP_EXECUTABLE" 0.1
|
|
437
437
|
done
|
|
438
438
|
if [ ! -f "$output" ]; then
|
|
439
|
-
echo "
|
|
439
|
+
echo "Hasna Recordings.app runtime smoke ${mode} timed out." >&2
|
|
440
440
|
"$SED_EXECUTABLE" -n '1,120p' "$log" >&2
|
|
441
441
|
return 1
|
|
442
442
|
fi
|
|
443
443
|
if [ -z "$SMOKE_APP_PID" ]; then
|
|
444
|
-
echo "
|
|
444
|
+
echo "Hasna Recordings.app runtime smoke ${mode} did not bind evidence to the exact app process." >&2
|
|
445
445
|
return 1
|
|
446
446
|
fi
|
|
447
447
|
|
|
@@ -546,7 +546,7 @@ run_smoke() {
|
|
|
546
546
|
' "$output"
|
|
547
547
|
|
|
548
548
|
if [ -e "$completion" ]; then
|
|
549
|
-
echo "
|
|
549
|
+
echo "Hasna Recordings.app runtime smoke ${mode} produced completion before the challenge." >&2
|
|
550
550
|
return 1
|
|
551
551
|
fi
|
|
552
552
|
challenge="$("$ENV_EXECUTABLE" -i HOME="$HOME_DIRECTORY" PATH="$SANITIZED_PATH" TMPDIR="$WORK_DIR" \
|
|
@@ -554,7 +554,7 @@ run_smoke() {
|
|
|
554
554
|
if ! rechecked_start_identity="$(
|
|
555
555
|
capture_process_start_identity "$result_pid" "$EXECUTABLE"
|
|
556
556
|
)" || [ "$rechecked_start_identity" != "$result_pid_start_identity" ]; then
|
|
557
|
-
echo "
|
|
557
|
+
echo "Hasna Recordings.app runtime smoke ${mode} process identity changed before completion challenge." >&2
|
|
558
558
|
return 1
|
|
559
559
|
fi
|
|
560
560
|
write_atomic_text_file "$acknowledgement" "$challenge"
|
|
@@ -564,7 +564,7 @@ run_smoke() {
|
|
|
564
564
|
"$SLEEP_EXECUTABLE" 0.1
|
|
565
565
|
done
|
|
566
566
|
if kill -0 "$SMOKE_PID" 2>/dev/null; then
|
|
567
|
-
echo "
|
|
567
|
+
echo "Hasna Recordings.app runtime smoke ${mode} completion handshake timed out." >&2
|
|
568
568
|
return 1
|
|
569
569
|
fi
|
|
570
570
|
if wait "$SMOKE_PID" 2>/dev/null; then
|
|
@@ -577,7 +577,7 @@ run_smoke() {
|
|
|
577
577
|
SMOKE_APP_PID=""
|
|
578
578
|
SMOKE_APP_PID_START_IDENTITY=""
|
|
579
579
|
if [ "$wrapper_status" -ne 0 ]; then
|
|
580
|
-
echo "
|
|
580
|
+
echo "Hasna Recordings.app runtime smoke ${mode} open -W wrapper exited unsuccessfully (${wrapper_status})." >&2
|
|
581
581
|
"$SED_EXECUTABLE" -n '1,120p' "$log" >&2
|
|
582
582
|
return 1
|
|
583
583
|
fi
|
|
@@ -602,7 +602,7 @@ run_smoke() {
|
|
|
602
602
|
process.exit(1);
|
|
603
603
|
}
|
|
604
604
|
' "$completion"; then
|
|
605
|
-
echo "
|
|
605
|
+
echo "Hasna Recordings.app runtime smoke ${mode} did not provide a valid completion response." >&2
|
|
606
606
|
return 1
|
|
607
607
|
fi
|
|
608
608
|
|
|
@@ -613,7 +613,7 @@ run_smoke normal
|
|
|
613
613
|
run_smoke permission-helper
|
|
614
614
|
run_smoke resolver
|
|
615
615
|
if [ "$SMOKE_VARIANT" = "bar" ]; then
|
|
616
|
-
echo "
|
|
616
|
+
echo "Hasna Recordings.app runtime smoke passed (bar variant): menu bar, windowless launch, helper isolation, and packaged resolver."
|
|
617
617
|
else
|
|
618
|
-
echo "
|
|
618
|
+
echo "Hasna Recordings.app runtime smoke passed: menu bar, retained window, helper isolation, and packaged resolver."
|
|
619
619
|
fi
|
|
@@ -1,83 +1,68 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
2
|
import RecordingsLib
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
enum RecorderPage {
|
|
5
|
+
case recorder, history, settings, advanced
|
|
6
|
+
var size: NSSize {
|
|
7
|
+
switch self {
|
|
8
|
+
case .recorder: NSSize(width: 224, height: 244)
|
|
9
|
+
case .history: NSSize(width: 500, height: 390)
|
|
10
|
+
case .settings: NSSize(width: 420, height: 350)
|
|
11
|
+
case .advanced: NSSize(width: 520, height: 540)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// One retained app window and one shared glass/title bar across every destination.
|
|
7
17
|
struct ContentView: View {
|
|
8
18
|
@ObservedObject var store: RecordingsStore
|
|
9
|
-
@
|
|
19
|
+
@ObservedObject var state: RecordingsAppState
|
|
20
|
+
let windowAction: (NSWindow.ButtonType) -> Void
|
|
10
21
|
|
|
11
22
|
var body: some View {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
VStack(spacing: 0) {
|
|
24
|
+
HStack(spacing: 7) {
|
|
25
|
+
windowDot(.red, label: "Close", kind: .closeButton)
|
|
26
|
+
windowDot(.yellow, label: "Minimize", kind: .miniaturizeButton)
|
|
27
|
+
windowDot(.green, label: "Zoom", kind: .zoomButton)
|
|
28
|
+
Spacer(minLength: 6)
|
|
29
|
+
Text("Hasna Recordings").font(.system(size: 11, weight: .medium)).foregroundStyle(.secondary)
|
|
30
|
+
Spacer(minLength: 0)
|
|
31
|
+
}.padding(.horizontal, 12).frame(height: 36)
|
|
32
|
+
switch state.page {
|
|
33
|
+
case .recorder:
|
|
34
|
+
RecordWorkspaceView(store: store)
|
|
35
|
+
HStack {
|
|
36
|
+
GlassIconButton(symbol: "clock", label: "Recordings", action: state.openHistory)
|
|
37
|
+
Spacer()
|
|
38
|
+
GlassIconButton(symbol: "gearshape", label: "Settings", action: state.openSettings)
|
|
39
|
+
}.padding(.horizontal, 18).padding(.bottom, 14)
|
|
40
|
+
case .history:
|
|
41
|
+
RecordingsListView(store: store, close: state.openRecordings)
|
|
42
|
+
case .settings:
|
|
43
|
+
RecorderSettingsView(store: store, close: state.openRecordings, advanced: state.openAdvancedSettings)
|
|
44
|
+
case .advanced:
|
|
45
|
+
HStack {
|
|
46
|
+
Button(action: state.openSettings) { Label("Settings", systemImage: "chevron.left") }.buttonStyle(GlassButtonStyle())
|
|
47
|
+
Spacer()
|
|
48
|
+
}.padding(.horizontal, 16)
|
|
49
|
+
SettingsView(engine: store.engine, shortcuts: store.voiceShortcuts, preferences: store.preferences)
|
|
32
50
|
}
|
|
33
|
-
.background(Theme.canvas(colorScheme))
|
|
34
51
|
}
|
|
35
|
-
.
|
|
36
|
-
.
|
|
52
|
+
.font(.system(size: 13))
|
|
53
|
+
.frame(width: state.page.size.width, height: state.page.size.height)
|
|
54
|
+
.background(FrostedBackground())
|
|
55
|
+
.ignoresSafeArea()
|
|
37
56
|
.onAppear { store.loadLibrary() }
|
|
38
|
-
.alert("Recordings Error", isPresented: Binding(
|
|
39
|
-
get: { store.operationError != nil },
|
|
40
|
-
set: { if !$0 { store.operationError = nil } }
|
|
41
|
-
)) {
|
|
57
|
+
.alert("Recordings Error", isPresented: Binding(get: { store.operationError != nil }, set: { if !$0 { store.operationError = nil } })) {
|
|
42
58
|
Button("OK", role: .cancel) { store.operationError = nil }
|
|
43
|
-
} message: {
|
|
44
|
-
Text(store.operationError ?? "The operation failed.")
|
|
45
|
-
}
|
|
59
|
+
} message: { Text(store.operationError ?? "The operation failed.") }
|
|
46
60
|
}
|
|
47
|
-
}
|
|
48
61
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
HStack(spacing: 6) {
|
|
55
|
-
Text("\(store.library.count) recording\(store.library.count == 1 ? "" : "s")")
|
|
56
|
-
if let updated = store.lastUpdated {
|
|
57
|
-
Text("·")
|
|
58
|
-
Text("Updated \(updated.relativeDescription)")
|
|
59
|
-
}
|
|
60
|
-
Spacer()
|
|
61
|
-
|
|
62
|
-
Button { store.loadLibrary() } label: {
|
|
63
|
-
Image(systemName: "arrow.clockwise")
|
|
64
|
-
}
|
|
65
|
-
.buttonStyle(.plain)
|
|
66
|
-
.help("Refresh library")
|
|
67
|
-
|
|
68
|
-
Button {
|
|
69
|
-
store.pane = .record
|
|
70
|
-
} label: {
|
|
71
|
-
Image(systemName: "mic.circle.fill")
|
|
72
|
-
}
|
|
73
|
-
.buttonStyle(.plain)
|
|
74
|
-
.foregroundStyle(Theme.accent)
|
|
75
|
-
.help("New recording")
|
|
76
|
-
}
|
|
77
|
-
.font(.system(.subheadline, design: .rounded))
|
|
78
|
-
.foregroundStyle(.secondary)
|
|
79
|
-
.padding(.horizontal, 18)
|
|
80
|
-
.padding(.top, 20)
|
|
81
|
-
.padding(.bottom, 10)
|
|
62
|
+
private func windowDot(_ color: Color, label: String, kind: NSWindow.ButtonType) -> some View {
|
|
63
|
+
Button { windowAction(kind) } label: {
|
|
64
|
+
Circle().fill(color.gradient).frame(width: 12, height: 12)
|
|
65
|
+
.overlay(Circle().strokeBorder(.white.opacity(0.6), lineWidth: 0.5))
|
|
66
|
+
}.buttonStyle(.plain).accessibilityLabel(label)
|
|
82
67
|
}
|
|
83
68
|
}
|
|
@@ -4,12 +4,17 @@ import RecordingsLib
|
|
|
4
4
|
|
|
5
5
|
struct MenuBarStatusLabel: View {
|
|
6
6
|
@ObservedObject var store: RecordingsStore
|
|
7
|
-
|
|
8
7
|
var body: some View {
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
HStack(spacing: 4) {
|
|
9
|
+
Image(systemName: presentation.iconName)
|
|
10
|
+
.symbolRenderingMode(.palette)
|
|
11
|
+
.foregroundStyle(store.engine.captureIsActive && !store.engine.isPaused ? .red : .primary)
|
|
12
|
+
if store.engine.captureIsActive || store.isPlaying {
|
|
13
|
+
Text(Theme.clock(store.isPlaying ? store.playbackTime : store.engine.recordingDuration))
|
|
14
|
+
.monospacedDigit()
|
|
15
|
+
}
|
|
16
|
+
}.accessibilityLabel(presentation.accessibilityLabel)
|
|
11
17
|
}
|
|
12
|
-
|
|
13
18
|
private var presentation: MenuBarPresentation {
|
|
14
19
|
MenuBarPresentation(
|
|
15
20
|
isRecording: store.engine.isRecording,
|
|
@@ -24,68 +29,88 @@ struct MenuBarStatusLabel: View {
|
|
|
24
29
|
struct MenuBarStatusView: View {
|
|
25
30
|
@ObservedObject var store: RecordingsStore
|
|
26
31
|
let openRecordings: () -> Void
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
let openSettings: () -> Void
|
|
33
|
+
let openHistory: () -> Void
|
|
34
|
+
let openRecent: () -> Void
|
|
35
|
+
var openBar: (() -> Void)? = nil
|
|
36
|
+
var closeBar: (() -> Void)? = nil
|
|
29
37
|
let barOnly: Bool
|
|
38
|
+
@State private var showsActions = false
|
|
39
|
+
private var width: CGFloat { min(820, (NSScreen.main?.visibleFrame.width ?? 1200) - 48) }
|
|
30
40
|
|
|
31
41
|
var body: some View {
|
|
32
|
-
VStack(
|
|
33
|
-
HStack(spacing:
|
|
34
|
-
|
|
35
|
-
.
|
|
36
|
-
VStack(alignment: .leading, spacing: 2) {
|
|
37
|
-
Text("Recordings")
|
|
38
|
-
.font(.headline)
|
|
39
|
-
Text(presentation.statusText)
|
|
40
|
-
.font(.caption)
|
|
41
|
-
.foregroundStyle(.secondary)
|
|
42
|
+
VStack(spacing: 16) {
|
|
43
|
+
HStack(spacing: 12) {
|
|
44
|
+
Button(action: toggleRecording) {
|
|
45
|
+
GlassCircle(symbol: recordButtonIcon, size: 34, red: store.engine.captureIsActive)
|
|
42
46
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.tint(store.engine.captureIsActive ? .red : .accentColor)
|
|
52
|
-
.disabled(!presentation.primaryActionEnabled)
|
|
53
|
-
|
|
54
|
-
Divider()
|
|
55
|
-
|
|
56
|
-
if !barOnly {
|
|
57
|
-
Button(action: openRecordings) {
|
|
58
|
-
Label("Open Recordings", systemImage: "macwindow")
|
|
47
|
+
.buttonStyle(.plain).disabled(!presentation.primaryActionEnabled)
|
|
48
|
+
.accessibilityLabel(recordButtonTitle)
|
|
49
|
+
AudioWaveform(level: store.engine.audioLevel, recording: store.engine.captureIsActive && !store.engine.isPaused)
|
|
50
|
+
.frame(width: 84, height: 30).accessibilityLabel("Microphone level")
|
|
51
|
+
Divider().frame(height: 30)
|
|
52
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
53
|
+
Text(presentation.statusText).font(.system(size: 11)).foregroundStyle(statusColor)
|
|
54
|
+
Text(transcript).font(.system(size: 13)).lineLimit(1)
|
|
59
55
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
60
56
|
}
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
57
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
58
|
+
Divider().frame(height: 28)
|
|
59
|
+
GlassIconButton(symbol: store.engine.isPaused ? "play.fill" : "pause.fill", label: store.engine.isPaused ? "Resume recording" : "Pause recording") { store.engine.togglePause() }
|
|
60
|
+
.disabled(!store.engine.isRecording)
|
|
61
|
+
GlassIconButton(symbol: store.isPlaying ? "pause.fill" : "play.fill", label: "Playback and recent pastes") {
|
|
62
|
+
if store.hasPlayback && !store.engine.captureIsActive { store.toggleLatestPlayback() }
|
|
63
|
+
openRecent()
|
|
64
|
+
}
|
|
65
|
+
Divider().frame(height: 28)
|
|
66
|
+
Toggle("Auto-paste", isOn: Binding(get: { store.engine.autoPasteEnabled }, set: { store.engine.autoPasteEnabled = $0 })).toggleStyle(.switch).controlSize(.small).fixedSize().font(.system(size: 12))
|
|
67
|
+
GlassIconButton(symbol: "ellipsis", label: "More", size: 26) { showsActions.toggle() }
|
|
68
|
+
.popover(isPresented: $showsActions, arrowEdge: .bottom) {
|
|
69
|
+
VStack(alignment: .leading, spacing: 14) {
|
|
70
|
+
if let openBar { Button("Keep bar visible", action: openBar) }
|
|
71
|
+
if let closeBar { Button("Hide bar", action: closeBar) }
|
|
72
|
+
Button("Recent pastes", action: openRecent)
|
|
73
|
+
Button("Recordings", action: openHistory)
|
|
74
|
+
if !barOnly { Button(action: openRecordings) { Text("Open Hasna Recordings") } }
|
|
75
|
+
Button(action: openSettings) { Text("Settings") }
|
|
76
|
+
if store.engine.canCancelIntentDelivery { Button("Cancel processing") { store.engine.cancelIntentProcessing() } }
|
|
77
|
+
if store.engine.captureIsActive { Button("Discard recording") { store.engine.cancelRecording() } }
|
|
78
|
+
Divider()
|
|
79
|
+
Button("Quit Hasna Recordings") { NSApplication.shared.terminate(nil) }
|
|
80
|
+
}.buttonStyle(GlassButtonStyle()).font(.system(size: 13)).padding(12).frame(width: 200, alignment: .leading).background(FrostedBackground())
|
|
81
|
+
}
|
|
75
82
|
}
|
|
76
|
-
.
|
|
83
|
+
.padding(.horizontal, 12).padding(.vertical, 10)
|
|
84
|
+
.frame(width: width, height: 60)
|
|
85
|
+
.background(FrostedBackground(radius: 16, cool: true))
|
|
77
86
|
}
|
|
78
|
-
.padding(
|
|
79
|
-
.frame(width: 260)
|
|
87
|
+
.padding(8)
|
|
80
88
|
}
|
|
81
89
|
|
|
90
|
+
private var transcript: String {
|
|
91
|
+
if !store.engine.liveTranscriptionText.isEmpty { return store.engine.liveTranscriptionText }
|
|
92
|
+
if store.engine.captureIsActive { return store.engine.isPaused ? "Paused" : "Listening…" }
|
|
93
|
+
if let blocked = store.engine.blockedReason { return blocked }
|
|
94
|
+
if store.engine.isTranscribing { return store.engine.statusMessage }
|
|
95
|
+
return store.engine.recentTranscriptions.first?.displayText ?? "Your words appear here as you speak…"
|
|
96
|
+
}
|
|
82
97
|
private var statusColor: Color {
|
|
83
98
|
if store.engine.captureIsActive { return .red }
|
|
84
|
-
// The popover repeats the menu-bar signal rather than relying on the caption text, which
|
|
85
|
-
// is rendered `.secondary` and reads as an aside.
|
|
86
99
|
return presentation.isBlocked ? .orange : .accentColor
|
|
87
100
|
}
|
|
88
|
-
|
|
101
|
+
private var recordButtonTitle: String {
|
|
102
|
+
store.engine.captureIsActive ? "Stop and Transcribe" : "Start Recording"
|
|
103
|
+
}
|
|
104
|
+
private var recordButtonIcon: String {
|
|
105
|
+
store.engine.captureIsActive ? "stop.fill" : "mic.fill"
|
|
106
|
+
}
|
|
107
|
+
private func toggleRecording() {
|
|
108
|
+
if store.engine.captureIsActive {
|
|
109
|
+
store.engine.stopAndTranscribe()
|
|
110
|
+
} else {
|
|
111
|
+
store.beginRecording()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
89
114
|
private var presentation: MenuBarPresentation {
|
|
90
115
|
MenuBarPresentation(
|
|
91
116
|
isRecording: store.engine.isRecording,
|
|
@@ -95,20 +120,105 @@ struct MenuBarStatusView: View {
|
|
|
95
120
|
blockedReason: store.engine.blockedReason
|
|
96
121
|
)
|
|
97
122
|
}
|
|
123
|
+
}
|
|
98
124
|
|
|
99
|
-
|
|
100
|
-
|
|
125
|
+
struct AudioWaveform: View {
|
|
126
|
+
let level: Double
|
|
127
|
+
let recording: Bool
|
|
128
|
+
var body: some View {
|
|
129
|
+
Canvas { context, size in
|
|
130
|
+
let count = 29
|
|
131
|
+
let step = size.width / CGFloat(count)
|
|
132
|
+
for i in 0..<count {
|
|
133
|
+
let envelope = 0.2 + 0.8 * abs(sin(Double(i) * 1.73))
|
|
134
|
+
let height = recording ? max(4, CGFloat(level * envelope) * size.height) : 4
|
|
135
|
+
let rect = CGRect(x: CGFloat(i) * step, y: (size.height - height) / 2, width: 2.5, height: height)
|
|
136
|
+
context.fill(Path(roundedRect: rect, cornerRadius: 1.25), with: .color(recording && i < 10 ? Theme.recordRed : .gray.opacity(0.55)))
|
|
137
|
+
}
|
|
138
|
+
}
|
|
101
139
|
}
|
|
140
|
+
}
|
|
102
141
|
|
|
103
|
-
|
|
104
|
-
|
|
142
|
+
struct RecentPastesView: View {
|
|
143
|
+
@ObservedObject var store: RecordingsStore
|
|
144
|
+
let close: () -> Void
|
|
145
|
+
var body: some View {
|
|
146
|
+
VStack(spacing: 12) {
|
|
147
|
+
HStack {
|
|
148
|
+
Text("Recent pastes").font(.system(size: 13, weight: .semibold))
|
|
149
|
+
Spacer()
|
|
150
|
+
Button("Clear all") { store.engine.clearRecentPastes() }
|
|
151
|
+
.buttonStyle(.plain).font(.system(size: 11)).padding(.horizontal, 14).padding(.vertical, 8)
|
|
152
|
+
.background(GlassInset())
|
|
153
|
+
Button(action: close) { Image(systemName: "xmark").font(.system(size: 12)) }
|
|
154
|
+
.buttonStyle(.plain).padding(.leading, 10).accessibilityLabel("Close recent pastes")
|
|
155
|
+
}.padding(.horizontal, 10)
|
|
156
|
+
VStack(spacing: 0) {
|
|
157
|
+
HStack(spacing: 0) {
|
|
158
|
+
Text("App").frame(width: 100, alignment: .leading)
|
|
159
|
+
Text("Location").frame(width: 100, alignment: .leading)
|
|
160
|
+
Text("Pasted content").frame(maxWidth: .infinity, alignment: .leading)
|
|
161
|
+
Text("Time").frame(width: 76, alignment: .leading)
|
|
162
|
+
Text("Status").frame(width: 92, alignment: .leading)
|
|
163
|
+
Spacer().frame(width: 24)
|
|
164
|
+
}.font(.system(size: 11)).foregroundStyle(.secondary).padding(10)
|
|
165
|
+
Divider().opacity(0.5)
|
|
166
|
+
if store.engine.recentPastes.isEmpty {
|
|
167
|
+
Spacer(); Text("Your pastes from this session appear here.").foregroundStyle(.secondary); Spacer()
|
|
168
|
+
} else {
|
|
169
|
+
ScrollView {
|
|
170
|
+
LazyVStack(spacing: 0) {
|
|
171
|
+
ForEach(store.engine.recentPastes) { paste in
|
|
172
|
+
HStack(spacing: 0) {
|
|
173
|
+
HStack(spacing: 10) {
|
|
174
|
+
Image(nsImage: appIcon(paste.bundleIdentifier)).resizable().frame(width: 22, height: 22)
|
|
175
|
+
Text(paste.appName).lineLimit(1)
|
|
176
|
+
}.frame(width: 100, alignment: .leading)
|
|
177
|
+
Text(paste.location).foregroundStyle(.secondary).frame(width: 100, alignment: .leading)
|
|
178
|
+
Text(paste.text).lineLimit(1).frame(maxWidth: .infinity, alignment: .leading)
|
|
179
|
+
Text(paste.timestamp.relativeDescription).foregroundStyle(.secondary).frame(width: 76, alignment: .leading)
|
|
180
|
+
Label(paste.status, systemImage: paste.verified ? "checkmark.circle.fill" : "questionmark.circle")
|
|
181
|
+
.labelStyle(PasteStatusStyle(verified: paste.verified)).frame(width: 92, alignment: .leading)
|
|
182
|
+
Menu {
|
|
183
|
+
Button("Copy pasted text") {
|
|
184
|
+
NSPasteboard.general.clearContents()
|
|
185
|
+
NSPasteboard.general.setString(paste.text, forType: .string)
|
|
186
|
+
}
|
|
187
|
+
} label: { Image(systemName: "ellipsis") }
|
|
188
|
+
.menuStyle(.borderlessButton).menuIndicator(.hidden).frame(width: 24)
|
|
189
|
+
}.font(.system(size: 12)).padding(.horizontal, 10).padding(.vertical, 10)
|
|
190
|
+
Divider().opacity(0.35)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}.background(GlassInset(radius: 10))
|
|
196
|
+
}.padding(14).background(FrostedBackground(radius: 17, cool: true))
|
|
197
|
+
}
|
|
198
|
+
private func appURL(_ id: String?) -> URL? { id.flatMap { NSWorkspace.shared.urlForApplication(withBundleIdentifier: $0) } }
|
|
199
|
+
private func appName(_ id: String?) -> String { appURL(id)?.deletingPathExtension().lastPathComponent ?? "Application" }
|
|
200
|
+
private func appIcon(_ id: String?) -> NSImage {
|
|
201
|
+
appURL(id).map { NSWorkspace.shared.icon(forFile: $0.path) } ?? NSImage(systemSymbolName: "app", accessibilityDescription: nil)!
|
|
105
202
|
}
|
|
203
|
+
}
|
|
106
204
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
205
|
+
private struct PasteStatusStyle: LabelStyle {
|
|
206
|
+
let verified: Bool
|
|
207
|
+
func makeBody(configuration: Configuration) -> some View {
|
|
208
|
+
HStack(spacing: 8) {
|
|
209
|
+
configuration.icon.foregroundStyle(verified ? .green : .orange).font(.system(size: 15))
|
|
210
|
+
configuration.title.foregroundStyle(.secondary)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
struct PanelPointer: Shape {
|
|
216
|
+
func path(in rect: CGRect) -> Path {
|
|
217
|
+
Path { path in
|
|
218
|
+
path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
|
|
219
|
+
path.addLine(to: CGPoint(x: rect.midX, y: rect.minY))
|
|
220
|
+
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
|
|
221
|
+
path.closeSubpath()
|
|
112
222
|
}
|
|
113
223
|
}
|
|
114
224
|
}
|