@lattices/cli 0.3.0 → 0.4.1

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 (111) hide show
  1. package/README.md +85 -9
  2. package/app/Info.plist +30 -0
  3. package/app/Lattices.app/Contents/Info.plist +8 -2
  4. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/app/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  6. package/app/Lattices.app/Contents/Resources/tap.wav +0 -0
  7. package/app/Lattices.app/Contents/_CodeSignature/CodeResources +139 -0
  8. package/app/Lattices.entitlements +15 -0
  9. package/app/Package.swift +8 -1
  10. package/app/Resources/tap.wav +0 -0
  11. package/app/Sources/AdvisorLearningStore.swift +90 -0
  12. package/app/Sources/AgentSession.swift +377 -0
  13. package/app/Sources/AppDelegate.swift +45 -12
  14. package/app/Sources/AppShellView.swift +81 -8
  15. package/app/Sources/AudioProvider.swift +386 -0
  16. package/app/Sources/CheatSheetHUD.swift +261 -19
  17. package/app/Sources/DaemonProtocol.swift +13 -0
  18. package/app/Sources/DaemonServer.swift +8 -0
  19. package/app/Sources/DesktopModel.swift +189 -6
  20. package/app/Sources/DesktopModelTypes.swift +2 -0
  21. package/app/Sources/DiagnosticLog.swift +104 -2
  22. package/app/Sources/EventBus.swift +1 -0
  23. package/app/Sources/HUDBottomBar.swift +279 -0
  24. package/app/Sources/HUDController.swift +1158 -0
  25. package/app/Sources/HUDLeftBar.swift +849 -0
  26. package/app/Sources/HUDMinimap.swift +179 -0
  27. package/app/Sources/HUDRightBar.swift +774 -0
  28. package/app/Sources/HUDState.swift +367 -0
  29. package/app/Sources/HUDTopBar.swift +243 -0
  30. package/app/Sources/HandsOffSession.swift +802 -0
  31. package/app/Sources/HomeDashboardView.swift +125 -0
  32. package/app/Sources/HotkeyManager.swift +2 -0
  33. package/app/Sources/HotkeyStore.swift +49 -9
  34. package/app/Sources/IntentEngine.swift +962 -0
  35. package/app/Sources/Intents/CreateLayerIntent.swift +54 -0
  36. package/app/Sources/Intents/DistributeIntent.swift +56 -0
  37. package/app/Sources/Intents/FocusIntent.swift +69 -0
  38. package/app/Sources/Intents/HelpIntent.swift +41 -0
  39. package/app/Sources/Intents/KillIntent.swift +47 -0
  40. package/app/Sources/Intents/LatticeIntent.swift +78 -0
  41. package/app/Sources/Intents/LaunchIntent.swift +67 -0
  42. package/app/Sources/Intents/ListSessionsIntent.swift +32 -0
  43. package/app/Sources/Intents/ListWindowsIntent.swift +30 -0
  44. package/app/Sources/Intents/ScanIntent.swift +52 -0
  45. package/app/Sources/Intents/SearchIntent.swift +190 -0
  46. package/app/Sources/Intents/SwitchLayerIntent.swift +50 -0
  47. package/app/Sources/Intents/TileIntent.swift +61 -0
  48. package/app/Sources/LatticesApi.swift +1275 -30
  49. package/app/Sources/LauncherHUD.swift +348 -0
  50. package/app/Sources/MainView.swift +147 -44
  51. package/app/Sources/MouseFinder.swift +222 -0
  52. package/app/Sources/OcrModel.swift +34 -1
  53. package/app/Sources/OmniSearchState.swift +99 -102
  54. package/app/Sources/OnboardingView.swift +457 -0
  55. package/app/Sources/PermissionChecker.swift +2 -12
  56. package/app/Sources/PiChatDock.swift +454 -0
  57. package/app/Sources/PiChatSession.swift +815 -0
  58. package/app/Sources/PiWorkspaceView.swift +364 -0
  59. package/app/Sources/PlacementSpec.swift +195 -0
  60. package/app/Sources/Preferences.swift +59 -0
  61. package/app/Sources/ProjectScanner.swift +58 -45
  62. package/app/Sources/ScreenMapState.swift +701 -55
  63. package/app/Sources/ScreenMapView.swift +843 -103
  64. package/app/Sources/ScreenMapWindowController.swift +22 -0
  65. package/app/Sources/SessionLayerStore.swift +285 -0
  66. package/app/Sources/SessionManager.swift +4 -1
  67. package/app/Sources/SettingsView.swift +186 -3
  68. package/app/Sources/Theme.swift +9 -8
  69. package/app/Sources/TmuxModel.swift +7 -0
  70. package/app/Sources/TmuxQuery.swift +27 -3
  71. package/app/Sources/VoiceChatView.swift +192 -0
  72. package/app/Sources/VoiceCommandWindow.swift +1594 -0
  73. package/app/Sources/VoiceIntentResolver.swift +671 -0
  74. package/app/Sources/VoxClient.swift +454 -0
  75. package/app/Sources/WindowTiler.swift +348 -87
  76. package/app/Sources/WorkspaceManager.swift +127 -18
  77. package/app/Tests/StageDragTests.swift +333 -0
  78. package/app/Tests/StageJoinTests.swift +313 -0
  79. package/app/Tests/StageManagerTests.swift +280 -0
  80. package/app/Tests/StageTileTests.swift +353 -0
  81. package/assets/AppIcon.icns +0 -0
  82. package/bin/client.ts +16 -0
  83. package/bin/{daemon-client.js → daemon-client.ts} +49 -30
  84. package/bin/handsoff-infer.ts +280 -0
  85. package/bin/handsoff-worker.ts +740 -0
  86. package/bin/lattices-app.ts +338 -0
  87. package/bin/lattices-dev +208 -0
  88. package/bin/{lattices.js → lattices.ts} +777 -140
  89. package/bin/project-twin.ts +645 -0
  90. package/docs/agent-execution-plan.md +562 -0
  91. package/docs/agent-layer-guide.md +207 -0
  92. package/docs/agents.md +142 -0
  93. package/docs/api.md +153 -34
  94. package/docs/app.md +29 -1
  95. package/docs/config.md +5 -1
  96. package/docs/handsoff-test-scenarios.md +84 -0
  97. package/docs/layers.md +20 -20
  98. package/docs/ocr.md +14 -5
  99. package/docs/overview.md +5 -1
  100. package/docs/presentation-execution-review.md +491 -0
  101. package/docs/prompts/hands-off-system.md +374 -0
  102. package/docs/prompts/hands-off-turn.md +30 -0
  103. package/docs/prompts/voice-advisor.md +31 -0
  104. package/docs/prompts/voice-fallback.md +23 -0
  105. package/docs/tiling-reference.md +167 -0
  106. package/docs/twins.md +138 -0
  107. package/docs/voice-command-protocol.md +278 -0
  108. package/docs/voice.md +219 -0
  109. package/package.json +29 -11
  110. package/bin/client.js +0 -4
  111. package/bin/lattices-app.js +0 -221
@@ -12,7 +12,7 @@ final class DiagnosticLog: ObservableObject {
12
12
  let message: String
13
13
  let level: Level
14
14
 
15
- enum Level { case info, success, warning, error }
15
+ enum Level: String { case info, success, warning, error }
16
16
 
17
17
  var icon: String {
18
18
  switch level {
@@ -27,14 +27,68 @@ final class DiagnosticLog: ObservableObject {
27
27
  @Published var entries: [Entry] = []
28
28
  private let maxEntries = 80
29
29
 
30
+ // Disk persistence
31
+ private let logFile: URL
32
+ private let fileHandle: FileHandle?
33
+ private let diskQueue = DispatchQueue(label: "com.lattices.log-writer")
34
+ private static let timeFmt: DateFormatter = {
35
+ let f = DateFormatter()
36
+ f.dateFormat = "HH:mm:ss.SSS"
37
+ return f
38
+ }()
39
+
40
+ private init() {
41
+ let dir = FileManager.default.homeDirectoryForCurrentUser
42
+ .appendingPathComponent(".lattices")
43
+ try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
44
+
45
+ logFile = dir.appendingPathComponent("lattices.log")
46
+
47
+ // Rotate if > 1MB
48
+ if let attrs = try? FileManager.default.attributesOfItem(atPath: logFile.path),
49
+ let size = attrs[.size] as? UInt64, size > 1_000_000 {
50
+ let prev = dir.appendingPathComponent("lattices.log.1")
51
+ try? FileManager.default.removeItem(at: prev)
52
+ try? FileManager.default.moveItem(at: logFile, to: prev)
53
+ }
54
+
55
+ // Create file if needed and open for appending
56
+ if !FileManager.default.fileExists(atPath: logFile.path) {
57
+ FileManager.default.createFile(atPath: logFile.path, contents: nil)
58
+ }
59
+ fileHandle = try? FileHandle(forWritingTo: logFile)
60
+ fileHandle?.seekToEndOfFile()
61
+
62
+ // Write session header
63
+ let header = "\n──── Lattices launched \(ISO8601DateFormatter().string(from: Date())) ────\n"
64
+ if let data = header.data(using: .utf8) {
65
+ fileHandle?.write(data)
66
+ }
67
+ }
68
+
69
+ deinit {
70
+ fileHandle?.closeFile()
71
+ }
72
+
30
73
  func log(_ message: String, level: Entry.Level = .info) {
31
74
  let entry = Entry(time: Date(), message: message, level: level)
75
+
76
+ // In-memory for UI
32
77
  DispatchQueue.main.async {
33
78
  self.entries.append(entry)
34
79
  if self.entries.count > self.maxEntries {
35
80
  self.entries.removeFirst(self.entries.count - self.maxEntries)
36
81
  }
37
82
  }
83
+
84
+ // Disk
85
+ diskQueue.async { [weak self] in
86
+ let ts = Self.timeFmt.string(from: entry.time)
87
+ let line = "\(ts) \(entry.icon) [\(level.rawValue)] \(message)\n"
88
+ if let data = line.data(using: .utf8) {
89
+ self?.fileHandle?.write(data)
90
+ }
91
+ }
38
92
  }
39
93
 
40
94
  func info(_ msg: String) { log(msg, level: .info) }
@@ -61,6 +115,54 @@ final class DiagnosticLog: ObservableObject {
61
115
  }
62
116
  }
63
117
 
118
+ // MARK: - Interaction Feedback
119
+
120
+ final class AppFeedback {
121
+ static let shared = AppFeedback()
122
+
123
+ private lazy var tapSound: NSSound? = {
124
+ guard let url = Bundle.main.url(forResource: "tap", withExtension: "wav") else { return nil }
125
+ return NSSound(contentsOf: url, byReference: true)
126
+ }()
127
+
128
+ private init() {}
129
+
130
+ @discardableResult
131
+ func beginTimed(_ label: String, state: HUDState? = nil, feedback: String? = nil, playSound: Bool = true) -> DiagnosticLog.TimedAction {
132
+ if playSound {
133
+ playTap()
134
+ }
135
+ if let feedback, let state {
136
+ state.showFeedback(feedback)
137
+ }
138
+ return DiagnosticLog.shared.startTimed(label)
139
+ }
140
+
141
+ func finish(_ action: DiagnosticLog.TimedAction, state: HUDState? = nil, feedback: String? = nil) {
142
+ if let feedback, let state {
143
+ state.showFeedback(feedback)
144
+ }
145
+ DiagnosticLog.shared.finish(action)
146
+ }
147
+
148
+ func acknowledge(_ label: String, state: HUDState? = nil, feedback: String? = nil, playSound: Bool = true) {
149
+ if playSound {
150
+ playTap()
151
+ }
152
+ if let feedback, let state {
153
+ state.showFeedback(feedback)
154
+ }
155
+ DiagnosticLog.shared.info(label)
156
+ }
157
+
158
+ private func playTap() {
159
+ DispatchQueue.main.async {
160
+ self.tapSound?.stop()
161
+ self.tapSound?.play()
162
+ }
163
+ }
164
+ }
165
+
64
166
  // MARK: - Diagnostic Window
65
167
 
66
168
  final class DiagnosticWindow {
@@ -145,7 +247,7 @@ final class DiagnosticWindow {
145
247
 
146
248
  // Show running sessions
147
249
  let task = Process()
148
- task.executableURL = URL(fileURLWithPath: "/opt/homebrew/bin/tmux")
250
+ task.executableURL = URL(fileURLWithPath: TmuxQuery.resolvedPath ?? "/opt/homebrew/bin/tmux")
149
251
  task.arguments = ["list-sessions", "-F", "#{session_name}"]
150
252
  let pipe = Pipe()
151
253
  task.standardOutput = pipe
@@ -6,6 +6,7 @@ enum ModelEvent {
6
6
  case layerSwitched(index: Int)
7
7
  case processesChanged(interesting: [Int])
8
8
  case ocrScanComplete(windowCount: Int, totalBlocks: Int)
9
+ case voiceCommand(text: String, confidence: Double)
9
10
  }
10
11
 
11
12
  final class EventBus {
@@ -0,0 +1,279 @@
1
+ import SwiftUI
2
+
3
+ // MARK: - HUDBottomBar (action playback tray)
4
+
5
+ struct HUDBottomBar: View {
6
+ @ObservedObject var state: HUDState
7
+ @ObservedObject private var handsOff = HandsOffSession.shared
8
+ var onDismiss: () -> Void
9
+
10
+ var body: some View {
11
+ HStack(spacing: 0) {
12
+ if state.tileMode {
13
+ tileModeView
14
+ } else if !handsOff.recentActions.isEmpty {
15
+ actionPlayback
16
+ } else if let feedback = state.feedbackMessage {
17
+ feedbackView(feedback)
18
+ } else if state.voiceActive {
19
+ voiceStatusView
20
+ } else {
21
+ shortcutsView
22
+ }
23
+ }
24
+ .frame(maxWidth: .infinity)
25
+ .frame(height: 48)
26
+ .background(Palette.bg)
27
+ }
28
+
29
+ // MARK: - Action playback (what just happened)
30
+
31
+ private var actionPlayback: some View {
32
+ HStack(spacing: 8) {
33
+ // Flash indicator
34
+ Image(systemName: "checkmark.circle.fill")
35
+ .font(.system(size: 12))
36
+ .foregroundColor(Palette.running)
37
+
38
+ // Action chips showing what was executed
39
+ ScrollView(.horizontal, showsIndicators: false) {
40
+ HStack(spacing: 6) {
41
+ ForEach(Array(handsOff.recentActions.enumerated()), id: \.offset) { _, action in
42
+ executedChip(action)
43
+ }
44
+ }
45
+ }
46
+
47
+ Spacer()
48
+
49
+ // Dismiss playback
50
+ Button {
51
+ handsOff.recentActions = []
52
+ } label: {
53
+ Image(systemName: "xmark")
54
+ .font(.system(size: 9, weight: .bold))
55
+ .foregroundColor(Palette.textMuted)
56
+ .frame(width: 20, height: 20)
57
+ }
58
+ .buttonStyle(.plain)
59
+ }
60
+ .padding(.horizontal, 16)
61
+ }
62
+
63
+ // MARK: - Executed action chip
64
+
65
+ private func executedChip(_ action: [String: Any]) -> some View {
66
+ let intent = action["intent"] as? String ?? "action"
67
+ let slots = action["slots"] as? [String: Any] ?? [:]
68
+ let summary = actionSummary(intent: intent, slots: slots)
69
+
70
+ return HStack(spacing: 5) {
71
+ Image(systemName: iconForIntent(intent))
72
+ .font(.system(size: 9))
73
+ .foregroundColor(Palette.running)
74
+ Text(summary)
75
+ .font(Typo.mono(10))
76
+ .foregroundColor(Palette.text)
77
+ .lineLimit(1)
78
+ Image(systemName: "checkmark")
79
+ .font(.system(size: 7, weight: .bold))
80
+ .foregroundColor(Palette.running)
81
+ }
82
+ .padding(.horizontal, 10)
83
+ .padding(.vertical, 5)
84
+ .background(
85
+ RoundedRectangle(cornerRadius: 5)
86
+ .fill(Palette.running.opacity(0.06))
87
+ .overlay(
88
+ RoundedRectangle(cornerRadius: 5)
89
+ .strokeBorder(Palette.running.opacity(0.2), lineWidth: 0.5)
90
+ )
91
+ )
92
+ }
93
+
94
+ // MARK: - Voice active status
95
+
96
+ private var voiceStatusView: some View {
97
+ HStack(spacing: 8) {
98
+ // Pulsing mic
99
+ Image(systemName: "waveform")
100
+ .font(.system(size: 11))
101
+ .foregroundColor(voiceColor)
102
+
103
+ Text(voiceLabel)
104
+ .font(Typo.monoBold(10))
105
+ .foregroundColor(voiceColor)
106
+
107
+ if let transcript = handsOff.lastTranscript {
108
+ Rectangle().fill(Palette.border).frame(width: 0.5, height: 20)
109
+ Text(transcript)
110
+ .font(Typo.mono(10))
111
+ .foregroundColor(Palette.textMuted)
112
+ .lineLimit(1)
113
+ }
114
+
115
+ Spacer()
116
+
117
+ if let response = handsOff.lastResponse {
118
+ Text(response)
119
+ .font(Typo.mono(9))
120
+ .foregroundColor(Palette.textDim)
121
+ .lineLimit(1)
122
+ .frame(maxWidth: 250, alignment: .trailing)
123
+ }
124
+ }
125
+ .padding(.horizontal, 16)
126
+ }
127
+
128
+ private var voiceColor: Color {
129
+ switch handsOff.state {
130
+ case .idle: return Palette.running
131
+ case .connecting: return Palette.detach
132
+ case .listening: return Palette.running
133
+ case .thinking: return Palette.detach
134
+ }
135
+ }
136
+
137
+ private var voiceLabel: String {
138
+ switch handsOff.state {
139
+ case .idle: return "ready"
140
+ case .connecting: return "connecting..."
141
+ case .listening: return "listening..."
142
+ case .thinking: return "thinking..."
143
+ }
144
+ }
145
+
146
+ // MARK: - Interaction feedback
147
+
148
+ private func feedbackView(_ message: String) -> some View {
149
+ HStack(spacing: 8) {
150
+ Image(systemName: "cursorarrow.click.2")
151
+ .font(.system(size: 11, weight: .medium))
152
+ .foregroundColor(Palette.running)
153
+ Text(message)
154
+ .font(Typo.monoBold(10))
155
+ .foregroundColor(Palette.text)
156
+ .lineLimit(1)
157
+ Spacer()
158
+ Text("working")
159
+ .font(Typo.mono(9))
160
+ .foregroundColor(Palette.textDim)
161
+ }
162
+ .padding(.horizontal, 16)
163
+ }
164
+
165
+ // MARK: - Tile mode
166
+
167
+ private var tileModeView: some View {
168
+ HStack(spacing: 8) {
169
+ // Mode indicator
170
+ HStack(spacing: 4) {
171
+ Image(systemName: "rectangle.split.2x2")
172
+ .font(.system(size: 11))
173
+ .foregroundColor(Palette.running)
174
+ Text("TILE")
175
+ .font(Typo.monoBold(10))
176
+ .foregroundColor(Palette.running)
177
+ }
178
+
179
+ Rectangle().fill(Palette.border).frame(width: 0.5, height: 20)
180
+
181
+ // Key hints
182
+ HStack(spacing: 6) {
183
+ tileKey("H", "←")
184
+ tileKey("J", "↓")
185
+ tileKey("K", "↑")
186
+ tileKey("L", "→")
187
+ tileKey("F", "max")
188
+ }
189
+
190
+ Rectangle().fill(Palette.border).frame(width: 0.5, height: 20)
191
+
192
+ HStack(spacing: 6) {
193
+ tileKey("Y", "◸")
194
+ tileKey("U", "◹")
195
+ tileKey("B", "◺")
196
+ tileKey("N", "◿")
197
+ }
198
+
199
+ Spacer()
200
+
201
+ Text("⎋ done")
202
+ .font(Typo.mono(9))
203
+ .foregroundColor(Palette.textMuted)
204
+ }
205
+ .padding(.horizontal, 16)
206
+ }
207
+
208
+ private func tileKey(_ key: String, _ hint: String) -> some View {
209
+ HStack(spacing: 2) {
210
+ Text(key)
211
+ .font(Typo.geistMonoBold(9))
212
+ .foregroundColor(Palette.text)
213
+ .frame(width: 16, height: 16)
214
+ .background(
215
+ RoundedRectangle(cornerRadius: 3)
216
+ .fill(Palette.surface)
217
+ .overlay(
218
+ RoundedRectangle(cornerRadius: 3)
219
+ .strokeBorder(Palette.border, lineWidth: 0.5)
220
+ )
221
+ )
222
+ Text(hint)
223
+ .font(Typo.mono(8))
224
+ .foregroundColor(Palette.textDim)
225
+ }
226
+ }
227
+
228
+ // MARK: - Shortcuts hint (default state)
229
+
230
+ private var shortcutsView: some View {
231
+ HStack(spacing: 8) {
232
+ Image(systemName: "keyboard")
233
+ .font(.system(size: 10))
234
+ .foregroundColor(Palette.textMuted.opacity(0.4))
235
+ Text("V voice / search 1-4 jump ⇥ tab ↵ go ⎋ close")
236
+ .font(Typo.mono(9))
237
+ .foregroundColor(Palette.textMuted.opacity(0.5))
238
+ Spacer()
239
+ }
240
+ .padding(.horizontal, 16)
241
+ }
242
+
243
+ // MARK: - Helpers
244
+
245
+ private func actionSummary(intent: String, slots: [String: Any]) -> String {
246
+ let target = slots["target"] as? String
247
+ ?? slots["app"] as? String
248
+ ?? slots["query"] as? String
249
+ ?? ""
250
+ let position = slots["position"] as? String ?? ""
251
+
252
+ switch intent {
253
+ case "tile_window":
254
+ let parts = [target, position].filter { !$0.isEmpty }
255
+ return "Tile \(parts.joined(separator: " "))"
256
+ case "focus", "focus_app":
257
+ return "Focus \(target)"
258
+ case "launch", "launch_project":
259
+ return "Launch \(target)"
260
+ case "close_window":
261
+ return "Close \(target)"
262
+ case "maximize":
263
+ return "Maximize \(target)"
264
+ default:
265
+ return target.isEmpty ? intent : "\(intent) \(target)"
266
+ }
267
+ }
268
+
269
+ private func iconForIntent(_ intent: String) -> String {
270
+ switch intent {
271
+ case "tile_window": return "rectangle.split.2x1"
272
+ case "focus", "focus_app": return "eye"
273
+ case "launch", "launch_project": return "play.fill"
274
+ case "close_window": return "xmark.circle"
275
+ case "maximize": return "arrow.up.left.and.arrow.down.right"
276
+ default: return "bolt"
277
+ }
278
+ }
279
+ }