@lattices/cli 0.4.10 → 0.4.11

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 (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +13 -13
  3. package/{app → apps/mac}/Lattices.app/Contents/Info.plist +10 -2
  4. package/{app → apps/mac}/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/{app → apps/mac}/Package.swift +2 -1
  6. package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
  7. package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
  8. package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
  9. package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
  10. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +27 -0
  11. package/apps/mac/Sources/AppShell/AppDelegate.swift +189 -0
  12. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +25 -0
  13. package/{app → apps/mac}/Sources/AppShell/AppShellView.swift +18 -3
  14. package/{app → apps/mac}/Sources/AppShell/AppUpdater.swift +4 -3
  15. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +87 -0
  16. package/{app → apps/mac}/Sources/AppShell/LatticesRuntime.swift +43 -0
  17. package/{app → apps/mac}/Sources/AppShell/MainView.swift +116 -63
  18. package/apps/mac/Sources/AppShell/MenuBarController.swift +177 -0
  19. package/{app → apps/mac}/Sources/AppShell/OnboardingView.swift +72 -60
  20. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +366 -0
  21. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +70 -0
  22. package/{app → apps/mac}/Sources/AppShell/Preferences.swift +37 -2
  23. package/{app → apps/mac}/Sources/AppShell/SettingsView.swift +815 -156
  24. package/{app → apps/mac}/Sources/AppShell/SettingsWindow.swift +10 -0
  25. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +13 -0
  26. package/{app → apps/mac}/Sources/Core/Actions/HotkeyStore.swift +6 -1
  27. package/{app → apps/mac}/Sources/Core/Actions/IntentEngine.swift +2 -0
  28. package/{app → apps/mac}/Sources/Core/Daemon/DaemonServer.swift +5 -0
  29. package/{app → apps/mac}/Sources/Core/Daemon/LatticesApi.swift +365 -0
  30. package/{app → apps/mac}/Sources/Core/Desktop/OcrModel.swift +17 -13
  31. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +33 -0
  32. package/{app → apps/mac}/Sources/Core/Desktop/WindowDragSnapController.swift +18 -217
  33. package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewStore.swift +4 -5
  34. package/{app → apps/mac}/Sources/Core/Desktop/WindowTiler.swift +19 -13
  35. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +124 -0
  36. package/apps/mac/Sources/Core/Input/EventTapThread.swift +54 -0
  37. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +20 -0
  38. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +335 -0
  39. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +141 -0
  40. package/{app → apps/mac}/Sources/Core/Input/MouseGestureConfig.swift +155 -20
  41. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +2259 -0
  42. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +170 -0
  43. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +39 -0
  44. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +624 -0
  45. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +56 -0
  46. package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +8 -8
  47. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +1240 -0
  48. package/{app → apps/mac}/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +11 -23
  49. package/{app → apps/mac}/Sources/Core/Pi/PiChatDock.swift +90 -43
  50. package/{app → apps/mac}/Sources/Core/Pi/PiChatSession.swift +676 -43
  51. package/{app → apps/mac}/Sources/Core/Pi/PiProviderSetupCallout.swift +5 -5
  52. package/{app → apps/mac}/Sources/Core/Pi/PiWorkspaceView.swift +93 -44
  53. package/apps/mac/Sources/Core/System/Capability.swift +79 -0
  54. package/{app → apps/mac}/Sources/Core/System/PermissionChecker.swift +43 -8
  55. package/{app → apps/mac}/Sources/Core/Voice/AudioProvider.swift +225 -56
  56. package/bin/handsoff-infer.ts +14 -5
  57. package/bin/handsoff-worker.ts +11 -7
  58. package/bin/infer.ts +406 -0
  59. package/bin/lattices-app.ts +57 -7
  60. package/bin/lattices-dev +40 -1
  61. package/bin/lattices.ts +1 -1
  62. package/docs/agent-execution-plan.md +9 -9
  63. package/docs/api.md +119 -0
  64. package/docs/app.md +1 -0
  65. package/docs/companion-deck.md +1 -1
  66. package/docs/gesture-customization-proposal.md +520 -0
  67. package/docs/mouse-gestures.md +79 -0
  68. package/docs/overview.md +2 -2
  69. package/docs/presentation-execution-review.md +9 -9
  70. package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
  71. package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
  72. package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
  73. package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
  74. package/docs/reference/dewey.config.ts +74 -0
  75. package/docs/reference/install-agent.md +79 -0
  76. package/docs/repo-structure.md +100 -0
  77. package/docs/voice-error-model.md +7 -7
  78. package/docs/voice.md +18 -0
  79. package/package.json +23 -13
  80. package/swift/Package.swift +20 -0
  81. package/swift/Sources/DeckKit/DeckAction.swift +51 -0
  82. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +152 -0
  83. package/swift/Sources/DeckKit/DeckCockpit.swift +82 -0
  84. package/swift/Sources/DeckKit/DeckHost.swift +7 -0
  85. package/swift/Sources/DeckKit/DeckManifest.swift +145 -0
  86. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +533 -0
  87. package/swift/Sources/DeckKit/DeckTrackpad.swift +63 -0
  88. package/swift/Sources/DeckKit/DeckValue.swift +93 -0
  89. package/swift/Sources/DeckKit/DeckVoiceError.swift +88 -0
  90. package/swift/Tests/DeckKitTests/DeckKitTests.swift +286 -0
  91. package/app/Sources/AppShell/AppDelegate.swift +0 -408
  92. package/app/Sources/Core/Input/KeyboardRemapController.swift +0 -184
  93. package/app/Sources/Core/Input/KeyboardRemapStore.swift +0 -84
  94. package/app/Sources/Core/Input/MouseGestureController.swift +0 -1203
  95. package/app/Sources/Core/Input/MouseShortcutStore.swift +0 -107
  96. /package/{app → apps/mac}/Info.plist +0 -0
  97. /package/{app → apps/mac}/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  98. /package/{app → apps/mac}/Lattices.app/Contents/Resources/tap.wav +0 -0
  99. /package/{app → apps/mac}/Lattices.app/Contents/_CodeSignature/CodeResources +0 -0
  100. /package/{app → apps/mac}/Lattices.entitlements +0 -0
  101. /package/{app → apps/mac}/Resources/tap.wav +0 -0
  102. /package/{app → apps/mac}/Sources/AppShell/App.swift +0 -0
  103. /package/{app → apps/mac}/Sources/AppShell/CliActionLauncher.swift +0 -0
  104. /package/{app → apps/mac}/Sources/AppShell/HomeDashboardView.swift +0 -0
  105. /package/{app → apps/mac}/Sources/AppShell/KeyRecorderView.swift +0 -0
  106. /package/{app → apps/mac}/Sources/AppShell/MainWindow.swift +0 -0
  107. /package/{app → apps/mac}/Sources/Core/Actions/HotkeyManager.swift +0 -0
  108. /package/{app → apps/mac}/Sources/Core/Actions/IntentSchema.swift +0 -0
  109. /package/{app → apps/mac}/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -0
  110. /package/{app → apps/mac}/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -0
  111. /package/{app → apps/mac}/Sources/Core/Actions/Intents/FocusIntent.swift +0 -0
  112. /package/{app → apps/mac}/Sources/Core/Actions/Intents/HelpIntent.swift +0 -0
  113. /package/{app → apps/mac}/Sources/Core/Actions/Intents/KillIntent.swift +0 -0
  114. /package/{app → apps/mac}/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -0
  115. /package/{app → apps/mac}/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -0
  116. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -0
  117. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -0
  118. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ScanIntent.swift +0 -0
  119. /package/{app → apps/mac}/Sources/Core/Actions/Intents/SearchIntent.swift +0 -0
  120. /package/{app → apps/mac}/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -0
  121. /package/{app → apps/mac}/Sources/Core/Actions/Intents/TileIntent.swift +0 -0
  122. /package/{app → apps/mac}/Sources/Core/Actions/PaletteCommand.swift +0 -0
  123. /package/{app → apps/mac}/Sources/Core/Actions/VoiceIntentResolver.swift +0 -0
  124. /package/{app → apps/mac}/Sources/Core/Companion/CompanionActivityLog.swift +0 -0
  125. /package/{app → apps/mac}/Sources/Core/Companion/CompanionKeyboardController.swift +0 -0
  126. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -0
  127. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -0
  128. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -0
  129. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -0
  130. /package/{app → apps/mac}/Sources/Core/Companion/LatticesDeckHost.swift +0 -0
  131. /package/{app → apps/mac}/Sources/Core/Daemon/DaemonProtocol.swift +0 -0
  132. /package/{app → apps/mac}/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -0
  133. /package/{app → apps/mac}/Sources/Core/Desktop/AppTypeClassifier.swift +0 -0
  134. /package/{app → apps/mac}/Sources/Core/Desktop/DesktopModel.swift +0 -0
  135. /package/{app → apps/mac}/Sources/Core/Desktop/DesktopModelTypes.swift +0 -0
  136. /package/{app → apps/mac}/Sources/Core/Desktop/InventoryManager.swift +0 -0
  137. /package/{app → apps/mac}/Sources/Core/Desktop/InventoryPath.swift +0 -0
  138. /package/{app → apps/mac}/Sources/Core/Desktop/MouseFinder.swift +0 -0
  139. /package/{app → apps/mac}/Sources/Core/Desktop/OcrStore.swift +0 -0
  140. /package/{app → apps/mac}/Sources/Core/Desktop/PlacementSpec.swift +0 -0
  141. /package/{app → apps/mac}/Sources/Core/Desktop/SessionWindowLocator.swift +0 -0
  142. /package/{app → apps/mac}/Sources/Core/Desktop/TilePickerView.swift +0 -0
  143. /package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewCard.swift +0 -0
  144. /package/{app → apps/mac}/Sources/Core/Desktop/WindowSelectionStore.swift +0 -0
  145. /package/{app → apps/mac}/Sources/Core/Input/KeyboardRemapConfig.swift +0 -0
  146. /package/{app → apps/mac}/Sources/Core/Input/MouseInputDeviceStore.swift +0 -0
  147. /package/{app → apps/mac}/Sources/Core/Input/MouseInputEventViewer.swift +0 -0
  148. /package/{app → apps/mac}/Sources/Core/Overlays/AppWindowShell.swift +0 -0
  149. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -0
  150. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -0
  151. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -0
  152. /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -0
  153. /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -0
  154. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -0
  155. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -0
  156. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDController.swift +0 -0
  157. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -0
  158. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -0
  159. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -0
  160. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDState.swift +0 -0
  161. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -0
  162. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -0
  163. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -0
  164. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -0
  165. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -0
  166. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -0
  167. /package/{app → apps/mac}/Sources/Core/Overlays/OverlayPanelShell.swift +0 -0
  168. /package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -0
  169. /package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -0
  170. /package/{app → apps/mac}/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -0
  171. /package/{app → apps/mac}/Sources/Core/Pi/PiAuthPromptCard.swift +0 -0
  172. /package/{app → apps/mac}/Sources/Core/Pi/PiInstallCallout.swift +0 -0
  173. /package/{app → apps/mac}/Sources/Core/System/DiagnosticLog.swift +0 -0
  174. /package/{app → apps/mac}/Sources/Core/System/EventBus.swift +0 -0
  175. /package/{app → apps/mac}/Sources/Core/System/ProcessModel.swift +0 -0
  176. /package/{app → apps/mac}/Sources/Core/System/ProcessQuery.swift +0 -0
  177. /package/{app → apps/mac}/Sources/Core/System/SystemTelemetryMonitor.swift +0 -0
  178. /package/{app → apps/mac}/Sources/Core/Voice/AdvisorLearningStore.swift +0 -0
  179. /package/{app → apps/mac}/Sources/Core/Voice/AgentSession.swift +0 -0
  180. /package/{app → apps/mac}/Sources/Core/Voice/HandsOffSession.swift +0 -0
  181. /package/{app → apps/mac}/Sources/Core/Voice/VoiceChatView.swift +0 -0
  182. /package/{app → apps/mac}/Sources/Core/Voice/VoxClient.swift +0 -0
  183. /package/{app → apps/mac}/Sources/Core/Workspace/Project.swift +0 -0
  184. /package/{app → apps/mac}/Sources/Core/Workspace/ProjectScanner.swift +0 -0
  185. /package/{app → apps/mac}/Sources/Core/Workspace/SessionLayerStore.swift +0 -0
  186. /package/{app → apps/mac}/Sources/Core/Workspace/SessionManager.swift +0 -0
  187. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/Terminal.swift +0 -0
  188. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -0
  189. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -0
  190. /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -0
  191. /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -0
  192. /package/{app → apps/mac}/Sources/Core/Workspace/WorkspaceManager.swift +0 -0
  193. /package/{app → apps/mac}/Sources/UI/ActionRow.swift +0 -0
  194. /package/{app → apps/mac}/Sources/UI/OrphanRow.swift +0 -0
  195. /package/{app → apps/mac}/Sources/UI/ProjectRow.swift +0 -0
  196. /package/{app → apps/mac}/Sources/UI/TabGroupRow.swift +0 -0
  197. /package/{app → apps/mac}/Sources/UI/Theme.swift +0 -0
  198. /package/{app → apps/mac}/Tests/StageDragTests.swift +0 -0
  199. /package/{app → apps/mac}/Tests/StageJoinTests.swift +0 -0
  200. /package/{app → apps/mac}/Tests/StageManagerTests.swift +0 -0
  201. /package/{app → apps/mac}/Tests/StageTileTests.swift +0 -0
@@ -14,7 +14,6 @@ struct MainView: View {
14
14
  @StateObject private var inventory = InventoryManager.shared
15
15
  @State private var searchText = ""
16
16
  @State private var hasCheckedSetup = false
17
- @State private var bannerDismissed = false
18
17
  @State private var tmuxBannerDismissed = false
19
18
  @ObservedObject private var tmuxModel = TmuxModel.shared
20
19
  @State private var orphanSectionCollapsed = true
@@ -93,10 +92,13 @@ struct MainView: View {
93
92
  permChecker.check()
94
93
  DiagnosticLog.shared.finish(tPerm)
95
94
 
96
- bannerDismissed = false
97
95
  DiagnosticLog.shared.finish(tTotal)
98
96
  }
99
97
 
98
+ private var visiblyMissingCapabilities: [Capability] {
99
+ Capability.allCases.filter { !$0.isGranted && !prefs.isCapabilityDismissed($0.rawValue) }
100
+ }
101
+
100
102
  private var mainContent: some View {
101
103
  VStack(spacing: 0) {
102
104
  if layout == .popover {
@@ -104,23 +106,24 @@ struct MainView: View {
104
106
  Text("Lattices")
105
107
  .font(Typo.mono(14))
106
108
  .foregroundColor(Palette.text)
109
+ buildChannelBadge
107
110
 
108
111
  Spacer()
109
112
 
110
113
  headerButton(icon: "house") {
111
- (NSApp.delegate as? AppDelegate)?.dismissPopover()
114
+ MenuBarController.shared.dismissPopover()
112
115
  ScreenMapWindowController.shared.showPage(.home)
113
116
  }
114
117
  headerButton(icon: "rectangle.3.group") {
115
- (NSApp.delegate as? AppDelegate)?.dismissPopover()
118
+ MenuBarController.shared.dismissPopover()
116
119
  ScreenMapWindowController.shared.showPage(.screenMap)
117
120
  }
118
121
  headerButton(icon: "magnifyingglass") {
119
- (NSApp.delegate as? AppDelegate)?.dismissPopover()
122
+ MenuBarController.shared.dismissPopover()
120
123
  ScreenMapWindowController.shared.showPage(.desktopInventory)
121
124
  }
122
125
  headerButton(icon: "command") {
123
- (NSApp.delegate as? AppDelegate)?.dismissPopover()
126
+ MenuBarController.shared.dismissPopover()
124
127
  CommandPaletteWindow.shared.toggle()
125
128
  }
126
129
  headerButton(icon: "arrow.clockwise") { scanner.scan(); inventory.refresh() }
@@ -157,8 +160,8 @@ struct MainView: View {
157
160
  .padding(.bottom, 10)
158
161
  }
159
162
 
160
- // Permission banner
161
- if !permChecker.allGranted && !bannerDismissed {
163
+ // Permission banner — only when something is missing AND not snoozed
164
+ if !visiblyMissingCapabilities.isEmpty {
162
165
  permissionBanner
163
166
  }
164
167
 
@@ -401,12 +404,20 @@ struct MainView: View {
401
404
  SettingsWindowController.shared.show()
402
405
  }
403
406
 
404
- HStack(spacing: 4) {
405
- if !permChecker.allGranted {
406
- Circle()
407
- .fill(Palette.detach)
408
- .frame(width: 5, height: 5)
407
+ if !permChecker.allGranted {
408
+ Button {
409
+ PermissionsAssistantWindowController.shared.show()
410
+ } label: {
411
+ HStack(spacing: 4) {
412
+ Circle()
413
+ .fill(Palette.detach)
414
+ .frame(width: 5, height: 5)
415
+ Text("Permissions")
416
+ .font(Typo.mono(9))
417
+ .foregroundColor(Palette.textMuted)
418
+ }
409
419
  }
420
+ .buttonStyle(.plain)
410
421
  }
411
422
 
412
423
  Spacer()
@@ -502,33 +513,61 @@ struct MainView: View {
502
513
  // MARK: - Permission banner
503
514
 
504
515
  private var permissionBanner: some View {
505
- VStack(alignment: .leading, spacing: 6) {
516
+ let missing = visiblyMissingCapabilities
517
+
518
+ return VStack(alignment: .leading, spacing: 8) {
506
519
  HStack {
507
- Image(systemName: "exclamationmark.triangle.fill")
520
+ Image(systemName: "sparkles")
508
521
  .font(.system(size: 10))
509
522
  .foregroundColor(Palette.detach)
510
- Text("PERMISSIONS NEEDED")
523
+ Text("OPTIONAL CAPABILITIES")
511
524
  .font(Typo.monoBold(10))
512
525
  .foregroundColor(Palette.detach)
513
526
  Spacer()
514
- Button { bannerDismissed = true } label: {
515
- Image(systemName: "xmark")
516
- .font(.system(size: 8, weight: .bold))
527
+ Button {
528
+ for cap in missing { prefs.dismissCapability(cap.rawValue) }
529
+ } label: {
530
+ Text("Maybe later")
531
+ .font(Typo.mono(9))
517
532
  .foregroundColor(Palette.textMuted)
518
533
  }
519
534
  .buttonStyle(.plain)
520
535
  }
521
536
 
522
- permissionRow("Accessibility", granted: permChecker.accessibility) {
523
- permChecker.requestAccessibility()
524
- }
525
- permissionRow("Screen Capture", granted: permChecker.screenRecording) {
526
- permChecker.requestScreenRecording()
537
+ Text(bannerSummary(missing))
538
+ .font(Typo.mono(10))
539
+ .foregroundColor(Palette.text)
540
+ .fixedSize(horizontal: false, vertical: true)
541
+
542
+ HStack(spacing: 8) {
543
+ ForEach(missing) { cap in
544
+ capabilityChip(cap)
545
+ }
546
+ Spacer(minLength: 0)
527
547
  }
528
548
 
529
- Text("Click a row to continue the permission flow in macOS.")
530
- .font(Typo.mono(9))
531
- .foregroundColor(Palette.textMuted)
549
+ Button {
550
+ PermissionsAssistantWindowController.shared.show(focus: missing.first)
551
+ } label: {
552
+ HStack(spacing: 6) {
553
+ Image(systemName: "slider.horizontal.3")
554
+ .font(.system(size: 10, weight: .semibold))
555
+ Text("Open Permissions Assistant")
556
+ .font(Typo.monoBold(10))
557
+ }
558
+ .foregroundColor(.white)
559
+ .padding(.horizontal, 10)
560
+ .padding(.vertical, 6)
561
+ .background(
562
+ RoundedRectangle(cornerRadius: 4)
563
+ .fill(Palette.detach.opacity(0.18))
564
+ .overlay(
565
+ RoundedRectangle(cornerRadius: 4)
566
+ .strokeBorder(Palette.detach.opacity(0.45), lineWidth: 0.5)
567
+ )
568
+ )
569
+ }
570
+ .buttonStyle(.plain)
532
571
  }
533
572
  .padding(12)
534
573
  .background(
@@ -543,6 +582,56 @@ struct MainView: View {
543
582
  .padding(.bottom, 10)
544
583
  }
545
584
 
585
+ private func bannerSummary(_ missing: [Capability]) -> String {
586
+ switch missing.count {
587
+ case 0: return ""
588
+ case 1: return "\(missing[0].title) is off. Lattices works without it."
589
+ default: return "\(missing.count) capabilities are off. Turn on whichever you want; the rest of the app works without them."
590
+ }
591
+ }
592
+
593
+ private func capabilityChip(_ cap: Capability) -> some View {
594
+ Button {
595
+ PermissionsAssistantWindowController.shared.show(focus: cap)
596
+ } label: {
597
+ HStack(spacing: 5) {
598
+ Image(systemName: cap.iconName)
599
+ .font(.system(size: 9, weight: .semibold))
600
+ Text(cap.title)
601
+ .font(Typo.mono(9))
602
+ }
603
+ .foregroundColor(Palette.textDim)
604
+ .padding(.horizontal, 7)
605
+ .padding(.vertical, 3)
606
+ .background(
607
+ Capsule()
608
+ .fill(Palette.surface)
609
+ .overlay(
610
+ Capsule().strokeBorder(Palette.border, lineWidth: 0.5)
611
+ )
612
+ )
613
+ }
614
+ .buttonStyle(.plain)
615
+ }
616
+
617
+ private var buildChannelBadge: some View {
618
+ let tint = LatticesRuntime.isDevBuild ? Palette.detach : Palette.running
619
+
620
+ return Text(LatticesRuntime.buildChannelLabel)
621
+ .font(Typo.monoBold(9))
622
+ .foregroundColor(tint)
623
+ .padding(.horizontal, 6)
624
+ .padding(.vertical, 3)
625
+ .background(
626
+ Capsule()
627
+ .fill(tint.opacity(0.12))
628
+ .overlay(
629
+ Capsule()
630
+ .strokeBorder(tint.opacity(0.28), lineWidth: 0.5)
631
+ )
632
+ )
633
+ }
634
+
546
635
  // MARK: - tmux banner
547
636
 
548
637
  private var tmuxBanner: some View {
@@ -607,42 +696,6 @@ struct MainView: View {
607
696
  .padding(.bottom, 10)
608
697
  }
609
698
 
610
- private func permissionRow(_ name: String, granted: Bool, open: @escaping () -> Void) -> some View {
611
- Button(action: { if !granted { open() } }) {
612
- HStack(spacing: 6) {
613
- Image(systemName: granted ? "checkmark.circle.fill" : "circle")
614
- .font(.system(size: 10))
615
- .foregroundColor(granted ? Palette.running : Palette.detach)
616
- Text(name)
617
- .font(Typo.mono(10))
618
- .foregroundColor(Palette.text)
619
- Spacer()
620
- if granted {
621
- Text("granted")
622
- .font(Typo.mono(9))
623
- .foregroundColor(Palette.running)
624
- } else {
625
- HStack(spacing: 4) {
626
- Text("not set")
627
- .font(Typo.mono(9))
628
- .foregroundColor(Palette.detach)
629
- Image(systemName: "arrow.up.forward.square")
630
- .font(.system(size: 9))
631
- .foregroundColor(Palette.detach)
632
- }
633
- }
634
- }
635
- .padding(.vertical, 4)
636
- .padding(.horizontal, 8)
637
- .background(
638
- RoundedRectangle(cornerRadius: 4)
639
- .fill(granted ? Color.clear : Palette.detach.opacity(0.06))
640
- )
641
- }
642
- .buttonStyle(.plain)
643
- .disabled(granted)
644
- }
645
-
646
699
  // MARK: - Helpers
647
700
 
648
701
  private func headerButton(icon: String, action: @escaping () -> Void) -> some View {
@@ -0,0 +1,177 @@
1
+ import AppKit
2
+ import SwiftUI
3
+
4
+ final class MenuBarController: NSObject, NSPopoverDelegate {
5
+ static let shared = MenuBarController()
6
+
7
+ private var statusItem: NSStatusItem?
8
+ private var popover: NSPopover?
9
+ private var contextMenu: NSMenu?
10
+
11
+ var isPopoverShown: Bool {
12
+ popover?.isShown == true
13
+ }
14
+
15
+ private override init() {
16
+ super.init()
17
+ }
18
+
19
+ func start() {
20
+ guard statusItem == nil else { return }
21
+
22
+ statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
23
+ if let button = statusItem?.button {
24
+ button.image = Self.menuBarIcon
25
+ button.action = #selector(statusItemClicked(_:))
26
+ button.sendAction(on: [.leftMouseUp, .rightMouseUp])
27
+ button.target = self
28
+ }
29
+
30
+ contextMenu = buildContextMenu()
31
+ }
32
+
33
+ func warmUpPopover() {
34
+ let popover = makePopover()
35
+ _ = popover.contentViewController?.view
36
+ }
37
+
38
+ func dismissPopover() {
39
+ popover?.performClose(nil)
40
+ }
41
+
42
+ private func showProjectsPopover() {
43
+ guard let button = statusItem?.button else { return }
44
+ let popover = makePopover()
45
+ popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
46
+ popover.contentViewController?.view.window?.makeKey()
47
+ }
48
+
49
+ @objc private func statusItemClicked(_ sender: Any?) {
50
+ guard let event = NSApp.currentEvent,
51
+ let button = statusItem?.button else { return }
52
+
53
+ if event.type == .rightMouseUp {
54
+ contextMenu?.popUp(positioning: nil, at: NSPoint(x: 0, y: button.bounds.height + 4), in: button)
55
+ } else if let shown = popover, shown.isShown {
56
+ shown.performClose(sender)
57
+ } else {
58
+ showProjectsPopover()
59
+ }
60
+ }
61
+
62
+ private func makePopover() -> NSPopover {
63
+ if let popover { return popover }
64
+ let timed = DiagnosticLog.shared.startTimed("makePopover")
65
+ let popover = NSPopover()
66
+ popover.contentViewController = NSHostingController(rootView: MainView(scanner: ProjectScanner.shared))
67
+ popover.behavior = .transient
68
+ popover.contentSize = NSSize(width: 380, height: 300)
69
+ popover.appearance = NSAppearance(named: .darkAqua)
70
+ popover.delegate = self
71
+ self.popover = popover
72
+ DiagnosticLog.shared.finish(timed)
73
+ return popover
74
+ }
75
+
76
+ func popoverWillShow(_ notification: Notification) {
77
+ AppActivationCoordinator.shared.refresh()
78
+ NotificationCenter.default.post(name: .latticesPopoverWillShow, object: nil)
79
+ }
80
+
81
+ func popoverDidClose(_ notification: Notification) {
82
+ AppActivationCoordinator.shared.refresh()
83
+ }
84
+
85
+ private func buildContextMenu() -> NSMenu {
86
+ let menu = NSMenu()
87
+
88
+ let actions: [(String, String, Selector)] = [
89
+ ("Home", "", #selector(menuWorkspace)),
90
+ ("Layout", "", #selector(menuLayout)),
91
+ ("Search", "", #selector(menuSearch)),
92
+ ("Command Palette", "⌘⇧M", #selector(menuCommandPalette)),
93
+ ]
94
+ for (title, shortcut, action) in actions {
95
+ let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
96
+ item.target = self
97
+ if !shortcut.isEmpty {
98
+ // Display-only; the actual hotkey is global.
99
+ }
100
+ menu.addItem(item)
101
+ }
102
+
103
+ menu.addItem(.separator())
104
+
105
+ let cliActions: [(String, Selector)] = [
106
+ ("Projects…", #selector(menuProjects)),
107
+ ("Initialize Project in Terminal…", #selector(menuInitializeProject)),
108
+ ("Launch Project in Terminal…", #selector(menuLaunchProject)),
109
+ ]
110
+ for (title, action) in cliActions {
111
+ let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
112
+ item.target = self
113
+ menu.addItem(item)
114
+ }
115
+
116
+ menu.addItem(.separator())
117
+
118
+ let update = NSMenuItem(title: "Update Lattices…", action: #selector(menuUpdate), keyEquivalent: "")
119
+ update.target = self
120
+ menu.addItem(update)
121
+
122
+ menu.addItem(.separator())
123
+
124
+ let settings = NSMenuItem(title: "Help & Settings…", action: #selector(menuSettings), keyEquivalent: ",")
125
+ settings.target = self
126
+ menu.addItem(settings)
127
+
128
+ menu.addItem(.separator())
129
+
130
+ let quit = NSMenuItem(title: "Quit Lattices", action: #selector(menuQuit), keyEquivalent: "q")
131
+ quit.target = self
132
+ menu.addItem(quit)
133
+
134
+ return menu
135
+ }
136
+
137
+ @objc private func menuCommandPalette() { CommandPaletteWindow.shared.toggle() }
138
+ @objc private func menuWorkspace() { ScreenMapWindowController.shared.showPage(.home) }
139
+ @objc private func menuLayout() { ScreenMapWindowController.shared.showPage(.screenMap) }
140
+ @objc private func menuSearch() { ScreenMapWindowController.shared.showPage(.desktopInventory) }
141
+ @objc private func menuProjects() { DispatchQueue.main.async { self.showProjectsPopover() } }
142
+ @objc private func menuInitializeProject() { CliActionLauncher.initializeProjectInTerminal() }
143
+ @objc private func menuLaunchProject() { CliActionLauncher.launchProjectInTerminal() }
144
+ @MainActor @objc private func menuUpdate() { AppUpdater.shared.promptForUpdate() }
145
+ @objc private func menuSettings() { SettingsWindowController.shared.show() }
146
+ @objc private func menuQuit() { NSApp.terminate(nil) }
147
+
148
+ private static let menuBarIcon: NSImage = {
149
+ let size: CGFloat = 18
150
+ let image = NSImage(size: NSSize(width: size, height: size), flipped: true) { _ in
151
+ let pad: CGFloat = 2
152
+ let gap: CGFloat = 1.5
153
+ let cellSize = (size - 2 * pad - 2 * gap) / 3
154
+ let solidCells: Set<Int> = [0, 3, 6, 7, 8]
155
+
156
+ for row in 0..<3 {
157
+ for column in 0..<3 {
158
+ let index = row * 3 + column
159
+ let x = pad + CGFloat(column) * (cellSize + gap)
160
+ let y = pad + CGFloat(row) * (cellSize + gap)
161
+ let rect = NSRect(x: x, y: y, width: cellSize, height: cellSize)
162
+
163
+ if solidCells.contains(index) {
164
+ NSColor.black.setFill()
165
+ } else {
166
+ NSColor.black.withAlphaComponent(0.25).setFill()
167
+ }
168
+ let path = NSBezierPath(roundedRect: rect, xRadius: 0.8, yRadius: 0.8)
169
+ path.fill()
170
+ }
171
+ }
172
+ return true
173
+ }
174
+ image.isTemplate = true
175
+ return image
176
+ }()
177
+ }
@@ -4,8 +4,8 @@ import AppKit
4
4
  // MARK: - Onboarding Flow
5
5
 
6
6
  /// A step-by-step welcome screen shown on first launch.
7
- /// Walks the user through granting Accessibility, Screen Recording,
8
- /// choosing a project root, and optionally installing tmux.
7
+ /// Keeps setup quiet: permissions are introduced as optional capabilities
8
+ /// and requested later from the feature that needs them.
9
9
  struct OnboardingView: View {
10
10
  @ObservedObject private var permChecker = PermissionChecker.shared
11
11
  @ObservedObject private var prefs = Preferences.shared
@@ -15,8 +15,7 @@ struct OnboardingView: View {
15
15
 
16
16
  enum Step: Int, CaseIterable {
17
17
  case welcome
18
- case accessibility
19
- case screenRecording
18
+ case capabilities
20
19
  case projectRoot
21
20
  case tmux
22
21
  case done
@@ -39,8 +38,7 @@ struct OnboardingView: View {
39
38
  Group {
40
39
  switch step {
41
40
  case .welcome: welcomeStep
42
- case .accessibility: accessibilityStep
43
- case .screenRecording: screenRecordingStep
41
+ case .capabilities: capabilitiesStep
44
42
  case .projectRoot: projectRootStep
45
43
  case .tmux: tmuxStep
46
44
  case .done: doneStep
@@ -98,24 +96,37 @@ struct OnboardingView: View {
98
96
  }
99
97
  }
100
98
 
101
- private var accessibilityStep: some View {
102
- permissionStep(
103
- icon: "hand.raised.fill",
104
- title: "Accessibility",
105
- description: "Lattices needs Accessibility access to read window titles, move and resize windows, and tile your workspace.",
106
- granted: permChecker.accessibility,
107
- action: { permChecker.requestAccessibility() }
108
- )
109
- }
99
+ private var capabilitiesStep: some View {
100
+ VStack(spacing: 16) {
101
+ Image(systemName: "slider.horizontal.3")
102
+ .font(.system(size: 28))
103
+ .foregroundColor(.white.opacity(0.7))
110
104
 
111
- private var screenRecordingStep: some View {
112
- permissionStep(
113
- icon: "rectangle.dashed.badge.record",
114
- title: "Screen Capture",
115
- description: "Allows Lattices to index on-screen text with OCR so you can search across all your windows. On newer macOS versions this finishes in System Settings under Screen & System Audio Recording.",
116
- granted: permChecker.screenRecording,
117
- action: { permChecker.requestScreenRecording() }
118
- )
105
+ Text("Enable more when you need it")
106
+ .font(Typo.title(16))
107
+ .foregroundColor(Palette.text)
108
+
109
+ Text("Lattices launches projects without any extra permissions. Click a capability to set it up now in the Permissions Assistant or skip and turn it on later.")
110
+ .font(Typo.body(12))
111
+ .foregroundColor(Palette.textDim)
112
+ .multilineTextAlignment(.center)
113
+ .lineSpacing(3)
114
+
115
+ VStack(alignment: .leading, spacing: 8) {
116
+ ForEach(Capability.allCases) { cap in
117
+ capabilityRow(cap)
118
+ }
119
+ }
120
+ .padding(10)
121
+ .background(
122
+ RoundedRectangle(cornerRadius: 6)
123
+ .fill(Palette.surface)
124
+ .overlay(
125
+ RoundedRectangle(cornerRadius: 6)
126
+ .strokeBorder(Palette.border, lineWidth: 0.5)
127
+ )
128
+ )
129
+ }
119
130
  }
120
131
 
121
132
  private var projectRootStep: some View {
@@ -251,8 +262,7 @@ struct OnboardingView: View {
251
262
  .foregroundColor(Palette.text)
252
263
 
253
264
  VStack(alignment: .leading, spacing: 8) {
254
- statusRow("Accessibility", granted: permChecker.accessibility)
255
- statusRow("Screen Recording", granted: permChecker.screenRecording)
265
+ statusRow("Permission prompts", granted: true, detail: "shown when needed")
256
266
  statusRow("Project root", granted: !prefs.scanRoot.isEmpty,
257
267
  detail: prefs.scanRoot.isEmpty ? "not set" : abbreviatePath(prefs.scanRoot))
258
268
  statusRow("tmux", granted: tmux.isAvailable,
@@ -293,43 +303,47 @@ struct OnboardingView: View {
293
303
 
294
304
  // MARK: - Shared helpers
295
305
 
296
- private func permissionStep(icon: String, title: String, description: String, granted: Bool, action: @escaping () -> Void) -> some View {
297
- VStack(spacing: 16) {
298
- Image(systemName: icon)
299
- .font(.system(size: 28))
300
- .foregroundColor(.white.opacity(0.7))
301
-
302
- Text(title)
303
- .font(Typo.title(16))
304
- .foregroundColor(Palette.text)
305
-
306
- Text(description)
307
- .font(Typo.body(12))
308
- .foregroundColor(Palette.textDim)
309
- .multilineTextAlignment(.center)
310
- .lineSpacing(3)
311
-
312
- if granted {
313
- HStack(spacing: 6) {
314
- Image(systemName: "checkmark.circle.fill")
315
- .foregroundColor(Palette.running)
316
- Text("Granted")
306
+ private func capabilityRow(_ cap: Capability) -> some View {
307
+ let granted = cap.isGranted
308
+ return Button {
309
+ PermissionsAssistantWindowController.shared.show(focus: cap)
310
+ } label: {
311
+ HStack(alignment: .top, spacing: 10) {
312
+ Image(systemName: cap.iconName)
313
+ .font(.system(size: 12, weight: .semibold))
314
+ .foregroundColor(granted ? Palette.running : Palette.text.opacity(0.85))
315
+ .frame(width: 18)
316
+ VStack(alignment: .leading, spacing: 3) {
317
+ Text(cap.title)
317
318
  .font(Typo.monoBold(11))
318
- .foregroundColor(Palette.running)
319
- }
320
- } else {
321
- Button(action: action) {
322
- Text("Grant \(title)")
323
- .angularButton(.white, filled: false)
319
+ .foregroundColor(Palette.text)
320
+ Text(cap.requirementLabel)
321
+ .font(Typo.mono(10))
322
+ .foregroundColor(Palette.textMuted)
324
323
  }
325
- .buttonStyle(.plain)
326
-
327
- Text("macOS may send you to System Settings to finish this step.")
328
- .font(Typo.mono(10))
329
- .foregroundColor(Palette.textMuted)
330
- .multilineTextAlignment(.center)
324
+ Spacer(minLength: 0)
325
+ Text(granted ? "ON" : "Set up")
326
+ .font(Typo.monoBold(9))
327
+ .foregroundColor(granted ? Palette.running : Palette.textDim)
328
+ .padding(.horizontal, 6)
329
+ .padding(.vertical, 3)
330
+ .background(
331
+ Capsule().fill((granted ? Palette.running : Palette.borderLit).opacity(0.12))
332
+ )
331
333
  }
334
+ .padding(.horizontal, 8)
335
+ .padding(.vertical, 6)
336
+ .contentShape(Rectangle())
337
+ .background(
338
+ RoundedRectangle(cornerRadius: 5)
339
+ .fill(Color.clear)
340
+ .overlay(
341
+ RoundedRectangle(cornerRadius: 5)
342
+ .strokeBorder(Palette.border.opacity(0.6), lineWidth: 0.5)
343
+ )
344
+ )
332
345
  }
346
+ .buttonStyle(.plain)
333
347
  }
334
348
 
335
349
  private func statusRow(_ label: String, granted: Bool, detail: String? = nil) -> some View {
@@ -382,8 +396,6 @@ struct OnboardingView: View {
382
396
 
383
397
  private var nextLabel: String {
384
398
  switch step {
385
- case .accessibility where !permChecker.accessibility: return "Continue anyway"
386
- case .screenRecording where !permChecker.screenRecording: return "Continue anyway"
387
399
  case .projectRoot where prefs.scanRoot.isEmpty: return "Skip for now"
388
400
  case .tmux where !tmux.isAvailable: return "Skip"
389
401
  default: return "Continue"