@lattices/cli 0.4.10 → 0.4.11

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 (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +13 -13
  3. package/{app → apps/mac}/Lattices.app/Contents/Info.plist +10 -2
  4. package/{app → apps/mac}/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/{app → apps/mac}/Package.swift +2 -1
  6. package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
  7. package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
  8. package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
  9. package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
  10. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +27 -0
  11. package/apps/mac/Sources/AppShell/AppDelegate.swift +189 -0
  12. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +25 -0
  13. package/{app → apps/mac}/Sources/AppShell/AppShellView.swift +18 -3
  14. package/{app → apps/mac}/Sources/AppShell/AppUpdater.swift +4 -3
  15. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +87 -0
  16. package/{app → apps/mac}/Sources/AppShell/LatticesRuntime.swift +43 -0
  17. package/{app → apps/mac}/Sources/AppShell/MainView.swift +116 -63
  18. package/apps/mac/Sources/AppShell/MenuBarController.swift +177 -0
  19. package/{app → apps/mac}/Sources/AppShell/OnboardingView.swift +72 -60
  20. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +366 -0
  21. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +70 -0
  22. package/{app → apps/mac}/Sources/AppShell/Preferences.swift +37 -2
  23. package/{app → apps/mac}/Sources/AppShell/SettingsView.swift +815 -156
  24. package/{app → apps/mac}/Sources/AppShell/SettingsWindow.swift +10 -0
  25. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +13 -0
  26. package/{app → apps/mac}/Sources/Core/Actions/HotkeyStore.swift +6 -1
  27. package/{app → apps/mac}/Sources/Core/Actions/IntentEngine.swift +2 -0
  28. package/{app → apps/mac}/Sources/Core/Daemon/DaemonServer.swift +5 -0
  29. package/{app → apps/mac}/Sources/Core/Daemon/LatticesApi.swift +365 -0
  30. package/{app → apps/mac}/Sources/Core/Desktop/OcrModel.swift +17 -13
  31. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +33 -0
  32. package/{app → apps/mac}/Sources/Core/Desktop/WindowDragSnapController.swift +18 -217
  33. package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewStore.swift +4 -5
  34. package/{app → apps/mac}/Sources/Core/Desktop/WindowTiler.swift +19 -13
  35. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +124 -0
  36. package/apps/mac/Sources/Core/Input/EventTapThread.swift +54 -0
  37. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +20 -0
  38. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +335 -0
  39. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +141 -0
  40. package/{app → apps/mac}/Sources/Core/Input/MouseGestureConfig.swift +155 -20
  41. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +2259 -0
  42. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +170 -0
  43. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +39 -0
  44. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +624 -0
  45. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +56 -0
  46. package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +8 -8
  47. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +1240 -0
  48. package/{app → apps/mac}/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +11 -23
  49. package/{app → apps/mac}/Sources/Core/Pi/PiChatDock.swift +90 -43
  50. package/{app → apps/mac}/Sources/Core/Pi/PiChatSession.swift +676 -43
  51. package/{app → apps/mac}/Sources/Core/Pi/PiProviderSetupCallout.swift +5 -5
  52. package/{app → apps/mac}/Sources/Core/Pi/PiWorkspaceView.swift +93 -44
  53. package/apps/mac/Sources/Core/System/Capability.swift +79 -0
  54. package/{app → apps/mac}/Sources/Core/System/PermissionChecker.swift +43 -8
  55. package/{app → apps/mac}/Sources/Core/Voice/AudioProvider.swift +225 -56
  56. package/bin/handsoff-infer.ts +14 -5
  57. package/bin/handsoff-worker.ts +11 -7
  58. package/bin/infer.ts +406 -0
  59. package/bin/lattices-app.ts +57 -7
  60. package/bin/lattices-dev +40 -1
  61. package/bin/lattices.ts +1 -1
  62. package/docs/agent-execution-plan.md +9 -9
  63. package/docs/api.md +119 -0
  64. package/docs/app.md +1 -0
  65. package/docs/companion-deck.md +1 -1
  66. package/docs/gesture-customization-proposal.md +520 -0
  67. package/docs/mouse-gestures.md +79 -0
  68. package/docs/overview.md +2 -2
  69. package/docs/presentation-execution-review.md +9 -9
  70. package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
  71. package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
  72. package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
  73. package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
  74. package/docs/reference/dewey.config.ts +74 -0
  75. package/docs/reference/install-agent.md +79 -0
  76. package/docs/repo-structure.md +100 -0
  77. package/docs/voice-error-model.md +7 -7
  78. package/docs/voice.md +18 -0
  79. package/package.json +23 -13
  80. package/swift/Package.swift +20 -0
  81. package/swift/Sources/DeckKit/DeckAction.swift +51 -0
  82. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +152 -0
  83. package/swift/Sources/DeckKit/DeckCockpit.swift +82 -0
  84. package/swift/Sources/DeckKit/DeckHost.swift +7 -0
  85. package/swift/Sources/DeckKit/DeckManifest.swift +145 -0
  86. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +533 -0
  87. package/swift/Sources/DeckKit/DeckTrackpad.swift +63 -0
  88. package/swift/Sources/DeckKit/DeckValue.swift +93 -0
  89. package/swift/Sources/DeckKit/DeckVoiceError.swift +88 -0
  90. package/swift/Tests/DeckKitTests/DeckKitTests.swift +286 -0
  91. package/app/Sources/AppShell/AppDelegate.swift +0 -408
  92. package/app/Sources/Core/Input/KeyboardRemapController.swift +0 -184
  93. package/app/Sources/Core/Input/KeyboardRemapStore.swift +0 -84
  94. package/app/Sources/Core/Input/MouseGestureController.swift +0 -1203
  95. package/app/Sources/Core/Input/MouseShortcutStore.swift +0 -107
  96. /package/{app → apps/mac}/Info.plist +0 -0
  97. /package/{app → apps/mac}/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  98. /package/{app → apps/mac}/Lattices.app/Contents/Resources/tap.wav +0 -0
  99. /package/{app → apps/mac}/Lattices.app/Contents/_CodeSignature/CodeResources +0 -0
  100. /package/{app → apps/mac}/Lattices.entitlements +0 -0
  101. /package/{app → apps/mac}/Resources/tap.wav +0 -0
  102. /package/{app → apps/mac}/Sources/AppShell/App.swift +0 -0
  103. /package/{app → apps/mac}/Sources/AppShell/CliActionLauncher.swift +0 -0
  104. /package/{app → apps/mac}/Sources/AppShell/HomeDashboardView.swift +0 -0
  105. /package/{app → apps/mac}/Sources/AppShell/KeyRecorderView.swift +0 -0
  106. /package/{app → apps/mac}/Sources/AppShell/MainWindow.swift +0 -0
  107. /package/{app → apps/mac}/Sources/Core/Actions/HotkeyManager.swift +0 -0
  108. /package/{app → apps/mac}/Sources/Core/Actions/IntentSchema.swift +0 -0
  109. /package/{app → apps/mac}/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -0
  110. /package/{app → apps/mac}/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -0
  111. /package/{app → apps/mac}/Sources/Core/Actions/Intents/FocusIntent.swift +0 -0
  112. /package/{app → apps/mac}/Sources/Core/Actions/Intents/HelpIntent.swift +0 -0
  113. /package/{app → apps/mac}/Sources/Core/Actions/Intents/KillIntent.swift +0 -0
  114. /package/{app → apps/mac}/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -0
  115. /package/{app → apps/mac}/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -0
  116. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -0
  117. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -0
  118. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ScanIntent.swift +0 -0
  119. /package/{app → apps/mac}/Sources/Core/Actions/Intents/SearchIntent.swift +0 -0
  120. /package/{app → apps/mac}/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -0
  121. /package/{app → apps/mac}/Sources/Core/Actions/Intents/TileIntent.swift +0 -0
  122. /package/{app → apps/mac}/Sources/Core/Actions/PaletteCommand.swift +0 -0
  123. /package/{app → apps/mac}/Sources/Core/Actions/VoiceIntentResolver.swift +0 -0
  124. /package/{app → apps/mac}/Sources/Core/Companion/CompanionActivityLog.swift +0 -0
  125. /package/{app → apps/mac}/Sources/Core/Companion/CompanionKeyboardController.swift +0 -0
  126. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -0
  127. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -0
  128. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -0
  129. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -0
  130. /package/{app → apps/mac}/Sources/Core/Companion/LatticesDeckHost.swift +0 -0
  131. /package/{app → apps/mac}/Sources/Core/Daemon/DaemonProtocol.swift +0 -0
  132. /package/{app → apps/mac}/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -0
  133. /package/{app → apps/mac}/Sources/Core/Desktop/AppTypeClassifier.swift +0 -0
  134. /package/{app → apps/mac}/Sources/Core/Desktop/DesktopModel.swift +0 -0
  135. /package/{app → apps/mac}/Sources/Core/Desktop/DesktopModelTypes.swift +0 -0
  136. /package/{app → apps/mac}/Sources/Core/Desktop/InventoryManager.swift +0 -0
  137. /package/{app → apps/mac}/Sources/Core/Desktop/InventoryPath.swift +0 -0
  138. /package/{app → apps/mac}/Sources/Core/Desktop/MouseFinder.swift +0 -0
  139. /package/{app → apps/mac}/Sources/Core/Desktop/OcrStore.swift +0 -0
  140. /package/{app → apps/mac}/Sources/Core/Desktop/PlacementSpec.swift +0 -0
  141. /package/{app → apps/mac}/Sources/Core/Desktop/SessionWindowLocator.swift +0 -0
  142. /package/{app → apps/mac}/Sources/Core/Desktop/TilePickerView.swift +0 -0
  143. /package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewCard.swift +0 -0
  144. /package/{app → apps/mac}/Sources/Core/Desktop/WindowSelectionStore.swift +0 -0
  145. /package/{app → apps/mac}/Sources/Core/Input/KeyboardRemapConfig.swift +0 -0
  146. /package/{app → apps/mac}/Sources/Core/Input/MouseInputDeviceStore.swift +0 -0
  147. /package/{app → apps/mac}/Sources/Core/Input/MouseInputEventViewer.swift +0 -0
  148. /package/{app → apps/mac}/Sources/Core/Overlays/AppWindowShell.swift +0 -0
  149. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -0
  150. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -0
  151. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -0
  152. /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -0
  153. /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -0
  154. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -0
  155. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -0
  156. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDController.swift +0 -0
  157. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -0
  158. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -0
  159. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -0
  160. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDState.swift +0 -0
  161. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -0
  162. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -0
  163. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -0
  164. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -0
  165. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -0
  166. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -0
  167. /package/{app → apps/mac}/Sources/Core/Overlays/OverlayPanelShell.swift +0 -0
  168. /package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -0
  169. /package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -0
  170. /package/{app → apps/mac}/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -0
  171. /package/{app → apps/mac}/Sources/Core/Pi/PiAuthPromptCard.swift +0 -0
  172. /package/{app → apps/mac}/Sources/Core/Pi/PiInstallCallout.swift +0 -0
  173. /package/{app → apps/mac}/Sources/Core/System/DiagnosticLog.swift +0 -0
  174. /package/{app → apps/mac}/Sources/Core/System/EventBus.swift +0 -0
  175. /package/{app → apps/mac}/Sources/Core/System/ProcessModel.swift +0 -0
  176. /package/{app → apps/mac}/Sources/Core/System/ProcessQuery.swift +0 -0
  177. /package/{app → apps/mac}/Sources/Core/System/SystemTelemetryMonitor.swift +0 -0
  178. /package/{app → apps/mac}/Sources/Core/Voice/AdvisorLearningStore.swift +0 -0
  179. /package/{app → apps/mac}/Sources/Core/Voice/AgentSession.swift +0 -0
  180. /package/{app → apps/mac}/Sources/Core/Voice/HandsOffSession.swift +0 -0
  181. /package/{app → apps/mac}/Sources/Core/Voice/VoiceChatView.swift +0 -0
  182. /package/{app → apps/mac}/Sources/Core/Voice/VoxClient.swift +0 -0
  183. /package/{app → apps/mac}/Sources/Core/Workspace/Project.swift +0 -0
  184. /package/{app → apps/mac}/Sources/Core/Workspace/ProjectScanner.swift +0 -0
  185. /package/{app → apps/mac}/Sources/Core/Workspace/SessionLayerStore.swift +0 -0
  186. /package/{app → apps/mac}/Sources/Core/Workspace/SessionManager.swift +0 -0
  187. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/Terminal.swift +0 -0
  188. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -0
  189. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -0
  190. /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -0
  191. /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -0
  192. /package/{app → apps/mac}/Sources/Core/Workspace/WorkspaceManager.swift +0 -0
  193. /package/{app → apps/mac}/Sources/UI/ActionRow.swift +0 -0
  194. /package/{app → apps/mac}/Sources/UI/OrphanRow.swift +0 -0
  195. /package/{app → apps/mac}/Sources/UI/ProjectRow.swift +0 -0
  196. /package/{app → apps/mac}/Sources/UI/TabGroupRow.swift +0 -0
  197. /package/{app → apps/mac}/Sources/UI/Theme.swift +0 -0
  198. /package/{app → apps/mac}/Tests/StageDragTests.swift +0 -0
  199. /package/{app → apps/mac}/Tests/StageJoinTests.swift +0 -0
  200. /package/{app → apps/mac}/Tests/StageManagerTests.swift +0 -0
  201. /package/{app → apps/mac}/Tests/StageTileTests.swift +0 -0
@@ -6,55 +6,55 @@ import SwiftUI
6
6
  struct SettingsContentView: View {
7
7
  private enum SettingsSection: String, CaseIterable, Identifiable {
8
8
  case general
9
- case companion
9
+ case shortcuts
10
10
  case ai
11
11
  case search
12
- case shortcuts
12
+ case companion
13
13
 
14
14
  var id: String { rawValue }
15
15
 
16
16
  var title: String {
17
17
  switch self {
18
18
  case .general: return "General"
19
- case .companion: return "Companion"
19
+ case .shortcuts: return "Shortcuts"
20
20
  case .ai: return "AI"
21
21
  case .search: return "Search & OCR"
22
- case .shortcuts: return "Shortcuts"
22
+ case .companion: return "LATS iOS Companion"
23
23
  }
24
24
  }
25
25
 
26
26
  var icon: String {
27
27
  switch self {
28
28
  case .general: return "slider.horizontal.3"
29
- case .companion: return "ipad.and.iphone"
29
+ case .shortcuts: return "command"
30
30
  case .ai: return "sparkles"
31
31
  case .search: return "text.viewfinder"
32
- case .shortcuts: return "command"
32
+ case .companion: return "ipad.and.iphone"
33
33
  }
34
34
  }
35
35
 
36
36
  var eyebrow: String {
37
37
  switch self {
38
38
  case .general: return "Workspace"
39
- case .companion: return "Local Bridge"
39
+ case .shortcuts: return "Controls"
40
40
  case .ai: return "Agents"
41
41
  case .search: return "Indexing"
42
- case .shortcuts: return "Controls"
42
+ case .companion: return "Local Bridge"
43
43
  }
44
44
  }
45
45
 
46
46
  var summary: String {
47
47
  switch self {
48
48
  case .general:
49
- return "Terminal defaults, scan roots, window snapping, and app updates."
50
- case .companion:
51
- return "Local-network pairing, trusted iPad devices, and bridge security."
49
+ return "App updates, permissions, terminal defaults, project discovery, and interaction behavior."
50
+ case .shortcuts:
51
+ return "A full map of global hotkeys for workspace movement and tmux flow."
52
52
  case .ai:
53
53
  return "Claude CLI detection plus advisor model and spending controls."
54
54
  case .search:
55
55
  return "OCR cadence, quality, and recent capture visibility."
56
- case .shortcuts:
57
- return "A full map of global hotkeys for workspace movement and tmux flow."
56
+ case .companion:
57
+ return "Local-network pairing, trusted iPad devices, and bridge security."
58
58
  }
59
59
  }
60
60
  }
@@ -67,6 +67,9 @@ struct SettingsContentView: View {
67
67
  @ObservedObject var appUpdater: AppUpdater = .shared
68
68
  @ObservedObject var mouseShortcutStore: MouseShortcutStore = .shared
69
69
  @ObservedObject var keyboardRemapStore: KeyboardRemapStore = .shared
70
+ @ObservedObject var permChecker: PermissionChecker = .shared
71
+ @ObservedObject var mouseGestureController: MouseGestureController = .shared
72
+ @ObservedObject var keyboardRemapController: KeyboardRemapController = .shared
70
73
  var onBack: (() -> Void)? = nil
71
74
 
72
75
  @State private var selectedTab: SettingsSection = .general
@@ -86,10 +89,17 @@ struct SettingsContentView: View {
86
89
  .clipped()
87
90
  .background(PanelBackground())
88
91
  .onAppear {
92
+ permChecker.check()
89
93
  if page == .companionSettings {
90
94
  selectedTab = .companion
91
95
  }
92
96
  }
97
+ .onReceive(NotificationCenter.default.publisher(for: .latticesShowGeneralSettings)) { _ in
98
+ selectedTab = .general
99
+ }
100
+ .onReceive(NotificationCenter.default.publisher(for: .latticesShowAssistantSettings)) { _ in
101
+ selectedTab = .ai
102
+ }
93
103
  }
94
104
 
95
105
  // MARK: - Back Bar
@@ -138,7 +148,7 @@ struct SettingsContentView: View {
138
148
  private var settingsBody: some View {
139
149
  HStack(spacing: 0) {
140
150
  settingsSidebar
141
- .frame(width: 220, alignment: .top)
151
+ .frame(width: 190, alignment: .top)
142
152
 
143
153
  Rectangle()
144
154
  .fill(Palette.border)
@@ -188,23 +198,22 @@ struct SettingsContentView: View {
188
198
  Image(systemName: section.icon)
189
199
  .font(.system(size: 11, weight: .semibold))
190
200
  .foregroundColor(active ? Palette.text : Palette.textMuted)
191
- .frame(width: 16, alignment: .center)
201
+ .frame(width: 16, height: 18, alignment: .center)
192
202
 
193
203
  VStack(alignment: .leading, spacing: 3) {
194
204
  Text(section.title)
195
205
  .font(Typo.mono(11))
196
206
  .foregroundColor(active ? Palette.text : Palette.textMuted)
197
207
 
198
- Text(section.summary)
199
- .font(Typo.caption(9.5))
200
- .foregroundColor(Palette.textMuted.opacity(active ? 0.9 : 0.7))
201
- .fixedSize(horizontal: false, vertical: true)
208
+ Text(section.eyebrow)
209
+ .font(Typo.caption(9))
210
+ .foregroundColor(Palette.textMuted.opacity(active ? 0.85 : 0.62))
202
211
  }
203
212
 
204
213
  Spacer(minLength: 0)
205
214
  }
206
215
  .padding(.horizontal, 10)
207
- .padding(.vertical, 9)
216
+ .padding(.vertical, 8)
208
217
  .contentShape(RoundedRectangle(cornerRadius: 8))
209
218
  .background(
210
219
  ZStack {
@@ -254,14 +263,14 @@ struct SettingsContentView: View {
254
263
  switch selectedTab {
255
264
  case .general:
256
265
  generalContent
257
- case .companion:
258
- companionContent
266
+ case .shortcuts:
267
+ shortcutsContent
259
268
  case .ai:
260
269
  aiContent
261
270
  case .search:
262
271
  searchOcrContent
263
- case .shortcuts:
264
- shortcutsContent
272
+ case .companion:
273
+ companionContent
265
274
  }
266
275
  }
267
276
 
@@ -286,24 +295,614 @@ struct SettingsContentView: View {
286
295
  }
287
296
  }
288
297
 
289
- // MARK: - General
290
-
291
- private var generalContent: some View {
298
+ // MARK: - General
299
+
300
+ private var permissionsAssistantCard: some View {
301
+ let missing = Capability.allCases.filter { !$0.isGranted }
302
+ return settingsCard {
303
+ VStack(alignment: .leading, spacing: 12) {
304
+ HStack(alignment: .center, spacing: 8) {
305
+ RoundedRectangle(cornerRadius: 5)
306
+ .fill((missing.isEmpty ? Palette.running : Palette.detach).opacity(0.13))
307
+ .overlay(
308
+ Image(systemName: missing.isEmpty ? "checkmark.shield.fill" : "exclamationmark.shield.fill")
309
+ .font(.system(size: 12, weight: .semibold))
310
+ .foregroundColor(missing.isEmpty ? Palette.running : Palette.detach)
311
+ )
312
+ .frame(width: 26, height: 26)
313
+
314
+ VStack(alignment: .leading, spacing: 2) {
315
+ Text("Permissions")
316
+ .font(Typo.mono(12))
317
+ .foregroundColor(Palette.text)
318
+ Text(missing.isEmpty ? "Ready for window control, gestures, and OCR" : "\(missing.count) permission \(missing.count == 1 ? "needs" : "need") attention")
319
+ .font(Typo.caption(9.5))
320
+ .foregroundColor(Palette.textMuted)
321
+ }
322
+
323
+ Spacer()
324
+
325
+ Text(missing.isEmpty ? "All on" : "\(missing.count) off")
326
+ .font(Typo.monoBold(9.5))
327
+ .foregroundColor(missing.isEmpty ? Palette.running : Palette.detach)
328
+ .padding(.horizontal, 7)
329
+ .padding(.vertical, 3)
330
+ .background(
331
+ Capsule()
332
+ .fill((missing.isEmpty ? Palette.running : Palette.detach).opacity(0.10))
333
+ .overlay(Capsule().strokeBorder((missing.isEmpty ? Palette.running : Palette.detach).opacity(0.18), lineWidth: 0.5))
334
+ )
335
+ }
336
+
337
+ Text("The assistant explains each macOS prompt before you grant it. Advanced privacy panes stay available for review when a synthetic shortcut or input capture needs macOS-level repair.")
338
+ .font(Typo.caption(10))
339
+ .foregroundColor(Palette.textMuted)
340
+
341
+ HStack(spacing: 8) {
342
+ ForEach(Capability.allCases) { cap in
343
+ HStack(spacing: 4) {
344
+ Circle()
345
+ .fill(cap.isGranted ? Palette.running : Palette.detach)
346
+ .frame(width: 5, height: 5)
347
+ Text(cap.title)
348
+ .font(Typo.mono(9))
349
+ .foregroundColor(Palette.textMuted)
350
+ }
351
+ .padding(.horizontal, 6)
352
+ .padding(.vertical, 2)
353
+ .background(
354
+ Capsule().fill(Palette.surface)
355
+ )
356
+ }
357
+ Spacer(minLength: 0)
358
+ }
359
+
360
+ HStack(spacing: 8) {
361
+ Button {
362
+ PermissionsAssistantWindowController.shared.show(focus: missing.first)
363
+ } label: {
364
+ Text(missing.isEmpty ? "Open Assistant" : "Set Up")
365
+ .font(Typo.monoBold(10))
366
+ .foregroundColor(Palette.text)
367
+ .padding(.horizontal, 12)
368
+ .padding(.vertical, 5)
369
+ .background(
370
+ RoundedRectangle(cornerRadius: 4)
371
+ .fill(Palette.surfaceHov)
372
+ .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
373
+ )
374
+ }
375
+ .buttonStyle(.plain)
376
+
377
+ Button {
378
+ permChecker.check()
379
+ } label: {
380
+ Image(systemName: "arrow.clockwise")
381
+ .font(.system(size: 10, weight: .semibold))
382
+ .foregroundColor(Palette.textDim)
383
+ .frame(width: 24, height: 22)
384
+ }
385
+ .buttonStyle(.plain)
386
+ .help("Refresh permission status")
387
+
388
+ Spacer()
389
+
390
+ Button {
391
+ permChecker.openAutomationSettings()
392
+ } label: {
393
+ Text("Automation")
394
+ .font(Typo.caption(9))
395
+ .foregroundColor(Palette.textMuted.opacity(0.9))
396
+ }
397
+ .buttonStyle(.plain)
398
+
399
+ Button {
400
+ permChecker.openInputMonitoringSettings()
401
+ } label: {
402
+ Text("Input Monitoring")
403
+ .font(Typo.caption(9))
404
+ .foregroundColor(Palette.textMuted.opacity(0.9))
405
+ }
406
+ .buttonStyle(.plain)
407
+ }
408
+ }
409
+ }
410
+ }
411
+
412
+ private var permissionsDetailCard: some View {
413
+ settingsCard {
414
+ VStack(alignment: .leading, spacing: 10) {
415
+ HStack(alignment: .center, spacing: 8) {
416
+ Image(systemName: permChecker.allGranted ? "checkmark.shield.fill" : "exclamationmark.shield.fill")
417
+ .font(.system(size: 11, weight: .medium))
418
+ .foregroundColor(permChecker.allGranted ? Palette.running : Palette.detach)
419
+ Text("macOS permissions")
420
+ .font(Typo.mono(12))
421
+ .foregroundColor(Palette.text)
422
+ Spacer()
423
+ Button {
424
+ permChecker.check()
425
+ } label: {
426
+ Image(systemName: "arrow.clockwise")
427
+ .font(.system(size: 10, weight: .semibold))
428
+ .foregroundColor(Palette.textDim)
429
+ .frame(width: 24, height: 22)
430
+ }
431
+ .buttonStyle(.plain)
432
+ .help("Refresh permission status")
433
+ }
434
+
435
+ Text("Window discovery, gestures, remaps, OCR, and synthetic shortcuts all depend on these macOS grants.")
436
+ .font(Typo.caption(10))
437
+ .foregroundColor(Palette.textMuted)
438
+
439
+ VStack(alignment: .leading, spacing: 6) {
440
+ permissionSettingsRow(
441
+ "Accessibility",
442
+ granted: permChecker.accessibility,
443
+ detail: "Required for mouse gestures, keyboard remaps, window movement, and focusing windows."
444
+ ) {
445
+ permChecker.requestAccessibility()
446
+ }
447
+
448
+ permissionSettingsRow(
449
+ "Screen Recording",
450
+ granted: permChecker.screenRecording,
451
+ detail: "Required for reliable window titles, OCR, and Space-aware window discovery."
452
+ ) {
453
+ permChecker.requestScreenRecording()
454
+ }
455
+
456
+ permissionReviewRow(
457
+ "Automation",
458
+ detail: "Needed when Lattices sends shortcuts through System Events, including gesture-triggered dictation."
459
+ ) {
460
+ permChecker.openAutomationSettings()
461
+ }
462
+
463
+ permissionReviewRow(
464
+ "Input Monitoring",
465
+ detail: "Useful to review if global input capture or synthetic shortcut behavior starts failing."
466
+ ) {
467
+ permChecker.openInputMonitoringSettings()
468
+ }
469
+ }
470
+ }
471
+ }
472
+ }
473
+
474
+ private var appUpdateCard: some View {
475
+ settingsCard {
476
+ VStack(alignment: .leading, spacing: 10) {
477
+ HStack(alignment: .center, spacing: 10) {
478
+ RoundedRectangle(cornerRadius: 6)
479
+ .fill((LatticesRuntime.isDevBuild ? Palette.detach : Palette.running).opacity(0.13))
480
+ .overlay(
481
+ Image(systemName: LatticesRuntime.isDevBuild ? "hammer.fill" : "checkmark.seal.fill")
482
+ .font(.system(size: 13, weight: .semibold))
483
+ .foregroundColor(LatticesRuntime.isDevBuild ? Palette.detach : Palette.running)
484
+ )
485
+ .frame(width: 30, height: 30)
486
+
487
+ VStack(alignment: .leading, spacing: 4) {
488
+ HStack(spacing: 8) {
489
+ Text("Lattices app")
490
+ .font(Typo.mono(12))
491
+ .foregroundColor(Palette.text)
492
+ buildChannelBadge
493
+ }
494
+
495
+ HStack(alignment: .firstTextBaseline, spacing: 8) {
496
+ Text(appUpdater.currentDisplayVersion)
497
+ .font(Typo.monoBold(13))
498
+ .foregroundColor(Palette.text)
499
+ Text(LatticesRuntime.buildStatusLabel)
500
+ .font(Typo.monoBold(9.5))
501
+ .foregroundColor(LatticesRuntime.isDevBuild ? Palette.detach : Palette.running)
502
+ }
503
+ }
504
+
505
+ Spacer()
506
+
507
+ Toggle("Auto", isOn: $appUpdater.autoCheckEnabled)
508
+ .font(Typo.caption(9))
509
+ .toggleStyle(.checkbox)
510
+ .foregroundColor(Palette.textMuted.opacity(0.9))
511
+ }
512
+
513
+ if let update = appUpdater.availableUpdate {
514
+ Text("New version v\(update.version) is ready")
515
+ .font(Typo.monoBold(10))
516
+ .foregroundColor(Palette.detach)
517
+ } else if appUpdater.isChecking {
518
+ Text("Checking for updates...")
519
+ .font(Typo.caption(9))
520
+ .foregroundColor(Palette.textMuted)
521
+ } else if let error = appUpdater.lastError {
522
+ Text(error)
523
+ .font(Typo.caption(9))
524
+ .foregroundColor(Palette.detach.opacity(0.9))
525
+ } else if let checked = appUpdater.lastChecked {
526
+ Text("Last checked \(checked, style: .relative)")
527
+ .font(Typo.caption(9))
528
+ .foregroundColor(Palette.textMuted.opacity(0.8))
529
+ }
530
+
531
+ HStack(spacing: 10) {
532
+ Button {
533
+ appUpdater.promptForUpdate()
534
+ } label: {
535
+ Text(appUpdater.isUpdating ? "Preparing..." : (appUpdater.availableUpdate == nil ? "Check for Updates" : "Update to v\(appUpdater.availableUpdate?.version ?? "")"))
536
+ .font(Typo.monoBold(10))
537
+ .foregroundColor(Palette.text)
538
+ .padding(.horizontal, 12)
539
+ .padding(.vertical, 5)
540
+ .background(
541
+ RoundedRectangle(cornerRadius: 4)
542
+ .fill(Palette.surfaceHov)
543
+ .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
544
+ )
545
+ }
546
+ .buttonStyle(.plain)
547
+ .disabled(appUpdater.isUpdating)
548
+
549
+ Button {
550
+ Task { await appUpdater.check() }
551
+ } label: {
552
+ Text(appUpdater.isChecking ? "Checking..." : "Check Now")
553
+ .font(Typo.caption(9))
554
+ .foregroundColor(Palette.textMuted.opacity(0.9))
555
+ }
556
+ .buttonStyle(.plain)
557
+ .disabled(appUpdater.isChecking)
558
+
559
+ Spacer()
560
+ }
561
+ }
562
+ }
563
+ }
564
+
565
+ private var interactionBehaviorCard: some View {
566
+ settingsCard {
567
+ VStack(alignment: .leading, spacing: 12) {
568
+ Text("Session behavior")
569
+ .font(Typo.mono(12))
570
+ .foregroundColor(Palette.text)
571
+
572
+ HStack {
573
+ Text("Detach mode")
574
+ .font(Typo.mono(10))
575
+ .foregroundColor(Palette.textDim)
576
+ Spacer()
577
+ Picker("", selection: $prefs.mode) {
578
+ Text("Learning").tag(InteractionMode.learning)
579
+ Text("Auto").tag(InteractionMode.auto)
580
+ }
581
+ .pickerStyle(.segmented)
582
+ .labelsHidden()
583
+ .frame(width: 160)
584
+ }
585
+
586
+ Text(prefs.mode == .learning
587
+ ? "Shows keybinding hints when you detach from a tmux session."
588
+ : "Detaches sessions quietly once Lattices has done the workspace handoff.")
589
+ .font(Typo.caption(9.5))
590
+ .foregroundColor(Palette.textMuted.opacity(0.75))
591
+ }
592
+ }
593
+ }
594
+
595
+ private var inputControlsCard: some View {
596
+ shortcutSectionCard(
597
+ title: "Input Controls",
598
+ eyebrow: "Gestures & Remaps",
599
+ summary: "Mouse gestures, drag snapping, and Hyper key remaps live alongside the shortcuts they trigger."
600
+ ) {
601
+ VStack(alignment: .leading, spacing: 12) {
602
+ HStack(alignment: .top, spacing: 12) {
603
+ VStack(alignment: .leading, spacing: 5) {
604
+ Text("Drag-to-snap")
605
+ .font(Typo.monoBold(11))
606
+ .foregroundColor(Palette.text)
607
+ Text("Hold a modifier while dragging a window to reveal snap targets.")
608
+ .font(Typo.caption(10))
609
+ .foregroundColor(Palette.textMuted)
610
+ }
611
+
612
+ Spacer()
613
+
614
+ Toggle("", isOn: $prefs.dragSnapEnabled)
615
+ .toggleStyle(.switch)
616
+ .controlSize(.small)
617
+ .labelsHidden()
618
+ }
619
+
620
+ HStack {
621
+ Text("Snap modifier")
622
+ .font(Typo.mono(10))
623
+ .foregroundColor(Palette.textDim)
624
+ Spacer()
625
+ Picker("", selection: snapModifierBinding) {
626
+ ForEach(SnapModifierKey.allCases) { modifier in
627
+ Text(modifier.shortLabel).tag(modifier)
628
+ }
629
+ }
630
+ .pickerStyle(.segmented)
631
+ .labelsHidden()
632
+ .frame(width: 220)
633
+ }
634
+
635
+ cardDivider
636
+
637
+ HStack(alignment: .top, spacing: 12) {
638
+ VStack(alignment: .leading, spacing: 5) {
639
+ Text("Middle-click gestures")
640
+ .font(Typo.monoBold(11))
641
+ .foregroundColor(Palette.text)
642
+ Text("Directional mouse gestures can switch Spaces, open the Screen Map, or trigger dictation.")
643
+ .font(Typo.caption(10))
644
+ .foregroundColor(Palette.textMuted)
645
+ }
646
+
647
+ Spacer()
648
+
649
+ Toggle("", isOn: $prefs.mouseGesturesEnabled)
650
+ .toggleStyle(.switch)
651
+ .controlSize(.small)
652
+ .labelsHidden()
653
+ }
654
+
655
+ breakerStatusRow(
656
+ state: mouseGestureController.breakerState,
657
+ label: "Mouse gestures"
658
+ ) {
659
+ mouseGestureController.reArmAfterBreakerTrip()
660
+ }
661
+
662
+ cardDivider
663
+
664
+ HStack(alignment: .top, spacing: 12) {
665
+ VStack(alignment: .leading, spacing: 5) {
666
+ Text("Caps Lock as Hyper")
667
+ .font(Typo.monoBold(11))
668
+ .foregroundColor(Palette.text)
669
+ Text("Hold Caps Lock for Hyper shortcuts, tap it for Escape.")
670
+ .font(Typo.caption(10))
671
+ .foregroundColor(Palette.textMuted)
672
+ }
673
+
674
+ Spacer()
675
+
676
+ Toggle("", isOn: $prefs.keyboardRemapsEnabled)
677
+ .toggleStyle(.switch)
678
+ .controlSize(.small)
679
+ .labelsHidden()
680
+ }
681
+
682
+ breakerStatusRow(
683
+ state: keyboardRemapController.breakerState,
684
+ label: "Keyboard remaps"
685
+ ) {
686
+ keyboardRemapController.reArmAfterBreakerTrip()
687
+ }
688
+ }
689
+ }
690
+ }
691
+
692
+ private var generalContent: some View {
693
+ ScrollView {
694
+ VStack(alignment: .leading, spacing: 12) {
695
+ appUpdateCard
696
+
697
+ permissionsAssistantCard
698
+
699
+ settingsCard {
700
+ VStack(alignment: .leading, spacing: 8) {
701
+ Text("Terminal")
702
+ .font(Typo.mono(11))
703
+ .foregroundColor(Palette.text)
704
+
705
+ Picker("", selection: $prefs.terminal) {
706
+ ForEach(Terminal.installed) { t in
707
+ Text(t.rawValue).tag(t)
708
+ }
709
+ }
710
+ .pickerStyle(.segmented)
711
+ .labelsHidden()
712
+
713
+ Text("Used for attaching to tmux sessions")
714
+ .font(Typo.caption(10))
715
+ .foregroundColor(Palette.textMuted)
716
+ }
717
+ }
718
+
719
+ settingsCard {
720
+ VStack(alignment: .leading, spacing: 10) {
721
+ Text("Project discovery")
722
+ .font(Typo.mono(11))
723
+ .foregroundColor(Palette.text)
724
+
725
+ Text("Project scan root")
726
+ .font(Typo.mono(10))
727
+ .foregroundColor(Palette.textDim)
728
+
729
+ HStack(spacing: 6) {
730
+ TextField("~/dev", text: $prefs.scanRoot)
731
+ .textFieldStyle(.plain)
732
+ .font(Typo.mono(11))
733
+ .foregroundColor(Palette.text)
734
+ .padding(.horizontal, 8)
735
+ .padding(.vertical, 5)
736
+ .background(
737
+ RoundedRectangle(cornerRadius: 5)
738
+ .fill(Color.white.opacity(0.06))
739
+ .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5))
740
+ )
741
+
742
+ Button {
743
+ let panel = NSOpenPanel()
744
+ panel.canChooseDirectories = true
745
+ panel.canChooseFiles = false
746
+ panel.allowsMultipleSelection = false
747
+ if !prefs.scanRoot.isEmpty {
748
+ panel.directoryURL = URL(fileURLWithPath: prefs.scanRoot)
749
+ }
750
+ if panel.runModal() == .OK, let url = panel.url {
751
+ prefs.scanRoot = url.path
752
+ }
753
+ } label: {
754
+ Image(systemName: "folder")
755
+ .font(.system(size: 11))
756
+ .foregroundColor(Palette.textDim)
757
+ .padding(6)
758
+ .background(
759
+ RoundedRectangle(cornerRadius: 5)
760
+ .fill(Color.white.opacity(0.06))
761
+ .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5))
762
+ )
763
+ }
764
+ .buttonStyle(.plain)
765
+ }
766
+
767
+ HStack {
768
+ Text("Scans for .lattices.json project configs")
769
+ .font(Typo.caption(9))
770
+ .foregroundColor(Palette.textMuted.opacity(0.7))
771
+ Spacer()
772
+ Button {
773
+ scanner.updateRoot(prefs.scanRoot)
774
+ scanner.scan()
775
+ } label: {
776
+ Text("Rescan")
777
+ .font(Typo.monoBold(10))
778
+ .foregroundColor(Palette.text)
779
+ .padding(.horizontal, 12)
780
+ .padding(.vertical, 4)
781
+ .background(
782
+ RoundedRectangle(cornerRadius: 4)
783
+ .fill(Palette.surfaceHov)
784
+ .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
785
+ )
786
+ }
787
+ .buttonStyle(.plain)
788
+ }
789
+ }
790
+ }
791
+
792
+ interactionBehaviorCard
793
+ }
794
+ .padding(16)
795
+ .frame(maxWidth: 760, alignment: .leading)
796
+ .frame(maxWidth: .infinity, alignment: .leading)
797
+ }
798
+ }
799
+
800
+ private var permissionsContent: some View {
292
801
  ScrollView {
293
802
  VStack(alignment: .leading, spacing: 12) {
803
+ permissionsAssistantCard
804
+
294
805
  settingsCard {
295
806
  VStack(alignment: .leading, spacing: 10) {
296
807
  HStack(alignment: .center, spacing: 8) {
297
- Image(systemName: "arrow.down.circle")
808
+ Image(systemName: permChecker.allGranted ? "checkmark.shield.fill" : "exclamationmark.shield.fill")
298
809
  .font(.system(size: 11, weight: .medium))
299
- .foregroundColor(Palette.running)
300
- Text("Lattices app")
810
+ .foregroundColor(permChecker.allGranted ? Palette.running : Palette.detach)
811
+ Text("Permissions")
301
812
  .font(Typo.mono(12))
302
813
  .foregroundColor(Palette.text)
303
814
  Spacer()
304
- Text("Current v\(appUpdater.currentVersion)")
305
- .font(Typo.caption(10))
306
- .foregroundColor(Palette.textMuted)
815
+ Button {
816
+ permChecker.check()
817
+ } label: {
818
+ Image(systemName: "arrow.clockwise")
819
+ .font(.system(size: 10, weight: .semibold))
820
+ .foregroundColor(Palette.textDim)
821
+ .frame(width: 24, height: 22)
822
+ }
823
+ .buttonStyle(.plain)
824
+ .help("Refresh permission status")
825
+ }
826
+
827
+ Text("Lattices uses macOS privacy permissions for window discovery, tiling, gestures, remaps, and synthetic shortcuts.")
828
+ .font(Typo.caption(10))
829
+ .foregroundColor(Palette.textMuted)
830
+
831
+ VStack(alignment: .leading, spacing: 6) {
832
+ permissionSettingsRow(
833
+ "Accessibility",
834
+ granted: permChecker.accessibility,
835
+ detail: "Required for mouse gestures, keyboard remaps, window movement, and focusing windows."
836
+ ) {
837
+ permChecker.requestAccessibility()
838
+ }
839
+
840
+ permissionSettingsRow(
841
+ "Screen Recording",
842
+ granted: permChecker.screenRecording,
843
+ detail: "Required for reliable window titles, OCR, and Space-aware window discovery."
844
+ ) {
845
+ permChecker.requestScreenRecording()
846
+ }
847
+
848
+ permissionReviewRow(
849
+ "Automation",
850
+ detail: "Needed when Lattices sends shortcuts through System Events, including gesture-triggered dictation."
851
+ ) {
852
+ permChecker.openAutomationSettings()
853
+ }
854
+
855
+ permissionReviewRow(
856
+ "Input Monitoring",
857
+ detail: "Useful to review if global input capture or synthetic shortcut behavior starts failing."
858
+ ) {
859
+ permChecker.openInputMonitoringSettings()
860
+ }
861
+ }
862
+ }
863
+ }
864
+ }
865
+ .padding(16)
866
+ .frame(maxWidth: 760, alignment: .leading)
867
+ .frame(maxWidth: .infinity, alignment: .leading)
868
+ }
869
+ }
870
+
871
+ private var appContent: some View {
872
+ ScrollView {
873
+ VStack(alignment: .leading, spacing: 12) {
874
+ settingsCard {
875
+ VStack(alignment: .leading, spacing: 12) {
876
+ HStack(alignment: .top, spacing: 10) {
877
+ Image(systemName: LatticesRuntime.isDevBuild ? "hammer.fill" : "checkmark.seal.fill")
878
+ .font(.system(size: 13, weight: .semibold))
879
+ .foregroundColor(LatticesRuntime.isDevBuild ? Palette.detach : Palette.running)
880
+ .frame(width: 24, height: 24)
881
+
882
+ VStack(alignment: .leading, spacing: 6) {
883
+ HStack(spacing: 8) {
884
+ Text("Lattices app")
885
+ .font(Typo.mono(12))
886
+ .foregroundColor(Palette.text)
887
+ buildChannelBadge
888
+ Spacer()
889
+ }
890
+
891
+ HStack(alignment: .firstTextBaseline, spacing: 8) {
892
+ Text(appUpdater.currentDisplayVersion)
893
+ .font(Typo.heading(20))
894
+ .foregroundColor(Palette.text)
895
+ Text(LatticesRuntime.buildStatusLabel)
896
+ .font(Typo.monoBold(10))
897
+ .foregroundColor(LatticesRuntime.isDevBuild ? Palette.detach : Palette.running)
898
+ }
899
+
900
+ if let revision = LatticesRuntime.buildRevision {
901
+ Text("Build \(revision)")
902
+ .font(Typo.caption(9))
903
+ .foregroundColor(Palette.textMuted.opacity(0.8))
904
+ }
905
+ }
307
906
  }
308
907
 
309
908
  Text("Lattices can check for new signed releases and prepare the update here. You’ll confirm before the app quits and relaunches.")
@@ -364,7 +963,7 @@ struct SettingsContentView: View {
364
963
  Button {
365
964
  appUpdater.promptForUpdate()
366
965
  } label: {
367
- Text(appUpdater.isUpdating ? "Preparing" : (appUpdater.availableUpdate == nil ? "Check for Updates" : "Update to v\(appUpdater.availableUpdate?.version ?? "")"))
966
+ Text(appUpdater.isUpdating ? "Preparing..." : (appUpdater.availableUpdate == nil ? "Check for Updates" : "Update to v\(appUpdater.availableUpdate?.version ?? "")"))
368
967
  .font(Typo.monoBold(10))
369
968
  .foregroundColor(Palette.text)
370
969
  .padding(.horizontal, 12)
@@ -421,36 +1020,22 @@ struct SettingsContentView: View {
421
1020
  }
422
1021
  }
423
1022
  }
1023
+ }
1024
+ .padding(16)
1025
+ .frame(maxWidth: 760, alignment: .leading)
1026
+ .frame(maxWidth: .infinity, alignment: .leading)
1027
+ }
1028
+ }
424
1029
 
425
- // ── Terminal ──
426
- settingsCard {
427
- VStack(alignment: .leading, spacing: 8) {
428
- Text("Terminal")
429
- .font(Typo.mono(11))
430
- .foregroundColor(Palette.text)
431
-
432
- Picker("", selection: $prefs.terminal) {
433
- ForEach(Terminal.installed) { t in
434
- Text(t.rawValue).tag(t)
435
- }
436
- }
437
- .pickerStyle(.segmented)
438
- .labelsHidden()
439
-
440
- Text("Used for attaching to tmux sessions")
441
- .font(Typo.caption(10))
442
- .foregroundColor(Palette.textMuted)
443
- }
444
- }
445
-
446
- // ── tmux ──
1030
+ private var behaviorContent: some View {
1031
+ ScrollView {
1032
+ VStack(alignment: .leading, spacing: 12) {
447
1033
  settingsCard {
448
1034
  VStack(alignment: .leading, spacing: 10) {
449
1035
  Text("tmux")
450
1036
  .font(Typo.mono(11))
451
1037
  .foregroundColor(Palette.text)
452
1038
 
453
- // Mode
454
1039
  HStack {
455
1040
  Text("Detach mode")
456
1041
  .font(Typo.mono(10))
@@ -470,74 +1055,6 @@ struct SettingsContentView: View {
470
1055
  : "Detaches sessions silently")
471
1056
  .font(Typo.caption(9))
472
1057
  .foregroundColor(Palette.textMuted.opacity(0.7))
473
-
474
- cardDivider
475
-
476
- // Project scan root
477
- Text("Project scan root")
478
- .font(Typo.mono(10))
479
- .foregroundColor(Palette.textDim)
480
-
481
- HStack(spacing: 6) {
482
- TextField("~/dev", text: $prefs.scanRoot)
483
- .textFieldStyle(.plain)
484
- .font(Typo.mono(11))
485
- .foregroundColor(Palette.text)
486
- .padding(.horizontal, 8)
487
- .padding(.vertical, 5)
488
- .background(
489
- RoundedRectangle(cornerRadius: 5)
490
- .fill(Color.white.opacity(0.06))
491
- .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5))
492
- )
493
-
494
- Button {
495
- let panel = NSOpenPanel()
496
- panel.canChooseDirectories = true
497
- panel.canChooseFiles = false
498
- panel.allowsMultipleSelection = false
499
- if !prefs.scanRoot.isEmpty {
500
- panel.directoryURL = URL(fileURLWithPath: prefs.scanRoot)
501
- }
502
- if panel.runModal() == .OK, let url = panel.url {
503
- prefs.scanRoot = url.path
504
- }
505
- } label: {
506
- Image(systemName: "folder")
507
- .font(.system(size: 11))
508
- .foregroundColor(Palette.textDim)
509
- .padding(6)
510
- .background(
511
- RoundedRectangle(cornerRadius: 5)
512
- .fill(Color.white.opacity(0.06))
513
- .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5))
514
- )
515
- }
516
- .buttonStyle(.plain)
517
- }
518
-
519
- HStack {
520
- Text("Scans for .lattices.json project configs")
521
- .font(Typo.caption(9))
522
- .foregroundColor(Palette.textMuted.opacity(0.7))
523
- Spacer()
524
- Button {
525
- scanner.updateRoot(prefs.scanRoot)
526
- scanner.scan()
527
- } label: {
528
- Text("Rescan")
529
- .font(Typo.monoBold(10))
530
- .foregroundColor(Palette.text)
531
- .padding(.horizontal, 12)
532
- .padding(.vertical, 4)
533
- .background(
534
- RoundedRectangle(cornerRadius: 4)
535
- .fill(Palette.surfaceHov)
536
- .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
537
- )
538
- }
539
- .buttonStyle(.plain)
540
- }
541
1058
  }
542
1059
  }
543
1060
 
@@ -626,6 +1143,13 @@ struct SettingsContentView: View {
626
1143
  }
627
1144
  }
628
1145
 
1146
+ breakerStatusRow(
1147
+ state: mouseGestureController.breakerState,
1148
+ label: "Mouse gestures"
1149
+ ) {
1150
+ mouseGestureController.reArmAfterBreakerTrip()
1151
+ }
1152
+
629
1153
  HStack(spacing: 8) {
630
1154
  Button {
631
1155
  mouseShortcutStore.openConfiguration()
@@ -723,6 +1247,13 @@ struct SettingsContentView: View {
723
1247
  }
724
1248
  }
725
1249
 
1250
+ breakerStatusRow(
1251
+ state: keyboardRemapController.breakerState,
1252
+ label: "Keyboard remaps"
1253
+ ) {
1254
+ keyboardRemapController.reArmAfterBreakerTrip()
1255
+ }
1256
+
726
1257
  HStack(spacing: 8) {
727
1258
  Button {
728
1259
  keyboardRemapStore.openConfiguration()
@@ -772,35 +1303,7 @@ struct SettingsContentView: View {
772
1303
  VStack(alignment: .leading, spacing: 12) {
773
1304
  companionBridgeOverviewCard
774
1305
  companionTrustedDevicesCard
775
-
776
- settingsCard {
777
- VStack(alignment: .leading, spacing: 10) {
778
- Text("Input")
779
- .font(Typo.mono(11))
780
- .foregroundColor(Palette.text)
781
-
782
- HStack(alignment: .top, spacing: 12) {
783
- VStack(alignment: .leading, spacing: 4) {
784
- Text("Trackpad proxy")
785
- .font(Typo.mono(10))
786
- .foregroundColor(Palette.textDim)
787
- Text("Allow paired companions with the input.trackpad grant to move the Mac pointer through the encrypted bridge.")
788
- .font(Typo.caption(9.5))
789
- .foregroundColor(Palette.textMuted.opacity(0.75))
790
- .fixedSize(horizontal: false, vertical: true)
791
- }
792
-
793
- Spacer()
794
-
795
- Toggle("", isOn: $prefs.companionTrackpadEnabled)
796
- .toggleStyle(.switch)
797
- .controlSize(.small)
798
- .labelsHidden()
799
- .disabled(!prefs.companionBridgeEnabled)
800
- .opacity(prefs.companionBridgeEnabled ? 1 : 0.45)
801
- }
802
- }
803
- }
1306
+ companionCockpitCard
804
1307
  }
805
1308
  .padding(16)
806
1309
  .frame(maxWidth: 760, alignment: .leading)
@@ -1291,6 +1794,20 @@ struct SettingsContentView: View {
1291
1794
  }
1292
1795
  }
1293
1796
 
1797
+ private var buildChannelBadge: some View {
1798
+ let tint = LatticesRuntime.isDevBuild ? Palette.detach : Palette.running
1799
+
1800
+ return Text(LatticesRuntime.buildChannelLabel)
1801
+ .font(Typo.monoBold(9))
1802
+ .foregroundColor(tint)
1803
+ .padding(.horizontal, 6)
1804
+ .padding(.vertical, 3)
1805
+ .background(
1806
+ Capsule()
1807
+ .fill(tint.opacity(0.12))
1808
+ )
1809
+ }
1810
+
1294
1811
  // MARK: - Search & OCR
1295
1812
 
1296
1813
  private func ocrNumField(_ value: Binding<Double>, width: CGFloat = 50) -> some View {
@@ -1688,6 +2205,149 @@ struct SettingsContentView: View {
1688
2205
  .padding(.vertical, 3)
1689
2206
  }
1690
2207
 
2208
+ private func permissionSettingsRow(
2209
+ _ title: String,
2210
+ granted: Bool,
2211
+ detail: String,
2212
+ action: @escaping () -> Void
2213
+ ) -> some View {
2214
+ Button {
2215
+ if granted {
2216
+ permChecker.check()
2217
+ } else {
2218
+ action()
2219
+ }
2220
+ } label: {
2221
+ permissionRowContent(
2222
+ title,
2223
+ status: granted ? "granted" : "not set",
2224
+ statusColor: granted ? Palette.running : Palette.detach,
2225
+ icon: granted ? "checkmark.circle.fill" : "exclamationmark.circle.fill",
2226
+ iconColor: granted ? Palette.running : Palette.detach,
2227
+ detail: detail,
2228
+ showsExternalLink: !granted
2229
+ )
2230
+ }
2231
+ .buttonStyle(.plain)
2232
+ .help(granted ? "Refresh permission status" : "Open macOS permission flow")
2233
+ }
2234
+
2235
+ private func permissionReviewRow(
2236
+ _ title: String,
2237
+ detail: String,
2238
+ action: @escaping () -> Void
2239
+ ) -> some View {
2240
+ Button(action: action) {
2241
+ permissionRowContent(
2242
+ title,
2243
+ status: "review",
2244
+ statusColor: Palette.textDim,
2245
+ icon: "gearshape.2.fill",
2246
+ iconColor: Palette.textDim,
2247
+ detail: detail,
2248
+ showsExternalLink: true
2249
+ )
2250
+ }
2251
+ .buttonStyle(.plain)
2252
+ .help("Open macOS Privacy & Security settings")
2253
+ }
2254
+
2255
+ private func permissionRowContent(
2256
+ _ title: String,
2257
+ status: String,
2258
+ statusColor: Color,
2259
+ icon: String,
2260
+ iconColor: Color,
2261
+ detail: String,
2262
+ showsExternalLink: Bool
2263
+ ) -> some View {
2264
+ HStack(alignment: .top, spacing: 8) {
2265
+ Image(systemName: icon)
2266
+ .font(.system(size: 10, weight: .semibold))
2267
+ .foregroundColor(iconColor)
2268
+ .frame(width: 12, height: 16)
2269
+
2270
+ VStack(alignment: .leading, spacing: 2) {
2271
+ HStack(spacing: 6) {
2272
+ Text(title)
2273
+ .font(Typo.mono(10))
2274
+ .foregroundColor(Palette.text)
2275
+ Text(status)
2276
+ .font(Typo.mono(9))
2277
+ .foregroundColor(statusColor)
2278
+ Spacer()
2279
+ if showsExternalLink {
2280
+ Image(systemName: "arrow.up.forward.square")
2281
+ .font(.system(size: 9))
2282
+ .foregroundColor(Palette.textMuted)
2283
+ }
2284
+ }
2285
+
2286
+ Text(detail)
2287
+ .font(Typo.caption(9))
2288
+ .foregroundColor(Palette.textMuted.opacity(0.75))
2289
+ .fixedSize(horizontal: false, vertical: true)
2290
+ }
2291
+ }
2292
+ .padding(.horizontal, 8)
2293
+ .padding(.vertical, 7)
2294
+ .background(
2295
+ RoundedRectangle(cornerRadius: 5)
2296
+ .fill(Palette.surfaceHov.opacity(status == "not set" ? 0.75 : 0.35))
2297
+ .overlay(
2298
+ RoundedRectangle(cornerRadius: 5)
2299
+ .strokeBorder(status == "not set" ? Palette.detach.opacity(0.22) : Palette.borderLit.opacity(0.6), lineWidth: 0.5)
2300
+ )
2301
+ )
2302
+ }
2303
+
2304
+ @ViewBuilder
2305
+ private func breakerStatusRow(
2306
+ state: EventTapBreaker.State,
2307
+ label: String,
2308
+ onReArm: @escaping () -> Void
2309
+ ) -> some View {
2310
+ switch state {
2311
+ case .armed:
2312
+ EmptyView()
2313
+ case .paused(let cooldownSec):
2314
+ HStack(spacing: 8) {
2315
+ Circle()
2316
+ .fill(Color.orange)
2317
+ .frame(width: 6, height: 6)
2318
+ Text("\(label) paused — \(cooldownSec)s cooldown")
2319
+ .font(Typo.caption(9))
2320
+ .foregroundColor(Palette.textMuted)
2321
+ Spacer()
2322
+ }
2323
+ case .disabled:
2324
+ HStack(spacing: 8) {
2325
+ Circle()
2326
+ .fill(Color.red)
2327
+ .frame(width: 6, height: 6)
2328
+ Text("\(label) disabled — tap callback exceeded OS budget repeatedly")
2329
+ .font(Typo.caption(9))
2330
+ .foregroundColor(Palette.textMuted)
2331
+ Spacer()
2332
+ Button {
2333
+ onReArm()
2334
+ } label: {
2335
+ Text("Re-enable")
2336
+ .font(Typo.monoBold(10))
2337
+ .foregroundColor(Palette.text)
2338
+ .padding(.horizontal, 10)
2339
+ .padding(.vertical, 3)
2340
+ .background(
2341
+ RoundedRectangle(cornerRadius: 4)
2342
+ .fill(Palette.surfaceHov)
2343
+ .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
2344
+ )
2345
+ }
2346
+ .buttonStyle(.plain)
2347
+ }
2348
+ }
2349
+ }
2350
+
1691
2351
  // MARK: - Shortcuts
1692
2352
 
1693
2353
  private var shortcutsContent: some View {
@@ -1707,9 +2367,8 @@ struct SettingsContentView: View {
1707
2367
  ScrollView {
1708
2368
  VStack(alignment: .leading, spacing: 0) {
1709
2369
  VStack(alignment: .leading, spacing: 16) {
1710
- companionCockpitCard
1711
-
1712
2370
  shortcutsOverviewCard
2371
+ inputControlsCard
1713
2372
 
1714
2373
  LazyVGrid(columns: sectionColumns, alignment: .leading, spacing: 16) {
1715
2374
  shortcutsAppCard