@arach/lattices 0.2.0 → 0.6.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 (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -86
  3. package/apps/mac/Info.plist +43 -0
  4. package/apps/mac/Lattices.app/Contents/Info.plist +43 -0
  5. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  6. package/apps/mac/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  7. package/apps/mac/Lattices.app/Contents/Resources/docs/assistant-knowledge.md +130 -0
  8. package/apps/mac/Lattices.app/Contents/Resources/tap.wav +0 -0
  9. package/apps/mac/Lattices.app/Contents/_CodeSignature/CodeResources +150 -0
  10. package/apps/mac/Lattices.entitlements +21 -0
  11. package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
  12. package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
  13. package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
  14. package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
  15. package/apps/mac/Resources/tap.wav +0 -0
  16. package/assets/AppIcon.icns +0 -0
  17. package/bin/assistant-intelligence.ts +912 -0
  18. package/bin/cli/capture.ts +252 -0
  19. package/bin/cli/daemon.ts +22 -0
  20. package/bin/cli/helpers.ts +105 -0
  21. package/bin/cli/layer.ts +178 -0
  22. package/bin/cli/runs.ts +43 -0
  23. package/bin/cli/search.ts +141 -0
  24. package/bin/cli/session.ts +32 -0
  25. package/bin/client.ts +17 -0
  26. package/bin/cua.ts +26 -0
  27. package/bin/{daemon-client.js → daemon-client.ts} +49 -30
  28. package/bin/handsoff-infer.ts +96 -0
  29. package/bin/handsoff-worker.ts +531 -0
  30. package/bin/infer.ts +424 -0
  31. package/bin/keychain.ts +75 -0
  32. package/bin/lattices-app.ts +655 -0
  33. package/bin/lattices-build +125 -0
  34. package/bin/lattices-build-env.ts +77 -0
  35. package/bin/lattices-dev +362 -0
  36. package/bin/lattices.ts +3260 -0
  37. package/bin/project-twin.ts +645 -0
  38. package/docs/agent-execution-plan.md +562 -0
  39. package/docs/agent-layer-guide.md +207 -0
  40. package/docs/agents.md +233 -0
  41. package/docs/ai-chat-ux-review.md +416 -0
  42. package/docs/api.md +1041 -47
  43. package/docs/app.md +96 -13
  44. package/docs/assistant-knowledge.md +130 -0
  45. package/docs/companion-deck.md +209 -0
  46. package/docs/component-extraction-roadmap.md +392 -0
  47. package/docs/concepts.md +13 -12
  48. package/docs/config.md +83 -10
  49. package/docs/gesture-customization-proposal.md +520 -0
  50. package/docs/handsoff-test-scenarios.md +84 -0
  51. package/docs/hyperspace-grid-snappiness.md +210 -0
  52. package/docs/layers.md +176 -28
  53. package/docs/mouse-gestures.md +244 -0
  54. package/docs/ocr.md +21 -9
  55. package/docs/overview.md +42 -23
  56. package/docs/presentation-execution-review.md +491 -0
  57. package/docs/prompts/hands-off-system.md +382 -0
  58. package/docs/prompts/hands-off-turn.md +30 -0
  59. package/docs/prompts/voice-advisor.md +31 -0
  60. package/docs/prompts/voice-fallback.md +23 -0
  61. package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
  62. package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
  63. package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
  64. package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
  65. package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
  66. package/docs/proposals/LAT-006-followup-gaps.md +103 -0
  67. package/docs/proposals/LAT-006-runs-and-capture-in-lattices.md +566 -0
  68. package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
  69. package/docs/quickstart.md +8 -12
  70. package/docs/reference/dewey.config.ts +74 -0
  71. package/docs/reference/install-agent.md +79 -0
  72. package/docs/release.md +172 -0
  73. package/docs/repo-structure.md +100 -0
  74. package/docs/terminal-kit.md +87 -0
  75. package/docs/tiling-reference.md +224 -0
  76. package/docs/twins.md +138 -0
  77. package/docs/voice-command-protocol.md +278 -0
  78. package/docs/voice-error-model.md +73 -0
  79. package/docs/voice.md +221 -0
  80. package/package.json +69 -16
  81. package/packages/npm/sdk/cua.d.mts +1 -0
  82. package/packages/npm/sdk/cua.d.ts +188 -0
  83. package/packages/npm/sdk/cua.mjs +376 -0
  84. package/app/Lattices.app/Contents/Info.plist +0 -24
  85. package/app/Package.swift +0 -13
  86. package/app/Sources/ActionRow.swift +0 -61
  87. package/app/Sources/App.swift +0 -10
  88. package/app/Sources/AppDelegate.swift +0 -234
  89. package/app/Sources/AppShellView.swift +0 -62
  90. package/app/Sources/AppTypeClassifier.swift +0 -70
  91. package/app/Sources/AppWindowShell.swift +0 -63
  92. package/app/Sources/CheatSheetHUD.swift +0 -332
  93. package/app/Sources/CommandModeState.swift +0 -1362
  94. package/app/Sources/CommandModeView.swift +0 -1405
  95. package/app/Sources/CommandModeWindow.swift +0 -192
  96. package/app/Sources/CommandPaletteView.swift +0 -307
  97. package/app/Sources/CommandPaletteWindow.swift +0 -134
  98. package/app/Sources/DaemonProtocol.swift +0 -101
  99. package/app/Sources/DaemonServer.swift +0 -414
  100. package/app/Sources/DesktopModel.swift +0 -121
  101. package/app/Sources/DesktopModelTypes.swift +0 -71
  102. package/app/Sources/DiagnosticLog.swift +0 -271
  103. package/app/Sources/EventBus.swift +0 -30
  104. package/app/Sources/HotkeyManager.swift +0 -250
  105. package/app/Sources/HotkeyStore.swift +0 -338
  106. package/app/Sources/InventoryManager.swift +0 -35
  107. package/app/Sources/InventoryPath.swift +0 -43
  108. package/app/Sources/KeyRecorderView.swift +0 -210
  109. package/app/Sources/LatticesApi.swift +0 -1125
  110. package/app/Sources/MainView.swift +0 -467
  111. package/app/Sources/MainWindow.swift +0 -83
  112. package/app/Sources/OcrModel.swift +0 -309
  113. package/app/Sources/OcrStore.swift +0 -295
  114. package/app/Sources/OmniSearchState.swift +0 -283
  115. package/app/Sources/OmniSearchView.swift +0 -288
  116. package/app/Sources/OmniSearchWindow.swift +0 -105
  117. package/app/Sources/OrphanRow.swift +0 -129
  118. package/app/Sources/PaletteCommand.swift +0 -419
  119. package/app/Sources/PermissionChecker.swift +0 -125
  120. package/app/Sources/Preferences.swift +0 -92
  121. package/app/Sources/ProcessModel.swift +0 -199
  122. package/app/Sources/ProcessQuery.swift +0 -151
  123. package/app/Sources/Project.swift +0 -28
  124. package/app/Sources/ProjectRow.swift +0 -368
  125. package/app/Sources/ProjectScanner.swift +0 -121
  126. package/app/Sources/ScreenMapState.swift +0 -2387
  127. package/app/Sources/ScreenMapView.swift +0 -2820
  128. package/app/Sources/ScreenMapWindowController.swift +0 -89
  129. package/app/Sources/SessionManager.swift +0 -72
  130. package/app/Sources/SettingsView.swift +0 -1053
  131. package/app/Sources/SettingsWindow.swift +0 -20
  132. package/app/Sources/TabGroupRow.swift +0 -178
  133. package/app/Sources/Terminal.swift +0 -259
  134. package/app/Sources/TerminalQuery.swift +0 -156
  135. package/app/Sources/TerminalSynthesizer.swift +0 -200
  136. package/app/Sources/Theme.swift +0 -163
  137. package/app/Sources/TilePickerView.swift +0 -209
  138. package/app/Sources/TmuxModel.swift +0 -53
  139. package/app/Sources/TmuxQuery.swift +0 -81
  140. package/app/Sources/WindowTiler.swift +0 -1755
  141. package/app/Sources/WorkspaceManager.swift +0 -434
  142. package/bin/lattices-app.js +0 -221
  143. package/bin/lattices.js +0 -1418
@@ -1,1053 +0,0 @@
1
- import SwiftUI
2
-
3
- /// Settings content with internal General / Shortcuts tabs.
4
- /// Can also render the Docs page when `page == .docs`.
5
- struct SettingsContentView: View {
6
- var page: AppPage = .settings
7
- @ObservedObject var prefs: Preferences
8
- @ObservedObject var scanner: ProjectScanner
9
- @ObservedObject var hotkeyStore: HotkeyStore = .shared
10
- var onBack: (() -> Void)? = nil
11
-
12
- @State private var selectedTab = "shortcuts"
13
-
14
- var body: some View {
15
- VStack(spacing: 0) {
16
- // Back bar
17
- backBar
18
-
19
- if page == .docs {
20
- docsContent
21
- } else {
22
- settingsBody
23
- }
24
- }
25
- .frame(maxWidth: .infinity, maxHeight: .infinity)
26
- .clipped()
27
- .background(PanelBackground())
28
- }
29
-
30
- // MARK: - Back Bar
31
-
32
- private var currentTabLabel: String {
33
- switch selectedTab {
34
- case "general": return "General"
35
- case "search": return "Search & OCR"
36
- case "shortcuts": return "Shortcuts"
37
- default: return page.label
38
- }
39
- }
40
-
41
- private var backBar: some View {
42
- VStack(spacing: 0) {
43
- HStack(spacing: 8) {
44
- Button {
45
- onBack?()
46
- } label: {
47
- Image(systemName: "chevron.left")
48
- .font(.system(size: 10, weight: .semibold))
49
- .foregroundColor(Palette.textMuted)
50
- }
51
- .buttonStyle(.plain)
52
- .onHover { h in if h { NSCursor.pointingHand.push() } else { NSCursor.pop() } }
53
-
54
- Text(page == .docs ? "Docs" : currentTabLabel)
55
- .font(Typo.heading(13))
56
- .foregroundColor(Palette.text)
57
-
58
- Spacer()
59
- }
60
- .padding(.horizontal, 16)
61
- .padding(.vertical, 8)
62
-
63
- Rectangle().fill(Palette.border).frame(height: 0.5)
64
- }
65
- }
66
-
67
- // MARK: - Settings Body (General + Shortcuts tabs)
68
-
69
- private var settingsBody: some View {
70
- VStack(spacing: 0) {
71
- // Tab bar
72
- HStack(spacing: 2) {
73
- settingsTab(label: "General", id: "general")
74
- settingsTab(label: "Search & OCR", id: "search")
75
- settingsTab(label: "Shortcuts", id: "shortcuts")
76
- Spacer()
77
- }
78
- .padding(.horizontal, 14)
79
- .padding(.vertical, 6)
80
-
81
- Rectangle().fill(Palette.border).frame(height: 0.5)
82
-
83
- // Tab content
84
- switch selectedTab {
85
- case "shortcuts": shortcutsContent
86
- case "search": searchOcrContent
87
- default: generalContent
88
- }
89
- }
90
- }
91
-
92
- private func settingsTab(label: String, id: String) -> some View {
93
- let active = selectedTab == id
94
- return Button {
95
- selectedTab = id
96
- } label: {
97
- Text(label)
98
- .font(Typo.mono(11))
99
- .foregroundColor(active ? Palette.text : Palette.textMuted)
100
- .padding(.horizontal, 10)
101
- .padding(.vertical, 5)
102
- .background(
103
- ZStack {
104
- if active {
105
- RoundedRectangle(cornerRadius: 6)
106
- .fill(Color.white.opacity(0.06))
107
- RoundedRectangle(cornerRadius: 6)
108
- .strokeBorder(
109
- LinearGradient(
110
- colors: [Color.white.opacity(0.12), Color.white.opacity(0.04)],
111
- startPoint: .top,
112
- endPoint: .bottom
113
- ),
114
- lineWidth: 0.5
115
- )
116
- }
117
- }
118
- )
119
- }
120
- .buttonStyle(.plain)
121
- }
122
-
123
- // MARK: - Sticky section header
124
-
125
- private func stickyHeader(_ title: String) -> some View {
126
- VStack(spacing: 0) {
127
- HStack {
128
- Text(title.uppercased())
129
- .font(Typo.pixel(14))
130
- .foregroundColor(Palette.textDim)
131
- .tracking(1)
132
- Spacer()
133
- }
134
- .padding(.horizontal, 20)
135
- .padding(.vertical, 8)
136
- .background(Palette.bg)
137
-
138
- Rectangle()
139
- .fill(Palette.border)
140
- .frame(height: 0.5)
141
- }
142
- }
143
-
144
- // MARK: - General
145
-
146
- private var generalContent: some View {
147
- ScrollView {
148
- VStack(alignment: .leading, spacing: 12) {
149
- // ── Terminal ──
150
- settingsCard {
151
- VStack(alignment: .leading, spacing: 8) {
152
- Text("Terminal")
153
- .font(Typo.mono(11))
154
- .foregroundColor(Palette.text)
155
-
156
- Picker("", selection: $prefs.terminal) {
157
- ForEach(Terminal.installed) { t in
158
- Text(t.rawValue).tag(t)
159
- }
160
- }
161
- .pickerStyle(.segmented)
162
- .labelsHidden()
163
-
164
- Text("Used for attaching to tmux sessions")
165
- .font(Typo.caption(10))
166
- .foregroundColor(Palette.textMuted)
167
- }
168
- }
169
-
170
- // ── tmux ──
171
- settingsCard {
172
- VStack(alignment: .leading, spacing: 10) {
173
- Text("tmux")
174
- .font(Typo.mono(11))
175
- .foregroundColor(Palette.text)
176
-
177
- // Mode
178
- HStack {
179
- Text("Detach mode")
180
- .font(Typo.mono(10))
181
- .foregroundColor(Palette.textDim)
182
- Spacer()
183
- Picker("", selection: $prefs.mode) {
184
- Text("Learning").tag(InteractionMode.learning)
185
- Text("Auto").tag(InteractionMode.auto)
186
- }
187
- .pickerStyle(.segmented)
188
- .labelsHidden()
189
- .frame(width: 160)
190
- }
191
-
192
- Text(prefs.mode == .learning
193
- ? "Shows keybinding hints on detach"
194
- : "Detaches sessions silently")
195
- .font(Typo.caption(9))
196
- .foregroundColor(Palette.textMuted.opacity(0.7))
197
-
198
- cardDivider
199
-
200
- // Project scan root
201
- Text("Project scan root")
202
- .font(Typo.mono(10))
203
- .foregroundColor(Palette.textDim)
204
-
205
- HStack(spacing: 6) {
206
- TextField("~/dev", text: $prefs.scanRoot)
207
- .textFieldStyle(.plain)
208
- .font(Typo.mono(11))
209
- .foregroundColor(Palette.text)
210
- .padding(.horizontal, 8)
211
- .padding(.vertical, 5)
212
- .background(
213
- RoundedRectangle(cornerRadius: 5)
214
- .fill(Color.white.opacity(0.06))
215
- .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5))
216
- )
217
-
218
- Button {
219
- let panel = NSOpenPanel()
220
- panel.canChooseDirectories = true
221
- panel.canChooseFiles = false
222
- panel.allowsMultipleSelection = false
223
- if !prefs.scanRoot.isEmpty {
224
- panel.directoryURL = URL(fileURLWithPath: prefs.scanRoot)
225
- }
226
- if panel.runModal() == .OK, let url = panel.url {
227
- prefs.scanRoot = url.path
228
- }
229
- } label: {
230
- Image(systemName: "folder")
231
- .font(.system(size: 11))
232
- .foregroundColor(Palette.textDim)
233
- .padding(6)
234
- .background(
235
- RoundedRectangle(cornerRadius: 5)
236
- .fill(Color.white.opacity(0.06))
237
- .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5))
238
- )
239
- }
240
- .buttonStyle(.plain)
241
- }
242
-
243
- HStack {
244
- Text("Scans for .lattices.json project configs")
245
- .font(Typo.caption(9))
246
- .foregroundColor(Palette.textMuted.opacity(0.7))
247
- Spacer()
248
- Button {
249
- scanner.updateRoot(prefs.scanRoot)
250
- scanner.scan()
251
- } label: {
252
- Text("Rescan")
253
- .font(Typo.monoBold(10))
254
- .foregroundColor(Palette.text)
255
- .padding(.horizontal, 12)
256
- .padding(.vertical, 4)
257
- .background(
258
- RoundedRectangle(cornerRadius: 4)
259
- .fill(Palette.surfaceHov)
260
- .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
261
- )
262
- }
263
- .buttonStyle(.plain)
264
- }
265
- }
266
- }
267
- }
268
- .padding(16)
269
- }
270
- }
271
-
272
- // MARK: - Search & OCR
273
-
274
- private func ocrNumField(_ value: Binding<Double>, width: CGFloat = 50) -> some View {
275
- TextField("", value: value, formatter: NumberFormatter())
276
- .textFieldStyle(.plain)
277
- .font(Typo.monoBold(11))
278
- .foregroundColor(Palette.text)
279
- .multilineTextAlignment(.center)
280
- .frame(width: width)
281
- .padding(.horizontal, 4)
282
- .padding(.vertical, 3)
283
- .background(
284
- RoundedRectangle(cornerRadius: 5)
285
- .fill(Color.white.opacity(0.06))
286
- .overlay(
287
- RoundedRectangle(cornerRadius: 5)
288
- .strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5)
289
- )
290
- )
291
- }
292
-
293
- private func ocrIntField(_ value: Binding<Int>, width: CGFloat = 36) -> some View {
294
- TextField("", value: value, formatter: NumberFormatter())
295
- .textFieldStyle(.plain)
296
- .font(Typo.monoBold(11))
297
- .foregroundColor(Palette.text)
298
- .multilineTextAlignment(.center)
299
- .frame(width: width)
300
- .padding(.horizontal, 4)
301
- .padding(.vertical, 3)
302
- .background(
303
- RoundedRectangle(cornerRadius: 5)
304
- .fill(Color.white.opacity(0.06))
305
- .overlay(
306
- RoundedRectangle(cornerRadius: 5)
307
- .strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5)
308
- )
309
- )
310
- }
311
-
312
- private func ocrSectionLabel(_ text: String) -> some View {
313
- Text(text)
314
- .font(Typo.monoBold(10))
315
- .foregroundColor(Palette.textDim)
316
- .tracking(0.5)
317
- }
318
-
319
- private var searchOcrContent: some View {
320
- ScrollView {
321
- VStack(spacing: 12) {
322
- // ── Screen Text Recognition Card ──
323
- settingsCard {
324
- VStack(alignment: .leading, spacing: 10) {
325
- // Header row: label + toggle
326
- HStack {
327
- HStack(spacing: 8) {
328
- RoundedRectangle(cornerRadius: 4)
329
- .fill(prefs.ocrEnabled ? Palette.running.opacity(0.15) : Palette.surface)
330
- .overlay(
331
- Image(systemName: "text.viewfinder")
332
- .font(.system(size: 11, weight: .medium))
333
- .foregroundColor(prefs.ocrEnabled ? Palette.running : Palette.textMuted)
334
- )
335
- .frame(width: 24, height: 24)
336
-
337
- VStack(alignment: .leading, spacing: 1) {
338
- Text("Screen text recognition")
339
- .font(Typo.mono(12))
340
- .foregroundColor(Palette.text)
341
- Text("Vision OCR on visible windows")
342
- .font(Typo.caption(10))
343
- .foregroundColor(Palette.textMuted)
344
- }
345
- }
346
- Spacer()
347
- Toggle("", isOn: Binding(
348
- get: { prefs.ocrEnabled },
349
- set: { OcrModel.shared.setEnabled($0) }
350
- ))
351
- .toggleStyle(.switch)
352
- .controlSize(.small)
353
- .labelsHidden()
354
- }
355
-
356
- // Accuracy
357
- HStack(spacing: 8) {
358
- Text("Accuracy")
359
- .font(Typo.mono(10))
360
- .foregroundColor(Palette.textDim)
361
- Picker("", selection: $prefs.ocrAccuracy) {
362
- Text("Accurate").tag("accurate")
363
- Text("Fast").tag("fast")
364
- }
365
- .pickerStyle(.segmented)
366
- .labelsHidden()
367
- .frame(width: 140)
368
- Spacer()
369
- }
370
- .padding(.leading, 32)
371
- }
372
- }
373
-
374
- // ── Scan Schedule Card ──
375
- settingsCard {
376
- VStack(alignment: .leading, spacing: 10) {
377
- ocrSectionLabel("Schedule")
378
-
379
- // Quick scan sentence
380
- HStack(spacing: 0) {
381
- Text("Quick scan top ")
382
- .font(Typo.mono(11))
383
- .foregroundColor(Palette.textDim)
384
- ocrIntField($prefs.ocrQuickLimit, width: 32)
385
- Text(" windows every ")
386
- .font(Typo.mono(11))
387
- .foregroundColor(Palette.textDim)
388
- ocrNumField($prefs.ocrQuickInterval, width: 42)
389
- Text("s")
390
- .font(Typo.mono(11))
391
- .foregroundColor(Palette.textDim)
392
- Spacer()
393
- }
394
-
395
- cardDivider
396
-
397
- // Deep scan sentence
398
- HStack(spacing: 0) {
399
- Text("Deep scan up to ")
400
- .font(Typo.mono(11))
401
- .foregroundColor(Palette.textDim)
402
- ocrIntField($prefs.ocrDeepLimit, width: 32)
403
- Text(" windows every ")
404
- .font(Typo.mono(11))
405
- .foregroundColor(Palette.textDim)
406
- ocrNumField($prefs.ocrDeepInterval, width: 52)
407
- Text("s")
408
- .font(Typo.mono(11))
409
- .foregroundColor(Palette.textDim)
410
- Spacer()
411
- }
412
-
413
- // Human-readable deep interval
414
- let h = Int(prefs.ocrDeepInterval / 3600)
415
- let m = Int(prefs.ocrDeepInterval.truncatingRemainder(dividingBy: 3600) / 60)
416
- if h > 0 || m > 0 {
417
- Text("≈ \(h > 0 ? "\(h)h" : "")\(m > 0 ? " \(m)m" : "")")
418
- .font(Typo.caption(9))
419
- .foregroundColor(Palette.textMuted.opacity(0.6))
420
- .padding(.leading, 2)
421
- }
422
- }
423
- }
424
-
425
- // ── Status Card ──
426
- settingsCard {
427
- HStack(spacing: 8) {
428
- let ocrResults = OcrModel.shared.results
429
- let isScanning = OcrModel.shared.isScanning
430
-
431
- Circle()
432
- .fill(isScanning ? Palette.detach : (prefs.ocrEnabled ? Palette.running : Palette.textMuted))
433
- .frame(width: 6, height: 6)
434
-
435
- Text(isScanning ? "Scanning..." : (prefs.ocrEnabled ? "\(ocrResults.count) windows cached" : "Disabled"))
436
- .font(Typo.mono(10))
437
- .foregroundColor(Palette.textMuted)
438
-
439
- Spacer()
440
-
441
- Button {
442
- OcrModel.shared.scan()
443
- } label: {
444
- HStack(spacing: 4) {
445
- Image(systemName: "arrow.clockwise")
446
- .font(.system(size: 9, weight: .semibold))
447
- Text("Scan Now")
448
- .font(Typo.monoBold(10))
449
- }
450
- .foregroundColor(prefs.ocrEnabled ? Palette.text : Palette.textMuted)
451
- .padding(.horizontal, 10)
452
- .padding(.vertical, 4)
453
- .background(
454
- RoundedRectangle(cornerRadius: 4)
455
- .fill(prefs.ocrEnabled ? Palette.surfaceHov : Palette.surface)
456
- .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
457
- )
458
- }
459
- .buttonStyle(.plain)
460
- .disabled(!prefs.ocrEnabled)
461
- }
462
- }
463
-
464
- // ── Recent Captures ──
465
- recentCapturesSection
466
- }
467
- .padding(16)
468
- }
469
- }
470
-
471
- // MARK: - Recent Captures Browser
472
-
473
- private var recentCapturesSection: some View {
474
- let ocrResults = OcrModel.shared.results
475
- let grouped = Dictionary(grouping: ocrResults.values, by: \.app)
476
- .sorted { $0.value.count > $1.value.count }
477
-
478
- return Group {
479
- if !grouped.isEmpty {
480
- settingsCard {
481
- VStack(alignment: .leading, spacing: 8) {
482
- ocrSectionLabel("Recent Captures")
483
-
484
- ForEach(grouped, id: \.key) { app, windows in
485
- ocrAppGroup(app: app, windows: windows.sorted { $0.timestamp > $1.timestamp })
486
- }
487
- }
488
- }
489
- }
490
- }
491
- }
492
-
493
- private func ocrAppGroup(app: String, windows: [OcrWindowResult]) -> some View {
494
- let isCollapsed = collapsedOcrApps.contains(app)
495
-
496
- return VStack(alignment: .leading, spacing: 0) {
497
- // App header
498
- Button {
499
- withAnimation(.easeInOut(duration: 0.15)) {
500
- if isCollapsed {
501
- collapsedOcrApps.remove(app)
502
- } else {
503
- collapsedOcrApps.insert(app)
504
- }
505
- }
506
- } label: {
507
- HStack(spacing: 6) {
508
- Image(systemName: isCollapsed ? "chevron.right" : "chevron.down")
509
- .font(.system(size: 8, weight: .semibold))
510
- .foregroundColor(Palette.textMuted)
511
- .frame(width: 10)
512
-
513
- Text(app)
514
- .font(Typo.monoBold(11))
515
- .foregroundColor(Palette.text)
516
-
517
- Text("(\(windows.count))")
518
- .font(Typo.mono(10))
519
- .foregroundColor(Palette.textMuted)
520
-
521
- Spacer()
522
- }
523
- .padding(.vertical, 4)
524
- .contentShape(Rectangle())
525
- }
526
- .buttonStyle(.plain)
527
-
528
- if !isCollapsed {
529
- VStack(alignment: .leading, spacing: 2) {
530
- ForEach(windows, id: \.wid) { win in
531
- ocrWindowRow(win)
532
- }
533
- }
534
- .padding(.leading, 16)
535
- }
536
- }
537
- }
538
-
539
- private func ocrWindowRow(_ win: OcrWindowResult) -> some View {
540
- let isExpanded = expandedOcrWindow == win.wid
541
- let preview = String(win.fullText.prefix(80)).replacingOccurrences(of: "\n", with: " ")
542
-
543
- return VStack(alignment: .leading, spacing: 0) {
544
- Button {
545
- withAnimation(.easeInOut(duration: 0.15)) {
546
- expandedOcrWindow = isExpanded ? nil : win.wid
547
- }
548
- } label: {
549
- HStack(spacing: 6) {
550
- Image(systemName: isExpanded ? "chevron.down" : "chevron.right")
551
- .font(.system(size: 7, weight: .semibold))
552
- .foregroundColor(Palette.textMuted)
553
- .frame(width: 8)
554
-
555
- VStack(alignment: .leading, spacing: 2) {
556
- HStack(spacing: 6) {
557
- Text(win.title.isEmpty ? "Untitled" : win.title)
558
- .font(Typo.mono(10))
559
- .foregroundColor(Palette.text)
560
- .lineLimit(1)
561
-
562
- Spacer()
563
-
564
- Text(ocrRelativeTime(win.timestamp))
565
- .font(Typo.caption(9))
566
- .foregroundColor(Palette.textMuted)
567
- }
568
-
569
- if !isExpanded && !preview.isEmpty {
570
- Text(preview)
571
- .font(Typo.caption(9))
572
- .foregroundColor(Palette.textMuted.opacity(0.7))
573
- .lineLimit(1)
574
- }
575
- }
576
- }
577
- .padding(.vertical, 4)
578
- .contentShape(Rectangle())
579
- }
580
- .buttonStyle(.plain)
581
-
582
- if isExpanded {
583
- ocrExpandedDetail(win)
584
- .padding(.leading, 14)
585
- .padding(.vertical, 4)
586
- }
587
- }
588
- }
589
-
590
- private func ocrExpandedDetail(_ win: OcrWindowResult) -> some View {
591
- VStack(alignment: .leading, spacing: 6) {
592
- // Metadata row
593
- HStack(spacing: 10) {
594
- let avgConfidence = win.texts.isEmpty ? 0 : win.texts.map(\.confidence).reduce(0, +) / Float(win.texts.count)
595
- Text("\(win.texts.count) blocks")
596
- .font(Typo.caption(9))
597
- .foregroundColor(Palette.textMuted)
598
- Text("confidence: \(String(format: "%.0f%%", avgConfidence * 100))")
599
- .font(Typo.caption(9))
600
- .foregroundColor(Palette.textMuted)
601
- Spacer()
602
- }
603
-
604
- // Full text in scrollable monospaced area
605
- ScrollView {
606
- Text(win.fullText)
607
- .font(.system(size: 10, design: .monospaced))
608
- .foregroundColor(Palette.textDim)
609
- .textSelection(.enabled)
610
- .frame(maxWidth: .infinity, alignment: .leading)
611
- .padding(8)
612
- }
613
- .frame(maxHeight: 150)
614
- .background(
615
- RoundedRectangle(cornerRadius: 4)
616
- .fill(Color.black.opacity(0.2))
617
- .overlay(
618
- RoundedRectangle(cornerRadius: 4)
619
- .strokeBorder(Color.white.opacity(0.06), lineWidth: 0.5)
620
- )
621
- )
622
- }
623
- }
624
-
625
- private func ocrRelativeTime(_ date: Date) -> String {
626
- let seconds = Int(-date.timeIntervalSinceNow)
627
- if seconds < 60 { return "just now" }
628
- let minutes = seconds / 60
629
- if minutes < 60 { return "\(minutes)m ago" }
630
- let hours = minutes / 60
631
- if hours < 24 { return "\(hours)h ago" }
632
- return "\(hours / 24)d ago"
633
- }
634
-
635
- // MARK: - Settings Card
636
-
637
- private func settingsCard<Content: View>(@ViewBuilder content: () -> Content) -> some View {
638
- content()
639
- .padding(.horizontal, 14)
640
- .padding(.vertical, 12)
641
- .frame(maxWidth: .infinity, alignment: .leading)
642
- .liquidGlass()
643
- }
644
-
645
- private var cardDivider: some View {
646
- Rectangle()
647
- .fill(
648
- LinearGradient(
649
- colors: [Color.white.opacity(0.03), Color.white.opacity(0.08), Color.white.opacity(0.03)],
650
- startPoint: .leading,
651
- endPoint: .trailing
652
- )
653
- )
654
- .frame(height: 0.5)
655
- .padding(.vertical, 3)
656
- }
657
-
658
- // MARK: - Shortcuts (Spatial Layout)
659
-
660
- private var shortcutsContent: some View {
661
- VStack(spacing: 0) {
662
- GeometryReader { geo in
663
- let spacing: CGFloat = 16
664
- let pad: CGFloat = 20
665
- let total = geo.size.width - pad * 2 - spacing * 2
666
- let leftW = total * 0.35
667
- let centerW = total * 0.35
668
- let rightW = total * 0.30
669
-
670
- ScrollView {
671
- VStack(alignment: .leading, spacing: 0) {
672
- HStack(alignment: .top, spacing: spacing) {
673
- shortcutsLeftColumn
674
- .frame(width: leftW, alignment: .leading)
675
- .clipped()
676
- shortcutsCenterColumn
677
- .frame(width: centerW, alignment: .leading)
678
- .clipped()
679
- shortcutsRightColumn
680
- .frame(width: rightW, alignment: .leading)
681
- .clipped()
682
- }
683
- .padding(.horizontal, pad)
684
- .padding(.vertical, 16)
685
- }
686
- }
687
- }
688
-
689
- Spacer(minLength: 0)
690
-
691
- separator
692
-
693
- HStack {
694
- Spacer()
695
- Button {
696
- hotkeyStore.resetAll()
697
- } label: {
698
- Text("Reset All to Defaults")
699
- .font(Typo.caption(11))
700
- .foregroundColor(Palette.textDim)
701
- .padding(.horizontal, 12)
702
- .padding(.vertical, 5)
703
- .background(
704
- RoundedRectangle(cornerRadius: 3)
705
- .fill(Palette.surface)
706
- .overlay(
707
- RoundedRectangle(cornerRadius: 3)
708
- .strokeBorder(Palette.border, lineWidth: 0.5)
709
- )
710
- )
711
- }
712
- .buttonStyle(.plain)
713
- }
714
- .padding(.horizontal, 20)
715
- .padding(.vertical, 10)
716
- }
717
- }
718
-
719
- // MARK: - Shortcuts: Left Column (App + Layers)
720
-
721
- private var shortcutsLeftColumn: some View {
722
- VStack(alignment: .leading, spacing: 12) {
723
- columnHeader("App & Layers")
724
-
725
- VStack(alignment: .leading, spacing: 2) {
726
- ForEach(HotkeyAction.allCases.filter { $0.group == .app }, id: \.rawValue) { action in
727
- compactKeyRecorder(action: action)
728
- }
729
- }
730
-
731
- Rectangle().fill(Palette.border).frame(height: 0.5)
732
-
733
- VStack(alignment: .leading, spacing: 2) {
734
- ForEach(HotkeyAction.layerActions, id: \.rawValue) { action in
735
- compactKeyRecorder(action: action)
736
- }
737
- }
738
- }
739
- }
740
-
741
- // MARK: - Shortcuts: Center Column (Tiling)
742
-
743
- private var shortcutsCenterColumn: some View {
744
- VStack(alignment: .leading, spacing: 12) {
745
- columnHeader("Tiling")
746
-
747
- // Monitor visualization — 3x3 grid
748
- VStack(spacing: 2) {
749
- HStack(spacing: 2) {
750
- tileCell(action: .tileTopLeft, label: "TL")
751
- tileCell(action: .tileTop, label: "Top")
752
- tileCell(action: .tileTopRight, label: "TR")
753
- }
754
- HStack(spacing: 2) {
755
- tileCell(action: .tileLeft, label: "Left")
756
- tileCell(action: .tileMaximize, label: "Max")
757
- tileCell(action: .tileRight, label: "Right")
758
- }
759
- HStack(spacing: 2) {
760
- tileCell(action: .tileBottomLeft, label: "BL")
761
- tileCell(action: .tileBottom, label: "Bottom")
762
- tileCell(action: .tileBottomRight, label: "BR")
763
- }
764
- }
765
- .padding(6)
766
- .background(
767
- RoundedRectangle(cornerRadius: 6)
768
- .fill(Color.black.opacity(0.25))
769
- .overlay(
770
- RoundedRectangle(cornerRadius: 6)
771
- .strokeBorder(Palette.border, lineWidth: 0.5)
772
- )
773
- )
774
-
775
- // Thirds row
776
- HStack(spacing: 2) {
777
- tileCell(action: .tileLeftThird, label: "\u{2153}L")
778
- tileCell(action: .tileCenterThird, label: "\u{2153}C")
779
- tileCell(action: .tileRightThird, label: "\u{2153}R")
780
- }
781
- .padding(6)
782
- .background(
783
- RoundedRectangle(cornerRadius: 6)
784
- .fill(Color.black.opacity(0.25))
785
- .overlay(
786
- RoundedRectangle(cornerRadius: 6)
787
- .strokeBorder(Palette.border, lineWidth: 0.5)
788
- )
789
- )
790
-
791
- // Center + Distribute
792
- HStack(spacing: 4) {
793
- compactKeyRecorder(action: .tileCenter)
794
- compactKeyRecorder(action: .tileDistribute)
795
- }
796
- }
797
- }
798
-
799
- // MARK: - Shortcuts: Right Column (tmux)
800
-
801
- private var shortcutsRightColumn: some View {
802
- VStack(alignment: .leading, spacing: 12) {
803
- columnHeader("Inside tmux")
804
-
805
- VStack(alignment: .leading, spacing: 6) {
806
- shortcutRow("Detach", keys: ["Ctrl+B", "D"])
807
- shortcutRow("Kill pane", keys: ["Ctrl+B", "X"])
808
- shortcutRow("Pane left", keys: ["Ctrl+B", "\u{2190}"])
809
- shortcutRow("Pane right", keys: ["Ctrl+B", "\u{2192}"])
810
- shortcutRow("Zoom toggle", keys: ["Ctrl+B", "Z"])
811
- shortcutRow("Scroll mode", keys: ["Ctrl+B", "["])
812
- }
813
- }
814
- }
815
-
816
- // MARK: - Column header
817
-
818
- private func columnHeader(_ title: String) -> some View {
819
- Text(title.uppercased())
820
- .font(Typo.pixel(12))
821
- .foregroundColor(Palette.textDim)
822
- .tracking(1)
823
- }
824
-
825
- // MARK: - Tile cell (spatial grid item)
826
-
827
- private func tileCell(action: HotkeyAction, label: String) -> some View {
828
- let binding = hotkeyStore.bindings[action]
829
- let badgeText = binding?.displayParts.last ?? ""
830
-
831
- return Button {
832
- // Open inline key recorder for this action
833
- } label: {
834
- VStack(spacing: 3) {
835
- Text(label)
836
- .font(Typo.caption(9))
837
- .foregroundColor(Palette.textDim)
838
- Text(badgeText)
839
- .font(Typo.geistMonoBold(9))
840
- .foregroundColor(Palette.text)
841
- }
842
- .frame(maxWidth: .infinity)
843
- .frame(height: 42)
844
- .background(
845
- RoundedRectangle(cornerRadius: 4)
846
- .fill(Palette.surface)
847
- .overlay(
848
- RoundedRectangle(cornerRadius: 4)
849
- .strokeBorder(Palette.border, lineWidth: 0.5)
850
- )
851
- )
852
- }
853
- .buttonStyle(.plain)
854
- .popover(isPresented: tileCellPopoverBinding(for: action)) {
855
- KeyRecorderView(action: action, store: hotkeyStore)
856
- .padding(12)
857
- .frame(width: 300)
858
- }
859
- }
860
-
861
- @State private var expandedOcrWindow: UInt32?
862
- @State private var collapsedOcrApps: Set<String> = []
863
-
864
- @State private var activeTilePopover: HotkeyAction?
865
-
866
- private func tileCellPopoverBinding(for action: HotkeyAction) -> Binding<Bool> {
867
- Binding(
868
- get: { activeTilePopover == action },
869
- set: { if !$0 { activeTilePopover = nil } }
870
- )
871
- }
872
-
873
- // MARK: - Compact key recorder
874
-
875
- private func compactKeyRecorder(action: HotkeyAction) -> some View {
876
- KeyRecorderView(action: action, store: hotkeyStore)
877
- }
878
-
879
- // MARK: - Shortcut row (read-only, for tmux)
880
-
881
- private func shortcutRow(_ label: String, keys: [String]) -> some View {
882
- HStack {
883
- Text(label)
884
- .font(Typo.caption(11))
885
- .foregroundColor(Palette.textDim)
886
- .frame(width: 80, alignment: .trailing)
887
-
888
- HStack(spacing: 4) {
889
- ForEach(keys, id: \.self) { key in
890
- keyBadge(key)
891
- }
892
- }
893
- .padding(.leading, 8)
894
-
895
- Spacer()
896
- }
897
- }
898
-
899
- // MARK: - Docs
900
-
901
- private var docsContent: some View {
902
- ScrollView {
903
- LazyVStack(alignment: .leading, spacing: 0, pinnedViews: .sectionHeaders) {
904
- Section(header: stickyHeader("What is lattices?")) {
905
- Text("A developer workspace launcher. It creates pre-configured terminal layouts for your projects using tmux \u{2014} go from \u{201C}I want to work on X\u{201D} to a full environment in one click.")
906
- .font(Typo.caption(11))
907
- .foregroundColor(Palette.textDim)
908
- .lineSpacing(3)
909
- .padding(.horizontal, 20)
910
- .padding(.vertical, 12)
911
- }
912
-
913
- Section(header: stickyHeader("Glossary")) {
914
- VStack(alignment: .leading, spacing: 12) {
915
- glossaryItem("Session",
916
- "A persistent workspace that lives in the background. Survives terminal crashes, disconnects, even closing your laptop.")
917
- glossaryItem("Pane",
918
- "A single terminal view inside a session. A typical setup has two panes \u{2014} Claude Code on the left, dev server on the right.")
919
- glossaryItem("Attach",
920
- "Connect your terminal window to an existing session. The session was already running \u{2014} you\u{2019}re just viewing it.")
921
- glossaryItem("Detach",
922
- "Disconnect your terminal but keep the session alive. Your dev server keeps running, Claude keeps thinking.")
923
- glossaryItem("tmux",
924
- "Terminal multiplexer \u{2014} the engine behind lattices. It manages sessions, panes, and layouts. lattices configures it so you don\u{2019}t have to.")
925
- }
926
- .padding(.horizontal, 20)
927
- .padding(.vertical, 12)
928
- }
929
-
930
- Section(header: stickyHeader("How it works")) {
931
- VStack(alignment: .leading, spacing: 8) {
932
- flowStep("1", "Create a .lattices.json in your project root")
933
- flowStep("2", "lattices reads the config and builds a tmux session")
934
- flowStep("3", "Each pane gets its command (claude, dev server, etc.)")
935
- flowStep("4", "Session persists in the background until you kill it")
936
- flowStep("5", "Attach and detach from any terminal, any time")
937
- }
938
- .padding(.horizontal, 20)
939
- .padding(.vertical, 12)
940
- }
941
-
942
- Section(header: stickyHeader("Reference")) {
943
- HStack(spacing: 8) {
944
- docsLinkButton(icon: "doc.text", label: "Config format", file: "config.md")
945
- docsLinkButton(icon: "book", label: "Full concepts", file: "concepts.md")
946
- }
947
- .padding(.horizontal, 20)
948
- .padding(.vertical, 12)
949
- }
950
- }
951
- }
952
- }
953
-
954
- // MARK: - Docs helpers
955
-
956
- private func glossaryItem(_ term: String, _ definition: String) -> some View {
957
- VStack(alignment: .leading, spacing: 3) {
958
- Text(term)
959
- .font(Typo.monoBold(11))
960
- .foregroundColor(Palette.text)
961
- Text(definition)
962
- .font(Typo.caption(10.5))
963
- .foregroundColor(Palette.textMuted)
964
- .lineSpacing(2)
965
- }
966
- }
967
-
968
- private func flowStep(_ number: String, _ text: String) -> some View {
969
- HStack(alignment: .top, spacing: 8) {
970
- Text(number)
971
- .font(Typo.monoBold(10))
972
- .foregroundColor(Palette.running)
973
- .frame(width: 14)
974
- Text(text)
975
- .font(Typo.caption(11))
976
- .foregroundColor(Palette.textDim)
977
- }
978
- }
979
-
980
- private func docsLinkButton(icon: String, label: String, file: String) -> some View {
981
- Button {
982
- let path = resolveDocsFile(file)
983
- NSWorkspace.shared.open(URL(fileURLWithPath: path))
984
- } label: {
985
- HStack(spacing: 6) {
986
- Image(systemName: icon)
987
- .font(.system(size: 10))
988
- Text(label)
989
- .font(Typo.caption(11))
990
- }
991
- .foregroundColor(Palette.textDim)
992
- .padding(.horizontal, 12)
993
- .padding(.vertical, 6)
994
- .background(
995
- RoundedRectangle(cornerRadius: 3)
996
- .fill(Palette.surface)
997
- .overlay(
998
- RoundedRectangle(cornerRadius: 3)
999
- .strokeBorder(Palette.border, lineWidth: 0.5)
1000
- )
1001
- )
1002
- }
1003
- .buttonStyle(.plain)
1004
- }
1005
-
1006
- private func resolveDocsFile(_ file: String) -> String {
1007
- let devPath = "/Users/arach/dev/lattice/docs/\(file)"
1008
- if FileManager.default.fileExists(atPath: devPath) { return devPath }
1009
- let bundle = Bundle.main.bundlePath
1010
- let appDir = (bundle as NSString).deletingLastPathComponent
1011
- let docsPath = ((appDir as NSString).appendingPathComponent("../docs/\(file)") as NSString).standardizingPath
1012
- if FileManager.default.fileExists(atPath: docsPath) { return docsPath }
1013
- return devPath
1014
- }
1015
-
1016
- // MARK: - Shared helpers
1017
-
1018
- private var separator: some View {
1019
- Rectangle()
1020
- .fill(Palette.border)
1021
- .frame(height: 0.5)
1022
- }
1023
-
1024
- private func settingsRow<Content: View>(_ label: String, @ViewBuilder content: () -> Content) -> some View {
1025
- HStack(alignment: .top, spacing: 0) {
1026
- Text(label)
1027
- .font(Typo.caption(11))
1028
- .foregroundColor(Palette.textDim)
1029
- .frame(width: 100, alignment: .trailing)
1030
- .padding(.top, 2)
1031
-
1032
- content()
1033
- .padding(.leading, 16)
1034
- .frame(maxWidth: .infinity, alignment: .leading)
1035
- }
1036
- }
1037
-
1038
- private func keyBadge(_ key: String) -> some View {
1039
- Text(key)
1040
- .font(Typo.geistMonoBold(10))
1041
- .foregroundColor(Palette.text)
1042
- .padding(.horizontal, 6)
1043
- .padding(.vertical, 3)
1044
- .background(
1045
- RoundedRectangle(cornerRadius: 3)
1046
- .fill(Palette.surface)
1047
- .overlay(
1048
- RoundedRectangle(cornerRadius: 3)
1049
- .strokeBorder(Palette.border, lineWidth: 0.5)
1050
- )
1051
- )
1052
- }
1053
- }