@lattices/cli 0.4.5 → 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 (130) hide show
  1. package/app/Info.plist +2 -2
  2. package/app/Lattices.app/Contents/Info.plist +2 -2
  3. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  4. package/app/Sources/{AppDelegate.swift → AppShell/AppDelegate.swift} +4 -0
  5. package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +10 -1
  6. package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +2 -1
  7. package/app/Sources/{IntentEngine.swift → Core/Actions/IntentEngine.swift} +44 -26
  8. package/app/Sources/Core/Actions/IntentSchema.swift +94 -0
  9. package/app/Sources/{Intents → Core/Actions/Intents}/LatticeIntent.swift +0 -25
  10. package/app/Sources/{VoiceIntentResolver.swift → Core/Actions/VoiceIntentResolver.swift} +46 -4
  11. package/app/Sources/{DesktopModel.swift → Core/Desktop/DesktopModel.swift} +2 -8
  12. package/app/Sources/Core/Desktop/SessionWindowLocator.swift +139 -0
  13. package/app/Sources/Core/Desktop/WindowPreviewCard.swift +100 -0
  14. package/app/Sources/Core/Desktop/WindowPreviewStore.swift +113 -0
  15. package/app/Sources/Core/Desktop/WindowSelectionStore.swift +76 -0
  16. package/app/Sources/{WindowTiler.swift → Core/Desktop/WindowTiler.swift} +24 -108
  17. package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +127 -24
  18. package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +488 -55
  19. package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -0
  20. package/app/Sources/{HUDRightBar.swift → Core/Overlays/HUD/HUDRightBar.swift} +23 -201
  21. package/app/Sources/{LauncherHUD.swift → Core/Overlays/HUD/LauncherHUD.swift} +12 -26
  22. package/app/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +94 -0
  23. package/app/Sources/Core/Overlays/OverlayPanelShell.swift +241 -0
  24. package/app/Sources/{ScreenMapState.swift → Core/Overlays/ScreenMap/ScreenMapState.swift} +25 -1
  25. package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -74
  26. package/docs/component-extraction-roadmap.md +392 -0
  27. package/package.json +3 -1
  28. package/app/Sources/CommandPaletteWindow.swift +0 -134
  29. package/app/Sources/OmniSearchWindow.swift +0 -165
  30. /package/app/Sources/{App.swift → AppShell/App.swift} +0 -0
  31. /package/app/Sources/{AppUpdater.swift → AppShell/AppUpdater.swift} +0 -0
  32. /package/app/Sources/{CliActionLauncher.swift → AppShell/CliActionLauncher.swift} +0 -0
  33. /package/app/Sources/{HomeDashboardView.swift → AppShell/HomeDashboardView.swift} +0 -0
  34. /package/app/Sources/{KeyRecorderView.swift → AppShell/KeyRecorderView.swift} +0 -0
  35. /package/app/Sources/{LatticesRuntime.swift → AppShell/LatticesRuntime.swift} +0 -0
  36. /package/app/Sources/{MainView.swift → AppShell/MainView.swift} +0 -0
  37. /package/app/Sources/{MainWindow.swift → AppShell/MainWindow.swift} +0 -0
  38. /package/app/Sources/{OnboardingView.swift → AppShell/OnboardingView.swift} +0 -0
  39. /package/app/Sources/{Preferences.swift → AppShell/Preferences.swift} +0 -0
  40. /package/app/Sources/{SettingsView.swift → AppShell/SettingsView.swift} +0 -0
  41. /package/app/Sources/{SettingsWindow.swift → AppShell/SettingsWindow.swift} +0 -0
  42. /package/app/Sources/{HotkeyManager.swift → Core/Actions/HotkeyManager.swift} +0 -0
  43. /package/app/Sources/{Intents → Core/Actions/Intents}/CreateLayerIntent.swift +0 -0
  44. /package/app/Sources/{Intents → Core/Actions/Intents}/DistributeIntent.swift +0 -0
  45. /package/app/Sources/{Intents → Core/Actions/Intents}/FocusIntent.swift +0 -0
  46. /package/app/Sources/{Intents → Core/Actions/Intents}/HelpIntent.swift +0 -0
  47. /package/app/Sources/{Intents → Core/Actions/Intents}/KillIntent.swift +0 -0
  48. /package/app/Sources/{Intents → Core/Actions/Intents}/LaunchIntent.swift +0 -0
  49. /package/app/Sources/{Intents → Core/Actions/Intents}/ListSessionsIntent.swift +0 -0
  50. /package/app/Sources/{Intents → Core/Actions/Intents}/ListWindowsIntent.swift +0 -0
  51. /package/app/Sources/{Intents → Core/Actions/Intents}/ScanIntent.swift +0 -0
  52. /package/app/Sources/{Intents → Core/Actions/Intents}/SearchIntent.swift +0 -0
  53. /package/app/Sources/{Intents → Core/Actions/Intents}/SwitchLayerIntent.swift +0 -0
  54. /package/app/Sources/{Intents → Core/Actions/Intents}/TileIntent.swift +0 -0
  55. /package/app/Sources/{PaletteCommand.swift → Core/Actions/PaletteCommand.swift} +0 -0
  56. /package/app/Sources/{CompanionActivityLog.swift → Core/Companion/CompanionActivityLog.swift} +0 -0
  57. /package/app/Sources/{CompanionKeyboardController.swift → Core/Companion/CompanionKeyboardController.swift} +0 -0
  58. /package/app/Sources/{LatticesCompanionBridgeServer.swift → Core/Companion/LatticesCompanionBridgeServer.swift} +0 -0
  59. /package/app/Sources/{LatticesCompanionCockpit.swift → Core/Companion/LatticesCompanionCockpit.swift} +0 -0
  60. /package/app/Sources/{LatticesCompanionSecurityCoordinator.swift → Core/Companion/LatticesCompanionSecurityCoordinator.swift} +0 -0
  61. /package/app/Sources/{LatticesCompanionTrackpadController.swift → Core/Companion/LatticesCompanionTrackpadController.swift} +0 -0
  62. /package/app/Sources/{LatticesDeckHost.swift → Core/Companion/LatticesDeckHost.swift} +0 -0
  63. /package/app/Sources/{DaemonProtocol.swift → Core/Daemon/DaemonProtocol.swift} +0 -0
  64. /package/app/Sources/{DaemonServer.swift → Core/Daemon/DaemonServer.swift} +0 -0
  65. /package/app/Sources/{LatticesApi.swift → Core/Daemon/LatticesApi.swift} +0 -0
  66. /package/app/Sources/{AccessibilityTextExtractor.swift → Core/Desktop/AccessibilityTextExtractor.swift} +0 -0
  67. /package/app/Sources/{AppTypeClassifier.swift → Core/Desktop/AppTypeClassifier.swift} +0 -0
  68. /package/app/Sources/{DesktopModelTypes.swift → Core/Desktop/DesktopModelTypes.swift} +0 -0
  69. /package/app/Sources/{InventoryManager.swift → Core/Desktop/InventoryManager.swift} +0 -0
  70. /package/app/Sources/{InventoryPath.swift → Core/Desktop/InventoryPath.swift} +0 -0
  71. /package/app/Sources/{MouseFinder.swift → Core/Desktop/MouseFinder.swift} +0 -0
  72. /package/app/Sources/{OcrModel.swift → Core/Desktop/OcrModel.swift} +0 -0
  73. /package/app/Sources/{OcrStore.swift → Core/Desktop/OcrStore.swift} +0 -0
  74. /package/app/Sources/{PlacementSpec.swift → Core/Desktop/PlacementSpec.swift} +0 -0
  75. /package/app/Sources/{TilePickerView.swift → Core/Desktop/TilePickerView.swift} +0 -0
  76. /package/app/Sources/{WindowDragSnapController.swift → Core/Desktop/WindowDragSnapController.swift} +0 -0
  77. /package/app/Sources/{MouseGestureConfig.swift → Core/Input/MouseGestureConfig.swift} +0 -0
  78. /package/app/Sources/{MouseGestureController.swift → Core/Input/MouseGestureController.swift} +0 -0
  79. /package/app/Sources/{MouseInputDeviceStore.swift → Core/Input/MouseInputDeviceStore.swift} +0 -0
  80. /package/app/Sources/{MouseInputEventViewer.swift → Core/Input/MouseInputEventViewer.swift} +0 -0
  81. /package/app/Sources/{MouseShortcutStore.swift → Core/Input/MouseShortcutStore.swift} +0 -0
  82. /package/app/Sources/{AppWindowShell.swift → Core/Overlays/AppWindowShell.swift} +0 -0
  83. /package/app/Sources/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +0 -0
  84. /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
  85. /package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.swift} +0 -0
  86. /package/app/Sources/{HUDBottomBar.swift → Core/Overlays/HUD/HUDBottomBar.swift} +0 -0
  87. /package/app/Sources/{HUDController.swift → Core/Overlays/HUD/HUDController.swift} +0 -0
  88. /package/app/Sources/{HUDLeftBar.swift → Core/Overlays/HUD/HUDLeftBar.swift} +0 -0
  89. /package/app/Sources/{HUDMinimap.swift → Core/Overlays/HUD/HUDMinimap.swift} +0 -0
  90. /package/app/Sources/{HUDState.swift → Core/Overlays/HUD/HUDState.swift} +0 -0
  91. /package/app/Sources/{HUDTopBar.swift → Core/Overlays/HUD/HUDTopBar.swift} +0 -0
  92. /package/app/Sources/{LayerBezel.swift → Core/Overlays/HUD/LayerBezel.swift} +0 -0
  93. /package/app/Sources/{OmniSearchState.swift → Core/Overlays/OmniSearch/OmniSearchState.swift} +0 -0
  94. /package/app/Sources/{OmniSearchView.swift → Core/Overlays/OmniSearch/OmniSearchView.swift} +0 -0
  95. /package/app/Sources/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.swift} +0 -0
  96. /package/app/Sources/{ScreenMapWindowController.swift → Core/Overlays/ScreenMap/ScreenMapWindowController.swift} +0 -0
  97. /package/app/Sources/{PiAuthNextStepCard.swift → Core/Pi/PiAuthNextStepCard.swift} +0 -0
  98. /package/app/Sources/{PiAuthPromptCard.swift → Core/Pi/PiAuthPromptCard.swift} +0 -0
  99. /package/app/Sources/{PiChatDock.swift → Core/Pi/PiChatDock.swift} +0 -0
  100. /package/app/Sources/{PiChatSession.swift → Core/Pi/PiChatSession.swift} +0 -0
  101. /package/app/Sources/{PiInstallCallout.swift → Core/Pi/PiInstallCallout.swift} +0 -0
  102. /package/app/Sources/{PiProviderSetupCallout.swift → Core/Pi/PiProviderSetupCallout.swift} +0 -0
  103. /package/app/Sources/{PiWorkspaceView.swift → Core/Pi/PiWorkspaceView.swift} +0 -0
  104. /package/app/Sources/{DiagnosticLog.swift → Core/System/DiagnosticLog.swift} +0 -0
  105. /package/app/Sources/{EventBus.swift → Core/System/EventBus.swift} +0 -0
  106. /package/app/Sources/{PermissionChecker.swift → Core/System/PermissionChecker.swift} +0 -0
  107. /package/app/Sources/{ProcessModel.swift → Core/System/ProcessModel.swift} +0 -0
  108. /package/app/Sources/{ProcessQuery.swift → Core/System/ProcessQuery.swift} +0 -0
  109. /package/app/Sources/{SystemTelemetryMonitor.swift → Core/System/SystemTelemetryMonitor.swift} +0 -0
  110. /package/app/Sources/{AdvisorLearningStore.swift → Core/Voice/AdvisorLearningStore.swift} +0 -0
  111. /package/app/Sources/{AgentSession.swift → Core/Voice/AgentSession.swift} +0 -0
  112. /package/app/Sources/{AudioProvider.swift → Core/Voice/AudioProvider.swift} +0 -0
  113. /package/app/Sources/{HandsOffSession.swift → Core/Voice/HandsOffSession.swift} +0 -0
  114. /package/app/Sources/{VoiceChatView.swift → Core/Voice/VoiceChatView.swift} +0 -0
  115. /package/app/Sources/{VoxClient.swift → Core/Voice/VoxClient.swift} +0 -0
  116. /package/app/Sources/{Project.swift → Core/Workspace/Project.swift} +0 -0
  117. /package/app/Sources/{ProjectScanner.swift → Core/Workspace/ProjectScanner.swift} +0 -0
  118. /package/app/Sources/{SessionLayerStore.swift → Core/Workspace/SessionLayerStore.swift} +0 -0
  119. /package/app/Sources/{SessionManager.swift → Core/Workspace/SessionManager.swift} +0 -0
  120. /package/app/Sources/{Terminal.swift → Core/Workspace/Terminal/Terminal.swift} +0 -0
  121. /package/app/Sources/{TerminalQuery.swift → Core/Workspace/Terminal/TerminalQuery.swift} +0 -0
  122. /package/app/Sources/{TerminalSynthesizer.swift → Core/Workspace/Terminal/TerminalSynthesizer.swift} +0 -0
  123. /package/app/Sources/{TmuxModel.swift → Core/Workspace/Tmux/TmuxModel.swift} +0 -0
  124. /package/app/Sources/{TmuxQuery.swift → Core/Workspace/Tmux/TmuxQuery.swift} +0 -0
  125. /package/app/Sources/{WorkspaceManager.swift → Core/Workspace/WorkspaceManager.swift} +0 -0
  126. /package/app/Sources/{ActionRow.swift → UI/ActionRow.swift} +0 -0
  127. /package/app/Sources/{OrphanRow.swift → UI/OrphanRow.swift} +0 -0
  128. /package/app/Sources/{ProjectRow.swift → UI/ProjectRow.swift} +0 -0
  129. /package/app/Sources/{TabGroupRow.swift → UI/TabGroupRow.swift} +0 -0
  130. /package/app/Sources/{Theme.swift → UI/Theme.swift} +0 -0
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.5</string>
18
+ <string>0.4.6</string>
19
19
  <key>CFBundleShortVersionString</key>
20
- <string>0.4.5</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.5</string>
18
+ <string>0.4.6</string>
19
19
  <key>CFBundleShortVersionString</key>
20
- <string>0.4.5</string>
20
+ <string>0.4.6</string>
21
21
  <key>LSMinimumSystemVersion</key>
22
22
  <string>13.0</string>
23
23
  <key>LSUIElement</key>
@@ -91,6 +91,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
91
91
  store.register(action: .unifiedWindow) { ScreenMapWindowController.shared.toggle() }
92
92
  store.register(action: .bezel) { Self.showWorkspaceInspector() }
93
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
+ }
94
98
  store.register(action: .voiceCommand) {
95
99
  DiagnosticLog.shared.info("Hotkey: voiceCommand triggered")
96
100
  VoiceCommandWindow.shared.toggle()
@@ -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
 
@@ -116,7 +120,7 @@ struct AppShellView: View {
116
120
  windowController.activePage = page
117
121
  })
118
122
  case .desktopInventory:
119
- CommandModeView(state: commandState)
123
+ CommandModeView(state: commandState, presentation: .embedded)
120
124
  case .pi:
121
125
  PiWorkspaceView()
122
126
  case .settings:
@@ -134,4 +138,9 @@ struct AppShellView: View {
134
138
  )
135
139
  }
136
140
  }
141
+
142
+ private func syncPageState(_ page: AppPage) {
143
+ if page == .screenMap { controller.enter() }
144
+ if page == .desktopInventory { commandState.enter() }
145
+ }
137
146
  }
@@ -40,7 +40,7 @@ enum HotkeyAction: String, CaseIterable, Codable {
40
40
  case .screenMap: return "Screen Map"
41
41
  case .bezel: return "Window Bezel"
42
42
  case .cheatSheet: return "Cheat Sheet"
43
- case .desktopInventory: return "Search"
43
+ case .desktopInventory: return "Window Selector"
44
44
  case .omniSearch: return "Search"
45
45
  case .voiceCommand: return "Voice Command"
46
46
  case .handsOff: return "Hands-Off Mode"
@@ -231,6 +231,7 @@ class HotkeyStore: ObservableObject {
231
231
  bind(.unifiedWindow, 18, hyper) // Hyper+1 (Workspace Home)
232
232
  bind(.bezel, 19, hyper) // Hyper+2
233
233
  bind(.hud, 20, hyper) // Hyper+3 (HUD overlay)
234
+ bind(.desktopInventory, 5, hyper) // Hyper+G
234
235
  bind(.voiceCommand, 21, hyper) // Hyper+4 (moved from Hyper+3)
235
236
  let cmdCtrl = UInt32(cmdKey | controlKey)
236
237
  bind(.handsOff, 46, cmdCtrl) // Ctrl+Cmd+M
@@ -1,31 +1,5 @@
1
1
  import AppKit
2
2
 
3
- // MARK: - Intent Definition
4
-
5
- struct IntentDef {
6
- let name: String
7
- let description: String
8
- let examples: [String] // Example phrases that map to this intent
9
- let slots: [IntentSlot] // Named parameters extracted from the utterance
10
- let handler: (IntentRequest) throws -> JSON
11
- }
12
-
13
- struct IntentSlot {
14
- let name: String
15
- let type: String // "string", "int", "position", "query"
16
- let required: Bool
17
- let description: String
18
- let enumValues: [String]? // For constrained slots like tile positions
19
- }
20
-
21
- struct IntentRequest {
22
- let intent: String
23
- let slots: [String: JSON]
24
- let rawText: String? // Original transcription, for fallback matching
25
- let confidence: Double? // Transcription confidence from voice service
26
- let source: String? // "vox", "siri", "cli", etc.
27
- }
28
-
29
3
  // MARK: - Intent Engine
30
4
 
31
5
  final class IntentEngine {
@@ -133,6 +107,8 @@ final class IntentEngine {
133
107
  description: "Target window ID", enumValues: nil),
134
108
  IntentSlot(name: "session", type: "string", required: false,
135
109
  description: "Target session name", enumValues: nil),
110
+ IntentSlot(name: "selection", type: "bool", required: false,
111
+ description: "Apply to the active multi-window selection instead of a single window", enumValues: nil),
136
112
  ],
137
113
  handler: { req in
138
114
  guard let posStr = req.slots["position"]?.stringValue else {
@@ -176,6 +152,34 @@ final class IntentEngine {
176
152
  throw IntentError.targetNotFound("No window found for app '\(app)'")
177
153
  }
178
154
 
155
+ if req.slots["selection"]?.boolValue == true {
156
+ let selectionIds = WindowSelectionStore.shared.windowIds
157
+ guard !selectionIds.isEmpty else {
158
+ throw IntentError.targetNotFound("No active window selection")
159
+ }
160
+
161
+ if selectionIds.count == 1,
162
+ let wid = selectionIds.first,
163
+ let entry = DesktopModel.shared.windows[wid] {
164
+ tileEntry(entry)
165
+ return .object([
166
+ "ok": .bool(true),
167
+ "target": .string("selection"),
168
+ "wid": .int(Int(wid)),
169
+ "position": .string(posStr)
170
+ ])
171
+ }
172
+
173
+ return try LatticesApi.shared.dispatch(
174
+ method: "space.optimize",
175
+ params: .object([
176
+ "scope": .string("selection"),
177
+ "windowIds": .array(selectionIds.map { .int(Int($0)) }),
178
+ "region": .string(posStr)
179
+ ])
180
+ )
181
+ }
182
+
179
183
  // Default: tile frontmost window
180
184
  DispatchQueue.main.async {
181
185
  WindowTiler.tileFrontmostViaAX(to: placement)
@@ -401,6 +405,8 @@ final class IntentEngine {
401
405
  description: "Constrain the grid to a screen region. Uses tile position names.",
402
406
  enumValues: ["left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right",
403
407
  "left-third", "center-third", "right-third"]),
408
+ IntentSlot(name: "selection", type: "bool", required: false,
409
+ description: "Use the active selected windows instead of all visible windows", enumValues: nil),
404
410
  ],
405
411
  handler: { req in
406
412
  var params: [String: JSON] = [:]
@@ -413,6 +419,18 @@ final class IntentEngine {
413
419
  if let region = req.slots["region"]?.stringValue {
414
420
  params["region"] = .string(region)
415
421
  }
422
+ if req.slots["selection"]?.boolValue == true {
423
+ let selectionIds = WindowSelectionStore.shared.windowIds
424
+ guard !selectionIds.isEmpty else {
425
+ throw IntentError.targetNotFound("No active window selection")
426
+ }
427
+ params["scope"] = .string("selection")
428
+ params["windowIds"] = .array(selectionIds.map { .int(Int($0)) })
429
+ return try LatticesApi.shared.dispatch(
430
+ method: "space.optimize",
431
+ params: .object(params)
432
+ )
433
+ }
416
434
  return try LatticesApi.shared.dispatch(
417
435
  method: "layout.distribute",
418
436
  params: params.isEmpty ? nil : .object(params)
@@ -0,0 +1,94 @@
1
+ import Foundation
2
+
3
+ struct IntentDef {
4
+ let name: String
5
+ let description: String
6
+ let examples: [String]
7
+ let slots: [IntentSlot]
8
+ let handler: (IntentRequest) throws -> JSON
9
+ }
10
+
11
+ struct IntentSlot {
12
+ let name: String
13
+ let type: String
14
+ let required: Bool
15
+ let description: String
16
+ let enumValues: [String]?
17
+ let defaultValue: JSON?
18
+
19
+ init(
20
+ name: String,
21
+ type: String,
22
+ required: Bool,
23
+ description: String,
24
+ enumValues: [String]? = nil,
25
+ defaultValue: JSON? = nil
26
+ ) {
27
+ self.name = name
28
+ self.type = type
29
+ self.required = required
30
+ self.description = description
31
+ self.enumValues = enumValues
32
+ self.defaultValue = defaultValue
33
+ }
34
+ }
35
+
36
+ struct IntentRequest {
37
+ let intent: String
38
+ let slots: [String: JSON]
39
+ let rawText: String?
40
+ let confidence: Double?
41
+ let source: String?
42
+ }
43
+
44
+ enum SlotType {
45
+ case string
46
+ case int
47
+ case bool
48
+ case position
49
+ case query
50
+ case app
51
+ case session
52
+ case layer
53
+ case enumerated([String])
54
+
55
+ var typeLabel: String {
56
+ switch self {
57
+ case .string: return "string"
58
+ case .int: return "int"
59
+ case .bool: return "bool"
60
+ case .position: return "position"
61
+ case .query: return "query"
62
+ case .app: return "app"
63
+ case .session: return "session"
64
+ case .layer: return "layer"
65
+ case .enumerated: return "string"
66
+ }
67
+ }
68
+
69
+ var enumValues: [String]? {
70
+ guard case .enumerated(let values) = self else { return nil }
71
+ return values
72
+ }
73
+ }
74
+
75
+ typealias SlotDef = IntentSlot
76
+
77
+ extension IntentSlot {
78
+ init(
79
+ name: String,
80
+ type: SlotType,
81
+ required: Bool = true,
82
+ description: String = "",
83
+ defaultValue: JSON? = nil
84
+ ) {
85
+ self.init(
86
+ name: name,
87
+ type: type.typeLabel,
88
+ required: required,
89
+ description: description,
90
+ enumValues: type.enumValues,
91
+ defaultValue: defaultValue
92
+ )
93
+ }
94
+ }
@@ -12,31 +12,6 @@ protocol LatticeIntent {
12
12
  func perform(slots: [String: JSON]) throws -> JSON
13
13
  }
14
14
 
15
- // MARK: - Slot Definition
16
-
17
- enum SlotType {
18
- case string // Free-form text
19
- case position // Tile position (left, right, maximize, etc.)
20
- case app // Running app name
21
- case session // Active tmux session
22
- case layer // Layer name
23
- case enumerated([String]) // Fixed set of values
24
- }
25
-
26
- struct SlotDef {
27
- let name: String
28
- let type: SlotType
29
- let required: Bool
30
- let defaultValue: JSON?
31
-
32
- init(name: String, type: SlotType, required: Bool = true, defaultValue: JSON? = nil) {
33
- self.name = name
34
- self.type = type
35
- self.required = required
36
- self.defaultValue = defaultValue
37
- }
38
- }
39
-
40
15
  // MARK: - Compiled Phrase Template
41
16
 
42
17
  struct CompiledPhrase {
@@ -141,10 +141,43 @@ final class VoiceIntentResolver {
141
141
  private func extractSlots(for intentName: String, input: String) -> ExtractedSlots {
142
142
  switch intentName {
143
143
  case "tile_window":
144
- guard let position = resolvePosition(in: input) else {
144
+ var slots: [String: JSON] = [:]
145
+ var boost = 0.0
146
+
147
+ if let position = resolvePosition(in: input) {
148
+ slots["position"] = .string(position)
149
+ boost += 0.28
150
+ } else {
145
151
  return ExtractedSlots(slots: [:], boost: 0)
146
152
  }
147
- return ExtractedSlots(slots: ["position": .string(position)], boost: 0.28)
153
+
154
+ if refersToSelection(in: input) {
155
+ slots["selection"] = .bool(true)
156
+ boost += 0.08
157
+ }
158
+
159
+ return ExtractedSlots(slots: slots, boost: boost)
160
+
161
+ case "distribute":
162
+ var slots: [String: JSON] = [:]
163
+ var boost = 0.0
164
+
165
+ if let region = resolvePosition(in: input) {
166
+ slots["region"] = .string(region)
167
+ boost += 0.18
168
+ }
169
+
170
+ if let app = detectKnownApp(in: input) {
171
+ slots["app"] = .string(app)
172
+ boost += 0.14
173
+ }
174
+
175
+ if refersToSelection(in: input) {
176
+ slots["selection"] = .bool(true)
177
+ boost += 0.12
178
+ }
179
+
180
+ return ExtractedSlots(slots: slots, boost: boost)
148
181
 
149
182
  case "focus":
150
183
  if let app = detectKnownApp(in: input) ?? extractEntity(in: input, prefixes: focusPrefixes) {
@@ -426,6 +459,15 @@ final class VoiceIntentResolver {
426
459
  return nil
427
460
  }
428
461
 
462
+ private func refersToSelection(in input: String) -> Bool {
463
+ let markers = [
464
+ "grid that", "grid these", "grid those",
465
+ "tile that", "tile these", "tile those",
466
+ "selected windows", "selection", "selected", "these windows", "those windows", "them"
467
+ ]
468
+ return markers.contains(where: input.contains)
469
+ }
470
+
429
471
  private func detectKnownApp(in input: String) -> String? {
430
472
  for app in knownApps() {
431
473
  let lower = app.lowercased()
@@ -601,7 +643,7 @@ final class VoiceIntentResolver {
601
643
  "search": ["find", "search", "look for", "where is", "where d", "locate", "lost", "show me all", "windows"],
602
644
  "list_windows": ["what s open", "list windows", "which windows", "what do i have open"],
603
645
  "list_sessions": ["list sessions", "what s running", "which projects", "show my sessions"],
604
- "distribute": ["distribute", "spread", "organize", "arrange", "tidy", "clean up", "grid"],
646
+ "distribute": ["distribute", "spread", "organize", "arrange", "tidy", "clean up", "grid", "selected", "selection"],
605
647
  "create_layer": ["create layer", "save layout", "snapshot", "remember this layout"],
606
648
  "kill": ["kill", "stop", "shut down", "close", "terminate", "end"],
607
649
  "scan": ["scan", "rescan", "ocr", "read the screen", "what s on my screen", "screen text"],
@@ -662,7 +704,7 @@ final class VoiceIntentResolver {
662
704
  "search": ["where d my slack go", "pull up everything with dewey in it", "show me all the chrome windows", "dewey"],
663
705
  "list_windows": ["what do i have open", "what windows do i have"],
664
706
  "list_sessions": ["show me my sessions", "which projects are active"],
665
- "distribute": ["tidy up", "line everything up", "clean up the windows"],
707
+ "distribute": ["tidy up", "line everything up", "clean up the windows", "grid that in the bottom half", "arrange the selected windows"],
666
708
  "create_layer": ["snapshot this", "remember this layout"],
667
709
  "kill": ["close the dewey session", "stop my session"],
668
710
  "scan": ["what s on my screen", "read the screen", "give me a fresh scan"],
@@ -106,8 +106,7 @@ final class DesktopModel: ObservableObject {
106
106
  }
107
107
 
108
108
  func windowForSession(_ session: String) -> WindowEntry? {
109
- let tag = Terminal.windowTag(for: session)
110
- return windows.values.first { $0.title.contains(tag) }
109
+ SessionWindowLocator.cachedWindow(forSession: session, in: windows)
111
110
  }
112
111
 
113
112
  /// Assign a layer tag to a window (in-memory only)
@@ -205,12 +204,7 @@ final class DesktopModel: ObservableObject {
205
204
 
206
205
  let spaceIds = WindowTiler.getSpacesForWindow(wid)
207
206
 
208
- // Extract lattices session tag from title: [lattices:session-name]
209
- var latticesSession: String?
210
- if let range = title.range(of: #"\[lattices:([^\]]+)\]"#, options: .regularExpression) {
211
- let match = String(title[range])
212
- latticesSession = String(match.dropFirst(9).dropLast(1)) // drop "[lattices:" and "]"
213
- }
207
+ let latticesSession = SessionWindowLocator.extractSessionName(from: title)
214
208
 
215
209
  var entry = WindowEntry(
216
210
  wid: wid,
@@ -0,0 +1,139 @@
1
+ import AppKit
2
+ import ApplicationServices
3
+ import CoreGraphics
4
+
5
+ struct LocatedWindow {
6
+ let wid: UInt32
7
+ let pid: pid_t
8
+ }
9
+
10
+ enum SessionWindowLocator {
11
+ static func tag(for session: String) -> String {
12
+ Terminal.windowTag(for: session)
13
+ }
14
+
15
+ static func extractSessionName(from title: String) -> String? {
16
+ guard let range = title.range(of: #"\[lattices:([^\]]+)\]"#, options: .regularExpression) else {
17
+ return nil
18
+ }
19
+ let match = String(title[range])
20
+ return String(match.dropFirst(10).dropLast(1))
21
+ }
22
+
23
+ static func matches(session: String, title: String, extractedSessionName: String? = nil) -> Bool {
24
+ if extractedSessionName == session {
25
+ return true
26
+ }
27
+ return title.contains(tag(for: session))
28
+ }
29
+
30
+ static func cachedWindow(forSession session: String, in windows: [UInt32: WindowEntry]) -> WindowEntry? {
31
+ windows.values.first { entry in
32
+ matches(session: session, title: entry.title, extractedSessionName: entry.latticesSession)
33
+ }
34
+ }
35
+
36
+ static func cachedWindow(forSession session: String, desktopModel: DesktopModel = .shared) -> WindowEntry? {
37
+ cachedWindow(forSession: session, in: desktopModel.windows)
38
+ }
39
+
40
+ static func findCGWindow(tag: String) -> LocatedWindow? {
41
+ guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else {
42
+ return nil
43
+ }
44
+
45
+ for info in windowList {
46
+ if let name = info[kCGWindowName as String] as? String,
47
+ name.contains(tag),
48
+ let wid = info[kCGWindowNumber as String] as? UInt32,
49
+ let pid = info[kCGWindowOwnerPID as String] as? pid_t {
50
+ return LocatedWindow(wid: wid, pid: pid)
51
+ }
52
+ }
53
+ return nil
54
+ }
55
+
56
+ static func findWindow(session: String, terminal: Terminal) -> LocatedWindow? {
57
+ findWindow(tag: tag(for: session), terminal: terminal)
58
+ }
59
+
60
+ static func findWindow(tag: String, terminal: Terminal) -> LocatedWindow? {
61
+ if let match = findCGWindow(tag: tag) {
62
+ return match
63
+ }
64
+
65
+ if let ax = findAXWindow(terminal: terminal, tag: tag),
66
+ let wid = matchCGWindow(pid: ax.pid, axWindow: ax.window) {
67
+ return LocatedWindow(wid: wid, pid: ax.pid)
68
+ }
69
+
70
+ return nil
71
+ }
72
+
73
+ static func findAXWindow(terminal: Terminal, tag: String) -> (pid: pid_t, window: AXUIElement)? {
74
+ let diag = DiagnosticLog.shared
75
+ guard let app = NSWorkspace.shared.runningApplications.first(where: {
76
+ $0.bundleIdentifier == terminal.bundleId
77
+ }) else {
78
+ diag.error("SessionWindowLocator.findAXWindow: \(terminal.rawValue) (\(terminal.bundleId)) not running")
79
+ return nil
80
+ }
81
+
82
+ let pid = app.processIdentifier
83
+ let appRef = AXUIElementCreateApplication(pid)
84
+ var windowsRef: CFTypeRef?
85
+ let err = AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &windowsRef)
86
+ guard err == .success, let windows = windowsRef as? [AXUIElement] else {
87
+ diag.error("SessionWindowLocator.findAXWindow: AX error \(err.rawValue) — Accessibility not granted?")
88
+ return nil
89
+ }
90
+
91
+ diag.info("SessionWindowLocator.findAXWindow: \(windows.count) windows for \(terminal.rawValue), searching for \(tag)")
92
+ for win in windows {
93
+ var titleRef: CFTypeRef?
94
+ AXUIElementCopyAttributeValue(win, kAXTitleAttribute as CFString, &titleRef)
95
+ let title = titleRef as? String ?? "<no title>"
96
+ if title.contains(tag) {
97
+ diag.success("SessionWindowLocator.findAXWindow: matched \"\(title)\"")
98
+ return (pid, win)
99
+ } else {
100
+ diag.info(" skip: \"\(title)\"")
101
+ }
102
+ }
103
+
104
+ diag.warn("SessionWindowLocator.findAXWindow: no window matched tag \(tag)")
105
+ return nil
106
+ }
107
+
108
+ static func matchCGWindow(pid: pid_t, axWindow: AXUIElement) -> UInt32? {
109
+ var posRef: CFTypeRef?
110
+ var sizeRef: CFTypeRef?
111
+ AXUIElementCopyAttributeValue(axWindow, kAXPositionAttribute as CFString, &posRef)
112
+ AXUIElementCopyAttributeValue(axWindow, kAXSizeAttribute as CFString, &sizeRef)
113
+ guard let pv = posRef, let sv = sizeRef else { return nil }
114
+
115
+ var pos = CGPoint.zero
116
+ var size = CGSize.zero
117
+ AXValueGetValue(pv as! AXValue, .cgPoint, &pos)
118
+ AXValueGetValue(sv as! AXValue, .cgSize, &size)
119
+
120
+ guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else {
121
+ return nil
122
+ }
123
+
124
+ for info in windowList {
125
+ guard let wPid = info[kCGWindowOwnerPID as String] as? pid_t,
126
+ wPid == pid,
127
+ let wid = info[kCGWindowNumber as String] as? UInt32,
128
+ let boundsDict = info[kCGWindowBounds as String] as? NSDictionary else { continue }
129
+ var rect = CGRect.zero
130
+ if CGRectMakeWithDictionaryRepresentation(boundsDict, &rect) {
131
+ if abs(rect.origin.x - pos.x) < 2 && abs(rect.origin.y - pos.y) < 2 &&
132
+ abs(rect.width - size.width) < 2 && abs(rect.height - size.height) < 2 {
133
+ return wid
134
+ }
135
+ }
136
+ }
137
+ return nil
138
+ }
139
+ }
@@ -0,0 +1,100 @@
1
+ import AppKit
2
+ import SwiftUI
3
+
4
+ struct WindowPreviewCardStyle {
5
+ var containerCornerRadius: CGFloat = 10
6
+ var imageCornerRadius: CGFloat = 8
7
+ var imagePadding: CGFloat = 8
8
+ var background: Color = Palette.surface.opacity(0.8)
9
+ var border: Color = Palette.border
10
+ }
11
+
12
+ struct WindowPreviewCard<Overlay: View>: View {
13
+ let image: NSImage?
14
+ let isLoading: Bool
15
+ let appName: String
16
+ var loadingTitle: String = "Capturing preview"
17
+ var unavailableTitle: String = "Preview unavailable"
18
+ var style: WindowPreviewCardStyle = WindowPreviewCardStyle()
19
+ var holdingPreviousPreview: Bool = false
20
+ @ViewBuilder let overlay: () -> Overlay
21
+
22
+ var body: some View {
23
+ ZStack {
24
+ RoundedRectangle(cornerRadius: style.containerCornerRadius)
25
+ .fill(style.background)
26
+ .overlay(
27
+ RoundedRectangle(cornerRadius: style.containerCornerRadius)
28
+ .strokeBorder(style.border, lineWidth: 0.5)
29
+ )
30
+
31
+ if let image {
32
+ Image(nsImage: image)
33
+ .resizable()
34
+ .aspectRatio(contentMode: .fit)
35
+ .clipShape(RoundedRectangle(cornerRadius: style.imageCornerRadius))
36
+ .padding(style.imagePadding)
37
+ .opacity(holdingPreviousPreview ? 0.88 : 1)
38
+ } else if isLoading {
39
+ WindowPreviewPlaceholder(
40
+ icon: "photo",
41
+ title: loadingTitle,
42
+ subtitle: appName
43
+ )
44
+ } else {
45
+ WindowPreviewPlaceholder(
46
+ icon: "eye.slash",
47
+ title: unavailableTitle,
48
+ subtitle: appName
49
+ )
50
+ }
51
+
52
+ overlay()
53
+ }
54
+ }
55
+ }
56
+
57
+ extension WindowPreviewCard where Overlay == EmptyView {
58
+ init(
59
+ image: NSImage?,
60
+ isLoading: Bool,
61
+ appName: String,
62
+ loadingTitle: String = "Capturing preview",
63
+ unavailableTitle: String = "Preview unavailable",
64
+ style: WindowPreviewCardStyle = WindowPreviewCardStyle(),
65
+ holdingPreviousPreview: Bool = false
66
+ ) {
67
+ self.init(
68
+ image: image,
69
+ isLoading: isLoading,
70
+ appName: appName,
71
+ loadingTitle: loadingTitle,
72
+ unavailableTitle: unavailableTitle,
73
+ style: style,
74
+ holdingPreviousPreview: holdingPreviousPreview,
75
+ overlay: { EmptyView() }
76
+ )
77
+ }
78
+ }
79
+
80
+ private struct WindowPreviewPlaceholder: View {
81
+ let icon: String
82
+ let title: String
83
+ let subtitle: String
84
+
85
+ var body: some View {
86
+ VStack(spacing: 8) {
87
+ Image(systemName: icon)
88
+ .font(.system(size: 18, weight: .medium))
89
+ .foregroundColor(Palette.textMuted.opacity(0.7))
90
+ Text(title)
91
+ .font(Typo.monoBold(10))
92
+ .foregroundColor(Palette.textMuted)
93
+ Text(subtitle)
94
+ .font(Typo.mono(9))
95
+ .foregroundColor(Palette.textDim)
96
+ .lineLimit(1)
97
+ }
98
+ .padding(16)
99
+ }
100
+ }