@lattices/cli 0.3.0 → 0.4.0

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 (95) hide show
  1. package/README.md +85 -9
  2. package/app/Package.swift +8 -1
  3. package/app/Sources/AdvisorLearningStore.swift +90 -0
  4. package/app/Sources/AgentSession.swift +377 -0
  5. package/app/Sources/AppDelegate.swift +44 -12
  6. package/app/Sources/AppShellView.swift +81 -8
  7. package/app/Sources/AudioProvider.swift +386 -0
  8. package/app/Sources/CheatSheetHUD.swift +261 -19
  9. package/app/Sources/DaemonProtocol.swift +13 -0
  10. package/app/Sources/DaemonServer.swift +8 -0
  11. package/app/Sources/DesktopModel.swift +164 -5
  12. package/app/Sources/DesktopModelTypes.swift +2 -0
  13. package/app/Sources/DiagnosticLog.swift +104 -2
  14. package/app/Sources/EventBus.swift +1 -0
  15. package/app/Sources/HUDBottomBar.swift +279 -0
  16. package/app/Sources/HUDController.swift +1158 -0
  17. package/app/Sources/HUDLeftBar.swift +849 -0
  18. package/app/Sources/HUDMinimap.swift +179 -0
  19. package/app/Sources/HUDRightBar.swift +774 -0
  20. package/app/Sources/HUDState.swift +367 -0
  21. package/app/Sources/HUDTopBar.swift +243 -0
  22. package/app/Sources/HandsOffSession.swift +733 -0
  23. package/app/Sources/HomeDashboardView.swift +125 -0
  24. package/app/Sources/HotkeyManager.swift +2 -0
  25. package/app/Sources/HotkeyStore.swift +45 -9
  26. package/app/Sources/IntentEngine.swift +925 -0
  27. package/app/Sources/Intents/CreateLayerIntent.swift +54 -0
  28. package/app/Sources/Intents/DistributeIntent.swift +56 -0
  29. package/app/Sources/Intents/FocusIntent.swift +69 -0
  30. package/app/Sources/Intents/HelpIntent.swift +41 -0
  31. package/app/Sources/Intents/KillIntent.swift +47 -0
  32. package/app/Sources/Intents/LatticeIntent.swift +78 -0
  33. package/app/Sources/Intents/LaunchIntent.swift +67 -0
  34. package/app/Sources/Intents/ListSessionsIntent.swift +32 -0
  35. package/app/Sources/Intents/ListWindowsIntent.swift +30 -0
  36. package/app/Sources/Intents/ScanIntent.swift +52 -0
  37. package/app/Sources/Intents/SearchIntent.swift +190 -0
  38. package/app/Sources/Intents/SwitchLayerIntent.swift +50 -0
  39. package/app/Sources/Intents/TileIntent.swift +61 -0
  40. package/app/Sources/LatticesApi.swift +1235 -30
  41. package/app/Sources/LauncherHUD.swift +348 -0
  42. package/app/Sources/MainView.swift +147 -44
  43. package/app/Sources/OcrModel.swift +34 -1
  44. package/app/Sources/OmniSearchState.swift +99 -102
  45. package/app/Sources/OnboardingView.swift +457 -0
  46. package/app/Sources/PermissionChecker.swift +2 -12
  47. package/app/Sources/PiChatDock.swift +454 -0
  48. package/app/Sources/PiChatSession.swift +815 -0
  49. package/app/Sources/PiWorkspaceView.swift +364 -0
  50. package/app/Sources/PlacementSpec.swift +195 -0
  51. package/app/Sources/Preferences.swift +59 -0
  52. package/app/Sources/ProjectScanner.swift +1 -1
  53. package/app/Sources/ScreenMapState.swift +701 -55
  54. package/app/Sources/ScreenMapView.swift +843 -103
  55. package/app/Sources/ScreenMapWindowController.swift +22 -0
  56. package/app/Sources/SessionLayerStore.swift +285 -0
  57. package/app/Sources/SessionManager.swift +4 -1
  58. package/app/Sources/SettingsView.swift +186 -3
  59. package/app/Sources/Theme.swift +9 -8
  60. package/app/Sources/TmuxModel.swift +7 -0
  61. package/app/Sources/TmuxQuery.swift +27 -3
  62. package/app/Sources/VoiceChatView.swift +192 -0
  63. package/app/Sources/VoiceCommandWindow.swift +1594 -0
  64. package/app/Sources/VoiceIntentResolver.swift +671 -0
  65. package/app/Sources/VoxClient.swift +454 -0
  66. package/app/Sources/WindowTiler.swift +348 -87
  67. package/app/Sources/WorkspaceManager.swift +127 -18
  68. package/bin/client.ts +16 -0
  69. package/bin/{daemon-client.js → daemon-client.ts} +49 -30
  70. package/bin/handsoff-infer.ts +280 -0
  71. package/bin/handsoff-worker.ts +731 -0
  72. package/bin/{lattices-app.js → lattices-app.ts} +67 -32
  73. package/bin/lattices-dev +160 -0
  74. package/bin/{lattices.js → lattices.ts} +600 -137
  75. package/bin/project-twin.ts +645 -0
  76. package/docs/agent-execution-plan.md +562 -0
  77. package/docs/agents.md +142 -0
  78. package/docs/api.md +153 -34
  79. package/docs/app.md +29 -1
  80. package/docs/config.md +5 -1
  81. package/docs/handsoff-test-scenarios.md +84 -0
  82. package/docs/layers.md +20 -20
  83. package/docs/ocr.md +14 -5
  84. package/docs/overview.md +5 -1
  85. package/docs/presentation-execution-review.md +491 -0
  86. package/docs/prompts/hands-off-system.md +374 -0
  87. package/docs/prompts/hands-off-turn.md +30 -0
  88. package/docs/prompts/voice-advisor.md +31 -0
  89. package/docs/prompts/voice-fallback.md +23 -0
  90. package/docs/tiling-reference.md +167 -0
  91. package/docs/twins.md +138 -0
  92. package/docs/voice-command-protocol.md +278 -0
  93. package/docs/voice.md +219 -0
  94. package/package.json +21 -10
  95. package/bin/client.js +0 -4
@@ -13,14 +13,14 @@ final class PermissionChecker: ObservableObject {
13
13
 
14
14
  var allGranted: Bool { accessibility && screenRecording }
15
15
 
16
- /// Check current permission state, prompting on first launch if not granted.
16
+ /// Check current permission state without prompting.
17
17
  func check() {
18
18
  let diag = DiagnosticLog.shared
19
19
 
20
20
  let ax = AXIsProcessTrusted()
21
21
  let sr = CGPreflightScreenCaptureAccess()
22
22
 
23
- // First check: log identity info and prompt if needed
23
+ // First check: log identity info only
24
24
  if !hasLoggedInitial {
25
25
  hasLoggedInitial = true
26
26
  let bundleId = Bundle.main.bundleIdentifier ?? "<no bundle id>"
@@ -30,16 +30,6 @@ final class PermissionChecker: ObservableObject {
30
30
  diag.info("PermissionChecker: exec=\(execPath)")
31
31
  diag.info("AXIsProcessTrusted() → \(ax)")
32
32
  diag.info("CGPreflightScreenCaptureAccess() → \(sr)")
33
-
34
- // Prompt for missing permissions on first check
35
- if !ax {
36
- requestAccessibility()
37
- return
38
- }
39
- if !sr {
40
- requestScreenRecording()
41
- return
42
- }
43
33
  }
44
34
 
45
35
  // Log on state changes
@@ -0,0 +1,454 @@
1
+ import SwiftUI
2
+
3
+ struct PiChatDock: View {
4
+ @ObservedObject var session: PiChatSession
5
+ @FocusState private var composerFocused: Bool
6
+ @FocusState private var authFieldFocused: Bool
7
+ @State private var resizeStartHeight: CGFloat?
8
+
9
+ private static let timeFormatter: DateFormatter = {
10
+ let formatter = DateFormatter()
11
+ formatter.dateFormat = "HH:mm"
12
+ return formatter
13
+ }()
14
+
15
+ var body: some View {
16
+ VStack(spacing: 0) {
17
+ topHandle
18
+
19
+ if session.isAuthPanelVisible {
20
+ Rectangle()
21
+ .fill(Palette.border)
22
+ .frame(height: 0.5)
23
+
24
+ authPanel
25
+
26
+ Rectangle()
27
+ .fill(Palette.border)
28
+ .frame(height: 0.5)
29
+ }
30
+
31
+ transcript
32
+
33
+ Rectangle()
34
+ .fill(Palette.border)
35
+ .frame(height: 0.5)
36
+
37
+ composer
38
+
39
+ Rectangle()
40
+ .fill(Palette.border)
41
+ .frame(height: 0.5)
42
+
43
+ footerBar
44
+ }
45
+ .frame(maxWidth: .infinity)
46
+ .frame(height: session.dockHeight)
47
+ .background(
48
+ LinearGradient(
49
+ colors: [
50
+ Color.black.opacity(0.96),
51
+ Color(red: 0.02, green: 0.05, blue: 0.03),
52
+ ],
53
+ startPoint: .top,
54
+ endPoint: .bottom
55
+ )
56
+ )
57
+ .overlay(
58
+ RoundedRectangle(cornerRadius: 0)
59
+ .strokeBorder(Palette.border, lineWidth: 0.5)
60
+ )
61
+ .onAppear {
62
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
63
+ composerFocused = true
64
+ }
65
+ }
66
+ }
67
+
68
+ private var topHandle: some View {
69
+ HStack {
70
+ Spacer()
71
+
72
+ Capsule()
73
+ .fill(Palette.borderLit)
74
+ .frame(width: 64, height: 4)
75
+
76
+ Spacer()
77
+
78
+ Button {
79
+ session.isVisible = false
80
+ } label: {
81
+ Image(systemName: "xmark")
82
+ .font(.system(size: 9, weight: .bold))
83
+ .foregroundColor(Palette.textMuted)
84
+ .padding(6)
85
+ .background(
86
+ Circle()
87
+ .fill(Color.white.opacity(0.03))
88
+ .overlay(
89
+ Circle()
90
+ .strokeBorder(Palette.border, lineWidth: 0.5)
91
+ )
92
+ )
93
+ }
94
+ .buttonStyle(.plain)
95
+ }
96
+ .padding(.horizontal, 10)
97
+ .padding(.vertical, 8)
98
+ .contentShape(Rectangle())
99
+ .gesture(
100
+ DragGesture(minimumDistance: 1)
101
+ .onChanged { value in
102
+ if resizeStartHeight == nil {
103
+ resizeStartHeight = session.dockHeight
104
+ }
105
+ let start = resizeStartHeight ?? session.dockHeight
106
+ session.dockHeight = start - value.translation.height
107
+ }
108
+ .onEnded { _ in
109
+ resizeStartHeight = nil
110
+ }
111
+ )
112
+ }
113
+
114
+ private var authPanel: some View {
115
+ VStack(alignment: .leading, spacing: 10) {
116
+ HStack(spacing: 8) {
117
+ Text("provider")
118
+ .font(Typo.geistMonoBold(9))
119
+ .foregroundColor(Palette.textMuted)
120
+
121
+ Picker("Provider", selection: $session.authProviderID) {
122
+ ForEach(session.providerOptions) { provider in
123
+ Text(provider.name).tag(provider.id)
124
+ }
125
+ }
126
+ .labelsHidden()
127
+ .pickerStyle(.menu)
128
+ .font(Typo.mono(10))
129
+
130
+ Spacer()
131
+
132
+ capsuleLabel(
133
+ session.currentProvider.authMode == .oauth ? "OAUTH" : "TOKEN",
134
+ tint: session.currentProvider.authMode == .oauth ? Palette.detach : Palette.running
135
+ )
136
+ }
137
+
138
+ Text(session.currentProvider.helpText)
139
+ .font(Typo.mono(10))
140
+ .foregroundColor(Palette.textDim)
141
+ .fixedSize(horizontal: false, vertical: true)
142
+
143
+ if session.currentProvider.authMode == .apiKey {
144
+ HStack(spacing: 8) {
145
+ SecureField(session.currentProvider.tokenPlaceholder, text: $session.authToken)
146
+ .textFieldStyle(.plain)
147
+ .font(Typo.mono(11))
148
+ .foregroundColor(Palette.text)
149
+ .focused($authFieldFocused)
150
+ .onSubmit {
151
+ session.saveSelectedToken()
152
+ }
153
+
154
+ footerButton("save") {
155
+ session.saveSelectedToken()
156
+ }
157
+
158
+ if session.hasSelectedCredential {
159
+ footerButton("clear") {
160
+ session.removeSelectedCredential()
161
+ }
162
+ }
163
+ }
164
+ .padding(.horizontal, 10)
165
+ .padding(.vertical, 8)
166
+ .background(authCardBackground(tint: Palette.running))
167
+ } else {
168
+ HStack(spacing: 8) {
169
+ footerButton(session.isAuthenticating ? "cancel" : "login") {
170
+ if session.isAuthenticating {
171
+ session.cancelAuthFlow()
172
+ } else {
173
+ session.startSelectedAuthFlow()
174
+ }
175
+ }
176
+
177
+ if session.hasSelectedCredential {
178
+ footerButton("clear") {
179
+ session.removeSelectedCredential()
180
+ }
181
+ }
182
+ }
183
+ .padding(.horizontal, 10)
184
+ .padding(.vertical, 8)
185
+ .background(authCardBackground(tint: session.isAuthenticating ? Palette.detach : Palette.running))
186
+
187
+ if let prompt = session.pendingAuthPrompt {
188
+ HStack(spacing: 8) {
189
+ TextField(prompt.placeholder ?? prompt.message, text: $session.authPromptInput)
190
+ .textFieldStyle(.plain)
191
+ .font(Typo.mono(11))
192
+ .foregroundColor(Palette.text)
193
+ .focused($authFieldFocused)
194
+ .onSubmit {
195
+ session.submitAuthPrompt()
196
+ }
197
+
198
+ footerButton("continue") {
199
+ session.submitAuthPrompt()
200
+ }
201
+ }
202
+ .padding(.horizontal, 10)
203
+ .padding(.vertical, 8)
204
+ .background(authCardBackground(tint: Palette.detach))
205
+ }
206
+ }
207
+
208
+ if let notice = session.authNoticeText, !notice.isEmpty {
209
+ Text(notice)
210
+ .font(Typo.mono(9))
211
+ .foregroundColor(Palette.textDim)
212
+ .fixedSize(horizontal: false, vertical: true)
213
+ }
214
+
215
+ if let error = session.authErrorText, !error.isEmpty {
216
+ Text(error)
217
+ .font(Typo.mono(9))
218
+ .foregroundColor(Palette.kill)
219
+ .fixedSize(horizontal: false, vertical: true)
220
+ }
221
+ }
222
+ .padding(.horizontal, 10)
223
+ .padding(.vertical, 10)
224
+ .background(
225
+ LinearGradient(
226
+ colors: [
227
+ Palette.running.opacity(0.07),
228
+ Color.black.opacity(0.26),
229
+ ],
230
+ startPoint: .topLeading,
231
+ endPoint: .bottomTrailing
232
+ )
233
+ )
234
+ .onAppear {
235
+ focusAuthFieldIfNeeded()
236
+ }
237
+ .onChange(of: session.authProviderID) { _ in
238
+ focusAuthFieldIfNeeded()
239
+ }
240
+ .onChange(of: session.pendingAuthPrompt?.message) { prompt in
241
+ if prompt != nil {
242
+ focusAuthFieldIfNeeded()
243
+ }
244
+ }
245
+ }
246
+
247
+ private var transcript: some View {
248
+ ScrollViewReader { proxy in
249
+ ScrollView(.vertical, showsIndicators: true) {
250
+ LazyVStack(alignment: .leading, spacing: 8) {
251
+ ForEach(session.messages) { message in
252
+ row(message)
253
+ .id(message.id)
254
+ }
255
+ }
256
+ .padding(.horizontal, 10)
257
+ .padding(.vertical, 10)
258
+ }
259
+ .background(Color.black.opacity(0.35))
260
+ .onAppear {
261
+ if let last = session.messages.last?.id {
262
+ proxy.scrollTo(last, anchor: .bottom)
263
+ }
264
+ }
265
+ .onChange(of: session.messages.count) { _ in
266
+ if let last = session.messages.last?.id {
267
+ withAnimation(.easeOut(duration: 0.15)) {
268
+ proxy.scrollTo(last, anchor: .bottom)
269
+ }
270
+ }
271
+ }
272
+ }
273
+ }
274
+
275
+ private func row(_ message: PiChatMessage) -> some View {
276
+ HStack(alignment: .top, spacing: 10) {
277
+ VStack(alignment: .leading, spacing: 4) {
278
+ capsuleLabel(roleLabel(for: message.role).uppercased(), tint: roleColor(for: message.role))
279
+
280
+ Text(timestamp(for: message.timestamp))
281
+ .font(Typo.mono(8))
282
+ .foregroundColor(Palette.textMuted)
283
+ }
284
+ .frame(width: 52, alignment: .leading)
285
+
286
+ VStack(alignment: .leading, spacing: 6) {
287
+ Rectangle()
288
+ .fill(roleColor(for: message.role).opacity(0.9))
289
+ .frame(width: 14, height: 1.5)
290
+
291
+ Text(message.text)
292
+ .font(Typo.mono(11))
293
+ .foregroundColor(Palette.text)
294
+ .textSelection(.enabled)
295
+ .frame(maxWidth: .infinity, alignment: .leading)
296
+ }
297
+ }
298
+ .padding(.horizontal, 10)
299
+ .padding(.vertical, 9)
300
+ .background(
301
+ RoundedRectangle(cornerRadius: 6)
302
+ .fill(roleColor(for: message.role).opacity(message.role == .assistant ? 0.11 : 0.06))
303
+ .overlay(
304
+ RoundedRectangle(cornerRadius: 6)
305
+ .strokeBorder(roleColor(for: message.role).opacity(0.22), lineWidth: 0.5)
306
+ )
307
+ )
308
+ }
309
+
310
+ private var composer: some View {
311
+ HStack(spacing: 10) {
312
+ HStack(spacing: 8) {
313
+ Text(">")
314
+ .font(Typo.geistMonoBold(11))
315
+ .foregroundColor(Palette.running)
316
+
317
+ TextField("Ask Pi something lightweight...", text: $session.draft, axis: .vertical)
318
+ .textFieldStyle(.plain)
319
+ .font(Typo.mono(11))
320
+ .foregroundColor(Palette.text)
321
+ .lineLimit(1...4)
322
+ .focused($composerFocused)
323
+ .onSubmit {
324
+ session.sendDraft()
325
+ }
326
+ }
327
+ .padding(.horizontal, 10)
328
+ .padding(.vertical, 9)
329
+ .background(
330
+ RoundedRectangle(cornerRadius: 6)
331
+ .fill(Color.black.opacity(0.38))
332
+ .overlay(
333
+ RoundedRectangle(cornerRadius: 6)
334
+ .strokeBorder(Palette.running.opacity(0.16), lineWidth: 0.5)
335
+ )
336
+ )
337
+
338
+ footerButton("send", tint: Palette.running) {
339
+ session.sendDraft()
340
+ }
341
+ .disabled(session.isSending)
342
+ }
343
+ .padding(.horizontal, 10)
344
+ .padding(.vertical, 10)
345
+ .background(Color.black.opacity(0.62))
346
+ }
347
+
348
+ private var footerBar: some View {
349
+ HStack(spacing: 8) {
350
+ Circle()
351
+ .fill(session.hasPiBinary ? Palette.running : Palette.kill)
352
+ .frame(width: 6, height: 6)
353
+
354
+ Text("PI DOCK")
355
+ .font(Typo.geistMonoBold(9))
356
+ .foregroundColor(Palette.text)
357
+
358
+ Text(footerStatusText)
359
+ .font(Typo.mono(9))
360
+ .foregroundColor(Palette.textMuted)
361
+ .lineLimit(1)
362
+
363
+ Spacer()
364
+
365
+ footerButton(session.isAuthPanelVisible ? "auth -" : "auth +") {
366
+ session.toggleAuthPanel()
367
+ }
368
+
369
+ footerButton("reset") {
370
+ session.clearConversation()
371
+ }
372
+ }
373
+ .padding(.horizontal, 10)
374
+ .padding(.vertical, 7)
375
+ .background(Color.white.opacity(0.015))
376
+ }
377
+
378
+ private var footerStatusText: String {
379
+ if session.statusText == "idle" {
380
+ return session.currentProvider.name
381
+ }
382
+ return "\(session.currentProvider.name) · \(session.statusText)"
383
+ }
384
+
385
+ private func focusAuthFieldIfNeeded() {
386
+ if session.currentProvider.authMode == .apiKey || session.pendingAuthPrompt != nil {
387
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
388
+ authFieldFocused = true
389
+ }
390
+ }
391
+ }
392
+
393
+ private func roleLabel(for role: PiChatMessage.Role) -> String {
394
+ switch role {
395
+ case .system: return "system"
396
+ case .user: return "you"
397
+ case .assistant: return "pi"
398
+ }
399
+ }
400
+
401
+ private func roleColor(for role: PiChatMessage.Role) -> Color {
402
+ switch role {
403
+ case .system: return Palette.detach
404
+ case .user: return Palette.textDim
405
+ case .assistant: return Palette.running
406
+ }
407
+ }
408
+
409
+ private func timestamp(for date: Date) -> String {
410
+ Self.timeFormatter.string(from: date)
411
+ }
412
+
413
+ private func capsuleLabel(_ text: String, tint: Color) -> some View {
414
+ Text(text)
415
+ .font(Typo.geistMonoBold(9))
416
+ .foregroundColor(tint.opacity(0.95))
417
+ .padding(.horizontal, 7)
418
+ .padding(.vertical, 4)
419
+ .background(
420
+ Capsule()
421
+ .fill(tint.opacity(0.10))
422
+ .overlay(
423
+ Capsule()
424
+ .strokeBorder(tint.opacity(0.28), lineWidth: 0.5)
425
+ )
426
+ )
427
+ }
428
+
429
+ private func footerButton(_ label: String, tint: Color = Palette.textMuted, action: @escaping () -> Void) -> some View {
430
+ Button(label, action: action)
431
+ .buttonStyle(.plain)
432
+ .font(Typo.geistMonoBold(9))
433
+ .foregroundColor(tint)
434
+ .padding(.horizontal, 8)
435
+ .padding(.vertical, 5)
436
+ .background(
437
+ Capsule()
438
+ .fill(Color.white.opacity(0.03))
439
+ .overlay(
440
+ Capsule()
441
+ .strokeBorder(Palette.border, lineWidth: 0.5)
442
+ )
443
+ )
444
+ }
445
+
446
+ private func authCardBackground(tint: Color) -> some View {
447
+ RoundedRectangle(cornerRadius: 6)
448
+ .fill(tint.opacity(0.06))
449
+ .overlay(
450
+ RoundedRectangle(cornerRadius: 6)
451
+ .strokeBorder(tint.opacity(0.24), lineWidth: 0.5)
452
+ )
453
+ }
454
+ }