@lattices/cli 0.4.14 → 0.5.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 (180) hide show
  1. package/README.md +5 -7
  2. package/apps/mac/Info.plist +2 -2
  3. package/apps/mac/Lattices.app/Contents/Info.plist +4 -12
  4. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/bin/lattices-app.ts +110 -17
  6. package/bin/lattices-build +125 -0
  7. package/bin/lattices-dev +89 -16
  8. package/bin/lattices.ts +977 -16
  9. package/docs/agents.md +81 -4
  10. package/docs/ai-chat-ux-review.md +416 -0
  11. package/docs/api.md +135 -3
  12. package/docs/app.md +30 -8
  13. package/docs/config.md +4 -0
  14. package/docs/mouse-gestures.md +60 -1
  15. package/docs/proposals/LAT-004-interactive-overlay-actors.md +1 -1
  16. package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
  17. package/docs/proposals/LAT-006-mira-in-lattices.md +553 -0
  18. package/docs/reference/dewey.config.ts +2 -2
  19. package/docs/release.md +171 -0
  20. package/docs/repo-structure.md +4 -5
  21. package/docs/voice.md +11 -27
  22. package/package.json +9 -10
  23. package/apps/mac/Package.swift +0 -27
  24. package/apps/mac/Sources/AppShell/App.swift +0 -26
  25. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
  26. package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
  27. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
  28. package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
  29. package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
  30. package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
  31. package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
  32. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
  33. package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
  34. package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
  35. package/apps/mac/Sources/AppShell/MainView.swift +0 -847
  36. package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
  37. package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
  38. package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
  39. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
  40. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
  41. package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
  42. package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
  43. package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
  44. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
  45. package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
  46. package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
  47. package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
  48. package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
  49. package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
  50. package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
  51. package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
  52. package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
  53. package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
  54. package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
  55. package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
  56. package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
  57. package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
  58. package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
  59. package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
  60. package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
  61. package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
  62. package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
  63. package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
  64. package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
  65. package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
  66. package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
  67. package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
  68. package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
  69. package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
  70. package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
  71. package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
  72. package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
  73. package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
  74. package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
  75. package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
  76. package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
  77. package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
  78. package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
  79. package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
  80. package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
  81. package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
  82. package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
  83. package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
  84. package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
  85. package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
  86. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
  87. package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
  88. package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
  89. package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
  90. package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
  91. package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
  92. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
  93. package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
  94. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
  95. package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
  96. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
  97. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
  98. package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
  99. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
  100. package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
  101. package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
  102. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
  103. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
  104. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
  105. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
  106. package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
  107. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
  108. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
  109. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
  110. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
  111. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
  112. package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
  113. package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
  114. package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
  115. package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
  116. package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
  117. package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
  118. package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
  119. package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
  120. package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
  121. package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
  122. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
  123. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
  124. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
  125. package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
  126. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
  127. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
  128. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
  129. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
  130. package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
  131. package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
  132. package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
  133. package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
  134. package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
  135. package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
  136. package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
  137. package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
  138. package/apps/mac/Sources/Core/System/Capability.swift +0 -79
  139. package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
  140. package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
  141. package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
  142. package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
  143. package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
  144. package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
  145. package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
  146. package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
  147. package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
  148. package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
  149. package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
  150. package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
  151. package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
  152. package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
  153. package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
  154. package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
  155. package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
  156. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
  157. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
  158. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
  159. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
  160. package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
  161. package/apps/mac/Sources/UI/ActionRow.swift +0 -78
  162. package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
  163. package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
  164. package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
  165. package/apps/mac/Sources/UI/Theme.swift +0 -164
  166. package/apps/mac/Tests/StageDragTests.swift +0 -333
  167. package/apps/mac/Tests/StageJoinTests.swift +0 -313
  168. package/apps/mac/Tests/StageManagerTests.swift +0 -280
  169. package/apps/mac/Tests/StageTileTests.swift +0 -353
  170. package/swift/Package.swift +0 -20
  171. package/swift/Sources/DeckKit/DeckAction.swift +0 -51
  172. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
  173. package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
  174. package/swift/Sources/DeckKit/DeckHost.swift +0 -7
  175. package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
  176. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
  177. package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
  178. package/swift/Sources/DeckKit/DeckValue.swift +0 -93
  179. package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
  180. package/swift/Tests/DeckKitTests/DeckKitTests.swift +0 -286
@@ -1,3163 +0,0 @@
1
- import DeckKit
2
- import SwiftUI
3
-
4
- /// Settings content with internal General / Shortcuts tabs.
5
- /// Can also render the Docs page when `page == .docs`.
6
- struct SettingsContentView: View {
7
- private enum SettingsSection: String, CaseIterable, Identifiable {
8
- case general
9
- case shortcuts
10
- case ai
11
- case search
12
- case companion
13
-
14
- var id: String { rawValue }
15
-
16
- var title: String {
17
- switch self {
18
- case .general: return "General"
19
- case .shortcuts: return "Shortcuts"
20
- case .ai: return "AI"
21
- case .search: return "Search & OCR"
22
- case .companion: return "LATS iOS Companion"
23
- }
24
- }
25
-
26
- var icon: String {
27
- switch self {
28
- case .general: return "slider.horizontal.3"
29
- case .shortcuts: return "command"
30
- case .ai: return "sparkles"
31
- case .search: return "text.viewfinder"
32
- case .companion: return "ipad.and.iphone"
33
- }
34
- }
35
-
36
- var eyebrow: String {
37
- switch self {
38
- case .general: return "Workspace"
39
- case .shortcuts: return "Controls"
40
- case .ai: return "Agents"
41
- case .search: return "Indexing"
42
- case .companion: return "Local Bridge"
43
- }
44
- }
45
-
46
- var summary: String {
47
- switch self {
48
- case .general:
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
- case .ai:
53
- return "Claude CLI detection plus advisor model and spending controls."
54
- case .search:
55
- return "OCR cadence, quality, and recent capture visibility."
56
- case .companion:
57
- return "Local-network pairing, trusted iPad devices, and bridge security."
58
- }
59
- }
60
- }
61
-
62
- var page: AppPage = .settings
63
- @ObservedObject var prefs: Preferences
64
- @ObservedObject var scanner: ProjectScanner
65
- @ObservedObject var hotkeyStore: HotkeyStore = .shared
66
- @ObservedObject var workspaceManager: WorkspaceManager = .shared
67
- @ObservedObject var appUpdater: AppUpdater = .shared
68
- @ObservedObject var mouseShortcutStore: MouseShortcutStore = .shared
69
- @ObservedObject var keyboardRemapStore: KeyboardRemapStore = .shared
70
- @ObservedObject var permChecker: PermissionChecker = .shared
71
- @ObservedObject var mouseGestureController: MouseGestureController = .shared
72
- @ObservedObject var keyboardRemapController: KeyboardRemapController = .shared
73
- var onBack: (() -> Void)? = nil
74
-
75
- @State private var selectedTab: SettingsSection = .general
76
-
77
- var body: some View {
78
- VStack(spacing: 0) {
79
- // Back bar
80
- backBar
81
-
82
- if page == .docs {
83
- docsContent
84
- } else {
85
- settingsBody
86
- }
87
- }
88
- .frame(maxWidth: .infinity, maxHeight: .infinity)
89
- .clipped()
90
- .background(PanelBackground())
91
- .onAppear {
92
- permChecker.check()
93
- if page == .companionSettings {
94
- selectedTab = .companion
95
- }
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
- }
103
- }
104
-
105
- // MARK: - Back Bar
106
-
107
- private var currentTabLabel: String {
108
- page == .docs ? "Docs" : selectedTab.title
109
- }
110
-
111
- private var snapModifierBinding: Binding<SnapModifierKey> {
112
- Binding(
113
- get: { workspaceManager.snapZonesConfig.modifier ?? .command },
114
- set: { workspaceManager.updateSnapModifier($0) }
115
- )
116
- }
117
-
118
- private var backBar: some View {
119
- VStack(spacing: 0) {
120
- HStack(spacing: 8) {
121
- if let onBack {
122
- Button {
123
- onBack()
124
- } label: {
125
- Image(systemName: "chevron.left")
126
- .font(.system(size: 10, weight: .semibold))
127
- .foregroundColor(Palette.textMuted)
128
- }
129
- .buttonStyle(.plain)
130
- .onHover { h in if h { NSCursor.pointingHand.push() } else { NSCursor.pop() } }
131
- }
132
-
133
- Text(page == .docs ? "Docs" : currentTabLabel)
134
- .font(Typo.heading(13))
135
- .foregroundColor(Palette.text)
136
-
137
- Spacer()
138
- }
139
- .padding(.horizontal, 16)
140
- .padding(.vertical, 8)
141
-
142
- Rectangle().fill(Palette.border).frame(height: 0.5)
143
- }
144
- }
145
-
146
- // MARK: - Settings Body
147
-
148
- private var settingsBody: some View {
149
- HStack(spacing: 0) {
150
- settingsSidebar
151
- .frame(width: 190, alignment: .top)
152
-
153
- Rectangle()
154
- .fill(Palette.border)
155
- .frame(width: 0.5)
156
- .frame(maxHeight: .infinity)
157
-
158
- VStack(spacing: 0) {
159
- settingsSectionHero(selectedTab)
160
-
161
- Rectangle().fill(Palette.border).frame(height: 0.5)
162
-
163
- selectedSectionContent
164
- }
165
- }
166
- }
167
-
168
- private var settingsSidebar: some View {
169
- VStack(alignment: .leading, spacing: 14) {
170
- VStack(alignment: .leading, spacing: 6) {
171
- Text("SETTINGS")
172
- .font(Typo.pixel(14))
173
- .foregroundColor(Palette.textDim)
174
- .tracking(1)
175
- Text("Tune how Lattices launches workspaces, listens for commands, and navigates the desktop.")
176
- .font(Typo.caption(11))
177
- .foregroundColor(Palette.textMuted)
178
- .fixedSize(horizontal: false, vertical: true)
179
- }
180
-
181
- VStack(spacing: 6) {
182
- ForEach(SettingsSection.allCases) { section in
183
- settingsTab(section)
184
- }
185
- }
186
-
187
- Spacer(minLength: 0)
188
- }
189
- .padding(16)
190
- }
191
-
192
- private func settingsTab(_ section: SettingsSection) -> some View {
193
- let active = selectedTab == section
194
- return Button {
195
- selectedTab = section
196
- } label: {
197
- HStack(alignment: .top, spacing: 10) {
198
- Image(systemName: section.icon)
199
- .font(.system(size: 11, weight: .semibold))
200
- .foregroundColor(active ? Palette.text : Palette.textMuted)
201
- .frame(width: 16, height: 18, alignment: .center)
202
-
203
- VStack(alignment: .leading, spacing: 3) {
204
- Text(section.title)
205
- .font(Typo.mono(11))
206
- .foregroundColor(active ? Palette.text : Palette.textMuted)
207
-
208
- Text(section.eyebrow)
209
- .font(Typo.caption(9))
210
- .foregroundColor(Palette.textMuted.opacity(active ? 0.85 : 0.62))
211
- }
212
-
213
- Spacer(minLength: 0)
214
- }
215
- .padding(.horizontal, 10)
216
- .padding(.vertical, 8)
217
- .contentShape(RoundedRectangle(cornerRadius: 8))
218
- .background(
219
- ZStack {
220
- if active {
221
- RoundedRectangle(cornerRadius: 8)
222
- .fill(Color.white.opacity(0.06))
223
- RoundedRectangle(cornerRadius: 8)
224
- .strokeBorder(
225
- LinearGradient(
226
- colors: [Color.white.opacity(0.12), Color.white.opacity(0.04)],
227
- startPoint: .top,
228
- endPoint: .bottom
229
- ),
230
- lineWidth: 0.5
231
- )
232
- }
233
- }
234
- )
235
- }
236
- .buttonStyle(.plain)
237
- }
238
-
239
- private func settingsSectionHero(_ section: SettingsSection) -> some View {
240
- VStack(alignment: .leading, spacing: 8) {
241
- Text(section.eyebrow.uppercased())
242
- .font(Typo.pixel(14))
243
- .foregroundColor(Palette.textDim)
244
- .tracking(1)
245
-
246
- Text(section.title)
247
- .font(Typo.heading(16))
248
- .foregroundColor(Palette.text)
249
-
250
- Text(section.summary)
251
- .font(Typo.caption(11))
252
- .foregroundColor(Palette.textMuted)
253
- .fixedSize(horizontal: false, vertical: true)
254
- }
255
- .frame(maxWidth: .infinity, alignment: .leading)
256
- .padding(.horizontal, 20)
257
- .padding(.vertical, 14)
258
- .background(Palette.bg)
259
- }
260
-
261
- @ViewBuilder
262
- private var selectedSectionContent: some View {
263
- switch selectedTab {
264
- case .general:
265
- generalContent
266
- case .shortcuts:
267
- shortcutsContent
268
- case .ai:
269
- aiContent
270
- case .search:
271
- searchOcrContent
272
- case .companion:
273
- companionContent
274
- }
275
- }
276
-
277
- // MARK: - Sticky section header
278
-
279
- private func stickyHeader(_ title: String) -> some View {
280
- VStack(spacing: 0) {
281
- HStack {
282
- Text(title.uppercased())
283
- .font(Typo.pixel(14))
284
- .foregroundColor(Palette.textDim)
285
- .tracking(1)
286
- Spacer()
287
- }
288
- .padding(.horizontal, 20)
289
- .padding(.vertical, 8)
290
- .background(Palette.bg)
291
-
292
- Rectangle()
293
- .fill(Palette.border)
294
- .frame(height: 0.5)
295
- }
296
- }
297
-
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 {
801
- ScrollView {
802
- VStack(alignment: .leading, spacing: 12) {
803
- permissionsAssistantCard
804
-
805
- settingsCard {
806
- VStack(alignment: .leading, spacing: 10) {
807
- HStack(alignment: .center, spacing: 8) {
808
- Image(systemName: permChecker.allGranted ? "checkmark.shield.fill" : "exclamationmark.shield.fill")
809
- .font(.system(size: 11, weight: .medium))
810
- .foregroundColor(permChecker.allGranted ? Palette.running : Palette.detach)
811
- Text("Permissions")
812
- .font(Typo.mono(12))
813
- .foregroundColor(Palette.text)
814
- Spacer()
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
- }
906
- }
907
-
908
- Text("Lattices can check for new signed releases and prepare the update here. You’ll confirm before the app quits and relaunches.")
909
- .font(Typo.caption(10))
910
- .foregroundColor(Palette.textMuted)
911
-
912
- if let update = appUpdater.availableUpdate {
913
- VStack(alignment: .leading, spacing: 6) {
914
- HStack(spacing: 6) {
915
- Image(systemName: "gift.fill")
916
- .font(.system(size: 10, weight: .semibold))
917
- .foregroundColor(Palette.detach)
918
- Text("New version v\(update.version) is ready")
919
- .font(Typo.monoBold(10))
920
- .foregroundColor(Palette.detach)
921
- }
922
-
923
- if !update.releaseNotes.isEmpty {
924
- Text(String(update.releaseNotes.prefix(180)) + (update.releaseNotes.count > 180 ? "..." : ""))
925
- .font(Typo.caption(9))
926
- .foregroundColor(Palette.textMuted)
927
- .lineLimit(3)
928
- }
929
- }
930
- .padding(8)
931
- .background(
932
- RoundedRectangle(cornerRadius: 5)
933
- .fill(Palette.surfaceHov.opacity(0.65))
934
- .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Palette.detach.opacity(0.35), lineWidth: 0.5))
935
- )
936
- } else if appUpdater.isChecking {
937
- Text("Checking for updates...")
938
- .font(Typo.caption(9))
939
- .foregroundColor(Palette.textMuted)
940
- } else if let error = appUpdater.lastError {
941
- Text(error)
942
- .font(Typo.caption(9))
943
- .foregroundColor(Palette.detach.opacity(0.9))
944
- } else if let checked = appUpdater.lastChecked {
945
- Text("Last checked \(checked, style: .relative)")
946
- .font(Typo.caption(9))
947
- .foregroundColor(Palette.textMuted.opacity(0.8))
948
- }
949
-
950
- if let status = appUpdater.statusMessage {
951
- Text(status)
952
- .font(Typo.caption(9))
953
- .foregroundColor(Palette.running.opacity(0.85))
954
- }
955
-
956
- if let reason = appUpdater.unavailableReason {
957
- Text(reason)
958
- .font(Typo.caption(9))
959
- .foregroundColor(Palette.detach.opacity(0.9))
960
- }
961
-
962
- HStack(spacing: 10) {
963
- Button {
964
- appUpdater.promptForUpdate()
965
- } label: {
966
- Text(appUpdater.isUpdating ? "Preparing..." : (appUpdater.availableUpdate == nil ? "Check for Updates" : "Update to v\(appUpdater.availableUpdate?.version ?? "")"))
967
- .font(Typo.monoBold(10))
968
- .foregroundColor(Palette.text)
969
- .padding(.horizontal, 12)
970
- .padding(.vertical, 5)
971
- .background(
972
- RoundedRectangle(cornerRadius: 4)
973
- .fill(Palette.surfaceHov)
974
- .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
975
- )
976
- }
977
- .buttonStyle(.plain)
978
- .disabled(appUpdater.isUpdating)
979
-
980
- Button {
981
- Task { await appUpdater.check() }
982
- } label: {
983
- Text(appUpdater.isChecking ? "Checking..." : "Check Now")
984
- .font(Typo.caption(9))
985
- .foregroundColor(Palette.textMuted.opacity(0.9))
986
- }
987
- .buttonStyle(.plain)
988
- .disabled(appUpdater.isChecking)
989
-
990
- Toggle("Auto", isOn: $appUpdater.autoCheckEnabled)
991
- .font(Typo.caption(9))
992
- .toggleStyle(.checkbox)
993
- .foregroundColor(Palette.textMuted.opacity(0.9))
994
-
995
- if appUpdater.availableUpdate != nil {
996
- Button {
997
- appUpdater.viewCurrentRelease()
998
- } label: {
999
- Text("Release Notes")
1000
- .font(Typo.caption(9))
1001
- .foregroundColor(Palette.textMuted.opacity(0.9))
1002
- }
1003
- .buttonStyle(.plain)
1004
-
1005
- Button {
1006
- appUpdater.skipCurrentUpdate()
1007
- } label: {
1008
- Text("Skip")
1009
- .font(Typo.caption(9))
1010
- .foregroundColor(Palette.textMuted.opacity(0.75))
1011
- }
1012
- .buttonStyle(.plain)
1013
- }
1014
-
1015
- Spacer()
1016
-
1017
- Text("CLI: `lattices app update`")
1018
- .font(Typo.caption(9))
1019
- .foregroundColor(Palette.textMuted.opacity(0.8))
1020
- }
1021
- }
1022
- }
1023
- }
1024
- .padding(16)
1025
- .frame(maxWidth: 760, alignment: .leading)
1026
- .frame(maxWidth: .infinity, alignment: .leading)
1027
- }
1028
- }
1029
-
1030
- private var behaviorContent: some View {
1031
- ScrollView {
1032
- VStack(alignment: .leading, spacing: 12) {
1033
- settingsCard {
1034
- VStack(alignment: .leading, spacing: 10) {
1035
- Text("tmux")
1036
- .font(Typo.mono(11))
1037
- .foregroundColor(Palette.text)
1038
-
1039
- HStack {
1040
- Text("Detach mode")
1041
- .font(Typo.mono(10))
1042
- .foregroundColor(Palette.textDim)
1043
- Spacer()
1044
- Picker("", selection: $prefs.mode) {
1045
- Text("Learning").tag(InteractionMode.learning)
1046
- Text("Auto").tag(InteractionMode.auto)
1047
- }
1048
- .pickerStyle(.segmented)
1049
- .labelsHidden()
1050
- .frame(width: 160)
1051
- }
1052
-
1053
- Text(prefs.mode == .learning
1054
- ? "Shows keybinding hints on detach"
1055
- : "Detaches sessions silently")
1056
- .font(Typo.caption(9))
1057
- .foregroundColor(Palette.textMuted.opacity(0.7))
1058
- }
1059
- }
1060
-
1061
- settingsCard {
1062
- VStack(alignment: .leading, spacing: 10) {
1063
- Text("Window drag snap")
1064
- .font(Typo.mono(11))
1065
- .foregroundColor(Palette.text)
1066
-
1067
- HStack {
1068
- Text("Drag-to-snap")
1069
- .font(Typo.mono(10))
1070
- .foregroundColor(Palette.textDim)
1071
- Spacer()
1072
- Toggle("", isOn: $prefs.dragSnapEnabled)
1073
- .toggleStyle(.switch)
1074
- .controlSize(.small)
1075
- .labelsHidden()
1076
- }
1077
-
1078
- HStack {
1079
- Text("Snap modifier")
1080
- .font(Typo.mono(10))
1081
- .foregroundColor(Palette.textDim)
1082
- Spacer()
1083
- Picker("", selection: snapModifierBinding) {
1084
- ForEach(SnapModifierKey.allCases) { modifier in
1085
- Text(modifier.shortLabel).tag(modifier)
1086
- }
1087
- }
1088
- .pickerStyle(.segmented)
1089
- .labelsHidden()
1090
- .frame(width: 220)
1091
- }
1092
-
1093
- Text("Dragging stays normal until you hold \(snapModifierBinding.wrappedValue.label). While that key is down, Lattices reveals snap targets and a live preview for the window you’re moving.")
1094
- .font(Typo.caption(9))
1095
- .foregroundColor(Palette.textMuted.opacity(0.7))
1096
-
1097
- cardDivider
1098
-
1099
- Text("Advanced landing-zone rules still live in ~/.lattices/snap-zones.json. Modifier changes here take effect on the next drag.")
1100
- .font(Typo.caption(9))
1101
- .foregroundColor(Palette.textMuted.opacity(0.7))
1102
- }
1103
- }
1104
-
1105
- settingsCard {
1106
- VStack(alignment: .leading, spacing: 10) {
1107
- Text("Mouse gestures")
1108
- .font(Typo.mono(11))
1109
- .foregroundColor(Palette.text)
1110
-
1111
- HStack {
1112
- Text("Middle-click gestures")
1113
- .font(Typo.mono(10))
1114
- .foregroundColor(Palette.textDim)
1115
- Spacer()
1116
- Toggle("", isOn: $prefs.mouseGesturesEnabled)
1117
- .toggleStyle(.switch)
1118
- .controlSize(.small)
1119
- .labelsHidden()
1120
- }
1121
-
1122
- Text("Rules live in ~/.lattices/mouse-shortcuts.json. The current defaults preserve the working setup: middle-click drag left/right switches Spaces and drag down opens the Screen Map overview.")
1123
- .font(Typo.caption(9))
1124
- .foregroundColor(Palette.textMuted.opacity(0.7))
1125
-
1126
- cardDivider
1127
-
1128
- VStack(alignment: .leading, spacing: 6) {
1129
- Text("Active drag mappings")
1130
- .font(Typo.mono(10))
1131
- .foregroundColor(Palette.textDim)
1132
-
1133
- ForEach(mouseShortcutStore.summaryLines.prefix(4), id: \.self) { line in
1134
- Text(line)
1135
- .font(Typo.caption(9))
1136
- .foregroundColor(Palette.textMuted.opacity(0.78))
1137
- }
1138
-
1139
- if mouseShortcutStore.summaryLines.isEmpty {
1140
- Text("No active mappings")
1141
- .font(Typo.caption(9))
1142
- .foregroundColor(Palette.textMuted.opacity(0.6))
1143
- }
1144
- }
1145
-
1146
- breakerStatusRow(
1147
- state: mouseGestureController.breakerState,
1148
- label: "Mouse gestures"
1149
- ) {
1150
- mouseGestureController.reArmAfterBreakerTrip()
1151
- }
1152
-
1153
- HStack(spacing: 8) {
1154
- Button {
1155
- mouseShortcutStore.openConfiguration()
1156
- } label: {
1157
- Text("Configure...")
1158
- .font(Typo.monoBold(10))
1159
- .foregroundColor(Palette.text)
1160
- .padding(.horizontal, 12)
1161
- .padding(.vertical, 4)
1162
- .background(
1163
- RoundedRectangle(cornerRadius: 4)
1164
- .fill(Palette.surfaceHov)
1165
- .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
1166
- )
1167
- }
1168
- .buttonStyle(.plain)
1169
-
1170
- Button {
1171
- MouseInputEventViewer.shared.show()
1172
- } label: {
1173
- Text("Open Event Viewer")
1174
- .font(Typo.monoBold(10))
1175
- .foregroundColor(Palette.text)
1176
- .padding(.horizontal, 12)
1177
- .padding(.vertical, 4)
1178
- .background(
1179
- RoundedRectangle(cornerRadius: 4)
1180
- .fill(Palette.surfaceHov)
1181
- .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
1182
- )
1183
- }
1184
- .buttonStyle(.plain)
1185
-
1186
- Button {
1187
- mouseShortcutStore.restoreDefaults()
1188
- } label: {
1189
- Text("Restore Defaults")
1190
- .font(Typo.monoBold(10))
1191
- .foregroundColor(Palette.text)
1192
- .padding(.horizontal, 12)
1193
- .padding(.vertical, 4)
1194
- .background(
1195
- RoundedRectangle(cornerRadius: 4)
1196
- .fill(Palette.surfaceHov)
1197
- .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
1198
- )
1199
- }
1200
- .buttonStyle(.plain)
1201
- }
1202
-
1203
- Text("Use Event Viewer to discover what your mouse emits on this machine. The config schema already accepts device selectors, but live gesture matching currently falls back to global rules when macOS doesn't expose the source device.")
1204
- .font(Typo.caption(9))
1205
- .foregroundColor(Palette.textMuted.opacity(0.7))
1206
- }
1207
- }
1208
-
1209
- settingsCard {
1210
- VStack(alignment: .leading, spacing: 10) {
1211
- Text("Keyboard remaps")
1212
- .font(Typo.mono(11))
1213
- .foregroundColor(Palette.text)
1214
-
1215
- HStack {
1216
- Text("Caps Lock as Hyper")
1217
- .font(Typo.mono(10))
1218
- .foregroundColor(Palette.textDim)
1219
- Spacer()
1220
- Toggle("", isOn: $prefs.keyboardRemapsEnabled)
1221
- .toggleStyle(.switch)
1222
- .controlSize(.small)
1223
- .labelsHidden()
1224
- }
1225
-
1226
- Text("Rules live in ~/.lattices/keyboard-remaps.json. The default maps hold Caps Lock to Hyper and tap Caps Lock to Escape, so the existing Hyper shortcuts work on the laptop keyboard.")
1227
- .font(Typo.caption(9))
1228
- .foregroundColor(Palette.textMuted.opacity(0.7))
1229
-
1230
- cardDivider
1231
-
1232
- VStack(alignment: .leading, spacing: 6) {
1233
- Text("Active remaps")
1234
- .font(Typo.mono(10))
1235
- .foregroundColor(Palette.textDim)
1236
-
1237
- ForEach(keyboardRemapStore.summaryLines.prefix(4), id: \.self) { line in
1238
- Text(line)
1239
- .font(Typo.caption(9))
1240
- .foregroundColor(Palette.textMuted.opacity(0.78))
1241
- }
1242
-
1243
- if keyboardRemapStore.summaryLines.isEmpty {
1244
- Text("No active remaps")
1245
- .font(Typo.caption(9))
1246
- .foregroundColor(Palette.textMuted.opacity(0.6))
1247
- }
1248
- }
1249
-
1250
- breakerStatusRow(
1251
- state: keyboardRemapController.breakerState,
1252
- label: "Keyboard remaps"
1253
- ) {
1254
- keyboardRemapController.reArmAfterBreakerTrip()
1255
- }
1256
-
1257
- HStack(spacing: 8) {
1258
- Button {
1259
- keyboardRemapStore.openConfiguration()
1260
- } label: {
1261
- Text("Configure...")
1262
- .font(Typo.monoBold(10))
1263
- .foregroundColor(Palette.text)
1264
- .padding(.horizontal, 12)
1265
- .padding(.vertical, 4)
1266
- .background(
1267
- RoundedRectangle(cornerRadius: 4)
1268
- .fill(Palette.surfaceHov)
1269
- .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
1270
- )
1271
- }
1272
- .buttonStyle(.plain)
1273
-
1274
- Button {
1275
- keyboardRemapStore.restoreDefaults()
1276
- } label: {
1277
- Text("Restore Defaults")
1278
- .font(Typo.monoBold(10))
1279
- .foregroundColor(Palette.text)
1280
- .padding(.horizontal, 12)
1281
- .padding(.vertical, 4)
1282
- .background(
1283
- RoundedRectangle(cornerRadius: 4)
1284
- .fill(Palette.surfaceHov)
1285
- .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
1286
- )
1287
- }
1288
- .buttonStyle(.plain)
1289
- }
1290
- }
1291
- }
1292
- }
1293
- .padding(16)
1294
- .frame(maxWidth: 760, alignment: .leading)
1295
- .frame(maxWidth: .infinity, alignment: .leading)
1296
- }
1297
- }
1298
-
1299
- // MARK: - Companion
1300
-
1301
- private var companionContent: some View {
1302
- ScrollView {
1303
- VStack(alignment: .leading, spacing: 12) {
1304
- companionBridgeOverviewCard
1305
- companionTrustedDevicesCard
1306
- companionCockpitCard
1307
- }
1308
- .padding(16)
1309
- .frame(maxWidth: 760, alignment: .leading)
1310
- .frame(maxWidth: .infinity, alignment: .leading)
1311
- }
1312
- }
1313
-
1314
- private var companionBridgeOverviewCard: some View {
1315
- settingsCard {
1316
- VStack(alignment: .leading, spacing: 12) {
1317
- HStack(alignment: .top, spacing: 10) {
1318
- RoundedRectangle(cornerRadius: 6)
1319
- .fill(Palette.running.opacity(0.14))
1320
- .overlay(
1321
- Image(systemName: "lock.shield")
1322
- .font(.system(size: 13, weight: .semibold))
1323
- .foregroundColor(Palette.running)
1324
- )
1325
- .frame(width: 30, height: 30)
1326
-
1327
- VStack(alignment: .leading, spacing: 3) {
1328
- Text(prefs.companionBridgeEnabled ? "Secure local bridge" : "Local bridge off")
1329
- .font(Typo.mono(12))
1330
- .foregroundColor(Palette.text)
1331
- Text(prefs.companionBridgeEnabled
1332
- ? "Bonjour discovery with explicit Mac approval, signed requests, encrypted payloads, and capability grants."
1333
- : "The companion bridge is not listening or advertising on the local network until you turn it on.")
1334
- .font(Typo.caption(10))
1335
- .foregroundColor(Palette.textMuted)
1336
- .fixedSize(horizontal: false, vertical: true)
1337
- }
1338
-
1339
- Spacer()
1340
-
1341
- Toggle("", isOn: $prefs.companionBridgeEnabled)
1342
- .toggleStyle(.switch)
1343
- .controlSize(.small)
1344
- .labelsHidden()
1345
- }
1346
-
1347
- cardDivider
1348
-
1349
- LazyVGrid(
1350
- columns: [
1351
- GridItem(.flexible(minimum: 120), spacing: 10, alignment: .leading),
1352
- GridItem(.flexible(minimum: 120), spacing: 10, alignment: .leading),
1353
- GridItem(.flexible(minimum: 120), spacing: 10, alignment: .leading),
1354
- ],
1355
- alignment: .leading,
1356
- spacing: 10
1357
- ) {
1358
- companionBridgeFact(
1359
- label: "Status",
1360
- value: prefs.companionBridgeEnabled ? "enabled" : "off"
1361
- )
1362
- companionBridgeFact(
1363
- label: "Port",
1364
- value: String(LatticesCompanionBridgeServer.defaultPort)
1365
- )
1366
- companionBridgeFact(
1367
- label: "Protocol",
1368
- value: "v\(LatticesCompanionBridgeServer.protocolVersion)"
1369
- )
1370
- }
1371
-
1372
- VStack(alignment: .leading, spacing: 5) {
1373
- Text("Enable deep link")
1374
- .font(Typo.mono(10))
1375
- .foregroundColor(Palette.textDim)
1376
- Text("lattices://companion/enable")
1377
- .font(Typo.monoBold(12))
1378
- .foregroundColor(Palette.text)
1379
- .textSelection(.enabled)
1380
- }
1381
- .padding(10)
1382
- .frame(maxWidth: .infinity, alignment: .leading)
1383
- .background(shortcutsInsetPanel)
1384
-
1385
- VStack(alignment: .leading, spacing: 5) {
1386
- Text("Mac bridge fingerprint")
1387
- .font(Typo.mono(10))
1388
- .foregroundColor(Palette.textDim)
1389
- Text(LatticesCompanionSecurityCoordinator.shared.bridgeFingerprint)
1390
- .font(Typo.monoBold(13))
1391
- .foregroundColor(Palette.text)
1392
- .textSelection(.enabled)
1393
- }
1394
- .padding(10)
1395
- .frame(maxWidth: .infinity, alignment: .leading)
1396
- .background(shortcutsInsetPanel)
1397
-
1398
- HStack(spacing: 6) {
1399
- ForEach(DeckBridgeCapability.defaultCompanionCapabilities, id: \.self) { capability in
1400
- companionCapabilityBadge(capability)
1401
- }
1402
- Spacer(minLength: 0)
1403
- }
1404
- }
1405
- }
1406
- }
1407
-
1408
- private var companionTrustedDevicesCard: some View {
1409
- let trustedDevices = companionTrustedDevices(revision: companionTrustRevision)
1410
-
1411
- return settingsCard {
1412
- VStack(alignment: .leading, spacing: 12) {
1413
- HStack(alignment: .top, spacing: 12) {
1414
- VStack(alignment: .leading, spacing: 4) {
1415
- Text("Paired devices")
1416
- .font(Typo.mono(12))
1417
- .foregroundColor(Palette.text)
1418
- Text("Only trusted devices can call protected deck and input routes. Pairing grants are listed per device.")
1419
- .font(Typo.caption(10))
1420
- .foregroundColor(Palette.textMuted)
1421
- .fixedSize(horizontal: false, vertical: true)
1422
- }
1423
-
1424
- Spacer()
1425
-
1426
- HStack(spacing: 8) {
1427
- Button {
1428
- companionTrustRevision += 1
1429
- } label: {
1430
- Image(systemName: "arrow.clockwise")
1431
- .font(.system(size: 10, weight: .semibold))
1432
- .foregroundColor(Palette.textDim)
1433
- .frame(width: 24, height: 24)
1434
- .background(
1435
- RoundedRectangle(cornerRadius: 5)
1436
- .fill(Palette.surfaceHov)
1437
- .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Palette.borderLit, lineWidth: 0.5))
1438
- )
1439
- }
1440
- .buttonStyle(.plain)
1441
-
1442
- if trustedDevices.isEmpty == false {
1443
- Button {
1444
- guard confirmForgetTrustedDevices() else { return }
1445
- LatticesCompanionSecurityCoordinator.shared.clearTrustedDevices()
1446
- companionTrustRevision += 1
1447
- } label: {
1448
- Text("Forget All")
1449
- .font(Typo.monoBold(10))
1450
- .foregroundColor(Palette.kill.opacity(0.9))
1451
- .padding(.horizontal, 10)
1452
- .padding(.vertical, 5)
1453
- .background(
1454
- RoundedRectangle(cornerRadius: 5)
1455
- .fill(Palette.kill.opacity(0.10))
1456
- .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Palette.kill.opacity(0.22), lineWidth: 0.5))
1457
- )
1458
- }
1459
- .buttonStyle(.plain)
1460
- }
1461
- }
1462
- }
1463
-
1464
- if trustedDevices.isEmpty {
1465
- VStack(alignment: .leading, spacing: 6) {
1466
- Image(systemName: "ipad.and.iphone")
1467
- .font(.system(size: 18, weight: .semibold))
1468
- .foregroundColor(Palette.textMuted)
1469
-
1470
- Text("No paired iPad or iPhone devices yet.")
1471
- .font(Typo.caption(10.5))
1472
- .foregroundColor(Palette.textMuted)
1473
-
1474
- Text("Open the Lattices companion app on your iPad and select this Mac. You’ll approve the pairing prompt here.")
1475
- .font(Typo.caption(9.5))
1476
- .foregroundColor(Palette.textMuted.opacity(0.72))
1477
- .fixedSize(horizontal: false, vertical: true)
1478
- }
1479
- .padding(12)
1480
- .frame(maxWidth: .infinity, alignment: .leading)
1481
- .background(shortcutsInsetPanel)
1482
- } else {
1483
- VStack(alignment: .leading, spacing: 8) {
1484
- ForEach(trustedDevices) { device in
1485
- companionDeviceRow(device)
1486
- }
1487
- }
1488
- }
1489
- }
1490
- }
1491
- }
1492
-
1493
- private func companionBridgeFact(label: String, value: String) -> some View {
1494
- VStack(alignment: .leading, spacing: 5) {
1495
- Text(label.uppercased())
1496
- .font(Typo.pixel(11))
1497
- .foregroundColor(Palette.textDim)
1498
- .tracking(1)
1499
- Text(value)
1500
- .font(Typo.monoBold(11))
1501
- .foregroundColor(Palette.text)
1502
- .lineLimit(1)
1503
- .truncationMode(.middle)
1504
- }
1505
- .padding(10)
1506
- .frame(maxWidth: .infinity, alignment: .leading)
1507
- .background(shortcutsInsetPanel)
1508
- }
1509
-
1510
- private func companionDeviceRow(_ device: DeckTrustedDeviceSummary) -> some View {
1511
- HStack(alignment: .top, spacing: 10) {
1512
- RoundedRectangle(cornerRadius: 6)
1513
- .fill(Palette.surfaceHov)
1514
- .overlay(
1515
- Image(systemName: companionDeviceIcon(for: device.name))
1516
- .font(.system(size: 13, weight: .semibold))
1517
- .foregroundColor(Palette.textDim)
1518
- )
1519
- .frame(width: 30, height: 30)
1520
-
1521
- VStack(alignment: .leading, spacing: 6) {
1522
- HStack(alignment: .firstTextBaseline, spacing: 8) {
1523
- Text(device.name)
1524
- .font(Typo.monoBold(11))
1525
- .foregroundColor(Palette.text)
1526
- .lineLimit(1)
1527
-
1528
- Text(device.fingerprint)
1529
- .font(Typo.mono(10))
1530
- .foregroundColor(Palette.textMuted)
1531
- .lineLimit(1)
1532
-
1533
- Spacer(minLength: 0)
1534
- }
1535
-
1536
- HStack(spacing: 10) {
1537
- Text("Paired \(relativeTimestamp(device.pairedAt))")
1538
- Text("Last seen \(relativeTimestamp(device.lastSeenAt))")
1539
- }
1540
- .font(Typo.caption(9.5))
1541
- .foregroundColor(Palette.textMuted.opacity(0.78))
1542
-
1543
- HStack(spacing: 6) {
1544
- ForEach(device.capabilities, id: \.self) { capability in
1545
- companionCapabilityBadge(capability)
1546
- }
1547
- }
1548
- }
1549
-
1550
- Spacer(minLength: 0)
1551
-
1552
- Button {
1553
- guard confirmRevokeTrustedDevice(device) else { return }
1554
- LatticesCompanionSecurityCoordinator.shared.revokeTrustedDevice(id: device.id)
1555
- companionTrustRevision += 1
1556
- } label: {
1557
- HStack(spacing: 5) {
1558
- Image(systemName: "xmark.shield")
1559
- .font(.system(size: 10, weight: .semibold))
1560
- Text("Revoke")
1561
- .font(Typo.monoBold(9.5))
1562
- }
1563
- .foregroundColor(Palette.kill.opacity(0.95))
1564
- .padding(.horizontal, 8)
1565
- .padding(.vertical, 5)
1566
- .background(
1567
- RoundedRectangle(cornerRadius: 5)
1568
- .fill(Palette.kill.opacity(0.10))
1569
- .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Palette.kill.opacity(0.22), lineWidth: 0.5))
1570
- )
1571
- }
1572
- .buttonStyle(.plain)
1573
- .help("Revoke this paired device")
1574
- }
1575
- .padding(12)
1576
- .frame(maxWidth: .infinity, alignment: .leading)
1577
- .background(shortcutsInsetPanel)
1578
- }
1579
-
1580
- private func companionCapabilityBadge(_ capability: String) -> some View {
1581
- Text(companionCapabilityLabel(capability))
1582
- .font(Typo.monoBold(9))
1583
- .foregroundColor(Palette.running.opacity(0.92))
1584
- .padding(.horizontal, 7)
1585
- .padding(.vertical, 3)
1586
- .background(
1587
- Capsule()
1588
- .fill(Palette.running.opacity(0.10))
1589
- .overlay(Capsule().strokeBorder(Palette.running.opacity(0.18), lineWidth: 0.5))
1590
- )
1591
- }
1592
-
1593
- private func companionCapabilityLabel(_ capability: String) -> String {
1594
- switch capability {
1595
- case DeckBridgeCapability.deckRead:
1596
- return "Deck Read"
1597
- case DeckBridgeCapability.deckPerform:
1598
- return "Deck Actions"
1599
- case DeckBridgeCapability.inputTrackpad:
1600
- return "Trackpad"
1601
- default:
1602
- return capability
1603
- }
1604
- }
1605
-
1606
- private func companionDeviceIcon(for name: String) -> String {
1607
- name.localizedCaseInsensitiveContains("ipad") ? "ipad" : "iphone"
1608
- }
1609
-
1610
- private func confirmForgetTrustedDevices() -> Bool {
1611
- let alert = NSAlert()
1612
- alert.alertStyle = .warning
1613
- alert.messageText = "Forget all paired companion devices?"
1614
- alert.informativeText = "Your iPad or iPhone will need to pair again before it can control Lattices."
1615
- alert.addButton(withTitle: "Forget Devices")
1616
- alert.addButton(withTitle: "Cancel")
1617
- return alert.runModal() == .alertFirstButtonReturn
1618
- }
1619
-
1620
- private func confirmRevokeTrustedDevice(_ device: DeckTrustedDeviceSummary) -> Bool {
1621
- let alert = NSAlert()
1622
- alert.alertStyle = .warning
1623
- alert.messageText = "Revoke \(device.name)?"
1624
- alert.informativeText = """
1625
- This removes the paired-device trust record for \(device.name).
1626
-
1627
- Fingerprint: \(device.fingerprint)
1628
-
1629
- The device will need to pair again before it can control Lattices.
1630
- """
1631
- alert.addButton(withTitle: "Revoke Device")
1632
- alert.addButton(withTitle: "Cancel")
1633
- return alert.runModal() == .alertFirstButtonReturn
1634
- }
1635
-
1636
- // MARK: - AI
1637
-
1638
- private var aiContent: some View {
1639
- ScrollView {
1640
- VStack(spacing: 12) {
1641
- // ── Claude CLI ──
1642
- settingsCard {
1643
- VStack(alignment: .leading, spacing: 10) {
1644
- HStack(spacing: 8) {
1645
- Image(systemName: "sparkles")
1646
- .font(.system(size: 11, weight: .medium))
1647
- .foregroundColor(Palette.running)
1648
- Text("Claude CLI")
1649
- .font(Typo.mono(12))
1650
- .foregroundColor(Palette.text)
1651
- }
1652
-
1653
- HStack(spacing: 6) {
1654
- TextField("Auto-detected", text: $prefs.claudePath)
1655
- .textFieldStyle(.plain)
1656
- .font(Typo.mono(11))
1657
- .foregroundColor(Palette.text)
1658
- .padding(.horizontal, 8)
1659
- .padding(.vertical, 5)
1660
- .background(
1661
- RoundedRectangle(cornerRadius: 5)
1662
- .fill(Color.white.opacity(0.06))
1663
- .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5))
1664
- )
1665
-
1666
- Button {
1667
- if let resolved = Preferences.resolveClaudePath() {
1668
- prefs.claudePath = resolved
1669
- }
1670
- } label: {
1671
- Text("Detect")
1672
- .font(Typo.monoBold(10))
1673
- .foregroundColor(Palette.text)
1674
- .padding(.horizontal, 10)
1675
- .padding(.vertical, 4)
1676
- .background(
1677
- RoundedRectangle(cornerRadius: 4)
1678
- .fill(Palette.surfaceHov)
1679
- .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
1680
- )
1681
- }
1682
- .buttonStyle(.plain)
1683
- }
1684
-
1685
- let resolved = Preferences.resolveClaudePath()
1686
- if let path = resolved {
1687
- Text("Found: \(path)")
1688
- .font(Typo.caption(9))
1689
- .foregroundColor(Palette.running.opacity(0.8))
1690
- } else {
1691
- Text("Not found — install with: npm i -g @anthropic-ai/claude-code")
1692
- .font(Typo.caption(9))
1693
- .foregroundColor(Palette.detach)
1694
- }
1695
- }
1696
- }
1697
-
1698
- // ── Advisor ──
1699
- settingsCard {
1700
- VStack(alignment: .leading, spacing: 10) {
1701
- Text("Voice advisor")
1702
- .font(Typo.mono(11))
1703
- .foregroundColor(Palette.text)
1704
-
1705
- HStack {
1706
- Text("Model")
1707
- .font(Typo.mono(10))
1708
- .foregroundColor(Palette.textDim)
1709
- Spacer()
1710
- Picker("", selection: $prefs.advisorModel) {
1711
- Text("Haiku").tag("haiku")
1712
- Text("Sonnet").tag("sonnet")
1713
- }
1714
- .pickerStyle(.segmented)
1715
- .labelsHidden()
1716
- .frame(width: 160)
1717
- }
1718
-
1719
- Text("Haiku is fast and cheap. Sonnet is smarter but slower.")
1720
- .font(Typo.caption(9))
1721
- .foregroundColor(Palette.textMuted.opacity(0.7))
1722
-
1723
- cardDivider
1724
-
1725
- HStack {
1726
- Text("Budget per session")
1727
- .font(Typo.mono(10))
1728
- .foregroundColor(Palette.textDim)
1729
- Spacer()
1730
- HStack(spacing: 4) {
1731
- Text("$")
1732
- .font(Typo.mono(11))
1733
- .foregroundColor(Palette.textDim)
1734
- TextField("0.50", value: $prefs.advisorBudgetUSD, formatter: {
1735
- let f = NumberFormatter()
1736
- f.numberStyle = .decimal
1737
- f.minimumFractionDigits = 2
1738
- f.maximumFractionDigits = 2
1739
- return f
1740
- }())
1741
- .textFieldStyle(.plain)
1742
- .font(Typo.monoBold(11))
1743
- .foregroundColor(Palette.text)
1744
- .multilineTextAlignment(.center)
1745
- .frame(width: 50)
1746
- .padding(.horizontal, 4)
1747
- .padding(.vertical, 3)
1748
- .background(
1749
- RoundedRectangle(cornerRadius: 5)
1750
- .fill(Color.white.opacity(0.06))
1751
- .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5))
1752
- )
1753
- }
1754
- }
1755
-
1756
- Text("Max spend per Claude CLI invocation")
1757
- .font(Typo.caption(9))
1758
- .foregroundColor(Palette.textMuted.opacity(0.7))
1759
-
1760
- cardDivider
1761
-
1762
- // Session stats
1763
- let stats = AgentPool.shared.haiku.sessionStats
1764
- HStack(spacing: 12) {
1765
- if stats.contextWindow > 0 {
1766
- HStack(spacing: 4) {
1767
- Circle()
1768
- .fill(stats.contextUsage > 0.6 ? Palette.detach : Palette.running)
1769
- .frame(width: 5, height: 5)
1770
- Text("Context: \(Int(stats.contextUsage * 100))%")
1771
- .font(Typo.mono(10))
1772
- .foregroundColor(Palette.textMuted)
1773
- }
1774
- }
1775
- if stats.costUSD > 0 {
1776
- Text("Session cost: $\(String(format: "%.3f", stats.costUSD))")
1777
- .font(Typo.mono(10))
1778
- .foregroundColor(Palette.textMuted)
1779
- }
1780
-
1781
- Spacer()
1782
-
1783
- let learningCount = AdvisorLearningStore.shared.entryCount
1784
- if learningCount > 0 {
1785
- Text("\(learningCount) learned")
1786
- .font(Typo.mono(9))
1787
- .foregroundColor(Palette.textMuted.opacity(0.6))
1788
- }
1789
- }
1790
- }
1791
- }
1792
- }
1793
- .padding(16)
1794
- }
1795
- }
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
-
1811
- // MARK: - Search & OCR
1812
-
1813
- private func ocrNumField(_ value: Binding<Double>, width: CGFloat = 50) -> some View {
1814
- TextField("", value: value, formatter: NumberFormatter())
1815
- .textFieldStyle(.plain)
1816
- .font(Typo.monoBold(11))
1817
- .foregroundColor(Palette.text)
1818
- .multilineTextAlignment(.center)
1819
- .frame(width: width)
1820
- .padding(.horizontal, 4)
1821
- .padding(.vertical, 3)
1822
- .background(
1823
- RoundedRectangle(cornerRadius: 5)
1824
- .fill(Color.white.opacity(0.06))
1825
- .overlay(
1826
- RoundedRectangle(cornerRadius: 5)
1827
- .strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5)
1828
- )
1829
- )
1830
- }
1831
-
1832
- private func ocrIntField(_ value: Binding<Int>, width: CGFloat = 36) -> some View {
1833
- TextField("", value: value, formatter: NumberFormatter())
1834
- .textFieldStyle(.plain)
1835
- .font(Typo.monoBold(11))
1836
- .foregroundColor(Palette.text)
1837
- .multilineTextAlignment(.center)
1838
- .frame(width: width)
1839
- .padding(.horizontal, 4)
1840
- .padding(.vertical, 3)
1841
- .background(
1842
- RoundedRectangle(cornerRadius: 5)
1843
- .fill(Color.white.opacity(0.06))
1844
- .overlay(
1845
- RoundedRectangle(cornerRadius: 5)
1846
- .strokeBorder(Color.white.opacity(0.08), lineWidth: 0.5)
1847
- )
1848
- )
1849
- }
1850
-
1851
- private func ocrSectionLabel(_ text: String) -> some View {
1852
- Text(text)
1853
- .font(Typo.monoBold(10))
1854
- .foregroundColor(Palette.textDim)
1855
- .tracking(0.5)
1856
- }
1857
-
1858
- private var searchOcrContent: some View {
1859
- ScrollView {
1860
- VStack(spacing: 12) {
1861
- // ── Screen Text Recognition Card ──
1862
- settingsCard {
1863
- VStack(alignment: .leading, spacing: 10) {
1864
- // Header row: label + toggle
1865
- HStack {
1866
- HStack(spacing: 8) {
1867
- RoundedRectangle(cornerRadius: 4)
1868
- .fill(prefs.ocrEnabled ? Palette.running.opacity(0.15) : Palette.surface)
1869
- .overlay(
1870
- Image(systemName: "text.viewfinder")
1871
- .font(.system(size: 11, weight: .medium))
1872
- .foregroundColor(prefs.ocrEnabled ? Palette.running : Palette.textMuted)
1873
- )
1874
- .frame(width: 24, height: 24)
1875
-
1876
- VStack(alignment: .leading, spacing: 1) {
1877
- Text("Screen text recognition")
1878
- .font(Typo.mono(12))
1879
- .foregroundColor(Palette.text)
1880
- Text("Vision OCR on visible windows")
1881
- .font(Typo.caption(10))
1882
- .foregroundColor(Palette.textMuted)
1883
- }
1884
- }
1885
- Spacer()
1886
- Toggle("", isOn: Binding(
1887
- get: { prefs.ocrEnabled },
1888
- set: { OcrModel.shared.setEnabled($0) }
1889
- ))
1890
- .toggleStyle(.switch)
1891
- .controlSize(.small)
1892
- .labelsHidden()
1893
- }
1894
-
1895
- // Accuracy
1896
- HStack(spacing: 8) {
1897
- Text("Accuracy")
1898
- .font(Typo.mono(10))
1899
- .foregroundColor(Palette.textDim)
1900
- Picker("", selection: $prefs.ocrAccuracy) {
1901
- Text("Accurate").tag("accurate")
1902
- Text("Fast").tag("fast")
1903
- }
1904
- .pickerStyle(.segmented)
1905
- .labelsHidden()
1906
- .frame(width: 140)
1907
- Spacer()
1908
- }
1909
- .padding(.leading, 32)
1910
- }
1911
- }
1912
-
1913
- // ── Scan Schedule Card ──
1914
- settingsCard {
1915
- VStack(alignment: .leading, spacing: 10) {
1916
- ocrSectionLabel("Schedule")
1917
-
1918
- // Quick scan sentence
1919
- HStack(spacing: 0) {
1920
- Text("Quick scan top ")
1921
- .font(Typo.mono(11))
1922
- .foregroundColor(Palette.textDim)
1923
- ocrIntField($prefs.ocrQuickLimit, width: 32)
1924
- Text(" windows every ")
1925
- .font(Typo.mono(11))
1926
- .foregroundColor(Palette.textDim)
1927
- ocrNumField($prefs.ocrQuickInterval, width: 42)
1928
- Text("s")
1929
- .font(Typo.mono(11))
1930
- .foregroundColor(Palette.textDim)
1931
- Spacer()
1932
- }
1933
-
1934
- cardDivider
1935
-
1936
- // Deep scan sentence
1937
- HStack(spacing: 0) {
1938
- Text("Deep scan up to ")
1939
- .font(Typo.mono(11))
1940
- .foregroundColor(Palette.textDim)
1941
- ocrIntField($prefs.ocrDeepLimit, width: 32)
1942
- Text(" windows every ")
1943
- .font(Typo.mono(11))
1944
- .foregroundColor(Palette.textDim)
1945
- ocrNumField($prefs.ocrDeepInterval, width: 52)
1946
- Text("s")
1947
- .font(Typo.mono(11))
1948
- .foregroundColor(Palette.textDim)
1949
- Spacer()
1950
- }
1951
-
1952
- HStack(spacing: 0) {
1953
- Text("OCR budget: ")
1954
- .font(Typo.mono(11))
1955
- .foregroundColor(Palette.textDim)
1956
- ocrIntField($prefs.ocrDeepBudget, width: 32)
1957
- Text(" windows per scan")
1958
- .font(Typo.mono(11))
1959
- .foregroundColor(Palette.textDim)
1960
- Spacer()
1961
- }
1962
-
1963
- // Human-readable deep interval
1964
- let h = Int(prefs.ocrDeepInterval / 3600)
1965
- let m = Int(prefs.ocrDeepInterval.truncatingRemainder(dividingBy: 3600) / 60)
1966
- if h > 0 || m > 0 {
1967
- Text("≈ \(h > 0 ? "\(h)h" : "")\(m > 0 ? " \(m)m" : "")")
1968
- .font(Typo.caption(9))
1969
- .foregroundColor(Palette.textMuted.opacity(0.6))
1970
- .padding(.leading, 2)
1971
- }
1972
- }
1973
- }
1974
-
1975
- // ── Status Card ──
1976
- settingsCard {
1977
- HStack(spacing: 8) {
1978
- let ocrResults = OcrModel.shared.results
1979
- let isScanning = OcrModel.shared.isScanning
1980
-
1981
- Circle()
1982
- .fill(isScanning ? Palette.detach : (prefs.ocrEnabled ? Palette.running : Palette.textMuted))
1983
- .frame(width: 6, height: 6)
1984
-
1985
- Text(isScanning ? "Scanning..." : (prefs.ocrEnabled ? "\(ocrResults.count) windows cached" : "Disabled"))
1986
- .font(Typo.mono(10))
1987
- .foregroundColor(Palette.textMuted)
1988
-
1989
- Spacer()
1990
-
1991
- Button {
1992
- OcrModel.shared.scan()
1993
- } label: {
1994
- HStack(spacing: 4) {
1995
- Image(systemName: "arrow.clockwise")
1996
- .font(.system(size: 9, weight: .semibold))
1997
- Text("Scan Now")
1998
- .font(Typo.monoBold(10))
1999
- }
2000
- .foregroundColor(prefs.ocrEnabled ? Palette.text : Palette.textMuted)
2001
- .padding(.horizontal, 10)
2002
- .padding(.vertical, 4)
2003
- .background(
2004
- RoundedRectangle(cornerRadius: 4)
2005
- .fill(prefs.ocrEnabled ? Palette.surfaceHov : Palette.surface)
2006
- .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
2007
- )
2008
- }
2009
- .buttonStyle(.plain)
2010
- .disabled(!prefs.ocrEnabled)
2011
- }
2012
- }
2013
-
2014
- // ── Recent Captures ──
2015
- recentCapturesSection
2016
- }
2017
- .padding(16)
2018
- }
2019
- }
2020
-
2021
- // MARK: - Recent Captures Browser
2022
-
2023
- private var recentCapturesSection: some View {
2024
- let ocrResults = OcrModel.shared.results
2025
- let grouped = Dictionary(grouping: ocrResults.values, by: \.app)
2026
- .sorted { $0.value.count > $1.value.count }
2027
-
2028
- return Group {
2029
- if !grouped.isEmpty {
2030
- settingsCard {
2031
- VStack(alignment: .leading, spacing: 8) {
2032
- ocrSectionLabel("Recent Captures")
2033
-
2034
- ForEach(grouped, id: \.key) { app, windows in
2035
- ocrAppGroup(app: app, windows: windows.sorted { $0.timestamp > $1.timestamp })
2036
- }
2037
- }
2038
- }
2039
- }
2040
- }
2041
- }
2042
-
2043
- private func ocrAppGroup(app: String, windows: [OcrWindowResult]) -> some View {
2044
- let isCollapsed = collapsedOcrApps.contains(app)
2045
-
2046
- return VStack(alignment: .leading, spacing: 0) {
2047
- // App header
2048
- Button {
2049
- withAnimation(.easeInOut(duration: 0.15)) {
2050
- if isCollapsed {
2051
- collapsedOcrApps.remove(app)
2052
- } else {
2053
- collapsedOcrApps.insert(app)
2054
- }
2055
- }
2056
- } label: {
2057
- HStack(spacing: 6) {
2058
- Image(systemName: isCollapsed ? "chevron.right" : "chevron.down")
2059
- .font(.system(size: 8, weight: .semibold))
2060
- .foregroundColor(Palette.textMuted)
2061
- .frame(width: 10)
2062
-
2063
- Text(app)
2064
- .font(Typo.monoBold(11))
2065
- .foregroundColor(Palette.text)
2066
-
2067
- Text("(\(windows.count))")
2068
- .font(Typo.mono(10))
2069
- .foregroundColor(Palette.textMuted)
2070
-
2071
- Spacer()
2072
- }
2073
- .padding(.vertical, 4)
2074
- .contentShape(Rectangle())
2075
- }
2076
- .buttonStyle(.plain)
2077
-
2078
- if !isCollapsed {
2079
- VStack(alignment: .leading, spacing: 2) {
2080
- ForEach(windows, id: \.wid) { win in
2081
- ocrWindowRow(win)
2082
- }
2083
- }
2084
- .padding(.leading, 16)
2085
- }
2086
- }
2087
- }
2088
-
2089
- private func ocrWindowRow(_ win: OcrWindowResult) -> some View {
2090
- let isExpanded = expandedOcrWindow == win.wid
2091
- let preview = String(win.fullText.prefix(80)).replacingOccurrences(of: "\n", with: " ")
2092
-
2093
- return VStack(alignment: .leading, spacing: 0) {
2094
- Button {
2095
- withAnimation(.easeInOut(duration: 0.15)) {
2096
- expandedOcrWindow = isExpanded ? nil : win.wid
2097
- }
2098
- } label: {
2099
- HStack(spacing: 6) {
2100
- Image(systemName: isExpanded ? "chevron.down" : "chevron.right")
2101
- .font(.system(size: 7, weight: .semibold))
2102
- .foregroundColor(Palette.textMuted)
2103
- .frame(width: 8)
2104
-
2105
- VStack(alignment: .leading, spacing: 2) {
2106
- HStack(spacing: 6) {
2107
- Text(win.title.isEmpty ? "Untitled" : win.title)
2108
- .font(Typo.mono(10))
2109
- .foregroundColor(Palette.text)
2110
- .lineLimit(1)
2111
-
2112
- Spacer()
2113
-
2114
- Text(ocrRelativeTime(win.timestamp))
2115
- .font(Typo.caption(9))
2116
- .foregroundColor(Palette.textMuted)
2117
- }
2118
-
2119
- if !isExpanded && !preview.isEmpty {
2120
- Text(preview)
2121
- .font(Typo.caption(9))
2122
- .foregroundColor(Palette.textMuted.opacity(0.7))
2123
- .lineLimit(1)
2124
- }
2125
- }
2126
- }
2127
- .padding(.vertical, 4)
2128
- .contentShape(Rectangle())
2129
- }
2130
- .buttonStyle(.plain)
2131
-
2132
- if isExpanded {
2133
- ocrExpandedDetail(win)
2134
- .padding(.leading, 14)
2135
- .padding(.vertical, 4)
2136
- }
2137
- }
2138
- }
2139
-
2140
- private func ocrExpandedDetail(_ win: OcrWindowResult) -> some View {
2141
- VStack(alignment: .leading, spacing: 6) {
2142
- // Metadata row
2143
- HStack(spacing: 10) {
2144
- let avgConfidence = win.texts.isEmpty ? 0 : win.texts.map(\.confidence).reduce(0, +) / Float(win.texts.count)
2145
- Text("\(win.texts.count) blocks")
2146
- .font(Typo.caption(9))
2147
- .foregroundColor(Palette.textMuted)
2148
- Text("confidence: \(String(format: "%.0f%%", avgConfidence * 100))")
2149
- .font(Typo.caption(9))
2150
- .foregroundColor(Palette.textMuted)
2151
- Spacer()
2152
- }
2153
-
2154
- // Full text in scrollable monospaced area
2155
- ScrollView {
2156
- Text(win.fullText)
2157
- .font(.system(size: 10, design: .monospaced))
2158
- .foregroundColor(Palette.textDim)
2159
- .textSelection(.enabled)
2160
- .frame(maxWidth: .infinity, alignment: .leading)
2161
- .padding(8)
2162
- }
2163
- .frame(maxHeight: 150)
2164
- .background(
2165
- RoundedRectangle(cornerRadius: 4)
2166
- .fill(Color.black.opacity(0.2))
2167
- .overlay(
2168
- RoundedRectangle(cornerRadius: 4)
2169
- .strokeBorder(Color.white.opacity(0.06), lineWidth: 0.5)
2170
- )
2171
- )
2172
- }
2173
- }
2174
-
2175
- private func ocrRelativeTime(_ date: Date) -> String {
2176
- let seconds = Int(-date.timeIntervalSinceNow)
2177
- if seconds < 60 { return "just now" }
2178
- let minutes = seconds / 60
2179
- if minutes < 60 { return "\(minutes)m ago" }
2180
- let hours = minutes / 60
2181
- if hours < 24 { return "\(hours)h ago" }
2182
- return "\(hours / 24)d ago"
2183
- }
2184
-
2185
- // MARK: - Settings Card
2186
-
2187
- private func settingsCard<Content: View>(@ViewBuilder content: () -> Content) -> some View {
2188
- content()
2189
- .padding(.horizontal, 14)
2190
- .padding(.vertical, 12)
2191
- .frame(maxWidth: .infinity, alignment: .leading)
2192
- .liquidGlass()
2193
- }
2194
-
2195
- private var cardDivider: some View {
2196
- Rectangle()
2197
- .fill(
2198
- LinearGradient(
2199
- colors: [Color.white.opacity(0.03), Color.white.opacity(0.08), Color.white.opacity(0.03)],
2200
- startPoint: .leading,
2201
- endPoint: .trailing
2202
- )
2203
- )
2204
- .frame(height: 0.5)
2205
- .padding(.vertical, 3)
2206
- }
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
-
2351
- // MARK: - Shortcuts
2352
-
2353
- private var shortcutsContent: some View {
2354
- VStack(spacing: 0) {
2355
- GeometryReader { geo in
2356
- let contentWidth = max(geo.size.width - 40, 320)
2357
- let sectionColumns = [
2358
- GridItem(.adaptive(minimum: min(320, contentWidth), maximum: 440), spacing: 16, alignment: .top)
2359
- ]
2360
- let tilingColumns = contentWidth > 860
2361
- ? [
2362
- GridItem(.flexible(minimum: 280, maximum: 360), spacing: 16, alignment: .top),
2363
- GridItem(.flexible(minimum: 320, maximum: 640), spacing: 16, alignment: .top)
2364
- ]
2365
- : [GridItem(.flexible(minimum: 0, maximum: .infinity), spacing: 16, alignment: .top)]
2366
-
2367
- ScrollView {
2368
- VStack(alignment: .leading, spacing: 0) {
2369
- VStack(alignment: .leading, spacing: 16) {
2370
- shortcutsOverviewCard
2371
- inputControlsCard
2372
-
2373
- LazyVGrid(columns: sectionColumns, alignment: .leading, spacing: 16) {
2374
- shortcutsAppCard
2375
- shortcutsLayersCard
2376
- }
2377
-
2378
- shortcutSectionCard(
2379
- title: "Window Tiling",
2380
- eyebrow: "Desktop Layout",
2381
- summary: "See the directional map first, then edit the matching global shortcuts below."
2382
- ) {
2383
- LazyVGrid(columns: tilingColumns, alignment: .leading, spacing: 16) {
2384
- shortcutsTilingVisualizer
2385
- shortcutsTilingEditors
2386
- }
2387
- }
2388
-
2389
- shortcutsTmuxCard
2390
- }
2391
- .padding(.horizontal, 20)
2392
- .padding(.vertical, 16)
2393
- }
2394
- }
2395
- }
2396
-
2397
- Spacer(minLength: 0)
2398
-
2399
- separator
2400
-
2401
- HStack {
2402
- HStack(spacing: 8) {
2403
- footerActionButton(icon: "book", label: "Docs") {
2404
- ScreenMapWindowController.shared.showPage(.docs)
2405
- }
2406
-
2407
- footerActionButton(icon: "stethoscope", label: "Diagnostics") {
2408
- DiagnosticWindow.shared.show()
2409
- }
2410
- }
2411
-
2412
- Spacer()
2413
-
2414
- Button {
2415
- hotkeyStore.resetAll()
2416
- } label: {
2417
- Text("Reset All to Defaults")
2418
- .font(Typo.caption(11))
2419
- .foregroundColor(Palette.textDim)
2420
- .padding(.horizontal, 12)
2421
- .padding(.vertical, 5)
2422
- .background(
2423
- RoundedRectangle(cornerRadius: 3)
2424
- .fill(Palette.surface)
2425
- .overlay(
2426
- RoundedRectangle(cornerRadius: 3)
2427
- .strokeBorder(Palette.border, lineWidth: 0.5)
2428
- )
2429
- )
2430
- }
2431
- .buttonStyle(.plain)
2432
- }
2433
- .padding(.horizontal, 20)
2434
- .padding(.vertical, 10)
2435
- }
2436
- }
2437
-
2438
- // MARK: - Shortcuts: Overview
2439
-
2440
- private var companionCockpitCard: some View {
2441
- let layout = LatticesCompanionCockpitCatalog.normalized(prefs.companionCockpitLayout)
2442
- let selectedPage = layout.pages.first(where: { $0.id == selectedCompanionCockpitPageID }) ?? layout.pages.first
2443
- let categories = LatticesCompanionShortcutCategory.allCases
2444
- let trustedDeviceCount = companionTrustedDevices(revision: companionTrustRevision).count
2445
-
2446
- return shortcutSectionCard(
2447
- title: "Companion Cockpit",
2448
- eyebrow: "iPad & iPhone",
2449
- summary: "Define the Mac-authored command deck here, then let the companion app render it. Trackpad proxy runs through the same bridge."
2450
- ) {
2451
- VStack(alignment: .leading, spacing: 14) {
2452
- HStack(alignment: .top, spacing: 12) {
2453
- VStack(alignment: .leading, spacing: 5) {
2454
- Text("Trackpad Proxy")
2455
- .font(Typo.monoBold(11))
2456
- .foregroundColor(Palette.text)
2457
- Text("Enable remote pointer control for the iPad trackpad surface. Accessibility permission is still required on the Mac.")
2458
- .font(Typo.caption(10.5))
2459
- .foregroundColor(Palette.textMuted)
2460
- .fixedSize(horizontal: false, vertical: true)
2461
- }
2462
-
2463
- Spacer()
2464
-
2465
- Toggle("", isOn: $prefs.companionTrackpadEnabled)
2466
- .toggleStyle(.switch)
2467
- .labelsHidden()
2468
- .disabled(!prefs.companionBridgeEnabled)
2469
- .opacity(prefs.companionBridgeEnabled ? 1 : 0.45)
2470
- }
2471
-
2472
- HStack(alignment: .center, spacing: 12) {
2473
- VStack(alignment: .leading, spacing: 4) {
2474
- Text("Pairing and trust")
2475
- .font(Typo.monoBold(11))
2476
- .foregroundColor(Palette.text)
2477
- Text("\(trustedDeviceCount) paired \(trustedDeviceCount == 1 ? "device" : "devices"). Revoke devices and review bridge grants in Companion settings.")
2478
- .font(Typo.caption(10.5))
2479
- .foregroundColor(Palette.textMuted)
2480
- .fixedSize(horizontal: false, vertical: true)
2481
- }
2482
-
2483
- Spacer()
2484
-
2485
- Button {
2486
- selectedTab = .companion
2487
- } label: {
2488
- HStack(spacing: 5) {
2489
- Image(systemName: "ipad.and.iphone")
2490
- .font(.system(size: 10, weight: .semibold))
2491
- Text("Manage")
2492
- .font(Typo.monoBold(10))
2493
- }
2494
- .foregroundColor(Palette.text)
2495
- .padding(.horizontal, 10)
2496
- .padding(.vertical, 5)
2497
- .background(
2498
- RoundedRectangle(cornerRadius: 5)
2499
- .fill(Palette.surfaceHov)
2500
- .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Palette.borderLit, lineWidth: 0.5))
2501
- )
2502
- }
2503
- .buttonStyle(.plain)
2504
- }
2505
- .padding(12)
2506
- .background(shortcutsInsetPanel)
2507
-
2508
- if let selectedPage {
2509
- Picker("Companion page", selection: $selectedCompanionCockpitPageID) {
2510
- ForEach(layout.pages) { page in
2511
- Text(page.title).tag(page.id)
2512
- }
2513
- }
2514
- .pickerStyle(.segmented)
2515
-
2516
- VStack(alignment: .leading, spacing: 8) {
2517
- if let subtitle = selectedPage.subtitle, !subtitle.isEmpty {
2518
- Text(subtitle)
2519
- .font(Typo.caption(10.5))
2520
- .foregroundColor(Palette.textMuted)
2521
- }
2522
-
2523
- LazyVGrid(
2524
- columns: Array(
2525
- repeating: GridItem(.flexible(minimum: 120, maximum: 220), spacing: 8, alignment: .top),
2526
- count: max(2, selectedPage.columns)
2527
- ),
2528
- alignment: .leading,
2529
- spacing: 8
2530
- ) {
2531
- ForEach(Array(selectedPage.slotIDs.enumerated()), id: \.offset) { index, shortcutID in
2532
- companionCockpitSlotMenu(
2533
- pageID: selectedPage.id,
2534
- index: index,
2535
- shortcutID: shortcutID,
2536
- categories: categories
2537
- )
2538
- }
2539
- }
2540
- }
2541
- .padding(12)
2542
- .background(shortcutsInsetPanel)
2543
- }
2544
-
2545
- HStack(spacing: 10) {
2546
- Text("Changes appear in the iPad companion on the next snapshot refresh.")
2547
- .font(Typo.caption(10.5))
2548
- .foregroundColor(Palette.textMuted)
2549
-
2550
- Spacer()
2551
-
2552
- Button("Reset Companion Layout") {
2553
- prefs.resetCompanionCockpitLayout()
2554
- }
2555
- .buttonStyle(.plain)
2556
- .font(Typo.caption(10.5))
2557
- .foregroundColor(Palette.textDim)
2558
- }
2559
- }
2560
- }
2561
- }
2562
-
2563
- private var shortcutsOverviewCard: some View {
2564
- shortcutSectionCard(
2565
- title: "Shortcut Map",
2566
- eyebrow: "Quick Reference",
2567
- summary: "Global hotkeys are editable here. tmux shortcuts stay as a built-in reference so you can keep your workspace flow in one place."
2568
- ) {
2569
- LazyVGrid(
2570
- columns: [GridItem(.adaptive(minimum: 180, maximum: 240), spacing: 10, alignment: .top)],
2571
- alignment: .leading,
2572
- spacing: 10
2573
- ) {
2574
- shortcutFactCard(
2575
- icon: "command",
2576
- title: "Global Hotkeys",
2577
- detail: "Edit palette, search, voice, and workspace actions without leaving settings."
2578
- )
2579
- shortcutFactCard(
2580
- icon: "rectangle.split.3x3",
2581
- title: "Spatial Tiling",
2582
- detail: "The layout grid mirrors the screen positions used by the menu bar app."
2583
- )
2584
- shortcutFactCard(
2585
- icon: "terminal",
2586
- title: "tmux Muscle Memory",
2587
- detail: "Keep the core pane controls visible here while you tune the app-level shortcuts."
2588
- )
2589
- }
2590
- }
2591
- }
2592
-
2593
- // MARK: - Shortcuts: App
2594
-
2595
- private var shortcutsAppCard: some View {
2596
- shortcutSectionCard(
2597
- title: "App & Workspace",
2598
- eyebrow: "Global",
2599
- summary: "Commands for opening primary surfaces and navigating the desktop companion."
2600
- ) {
2601
- VStack(alignment: .leading, spacing: 8) {
2602
- ForEach(HotkeyAction.allCases.filter { $0.group == .app }, id: \.rawValue) { action in
2603
- compactKeyRecorder(action: action)
2604
- }
2605
- }
2606
- }
2607
- }
2608
-
2609
- // MARK: - Shortcuts: Layers
2610
-
2611
- private var shortcutsLayersCard: some View {
2612
- shortcutSectionCard(
2613
- title: "Layers",
2614
- eyebrow: "Workspace Stack",
2615
- summary: "Direct jumps stay grouped separately from layer cycling so the numeric map is easier to scan."
2616
- ) {
2617
- VStack(alignment: .leading, spacing: 12) {
2618
- shortcutSubsectionLabel("Jump to a Layer")
2619
-
2620
- VStack(alignment: .leading, spacing: 8) {
2621
- ForEach(HotkeyAction.layerActions, id: \.rawValue) { action in
2622
- compactKeyRecorder(action: action)
2623
- }
2624
- }
2625
-
2626
- cardDivider
2627
-
2628
- shortcutSubsectionLabel("Cycle & Tag")
2629
-
2630
- VStack(alignment: .leading, spacing: 8) {
2631
- ForEach([HotkeyAction.layerPrev, .layerNext, .layerTag], id: \.rawValue) { action in
2632
- compactKeyRecorder(action: action)
2633
- }
2634
- }
2635
- }
2636
- }
2637
- }
2638
-
2639
- // MARK: - Shortcuts: Tiling
2640
-
2641
- private var shortcutsTilingVisualizer: some View {
2642
- VStack(alignment: .leading, spacing: 12) {
2643
- shortcutSubsectionLabel("Screen Regions")
2644
-
2645
- VStack(alignment: .leading, spacing: 10) {
2646
- VStack(spacing: 2) {
2647
- HStack(spacing: 2) {
2648
- tileCell(action: .tileTopLeft, label: "TL")
2649
- tileCell(action: .tileTop, label: "Top")
2650
- tileCell(action: .tileTopRight, label: "TR")
2651
- }
2652
- HStack(spacing: 2) {
2653
- tileCell(action: .tileLeft, label: "Left")
2654
- tileCell(action: .tileMaximize, label: "Max")
2655
- tileCell(action: .tileRight, label: "Right")
2656
- }
2657
- HStack(spacing: 2) {
2658
- tileCell(action: .tileBottomLeft, label: "BL")
2659
- tileCell(action: .tileBottom, label: "Bottom")
2660
- tileCell(action: .tileBottomRight, label: "BR")
2661
- }
2662
- }
2663
- .padding(8)
2664
- .background(shortcutsInsetPanel)
2665
-
2666
- VStack(alignment: .leading, spacing: 6) {
2667
- Text("Thirds")
2668
- .font(Typo.caption(10.5))
2669
- .foregroundColor(Palette.textMuted)
2670
-
2671
- HStack(spacing: 2) {
2672
- tileCell(action: .tileLeftThird, label: "\u{2153}L")
2673
- tileCell(action: .tileCenterThird, label: "\u{2153}C")
2674
- tileCell(action: .tileRightThird, label: "\u{2153}R")
2675
- }
2676
- }
2677
- .padding(8)
2678
- .background(shortcutsInsetPanel)
2679
-
2680
- Text("Use the grid as a visual legend for where each shortcut will place the focused window.")
2681
- .font(Typo.caption(10.5))
2682
- .foregroundColor(Palette.textMuted)
2683
- .fixedSize(horizontal: false, vertical: true)
2684
- }
2685
- }
2686
- }
2687
-
2688
- private var shortcutsTilingEditors: some View {
2689
- VStack(alignment: .leading, spacing: 12) {
2690
- shortcutSubsectionLabel("Editable Bindings")
2691
-
2692
- VStack(alignment: .leading, spacing: 8) {
2693
- ForEach([
2694
- HotkeyAction.tileLeft, .tileRight, .tileTop, .tileBottom,
2695
- .tileTopLeft, .tileTopRight, .tileBottomLeft, .tileBottomRight
2696
- ], id: \.rawValue) { action in
2697
- compactKeyRecorder(action: action)
2698
- }
2699
- }
2700
-
2701
- cardDivider
2702
-
2703
- shortcutSubsectionLabel("Layout Helpers")
2704
-
2705
- VStack(alignment: .leading, spacing: 8) {
2706
- ForEach([
2707
- HotkeyAction.tileLeftThird, .tileCenterThird, .tileRightThird,
2708
- .tileCenter, .tileMaximize, .tileDistribute, .tileTypeGrid
2709
- ], id: \.rawValue) { action in
2710
- compactKeyRecorder(action: action)
2711
- }
2712
- }
2713
- }
2714
- }
2715
-
2716
- // MARK: - Shortcuts: tmux
2717
-
2718
- private var shortcutsTmuxCard: some View {
2719
- shortcutSectionCard(
2720
- title: "Inside tmux",
2721
- eyebrow: "Reference",
2722
- summary: "These are tmux-native controls. They are shown here for fast recall and are not edited by the app."
2723
- ) {
2724
- VStack(alignment: .leading, spacing: 10) {
2725
- VStack(alignment: .leading, spacing: 8) {
2726
- shortcutRow("Detach", keys: ["Ctrl+B", "D"])
2727
- shortcutRow("Kill pane", keys: ["Ctrl+B", "X"])
2728
- shortcutRow("Pane left", keys: ["Ctrl+B", "\u{2190}"])
2729
- shortcutRow("Pane right", keys: ["Ctrl+B", "\u{2192}"])
2730
- shortcutRow("Zoom toggle", keys: ["Ctrl+B", "Z"])
2731
- shortcutRow("Scroll mode", keys: ["Ctrl+B", "["])
2732
- }
2733
- .padding(12)
2734
- .background(shortcutsInsetPanel)
2735
-
2736
- Text("Tip: use this as your quick memory jogger while editing the global shortcuts above.")
2737
- .font(Typo.caption(10.5))
2738
- .foregroundColor(Palette.textMuted)
2739
- .fixedSize(horizontal: false, vertical: true)
2740
- }
2741
-
2742
- compactKeyRecorder(action: .tileOrganize)
2743
- }
2744
- }
2745
-
2746
- // MARK: - Shortcut section UI
2747
-
2748
- private func shortcutSectionCard<Content: View>(
2749
- title: String,
2750
- eyebrow: String,
2751
- summary: String,
2752
- @ViewBuilder content: () -> Content
2753
- ) -> some View {
2754
- settingsCard {
2755
- VStack(alignment: .leading, spacing: 12) {
2756
- VStack(alignment: .leading, spacing: 5) {
2757
- Text(eyebrow.uppercased())
2758
- .font(Typo.pixel(12))
2759
- .foregroundColor(Palette.textDim)
2760
- .tracking(1)
2761
-
2762
- Text(title)
2763
- .font(Typo.monoBold(12))
2764
- .foregroundColor(Palette.text)
2765
-
2766
- Text(summary)
2767
- .font(Typo.caption(10.5))
2768
- .foregroundColor(Palette.textMuted)
2769
- .fixedSize(horizontal: false, vertical: true)
2770
- }
2771
-
2772
- content()
2773
- }
2774
- }
2775
- }
2776
-
2777
- private func shortcutFactCard(icon: String, title: String, detail: String) -> some View {
2778
- VStack(alignment: .leading, spacing: 8) {
2779
- Image(systemName: icon)
2780
- .font(.system(size: 12, weight: .semibold))
2781
- .foregroundColor(Palette.textDim)
2782
-
2783
- Text(title)
2784
- .font(Typo.monoBold(11))
2785
- .foregroundColor(Palette.text)
2786
-
2787
- Text(detail)
2788
- .font(Typo.caption(10))
2789
- .foregroundColor(Palette.textMuted)
2790
- .fixedSize(horizontal: false, vertical: true)
2791
- }
2792
- .frame(maxWidth: .infinity, alignment: .leading)
2793
- .padding(12)
2794
- .background(shortcutsInsetPanel)
2795
- }
2796
-
2797
- private func shortcutSubsectionLabel(_ title: String) -> some View {
2798
- Text(title.uppercased())
2799
- .font(Typo.pixel(11))
2800
- .foregroundColor(Palette.textDim)
2801
- .tracking(1)
2802
- }
2803
-
2804
- private var shortcutsInsetPanel: some View {
2805
- RoundedRectangle(cornerRadius: 8)
2806
- .fill(Color.black.opacity(0.22))
2807
- .overlay(
2808
- RoundedRectangle(cornerRadius: 8)
2809
- .strokeBorder(Palette.border, lineWidth: 0.5)
2810
- )
2811
- }
2812
-
2813
- private func relativeTimestamp(_ date: Date) -> String {
2814
- RelativeDateTimeFormatter().localizedString(for: date, relativeTo: Date())
2815
- }
2816
-
2817
- private func companionTrustedDevices(revision: Int) -> [DeckTrustedDeviceSummary] {
2818
- _ = revision
2819
- return LatticesCompanionSecurityCoordinator.shared.trustedDeviceSummaries()
2820
- }
2821
-
2822
- // MARK: - Tile cell (spatial grid item)
2823
-
2824
- private func tileCell(action: HotkeyAction, label: String) -> some View {
2825
- let binding = hotkeyStore.bindings[action]
2826
- let badgeText = binding?.displayParts.last ?? ""
2827
-
2828
- return Button {
2829
- // Open inline key recorder for this action
2830
- } label: {
2831
- VStack(spacing: 3) {
2832
- Text(label)
2833
- .font(Typo.caption(9))
2834
- .foregroundColor(Palette.textDim)
2835
- Text(badgeText)
2836
- .font(Typo.geistMonoBold(9))
2837
- .foregroundColor(Palette.text)
2838
- }
2839
- .frame(maxWidth: .infinity)
2840
- .frame(height: 42)
2841
- .background(
2842
- RoundedRectangle(cornerRadius: 4)
2843
- .fill(Palette.surface)
2844
- .overlay(
2845
- RoundedRectangle(cornerRadius: 4)
2846
- .strokeBorder(Palette.border, lineWidth: 0.5)
2847
- )
2848
- )
2849
- }
2850
- .buttonStyle(.plain)
2851
- .popover(isPresented: tileCellPopoverBinding(for: action)) {
2852
- KeyRecorderView(action: action, store: hotkeyStore)
2853
- .padding(12)
2854
- .frame(width: 300)
2855
- }
2856
- }
2857
-
2858
- @State private var expandedOcrWindow: UInt32?
2859
- @State private var collapsedOcrApps: Set<String> = []
2860
-
2861
- @State private var activeTilePopover: HotkeyAction?
2862
- @State private var selectedCompanionCockpitPageID = "main"
2863
- @State private var companionTrustRevision = 0
2864
-
2865
- private func tileCellPopoverBinding(for action: HotkeyAction) -> Binding<Bool> {
2866
- Binding(
2867
- get: { activeTilePopover == action },
2868
- set: { if !$0 { activeTilePopover = nil } }
2869
- )
2870
- }
2871
-
2872
- // MARK: - Compact key recorder
2873
-
2874
- private func compactKeyRecorder(action: HotkeyAction) -> some View {
2875
- KeyRecorderView(action: action, store: hotkeyStore)
2876
- }
2877
-
2878
- private func companionCockpitSlotMenu(
2879
- pageID: String,
2880
- index: Int,
2881
- shortcutID: String,
2882
- categories: [LatticesCompanionShortcutCategory]
2883
- ) -> some View {
2884
- let definition = LatticesCompanionCockpitCatalog.definition(for: shortcutID)
2885
- let label = definition?.title ?? "Empty"
2886
- let subtitle = definition?.subtitle ?? "Choose a shortcut"
2887
- let icon = definition?.iconSystemName ?? "square.dashed"
2888
-
2889
- return Menu {
2890
- Button("Empty Slot") {
2891
- prefs.updateCompanionCockpitSlot(pageID: pageID, index: index, shortcutID: "")
2892
- }
2893
-
2894
- ForEach(categories) { category in
2895
- let shortcuts = LatticesCompanionCockpitCatalog.shortcuts.filter {
2896
- $0.category == category && !$0.id.isEmpty
2897
- }
2898
- if !shortcuts.isEmpty {
2899
- Section(category.title) {
2900
- ForEach(shortcuts) { shortcut in
2901
- Button {
2902
- prefs.updateCompanionCockpitSlot(
2903
- pageID: pageID,
2904
- index: index,
2905
- shortcutID: shortcut.id
2906
- )
2907
- } label: {
2908
- Label(shortcut.title, systemImage: shortcut.iconSystemName)
2909
- }
2910
- }
2911
- }
2912
- }
2913
- }
2914
- } label: {
2915
- VStack(alignment: .leading, spacing: 8) {
2916
- HStack(alignment: .top) {
2917
- Text("Slot \(index + 1)")
2918
- .font(Typo.pixel(10))
2919
- .foregroundColor(Palette.textDim)
2920
- Spacer(minLength: 0)
2921
- Image(systemName: "chevron.up.chevron.down")
2922
- .font(.system(size: 10, weight: .semibold))
2923
- .foregroundColor(Palette.textMuted)
2924
- }
2925
-
2926
- Image(systemName: icon)
2927
- .font(.system(size: 14, weight: .semibold))
2928
- .foregroundColor(Palette.textDim)
2929
-
2930
- Text(label)
2931
- .font(Typo.monoBold(11))
2932
- .foregroundColor(Palette.text)
2933
- .lineLimit(2)
2934
-
2935
- Text(subtitle)
2936
- .font(Typo.caption(9.5))
2937
- .foregroundColor(Palette.textMuted)
2938
- .lineLimit(3)
2939
- .fixedSize(horizontal: false, vertical: true)
2940
- }
2941
- .frame(maxWidth: .infinity, minHeight: 112, alignment: .topLeading)
2942
- .padding(10)
2943
- .background(
2944
- RoundedRectangle(cornerRadius: 8)
2945
- .fill(Palette.surface)
2946
- .overlay(
2947
- RoundedRectangle(cornerRadius: 8)
2948
- .strokeBorder(Palette.border, lineWidth: 0.5)
2949
- )
2950
- )
2951
- }
2952
- .buttonStyle(.plain)
2953
- }
2954
-
2955
- // MARK: - Shortcut row (read-only, for tmux)
2956
-
2957
- private func shortcutRow(_ label: String, keys: [String]) -> some View {
2958
- HStack {
2959
- Text(label)
2960
- .font(Typo.caption(11))
2961
- .foregroundColor(Palette.textDim)
2962
- .frame(width: 80, alignment: .trailing)
2963
-
2964
- HStack(spacing: 4) {
2965
- ForEach(keys, id: \.self) { key in
2966
- keyBadge(key)
2967
- }
2968
- }
2969
- .padding(.leading, 8)
2970
-
2971
- Spacer()
2972
- }
2973
- }
2974
-
2975
- // MARK: - Docs
2976
-
2977
- private var docsContent: some View {
2978
- ScrollView {
2979
- LazyVStack(alignment: .leading, spacing: 0, pinnedViews: .sectionHeaders) {
2980
- Section(header: stickyHeader("What is lattices?")) {
2981
- 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.")
2982
- .font(Typo.caption(11))
2983
- .foregroundColor(Palette.textDim)
2984
- .lineSpacing(3)
2985
- .padding(.horizontal, 20)
2986
- .padding(.vertical, 12)
2987
- }
2988
-
2989
- Section(header: stickyHeader("Glossary")) {
2990
- VStack(alignment: .leading, spacing: 12) {
2991
- glossaryItem("Session",
2992
- "A persistent workspace that lives in the background. Survives terminal crashes, disconnects, even closing your laptop.")
2993
- glossaryItem("Pane",
2994
- "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.")
2995
- glossaryItem("Attach",
2996
- "Connect your terminal window to an existing session. The session was already running \u{2014} you\u{2019}re just viewing it.")
2997
- glossaryItem("Detach",
2998
- "Disconnect your terminal but keep the session alive. Your dev server keeps running, Claude keeps thinking.")
2999
- glossaryItem("tmux",
3000
- "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.")
3001
- }
3002
- .padding(.horizontal, 20)
3003
- .padding(.vertical, 12)
3004
- }
3005
-
3006
- Section(header: stickyHeader("How it works")) {
3007
- VStack(alignment: .leading, spacing: 8) {
3008
- flowStep("1", "Create a .lattices.json in your project root")
3009
- flowStep("2", "lattices reads the config and builds a tmux session")
3010
- flowStep("3", "Each pane gets its command (claude, dev server, etc.)")
3011
- flowStep("4", "Session persists in the background until you kill it")
3012
- flowStep("5", "Attach and detach from any terminal, any time")
3013
- }
3014
- .padding(.horizontal, 20)
3015
- .padding(.vertical, 12)
3016
- }
3017
-
3018
- Section(header: stickyHeader("Voice commands")) {
3019
- VStack(alignment: .leading, spacing: 8) {
3020
- flowStep("⌥", "Hold Option key to speak, release to stop")
3021
- flowStep("⇥", "Tab to arm/disarm the mic")
3022
- flowStep("⎋", "Escape to dismiss")
3023
-
3024
- Text("Built-in commands: find, show, open, tile, kill, scan")
3025
- .font(Typo.caption(10.5))
3026
- .foregroundColor(Palette.textMuted)
3027
- .padding(.top, 4)
3028
-
3029
- Text("When local matching fails, Claude Haiku advises with follow-up suggestions. Configure the AI model and budget in Settings → AI.")
3030
- .font(Typo.caption(10.5))
3031
- .foregroundColor(Palette.textMuted)
3032
- .lineSpacing(2)
3033
- }
3034
- .padding(.horizontal, 20)
3035
- .padding(.vertical, 12)
3036
- }
3037
-
3038
- Section(header: stickyHeader("Reference")) {
3039
- HStack(spacing: 8) {
3040
- docsLinkButton(icon: "doc.text", label: "Config format", file: "config.md")
3041
- docsLinkButton(icon: "book", label: "Full concepts", file: "concepts.md")
3042
- footerActionButton(icon: "stethoscope", label: "Diagnostics") {
3043
- DiagnosticWindow.shared.show()
3044
- }
3045
- }
3046
- .padding(.horizontal, 20)
3047
- .padding(.vertical, 12)
3048
- }
3049
- }
3050
- }
3051
- }
3052
-
3053
- // MARK: - Docs helpers
3054
-
3055
- private func glossaryItem(_ term: String, _ definition: String) -> some View {
3056
- VStack(alignment: .leading, spacing: 3) {
3057
- Text(term)
3058
- .font(Typo.monoBold(11))
3059
- .foregroundColor(Palette.text)
3060
- Text(definition)
3061
- .font(Typo.caption(10.5))
3062
- .foregroundColor(Palette.textMuted)
3063
- .lineSpacing(2)
3064
- }
3065
- }
3066
-
3067
- private func flowStep(_ number: String, _ text: String) -> some View {
3068
- HStack(alignment: .top, spacing: 8) {
3069
- Text(number)
3070
- .font(Typo.monoBold(10))
3071
- .foregroundColor(Palette.running)
3072
- .frame(width: 14)
3073
- Text(text)
3074
- .font(Typo.caption(11))
3075
- .foregroundColor(Palette.textDim)
3076
- }
3077
- }
3078
-
3079
- private func docsLinkButton(icon: String, label: String, file: String) -> some View {
3080
- Button {
3081
- let path = resolveDocsFile(file)
3082
- NSWorkspace.shared.open(URL(fileURLWithPath: path))
3083
- } label: {
3084
- footerActionLabel(icon: icon, label: label)
3085
- }
3086
- .buttonStyle(.plain)
3087
- }
3088
-
3089
- private func footerActionButton(icon: String, label: String, action: @escaping () -> Void) -> some View {
3090
- Button(action: action) {
3091
- footerActionLabel(icon: icon, label: label)
3092
- }
3093
- .buttonStyle(.plain)
3094
- }
3095
-
3096
- private func footerActionLabel(icon: String, label: String) -> some View {
3097
- HStack(spacing: 6) {
3098
- Image(systemName: icon)
3099
- .font(.system(size: 10))
3100
- Text(label)
3101
- .font(Typo.caption(11))
3102
- }
3103
- .foregroundColor(Palette.textDim)
3104
- .padding(.horizontal, 12)
3105
- .padding(.vertical, 6)
3106
- .background(
3107
- RoundedRectangle(cornerRadius: 3)
3108
- .fill(Palette.surface)
3109
- .overlay(
3110
- RoundedRectangle(cornerRadius: 3)
3111
- .strokeBorder(Palette.border, lineWidth: 0.5)
3112
- )
3113
- )
3114
- }
3115
-
3116
- private func resolveDocsFile(_ file: String) -> String {
3117
- let bundle = Bundle.main.bundlePath
3118
- let appDir = (bundle as NSString).deletingLastPathComponent
3119
- let docsPath = ((appDir as NSString).appendingPathComponent("../docs/\(file)") as NSString).standardizingPath
3120
- if FileManager.default.fileExists(atPath: docsPath) { return docsPath }
3121
- // Fallback: look relative to the repo root (dev builds)
3122
- let repoGuess = ((appDir as NSString).appendingPathComponent("../../docs/\(file)") as NSString).standardizingPath
3123
- return FileManager.default.fileExists(atPath: repoGuess) ? repoGuess : docsPath
3124
- }
3125
-
3126
- // MARK: - Shared helpers
3127
-
3128
- private var separator: some View {
3129
- Rectangle()
3130
- .fill(Palette.border)
3131
- .frame(height: 0.5)
3132
- }
3133
-
3134
- private func settingsRow<Content: View>(_ label: String, @ViewBuilder content: () -> Content) -> some View {
3135
- HStack(alignment: .top, spacing: 0) {
3136
- Text(label)
3137
- .font(Typo.caption(11))
3138
- .foregroundColor(Palette.textDim)
3139
- .frame(width: 100, alignment: .trailing)
3140
- .padding(.top, 2)
3141
-
3142
- content()
3143
- .padding(.leading, 16)
3144
- .frame(maxWidth: .infinity, alignment: .leading)
3145
- }
3146
- }
3147
-
3148
- private func keyBadge(_ key: String) -> some View {
3149
- Text(key)
3150
- .font(Typo.geistMonoBold(10))
3151
- .foregroundColor(Palette.text)
3152
- .padding(.horizontal, 6)
3153
- .padding(.vertical, 3)
3154
- .background(
3155
- RoundedRectangle(cornerRadius: 3)
3156
- .fill(Palette.surface)
3157
- .overlay(
3158
- RoundedRectangle(cornerRadius: 3)
3159
- .strokeBorder(Palette.border, lineWidth: 0.5)
3160
- )
3161
- )
3162
- }
3163
- }