@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
@@ -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
+ }
@@ -0,0 +1,141 @@
1
+ import AppKit
2
+ import Foundation
3
+
4
+ enum CompanionKeyboardError: LocalizedError {
5
+ case unknownKey(String)
6
+ case eventSourceUnavailable
7
+
8
+ var errorDescription: String? {
9
+ switch self {
10
+ case .unknownKey(let key):
11
+ return "Unsupported key for forwarding: \(key)"
12
+ case .eventSourceUnavailable:
13
+ return "Unable to create a keyboard event source."
14
+ }
15
+ }
16
+ }
17
+
18
+ final class CompanionKeyboardController {
19
+ static let shared = CompanionKeyboardController()
20
+
21
+ private init() {}
22
+
23
+ func send(key rawKey: String, modifiers rawModifiers: [String]) throws -> String {
24
+ let parsed = parse(key: rawKey, modifiers: rawModifiers)
25
+ guard let keyCode = keyCode(for: parsed.key) else {
26
+ throw CompanionKeyboardError.unknownKey(rawKey)
27
+ }
28
+ guard let source = CGEventSource(stateID: .combinedSessionState) else {
29
+ throw CompanionKeyboardError.eventSourceUnavailable
30
+ }
31
+
32
+ let flags = eventFlags(for: parsed.modifiers)
33
+ guard
34
+ let down = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: true),
35
+ let up = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: false)
36
+ else {
37
+ throw CompanionKeyboardError.eventSourceUnavailable
38
+ }
39
+
40
+ down.flags = flags
41
+ up.flags = flags
42
+ down.post(tap: .cghidEventTap)
43
+ usleep(12_000)
44
+ up.post(tap: .cghidEventTap)
45
+
46
+ return displayName(key: parsed.key, modifiers: parsed.modifiers)
47
+ }
48
+ }
49
+
50
+ private extension CompanionKeyboardController {
51
+ func parse(key rawKey: String, modifiers rawModifiers: [String]) -> (key: String, modifiers: Set<String>) {
52
+ var modifiers = Set(rawModifiers.map(normalizeModifier).filter { !$0.isEmpty })
53
+ var key = rawKey
54
+ .trimmingCharacters(in: .whitespacesAndNewlines)
55
+ .lowercased()
56
+
57
+ let symbolModifiers: [(String, String)] = [
58
+ ("⌘", "command"),
59
+ ("cmd", "command"),
60
+ ("command", "command"),
61
+ ("⌥", "option"),
62
+ ("option", "option"),
63
+ ("alt", "option"),
64
+ ("⌃", "control"),
65
+ ("ctrl", "control"),
66
+ ("control", "control"),
67
+ ("⇧", "shift"),
68
+ ("shift", "shift"),
69
+ ]
70
+
71
+ for (symbol, modifier) in symbolModifiers where key.contains(symbol) {
72
+ modifiers.insert(modifier)
73
+ key = key.replacingOccurrences(of: symbol, with: "")
74
+ }
75
+
76
+ key = key
77
+ .replacingOccurrences(of: "+", with: "")
78
+ .replacingOccurrences(of: " ", with: "")
79
+ .replacingOccurrences(of: "⎋", with: "escape")
80
+ .replacingOccurrences(of: "⇥", with: "tab")
81
+ .replacingOccurrences(of: "↩", with: "enter")
82
+ .replacingOccurrences(of: "⏎", with: "enter")
83
+ .replacingOccurrences(of: "return", with: "enter")
84
+ .replacingOccurrences(of: "←", with: "left")
85
+ .replacingOccurrences(of: "→", with: "right")
86
+ .replacingOccurrences(of: "↑", with: "up")
87
+ .replacingOccurrences(of: "↓", with: "down")
88
+
89
+ if key == "esc" {
90
+ key = "escape"
91
+ }
92
+
93
+ return (key.isEmpty ? rawKey.lowercased() : key, modifiers)
94
+ }
95
+
96
+ func normalizeModifier(_ modifier: String) -> String {
97
+ switch modifier.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
98
+ case "cmd", "command", "meta", "⌘":
99
+ return "command"
100
+ case "opt", "option", "alt", "⌥":
101
+ return "option"
102
+ case "ctrl", "control", "⌃":
103
+ return "control"
104
+ case "shift", "⇧":
105
+ return "shift"
106
+ default:
107
+ return ""
108
+ }
109
+ }
110
+
111
+ func eventFlags(for modifiers: Set<String>) -> CGEventFlags {
112
+ var flags: CGEventFlags = []
113
+ if modifiers.contains("command") { flags.insert(.maskCommand) }
114
+ if modifiers.contains("option") { flags.insert(.maskAlternate) }
115
+ if modifiers.contains("control") { flags.insert(.maskControl) }
116
+ if modifiers.contains("shift") { flags.insert(.maskShift) }
117
+ return flags
118
+ }
119
+
120
+ func keyCode(for key: String) -> CGKeyCode? {
121
+ let codes: [String: CGKeyCode] = [
122
+ "a": 0, "s": 1, "d": 2, "f": 3, "h": 4, "g": 5, "z": 6, "x": 7,
123
+ "c": 8, "v": 9, "b": 11, "q": 12, "w": 13, "e": 14, "r": 15,
124
+ "y": 16, "t": 17, "1": 18, "2": 19, "3": 20, "4": 21, "6": 22,
125
+ "5": 23, "=": 24, "9": 25, "7": 26, "-": 27, "8": 28, "0": 29,
126
+ "]": 30, "o": 31, "u": 32, "[": 33, "i": 34, "p": 35, "enter": 36,
127
+ "l": 37, "j": 38, "'": 39, "k": 40, ";": 41, "\\": 42, ",": 43,
128
+ "/": 44, "n": 45, "m": 46, ".": 47, "tab": 48, "space": 49,
129
+ "`": 50, "delete": 51, "backspace": 51, "escape": 53,
130
+ "command": 55, "cmd": 55, "shift": 56, "capslock": 57, "option": 58,
131
+ "alt": 58, "control": 59, "left": 123, "right": 124, "down": 125,
132
+ "up": 126,
133
+ ]
134
+ return codes[key]
135
+ }
136
+
137
+ func displayName(key: String, modifiers: Set<String>) -> String {
138
+ let ordered = ["control", "option", "shift", "command"].filter { modifiers.contains($0) }
139
+ return (ordered + [key]).joined(separator: "+")
140
+ }
141
+ }
@@ -81,6 +81,10 @@ final class DesktopModel: ObservableObject {
81
81
  Array(windows.values).sorted { $0.zIndex < $1.zIndex }
82
82
  }
83
83
 
84
+ func frontmostWindow() -> WindowEntry? {
85
+ windows.values.min { $0.zIndex < $1.zIndex }
86
+ }
87
+
84
88
  func lastInteractionDate(for wid: UInt32) -> Date? {
85
89
  interactionDates[wid]
86
90
  }
@@ -43,7 +43,14 @@ final class HandsOffSession: ObservableObject {
43
43
  case thinking
44
44
  }
45
45
 
46
- @Published var state: State = .idle
46
+ @Published var state: State = .idle {
47
+ didSet {
48
+ if state != oldValue {
49
+ stateChangedAt = Date()
50
+ }
51
+ }
52
+ }
53
+ @Published private(set) var stateChangedAt: Date = Date()
47
54
  @Published var lastTranscript: String?
48
55
  @Published var lastResponse: String?
49
56
  @Published var audibleFeedbackEnabled: Bool = false
@@ -58,6 +65,7 @@ final class HandsOffSession: ObservableObject {
58
65
  let frame: WindowFrame
59
66
  }
60
67
  private(set) var frameHistory: [FrameSnapshot] = []
68
+ private(set) var frameHistoryUpdatedAt: Date?
61
69
 
62
70
  /// Snapshot current frames for all windows that are about to be moved.
63
71
  /// Stores frames in CG/AX coordinates (top-left origin) for direct use with batchRestoreWindows.
@@ -77,10 +85,12 @@ final class HandsOffSession: ObservableObject {
77
85
  break
78
86
  }
79
87
  }
88
+ frameHistoryUpdatedAt = frameHistory.isEmpty ? nil : Date()
80
89
  }
81
90
 
82
91
  func clearFrameHistory() {
83
92
  frameHistory.removeAll()
93
+ frameHistoryUpdatedAt = nil
84
94
  }
85
95
 
86
96
  /// Running chat log — visible in the voice chat panel. Persists across turns.
@@ -97,6 +107,15 @@ final class HandsOffSession: ObservableObject {
97
107
  private var workerBuffer = ""
98
108
  private let workerQueue = DispatchQueue(label: "com.lattices.handsoff-worker", qos: .userInitiated)
99
109
  private var lastCueAt: Date = .distantPast
110
+ private var workerRoot: String? {
111
+ if let idx = CommandLine.arguments.firstIndex(of: "--lattices-cli-root"),
112
+ CommandLine.arguments.indices.contains(idx + 1) {
113
+ return CommandLine.arguments[idx + 1]
114
+ }
115
+
116
+ let devRoot = NSHomeDirectory() + "/dev/lattices"
117
+ return FileManager.default.fileExists(atPath: devRoot) ? devRoot : nil
118
+ }
100
119
 
101
120
  /// JSONL log for full turn data — ~/.lattices/handsoff.jsonl
102
121
  private let turnLogPath = NSHomeDirectory() + "/.lattices/handsoff.jsonl"
@@ -122,7 +141,7 @@ final class HandsOffSession: ObservableObject {
122
141
  // MARK: - Lifecycle
123
142
 
124
143
  func start() {
125
- startWorker()
144
+ // Worker startup is lazy — only start it when a voice turn or cached cue needs it.
126
145
  }
127
146
 
128
147
  func setAudibleFeedbackEnabled(_ enabled: Bool) {
@@ -170,9 +189,10 @@ final class HandsOffSession: ObservableObject {
170
189
  }
171
190
  }
172
191
 
173
- private func startWorker() {
192
+ @discardableResult
193
+ private func startWorker() -> Bool {
174
194
  if workerProcess?.isRunning == true, workerStdin != nil {
175
- return
195
+ return true
176
196
  }
177
197
 
178
198
  let bunPaths = [
@@ -182,19 +202,24 @@ final class HandsOffSession: ObservableObject {
182
202
  ]
183
203
  guard let bunPath = bunPaths.first(where: { FileManager.default.isExecutableFile(atPath: $0) }) else {
184
204
  DiagnosticLog.shared.warn("HandsOff: bun not found, worker disabled")
185
- return
205
+ return false
186
206
  }
187
207
 
188
- let scriptPath = NSHomeDirectory() + "/dev/lattices/bin/handsoff-worker.ts"
208
+ guard let workerRoot else {
209
+ DiagnosticLog.shared.warn("HandsOff: worker root not found, worker disabled")
210
+ return false
211
+ }
212
+
213
+ let scriptPath = workerRoot + "/bin/handsoff-worker.ts"
189
214
  guard FileManager.default.fileExists(atPath: scriptPath) else {
190
215
  DiagnosticLog.shared.warn("HandsOff: worker script not found at \(scriptPath)")
191
- return
216
+ return false
192
217
  }
193
218
 
194
219
  let proc = Process()
195
220
  proc.executableURL = URL(fileURLWithPath: bunPath)
196
221
  proc.arguments = ["run", scriptPath]
197
- proc.currentDirectoryURL = URL(fileURLWithPath: NSHomeDirectory() + "/dev/lattices")
222
+ proc.currentDirectoryURL = URL(fileURLWithPath: workerRoot)
198
223
 
199
224
  var env = ProcessInfo.processInfo.environment
200
225
  env.removeValue(forKey: "CLAUDECODE")
@@ -211,7 +236,7 @@ final class HandsOffSession: ObservableObject {
211
236
  try proc.run()
212
237
  } catch {
213
238
  DiagnosticLog.shared.warn("HandsOff: failed to start worker — \(error)")
214
- return
239
+ return false
215
240
  }
216
241
 
217
242
  workerProcess = proc
@@ -235,17 +260,22 @@ final class HandsOffSession: ObservableObject {
235
260
 
236
261
  // Handle worker crash → restart
237
262
  proc.terminationHandler = { [weak self] proc in
238
- DiagnosticLog.shared.warn("HandsOff: worker exited (code \(proc.terminationStatus)), restarting in 2s")
239
- self?.workerProcess = nil
240
- self?.workerStdin = nil
263
+ guard let self else { return }
264
+ let keepWarm = self.audibleFeedbackEnabled || self.state != .idle
265
+ let suffix = keepWarm ? ", restarting in 2s" : ", staying idle"
266
+ DiagnosticLog.shared.warn("HandsOff: worker exited (code \(proc.terminationStatus))\(suffix)")
267
+ self.workerProcess = nil
268
+ self.workerStdin = nil
269
+ guard keepWarm else { return }
241
270
  DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
242
- self?.startWorker()
271
+ self.startWorker()
243
272
  }
244
273
  }
245
274
 
246
275
  // Ping to verify
247
276
  sendToWorker(["cmd": "ping"])
248
277
  DiagnosticLog.shared.info("HandsOff: worker started (pid \(proc.processIdentifier))")
278
+ return true
249
279
  }
250
280
 
251
281
  // MARK: - Worker communication
@@ -307,7 +337,9 @@ final class HandsOffSession: ObservableObject {
307
337
  }
308
338
  }
309
339
 
310
- // Single dispatch — all @Published mutations in one block
340
+ // Single dispatch — all @Published mutations in one block.
341
+ // The pending callback also mutates @Published state, so it must
342
+ // run on main with the rest of the turn completion.
311
343
  DispatchQueue.main.async { [weak self] in
312
344
  guard let self else { return }
313
345
  if let spoken { self.lastResponse = spoken }
@@ -322,9 +354,8 @@ final class HandsOffSession: ObservableObject {
322
354
  self.executeActions(actions)
323
355
  }
324
356
  self.state = .idle
357
+ cb?(json)
325
358
  }
326
-
327
- cb?(json)
328
359
  }
329
360
  }
330
361
 
@@ -486,6 +517,12 @@ final class HandsOffSession: ObservableObject {
486
517
 
487
518
  private func processTurn(_ transcript: String) {
488
519
  state = .thinking
520
+ guard startWorker() else {
521
+ state = .idle
522
+ DiagnosticLog.shared.warn("HandsOff: worker unavailable")
523
+ playSound("Basso")
524
+ return
525
+ }
489
526
  turnCount += 1
490
527
 
491
528
  let turnStart = Date()
@@ -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
  }
@@ -31,7 +31,7 @@ enum HotkeyAction: String, CaseIterable, Codable {
31
31
  // Tiling
32
32
  case tileLeft, tileRight, tileMaximize, tileCenter
33
33
  case tileTopLeft, tileTopRight, tileBottomLeft, tileBottomRight
34
- case tileTop, tileBottom, tileDistribute
34
+ case tileTop, tileBottom, tileDistribute, tileTypeGrid
35
35
  case tileLeftThird, tileCenterThird, tileRightThird
36
36
 
37
37
  var label: String {
@@ -40,11 +40,11 @@ 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 "Desktop Inventory"
44
- case .omniSearch: return "Omni Search"
43
+ case .desktopInventory: return "Search"
44
+ case .omniSearch: return "Search"
45
45
  case .voiceCommand: return "Voice Command"
46
46
  case .handsOff: return "Hands-Off Mode"
47
- case .unifiedWindow: return "Unified Window"
47
+ case .unifiedWindow: return "Workspace Home"
48
48
  case .hud: return "HUD"
49
49
  case .mouseFinder: return "Find Mouse"
50
50
  case .layer1: return "Layer 1"
@@ -70,6 +70,7 @@ enum HotkeyAction: String, CaseIterable, Codable {
70
70
  case .tileTop: return "Top Half"
71
71
  case .tileBottom: return "Bottom Half"
72
72
  case .tileDistribute: return "Distribute"
73
+ case .tileTypeGrid: return "Grid Type"
73
74
  case .tileLeftThird: return "Left Third"
74
75
  case .tileCenterThird: return "Center Third"
75
76
  case .tileRightThird: return "Right Third"
@@ -122,6 +123,7 @@ enum HotkeyAction: String, CaseIterable, Codable {
122
123
  case .tileTop: return 308
123
124
  case .tileBottom: return 309
124
125
  case .tileDistribute: return 310
126
+ case .tileTypeGrid: return 314
125
127
  case .tileLeftThird: return 311
126
128
  case .tileCenterThird: return 312
127
129
  case .tileRightThird: return 313
@@ -226,7 +228,7 @@ class HotkeyStore: ObservableObject {
226
228
 
227
229
  // App
228
230
  bind(.palette, 46, cmdShift) // Cmd+Shift+M
229
- bind(.unifiedWindow, 18, hyper) // Hyper+1 (Screen Map + Desktop Inventory)
231
+ bind(.unifiedWindow, 18, hyper) // Hyper+1 (Workspace Home)
230
232
  bind(.bezel, 19, hyper) // Hyper+2
231
233
  bind(.hud, 20, hyper) // Hyper+3 (HUD overlay)
232
234
  bind(.voiceCommand, 21, hyper) // Hyper+4 (moved from Hyper+3)
@@ -259,6 +261,7 @@ class HotkeyStore: ObservableObject {
259
261
  bind(.tileBottomLeft, 38, ctrlOpt) // Ctrl+Opt+J
260
262
  bind(.tileBottomRight, 40, ctrlOpt) // Ctrl+Opt+K
261
263
  bind(.tileDistribute, 2, ctrlOpt) // Ctrl+Opt+D
264
+ bind(.tileTypeGrid, 5, hyper) // Hyper+G
262
265
  bind(.tileLeftThird, 18, ctrlOpt) // Ctrl+Opt+1
263
266
  bind(.tileCenterThird, 19, ctrlOpt) // Ctrl+Opt+2
264
267
  bind(.tileRightThird, 20, ctrlOpt) // Ctrl+Opt+3
@@ -381,7 +381,7 @@ final class IntentEngine {
381
381
 
382
382
  register(IntentDef(
383
383
  name: "distribute",
384
- description: "Distribute windows evenly in a grid, optionally filtered by app and constrained to a screen region",
384
+ description: "Distribute windows evenly in a grid, optionally filtered by app or window type and constrained to a screen region",
385
385
  examples: [
386
386
  "spread out the windows",
387
387
  "distribute everything",
@@ -394,6 +394,9 @@ final class IntentEngine {
394
394
  slots: [
395
395
  IntentSlot(name: "app", type: "string", required: false,
396
396
  description: "Filter to windows of this app (e.g. 'iTerm2', 'Google Chrome')", enumValues: nil),
397
+ IntentSlot(name: "type", type: "string", required: false,
398
+ description: "Filter to a window type (e.g. 'terminal', 'browser', 'editor')",
399
+ enumValues: AppType.allCases.map(\.rawValue)),
397
400
  IntentSlot(name: "region", type: "position", required: false,
398
401
  description: "Constrain the grid to a screen region. Uses tile position names.",
399
402
  enumValues: ["left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right",
@@ -404,6 +407,9 @@ final class IntentEngine {
404
407
  if let app = req.slots["app"]?.stringValue {
405
408
  params["app"] = .string(app)
406
409
  }
410
+ if let type = req.slots["type"]?.stringValue {
411
+ params["type"] = .string(type)
412
+ }
407
413
  if let region = req.slots["region"]?.stringValue {
408
414
  params["region"] = .string(region)
409
415
  }