@lattices/cli 0.4.1 → 0.4.5

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 (71) 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/ActionRow.swift +43 -26
  7. package/app/Sources/App.swift +10 -0
  8. package/app/Sources/AppDelegate.swift +91 -30
  9. package/app/Sources/AppShellView.swift +2 -0
  10. package/app/Sources/AppTypeClassifier.swift +36 -0
  11. package/app/Sources/AppUpdater.swift +92 -0
  12. package/app/Sources/CheatSheetHUD.swift +1 -0
  13. package/app/Sources/CliActionLauncher.swift +50 -0
  14. package/app/Sources/CommandModeView.swift +4 -24
  15. package/app/Sources/CompanionActivityLog.swift +70 -0
  16. package/app/Sources/CompanionKeyboardController.swift +141 -0
  17. package/app/Sources/DesktopModel.swift +4 -0
  18. package/app/Sources/HandsOffSession.swift +53 -16
  19. package/app/Sources/HomeDashboardView.swift +18 -10
  20. package/app/Sources/HotkeyStore.swift +8 -5
  21. package/app/Sources/IntentEngine.swift +7 -1
  22. package/app/Sources/LatticesApi.swift +125 -4
  23. package/app/Sources/LatticesCompanionBridgeServer.swift +438 -0
  24. package/app/Sources/LatticesCompanionCockpit.swift +555 -0
  25. package/app/Sources/LatticesCompanionSecurityCoordinator.swift +594 -0
  26. package/app/Sources/LatticesCompanionTrackpadController.swift +204 -0
  27. package/app/Sources/LatticesDeckHost.swift +1463 -0
  28. package/app/Sources/LatticesRuntime.swift +61 -0
  29. package/app/Sources/MainView.swift +398 -186
  30. package/app/Sources/MouseFinder.swift +335 -30
  31. package/app/Sources/MouseGestureConfig.swift +364 -0
  32. package/app/Sources/MouseGestureController.swift +1203 -0
  33. package/app/Sources/MouseInputDeviceStore.swift +98 -0
  34. package/app/Sources/MouseInputEventViewer.swift +272 -0
  35. package/app/Sources/MouseShortcutStore.swift +107 -0
  36. package/app/Sources/OmniSearchView.swift +136 -2
  37. package/app/Sources/OmniSearchWindow.swift +65 -5
  38. package/app/Sources/OnboardingView.swift +30 -16
  39. package/app/Sources/PaletteCommand.swift +26 -6
  40. package/app/Sources/PermissionChecker.swift +76 -2
  41. package/app/Sources/PiAuthNextStepCard.swift +148 -0
  42. package/app/Sources/PiAuthPromptCard.swift +90 -0
  43. package/app/Sources/PiChatDock.swift +137 -74
  44. package/app/Sources/PiChatSession.swift +608 -108
  45. package/app/Sources/PiInstallCallout.swift +86 -0
  46. package/app/Sources/PiProviderSetupCallout.swift +99 -0
  47. package/app/Sources/PiWorkspaceView.swift +174 -77
  48. package/app/Sources/Preferences.swift +78 -0
  49. package/app/Sources/ScreenMapState.swift +91 -31
  50. package/app/Sources/ScreenMapView.swift +510 -524
  51. package/app/Sources/ScreenMapWindowController.swift +12 -4
  52. package/app/Sources/SettingsView.swift +869 -152
  53. package/app/Sources/SystemTelemetryMonitor.swift +273 -0
  54. package/app/Sources/VoiceCommandWindow.swift +23 -2
  55. package/app/Sources/WindowDragSnapController.swift +628 -0
  56. package/app/Sources/WindowTiler.swift +328 -65
  57. package/app/Sources/WorkspaceManager.swift +288 -0
  58. package/bin/assistant-intelligence.ts +874 -0
  59. package/bin/handsoff-infer.ts +16 -209
  60. package/bin/handsoff-worker.ts +45 -258
  61. package/bin/lattices-app.ts +65 -1
  62. package/bin/lattices-dev +4 -0
  63. package/bin/lattices.ts +125 -14
  64. package/docs/agents.md +14 -0
  65. package/docs/api.md +55 -0
  66. package/docs/app.md +3 -0
  67. package/docs/companion-deck.md +180 -0
  68. package/docs/config.md +25 -0
  69. package/docs/tiling-reference.md +55 -0
  70. package/docs/voice-error-model.md +73 -0
  71. package/package.json +4 -2
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.1</string>
18
+ <string>0.4.5</string>
19
19
  <key>CFBundleShortVersionString</key>
20
- <string>0.4.1</string>
20
+ <string>0.4.5</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.1</string>
18
+ <string>0.4.5</string>
19
19
  <key>CFBundleShortVersionString</key>
20
- <string>0.4.1</string>
20
+ <string>0.4.5</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"),
@@ -2,9 +2,9 @@ import SwiftUI
2
2
 
3
3
  /// A single action row with shortcut badge, label, optional icon, and hotkey hint.
4
4
  struct ActionRow: View {
5
- let shortcut: String
6
5
  let label: String
7
- var hotkey: String? = nil
6
+ var detail: String? = nil
7
+ var hotkeyTokens: [String] = []
8
8
  var icon: String? = nil
9
9
  var accentColor: Color = Palette.textDim
10
10
  var action: () -> Void
@@ -14,41 +14,58 @@ struct ActionRow: View {
14
14
  var body: some View {
15
15
  Button(action: action) {
16
16
  HStack(spacing: 10) {
17
- // Shortcut badge
18
- Text(shortcut)
19
- .font(Typo.monoBold(10))
20
- .foregroundColor(accentColor)
21
- .frame(width: 18, height: 18)
22
- .background(
23
- RoundedRectangle(cornerRadius: 4)
24
- .fill(accentColor.opacity(0.12))
25
- )
26
-
27
17
  // Icon
28
18
  if let icon {
29
- Image(systemName: icon)
30
- .font(.system(size: 11, weight: .medium))
31
- .foregroundColor(isHovered ? Palette.text : Palette.textDim)
32
- .frame(width: 14)
19
+ ZStack {
20
+ RoundedRectangle(cornerRadius: 5)
21
+ .fill(accentColor.opacity(isHovered ? 0.18 : 0.12))
22
+ Image(systemName: icon)
23
+ .font(.system(size: 11, weight: .medium))
24
+ .foregroundColor(isHovered ? Palette.text : accentColor)
25
+ }
26
+ .frame(width: 22, height: 22)
33
27
  }
34
28
 
35
29
  // Label
36
- Text(label)
37
- .font(Typo.mono(12))
38
- .foregroundColor(isHovered ? Palette.text : Palette.textDim)
39
- .lineLimit(1)
30
+ VStack(alignment: .leading, spacing: detail == nil ? 0 : 2) {
31
+ Text(label)
32
+ .font(Typo.body(12))
33
+ .foregroundColor(isHovered ? Palette.text : Palette.textDim)
34
+ .lineLimit(1)
35
+
36
+ if let detail {
37
+ Text(detail)
38
+ .font(Typo.mono(9))
39
+ .foregroundColor(Palette.textMuted)
40
+ .lineLimit(1)
41
+ }
42
+ }
40
43
 
41
44
  Spacer()
42
45
 
43
46
  // Hotkey
44
- if let hotkey {
45
- Text(hotkey)
46
- .font(Typo.mono(10))
47
- .foregroundColor(Palette.textMuted)
47
+ if !hotkeyTokens.isEmpty {
48
+ HStack(spacing: 4) {
49
+ ForEach(hotkeyTokens, id: \.self) { token in
50
+ Text(token)
51
+ .font(Typo.monoBold(8))
52
+ .foregroundColor(Palette.textMuted)
53
+ .padding(.horizontal, token.count > 3 ? 6 : 5)
54
+ .padding(.vertical, 3)
55
+ .background(
56
+ RoundedRectangle(cornerRadius: 4)
57
+ .fill(Palette.surface)
58
+ .overlay(
59
+ RoundedRectangle(cornerRadius: 4)
60
+ .strokeBorder(Palette.border, lineWidth: 0.5)
61
+ )
62
+ )
63
+ }
64
+ }
48
65
  }
49
66
  }
50
- .padding(.horizontal, 10)
51
- .padding(.vertical, 6)
67
+ .padding(.horizontal, 12)
68
+ .padding(.vertical, 8)
52
69
  .background(
53
70
  RoundedRectangle(cornerRadius: 5)
54
71
  .fill(isHovered ? Palette.surfaceHov : Color.clear)
@@ -6,5 +6,15 @@ struct LatticesApp: App {
6
6
 
7
7
  var body: some Scene {
8
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
+ }
9
19
  }
10
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,8 @@ 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() }
87
94
  store.register(action: .voiceCommand) {
88
95
  DiagnosticLog.shared.info("Hotkey: voiceCommand triggered")
89
96
  VoiceCommandWindow.shared.toggle()
@@ -103,7 +110,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
103
110
 
104
111
  // Pre-render HUD panels off-screen for instant first open
105
112
  DispatchQueue.main.async { HUDController.shared.warmUp() }
113
+ // Pre-build the menu bar popover so the first click doesn't pay the SwiftUI mount cost.
114
+ // Touching `.view` forces NSHostingController to materialize the SwiftUI view tree.
115
+ DispatchQueue.main.async { [weak self] in
116
+ guard let self = self else { return }
117
+ let p = self.makePopover()
118
+ _ = p.contentViewController?.view
119
+ }
106
120
  store.register(action: .omniSearch) { OmniSearchWindow.shared.toggle() }
121
+ WindowDragSnapController.shared.start()
122
+ MouseGestureController.shared.start()
107
123
 
108
124
  // Session layer cycling
109
125
  store.register(action: .layerNext) { SessionLayerStore.shared.cycleNext() }
@@ -140,7 +156,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
140
156
  for (action, position) in tileMap {
141
157
  store.register(action: action) { WindowTiler.tileFrontmostViaAX(to: position) }
142
158
  }
143
- store.register(action: .tileDistribute) { WindowTiler.distributeVisible() }
159
+ store.register(action: .tileDistribute) { WindowTiler.distributeVisible(reactivateLattices: false) }
160
+ store.register(action: .tileTypeGrid) { WindowTiler.distributeVisibleByFrontmostType(reactivateLattices: false) }
144
161
 
145
162
  // Onboarding on first launch; otherwise just check permissions
146
163
  if !OnboardingWindowController.shared.showIfNeeded() {
@@ -157,8 +174,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
157
174
  ProcessModel.shared.start()
158
175
  LatticesApi.setup()
159
176
  DaemonServer.shared.start()
177
+ LatticesCompanionBridgeServer.shared.start()
160
178
  AgentPool.shared.start()
161
- HandsOffSession.shared.start()
162
179
  diag.finish(tBoot)
163
180
 
164
181
  // --diagnostics flag: auto-open diagnostics panel on launch
@@ -168,14 +185,19 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
168
185
  }
169
186
  }
170
187
 
171
- // --screen-map flag: auto-open screen map on launch
188
+ // --screen-map flag: auto-open layout on launch
172
189
  if CommandLine.arguments.contains("--screen-map") {
173
190
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
174
- ScreenMapWindowController.shared.show()
191
+ ScreenMapWindowController.shared.showPage(.screenMap)
175
192
  }
176
193
  }
177
194
  }
178
195
 
196
+ func applicationWillTerminate(_ notification: Notification) {
197
+ LatticesCompanionBridgeServer.shared.stop()
198
+ DaemonServer.shared.stop()
199
+ }
200
+
179
201
  // MARK: - Status item click handler
180
202
 
181
203
  @objc private func statusItemClicked(_ sender: Any?) {
@@ -185,13 +207,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
185
207
  // Right-click → context menu
186
208
  contextMenu.popUp(positioning: nil, at: NSPoint(x: 0, y: button.bounds.height + 4), in: button)
187
209
  } else {
188
- // Left-click → toggle popover
210
+ // Left-click → toggle the menu bar projects popover.
189
211
  if let shown = popover, shown.isShown {
190
212
  shown.performClose(sender)
191
213
  } else {
192
- let p = makePopover()
193
- p.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
194
- p.contentViewController?.view.window?.makeKey()
214
+ showProjectsPopover()
195
215
  }
196
216
  }
197
217
  }
@@ -201,14 +221,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
201
221
  popover?.performClose(nil)
202
222
  }
203
223
 
204
- /// Create a fresh popover each time so the SwiftUI view tree isn't kept alive
205
- /// when the popover is closed prevents continuous CPU usage from @Published updates.
224
+ /// Cached popover built lazily on first click, reused on every subsequent open.
225
+ /// Keeping the SwiftUI view tree alive avoids rebuilding on each click (slow first paint).
226
+ /// Data refresh is driven from `popoverWillShow` + a notification MainView listens to.
206
227
  private func makePopover() -> NSPopover {
228
+ if let p = popover { return p }
207
229
  let t = DiagnosticLog.shared.startTimed("makePopover")
208
230
  let p = NSPopover()
209
231
  p.contentViewController = NSHostingController(rootView: MainView(scanner: ProjectScanner.shared))
210
232
  p.behavior = .transient
211
- p.contentSize = NSSize(width: 380, height: 560)
233
+ p.contentSize = NSSize(width: 380, height: 300)
212
234
  p.appearance = NSAppearance(named: .darkAqua)
213
235
  p.delegate = self
214
236
  popover = p
@@ -216,10 +238,20 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
216
238
  return p
217
239
  }
218
240
 
241
+ private func showProjectsPopover() {
242
+ guard let button = statusItem.button else { return }
243
+ let p = makePopover()
244
+ p.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
245
+ p.contentViewController?.view.window?.makeKey()
246
+ }
247
+
248
+ func popoverWillShow(_ notification: Notification) {
249
+ Self.updateActivationPolicy()
250
+ NotificationCenter.default.post(name: .latticesPopoverWillShow, object: nil)
251
+ }
252
+
219
253
  func popoverDidClose(_ notification: Notification) {
220
- // Tear down the SwiftUI view tree so observed models stop driving re-renders
221
- popover?.contentViewController = nil
222
- popover = nil
254
+ Self.updateActivationPolicy()
223
255
  }
224
256
 
225
257
  // MARK: - Context menu
@@ -228,12 +260,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
228
260
  let menu = NSMenu()
229
261
 
230
262
  let actions: [(String, String, Selector)] = [
263
+ ("Home", "", #selector(menuWorkspace)),
264
+ ("Layout", "", #selector(menuLayout)),
265
+ ("Search", "", #selector(menuSearch)),
231
266
  ("Command Palette", "⌘⇧M", #selector(menuCommandPalette)),
232
- ("Unified Window", "", #selector(menuScreenMap)),
233
- ("HUD", "", #selector(menuHUD)),
234
- ("Window Bezel", "", #selector(menuWindowBezel)),
235
- ("Cheat Sheet", "", #selector(menuCheatSheet)),
236
- ("Omni Search", "", #selector(menuOmniSearch)),
237
267
  ]
238
268
  for (title, shortcut, action) in actions {
239
269
  let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
@@ -246,14 +276,29 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
246
276
 
247
277
  menu.addItem(.separator())
248
278
 
249
- let settings = NSMenuItem(title: "Settings…", action: #selector(menuSettings), keyEquivalent: ",")
279
+ let cliActions: [(String, Selector)] = [
280
+ ("Projects…", #selector(menuProjects)),
281
+ ("Initialize Project in Terminal…", #selector(menuInitializeProject)),
282
+ ("Launch Project in Terminal…", #selector(menuLaunchProject)),
283
+ ]
284
+ for (title, action) in cliActions {
285
+ let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
286
+ item.target = self
287
+ menu.addItem(item)
288
+ }
289
+
290
+ menu.addItem(.separator())
291
+
292
+ let update = NSMenuItem(title: "Update Lattices…", action: #selector(menuUpdate), keyEquivalent: "")
293
+ update.target = self
294
+ menu.addItem(update)
295
+
296
+ menu.addItem(.separator())
297
+
298
+ let settings = NSMenuItem(title: "Help & Settings…", action: #selector(menuSettings), keyEquivalent: ",")
250
299
  settings.target = self
251
300
  menu.addItem(settings)
252
301
 
253
- let diag = NSMenuItem(title: "Diagnostics", action: #selector(menuDiagnostics), keyEquivalent: "")
254
- diag.target = self
255
- menu.addItem(diag)
256
-
257
302
  menu.addItem(.separator())
258
303
 
259
304
  let quit = NSMenuItem(title: "Quit Lattices", action: #selector(menuQuit), keyEquivalent: "q")
@@ -264,12 +309,28 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
264
309
  }
265
310
 
266
311
  @objc private func menuCommandPalette() { CommandPaletteWindow.shared.toggle() }
267
- @objc private func menuScreenMap() { ScreenMapWindowController.shared.toggle() }
312
+ @objc private func menuWorkspace() { ScreenMapWindowController.shared.showPage(.home) }
313
+ @objc private func menuLayout() { ScreenMapWindowController.shared.showPage(.screenMap) }
314
+ @objc private func menuSearch() { ScreenMapWindowController.shared.showPage(.desktopInventory) }
315
+ @objc private func menuDocs() { SettingsWindowController.shared.show() }
316
+ @objc private func menuProjects() { DispatchQueue.main.async { self.showProjectsPopover() } }
317
+ @objc private func menuInitializeProject() { CliActionLauncher.initializeProjectInTerminal() }
318
+ @objc private func menuLaunchProject() { CliActionLauncher.launchProjectInTerminal() }
268
319
  @objc private func menuHUD() { HUDController.shared.toggle() }
269
- @objc private func menuWindowBezel() { WindowBezel.showBezelForFrontmostWindow() }
270
- @objc private func menuCheatSheet() { CheatSheetHUD.shared.toggle() }
320
+ @objc private func menuWindowBezel() { Self.showWorkspaceInspector() }
321
+ @objc private func menuCheatSheet() { SettingsWindowController.shared.show() }
271
322
  @objc private func menuOmniSearch() { OmniSearchWindow.shared.toggle() }
323
+ @MainActor @objc private func menuUpdate() { AppUpdater.shared.promptForUpdate() }
272
324
  @objc private func menuSettings() { SettingsWindowController.shared.show() }
273
- @objc private func menuDiagnostics() { DiagnosticWindow.shared.toggle() }
274
325
  @objc private func menuQuit() { NSApp.terminate(nil) }
326
+
327
+ private static func showWorkspaceInspector() {
328
+ guard let entry = DesktopModel.shared.frontmostWindow(),
329
+ entry.app != "Lattices" else {
330
+ ScreenMapWindowController.shared.showPage(.screenMap)
331
+ return
332
+ }
333
+
334
+ ScreenMapWindowController.shared.showWindow(wid: entry.wid)
335
+ }
275
336
  }
@@ -66,6 +66,7 @@ struct AppShellView: View {
66
66
  ForEach(AppPage.primaryTabs, id: \.rawValue) { tab in
67
67
  tabButton(tab)
68
68
  }
69
+
69
70
  Spacer()
70
71
  }
71
72
  .padding(.horizontal, 12)
@@ -90,6 +91,7 @@ struct AppShellView: View {
90
91
  .foregroundColor(isActive ? Palette.text : Palette.textMuted)
91
92
  .padding(.horizontal, 12)
92
93
  .padding(.vertical, 6)
94
+ .contentShape(Rectangle())
93
95
  .background(
94
96
  RoundedRectangle(cornerRadius: 6)
95
97
  .fill(isActive ? Palette.surfaceHov : Color.clear)
@@ -13,6 +13,20 @@ enum AppType: String, CaseIterable {
13
13
  var label: String { rawValue }
14
14
  }
15
15
 
16
+ enum AppGrouping {
17
+ case type(AppType)
18
+ case exactApp(String)
19
+
20
+ var label: String {
21
+ switch self {
22
+ case .type(let type):
23
+ return type.label
24
+ case .exactApp(let appName):
25
+ return appName
26
+ }
27
+ }
28
+ }
29
+
16
30
  enum AppTypeClassifier {
17
31
  private static let nameMap: [String: AppType] = [
18
32
  // Terminals
@@ -67,4 +81,26 @@ enum AppTypeClassifier {
67
81
  if lower.contains("slack") || lower.contains("discord") || lower.contains("chat") || lower.contains("teams") { return .chat }
68
82
  return .other
69
83
  }
84
+
85
+ static func grouping(for appName: String) -> AppGrouping {
86
+ switch classify(appName) {
87
+ case .system, .other:
88
+ return .exactApp(appName)
89
+ case let type:
90
+ return .type(type)
91
+ }
92
+ }
93
+
94
+ static func matches(_ appName: String, grouping: AppGrouping) -> Bool {
95
+ switch grouping {
96
+ case .type(let type):
97
+ return classify(appName) == type
98
+ case .exactApp(let exactApp):
99
+ return appName.localizedCaseInsensitiveCompare(exactApp) == .orderedSame
100
+ }
101
+ }
102
+
103
+ static func matches(_ appName: String, type: AppType) -> Bool {
104
+ classify(appName) == type
105
+ }
70
106
  }
@@ -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
+ }
@@ -254,6 +254,7 @@ struct CheatSheetView: View {
254
254
  // Center + Distribute
255
255
  shortcutRow(action: .tileCenter)
256
256
  shortcutRow(action: .tileDistribute)
257
+ shortcutRow(action: .tileTypeGrid)
257
258
 
258
259
  // Hovered shortcut detail
259
260
  if let hovered = hoveredAction, let binding = hotkeyStore.bindings[hovered] {
@@ -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
+ }
@@ -121,23 +121,6 @@ struct CommandModeView: View {
121
121
 
122
122
  Spacer()
123
123
 
124
- if let layer = state.inventory.activeLayer {
125
- HStack(spacing: 4) {
126
- Text("Layer: \(layer)")
127
- .font(Typo.mono(10))
128
- .foregroundColor(Palette.running)
129
-
130
- Text("[\(state.inventory.layerCount > 0 ? "\(WorkspaceManager.shared.activeLayerIndex + 1)/\(state.inventory.layerCount)" : "—")]")
131
- .font(Typo.mono(10))
132
- .foregroundColor(Palette.textMuted)
133
- }
134
- .padding(.horizontal, 6)
135
- .padding(.vertical, 2)
136
- .background(
137
- RoundedRectangle(cornerRadius: 3)
138
- .fill(Palette.running.opacity(0.10))
139
- )
140
- }
141
124
  }
142
125
  .padding(.horizontal, 16)
143
126
  .padding(.vertical, 10)
@@ -155,15 +138,12 @@ struct CommandModeView: View {
155
138
  private var inventoryGrid: some View {
156
139
  ScrollView {
157
140
  LazyVStack(alignment: .leading, spacing: 0) {
158
- let grouped = groupedItems
159
- if grouped.isEmpty {
141
+ let items = state.inventory.items
142
+ if items.isEmpty {
160
143
  emptyState
161
144
  } else {
162
- ForEach(grouped, id: \.0) { section, items in
163
- sectionHeader(section)
164
- ForEach(Array(items.enumerated()), id: \.offset) { _, item in
165
- inventoryRow(item)
166
- }
145
+ ForEach(Array(items.enumerated()), id: \.offset) { _, item in
146
+ inventoryRow(item)
167
147
  }
168
148
  }
169
149
  }