@lattices/cli 0.4.1 → 0.4.2

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.
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.2</string>
19
19
  <key>CFBundleShortVersionString</key>
20
- <string>0.4.1</string>
20
+ <string>0.4.2</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.2</string>
19
19
  <key>CFBundleShortVersionString</key>
20
- <string>0.4.1</string>
20
+ <string>0.4.2</string>
21
21
  <key>LSMinimumSystemVersion</key>
22
22
  <string>13.0</string>
23
23
  <key>LSUIElement</key>
@@ -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)
@@ -158,7 +158,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
158
158
  LatticesApi.setup()
159
159
  DaemonServer.shared.start()
160
160
  AgentPool.shared.start()
161
- HandsOffSession.shared.start()
162
161
  diag.finish(tBoot)
163
162
 
164
163
  // --diagnostics flag: auto-open diagnostics panel on launch
@@ -229,11 +228,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
229
228
 
230
229
  let actions: [(String, String, Selector)] = [
231
230
  ("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)),
231
+ ("Workspace", "", #selector(menuWorkspace)),
232
+ ("Assistant", "", #selector(menuAssistant)),
233
+ ("Help & Shortcuts", "", #selector(menuDocs)),
237
234
  ]
238
235
  for (title, shortcut, action) in actions {
239
236
  let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
@@ -264,7 +261,15 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
264
261
  }
265
262
 
266
263
  @objc private func menuCommandPalette() { CommandPaletteWindow.shared.toggle() }
267
- @objc private func menuScreenMap() { ScreenMapWindowController.shared.toggle() }
264
+ @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) }
268
273
  @objc private func menuHUD() { HUDController.shared.toggle() }
269
274
  @objc private func menuWindowBezel() { WindowBezel.showBezelForFrontmostWindow() }
270
275
  @objc private func menuCheatSheet() { CheatSheetHUD.shared.toggle() }
@@ -97,6 +97,15 @@ final class HandsOffSession: ObservableObject {
97
97
  private var workerBuffer = ""
98
98
  private let workerQueue = DispatchQueue(label: "com.lattices.handsoff-worker", qos: .userInitiated)
99
99
  private var lastCueAt: Date = .distantPast
100
+ private var workerRoot: String? {
101
+ if let idx = CommandLine.arguments.firstIndex(of: "--lattices-cli-root"),
102
+ CommandLine.arguments.indices.contains(idx + 1) {
103
+ return CommandLine.arguments[idx + 1]
104
+ }
105
+
106
+ let devRoot = NSHomeDirectory() + "/dev/lattices"
107
+ return FileManager.default.fileExists(atPath: devRoot) ? devRoot : nil
108
+ }
100
109
 
101
110
  /// JSONL log for full turn data — ~/.lattices/handsoff.jsonl
102
111
  private let turnLogPath = NSHomeDirectory() + "/.lattices/handsoff.jsonl"
@@ -122,7 +131,7 @@ final class HandsOffSession: ObservableObject {
122
131
  // MARK: - Lifecycle
123
132
 
124
133
  func start() {
125
- startWorker()
134
+ // Worker startup is lazy — only start it when a voice turn or cached cue needs it.
126
135
  }
127
136
 
128
137
  func setAudibleFeedbackEnabled(_ enabled: Bool) {
@@ -170,9 +179,10 @@ final class HandsOffSession: ObservableObject {
170
179
  }
171
180
  }
172
181
 
173
- private func startWorker() {
182
+ @discardableResult
183
+ private func startWorker() -> Bool {
174
184
  if workerProcess?.isRunning == true, workerStdin != nil {
175
- return
185
+ return true
176
186
  }
177
187
 
178
188
  let bunPaths = [
@@ -182,19 +192,24 @@ final class HandsOffSession: ObservableObject {
182
192
  ]
183
193
  guard let bunPath = bunPaths.first(where: { FileManager.default.isExecutableFile(atPath: $0) }) else {
184
194
  DiagnosticLog.shared.warn("HandsOff: bun not found, worker disabled")
185
- return
195
+ return false
196
+ }
197
+
198
+ guard let workerRoot else {
199
+ DiagnosticLog.shared.warn("HandsOff: worker root not found, worker disabled")
200
+ return false
186
201
  }
187
202
 
188
- let scriptPath = NSHomeDirectory() + "/dev/lattices/bin/handsoff-worker.ts"
203
+ let scriptPath = workerRoot + "/bin/handsoff-worker.ts"
189
204
  guard FileManager.default.fileExists(atPath: scriptPath) else {
190
205
  DiagnosticLog.shared.warn("HandsOff: worker script not found at \(scriptPath)")
191
- return
206
+ return false
192
207
  }
193
208
 
194
209
  let proc = Process()
195
210
  proc.executableURL = URL(fileURLWithPath: bunPath)
196
211
  proc.arguments = ["run", scriptPath]
197
- proc.currentDirectoryURL = URL(fileURLWithPath: NSHomeDirectory() + "/dev/lattices")
212
+ proc.currentDirectoryURL = URL(fileURLWithPath: workerRoot)
198
213
 
199
214
  var env = ProcessInfo.processInfo.environment
200
215
  env.removeValue(forKey: "CLAUDECODE")
@@ -211,7 +226,7 @@ final class HandsOffSession: ObservableObject {
211
226
  try proc.run()
212
227
  } catch {
213
228
  DiagnosticLog.shared.warn("HandsOff: failed to start worker — \(error)")
214
- return
229
+ return false
215
230
  }
216
231
 
217
232
  workerProcess = proc
@@ -235,17 +250,22 @@ final class HandsOffSession: ObservableObject {
235
250
 
236
251
  // Handle worker crash → restart
237
252
  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
253
+ guard let self else { return }
254
+ let keepWarm = self.audibleFeedbackEnabled || self.state != .idle
255
+ let suffix = keepWarm ? ", restarting in 2s" : ", staying idle"
256
+ DiagnosticLog.shared.warn("HandsOff: worker exited (code \(proc.terminationStatus))\(suffix)")
257
+ self.workerProcess = nil
258
+ self.workerStdin = nil
259
+ guard keepWarm else { return }
241
260
  DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
242
- self?.startWorker()
261
+ self.startWorker()
243
262
  }
244
263
  }
245
264
 
246
265
  // Ping to verify
247
266
  sendToWorker(["cmd": "ping"])
248
267
  DiagnosticLog.shared.info("HandsOff: worker started (pid \(proc.processIdentifier))")
268
+ return true
249
269
  }
250
270
 
251
271
  // MARK: - Worker communication
@@ -486,6 +506,12 @@ final class HandsOffSession: ObservableObject {
486
506
 
487
507
  private func processTurn(_ transcript: String) {
488
508
  state = .thinking
509
+ guard startWorker() else {
510
+ state = .idle
511
+ DiagnosticLog.shared.warn("HandsOff: worker unavailable")
512
+ playSound("Basso")
513
+ return
514
+ }
489
515
  turnCount += 1
490
516
 
491
517
  let turnStart = Date()
@@ -283,27 +283,50 @@ struct MainView: View {
283
283
 
284
284
  private var actionsSection: some View {
285
285
  VStack(spacing: 0) {
286
- ActionRow(shortcut: "1", label: "Command Palette", hotkey: hotkeyLabel(.palette), icon: "command", accentColor: Palette.running) {
287
- CommandPaletteWindow.shared.toggle()
288
- }
289
- ActionRow(shortcut: "2", label: "Screen Map", hotkey: hotkeyLabel(.screenMap), icon: "rectangle.3.group") {
290
- ScreenMapWindowController.shared.toggle()
291
- }
292
- ActionRow(shortcut: "3", label: "Desktop Inventory", hotkey: hotkeyLabel(.desktopInventory), icon: "rectangle.split.2x1") {
293
- CommandModeWindow.shared.toggle()
294
- }
295
- ActionRow(shortcut: "4", label: "Window Bezel", hotkey: hotkeyLabel(.bezel), icon: "macwindow") {
296
- WindowBezel.showBezelForFrontmostWindow()
286
+ HStack {
287
+ Text("Quick Actions")
288
+ .font(Typo.monoBold(10))
289
+ .foregroundColor(Palette.textMuted)
290
+
291
+ Spacer()
292
+
293
+ Button("Help & shortcuts") {
294
+ ScreenMapWindowController.shared.showPage(.docs)
295
+ }
296
+ .buttonStyle(.plain)
297
+ .font(Typo.mono(9))
298
+ .foregroundColor(Palette.textMuted)
297
299
  }
298
- ActionRow(shortcut: "5", label: "Cheat Sheet", hotkey: hotkeyLabel(.cheatSheet), icon: "keyboard") {
299
- CheatSheetHUD.shared.toggle()
300
+ .padding(.horizontal, 14)
301
+ .padding(.top, 10)
302
+ .padding(.bottom, 6)
303
+
304
+ ActionRow(
305
+ label: "Command Palette",
306
+ detail: "Launch, attach, and control projects",
307
+ hotkeyTokens: hotkeyTokens(.palette),
308
+ icon: "command",
309
+ accentColor: Palette.running
310
+ ) {
311
+ CommandPaletteWindow.shared.toggle()
300
312
  }
301
- ActionRow(shortcut: "6", label: "Omni Search", hotkey: hotkeyLabel(.omniSearch), icon: "magnifyingglass", accentColor: Palette.running) {
302
- OmniSearchWindow.shared.toggle()
313
+ ActionRow(
314
+ label: "Workspace",
315
+ detail: "Screen map, inventory, and window context",
316
+ hotkeyTokens: hotkeyTokens(.unifiedWindow),
317
+ icon: "square.grid.2x2",
318
+ accentColor: Palette.text
319
+ ) {
320
+ ScreenMapWindowController.shared.showPage(.home)
303
321
  }
304
- ActionRow(shortcut: "7", label: "Voice Command", hotkey: hotkeyLabel(.voiceCommand), icon: "mic", accentColor: AudioLayer.shared.isListening ? Palette.running : Palette.textDim) {
305
- let audio = AudioLayer.shared
306
- if audio.isListening { audio.stopVoiceCommand() } else { audio.startVoiceCommand() }
322
+ ActionRow(
323
+ label: "Assistant",
324
+ detail: "Search now, or use voice when you need it",
325
+ hotkeyTokens: hotkeyTokens(.omniSearch),
326
+ icon: "magnifyingglass",
327
+ accentColor: AudioLayer.shared.isListening ? Palette.running : Palette.textDim
328
+ ) {
329
+ showAssistant()
307
330
  }
308
331
  }
309
332
  .padding(.vertical, 4)
@@ -351,9 +374,38 @@ struct MainView: View {
351
374
  .buttonStyle(.plain)
352
375
  }
353
376
 
354
- private func hotkeyLabel(_ action: HotkeyAction) -> String? {
355
- guard let binding = HotkeyStore.shared.bindings[action] else { return nil }
356
- return binding.displayParts.joined(separator: "")
377
+ private func hotkeyTokens(_ action: HotkeyAction) -> [String] {
378
+ guard let binding = HotkeyStore.shared.bindings[action],
379
+ let key = binding.displayParts.last else { return [] }
380
+
381
+ let modifiers = Set(binding.displayParts.dropLast())
382
+ if modifiers == Set(["Ctrl", "Option", "Shift", "Cmd"]) {
383
+ return ["Hyper", shortenHotkeyToken(key)]
384
+ }
385
+
386
+ return binding.displayParts.map(shortenHotkeyToken)
387
+ }
388
+
389
+ private func shortenHotkeyToken(_ token: String) -> String {
390
+ switch token {
391
+ case "Cmd": return "⌘"
392
+ case "Shift": return "⇧"
393
+ case "Option": return "⌥"
394
+ case "Ctrl": return "⌃"
395
+ case "Return": return "↩"
396
+ case "Escape": return "Esc"
397
+ case "Space": return "Space"
398
+ default: return token
399
+ }
400
+ }
401
+
402
+ private func showAssistant() {
403
+ if AudioLayer.shared.isListening || VoiceCommandWindow.shared.isVisible {
404
+ VoiceCommandWindow.shared.toggle()
405
+ return
406
+ }
407
+
408
+ OmniSearchWindow.shared.show()
357
409
  }
358
410
 
359
411
  // MARK: - Empty state
@@ -9,6 +9,7 @@ import type { IncomingMessage } from "node:http";
9
9
 
10
10
  const __dirname = import.meta.dir;
11
11
  const appDir = resolve(__dirname, "../app");
12
+ const cliRoot = resolve(__dirname, "..");
12
13
  const bundlePath = resolve(appDir, "Lattices.app");
13
14
  const binaryDir = resolve(bundlePath, "Contents/MacOS");
14
15
  const binaryPath = resolve(binaryDir, "Lattices");
@@ -72,7 +73,8 @@ function launch(extraArgs: string[] = []): void {
72
73
  return;
73
74
  }
74
75
  const args = [bundlePath];
75
- if (extraArgs.length) args.push("--args", ...extraArgs);
76
+ const appArgs = ["--lattices-cli-root", cliRoot, ...extraArgs];
77
+ if (appArgs.length) args.push("--args", ...appArgs);
76
78
  spawn("open", args, { detached: true, stdio: "ignore" }).unref();
77
79
  console.log("lattices app launched.");
78
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lattices/cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Agentic window manager for macOS — programmable workspace, smart layouts, managed tmux sessions, and a 35+-method agent API",
5
5
  "bin": {
6
6
  "lattices": "./bin/lattices.ts",
@@ -55,6 +55,7 @@
55
55
  "@ai-sdk/openai": "^3.0.41",
56
56
  "@ai-sdk/xai": "^3.0.67",
57
57
  "@arach/speakeasy": "^0.2.8",
58
- "ai": "^6.0.116"
58
+ "ai": "^6.0.116",
59
+ "zod": "^3.25.76"
59
60
  }
60
61
  }