@lattices/cli 0.4.2 → 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 (70) 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/App.swift +10 -0
  7. package/app/Sources/AppDelegate.swift +90 -34
  8. package/app/Sources/AppShellView.swift +2 -0
  9. package/app/Sources/AppTypeClassifier.swift +36 -0
  10. package/app/Sources/AppUpdater.swift +92 -0
  11. package/app/Sources/CheatSheetHUD.swift +1 -0
  12. package/app/Sources/CliActionLauncher.swift +50 -0
  13. package/app/Sources/CommandModeView.swift +4 -24
  14. package/app/Sources/CompanionActivityLog.swift +70 -0
  15. package/app/Sources/CompanionKeyboardController.swift +141 -0
  16. package/app/Sources/DesktopModel.swift +4 -0
  17. package/app/Sources/HandsOffSession.swift +15 -4
  18. package/app/Sources/HomeDashboardView.swift +18 -10
  19. package/app/Sources/HotkeyStore.swift +8 -5
  20. package/app/Sources/IntentEngine.swift +7 -1
  21. package/app/Sources/LatticesApi.swift +125 -4
  22. package/app/Sources/LatticesCompanionBridgeServer.swift +438 -0
  23. package/app/Sources/LatticesCompanionCockpit.swift +555 -0
  24. package/app/Sources/LatticesCompanionSecurityCoordinator.swift +594 -0
  25. package/app/Sources/LatticesCompanionTrackpadController.swift +204 -0
  26. package/app/Sources/LatticesDeckHost.swift +1463 -0
  27. package/app/Sources/LatticesRuntime.swift +61 -0
  28. package/app/Sources/MainView.swift +351 -191
  29. package/app/Sources/MouseFinder.swift +335 -30
  30. package/app/Sources/MouseGestureConfig.swift +364 -0
  31. package/app/Sources/MouseGestureController.swift +1203 -0
  32. package/app/Sources/MouseInputDeviceStore.swift +98 -0
  33. package/app/Sources/MouseInputEventViewer.swift +272 -0
  34. package/app/Sources/MouseShortcutStore.swift +107 -0
  35. package/app/Sources/OmniSearchView.swift +136 -2
  36. package/app/Sources/OmniSearchWindow.swift +65 -5
  37. package/app/Sources/OnboardingView.swift +30 -16
  38. package/app/Sources/PaletteCommand.swift +26 -6
  39. package/app/Sources/PermissionChecker.swift +76 -2
  40. package/app/Sources/PiAuthNextStepCard.swift +148 -0
  41. package/app/Sources/PiAuthPromptCard.swift +90 -0
  42. package/app/Sources/PiChatDock.swift +137 -74
  43. package/app/Sources/PiChatSession.swift +608 -108
  44. package/app/Sources/PiInstallCallout.swift +86 -0
  45. package/app/Sources/PiProviderSetupCallout.swift +99 -0
  46. package/app/Sources/PiWorkspaceView.swift +174 -77
  47. package/app/Sources/Preferences.swift +78 -0
  48. package/app/Sources/ScreenMapState.swift +91 -31
  49. package/app/Sources/ScreenMapView.swift +510 -524
  50. package/app/Sources/ScreenMapWindowController.swift +12 -4
  51. package/app/Sources/SettingsView.swift +869 -152
  52. package/app/Sources/SystemTelemetryMonitor.swift +273 -0
  53. package/app/Sources/VoiceCommandWindow.swift +23 -2
  54. package/app/Sources/WindowDragSnapController.swift +628 -0
  55. package/app/Sources/WindowTiler.swift +328 -65
  56. package/app/Sources/WorkspaceManager.swift +288 -0
  57. package/bin/assistant-intelligence.ts +874 -0
  58. package/bin/handsoff-infer.ts +16 -209
  59. package/bin/handsoff-worker.ts +45 -258
  60. package/bin/lattices-app.ts +62 -0
  61. package/bin/lattices-dev +4 -0
  62. package/bin/lattices.ts +125 -14
  63. package/docs/agents.md +14 -0
  64. package/docs/api.md +55 -0
  65. package/docs/app.md +3 -0
  66. package/docs/companion-deck.md +180 -0
  67. package/docs/config.md +25 -0
  68. package/docs/tiling-reference.md +55 -0
  69. package/docs/voice-error-model.md +73 -0
  70. package/package.json +2 -1
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.5</string>
19
19
  <key>CFBundleShortVersionString</key>
20
- <string>0.4.2</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.2</string>
18
+ <string>0.4.5</string>
19
19
  <key>CFBundleShortVersionString</key>
20
- <string>0.4.2</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"),
@@ -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,6 +174,7 @@ 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
179
  diag.finish(tBoot)
162
180
 
@@ -167,14 +185,19 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
167
185
  }
168
186
  }
169
187
 
170
- // --screen-map flag: auto-open screen map on launch
188
+ // --screen-map flag: auto-open layout on launch
171
189
  if CommandLine.arguments.contains("--screen-map") {
172
190
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
173
- ScreenMapWindowController.shared.show()
191
+ ScreenMapWindowController.shared.showPage(.screenMap)
174
192
  }
175
193
  }
176
194
  }
177
195
 
196
+ func applicationWillTerminate(_ notification: Notification) {
197
+ LatticesCompanionBridgeServer.shared.stop()
198
+ DaemonServer.shared.stop()
199
+ }
200
+
178
201
  // MARK: - Status item click handler
179
202
 
180
203
  @objc private func statusItemClicked(_ sender: Any?) {
@@ -184,13 +207,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
184
207
  // Right-click → context menu
185
208
  contextMenu.popUp(positioning: nil, at: NSPoint(x: 0, y: button.bounds.height + 4), in: button)
186
209
  } else {
187
- // Left-click → toggle popover
210
+ // Left-click → toggle the menu bar projects popover.
188
211
  if let shown = popover, shown.isShown {
189
212
  shown.performClose(sender)
190
213
  } else {
191
- let p = makePopover()
192
- p.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
193
- p.contentViewController?.view.window?.makeKey()
214
+ showProjectsPopover()
194
215
  }
195
216
  }
196
217
  }
@@ -200,14 +221,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
200
221
  popover?.performClose(nil)
201
222
  }
202
223
 
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.
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.
205
227
  private func makePopover() -> NSPopover {
228
+ if let p = popover { return p }
206
229
  let t = DiagnosticLog.shared.startTimed("makePopover")
207
230
  let p = NSPopover()
208
231
  p.contentViewController = NSHostingController(rootView: MainView(scanner: ProjectScanner.shared))
209
232
  p.behavior = .transient
210
- p.contentSize = NSSize(width: 380, height: 560)
233
+ p.contentSize = NSSize(width: 380, height: 300)
211
234
  p.appearance = NSAppearance(named: .darkAqua)
212
235
  p.delegate = self
213
236
  popover = p
@@ -215,10 +238,20 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
215
238
  return p
216
239
  }
217
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
+
218
253
  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
254
+ Self.updateActivationPolicy()
222
255
  }
223
256
 
224
257
  // MARK: - Context menu
@@ -227,10 +260,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
227
260
  let menu = NSMenu()
228
261
 
229
262
  let actions: [(String, String, Selector)] = [
263
+ ("Home", "", #selector(menuWorkspace)),
264
+ ("Layout", "", #selector(menuLayout)),
265
+ ("Search", "", #selector(menuSearch)),
230
266
  ("Command Palette", "⌘⇧M", #selector(menuCommandPalette)),
231
- ("Workspace", "", #selector(menuWorkspace)),
232
- ("Assistant", "", #selector(menuAssistant)),
233
- ("Help & Shortcuts", "", #selector(menuDocs)),
234
267
  ]
235
268
  for (title, shortcut, action) in actions {
236
269
  let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
@@ -243,14 +276,29 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
243
276
 
244
277
  menu.addItem(.separator())
245
278
 
246
- 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: ",")
247
299
  settings.target = self
248
300
  menu.addItem(settings)
249
301
 
250
- let diag = NSMenuItem(title: "Diagnostics", action: #selector(menuDiagnostics), keyEquivalent: "")
251
- diag.target = self
252
- menu.addItem(diag)
253
-
254
302
  menu.addItem(.separator())
255
303
 
256
304
  let quit = NSMenuItem(title: "Quit Lattices", action: #selector(menuQuit), keyEquivalent: "q")
@@ -262,19 +310,27 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
262
310
 
263
311
  @objc private func menuCommandPalette() { CommandPaletteWindow.shared.toggle() }
264
312
  @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) }
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() }
273
319
  @objc private func menuHUD() { HUDController.shared.toggle() }
274
- @objc private func menuWindowBezel() { WindowBezel.showBezelForFrontmostWindow() }
275
- @objc private func menuCheatSheet() { CheatSheetHUD.shared.toggle() }
320
+ @objc private func menuWindowBezel() { Self.showWorkspaceInspector() }
321
+ @objc private func menuCheatSheet() { SettingsWindowController.shared.show() }
276
322
  @objc private func menuOmniSearch() { OmniSearchWindow.shared.toggle() }
323
+ @MainActor @objc private func menuUpdate() { AppUpdater.shared.promptForUpdate() }
277
324
  @objc private func menuSettings() { SettingsWindowController.shared.show() }
278
- @objc private func menuDiagnostics() { DiagnosticWindow.shared.toggle() }
279
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
+ }
280
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
  }
@@ -0,0 +1,70 @@
1
+ import DeckKit
2
+ import Foundation
3
+
4
+ final class CompanionActivityLog {
5
+ static let shared = CompanionActivityLog()
6
+
7
+ private let lock = NSLock()
8
+ private var entries: [DeckActivityLogEntry] = []
9
+ private let maxEntries = 120
10
+
11
+ private init() {
12
+ EventBus.shared.subscribe { [weak self] event in
13
+ self?.record(event)
14
+ }
15
+ }
16
+
17
+ func record(tag: String, tint: String?, text: String) {
18
+ let entry = DeckActivityLogEntry(
19
+ id: UUID().uuidString,
20
+ createdAt: Date(),
21
+ tag: tag,
22
+ tint: tint,
23
+ text: text
24
+ )
25
+
26
+ lock.lock()
27
+ entries.append(entry)
28
+ if entries.count > maxEntries {
29
+ entries.removeFirst(entries.count - maxEntries)
30
+ }
31
+ lock.unlock()
32
+ }
33
+
34
+ func snapshot(limit: Int = 80) -> [DeckActivityLogEntry] {
35
+ lock.lock()
36
+ let copy = entries
37
+ lock.unlock()
38
+
39
+ return Array(copy.suffix(limit).reversed())
40
+ }
41
+ }
42
+
43
+ private extension CompanionActivityLog {
44
+ func record(_ event: ModelEvent) {
45
+ switch event {
46
+ case .windowsChanged(let windows, let added, let removed):
47
+ let delta = [added.isEmpty ? nil : "+\(added.count)", removed.isEmpty ? nil : "-\(removed.count)"]
48
+ .compactMap { $0 }
49
+ .joined(separator: " ")
50
+ let suffix = delta.isEmpty ? "" : " (\(delta))"
51
+ record(tag: "WIN", tint: "blue", text: "\(windows.count) desktop windows\(suffix)")
52
+
53
+ case .tmuxChanged(let sessions):
54
+ record(tag: "TMUX", tint: "green", text: "\(sessions.count) tmux sessions indexed")
55
+
56
+ case .layerSwitched(let index):
57
+ record(tag: "LAYER", tint: "violet", text: "Switched workspace layer \(index + 1)")
58
+
59
+ case .processesChanged(let interesting):
60
+ record(tag: "PROC", tint: "amber", text: "\(interesting.count) terminal processes changed")
61
+
62
+ case .ocrScanComplete(let windowCount, let totalBlocks):
63
+ record(tag: "OCR", tint: "teal", text: "Scanned \(totalBlocks) text blocks across \(windowCount) windows")
64
+
65
+ case .voiceCommand(let text, let confidence):
66
+ let pct = Int((confidence * 100).rounded())
67
+ record(tag: "VOICE", tint: "red", text: "\"\(text)\" · \(pct)%")
68
+ }
69
+ }
70
+ }