@lattices/cli 0.4.2 → 0.4.6

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 (146) hide show
  1. package/README.md +3 -0
  2. package/app/Info.plist +2 -2
  3. package/app/Lattices.app/Contents/Info.plist +2 -2
  4. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/app/Package.swift +6 -0
  6. package/app/Sources/AppShell/App.swift +20 -0
  7. package/app/Sources/{AppDelegate.swift → AppShell/AppDelegate.swift} +94 -34
  8. package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +12 -1
  9. package/app/Sources/AppShell/AppUpdater.swift +92 -0
  10. package/app/Sources/AppShell/CliActionLauncher.swift +50 -0
  11. package/app/Sources/{HomeDashboardView.swift → AppShell/HomeDashboardView.swift} +18 -10
  12. package/app/Sources/AppShell/LatticesRuntime.swift +61 -0
  13. package/app/Sources/{MainView.swift → AppShell/MainView.swift} +351 -191
  14. package/app/Sources/{OnboardingView.swift → AppShell/OnboardingView.swift} +30 -16
  15. package/app/Sources/{Preferences.swift → AppShell/Preferences.swift} +78 -0
  16. package/app/Sources/{SettingsView.swift → AppShell/SettingsView.swift} +869 -152
  17. package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +9 -5
  18. package/app/Sources/{IntentEngine.swift → Core/Actions/IntentEngine.swift} +51 -27
  19. package/app/Sources/Core/Actions/IntentSchema.swift +94 -0
  20. package/app/Sources/{Intents → Core/Actions/Intents}/LatticeIntent.swift +0 -25
  21. package/app/Sources/{PaletteCommand.swift → Core/Actions/PaletteCommand.swift} +26 -6
  22. package/app/Sources/{VoiceIntentResolver.swift → Core/Actions/VoiceIntentResolver.swift} +46 -4
  23. package/app/Sources/Core/Companion/CompanionActivityLog.swift +70 -0
  24. package/app/Sources/Core/Companion/CompanionKeyboardController.swift +141 -0
  25. package/app/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +438 -0
  26. package/app/Sources/Core/Companion/LatticesCompanionCockpit.swift +555 -0
  27. package/app/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +594 -0
  28. package/app/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +204 -0
  29. package/app/Sources/Core/Companion/LatticesDeckHost.swift +1463 -0
  30. package/app/Sources/{LatticesApi.swift → Core/Daemon/LatticesApi.swift} +125 -4
  31. package/app/Sources/{AppTypeClassifier.swift → Core/Desktop/AppTypeClassifier.swift} +36 -0
  32. package/app/Sources/{DesktopModel.swift → Core/Desktop/DesktopModel.swift} +6 -8
  33. package/app/Sources/Core/Desktop/MouseFinder.swift +527 -0
  34. package/app/Sources/Core/Desktop/SessionWindowLocator.swift +139 -0
  35. package/app/Sources/Core/Desktop/WindowDragSnapController.swift +628 -0
  36. package/app/Sources/Core/Desktop/WindowPreviewCard.swift +100 -0
  37. package/app/Sources/Core/Desktop/WindowPreviewStore.swift +113 -0
  38. package/app/Sources/Core/Desktop/WindowSelectionStore.swift +76 -0
  39. package/app/Sources/{WindowTiler.swift → Core/Desktop/WindowTiler.swift} +351 -172
  40. package/app/Sources/Core/Input/MouseGestureConfig.swift +364 -0
  41. package/app/Sources/Core/Input/MouseGestureController.swift +1203 -0
  42. package/app/Sources/Core/Input/MouseInputDeviceStore.swift +98 -0
  43. package/app/Sources/Core/Input/MouseInputEventViewer.swift +272 -0
  44. package/app/Sources/Core/Input/MouseShortcutStore.swift +107 -0
  45. package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +127 -24
  46. package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +492 -79
  47. package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -0
  48. package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.swift} +1 -0
  49. package/app/Sources/{HUDRightBar.swift → Core/Overlays/HUD/HUDRightBar.swift} +23 -201
  50. package/app/Sources/{LauncherHUD.swift → Core/Overlays/HUD/LauncherHUD.swift} +12 -26
  51. package/app/Sources/{OmniSearchView.swift → Core/Overlays/OmniSearch/OmniSearchView.swift} +136 -2
  52. package/app/Sources/{OmniSearchWindow.swift → Core/Overlays/OmniSearch/OmniSearchWindow.swift} +21 -32
  53. package/app/Sources/Core/Overlays/OverlayPanelShell.swift +241 -0
  54. package/app/Sources/{ScreenMapState.swift → Core/Overlays/ScreenMap/ScreenMapState.swift} +116 -32
  55. package/app/Sources/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.swift} +510 -524
  56. package/app/Sources/{ScreenMapWindowController.swift → Core/Overlays/ScreenMap/ScreenMapWindowController.swift} +12 -4
  57. package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -53
  58. package/app/Sources/Core/Pi/PiAuthNextStepCard.swift +148 -0
  59. package/app/Sources/Core/Pi/PiAuthPromptCard.swift +90 -0
  60. package/app/Sources/{PiChatDock.swift → Core/Pi/PiChatDock.swift} +137 -74
  61. package/app/Sources/{PiChatSession.swift → Core/Pi/PiChatSession.swift} +608 -108
  62. package/app/Sources/Core/Pi/PiInstallCallout.swift +86 -0
  63. package/app/Sources/Core/Pi/PiProviderSetupCallout.swift +99 -0
  64. package/app/Sources/{PiWorkspaceView.swift → Core/Pi/PiWorkspaceView.swift} +174 -77
  65. package/app/Sources/{PermissionChecker.swift → Core/System/PermissionChecker.swift} +76 -2
  66. package/app/Sources/Core/System/SystemTelemetryMonitor.swift +273 -0
  67. package/app/Sources/{HandsOffSession.swift → Core/Voice/HandsOffSession.swift} +15 -4
  68. package/app/Sources/{WorkspaceManager.swift → Core/Workspace/WorkspaceManager.swift} +288 -0
  69. package/bin/assistant-intelligence.ts +874 -0
  70. package/bin/handsoff-infer.ts +16 -209
  71. package/bin/handsoff-worker.ts +45 -258
  72. package/bin/lattices-app.ts +62 -0
  73. package/bin/lattices-dev +4 -0
  74. package/bin/lattices.ts +125 -14
  75. package/docs/agents.md +14 -0
  76. package/docs/api.md +55 -0
  77. package/docs/app.md +3 -0
  78. package/docs/companion-deck.md +180 -0
  79. package/docs/component-extraction-roadmap.md +392 -0
  80. package/docs/config.md +25 -0
  81. package/docs/tiling-reference.md +55 -0
  82. package/docs/voice-error-model.md +73 -0
  83. package/package.json +4 -1
  84. package/app/Sources/App.swift +0 -10
  85. package/app/Sources/CommandPaletteWindow.swift +0 -134
  86. package/app/Sources/MouseFinder.swift +0 -222
  87. /package/app/Sources/{KeyRecorderView.swift → AppShell/KeyRecorderView.swift} +0 -0
  88. /package/app/Sources/{MainWindow.swift → AppShell/MainWindow.swift} +0 -0
  89. /package/app/Sources/{SettingsWindow.swift → AppShell/SettingsWindow.swift} +0 -0
  90. /package/app/Sources/{HotkeyManager.swift → Core/Actions/HotkeyManager.swift} +0 -0
  91. /package/app/Sources/{Intents → Core/Actions/Intents}/CreateLayerIntent.swift +0 -0
  92. /package/app/Sources/{Intents → Core/Actions/Intents}/DistributeIntent.swift +0 -0
  93. /package/app/Sources/{Intents → Core/Actions/Intents}/FocusIntent.swift +0 -0
  94. /package/app/Sources/{Intents → Core/Actions/Intents}/HelpIntent.swift +0 -0
  95. /package/app/Sources/{Intents → Core/Actions/Intents}/KillIntent.swift +0 -0
  96. /package/app/Sources/{Intents → Core/Actions/Intents}/LaunchIntent.swift +0 -0
  97. /package/app/Sources/{Intents → Core/Actions/Intents}/ListSessionsIntent.swift +0 -0
  98. /package/app/Sources/{Intents → Core/Actions/Intents}/ListWindowsIntent.swift +0 -0
  99. /package/app/Sources/{Intents → Core/Actions/Intents}/ScanIntent.swift +0 -0
  100. /package/app/Sources/{Intents → Core/Actions/Intents}/SearchIntent.swift +0 -0
  101. /package/app/Sources/{Intents → Core/Actions/Intents}/SwitchLayerIntent.swift +0 -0
  102. /package/app/Sources/{Intents → Core/Actions/Intents}/TileIntent.swift +0 -0
  103. /package/app/Sources/{DaemonProtocol.swift → Core/Daemon/DaemonProtocol.swift} +0 -0
  104. /package/app/Sources/{DaemonServer.swift → Core/Daemon/DaemonServer.swift} +0 -0
  105. /package/app/Sources/{AccessibilityTextExtractor.swift → Core/Desktop/AccessibilityTextExtractor.swift} +0 -0
  106. /package/app/Sources/{DesktopModelTypes.swift → Core/Desktop/DesktopModelTypes.swift} +0 -0
  107. /package/app/Sources/{InventoryManager.swift → Core/Desktop/InventoryManager.swift} +0 -0
  108. /package/app/Sources/{InventoryPath.swift → Core/Desktop/InventoryPath.swift} +0 -0
  109. /package/app/Sources/{OcrModel.swift → Core/Desktop/OcrModel.swift} +0 -0
  110. /package/app/Sources/{OcrStore.swift → Core/Desktop/OcrStore.swift} +0 -0
  111. /package/app/Sources/{PlacementSpec.swift → Core/Desktop/PlacementSpec.swift} +0 -0
  112. /package/app/Sources/{TilePickerView.swift → Core/Desktop/TilePickerView.swift} +0 -0
  113. /package/app/Sources/{AppWindowShell.swift → Core/Overlays/AppWindowShell.swift} +0 -0
  114. /package/app/Sources/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +0 -0
  115. /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
  116. /package/app/Sources/{HUDBottomBar.swift → Core/Overlays/HUD/HUDBottomBar.swift} +0 -0
  117. /package/app/Sources/{HUDController.swift → Core/Overlays/HUD/HUDController.swift} +0 -0
  118. /package/app/Sources/{HUDLeftBar.swift → Core/Overlays/HUD/HUDLeftBar.swift} +0 -0
  119. /package/app/Sources/{HUDMinimap.swift → Core/Overlays/HUD/HUDMinimap.swift} +0 -0
  120. /package/app/Sources/{HUDState.swift → Core/Overlays/HUD/HUDState.swift} +0 -0
  121. /package/app/Sources/{HUDTopBar.swift → Core/Overlays/HUD/HUDTopBar.swift} +0 -0
  122. /package/app/Sources/{LayerBezel.swift → Core/Overlays/HUD/LayerBezel.swift} +0 -0
  123. /package/app/Sources/{OmniSearchState.swift → Core/Overlays/OmniSearch/OmniSearchState.swift} +0 -0
  124. /package/app/Sources/{DiagnosticLog.swift → Core/System/DiagnosticLog.swift} +0 -0
  125. /package/app/Sources/{EventBus.swift → Core/System/EventBus.swift} +0 -0
  126. /package/app/Sources/{ProcessModel.swift → Core/System/ProcessModel.swift} +0 -0
  127. /package/app/Sources/{ProcessQuery.swift → Core/System/ProcessQuery.swift} +0 -0
  128. /package/app/Sources/{AdvisorLearningStore.swift → Core/Voice/AdvisorLearningStore.swift} +0 -0
  129. /package/app/Sources/{AgentSession.swift → Core/Voice/AgentSession.swift} +0 -0
  130. /package/app/Sources/{AudioProvider.swift → Core/Voice/AudioProvider.swift} +0 -0
  131. /package/app/Sources/{VoiceChatView.swift → Core/Voice/VoiceChatView.swift} +0 -0
  132. /package/app/Sources/{VoxClient.swift → Core/Voice/VoxClient.swift} +0 -0
  133. /package/app/Sources/{Project.swift → Core/Workspace/Project.swift} +0 -0
  134. /package/app/Sources/{ProjectScanner.swift → Core/Workspace/ProjectScanner.swift} +0 -0
  135. /package/app/Sources/{SessionLayerStore.swift → Core/Workspace/SessionLayerStore.swift} +0 -0
  136. /package/app/Sources/{SessionManager.swift → Core/Workspace/SessionManager.swift} +0 -0
  137. /package/app/Sources/{Terminal.swift → Core/Workspace/Terminal/Terminal.swift} +0 -0
  138. /package/app/Sources/{TerminalQuery.swift → Core/Workspace/Terminal/TerminalQuery.swift} +0 -0
  139. /package/app/Sources/{TerminalSynthesizer.swift → Core/Workspace/Terminal/TerminalSynthesizer.swift} +0 -0
  140. /package/app/Sources/{TmuxModel.swift → Core/Workspace/Tmux/TmuxModel.swift} +0 -0
  141. /package/app/Sources/{TmuxQuery.swift → Core/Workspace/Tmux/TmuxQuery.swift} +0 -0
  142. /package/app/Sources/{ActionRow.swift → UI/ActionRow.swift} +0 -0
  143. /package/app/Sources/{OrphanRow.swift → UI/OrphanRow.swift} +0 -0
  144. /package/app/Sources/{ProjectRow.swift → UI/ProjectRow.swift} +0 -0
  145. /package/app/Sources/{TabGroupRow.swift → UI/TabGroupRow.swift} +0 -0
  146. /package/app/Sources/{Theme.swift → UI/Theme.swift} +0 -0
@@ -91,11 +91,14 @@ final class CommandModeState: ObservableObject {
91
91
  @Published var inventory = CommandModeInventory(activeLayer: nil, layerCount: 0, items: [])
92
92
  @Published var chords: [Chord] = []
93
93
  @Published var desktopSnapshot: DesktopInventorySnapshot?
94
- @Published var selectedWindowIds: Set<UInt32> = []
94
+ @Published var selectedWindowIds: Set<UInt32> = [] {
95
+ didSet { syncSharedSelection() }
96
+ }
95
97
  @Published var desktopMode: DesktopInventoryMode = .browsing
96
98
  @Published var activePreset: FilterPreset? = nil
97
99
  @Published var searchQuery: String = ""
98
100
  @Published var isSearching: Bool = false
101
+ @Published var gridPreviewPlacement: PlacementSpec? = nil
99
102
 
100
103
  // MARK: - Marquee Drag State
101
104
  @Published var isDragging: Bool = false
@@ -140,6 +143,26 @@ final class CommandModeState: ObservableObject {
140
143
  selectedWindowIds.first
141
144
  }
142
145
 
146
+ var selectedWindowSummaryText: String {
147
+ let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
148
+ let labels = windows.compactMap { $0.appName }.uniquePrefix(3)
149
+ guard !labels.isEmpty else { return "" }
150
+ if windows.count > labels.count {
151
+ return labels.joined(separator: " • ") + " +\(windows.count - labels.count)"
152
+ }
153
+ return labels.joined(separator: " • ")
154
+ }
155
+
156
+ var gridPreviewRegionLabel: String {
157
+ guard let placement = gridPreviewPlacement else { return "Full Screen" }
158
+ switch placement {
159
+ case .tile(let position):
160
+ return position.label
161
+ default:
162
+ return placement.wireValue
163
+ }
164
+ }
165
+
143
166
  func isSelected(_ id: UInt32) -> Bool {
144
167
  selectedWindowIds.contains(id)
145
168
  }
@@ -344,6 +367,7 @@ final class CommandModeState: ObservableObject {
344
367
  desktopSnapshot = buildDesktopInventory()
345
368
  clearSelection()
346
369
  desktopMode = .browsing
370
+ gridPreviewPlacement = nil
347
371
  phase = .desktopInventory
348
372
  // Don't call onPanelResize here — caller handles initial sizing
349
373
  }
@@ -535,6 +559,7 @@ final class CommandModeState: ObservableObject {
535
559
  if isSearching && selectedWindowIds.isEmpty { return false }
536
560
  if isSearching { deactivateSearch() }
537
561
  if !selectedWindowIds.isEmpty {
562
+ gridPreviewPlacement = nil
538
563
  desktopMode = .gridPreview
539
564
  }
540
565
  return true
@@ -614,15 +639,54 @@ final class CommandModeState: ObservableObject {
614
639
 
615
640
  private func handleGridPreviewKey(_ keyCode: UInt16) -> Bool {
616
641
  switch keyCode {
617
- case 53: // Escape — always dismiss
618
- onDismiss?()
642
+ case 53: // Escape — cancel preview, keep selection
643
+ gridPreviewPlacement = nil
644
+ desktopMode = .browsing
619
645
  return true
620
646
 
621
647
  case 36, 1: // Enter or s → apply the layout
622
- showAndDistributeSelected()
648
+ showAndDistributeSelected(in: gridPreviewPlacement)
649
+ gridPreviewPlacement = nil
623
650
  desktopMode = .browsing
624
651
  return true
625
652
 
653
+ case 123:
654
+ gridPreviewPlacement = .tile(.left)
655
+ return true
656
+ case 124:
657
+ gridPreviewPlacement = .tile(.right)
658
+ return true
659
+ case 126:
660
+ gridPreviewPlacement = .tile(.top)
661
+ return true
662
+ case 125:
663
+ gridPreviewPlacement = .tile(.bottom)
664
+ return true
665
+ case 18:
666
+ gridPreviewPlacement = .tile(.topLeft)
667
+ return true
668
+ case 19:
669
+ gridPreviewPlacement = .tile(.topRight)
670
+ return true
671
+ case 20:
672
+ gridPreviewPlacement = .tile(.bottomLeft)
673
+ return true
674
+ case 21:
675
+ gridPreviewPlacement = .tile(.bottomRight)
676
+ return true
677
+ case 23:
678
+ gridPreviewPlacement = .tile(.leftThird)
679
+ return true
680
+ case 22:
681
+ gridPreviewPlacement = .tile(.centerThird)
682
+ return true
683
+ case 26:
684
+ gridPreviewPlacement = .tile(.rightThird)
685
+ return true
686
+ case 8:
687
+ gridPreviewPlacement = .tile(.center)
688
+ return true
689
+
626
690
  default:
627
691
  return true
628
692
  }
@@ -795,20 +859,8 @@ final class CommandModeState: ObservableObject {
795
859
  private func tileAllSelected(to position: TilePosition) {
796
860
  let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
797
861
  guard !windows.isEmpty else { return }
798
-
799
- // For left/right with 2+ windows: distribute evenly across width
800
- if windows.count >= 2 && (position == .left || position == .right) {
801
- distributeSelectedHorizontally()
802
- return
803
- }
804
-
805
- DiagnosticLog.shared.info("Tile all \(windows.count): \(position.rawValue)")
806
- for win in windows {
807
- WindowTiler.tileWindowById(wid: win.id, pid: win.pid, to: position)
808
- }
809
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
810
- self?.desktopSnapshot = self?.buildDesktopInventory()
811
- }
862
+ DiagnosticLog.shared.info("Grid selected \(windows.count): \(position.rawValue)")
863
+ showAndDistributeSelected(in: .tile(position))
812
864
  }
813
865
 
814
866
  private func distributeSelectedHorizontally() {
@@ -849,28 +901,36 @@ final class CommandModeState: ObservableObject {
849
901
  }
850
902
 
851
903
  /// Show all selected windows AND distribute in smart grid — single batch operation
852
- func showAndDistributeSelected() {
904
+ func showAndDistributeSelected(in placement: PlacementSpec? = nil) {
853
905
  let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
854
906
  guard !windows.isEmpty else { return }
855
907
  savePositions(for: windows)
856
- WindowTiler.batchRaiseAndDistribute(windows: windows.map { (wid: $0.id, pid: $0.pid) })
908
+ WindowTiler.batchRaiseAndDistribute(
909
+ windows: windows.map { (wid: $0.id, pid: $0.pid) },
910
+ region: placement?.fractions
911
+ )
857
912
  let shape = WindowTiler.gridShape(for: windows.count)
858
913
  let grid = shape.map(String.init).joined(separator: "+")
859
- flash("\(windows.count) windows [\(grid)]")
914
+ let region = placement.map { " in \(self.regionLabel(for: $0))" } ?? ""
915
+ flash("\(windows.count) windows\(region) [\(grid)]")
860
916
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
861
917
  self?.desktopSnapshot = self?.buildDesktopInventory()
862
918
  }
863
919
  }
864
920
 
865
921
  /// Distribute selected in smart grid without raising
866
- func distributeSelected() {
922
+ func distributeSelected(in placement: PlacementSpec? = nil) {
867
923
  let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
868
924
  guard !windows.isEmpty else { return }
869
925
  savePositions(for: windows)
870
- WindowTiler.batchRaiseAndDistribute(windows: windows.map { (wid: $0.id, pid: $0.pid) })
926
+ WindowTiler.batchRaiseAndDistribute(
927
+ windows: windows.map { (wid: $0.id, pid: $0.pid) },
928
+ region: placement?.fractions
929
+ )
871
930
  let shape = WindowTiler.gridShape(for: windows.count)
872
931
  let grid = shape.map(String.init).joined(separator: "+")
873
- flash("\(windows.count) windows [\(grid)]")
932
+ let region = placement.map { " in \(self.regionLabel(for: $0))" } ?? ""
933
+ flash("\(windows.count) windows\(region) [\(grid)]")
874
934
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
875
935
  self?.desktopSnapshot = self?.buildDesktopInventory()
876
936
  }
@@ -960,6 +1020,36 @@ final class CommandModeState: ObservableObject {
960
1020
  onDismiss?()
961
1021
  }
962
1022
 
1023
+ private func syncSharedSelection() {
1024
+ guard !selectedWindowIds.isEmpty else {
1025
+ WindowSelectionStore.shared.clear(source: "desktop-inventory")
1026
+ return
1027
+ }
1028
+
1029
+ let summaries = flatWindowList
1030
+ .filter { selectedWindowIds.contains($0.id) }
1031
+ .map {
1032
+ SelectedWindowSummary(
1033
+ wid: $0.id,
1034
+ app: $0.appName ?? "Window",
1035
+ title: $0.title,
1036
+ latticesSession: $0.latticesSession
1037
+ )
1038
+ }
1039
+
1040
+ guard !summaries.isEmpty else { return }
1041
+ WindowSelectionStore.shared.setSelection(summaries, source: "desktop-inventory")
1042
+ }
1043
+
1044
+ private func regionLabel(for placement: PlacementSpec) -> String {
1045
+ switch placement {
1046
+ case .tile(let position):
1047
+ return position.label
1048
+ default:
1049
+ return placement.wireValue
1050
+ }
1051
+ }
1052
+
963
1053
  // MARK: - Inventory Builder
964
1054
 
965
1055
  private func buildInventory() -> CommandModeInventory {
@@ -1360,3 +1450,16 @@ final class CommandModeState: ObservableObject {
1360
1450
  return chords
1361
1451
  }
1362
1452
  }
1453
+
1454
+ private extension Sequence where Element == String {
1455
+ func uniquePrefix(_ count: Int) -> [String] {
1456
+ var seen = Set<String>()
1457
+ var result: [String] = []
1458
+ for item in self where !seen.contains(item) {
1459
+ seen.insert(item)
1460
+ result.append(item)
1461
+ if result.count == count { break }
1462
+ }
1463
+ return result
1464
+ }
1465
+ }