@hasna/recordings 0.1.17 → 0.1.19

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,7 +7,7 @@ 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.17",
10
+ version: "0.1.19",
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
13
  repository: {
@@ -10466,7 +10466,7 @@ async function processText(rawText, config, systemPrompt) {
10466
10466
  }
10467
10467
 
10468
10468
  // src/version.ts
10469
- var VERSION = "0.1.17";
10469
+ var VERSION = "0.1.19";
10470
10470
 
10471
10471
  // src/cli/index.ts
10472
10472
  var program = new Command;
package/dist/mcp/index.js CHANGED
@@ -21,7 +21,7 @@ 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.17",
24
+ version: "0.1.19",
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
27
  repository: {
@@ -14983,7 +14983,7 @@ async function processText(rawText, config, systemPrompt) {
14983
14983
  }
14984
14984
 
14985
14985
  // src/version.ts
14986
- var VERSION = "0.1.17";
14986
+ var VERSION = "0.1.19";
14987
14987
 
14988
14988
  // src/mcp/index.ts
14989
14989
  var config = loadConfig();
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.1.17";
1
+ export declare const VERSION = "0.1.19";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/recordings",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
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
6
  "repository": {
@@ -237,11 +237,10 @@ 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)
242
240
  withAnimation(.easeInOut(duration: 0.15)) {
243
241
  copiedIndex = i
244
242
  }
243
+ engine.pasteIntoFrontApp(item.displayText)
245
244
  DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
246
245
  withAnimation { if copiedIndex == i { copiedIndex = nil } }
247
246
  }
@@ -50,6 +50,12 @@ public enum RecordingTrigger {
50
50
  case keyboardShortcut
51
51
  }
52
52
 
53
+ struct PasteTargetCandidate: Equatable, Sendable {
54
+ let pid: pid_t
55
+ let bundleIdentifier: String?
56
+ let isRegularApp: Bool
57
+ }
58
+
53
59
  private actor PCMStreamState {
54
60
  private var recordedPCM = Data()
55
61
  private var pendingChunk = Data()
@@ -429,6 +435,7 @@ public final class RecordingEngine: ObservableObject {
429
435
 
430
436
  let curMode = mode
431
437
  let targetAppBundleIdentifier = targetAppBundleIdentifier
438
+ let targetAppPid = targetAppPid
432
439
  let activeProjectId = projectStore?.settings.activeProjectId
433
440
  let activeProjectName = projectStore?.activeProject?.name
434
441
  let audioPath = activeAudioPath
@@ -465,6 +472,7 @@ public final class RecordingEngine: ObservableObject {
465
472
  audioPath: audioPath,
466
473
  curMode: curMode,
467
474
  targetAppBundleIdentifier: targetAppBundleIdentifier,
475
+ targetAppPid: targetAppPid,
468
476
  activeProjectId: activeProjectId,
469
477
  activeProjectName: activeProjectName
470
478
  )
@@ -485,7 +493,7 @@ public final class RecordingEngine: ObservableObject {
485
493
  return trimmed.count < 12 || words.count <= 2
486
494
  }
487
495
 
488
- private func finishWithText(_ text: String, curMode: RecordingMode, targetAppBundleIdentifier: String?, activeProjectId: String?, activeProjectName: String?) {
496
+ private func finishWithText(_ text: String, curMode: RecordingMode, targetAppBundleIdentifier: String?, targetAppPid: pid_t?, activeProjectId: String?, activeProjectName: String?) {
489
497
  log("finishWithText mode=\(curMode.rawValue) chars=\(text.count)")
490
498
  if curMode == .command {
491
499
  runCommandMode(instruction: text)
@@ -494,7 +502,7 @@ public final class RecordingEngine: ObservableObject {
494
502
 
495
503
  let shortcutText = voiceShortcuts?.match(text)
496
504
  let output = shortcutText ?? text
497
- pasteIntoFrontApp(output, targetAppBundleIdentifier: targetAppBundleIdentifier)
505
+ pasteIntoFrontApp(output, targetAppBundleIdentifier: targetAppBundleIdentifier, targetAppPid: targetAppPid)
498
506
  recentTranscriptions.insert(
499
507
  TranscriptionResult(
500
508
  rawText: text,
@@ -576,7 +584,7 @@ public final class RecordingEngine: ObservableObject {
576
584
 
577
585
  // MARK: - Fallback Transcription
578
586
 
579
- private func fallbackTranscribe(audioPath: String, curMode: RecordingMode, targetAppBundleIdentifier: String?, activeProjectId: String?, activeProjectName: String?) {
587
+ private func fallbackTranscribe(audioPath: String, curMode: RecordingMode, targetAppBundleIdentifier: String?, targetAppPid: pid_t?, activeProjectId: String?, activeProjectName: String?) {
580
588
  let homePath = home
581
589
 
582
590
  isTranscribing = true
@@ -608,6 +616,7 @@ public final class RecordingEngine: ObservableObject {
608
616
  text,
609
617
  curMode: curMode,
610
618
  targetAppBundleIdentifier: targetAppBundleIdentifier,
619
+ targetAppPid: targetAppPid,
611
620
  activeProjectId: activeProjectId,
612
621
  activeProjectName: activeProjectName
613
622
  )
@@ -698,8 +707,8 @@ public final class RecordingEngine: ObservableObject {
698
707
 
699
708
  // MARK: - Paste
700
709
 
701
- func pasteIntoFrontApp(_ text: String, targetAppBundleIdentifier: String? = nil) {
702
- log("paste requested chars=\(text.count) target=\(targetAppBundleIdentifier ?? "nil") accessibility=\(AXIsProcessTrusted())")
710
+ func pasteIntoFrontApp(_ text: String, targetAppBundleIdentifier: String? = nil, targetAppPid: pid_t? = nil) {
711
+ log("paste requested chars=\(text.count) target=\(targetAppBundleIdentifier ?? "nil") pid=\(targetAppPid.map(String.init) ?? "nil") accessibility=\(AXIsProcessTrusted())")
703
712
  let pb = NSPasteboard.general
704
713
  pb.clearContents()
705
714
  pb.setString(text, forType: .string)
@@ -715,13 +724,22 @@ public final class RecordingEngine: ObservableObject {
715
724
 
716
725
  let myPID = ProcessInfo.processInfo.processIdentifier
717
726
  let runningApps = NSWorkspace.shared.runningApplications
718
- let targetApp = runningApps.first(where: {
719
- guard $0.processIdentifier != myPID, $0.activationPolicy == .regular else { return false }
720
- guard let bundleIdentifier = targetAppBundleIdentifier else { return false }
721
- return $0.bundleIdentifier == bundleIdentifier
722
- }) ?? runningApps.first(where: {
723
- $0.activationPolicy == .regular && $0.processIdentifier != myPID
724
- })
727
+ let candidates = runningApps.map {
728
+ PasteTargetCandidate(
729
+ pid: $0.processIdentifier,
730
+ bundleIdentifier: $0.bundleIdentifier,
731
+ isRegularApp: $0.activationPolicy == .regular
732
+ )
733
+ }
734
+ let selectedTarget = Self.selectPasteTarget(
735
+ candidates: candidates,
736
+ currentPid: myPID,
737
+ targetBundleIdentifier: targetAppBundleIdentifier,
738
+ targetPid: targetAppPid
739
+ )
740
+ let targetApp = selectedTarget.flatMap { selected in
741
+ runningApps.first { $0.processIdentifier == selected.pid }
742
+ }
725
743
 
726
744
  guard let app = targetApp else {
727
745
  log("paste target app not found")
@@ -729,8 +747,8 @@ public final class RecordingEngine: ObservableObject {
729
747
  return
730
748
  }
731
749
 
732
- // Activate target app and wait for focus to stabilize
733
- app.activate()
750
+ // Activate the exact app that owned focus when recording started, then paste after focus settles.
751
+ app.activate(options: [.activateIgnoringOtherApps])
734
752
 
735
753
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
736
754
  let src = CGEventSource(stateID: .hidSystemState)
@@ -746,6 +764,24 @@ public final class RecordingEngine: ObservableObject {
746
764
  }
747
765
  }
748
766
 
767
+ nonisolated static func selectPasteTarget(
768
+ candidates: [PasteTargetCandidate],
769
+ currentPid: pid_t,
770
+ targetBundleIdentifier: String?,
771
+ targetPid: pid_t?
772
+ ) -> PasteTargetCandidate? {
773
+ candidates.first {
774
+ guard let targetPid else { return false }
775
+ return $0.pid == targetPid && $0.pid != currentPid
776
+ } ?? candidates.first {
777
+ guard $0.pid != currentPid, $0.isRegularApp else { return false }
778
+ guard let targetBundleIdentifier else { return false }
779
+ return $0.bundleIdentifier == targetBundleIdentifier
780
+ } ?? candidates.first {
781
+ $0.isRegularApp && $0.pid != currentPid
782
+ }
783
+ }
784
+
749
785
  private func ensureAccessibilityPermission(prompt: Bool) -> Bool {
750
786
  if AXIsProcessTrusted() {
751
787
  return true
@@ -0,0 +1,38 @@
1
+ import Testing
2
+ @testable import RecordingsLib
3
+
4
+ struct PasteTargetTests {
5
+ @Test("paste target prefers captured process id over bundle fallback")
6
+ func prefersCapturedPid() {
7
+ let candidates = [
8
+ PasteTargetCandidate(pid: 10, bundleIdentifier: "com.editor", isRegularApp: true),
9
+ PasteTargetCandidate(pid: 20, bundleIdentifier: "com.editor", isRegularApp: true),
10
+ ]
11
+
12
+ let selected = RecordingEngine.selectPasteTarget(
13
+ candidates: candidates,
14
+ currentPid: 99,
15
+ targetBundleIdentifier: "com.editor",
16
+ targetPid: 20
17
+ )
18
+
19
+ #expect(selected?.pid == 20)
20
+ }
21
+
22
+ @Test("paste target ignores current app when falling back")
23
+ func ignoresCurrentApp() {
24
+ let candidates = [
25
+ PasteTargetCandidate(pid: 99, bundleIdentifier: "com.hasna.recordings", isRegularApp: true),
26
+ PasteTargetCandidate(pid: 30, bundleIdentifier: "com.notes", isRegularApp: true),
27
+ ]
28
+
29
+ let selected = RecordingEngine.selectPasteTarget(
30
+ candidates: candidates,
31
+ currentPid: 99,
32
+ targetBundleIdentifier: nil,
33
+ targetPid: nil
34
+ )
35
+
36
+ #expect(selected?.pid == 30)
37
+ }
38
+ }