@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
package/README.md CHANGED
@@ -58,6 +58,9 @@ To build a signed, notarized DMG for distribution:
58
58
  ```sh
59
59
  # Requires a Developer ID certificate and notarytool keychain profile
60
60
  ./scripts/build-dmg.sh
61
+
62
+ # Update v<package.json version> and upload the DMG to GitHub Releases
63
+ ./scripts/ship.sh
61
64
  ```
62
65
 
63
66
  ## Quick start
package/app/Info.plist CHANGED
@@ -15,9 +15,9 @@
15
15
  <key>CFBundlePackageType</key>
16
16
  <string>APPL</string>
17
17
  <key>CFBundleVersion</key>
18
- <string>0.4.2</string>
18
+ <string>0.4.6</string>
19
19
  <key>CFBundleShortVersionString</key>
20
- <string>0.4.2</string>
20
+ <string>0.4.6</string>
21
21
  <key>LSMinimumSystemVersion</key>
22
22
  <string>13.0</string>
23
23
  <key>LSUIElement</key>
@@ -15,9 +15,9 @@
15
15
  <key>CFBundlePackageType</key>
16
16
  <string>APPL</string>
17
17
  <key>CFBundleVersion</key>
18
- <string>0.4.2</string>
18
+ <string>0.4.6</string>
19
19
  <key>CFBundleShortVersionString</key>
20
- <string>0.4.2</string>
20
+ <string>0.4.6</string>
21
21
  <key>LSMinimumSystemVersion</key>
22
22
  <string>13.0</string>
23
23
  <key>LSUIElement</key>
package/app/Package.swift CHANGED
@@ -4,9 +4,15 @@ import PackageDescription
4
4
  let package = Package(
5
5
  name: "Lattices",
6
6
  platforms: [.macOS(.v13)],
7
+ dependencies: [
8
+ .package(path: "../swift")
9
+ ],
7
10
  targets: [
8
11
  .executableTarget(
9
12
  name: "Lattices",
13
+ dependencies: [
14
+ .product(name: "DeckKit", package: "swift")
15
+ ],
10
16
  path: "Sources",
11
17
  resources: [
12
18
  .copy("../Resources/tap.wav"),
@@ -0,0 +1,20 @@
1
+ import SwiftUI
2
+
3
+ @main
4
+ struct LatticesApp: App {
5
+ @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate
6
+
7
+ var body: some Scene {
8
+ Settings { EmptyView() }
9
+ .commands {
10
+ CommandGroup(after: .appInfo) {
11
+ Button("Update Lattices…") {
12
+ AppUpdater.shared.promptForUpdate()
13
+ }
14
+ .disabled(!AppUpdater.shared.canUpdate)
15
+
16
+ Divider()
17
+ }
18
+ }
19
+ }
20
+ }
@@ -1,9 +1,14 @@
1
1
  import AppKit
2
2
  import SwiftUI
3
3
 
4
+ extension Notification.Name {
5
+ static let latticesPopoverWillShow = Notification.Name("latticesPopoverWillShow")
6
+ }
7
+
4
8
  /// Manages the NSStatusItem (menu bar icon), left-click popover, and right-click context menu.
5
9
  /// Replaces the previous SwiftUI MenuBarExtra approach for full click-event control.
6
10
  class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
11
+ private static weak var shared: AppDelegate?
7
12
 
8
13
  private var statusItem: NSStatusItem!
9
14
  private var popover: NSPopover?
@@ -45,6 +50,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
45
50
  /// based on whether any managed windows are open.
46
51
  static func updateActivationPolicy() {
47
52
  let hasVisibleWindow =
53
+ (Self.shared?.popover?.isShown == true) ||
48
54
  CommandModeWindow.shared.isVisible ||
49
55
  CommandPaletteWindow.shared.isVisible ||
50
56
  MainWindow.shared.isVisible ||
@@ -60,6 +66,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
60
66
  }
61
67
 
62
68
  func applicationDidFinishLaunching(_ notification: Notification) {
69
+ Self.shared = self
63
70
  NSApp.setActivationPolicy(.accessory)
64
71
  NSApp.appearance = NSAppearance(named: .darkAqua)
65
72
 
@@ -82,8 +89,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
82
89
  let store = HotkeyStore.shared
83
90
  store.register(action: .palette) { CommandPaletteWindow.shared.toggle() }
84
91
  store.register(action: .unifiedWindow) { ScreenMapWindowController.shared.toggle() }
85
- store.register(action: .bezel) { WindowBezel.showBezelForFrontmostWindow() }
86
- store.register(action: .cheatSheet) { CheatSheetHUD.shared.toggle() }
92
+ store.register(action: .bezel) { Self.showWorkspaceInspector() }
93
+ store.register(action: .cheatSheet) { SettingsWindowController.shared.show() }
94
+ store.register(action: .desktopInventory) {
95
+ DiagnosticLog.shared.info("Hotkey: desktopInventory triggered")
96
+ ScreenMapWindowController.shared.showPage(.desktopInventory)
97
+ }
87
98
  store.register(action: .voiceCommand) {
88
99
  DiagnosticLog.shared.info("Hotkey: voiceCommand triggered")
89
100
  VoiceCommandWindow.shared.toggle()
@@ -103,7 +114,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
103
114
 
104
115
  // Pre-render HUD panels off-screen for instant first open
105
116
  DispatchQueue.main.async { HUDController.shared.warmUp() }
117
+ // Pre-build the menu bar popover so the first click doesn't pay the SwiftUI mount cost.
118
+ // Touching `.view` forces NSHostingController to materialize the SwiftUI view tree.
119
+ DispatchQueue.main.async { [weak self] in
120
+ guard let self = self else { return }
121
+ let p = self.makePopover()
122
+ _ = p.contentViewController?.view
123
+ }
106
124
  store.register(action: .omniSearch) { OmniSearchWindow.shared.toggle() }
125
+ WindowDragSnapController.shared.start()
126
+ MouseGestureController.shared.start()
107
127
 
108
128
  // Session layer cycling
109
129
  store.register(action: .layerNext) { SessionLayerStore.shared.cycleNext() }
@@ -140,7 +160,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
140
160
  for (action, position) in tileMap {
141
161
  store.register(action: action) { WindowTiler.tileFrontmostViaAX(to: position) }
142
162
  }
143
- store.register(action: .tileDistribute) { WindowTiler.distributeVisible() }
163
+ store.register(action: .tileDistribute) { WindowTiler.distributeVisible(reactivateLattices: false) }
164
+ store.register(action: .tileTypeGrid) { WindowTiler.distributeVisibleByFrontmostType(reactivateLattices: false) }
144
165
 
145
166
  // Onboarding on first launch; otherwise just check permissions
146
167
  if !OnboardingWindowController.shared.showIfNeeded() {
@@ -157,6 +178,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
157
178
  ProcessModel.shared.start()
158
179
  LatticesApi.setup()
159
180
  DaemonServer.shared.start()
181
+ LatticesCompanionBridgeServer.shared.start()
160
182
  AgentPool.shared.start()
161
183
  diag.finish(tBoot)
162
184
 
@@ -167,14 +189,19 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
167
189
  }
168
190
  }
169
191
 
170
- // --screen-map flag: auto-open screen map on launch
192
+ // --screen-map flag: auto-open layout on launch
171
193
  if CommandLine.arguments.contains("--screen-map") {
172
194
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
173
- ScreenMapWindowController.shared.show()
195
+ ScreenMapWindowController.shared.showPage(.screenMap)
174
196
  }
175
197
  }
176
198
  }
177
199
 
200
+ func applicationWillTerminate(_ notification: Notification) {
201
+ LatticesCompanionBridgeServer.shared.stop()
202
+ DaemonServer.shared.stop()
203
+ }
204
+
178
205
  // MARK: - Status item click handler
179
206
 
180
207
  @objc private func statusItemClicked(_ sender: Any?) {
@@ -184,13 +211,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
184
211
  // Right-click → context menu
185
212
  contextMenu.popUp(positioning: nil, at: NSPoint(x: 0, y: button.bounds.height + 4), in: button)
186
213
  } else {
187
- // Left-click → toggle popover
214
+ // Left-click → toggle the menu bar projects popover.
188
215
  if let shown = popover, shown.isShown {
189
216
  shown.performClose(sender)
190
217
  } else {
191
- let p = makePopover()
192
- p.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
193
- p.contentViewController?.view.window?.makeKey()
218
+ showProjectsPopover()
194
219
  }
195
220
  }
196
221
  }
@@ -200,14 +225,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
200
225
  popover?.performClose(nil)
201
226
  }
202
227
 
203
- /// Create a fresh popover each time so the SwiftUI view tree isn't kept alive
204
- /// when the popover is closed prevents continuous CPU usage from @Published updates.
228
+ /// Cached popover built lazily on first click, reused on every subsequent open.
229
+ /// Keeping the SwiftUI view tree alive avoids rebuilding on each click (slow first paint).
230
+ /// Data refresh is driven from `popoverWillShow` + a notification MainView listens to.
205
231
  private func makePopover() -> NSPopover {
232
+ if let p = popover { return p }
206
233
  let t = DiagnosticLog.shared.startTimed("makePopover")
207
234
  let p = NSPopover()
208
235
  p.contentViewController = NSHostingController(rootView: MainView(scanner: ProjectScanner.shared))
209
236
  p.behavior = .transient
210
- p.contentSize = NSSize(width: 380, height: 560)
237
+ p.contentSize = NSSize(width: 380, height: 300)
211
238
  p.appearance = NSAppearance(named: .darkAqua)
212
239
  p.delegate = self
213
240
  popover = p
@@ -215,10 +242,20 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
215
242
  return p
216
243
  }
217
244
 
245
+ private func showProjectsPopover() {
246
+ guard let button = statusItem.button else { return }
247
+ let p = makePopover()
248
+ p.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
249
+ p.contentViewController?.view.window?.makeKey()
250
+ }
251
+
252
+ func popoverWillShow(_ notification: Notification) {
253
+ Self.updateActivationPolicy()
254
+ NotificationCenter.default.post(name: .latticesPopoverWillShow, object: nil)
255
+ }
256
+
218
257
  func popoverDidClose(_ notification: Notification) {
219
- // Tear down the SwiftUI view tree so observed models stop driving re-renders
220
- popover?.contentViewController = nil
221
- popover = nil
258
+ Self.updateActivationPolicy()
222
259
  }
223
260
 
224
261
  // MARK: - Context menu
@@ -227,10 +264,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
227
264
  let menu = NSMenu()
228
265
 
229
266
  let actions: [(String, String, Selector)] = [
267
+ ("Home", "", #selector(menuWorkspace)),
268
+ ("Layout", "", #selector(menuLayout)),
269
+ ("Search", "", #selector(menuSearch)),
230
270
  ("Command Palette", "⌘⇧M", #selector(menuCommandPalette)),
231
- ("Workspace", "", #selector(menuWorkspace)),
232
- ("Assistant", "", #selector(menuAssistant)),
233
- ("Help & Shortcuts", "", #selector(menuDocs)),
234
271
  ]
235
272
  for (title, shortcut, action) in actions {
236
273
  let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
@@ -243,14 +280,29 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
243
280
 
244
281
  menu.addItem(.separator())
245
282
 
246
- let settings = NSMenuItem(title: "Settings…", action: #selector(menuSettings), keyEquivalent: ",")
283
+ let cliActions: [(String, Selector)] = [
284
+ ("Projects…", #selector(menuProjects)),
285
+ ("Initialize Project in Terminal…", #selector(menuInitializeProject)),
286
+ ("Launch Project in Terminal…", #selector(menuLaunchProject)),
287
+ ]
288
+ for (title, action) in cliActions {
289
+ let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
290
+ item.target = self
291
+ menu.addItem(item)
292
+ }
293
+
294
+ menu.addItem(.separator())
295
+
296
+ let update = NSMenuItem(title: "Update Lattices…", action: #selector(menuUpdate), keyEquivalent: "")
297
+ update.target = self
298
+ menu.addItem(update)
299
+
300
+ menu.addItem(.separator())
301
+
302
+ let settings = NSMenuItem(title: "Help & Settings…", action: #selector(menuSettings), keyEquivalent: ",")
247
303
  settings.target = self
248
304
  menu.addItem(settings)
249
305
 
250
- let diag = NSMenuItem(title: "Diagnostics", action: #selector(menuDiagnostics), keyEquivalent: "")
251
- diag.target = self
252
- menu.addItem(diag)
253
-
254
306
  menu.addItem(.separator())
255
307
 
256
308
  let quit = NSMenuItem(title: "Quit Lattices", action: #selector(menuQuit), keyEquivalent: "q")
@@ -262,19 +314,27 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
262
314
 
263
315
  @objc private func menuCommandPalette() { CommandPaletteWindow.shared.toggle() }
264
316
  @objc private func menuWorkspace() { ScreenMapWindowController.shared.showPage(.home) }
265
- @objc private func menuAssistant() {
266
- if AudioLayer.shared.isListening || VoiceCommandWindow.shared.isVisible {
267
- VoiceCommandWindow.shared.toggle()
268
- } else {
269
- OmniSearchWindow.shared.show()
270
- }
271
- }
272
- @objc private func menuDocs() { ScreenMapWindowController.shared.showPage(.docs) }
317
+ @objc private func menuLayout() { ScreenMapWindowController.shared.showPage(.screenMap) }
318
+ @objc private func menuSearch() { ScreenMapWindowController.shared.showPage(.desktopInventory) }
319
+ @objc private func menuDocs() { SettingsWindowController.shared.show() }
320
+ @objc private func menuProjects() { DispatchQueue.main.async { self.showProjectsPopover() } }
321
+ @objc private func menuInitializeProject() { CliActionLauncher.initializeProjectInTerminal() }
322
+ @objc private func menuLaunchProject() { CliActionLauncher.launchProjectInTerminal() }
273
323
  @objc private func menuHUD() { HUDController.shared.toggle() }
274
- @objc private func menuWindowBezel() { WindowBezel.showBezelForFrontmostWindow() }
275
- @objc private func menuCheatSheet() { CheatSheetHUD.shared.toggle() }
324
+ @objc private func menuWindowBezel() { Self.showWorkspaceInspector() }
325
+ @objc private func menuCheatSheet() { SettingsWindowController.shared.show() }
276
326
  @objc private func menuOmniSearch() { OmniSearchWindow.shared.toggle() }
327
+ @MainActor @objc private func menuUpdate() { AppUpdater.shared.promptForUpdate() }
277
328
  @objc private func menuSettings() { SettingsWindowController.shared.show() }
278
- @objc private func menuDiagnostics() { DiagnosticWindow.shared.toggle() }
279
329
  @objc private func menuQuit() { NSApp.terminate(nil) }
330
+
331
+ private static func showWorkspaceInspector() {
332
+ guard let entry = DesktopModel.shared.frontmostWindow(),
333
+ entry.app != "Lattices" else {
334
+ ScreenMapWindowController.shared.showPage(.screenMap)
335
+ return
336
+ }
337
+
338
+ ScreenMapWindowController.shared.showWindow(wid: entry.wid)
339
+ }
280
340
  }
@@ -56,6 +56,10 @@ struct AppShellView: View {
56
56
  .background(Palette.bg)
57
57
  .onAppear {
58
58
  commandState.onDismiss = { windowController.activePage = .home }
59
+ syncPageState(windowController.activePage)
60
+ }
61
+ .onChange(of: windowController.activePage) { page in
62
+ syncPageState(page)
59
63
  }
60
64
  }
61
65
 
@@ -66,6 +70,7 @@ struct AppShellView: View {
66
70
  ForEach(AppPage.primaryTabs, id: \.rawValue) { tab in
67
71
  tabButton(tab)
68
72
  }
73
+
69
74
  Spacer()
70
75
  }
71
76
  .padding(.horizontal, 12)
@@ -90,6 +95,7 @@ struct AppShellView: View {
90
95
  .foregroundColor(isActive ? Palette.text : Palette.textMuted)
91
96
  .padding(.horizontal, 12)
92
97
  .padding(.vertical, 6)
98
+ .contentShape(Rectangle())
93
99
  .background(
94
100
  RoundedRectangle(cornerRadius: 6)
95
101
  .fill(isActive ? Palette.surfaceHov : Color.clear)
@@ -114,7 +120,7 @@ struct AppShellView: View {
114
120
  windowController.activePage = page
115
121
  })
116
122
  case .desktopInventory:
117
- CommandModeView(state: commandState)
123
+ CommandModeView(state: commandState, presentation: .embedded)
118
124
  case .pi:
119
125
  PiWorkspaceView()
120
126
  case .settings:
@@ -132,4 +138,9 @@ struct AppShellView: View {
132
138
  )
133
139
  }
134
140
  }
141
+
142
+ private func syncPageState(_ page: AppPage) {
143
+ if page == .screenMap { controller.enter() }
144
+ if page == .desktopInventory { commandState.enter() }
145
+ }
135
146
  }
@@ -0,0 +1,92 @@
1
+ import AppKit
2
+ import Combine
3
+ import Foundation
4
+
5
+ @MainActor
6
+ final class AppUpdater: ObservableObject {
7
+ static let shared = AppUpdater()
8
+
9
+ @Published private(set) var isUpdating = false
10
+ @Published private(set) var statusMessage: String?
11
+
12
+ private init() {}
13
+
14
+ var currentVersion: String { LatticesRuntime.appVersion }
15
+
16
+ var canUpdate: Bool {
17
+ LatticesRuntime.bunPath != nil && LatticesRuntime.appHelperScriptPath != nil
18
+ }
19
+
20
+ var unavailableReason: String? {
21
+ if LatticesRuntime.bunPath == nil {
22
+ return "Install Bun to enable in-app updates."
23
+ }
24
+ if LatticesRuntime.appHelperScriptPath == nil {
25
+ return "Launch Lattices via `lattices app` so the updater can find the CLI bundle."
26
+ }
27
+ return nil
28
+ }
29
+
30
+ func promptForUpdate() {
31
+ guard canUpdate else {
32
+ presentAlert(
33
+ title: "Update Unavailable",
34
+ message: unavailableReason ?? "Lattices could not locate its updater."
35
+ )
36
+ return
37
+ }
38
+
39
+ let alert = NSAlert()
40
+ alert.alertStyle = .informational
41
+ alert.messageText = "Install the latest Lattices app update?"
42
+ alert.informativeText = "Lattices will download the latest released app bundle, close, and relaunch when the update is ready."
43
+ alert.addButton(withTitle: "Update")
44
+ alert.addButton(withTitle: "Cancel")
45
+
46
+ guard alert.runModal() == .alertFirstButtonReturn else { return }
47
+ startDetachedUpdate()
48
+ }
49
+
50
+ private func startDetachedUpdate() {
51
+ guard !isUpdating else { return }
52
+ guard let bunPath = LatticesRuntime.bunPath,
53
+ let scriptPath = LatticesRuntime.appHelperScriptPath else {
54
+ presentAlert(
55
+ title: "Update Unavailable",
56
+ message: unavailableReason ?? "Lattices could not locate its updater."
57
+ )
58
+ return
59
+ }
60
+
61
+ let proc = Process()
62
+ proc.executableURL = URL(fileURLWithPath: bunPath)
63
+ proc.arguments = [scriptPath, "update", "--detach", "--launch"]
64
+ if let cliRoot = LatticesRuntime.cliRoot {
65
+ proc.currentDirectoryURL = URL(fileURLWithPath: cliRoot)
66
+ }
67
+
68
+ var env = ProcessInfo.processInfo.environment
69
+ env.removeValue(forKey: "CLAUDECODE")
70
+ proc.environment = env
71
+
72
+ do {
73
+ try proc.run()
74
+ isUpdating = true
75
+ statusMessage = "Updating to the latest release. Lattices will relaunch when it's ready."
76
+ } catch {
77
+ presentAlert(
78
+ title: "Update Failed",
79
+ message: "Lattices could not start the updater.\n\n\(error.localizedDescription)"
80
+ )
81
+ }
82
+ }
83
+
84
+ private func presentAlert(title: String, message: String) {
85
+ let alert = NSAlert()
86
+ alert.alertStyle = .warning
87
+ alert.messageText = title
88
+ alert.informativeText = message
89
+ alert.addButton(withTitle: "OK")
90
+ alert.runModal()
91
+ }
92
+ }
@@ -0,0 +1,50 @@
1
+ import AppKit
2
+
3
+ enum CliActionLauncher {
4
+ private static var defaultDirectory: String {
5
+ let root = Preferences.shared.scanRoot
6
+ return root.isEmpty ? NSHomeDirectory() : root
7
+ }
8
+
9
+ private static func chooseProjectDirectory(message: String, prompt: String) -> String? {
10
+ let panel = NSOpenPanel()
11
+ panel.message = message
12
+ panel.prompt = prompt
13
+ panel.canChooseFiles = false
14
+ panel.canChooseDirectories = true
15
+ panel.allowsMultipleSelection = false
16
+ panel.directoryURL = URL(fileURLWithPath: defaultDirectory)
17
+ return panel.runModal() == .OK ? panel.url?.path : nil
18
+ }
19
+
20
+ static func initializeProjectInTerminal() {
21
+ guard let directory = chooseProjectDirectory(
22
+ message: "Choose a project folder to initialize with Lattices.",
23
+ prompt: "Initialize"
24
+ ) else { return }
25
+
26
+ Preferences.shared.terminal.launch(
27
+ command: "lattices init && lattices",
28
+ in: directory
29
+ )
30
+ }
31
+
32
+ static func launchProjectInTerminal() {
33
+ guard let directory = chooseProjectDirectory(
34
+ message: "Choose a project folder to launch with Lattices.",
35
+ prompt: "Launch"
36
+ ) else { return }
37
+
38
+ Preferences.shared.terminal.launch(
39
+ command: "lattices",
40
+ in: directory
41
+ )
42
+ }
43
+
44
+ static func installTmuxInTerminal() {
45
+ Preferences.shared.terminal.launch(
46
+ command: "brew install tmux",
47
+ in: defaultDirectory
48
+ )
49
+ }
50
+ }
@@ -4,6 +4,7 @@ struct HomeDashboardView: View {
4
4
  var onNavigate: ((AppPage) -> Void)? = nil
5
5
 
6
6
  @ObservedObject private var scanner = ProjectScanner.shared
7
+ @ObservedObject private var piSession = PiChatSession.shared
7
8
 
8
9
  var body: some View {
9
10
  VStack(spacing: 0) {
@@ -16,6 +17,9 @@ struct HomeDashboardView: View {
16
17
  MainView(scanner: scanner, layout: .embedded)
17
18
  }
18
19
  .background(Palette.bg)
20
+ .onAppear {
21
+ piSession.refreshBinaryAvailability()
22
+ }
19
23
  }
20
24
 
21
25
  private var hero: some View {
@@ -26,7 +30,7 @@ struct HomeDashboardView: View {
26
30
  .font(Typo.heading(18))
27
31
  .foregroundColor(Palette.text)
28
32
 
29
- Text("Launch workspaces, jump into the screen map, or open a full Pi session from one place.")
33
+ Text("Workspace status, project launch, layout, search, and chat in one place.")
30
34
  .font(Typo.mono(11))
31
35
  .foregroundColor(Palette.textDim)
32
36
  .fixedSize(horizontal: false, vertical: true)
@@ -37,8 +41,8 @@ struct HomeDashboardView: View {
37
41
 
38
42
  HStack(spacing: 10) {
39
43
  homeActionCard(
40
- title: "Screen Map",
41
- subtitle: "Spatial window layout",
44
+ title: "Layout",
45
+ subtitle: "Arrange windows",
42
46
  icon: "rectangle.3.group",
43
47
  tint: Palette.running
44
48
  ) {
@@ -46,19 +50,23 @@ struct HomeDashboardView: View {
46
50
  }
47
51
 
48
52
  homeActionCard(
49
- title: "Desktop Inventory",
50
- subtitle: "Enumerate displays and spaces",
51
- icon: "macwindow.on.rectangle",
53
+ title: "Search",
54
+ subtitle: "Find workspace context",
55
+ icon: "magnifyingglass",
52
56
  tint: Palette.detach
53
57
  ) {
54
58
  onNavigate?(.desktopInventory)
55
59
  }
56
60
 
57
61
  homeActionCard(
58
- title: "Pi Workspace",
59
- subtitle: "Standalone conversation surface",
60
- icon: "terminal",
61
- tint: Palette.text
62
+ title: "Chat",
63
+ subtitle: piSession.hasPiBinary
64
+ ? (piSession.needsProviderSetup || piSession.isAuthenticating
65
+ ? piSession.setupStatusSummary
66
+ : "Standalone conversation surface")
67
+ : "Install Pi to enable the assistant",
68
+ icon: "bubble.left.and.bubble.right",
69
+ tint: piSession.hasPiBinary ? Palette.text : Palette.kill
62
70
  ) {
63
71
  onNavigate?(.pi)
64
72
  }
@@ -0,0 +1,61 @@
1
+ import Foundation
2
+
3
+ enum LatticesRuntime {
4
+ static var cliRoot: String? {
5
+ if let idx = CommandLine.arguments.firstIndex(of: "--lattices-cli-root"),
6
+ CommandLine.arguments.indices.contains(idx + 1) {
7
+ let root = CommandLine.arguments[idx + 1]
8
+ if hasAppHelper(in: root) { return root }
9
+ }
10
+
11
+ let bundleDerivedRoot = Bundle.main.bundleURL
12
+ .deletingLastPathComponent()
13
+ .deletingLastPathComponent()
14
+ .path
15
+ if hasAppHelper(in: bundleDerivedRoot) {
16
+ return bundleDerivedRoot
17
+ }
18
+
19
+ let devRoot = NSHomeDirectory() + "/dev/lattices"
20
+ if hasAppHelper(in: devRoot) {
21
+ return devRoot
22
+ }
23
+
24
+ return nil
25
+ }
26
+
27
+ static var appHelperScriptPath: String? {
28
+ guard let cliRoot else { return nil }
29
+ let path = cliRoot + "/bin/lattices-app.ts"
30
+ return FileManager.default.fileExists(atPath: path) ? path : nil
31
+ }
32
+
33
+ static var bunPath: String? {
34
+ let candidates = [
35
+ NSHomeDirectory() + "/.bun/bin/bun",
36
+ "/usr/local/bin/bun",
37
+ "/opt/homebrew/bin/bun",
38
+ ]
39
+ if let path = candidates.first(where: { FileManager.default.isExecutableFile(atPath: $0) }) {
40
+ return path
41
+ }
42
+
43
+ let resolved = ProcessQuery.shell(["/bin/zsh", "-lc", "command -v bun 2>/dev/null"])
44
+ if !resolved.isEmpty, FileManager.default.isExecutableFile(atPath: resolved) {
45
+ return resolved
46
+ }
47
+
48
+ return nil
49
+ }
50
+
51
+ static var appVersion: String {
52
+ let info = Bundle.main.infoDictionary
53
+ return (info?["CFBundleShortVersionString"] as? String)
54
+ ?? (info?["CFBundleVersion"] as? String)
55
+ ?? "unknown"
56
+ }
57
+
58
+ private static func hasAppHelper(in root: String) -> Bool {
59
+ FileManager.default.fileExists(atPath: root + "/bin/lattices-app.ts")
60
+ }
61
+ }