@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 +2 -2
- package/app/Lattices.app/Contents/Info.plist +2 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Sources/ActionRow.swift +43 -26
- package/app/Sources/AppDelegate.swift +12 -7
- package/app/Sources/HandsOffSession.swift +38 -12
- package/app/Sources/MainView.swift +73 -21
- package/bin/lattices-app.ts +3 -1
- package/package.json +3 -2
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.
|
|
18
|
+
<string>0.4.2</string>
|
|
19
19
|
<key>CFBundleShortVersionString</key>
|
|
20
|
-
<string>0.4.
|
|
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.
|
|
18
|
+
<string>0.4.2</string>
|
|
19
19
|
<key>CFBundleShortVersionString</key>
|
|
20
|
-
<string>0.4.
|
|
20
|
+
<string>0.4.2</string>
|
|
21
21
|
<key>LSMinimumSystemVersion</key>
|
|
22
22
|
<string>13.0</string>
|
|
23
23
|
<key>LSUIElement</key>
|
|
Binary file
|
|
@@ -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
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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,
|
|
51
|
-
.padding(.vertical,
|
|
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
|
-
("
|
|
233
|
-
("
|
|
234
|
-
("
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
299
|
-
|
|
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(
|
|
302
|
-
|
|
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(
|
|
305
|
-
|
|
306
|
-
|
|
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
|
|
355
|
-
guard let binding = HotkeyStore.shared.bindings[action]
|
|
356
|
-
|
|
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
|
package/bin/lattices-app.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
}
|