@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.
Files changed (148) hide show
  1. package/README.md +279 -43
  2. package/bun.lock +37 -38
  3. package/contracts/v1/fixtures.json +1206 -0
  4. package/dist/cli/index.js +1909 -619
  5. package/dist/cli/macos-shortcut.d.ts +2 -2
  6. package/dist/contracts/hosted-v1.d.ts +105 -0
  7. package/dist/contracts/hosted-v1.d.ts.map +1 -0
  8. package/dist/contracts/hosted-v1.js +41 -0
  9. package/dist/contracts/stream-v1.d.ts +117 -0
  10. package/dist/contracts/stream-v1.d.ts.map +1 -0
  11. package/dist/contracts/stream-v1.js +35 -0
  12. package/dist/db/database.d.ts.map +1 -1
  13. package/dist/db/pg-migrations.d.ts.map +1 -1
  14. package/dist/db/recordings.d.ts +6 -0
  15. package/dist/db/recordings.d.ts.map +1 -1
  16. package/dist/hosted/index.d.ts +58 -0
  17. package/dist/hosted/index.d.ts.map +1 -0
  18. package/dist/hosted/index.js +266 -0
  19. package/dist/hosted/transport.d.ts +36 -0
  20. package/dist/hosted/transport.d.ts.map +1 -0
  21. package/dist/hosted-v1-aavn7ktb.js +4114 -0
  22. package/dist/hosted-v1-gdr9extc.js +84 -0
  23. package/dist/http/client.d.ts +183 -22
  24. package/dist/http/client.d.ts.map +1 -1
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2879 -2288
  28. package/dist/lib/audio-artifact-storage.d.ts +98 -0
  29. package/dist/lib/audio-artifact-storage.d.ts.map +1 -0
  30. package/dist/lib/capture-probe.d.ts +3 -3
  31. package/dist/lib/capture-probe.d.ts.map +1 -1
  32. package/dist/lib/config.d.ts.map +1 -1
  33. package/dist/lib/local-opt-in.d.ts +132 -0
  34. package/dist/lib/local-opt-in.d.ts.map +1 -0
  35. package/dist/lib/macos-bundle.d.ts +1 -1
  36. package/dist/lib/persistence-probe.d.ts +56 -17
  37. package/dist/lib/persistence-probe.d.ts.map +1 -1
  38. package/dist/lib/transcriber.d.ts.map +1 -1
  39. package/dist/mcp/index.d.ts.map +1 -1
  40. package/dist/mcp/index.js +1713 -459
  41. package/dist/sdk/index.d.ts +13 -5
  42. package/dist/sdk/index.d.ts.map +1 -1
  43. package/dist/sdk/index.js +904 -1
  44. package/dist/sdk/resolve.d.ts +53 -0
  45. package/dist/sdk/resolve.d.ts.map +1 -0
  46. package/dist/sdk/v1.generated.d.ts +6 -0
  47. package/dist/sdk/v1.generated.d.ts.map +1 -1
  48. package/dist/server/client-ip.d.ts +39 -0
  49. package/dist/server/client-ip.d.ts.map +1 -0
  50. package/dist/server/cloud-config.d.ts +12 -0
  51. package/dist/server/cloud-config.d.ts.map +1 -1
  52. package/dist/server/cloud-readiness.d.ts.map +1 -1
  53. package/dist/server/cloud.d.ts +20 -2
  54. package/dist/server/cloud.d.ts.map +1 -1
  55. package/dist/server/index.js +590 -287
  56. package/dist/server/migrate-command.d.ts +1 -1
  57. package/dist/server/migrate-command.d.ts.map +1 -1
  58. package/dist/server/openapi.d.ts +21 -0
  59. package/dist/server/openapi.d.ts.map +1 -1
  60. package/dist/server/repo.d.ts.map +1 -1
  61. package/dist/server/serve.d.ts.map +1 -1
  62. package/dist/storage.d.ts +2 -2
  63. package/dist/storage.d.ts.map +1 -1
  64. package/dist/storage.js +1358 -874
  65. package/dist/store.d.ts +12 -3
  66. package/dist/store.d.ts.map +1 -1
  67. package/dist/types/index.d.ts +15 -0
  68. package/dist/types/index.d.ts.map +1 -1
  69. package/docs/hosted-sdk.md +71 -0
  70. package/docs/wire-contracts.md +24 -0
  71. package/package.json +33 -11
  72. package/packaging/macos/build_release_pkg.sh +3 -3
  73. package/packaging/macos/managed_bootstrap.sh +4 -4
  74. package/packaging/macos/scripts/postinstall +2 -2
  75. package/packaging/macos/scripts/preinstall +2 -2
  76. package/scripts/build_companion_cli.sh +7 -1
  77. package/scripts/ci-linux-suite.ts +23 -13
  78. package/scripts/install_macos_app.sh +27 -24
  79. package/scripts/macos_artifact.ts +60 -49
  80. package/scripts/migrate.ts +4 -20
  81. package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
  82. package/scripts/native/recordings_fs_guard.c +36 -4
  83. package/scripts/native-core-receipt.py +171 -0
  84. package/scripts/native_fs_guard.ts +2 -0
  85. package/scripts/release-suite-gate.ts +227 -150
  86. package/scripts/resolve_tailscale_cli.sh +24 -3
  87. package/scripts/smoke_macos_app.sh +21 -21
  88. package/src/native/Recordings/App/ContentView.swift +53 -68
  89. package/src/native/Recordings/App/MenuBarStatusView.swift +171 -61
  90. package/src/native/Recordings/App/RecordWorkspaceView.swift +47 -449
  91. package/src/native/Recordings/App/RecorderSettingsView.swift +75 -0
  92. package/src/native/Recordings/App/RecordingDetailView.swift +3 -4
  93. package/src/native/Recordings/App/RecordingsApp.swift +185 -9
  94. package/src/native/Recordings/App/RecordingsListView.swift +60 -112
  95. package/src/native/Recordings/App/RecordingsStore.swift +124 -109
  96. package/src/native/Recordings/App/Theme.swift +73 -61
  97. package/src/native/Recordings/Package.swift +1 -0
  98. package/src/native/Recordings/RecordingsLib/BlockingOperation.swift +29 -0
  99. package/src/native/Recordings/RecordingsLib/CaptureMonitor.swift +65 -0
  100. package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
  101. package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +75 -21
  102. package/src/native/Recordings/RecordingsLib/ProjectStore.swift +4 -4
  103. package/src/native/Recordings/RecordingsLib/Recording.swift +14 -2
  104. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +539 -89
  105. package/src/native/Recordings/RecordingsLib/RecordingGlassBackground.swift +34 -0
  106. package/src/native/Recordings/RecordingsLib/RecordingPasteTarget.swift +114 -0
  107. package/src/native/Recordings/RecordingsLib/RecordingProvider.swift +112 -0
  108. package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +17 -10
  109. package/src/native/Recordings/RecordingsLib/ServiceAPIConfiguration.swift +107 -0
  110. package/src/native/Recordings/RecordingsLib/ServiceConnectionView.swift +76 -0
  111. package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -149
  112. package/src/native/Recordings/RecordingsLib/SettingsWindowController.swift +48 -0
  113. package/src/native/Recordings/RecordingsTests/BlockingOperationTests.swift +85 -0
  114. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +454 -76
  115. package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +43 -38
  116. package/src/native/Recordings/RecordingsTests/PipeClosureFixture.swift +215 -0
  117. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +10 -10
  118. package/src/native/Recordings/RecordingsTests/RecorderSurfaceTests.swift +60 -0
  119. package/src/native/Recordings/RecordingsTests/RecordingDateTests.swift +26 -0
  120. package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +1 -1
  121. package/src/native/Recordings/RecordingsTests/RecordingFrozenPasteTargetTests.swift +50 -0
  122. package/src/native/Recordings/RecordingsTests/RecordingPasteTargetTrackerTests.swift +48 -0
  123. package/src/native/Recordings/RecordingsTests/RecordingProviderTests.swift +308 -0
  124. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
  125. package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +123 -21
  126. package/src/native/Recordings/RecordingsTests/ServiceAPIConfigurationTests.swift +75 -0
  127. package/src/native/Recordings/RecordingsTests/SettingsWindowControllerTests.swift +28 -0
  128. package/src/native/Recordings/RecordingsTests/TestHomeDirectory.swift +5 -1
  129. package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
  130. package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
  131. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
  132. package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
  133. package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
  134. package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
  135. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
  136. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
  137. package/src/native/Recordings/build.sh +8 -7
  138. package/dist/__tests__/helpers/installer-guard-execution.d.ts +0 -22
  139. package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +0 -1
  140. package/dist/__tests__/helpers/installer-preflight.d.ts +0 -22
  141. package/dist/__tests__/helpers/installer-preflight.d.ts.map +0 -1
  142. package/dist/__tests__/helpers/native-fs-guard.d.ts +0 -2
  143. package/dist/__tests__/helpers/native-fs-guard.d.ts.map +0 -1
  144. package/dist/__tests__/helpers/source-assertions.d.ts +0 -171
  145. package/dist/__tests__/helpers/source-assertions.d.ts.map +0 -1
  146. package/dist/__tests__/preload.d.ts +0 -2
  147. package/dist/__tests__/preload.d.ts.map +0 -1
  148. package/src/native/Recordings/App/SidebarView.swift +0 -212
@@ -51,7 +51,7 @@ enum RecordingsBootstrapPreflight {
51
51
  let manifest = try JSONDecoder().decode(ReleaseManifest.self, from: manifestData)
52
52
  try validateManifest(manifest, payload: payload)
53
53
 
54
- let app = arguments.payloadRoot + "/Applications/HasnaRecordings.app"
54
+ let app = arguments.payloadRoot + "/Applications/Hasna Recordings.app"
55
55
  let client = app + "/" + RecordingsUpdateConstants.updateClientRelativePath
56
56
  let broker = arguments.payloadRoot + "/Library/PrivilegedHelperTools/com.hasna.recordings.updater"
57
57
  let artifactVerifier = arguments.payloadRoot
@@ -7,7 +7,7 @@ import RecordingsUpdateProtocol
7
7
  /// operation; first installs use exclusive rename semantics and can never replace a
8
8
  /// leaf created concurrently in `/Applications`.
9
9
  final class ApplicationNamespace {
10
- private static let liveLeaf = "HasnaRecordings.app"
10
+ private static let liveLeaf = "Hasna Recordings.app"
11
11
  private static let previousLeaf = "previous.app"
12
12
  private static let failedCandidateLeaf = "failed-candidate.app"
13
13
 
@@ -22,7 +22,7 @@ final class ApplicationNamespace {
22
22
  .deletingLastPathComponent
23
23
  guard journal.applicationPath == RecordingsUpdateConstants.applicationPath,
24
24
  journal.candidateApplicationPath == expectedTransactionDirectory
25
- + "/candidate/HasnaRecordings.app",
25
+ + "/candidate/Hasna Recordings.app",
26
26
  journal.previousApplicationPath == nil ||
27
27
  journal.previousApplicationPath == expectedTransactionDirectory + "/previous.app"
28
28
  else {
@@ -276,7 +276,7 @@ final class UpdateBrokerSession: NSObject, RecordingsUpdateXPCProtocol {
276
276
  guard mkdir(candidateRoot, 0o700) == 0 else {
277
277
  throw BrokerOperationError.candidateRejected
278
278
  }
279
- let candidateApplication = candidateRoot + "/HasnaRecordings.app"
279
+ let candidateApplication = candidateRoot + "/Hasna Recordings.app"
280
280
  let verifierIDs = try verifierAccountIDs()
281
281
  try CanonicalTreeCopier.copyApplication(
282
282
  from: verifierOutput,
@@ -22,7 +22,7 @@ enum CanonicalTreeCopier {
22
22
  }
23
23
  defer { Darwin.close(source) }
24
24
  let destinationRoot = URL(fileURLWithPath: rootCandidatePath).deletingLastPathComponent().path
25
- guard URL(fileURLWithPath: rootCandidatePath).lastPathComponent == "HasnaRecordings.app" else {
25
+ guard URL(fileURLWithPath: rootCandidatePath).lastPathComponent == "Hasna Recordings.app" else {
26
26
  throw CanonicalCopyError.nonCanonicalRoot
27
27
  }
28
28
  let destination = Darwin.open(
@@ -253,7 +253,7 @@ struct DurableInstallJournal {
253
253
  let leaf = URL(fileURLWithPath: transactionDirectory).lastPathComponent
254
254
  guard leaf.hasPrefix("transaction-") else { throw InstallJournalError.invalidJournal }
255
255
  let transactionID = String(leaf.dropFirst("transaction-".count))
256
- let expectedCandidate = transactionDirectory + "/candidate/HasnaRecordings.app"
256
+ let expectedCandidate = transactionDirectory + "/candidate/Hasna Recordings.app"
257
257
  let activationPhases: Set<String> = [
258
258
  "prepared", "launch-barrier-pending", "launch-barrier-held",
259
259
  "swap-pending", "swapped", "previous-retaining",
@@ -201,8 +201,8 @@ struct ActivationRecoveryPolicyTests {
201
201
  "Contents/Helpers/recordings": 493,
202
202
  "Contents/Helpers/recordings-update-client": 493
203
203
  },
204
- "candidate_application_path": "/Library/Application Support/Hasna/Recordings/Updates/transaction-00000000-0000-0000-0000-000000000001/candidate/HasnaRecordings.app",
205
- "application_path": "/Applications/HasnaRecordings.app"
204
+ "candidate_application_path": "/Library/Application Support/Hasna/Recordings/Updates/transaction-00000000-0000-0000-0000-000000000001/candidate/Hasna Recordings.app",
205
+ "application_path": "/Applications/Hasna Recordings.app"
206
206
  }
207
207
  """#.utf8)
208
208
  let journal = try JSONDecoder().decode(BrokerInstallJournal.self, from: legacyJSON)
@@ -7,7 +7,7 @@ public enum RecordingsUpdateConstants {
7
7
  public static let rootMaintenanceSupported = false
8
8
  public static let keyRotationSupported = false
9
9
  public static let machServiceName = "com.hasna.recordings.updater"
10
- public static let applicationPath = "/Applications/HasnaRecordings.app"
10
+ public static let applicationPath = "/Applications/Hasna Recordings.app"
11
11
  public static let brokerExecutablePath = "/Library/PrivilegedHelperTools/com.hasna.recordings.updater"
12
12
  public static let stateRoot = "/Library/Application Support/Hasna/Recordings/Updates"
13
13
  public static let trustRoot = "/Library/Application Support/Hasna/Recordings/Trust"
@@ -729,7 +729,7 @@ int recordings_copy_canonical_application_tree(
729
729
  if (entry == NULL) break;
730
730
  if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
731
731
  visible_entries += 1;
732
- if (strcmp(entry->d_name, "HasnaRecordings.app") != 0) only_application = false;
732
+ if (strcmp(entry->d_name, "Hasna Recordings.app") != 0) only_application = false;
733
733
  }
734
734
  int status = errno;
735
735
  closedir(directory);
@@ -739,7 +739,7 @@ int recordings_copy_canonical_application_tree(
739
739
  status = copy_directory_at(
740
740
  verifier_output_directory_descriptor,
741
741
  root_candidate_directory_descriptor,
742
- "HasnaRecordings.app",
742
+ "Hasna Recordings.app",
743
743
  verifier_user_id,
744
744
  &budget
745
745
  );
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # Build, sign, notarize, and finalize a HasnaRecordings.app artifact.
2
+ # Build, sign, notarize, and finalize a Hasna Recordings.app artifact.
3
3
  # Usage: ./build.sh [debug|local] | ./build.sh release <initial-bootstrap|app-update>
4
4
 
5
5
  set -euo pipefail
@@ -120,11 +120,11 @@ if [ "$VARIANT" = "bar" ]; then
120
120
  fi
121
121
  # Fleet naming rule (knowledge k_msxd5rz3_jfvl3i): every Hasna macOS app is
122
122
  # Hasna<Name>.app with 'Hasna' at the beginning. The bundle is named
123
- # HasnaRecordings.app for BOTH variants (full and bar); the 'bar' is a variant, never
123
+ # Hasna Recordings.app for BOTH variants (full and bar); the 'bar' is a variant, never
124
124
  # a separate app name. Bundle identifier stays com.hasna.recordings so TCC keeps
125
125
  # keying on bundle id + signing identity, and the executable inside the bundle stays
126
126
  # "Recordings".
127
- APP_BUNDLE_NAME="HasnaRecordings.app"
127
+ APP_BUNDLE_NAME="Hasna Recordings.app"
128
128
  readonly APP_BUNDLE_NAME
129
129
  APP_BASENAME="${APP_BUNDLE_NAME%.app}"
130
130
  PLIST_BUDDY="$(select_executable "/usr/libexec/PlistBuddy" "${RECORDINGS_TEST_PLIST_BUDDY_EXECUTABLE:-${PLIST_BUDDY:-}}")"
@@ -505,7 +505,8 @@ generate_and_verify_native_fs_guard() {
505
505
  import { createRequire } from "node:module";
506
506
  const addon = createRequire(import.meta.url)(process.argv[1]);
507
507
  const expected = [
508
- "chmodHandle", "close", "copyRegularNoReplaceAt", "fsyncHandle",
508
+ "chmodHandle", "close", "copyRegularNoReplaceAt", "duplicateDirectoryDescriptor",
509
+ "fsyncHandle", "handleHasNoExtendedAcl",
509
510
  "linkNoReplaceAt", "mkdirAt", "openDirAt", "openRegularAt", "openTrustedHome",
510
511
  "readDir", "readRegularAt", "removeTreeAt", "removeTreeHandleAt",
511
512
  "renameHandleNoReplaceAt", "renameNoReplaceAt", "renameReplaceAt",
@@ -1256,7 +1257,7 @@ fi
1256
1257
 
1257
1258
  generate_and_verify_native_fs_guard
1258
1259
 
1259
- echo "Building HasnaRecordings.app ($MODE)..."
1260
+ echo "Building Hasna Recordings.app ($MODE)..."
1260
1261
  if [ "$MODE" = "release" ]; then
1261
1262
  OUTPUT_BUILD_DIR="$BUILD_ROOT/release-output"
1262
1263
  else
@@ -1992,7 +1993,7 @@ if [ "$MODE" = "local" ]; then
1992
1993
  fi
1993
1994
 
1994
1995
  verify_signed_code "$HELPERS/recordings" "Companion CLI"
1995
- verify_signed_code "$APP_DIR" "HasnaRecordings.app"
1996
+ verify_signed_code "$APP_DIR" "Hasna Recordings.app"
1996
1997
  ARTIFACT_BASENAME="${APP_BASENAME}-${VERSION}-macos-${RELEASE_SUBTYPE}"
1997
1998
  NOTARY_ARCHIVE="$OUTPUT_BUILD_DIR/${ARTIFACT_BASENAME}-notarization.zip"
1998
1999
  FINAL_ARCHIVE="$OUTPUT_BUILD_DIR/${ARTIFACT_BASENAME}.zip"
@@ -2044,7 +2045,7 @@ run_xcrun stapler validate "$APP_DIR"
2044
2045
  run_release_sensitive_tool "$SPCTL_EXECUTABLE" --assess --type execute --verbose=2 "$APP_DIR"
2045
2046
  run_release_sensitive_tool "$SYSPOLICY_CHECK_EXECUTABLE" distribution "$APP_DIR"
2046
2047
  verify_signed_code "$HELPERS/recordings" "Companion CLI"
2047
- verify_signed_code "$APP_DIR" "HasnaRecordings.app"
2048
+ verify_signed_code "$APP_DIR" "Hasna Recordings.app"
2048
2049
  run_codesign --verify --deep --strict --verbose=2 "$APP_DIR"
2049
2050
 
2050
2051
  run_release_sensitive_tool "$DITTO_EXECUTABLE" -c -k --sequesterRsrc --keepParent "$APP_DIR" "$FINAL_ARCHIVE"
@@ -1,22 +0,0 @@
1
- export declare const EXISTING_IDENTITY_SHA256: string;
2
- export declare const CANDIDATE_IDENTITY_SHA256: string;
3
- export declare const MANIFEST_SHA256: string;
4
- export type ArtifactPolicy = "local-only" | "release";
5
- export type IncompatibleDirection = "installed-requirement-vs-candidate" | "candidate-requirement-vs-installed" | "both";
6
- export type GuardExecutionOptions = {
7
- identityMigration: boolean;
8
- incompatibleDirection?: IncompatibleDirection;
9
- artifactPolicy?: ArtifactPolicy;
10
- extraArguments?: string[];
11
- };
12
- export type GuardExecutionResult = {
13
- exitCode: number;
14
- stdout: string;
15
- stderr: string;
16
- reachedTransaction: boolean;
17
- codesignInvocations: string[];
18
- bunInvocations: string[];
19
- unstubbedInvocations: string[];
20
- };
21
- export declare function runInstallerToIdentityGuard(options: GuardExecutionOptions): GuardExecutionResult;
22
- //# sourceMappingURL=installer-guard-execution.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"installer-guard-execution.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/installer-guard-execution.ts"],"names":[],"mappings":"AA+DA,eAAO,MAAM,wBAAwB,QAAiB,CAAC;AACvD,eAAO,MAAM,yBAAyB,QAAiB,CAAC;AACxD,eAAO,MAAM,eAAe,QAAiB,CAAC;AAsB9C,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,SAAS,CAAC;AAuFtD,MAAM,MAAM,qBAAqB,GAE7B,oCAAoC,GAGpC,oCAAoC,GAEpC,MAAM,CAAC;AAEX,MAAM,MAAM,qBAAqB,GAAG;IAKlC,iBAAiB,EAAE,OAAO,CAAC;IAG3B,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;IAK9C,cAAc,CAAC,EAAE,cAAc,CAAC;IAEhC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IAKf,kBAAkB,EAAE,OAAO,CAAC;IAG5B,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAG9B,cAAc,EAAE,MAAM,EAAE,CAAC;IAMzB,oBAAoB,EAAE,MAAM,EAAE,CAAC;CAChC,CAAC;AAEF,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,qBAAqB,GAC7B,oBAAoB,CA2StB"}
@@ -1,22 +0,0 @@
1
- export declare const POLICY_RELATIVE_PATH = "scripts/policy/local-only-approved-targets.txt";
2
- export declare const READER_RELATIVE_PATH = "scripts/read_local_only_targets.sh";
3
- export declare const IDENTITY_GUARD_RELATIVE_PATH = "scripts/enforce_identity_migration.sh";
4
- export declare const readRepositoryFile: (relativePath: string) => string;
5
- export type InstallerPreflightOptions = {
6
- artifactPolicy?: "release" | "local-only";
7
- approvedTarget?: string;
8
- hostname?: string;
9
- extraArguments?: string[];
10
- policyContents?: string | null;
11
- removeReader?: boolean;
12
- removeIdentityGuard?: boolean;
13
- identityGuardContents?: string;
14
- symlinkIdentityGuard?: boolean;
15
- environment?: Record<string, string>;
16
- };
17
- export declare function runInstallerPreflight(options?: InstallerPreflightOptions): {
18
- exitCode: number;
19
- stdout: string;
20
- stderr: string;
21
- };
22
- //# sourceMappingURL=installer-preflight.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"installer-preflight.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/installer-preflight.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,oBAAoB,mDAAmD,CAAC;AACrF,eAAO,MAAM,oBAAoB,uCAAuC,CAAC;AACzE,eAAO,MAAM,4BAA4B,0CAA0C,CAAC;AAEpF,eAAO,MAAM,kBAAkB,GAAI,cAAc,MAAM,KAAG,MACA,CAAC;AAc3D,MAAM,MAAM,yBAAyB,GAAG;IACtC,cAAc,CAAC,EAAE,SAAS,GAAG,YAAY,CAAC;IAC1C,cAAc,CAAC,EAAE,MAAM,CAAC;IAGxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAG9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAG/B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAG/B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC,CAAC;AAEF,wBAAgB,qBAAqB,CACnC,OAAO,GAAE,yBAA8B,GACtC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CA8GtD"}
@@ -1,2 +0,0 @@
1
- export declare function ensureNativeFsGuardAddon(repositoryRoot?: string): string;
2
- //# sourceMappingURL=native-fs-guard.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"native-fs-guard.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/native-fs-guard.ts"],"names":[],"mappings":"AA2CA,wBAAgB,wBAAwB,CAAC,cAAc,SAAuC,GAAG,MAAM,CActG"}
@@ -1,171 +0,0 @@
1
- /**
2
- * Shared assertions for contract tests that read source text.
3
- *
4
- * These live here rather than inside one suite because the defect they exist to prevent is
5
- * repo-wide: a sweep of every `*.test.ts` found 40 ordering assertions written as
6
- * `indexOf(a) < indexOf(b)`, across 11 files. `indexOf` answers -1 when the needle is absent and
7
- * `-1 < anything` is true, so such an assertion PASSES when the thing being ordered is DELETED.
8
- * The same hole exists in `.slice(indexOf(...), indexOf(...))` region bounds, where a -1 silently
9
- * slices from the end of the file or to its start — a region assertion over the wrong text, or over
10
- * none of it, reads exactly like a satisfied one.
11
- *
12
- * ---------------------------------------------------------------------------------------------
13
- * BEFORE YOU BUILD A MUTATION BATTERY: three suites are RED ON A CONTENDED STATION, not on Linux.
14
- *
15
- * A mutation battery is evidence only when its clean control is GREEN. Include a suite that was
16
- * already failing and the run was non-zero before you changed anything, so every mutation "looks
17
- * caught" and every verdict is manufactured. That already produced one wrong all-clear here.
18
- *
19
- * These are the three suites it happens to. NO PASS/FAIL SPLIT IS RECORDED FOR THEM ON PURPOSE —
20
- * see below. `macos-app-lifecycle.test.ts` has 140 tests, `native-app-companion-contract.test.ts`
21
- * has 14, `config.test.ts` has 44, and how many of those fail is a property of the MACHINE:
22
- *
23
- * src/__tests__/macos-app-lifecycle.test.ts
24
- * src/__tests__/native-app-companion-contract.test.ts
25
- * src/__tests__/config.test.ts
26
- *
27
- * CORRECTED 2026-07-27, and the correction is the useful part. This block used to call them
28
- * "PERMANENTLY RED on Linux" and attribute fixed environmental causes — BSD `stat -f`, a fixture
29
- * port reading `NaN`, a `getDataDir` HOME-ancestor assumption. The first CI run this repository
30
- * ever had (run 30302342895, ubuntu-24.04) re-ran all three on a clean single-tenant runner and
31
- * every one of them PASSED:
32
- *
33
- * macos-app-lifecycle 140 pass / 0 fail (358.85s)
34
- * native-app-companion-contract 14 pass / 0 fail ( 4.50s)
35
- * config 44 pass / 0 fail ( 0.10s)
36
- *
37
- * So the cause is not the platform. Measure on a quiet machine, or in CI, before recording a suite
38
- * as red. There were TWO independent station-local causes, and every earlier version of this comment
39
- * named only one of them. In a 92-fail run of `macos-app-lifecycle.test.ts` on this station the
40
- * failure messages broke down as 38 × `Home ancestor has an unexpected owner.`, 22 × FIFO
41
- * synchronisation timeout, 24 × ENOENT on a fixture marker. The first cause is now fixed in the
42
- * fixture and only the second remains; do not collapse them into one, because a quiet machine with
43
- * `FORCE_COLOR` set still showed all 38 aborts, so "measure on a quiet machine" was necessary and
44
- * not sufficient:
45
- *
46
- * 1. `FORCE_COLOR` — **FIXED IN THE FIXTURE, no longer a live cause.** The `stat` stub answered
47
- * `%u` by shelling out to `bun -e '… console.log(statSync(…).uid)'`
48
- * (`macos-app-lifecycle.test.ts:217`) and spread `...Bun.env` into the installer, so with
49
- * `FORCE_COLOR` set Bun COLOURED the number: the installer compared `\e[0m\e[33m1000\e[0m`
50
- * against `id -u`'s `1000` at `install_macos_app.sh:143` and aborted before reaching any gate.
51
- * `NO_COLOR=1` did NOT help — FORCE_COLOR wins in Bun. The stub now writes bare integers with
52
- * `process.stdout.write` and `unset FORCE_COLOR`s its own children, which takes that message
53
- * from 38 to 0 in place, positive-controlled (the same grep still finds 38 in the pre-fix log).
54
- * Nothing about ancestor MODE was ever involved: `verify_secure_parent` and
55
- * `verify_safe_home_ancestor` each `stat` only the one path handed to them, the sole call is
56
- * `verify_safe_home_ancestor "$HOME"`, and the stub hardcodes every `%Lp` answer anyway.
57
- * Kept here rather than deleted because it is the reason this file's split moved, and because
58
- * the same trap recurs in any stub that parses `console.log` of a NUMBER: only strings are
59
- * left uncoloured.
60
- * 2. CONTENTION. The station routinely runs several full recordings suites at once out of
61
- * different worktrees, and this suite scans a shared /tmp — the hazard this very comment warns
62
- * about below. Those were FIFO timeouts at an internal 5000ms budget; the FIFO helper now uses
63
- * the suite's configured timeout too, so a raised test budget can distinguish load sensitivity
64
- * from a regression. With `FORCE_COLOR` unset the residual failures scaled with load: 132 pass /
65
- * 8 fail at load ~20, 114 pass / 26 fail at load 44-60 on the same commit, which is why no split
66
- * belongs here either. GitHub Actions sets neither `FORCE_COLOR` nor a competing suite, which
67
- * is why both causes were absent from the only clean measurement.
68
- *
69
- * WHY NO SPLIT IS RECORDED. Every split ever written here has gone stale, including two written as
70
- * corrections. On one unchanged tree, three consecutive runs measured 48/92, 48/92, 49/91; the
71
- * single test that flips is `runtime smoke timeout does not wait forever on a live open process`
72
- * (`macos-app-lifecycle.test.ts:3594`), which races a hardcoded internal `Bun.sleep(2_000)` that no
73
- * `--timeout` flag reaches either. So a count comparison across two trees shows a phantom delta
74
- * from this file alone — which is the concrete reason for the rule below: compare failing test
75
- * NAMES, never counts. Two trees whose failing NAME SETS are identical are identical regardless of
76
- * what the totals say.
77
- *
78
- * `@hasna/events` MUST resolve 0.1.11, as `bun.lock` pins it. A plain `bun install` pulls 0.1.14,
79
- * which dropped a shipped CLI command inside the patch range and fails `cli.test.ts` — and it can
80
- * drift back mid-session, so re-check it before quoting any cross-tree comparison.
81
- *
82
- * Corollary, also corrected: this repo NOW HAS CI. The repo-root
83
- * `.github/workflows/ci.yml` gates the whole TypeScript suite with no exemptions on every
84
- * pull request, so these suites are no longer gated only by somebody remembering to run
85
- * them. The Swift/C half compiles in CI through the repo-root
86
- * `.github/workflows/recordings-macos.yml` (`verify:ci-native`); see
87
- * `.github/native-known-errors.txt` for what that gate means.
88
- * Compare failing test NAMES, never counts — the suite is nondeterministic at the margin.
89
- * ---------------------------------------------------------------------------------------------
90
- */
91
- /**
92
- * Assert `first` appears before `second`, requiring BOTH to exist.
93
- *
94
- * Use this instead of comparing two `indexOf` results directly. `firstMatch: "last"` selects
95
- * `lastIndexOf` for the first operand, which has the identical -1 hole.
96
- */
97
- export declare function expectOrder(haystack: string, first: string, second: string, options?: {
98
- firstMatch?: "first" | "last";
99
- }): void;
100
- /**
101
- * Slice between two markers, requiring both to exist and the region to be non-trivial.
102
- *
103
- * The length floor matters as much as the -1 checks: `slice(-1)` yields a ONE-CHARACTER string, not
104
- * an empty one, so a `expect(region.length).toBeGreaterThan(0)` control passes on a region that
105
- * contains nothing worth asserting about. Any `not.toContain` over such a region is vacuous.
106
- */
107
- export declare function sliceBetween(source: string, open: string, close: string, options?: {
108
- minimumLength?: number;
109
- }): string;
110
- /**
111
- * Assert a marker occurs exactly once before slicing on it.
112
- *
113
- * A duplicated end marker silently extends a region: `let myPID = ProcessInfo…` occurs twice in
114
- * `RecordingEngine.swift`, so a region bounded by it could stretch ~127 KB and be satisfied by
115
- * copies of the needle from an unrelated function.
116
- */
117
- export declare function sliceBetweenUnique(source: string, open: string, close: string): string;
118
- /** Strip Swift line comments so an assertion about code is not defeated by prose. */
119
- export declare function withoutComments(source: string): string;
120
- /**
121
- * Index just past the `close` matching the `open` at `openIndex`, skipping comments and literals.
122
- *
123
- * Needed because `lastIndexOf("}")` is not brace matching, and the difference is a live defect: an
124
- * early exit written between a decision and its use —
125
- *
126
- * } // the decision table closes here
127
- * guard stillOwnsChangeCount else { return }
128
- * if shouldRestore { … }
129
- *
130
- * — puts a NEARER `}` (the `else` block's) between the two, so a "nothing between them" check
131
- * measured from the last brace saw only whitespace and passed. Counting braces naively fails the
132
- * other way: one `}` inside a string literal, such as `log("settlement }")`, cancels a real opener.
133
- * Both were measured surviving at EXIT=0.
134
- */
135
- export declare function matchingDelimiterIndex(source: string, openIndex: number, open: string, close: string): number;
136
- export declare function withoutAnyComments(source: string): string;
137
- /**
138
- * The arms of a Swift `switch` body, as a mapping from each matched case to its expression.
139
- *
140
- * Order- and grouping-independent, which matters because pinning an arm by its exact TEXT gets both
141
- * directions wrong. It false-positives on a pure reorder — `.deliveredUnverified, .deliveryNotObserved`
142
- * is the same table and failed — and it misses a real defect: splitting one outcome out of a group
143
- * into its own arm with a different expression leaves the pinned needle intact, so
144
- * `.targetUnavailable` could be given `false` while the six-outcome needle still matched.
145
- *
146
- * `body` is the text between the switch's braces, comments already stripped by the caller.
147
- */
148
- export declare function switchArmsByOutcome(body: string): Map<string, string>;
149
- /**
150
- * Evaluate a Swift boolean condition for a given binding of its identifiers.
151
- *
152
- * Folded in from PR #43, which pinned the clipboard-restore guard by evaluating it for BOTH values
153
- * of the decision rather than comparing its text. That is the half worth keeping: an exact
154
- * `toBe("shouldRestore")` on a captured condition kills the inversion, but it also fails on
155
- * `(shouldRestore)` and on a trailing comment — reporting refactors as defects while proving
156
- * nothing about behaviour.
157
- *
158
- * Deliberately narrow and fail-CLOSED: `true`, `false`, `!x`, whole-expression parentheses, and
159
- * identifiers present in `env`. Anything else throws. An added disjunct
160
- * (`shouldRestore || stillOwnsChangeCount`) is an unevaluatable expression, not a passing one —
161
- * which is the behaviour that matters, because that disjunct is exactly how the
162
- * transcript-destroying defect gets reintroduced. Extend the evaluator when a new shape is
163
- * legitimate; do not loosen the assertion.
164
- */
165
- export declare function evaluateSwiftCondition(condition: string, env: Record<string, boolean>): boolean;
166
- /**
167
- * Every Swift source under a root, so an absence claim can be made about the app rather than about
168
- * whichever files were on the reviewer's mind.
169
- */
170
- export declare function swiftSourcesUnder(root: string): Array<[path: string, source: string]>;
171
- //# sourceMappingURL=source-assertions.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"source-assertions.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/source-assertions.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyFG;AAEH;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;CAAO,GAC9C,IAAI,CAON;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAO,GACvC,MAAM,CAWR;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMtF;AAED,qFAAqF;AACrF,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAKtD;AAuED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GACZ,MAAM,CA0BR;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAoEzD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAyBrE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAiC/F;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAIrF"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=preload.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"preload.d.ts","sourceRoot":"","sources":["../../src/__tests__/preload.ts"],"names":[],"mappings":""}
@@ -1,212 +0,0 @@
1
- import SwiftUI
2
- import RecordingsLib
3
-
4
- /// Narrow violet sidebar: Record · Library · Projects · Modes · Machines. White text/tint
5
- /// over the gradient; selected rows get a translucent white highlight (Liquid Glass refracts
6
- /// the violet beneath).
7
- struct SidebarView: View {
8
- @ObservedObject var store: RecordingsStore
9
- @Environment(\.colorScheme) private var colorScheme
10
-
11
- @State private var addingProject = false
12
- @State private var newProjectName = ""
13
-
14
- var body: some View {
15
- ScrollView {
16
- VStack(alignment: .leading, spacing: 18) {
17
- workspaceSection
18
- librarySection
19
- projectsSection
20
- if hasAnyMode { modesSection }
21
- if !store.machines.isEmpty { machinesSection }
22
- Spacer(minLength: 8)
23
- }
24
- .padding(14)
25
- .padding(.top, 24)
26
- }
27
- .scrollContentBackground(.hidden)
28
- .foregroundStyle(.white)
29
- .tint(.white)
30
- }
31
-
32
- // MARK: - Workspace
33
-
34
- private var workspaceSection: some View {
35
- section(title: "Workspace") {
36
- Button {
37
- withAnimation(.spring(response: 0.3, dampingFraction: 0.85)) { store.pane = .record }
38
- } label: {
39
- rowLabel(icon: "mic.fill", label: "Record", count: nil,
40
- selected: store.pane == .record, accentDot: store.engine.captureIsActive)
41
- }
42
- .buttonStyle(.plain)
43
- }
44
- }
45
-
46
- // MARK: - Library
47
-
48
- private var librarySection: some View {
49
- section(title: "Library") {
50
- filterRow(.all, icon: "waveform", label: "All Recordings")
51
- if store.count(for: .noProject) > 0 {
52
- filterRow(.noProject, icon: "tray", label: "No Project")
53
- }
54
- }
55
- }
56
-
57
- // MARK: - Projects
58
-
59
- private var projectsSection: some View {
60
- section(title: "Projects", trailing: {
61
- Button { addingProject = true; newProjectName = "" } label: {
62
- Image(systemName: "plus").font(.system(size: 11, weight: .semibold))
63
- }
64
- .buttonStyle(.plain)
65
- .disabled(!store.projectStore.canMutateProjects)
66
- .help("Add Project")
67
- .popover(isPresented: $addingProject, arrowEdge: .trailing) {
68
- VStack(alignment: .leading, spacing: 10) {
69
- Text("New Project").font(.headline)
70
- TextField("Project name", text: $newProjectName)
71
- .textFieldStyle(.roundedBorder).frame(width: 200)
72
- .onSubmit(commitNewProject)
73
- HStack {
74
- Spacer()
75
- Button("Cancel") { addingProject = false }
76
- Button("Add") { commitNewProject() }
77
- .keyboardShortcut(.defaultAction)
78
- .disabled(newProjectName.trimmingCharacters(in: .whitespaces).isEmpty)
79
- }
80
- }
81
- .padding(14)
82
- }
83
- }) {
84
- if store.projects.isEmpty {
85
- Text("No projects")
86
- .font(.caption).foregroundStyle(.white.opacity(0.5)).padding(.leading, 6)
87
- }
88
- ForEach(store.projects) { project in
89
- filterRow(.project(project.id), icon: "folder", label: project.name)
90
- .disabled(!store.projectStore.canMutateProjects)
91
- .contextMenu {
92
- Button("Delete", role: .destructive) {
93
- do {
94
- try store.projectStore.removeProject(id: project.id)
95
- } catch {
96
- store.operationError = store.projectStore.persistenceError ?? error.localizedDescription
97
- }
98
- }
99
- }
100
- }
101
- }
102
- }
103
-
104
- private func commitNewProject() {
105
- let name = newProjectName.trimmingCharacters(in: .whitespacesAndNewlines)
106
- guard !name.isEmpty else { return }
107
- Task {
108
- do {
109
- try await store.projectStore.addProject(name: name)
110
- addingProject = false
111
- newProjectName = ""
112
- } catch {
113
- store.operationError = store.projectStore.persistenceError
114
- ?? (error as? RecordingsCLI.Failure)?.message
115
- ?? error.localizedDescription
116
- }
117
- }
118
- }
119
-
120
- // MARK: - Modes
121
-
122
- private var hasAnyMode: Bool {
123
- store.count(for: .mode("enhanced")) > 0 || store.count(for: .mode("raw")) > 0
124
- }
125
-
126
- private var modesSection: some View {
127
- section(title: "Modes") {
128
- if store.count(for: .mode("enhanced")) > 0 {
129
- filterRow(.mode("enhanced"), icon: "wand.and.stars", label: "Enhanced")
130
- }
131
- if store.count(for: .mode("raw")) > 0 {
132
- filterRow(.mode("raw"), icon: "text.quote", label: "Raw")
133
- }
134
- }
135
- }
136
-
137
- // MARK: - Machines
138
-
139
- private var machinesSection: some View {
140
- section(title: "Machines") {
141
- if store.count(for: .thisMachine) > 0 {
142
- filterRow(.thisMachine, icon: "desktopcomputer", label: "This Mac")
143
- }
144
- ForEach(otherMachines, id: \.self) { machine in
145
- filterRow(.machine(machine), icon: "macpro.gen3.server", label: machine)
146
- }
147
- }
148
- }
149
-
150
- private var otherMachines: [String] {
151
- store.machines.filter { $0 != store.localMachineID }
152
- }
153
-
154
- // MARK: - Building blocks
155
-
156
- @ViewBuilder
157
- private func section<Trailing: View, Content: View>(
158
- title: String,
159
- @ViewBuilder trailing: () -> Trailing = { EmptyView() },
160
- @ViewBuilder content: () -> Content
161
- ) -> some View {
162
- VStack(alignment: .leading, spacing: 6) {
163
- HStack {
164
- Text(title.uppercased())
165
- .font(.caption2.weight(.semibold))
166
- .foregroundStyle(.white.opacity(0.6))
167
- .padding(.leading, 6)
168
- Spacer()
169
- trailing().foregroundStyle(.white.opacity(0.85)).padding(.trailing, 6)
170
- }
171
- content()
172
- }
173
- }
174
-
175
- private func filterRow(_ target: LibraryFilter, icon: String, label: String) -> some View {
176
- let selected = store.pane == .library && store.filter == target
177
- let count = store.count(for: target)
178
- return Button {
179
- withAnimation(.spring(response: 0.32, dampingFraction: 0.82)) {
180
- store.filter = target
181
- store.pane = .library
182
- if !store.visibleRecordings.contains(where: { $0.id == store.selection }) {
183
- store.selection = store.visibleRecordings.first?.id
184
- }
185
- }
186
- } label: {
187
- rowLabel(icon: icon, label: label, count: count > 0 ? count : nil, selected: selected, accentDot: false)
188
- }
189
- .buttonStyle(.plain)
190
- }
191
-
192
- private func rowLabel(icon: String, label: String, count: Int?, selected: Bool, accentDot: Bool) -> some View {
193
- HStack(spacing: 9) {
194
- Image(systemName: icon).font(.system(size: 12, weight: .medium)).frame(width: 16)
195
- Text(label).font(.system(.subheadline, design: .rounded)).lineLimit(1)
196
- Spacer(minLength: 4)
197
- if accentDot {
198
- Circle().fill(Theme.recordRed).frame(width: 7, height: 7)
199
- } else if let count {
200
- Text("\(count)").font(.caption2.monospacedDigit()).foregroundStyle(.white.opacity(0.55))
201
- }
202
- }
203
- .foregroundStyle(.white.opacity(selected ? 1 : 0.85))
204
- .padding(.horizontal, 9).padding(.vertical, 7)
205
- .contentShape(Rectangle())
206
- .background {
207
- if selected {
208
- RoundedRectangle(cornerRadius: Theme.cornerSmall, style: .continuous).fill(.white.opacity(0.22))
209
- }
210
- }
211
- }
212
- }