@hasna/recordings 0.1.19 → 0.1.20

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/dist/cli/index.js CHANGED
@@ -7,13 +7,9 @@ var __require = import.meta.require;
7
7
  var require_package = __commonJS((exports, module) => {
8
8
  module.exports = {
9
9
  name: "@hasna/recordings",
10
- version: "0.1.19",
10
+ version: "0.1.11",
11
11
  type: "module",
12
12
  description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
13
- repository: {
14
- type: "git",
15
- url: "git+https://github.com/hasna/recordings.git"
16
- },
17
13
  main: "dist/index.js",
18
14
  types: "dist/index.d.ts",
19
15
  bin: {
@@ -9893,40 +9889,15 @@ function runMigrations(db) {
9893
9889
  const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
9894
9890
  const currentLevel = result?.max_id ?? -1;
9895
9891
  for (let i = currentLevel + 1;i < MIGRATIONS.length; i++) {
9892
+ db.run(MIGRATIONS[i]);
9896
9893
  try {
9897
- db.run(MIGRATIONS[i]);
9898
9894
  db.query("INSERT INTO _migrations (id) VALUES (?)").run(i);
9899
9895
  } catch (e) {
9900
- if (isBenignMigrationError(e)) {
9901
- db.query("INSERT OR IGNORE INTO _migrations (id) VALUES (?)").run(i);
9902
- continue;
9903
- }
9904
9896
  if (!(e instanceof Error && e.message.includes("UNIQUE constraint failed"))) {
9905
9897
  throw e;
9906
9898
  }
9907
9899
  }
9908
9900
  }
9909
- repairSchemaDrift(db);
9910
- }
9911
- function isBenignMigrationError(error) {
9912
- if (!(error instanceof Error))
9913
- return false;
9914
- return error.message.includes("duplicate column name");
9915
- }
9916
- function repairSchemaDrift(db) {
9917
- ensureColumn(db, "recordings", "goal", "TEXT");
9918
- ensureColumn(db, "recordings", "role", "TEXT");
9919
- ensureColumn(db, "recordings", "task_list_id", "TEXT");
9920
- ensureColumn(db, "recordings", "machine_id", "TEXT");
9921
- ensureColumn(db, "recordings", "metadata", "TEXT DEFAULT '{}'");
9922
- ensureColumn(db, "agents", "active_project_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
9923
- }
9924
- function ensureColumn(db, table, column, definition) {
9925
- const rows = db.query(`PRAGMA table_info(${table})`).all();
9926
- if (rows.some((row) => row.name === column)) {
9927
- return;
9928
- }
9929
- db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
9930
9901
  }
9931
9902
  function getAdapter() {
9932
9903
  if (!_adapter) {
@@ -10466,7 +10437,7 @@ async function processText(rawText, config, systemPrompt) {
10466
10437
  }
10467
10438
 
10468
10439
  // src/version.ts
10469
- var VERSION = "0.1.19";
10440
+ var VERSION = "0.1.11";
10470
10441
 
10471
10442
  // src/cli/index.ts
10472
10443
  var program = new Command;
@@ -10792,65 +10763,6 @@ appCommand.command("status").description("Show installed Recordings.app status")
10792
10763
  console.log(`Native sources: ${status.native_sources_available ? "available" : "missing"}`);
10793
10764
  console.log(`Installed app: ${status.installed ? status.installed_app_path : "missing"}`);
10794
10765
  console.log(`Executable: ${status.executable ? "available" : "missing"}`);
10795
- console.log(`Code hash: ${status.app_code_hash ?? "unavailable"}`);
10796
- if (process.platform === "darwin") {
10797
- console.log(`Microphone: ${status.microphone_permission}`);
10798
- console.log(`Accessibility: ${status.accessibility_permission}`);
10799
- console.log(`Log: ${status.log_path}`);
10800
- }
10801
- });
10802
- appCommand.command("permissions").description("Show macOS permission state for Recordings.app").action(() => {
10803
- const status = getMacOSAppStatus();
10804
- const permissions = {
10805
- platform: status.platform,
10806
- bundle_id: "com.hasna.recordings",
10807
- microphone: status.microphone_permission,
10808
- accessibility: status.accessibility_permission,
10809
- app_code_hash: status.app_code_hash,
10810
- ad_hoc_signed: status.ad_hoc_signed,
10811
- log_path: status.log_path
10812
- };
10813
- if (program.opts().json) {
10814
- console.log(JSON.stringify(permissions, null, 2));
10815
- return;
10816
- }
10817
- console.log(`Microphone: ${permissions.microphone}`);
10818
- console.log(`Accessibility: ${permissions.accessibility}`);
10819
- console.log(`Log: ${permissions.log_path}`);
10820
- });
10821
- appCommand.command("reset-permissions").description("Reset macOS Microphone and Accessibility permissions for Recordings.app").action(() => {
10822
- if (process.platform !== "darwin") {
10823
- console.error(chalk.red("Permission reset is only available on macOS"));
10824
- process.exit(1);
10825
- }
10826
- const services = ["Microphone", "Accessibility"];
10827
- for (const service of services) {
10828
- const result = spawnSync("tccutil", ["reset", service, "com.hasna.recordings"], {
10829
- stdio: "inherit"
10830
- });
10831
- if (result.error) {
10832
- console.error(chalk.red(result.error.message));
10833
- process.exit(1);
10834
- }
10835
- }
10836
- });
10837
- appCommand.command("log").description("Show the Recordings.app diagnostic log").option("-n, --lines <lines>", "Number of lines to print", "120").action((opts) => {
10838
- const status = getMacOSAppStatus();
10839
- if (!existsSync6(status.log_path)) {
10840
- console.log("");
10841
- return;
10842
- }
10843
- const lines = Math.max(1, parseInt(opts.lines, 10) || 120);
10844
- const result = spawnSync("tail", ["-n", String(lines), status.log_path], {
10845
- encoding: "utf8"
10846
- });
10847
- if (result.error) {
10848
- console.error(chalk.red(result.error.message));
10849
- process.exit(1);
10850
- }
10851
- process.stdout.write(result.stdout);
10852
- if (result.stderr)
10853
- process.stderr.write(result.stderr);
10854
10766
  });
10855
10767
  appCommand.command("open").description("Open the installed Recordings.app").action(() => {
10856
10768
  const status = getMacOSAppStatus();
@@ -11316,11 +11228,8 @@ function getMacOSAppStatus() {
11316
11228
  const home = process.env.HOME || process.env.USERPROFILE || "";
11317
11229
  const installedAppPath = pathJoin(home, ".hasna", "recordings", "Recordings.app");
11318
11230
  const executablePath = pathJoin(installedAppPath, "Contents", "MacOS", "Recordings");
11319
- const logPath = pathJoin(home, ".hasna", "recordings", "Recordings.log");
11320
11231
  const installerPath = pathJoin(packageRoot, "scripts", "install_macos_app.sh");
11321
11232
  const nativeSourcesPath = pathJoin(packageRoot, "src", "native", "Recordings");
11322
- const signingInfo = getCodeSigningInfo(installedAppPath);
11323
- const permissionCodeHash = signingInfo.adHoc ? signingInfo.cdHash : null;
11324
11233
  return {
11325
11234
  platform: process.platform,
11326
11235
  package_root: packageRoot,
@@ -11331,69 +11240,9 @@ function getMacOSAppStatus() {
11331
11240
  installed_app_path: installedAppPath,
11332
11241
  installed: existsSync6(installedAppPath),
11333
11242
  executable_path: executablePath,
11334
- executable: existsSync6(executablePath),
11335
- app_code_hash: signingInfo.cdHash,
11336
- ad_hoc_signed: signingInfo.adHoc,
11337
- microphone_permission: getTccPermission("kTCCServiceMicrophone", home, permissionCodeHash),
11338
- accessibility_permission: getTccPermission("kTCCServiceAccessibility", home, permissionCodeHash),
11339
- log_path: logPath
11243
+ executable: existsSync6(executablePath)
11340
11244
  };
11341
11245
  }
11342
- function getCodeSigningInfo(appPath) {
11343
- if (process.platform !== "darwin" || !existsSync6(appPath)) {
11344
- return { cdHash: null, adHoc: false };
11345
- }
11346
- const result = spawnSync("codesign", ["-d", "--verbose=4", appPath], {
11347
- encoding: "utf8",
11348
- stdio: ["ignore", "pipe", "pipe"]
11349
- });
11350
- const output = `${result.stdout}
11351
- ${result.stderr}`;
11352
- const cdHash = output.match(/^CDHash=([a-fA-F0-9]+)/m)?.[1]?.toLowerCase() ?? null;
11353
- const adHoc = /Signature=adhoc/.test(output);
11354
- return { cdHash, adHoc };
11355
- }
11356
- function getTccPermission(service, home, currentCodeHash) {
11357
- if (process.platform !== "darwin")
11358
- return "unsupported";
11359
- const dbPaths = [
11360
- pathJoin(home, "Library", "Application Support", "com.apple.TCC", "TCC.db"),
11361
- pathJoin("/", "Library", "Application Support", "com.apple.TCC", "TCC.db")
11362
- ];
11363
- const sql = "select auth_value || '|' || ifnull(hex(csreq), '') from access where service = '" + service.replace(/'/g, "''") + "' and client = 'com.hasna.recordings' order by last_modified desc limit 1;";
11364
- for (const dbPath of dbPaths) {
11365
- if (!existsSync6(dbPath))
11366
- continue;
11367
- const result = spawnSync("sqlite3", [dbPath, sql], {
11368
- encoding: "utf8",
11369
- stdio: ["ignore", "pipe", "ignore"]
11370
- });
11371
- const value = result.stdout.trim();
11372
- if (!value)
11373
- continue;
11374
- const [authValue, csreqHex = ""] = value.split("|");
11375
- const label = tccAuthValueLabel(authValue ?? "");
11376
- if (label === "allowed" && currentCodeHash && csreqHex && !csreqHex.toLowerCase().includes(currentCodeHash.toLowerCase())) {
11377
- return "stale_allowed_for_previous_app_build";
11378
- }
11379
- return label;
11380
- }
11381
- return "not_determined";
11382
- }
11383
- function tccAuthValueLabel(value) {
11384
- switch (value) {
11385
- case "0":
11386
- return "denied";
11387
- case "1":
11388
- return "unknown";
11389
- case "2":
11390
- return "allowed";
11391
- case "3":
11392
- return "limited";
11393
- default:
11394
- return `unknown(${value})`;
11395
- }
11396
- }
11397
11246
  function findPackageRoot() {
11398
11247
  let current = dirname3(fileURLToPath(import.meta.url));
11399
11248
  while (true) {
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AA+F7C,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAiBrD;AAsED,wBAAgB,aAAa,IAAI,IAAI,CAMpC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAGpC;AAED,oEAAoE;AACpE,wBAAgB,UAAU,IAAI,aAAa,CAK1C;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AA+F7C,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAiBrD;AA8BD,wBAAgB,aAAa,IAAI,IAAI,CAMpC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAGpC;AAED,oEAAoE;AACpE,wBAAgB,UAAU,IAAI,aAAa,CAK1C;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC"}
package/dist/index.js CHANGED
@@ -9844,40 +9844,15 @@ function runMigrations(db) {
9844
9844
  const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
9845
9845
  const currentLevel = result?.max_id ?? -1;
9846
9846
  for (let i = currentLevel + 1;i < MIGRATIONS.length; i++) {
9847
+ db.run(MIGRATIONS[i]);
9847
9848
  try {
9848
- db.run(MIGRATIONS[i]);
9849
9849
  db.query("INSERT INTO _migrations (id) VALUES (?)").run(i);
9850
9850
  } catch (e) {
9851
- if (isBenignMigrationError(e)) {
9852
- db.query("INSERT OR IGNORE INTO _migrations (id) VALUES (?)").run(i);
9853
- continue;
9854
- }
9855
9851
  if (!(e instanceof Error && e.message.includes("UNIQUE constraint failed"))) {
9856
9852
  throw e;
9857
9853
  }
9858
9854
  }
9859
9855
  }
9860
- repairSchemaDrift(db);
9861
- }
9862
- function isBenignMigrationError(error) {
9863
- if (!(error instanceof Error))
9864
- return false;
9865
- return error.message.includes("duplicate column name");
9866
- }
9867
- function repairSchemaDrift(db) {
9868
- ensureColumn(db, "recordings", "goal", "TEXT");
9869
- ensureColumn(db, "recordings", "role", "TEXT");
9870
- ensureColumn(db, "recordings", "task_list_id", "TEXT");
9871
- ensureColumn(db, "recordings", "machine_id", "TEXT");
9872
- ensureColumn(db, "recordings", "metadata", "TEXT DEFAULT '{}'");
9873
- ensureColumn(db, "agents", "active_project_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
9874
- }
9875
- function ensureColumn(db, table, column, definition) {
9876
- const rows = db.query(`PRAGMA table_info(${table})`).all();
9877
- if (rows.some((row) => row.name === column)) {
9878
- return;
9879
- }
9880
- db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
9881
9856
  }
9882
9857
  function closeDatabase() {
9883
9858
  if (_db) {
package/dist/mcp/index.js CHANGED
@@ -21,13 +21,9 @@ var __require = import.meta.require;
21
21
  var require_package = __commonJS((exports, module) => {
22
22
  module.exports = {
23
23
  name: "@hasna/recordings",
24
- version: "0.1.19",
24
+ version: "0.1.11",
25
25
  type: "module",
26
26
  description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
27
- repository: {
28
- type: "git",
29
- url: "git+https://github.com/hasna/recordings.git"
30
- },
31
27
  main: "dist/index.js",
32
28
  types: "dist/index.d.ts",
33
29
  bin: {
@@ -14530,40 +14526,15 @@ function runMigrations(db) {
14530
14526
  const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
14531
14527
  const currentLevel = result?.max_id ?? -1;
14532
14528
  for (let i = currentLevel + 1;i < MIGRATIONS.length; i++) {
14529
+ db.run(MIGRATIONS[i]);
14533
14530
  try {
14534
- db.run(MIGRATIONS[i]);
14535
14531
  db.query("INSERT INTO _migrations (id) VALUES (?)").run(i);
14536
14532
  } catch (e) {
14537
- if (isBenignMigrationError(e)) {
14538
- db.query("INSERT OR IGNORE INTO _migrations (id) VALUES (?)").run(i);
14539
- continue;
14540
- }
14541
14533
  if (!(e instanceof Error && e.message.includes("UNIQUE constraint failed"))) {
14542
14534
  throw e;
14543
14535
  }
14544
14536
  }
14545
14537
  }
14546
- repairSchemaDrift(db);
14547
- }
14548
- function isBenignMigrationError(error) {
14549
- if (!(error instanceof Error))
14550
- return false;
14551
- return error.message.includes("duplicate column name");
14552
- }
14553
- function repairSchemaDrift(db) {
14554
- ensureColumn(db, "recordings", "goal", "TEXT");
14555
- ensureColumn(db, "recordings", "role", "TEXT");
14556
- ensureColumn(db, "recordings", "task_list_id", "TEXT");
14557
- ensureColumn(db, "recordings", "machine_id", "TEXT");
14558
- ensureColumn(db, "recordings", "metadata", "TEXT DEFAULT '{}'");
14559
- ensureColumn(db, "agents", "active_project_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
14560
- }
14561
- function ensureColumn(db, table, column, definition) {
14562
- const rows = db.query(`PRAGMA table_info(${table})`).all();
14563
- if (rows.some((row) => row.name === column)) {
14564
- return;
14565
- }
14566
- db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
14567
14538
  }
14568
14539
  function getAdapter() {
14569
14540
  if (!_adapter) {
@@ -14983,7 +14954,7 @@ async function processText(rawText, config, systemPrompt) {
14983
14954
  }
14984
14955
 
14985
14956
  // src/version.ts
14986
- var VERSION = "0.1.19";
14957
+ var VERSION = "0.1.11";
14987
14958
 
14988
14959
  // src/mcp/index.ts
14989
14960
  var config = loadConfig();
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.1.19";
1
+ export declare const VERSION = "0.1.11";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,12 +1,8 @@
1
1
  {
2
2
  "name": "@hasna/recordings",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "type": "module",
5
5
  "description": "Speech-to-text recording tool with MCP and CLI — records, transcribes, and optionally enhances text using AI",
6
- "repository": {
7
- "type": "git",
8
- "url": "git+https://github.com/hasna/recordings.git"
9
- },
10
6
  "main": "dist/index.js",
11
7
  "types": "dist/index.d.ts",
12
8
  "bin": {
@@ -73,36 +73,4 @@ rm -rf "$APP_DEST"
73
73
  mkdir -p "$DATA_DIR"
74
74
  cp -R "$APP_SOURCE" "$APP_DEST" || warn_or_fail "failed to copy app bundle"
75
75
 
76
- current_cdhash() {
77
- codesign -d --verbose=4 "$1" 2>&1 | awk -F= '/^CDHash=/ { print toupper($2); exit }'
78
- }
79
-
80
- tcc_csreq_hex() {
81
- local db_path="$1"
82
- local service="$2"
83
- if [ ! -r "$db_path" ] || ! command -v sqlite3 >/dev/null 2>&1; then
84
- return 0
85
- fi
86
- sqlite3 "$db_path" \
87
- "SELECT hex(csreq) FROM access WHERE service = '${service}' AND client = 'com.hasna.recordings' ORDER BY last_modified DESC LIMIT 1;" \
88
- 2>/dev/null || true
89
- }
90
-
91
- reset_stale_permission() {
92
- local service="$1"
93
- local tcc_service="$2"
94
- local db_path="$3"
95
- local cdhash="$4"
96
- local csreq_hex
97
- csreq_hex="$(tcc_csreq_hex "$db_path" "$tcc_service" | tr '[:lower:]' '[:upper:]')"
98
- if [ -n "$cdhash" ] && [ -n "$csreq_hex" ] && [[ "$csreq_hex" != *"$cdhash"* ]]; then
99
- tccutil reset "$service" com.hasna.recordings >/dev/null 2>&1 || true
100
- echo "Reset stale ${service} permission for the newly installed Recordings.app."
101
- fi
102
- }
103
-
104
- APP_CDHASH="$(current_cdhash "$APP_DEST" || true)"
105
- reset_stale_permission "Microphone" "kTCCServiceMicrophone" "${HOME}/Library/Application Support/com.apple.TCC/TCC.db" "$APP_CDHASH"
106
- reset_stale_permission "Accessibility" "kTCCServiceAccessibility" "/Library/Application Support/com.apple.TCC/TCC.db" "$APP_CDHASH"
107
-
108
76
  echo "Installed Recordings.app from package: ${APP_DEST}"
@@ -1,5 +1,5 @@
1
1
  import SwiftUI
2
- @preconcurrency import KeyboardShortcuts
2
+ import KeyboardShortcuts
3
3
 
4
4
  public struct MenuBarPopover: View {
5
5
  @ObservedObject public var engine: RecordingEngine
@@ -237,10 +237,11 @@ public struct MenuBarPopover: View {
237
237
  showProject: filterProjectId == nil,
238
238
  isCopied: copiedIndex == i
239
239
  ) {
240
+ NSPasteboard.general.clearContents()
241
+ NSPasteboard.general.setString(item.displayText, forType: .string)
240
242
  withAnimation(.easeInOut(duration: 0.15)) {
241
243
  copiedIndex = i
242
244
  }
243
- engine.pasteIntoFrontApp(item.displayText)
244
245
  DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
245
246
  withAnimation { if copiedIndex == i { copiedIndex = nil } }
246
247
  }
@@ -16,20 +16,12 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
16
16
 
17
17
  private var ws: URLSessionWebSocketTask?
18
18
  private var receiveTask: Task<Void, Never>?
19
- private var outboundEventTask: Task<Void, Never>?
20
19
  private var isConfigured = false
21
20
  private var pendingAudioChunks: [Data] = []
22
21
  private var itemOrder: [String] = []
23
22
  private var deltaTextByItem: [String: String] = [:]
24
23
  private var completedTextByItem: [String: String] = [:]
25
- private var committedItemIDs = Set<String>()
26
- private var completedItemIDs = Set<String>()
27
24
  private var completedEventCount = 0
28
- private var queuedCommitCount = 0
29
- private var uncommittedAudioBytes = 0
30
- private var lastRealtimeEventAt = Date.distantPast
31
-
32
- private nonisolated static let minimumManualCommitBytes = 5_760
33
25
 
34
26
  private let apiKey: String
35
27
  private let homePath: String
@@ -60,12 +52,7 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
60
52
  itemOrder.removeAll(keepingCapacity: true)
61
53
  deltaTextByItem.removeAll(keepingCapacity: true)
62
54
  completedTextByItem.removeAll(keepingCapacity: true)
63
- committedItemIDs.removeAll(keepingCapacity: true)
64
- completedItemIDs.removeAll(keepingCapacity: true)
65
55
  completedEventCount = 0
66
- queuedCommitCount = 0
67
- uncommittedAudioBytes = 0
68
- lastRealtimeEventAt = Date()
69
56
  error = nil
70
57
 
71
58
  var request = URLRequest(url: Self.transcriptionURL)
@@ -111,51 +98,33 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
111
98
  return
112
99
  }
113
100
  let base64 = data.base64EncodedString()
114
- uncommittedAudioBytes += data.count
115
101
  let msg: [String: Any] = [
116
102
  "type": "input_audio_buffer.append",
117
103
  "audio": base64,
118
104
  ]
119
- enqueueOutboundEvent(msg)
105
+ Task { [weak self] in
106
+ try? await self?.sendEvent(msg)
107
+ }
120
108
  }
121
109
 
122
110
  /// Signal end of input — triggers final transcription completion.
123
- @discardableResult
124
- public func commitInput(reason: String = "final") async -> Bool {
125
- guard isStreaming, ws != nil else { return false }
111
+ public func commitInput() async {
112
+ guard isStreaming, ws != nil else { return }
126
113
  flushPendingAudio()
127
- let bytesToCommit = uncommittedAudioBytes
128
- guard Self.shouldManuallyCommit(uncommittedAudioBytes: bytesToCommit) else {
129
- return false
130
- }
131
114
  let msg: [String: Any] = [
132
115
  "type": "input_audio_buffer.commit",
133
116
  ]
134
- uncommittedAudioBytes = 0
135
- queuedCommitCount += 1
136
- lastRealtimeEventAt = Date()
137
- NativeAppLog.write("realtime commit queued reason=\(reason) bytes=\(bytesToCommit)", homePath: homePath)
138
- let commitTask = enqueueOutboundEvent(msg)
139
- await commitTask?.value
140
- return true
117
+ try? await sendEvent(msg)
141
118
  }
142
119
 
143
120
  /// Commit buffered input, wait briefly for a final completed event, then close.
144
- public func finish(timeoutMilliseconds: UInt64 = 2_800) async -> String {
121
+ public func finish(timeoutMilliseconds: UInt64 = 1_800) async -> String {
145
122
  guard isStreaming else { return accumulatedText }
146
- let completedCountBeforeCommit = completedEventCount
147
- let didManualCommit = await commitInput()
148
- let expectedCommitCount = queuedCommitCount
123
+ let initialCompletedCount = completedEventCount
124
+ await commitInput()
149
125
 
150
126
  let deadline = Date().addingTimeInterval(TimeInterval(timeoutMilliseconds) / 1_000)
151
- while Date() < deadline, isStreaming {
152
- let hasIncompleteCommittedItems = !committedItemIDs.subtracting(completedItemIDs).isEmpty
153
- let hasManualCommitCompletion = !didManualCommit || completedEventCount > completedCountBeforeCommit
154
- let hasQueuedCommitCompletion = completedEventCount >= expectedCommitCount
155
- let quietLongEnough = Date().timeIntervalSince(lastRealtimeEventAt) >= 0.35
156
- if !hasIncompleteCommittedItems, hasManualCommitCompletion, hasQueuedCommitCompletion, quietLongEnough {
157
- break
158
- }
127
+ while Date() < deadline, isStreaming, completedEventCount == initialCompletedCount {
159
128
  try? await Task.sleep(for: .milliseconds(50))
160
129
  }
161
130
 
@@ -170,10 +139,8 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
170
139
  isConfigured = false
171
140
  receiveTask?.cancel()
172
141
  ws?.cancel(with: .normalClosure, reason: nil)
173
- outboundEventTask?.cancel()
174
142
  ws = nil
175
143
  receiveTask = nil
176
- outboundEventTask = nil
177
144
  pendingAudioChunks.removeAll(keepingCapacity: true)
178
145
  let text = accumulatedText
179
146
  return text
@@ -213,14 +180,11 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
213
180
 
214
181
  switch type {
215
182
  case "input_audio_buffer.committed":
216
- lastRealtimeEventAt = Date()
217
183
  if let itemID = json["item_id"] as? String {
218
184
  registerItem(itemID, previousItemID: json["previous_item_id"] as? String)
219
- committedItemIDs.insert(itemID)
220
185
  }
221
186
 
222
187
  case "conversation.item.input_audio_transcription.delta":
223
- lastRealtimeEventAt = Date()
224
188
  let itemID = json["item_id"] as? String ?? "__default__"
225
189
  registerItem(itemID, previousItemID: nil)
226
190
  if let delta = json["delta"] as? String {
@@ -230,29 +194,21 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
230
194
 
231
195
  case "conversation.item.input_audio_transcription.completed":
232
196
  // The server may send the complete transcript here
233
- lastRealtimeEventAt = Date()
234
197
  let itemID = json["item_id"] as? String ?? "__default__"
235
198
  registerItem(itemID, previousItemID: nil)
236
- completedItemIDs.insert(itemID)
237
- completedEventCount += 1
238
199
  if let text = json["transcript"] as? String, !text.isEmpty {
239
200
  completedTextByItem[itemID] = text
201
+ completedEventCount += 1
202
+ rebuildAccumulatedText()
240
203
  }
241
- rebuildAccumulatedText()
242
204
 
243
205
  case "conversation.item.input_audio_transcription.failed":
244
- lastRealtimeEventAt = Date()
245
- if let itemID = json["item_id"] as? String {
246
- completedItemIDs.insert(itemID)
247
- completedEventCount += 1
248
- }
249
206
  if let msg = json["error"] as? [String: Any],
250
207
  let message = msg["message"] as? String {
251
208
  self.error = message
252
209
  }
253
210
 
254
211
  case "error":
255
- lastRealtimeEventAt = Date()
256
212
  if let detail = json["error"] as? [String: Any],
257
213
  let msg = detail["message"] as? String {
258
214
  fputs("[RealtimeClient] Error: \(msg)\n", stderr)
@@ -264,18 +220,6 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
264
220
  }
265
221
  }
266
222
 
267
- @discardableResult
268
- private func enqueueOutboundEvent(_ obj: [String: Any]) -> Task<Void, Never>? {
269
- guard ws != nil else { return nil }
270
- let previousTask = outboundEventTask
271
- let task = Task { [weak self] in
272
- await previousTask?.value
273
- try? await self?.sendEvent(obj)
274
- }
275
- outboundEventTask = task
276
- return task
277
- }
278
-
279
223
  private func registerItem(_ itemID: String, previousItemID: String?) {
280
224
  guard !itemOrder.contains(itemID) else { return }
281
225
  if let previousItemID, let previousIndex = itemOrder.firstIndex(of: previousItemID) {
@@ -351,10 +295,6 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
351
295
  return output
352
296
  }
353
297
 
354
- private nonisolated static func shouldManuallyCommit(uncommittedAudioBytes: Int) -> Bool {
355
- uncommittedAudioBytes >= minimumManualCommitBytes
356
- }
357
-
358
298
  private nonisolated static func transcriptionSessionUpdateEvent(transcription: [String: Any]) -> [String: Any] {
359
299
  [
360
300
  "type": "session.update",
@@ -423,10 +363,6 @@ extension RealtimeTranscriptionClient {
423
363
  joinTranscriptParts(parts)
424
364
  }
425
365
 
426
- public nonisolated static func shouldManuallyCommitTestHelper(uncommittedAudioBytes: Int) -> Bool {
427
- shouldManuallyCommit(uncommittedAudioBytes: uncommittedAudioBytes)
428
- }
429
-
430
366
  public nonisolated static func sessionUpdateTestHelper(prompt: String, language: String = "") -> [String: Any] {
431
367
  var transcription: [String: Any] = [
432
368
  "model": modelID,
@@ -1,5 +1,5 @@
1
1
  import SwiftUI
2
- @preconcurrency import KeyboardShortcuts
2
+ import KeyboardShortcuts
3
3
 
4
4
  public struct SettingsView: View {
5
5
  @ObservedObject public var engine: RecordingEngine
@@ -45,32 +45,6 @@ public struct SettingsView: View {
45
45
  .foregroundStyle(.secondary)
46
46
  }
47
47
 
48
- Section("Permissions") {
49
- HStack {
50
- Text("Microphone")
51
- Spacer()
52
- Text(engine.microphonePermissionLabel)
53
- .foregroundStyle(.secondary)
54
- }
55
- Button("Request Microphone") {
56
- engine.requestMicrophonePermission()
57
- }
58
- HStack {
59
- Text("Accessibility")
60
- Spacer()
61
- Text(engine.accessibilityPermissionLabel)
62
- .foregroundStyle(.secondary)
63
- }
64
- HStack {
65
- Button("Request Accessibility") {
66
- engine.requestAccessibilityPermission()
67
- }
68
- Button("Open Accessibility Settings") {
69
- engine.openAccessibilitySettings()
70
- }
71
- }
72
- }
73
-
74
48
  Section("System Prompt") {
75
49
  TextEditor(text: $projectStore.settings.globalSystemPrompt)
76
50
  .frame(height: 80)
@@ -110,17 +110,4 @@ struct RealtimeTranscriptionTests {
110
110
  let text = RealtimeTranscriptionClient.joinTranscriptPartsTestHelper(["Hello", "world.", " Next"])
111
111
  #expect(text == "Hello world. Next")
112
112
  }
113
-
114
- @Test("Manual commit waits for enough buffered PCM audio")
115
- func manualCommitThreshold() {
116
- #expect(RealtimeTranscriptionClient.shouldManuallyCommitTestHelper(uncommittedAudioBytes: 4_799) == false)
117
- #expect(RealtimeTranscriptionClient.shouldManuallyCommitTestHelper(uncommittedAudioBytes: 5_760) == true)
118
- }
119
-
120
- @Test("Partial realtime text falls back for longer recordings")
121
- func partialRealtimeFallback() {
122
- #expect(RecordingEngine.shouldFallbackFromPartialRealtime(text: "Hi", pcmByteCount: 96_000) == true)
123
- #expect(RecordingEngine.shouldFallbackFromPartialRealtime(text: "This is a complete sentence.", pcmByteCount: 96_000) == false)
124
- #expect(RecordingEngine.shouldFallbackFromPartialRealtime(text: "Hi", pcmByteCount: 12_000) == false)
125
- }
126
113
  }
@@ -1,23 +0,0 @@
1
- import Foundation
2
- import Testing
3
- @testable import RecordingsLib
4
-
5
- struct NativeAppDiagnosticsTests {
6
- @Test("Native app log writes to recordings log file")
7
- func logWritesFile() throws {
8
- let home = try makeHome()
9
-
10
- NativeAppLog.write("diagnostic-test", homePath: home)
11
-
12
- let path = "\(home)/.hasna/recordings/Recordings.log"
13
- let text = try String(contentsOfFile: path, encoding: .utf8)
14
- #expect(text.contains("diagnostic-test"))
15
- }
16
-
17
- private func makeHome() throws -> String {
18
- let url = FileManager.default.temporaryDirectory
19
- .appendingPathComponent("recordings-diagnostics-tests-\(UUID().uuidString)")
20
- try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
21
- return url.path
22
- }
23
- }