@lattices/cli 0.4.10 → 0.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +13 -13
  3. package/{app → apps/mac}/Lattices.app/Contents/Info.plist +10 -2
  4. package/{app → apps/mac}/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/{app → apps/mac}/Package.swift +2 -1
  6. package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
  7. package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
  8. package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
  9. package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
  10. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +27 -0
  11. package/apps/mac/Sources/AppShell/AppDelegate.swift +189 -0
  12. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +25 -0
  13. package/{app → apps/mac}/Sources/AppShell/AppShellView.swift +18 -3
  14. package/{app → apps/mac}/Sources/AppShell/AppUpdater.swift +4 -3
  15. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +87 -0
  16. package/{app → apps/mac}/Sources/AppShell/LatticesRuntime.swift +43 -0
  17. package/{app → apps/mac}/Sources/AppShell/MainView.swift +116 -63
  18. package/apps/mac/Sources/AppShell/MenuBarController.swift +177 -0
  19. package/{app → apps/mac}/Sources/AppShell/OnboardingView.swift +72 -60
  20. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +366 -0
  21. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +70 -0
  22. package/{app → apps/mac}/Sources/AppShell/Preferences.swift +37 -2
  23. package/{app → apps/mac}/Sources/AppShell/SettingsView.swift +815 -156
  24. package/{app → apps/mac}/Sources/AppShell/SettingsWindow.swift +10 -0
  25. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +13 -0
  26. package/{app → apps/mac}/Sources/Core/Actions/HotkeyStore.swift +6 -1
  27. package/{app → apps/mac}/Sources/Core/Actions/IntentEngine.swift +2 -0
  28. package/{app → apps/mac}/Sources/Core/Daemon/DaemonServer.swift +5 -0
  29. package/{app → apps/mac}/Sources/Core/Daemon/LatticesApi.swift +365 -0
  30. package/{app → apps/mac}/Sources/Core/Desktop/OcrModel.swift +17 -13
  31. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +33 -0
  32. package/{app → apps/mac}/Sources/Core/Desktop/WindowDragSnapController.swift +18 -217
  33. package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewStore.swift +4 -5
  34. package/{app → apps/mac}/Sources/Core/Desktop/WindowTiler.swift +19 -13
  35. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +124 -0
  36. package/apps/mac/Sources/Core/Input/EventTapThread.swift +54 -0
  37. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +20 -0
  38. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +335 -0
  39. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +141 -0
  40. package/{app → apps/mac}/Sources/Core/Input/MouseGestureConfig.swift +155 -20
  41. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +2259 -0
  42. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +170 -0
  43. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +39 -0
  44. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +624 -0
  45. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +56 -0
  46. package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +8 -8
  47. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +1240 -0
  48. package/{app → apps/mac}/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +11 -23
  49. package/{app → apps/mac}/Sources/Core/Pi/PiChatDock.swift +90 -43
  50. package/{app → apps/mac}/Sources/Core/Pi/PiChatSession.swift +676 -43
  51. package/{app → apps/mac}/Sources/Core/Pi/PiProviderSetupCallout.swift +5 -5
  52. package/{app → apps/mac}/Sources/Core/Pi/PiWorkspaceView.swift +93 -44
  53. package/apps/mac/Sources/Core/System/Capability.swift +79 -0
  54. package/{app → apps/mac}/Sources/Core/System/PermissionChecker.swift +43 -8
  55. package/{app → apps/mac}/Sources/Core/Voice/AudioProvider.swift +225 -56
  56. package/bin/handsoff-infer.ts +14 -5
  57. package/bin/handsoff-worker.ts +11 -7
  58. package/bin/infer.ts +406 -0
  59. package/bin/lattices-app.ts +57 -7
  60. package/bin/lattices-dev +40 -1
  61. package/bin/lattices.ts +1 -1
  62. package/docs/agent-execution-plan.md +9 -9
  63. package/docs/api.md +119 -0
  64. package/docs/app.md +1 -0
  65. package/docs/companion-deck.md +1 -1
  66. package/docs/gesture-customization-proposal.md +520 -0
  67. package/docs/mouse-gestures.md +79 -0
  68. package/docs/overview.md +2 -2
  69. package/docs/presentation-execution-review.md +9 -9
  70. package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
  71. package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
  72. package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
  73. package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
  74. package/docs/reference/dewey.config.ts +74 -0
  75. package/docs/reference/install-agent.md +79 -0
  76. package/docs/repo-structure.md +100 -0
  77. package/docs/voice-error-model.md +7 -7
  78. package/docs/voice.md +18 -0
  79. package/package.json +23 -13
  80. package/swift/Package.swift +20 -0
  81. package/swift/Sources/DeckKit/DeckAction.swift +51 -0
  82. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +152 -0
  83. package/swift/Sources/DeckKit/DeckCockpit.swift +82 -0
  84. package/swift/Sources/DeckKit/DeckHost.swift +7 -0
  85. package/swift/Sources/DeckKit/DeckManifest.swift +145 -0
  86. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +533 -0
  87. package/swift/Sources/DeckKit/DeckTrackpad.swift +63 -0
  88. package/swift/Sources/DeckKit/DeckValue.swift +93 -0
  89. package/swift/Sources/DeckKit/DeckVoiceError.swift +88 -0
  90. package/swift/Tests/DeckKitTests/DeckKitTests.swift +286 -0
  91. package/app/Sources/AppShell/AppDelegate.swift +0 -408
  92. package/app/Sources/Core/Input/KeyboardRemapController.swift +0 -184
  93. package/app/Sources/Core/Input/KeyboardRemapStore.swift +0 -84
  94. package/app/Sources/Core/Input/MouseGestureController.swift +0 -1203
  95. package/app/Sources/Core/Input/MouseShortcutStore.swift +0 -107
  96. /package/{app → apps/mac}/Info.plist +0 -0
  97. /package/{app → apps/mac}/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  98. /package/{app → apps/mac}/Lattices.app/Contents/Resources/tap.wav +0 -0
  99. /package/{app → apps/mac}/Lattices.app/Contents/_CodeSignature/CodeResources +0 -0
  100. /package/{app → apps/mac}/Lattices.entitlements +0 -0
  101. /package/{app → apps/mac}/Resources/tap.wav +0 -0
  102. /package/{app → apps/mac}/Sources/AppShell/App.swift +0 -0
  103. /package/{app → apps/mac}/Sources/AppShell/CliActionLauncher.swift +0 -0
  104. /package/{app → apps/mac}/Sources/AppShell/HomeDashboardView.swift +0 -0
  105. /package/{app → apps/mac}/Sources/AppShell/KeyRecorderView.swift +0 -0
  106. /package/{app → apps/mac}/Sources/AppShell/MainWindow.swift +0 -0
  107. /package/{app → apps/mac}/Sources/Core/Actions/HotkeyManager.swift +0 -0
  108. /package/{app → apps/mac}/Sources/Core/Actions/IntentSchema.swift +0 -0
  109. /package/{app → apps/mac}/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -0
  110. /package/{app → apps/mac}/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -0
  111. /package/{app → apps/mac}/Sources/Core/Actions/Intents/FocusIntent.swift +0 -0
  112. /package/{app → apps/mac}/Sources/Core/Actions/Intents/HelpIntent.swift +0 -0
  113. /package/{app → apps/mac}/Sources/Core/Actions/Intents/KillIntent.swift +0 -0
  114. /package/{app → apps/mac}/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -0
  115. /package/{app → apps/mac}/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -0
  116. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -0
  117. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -0
  118. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ScanIntent.swift +0 -0
  119. /package/{app → apps/mac}/Sources/Core/Actions/Intents/SearchIntent.swift +0 -0
  120. /package/{app → apps/mac}/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -0
  121. /package/{app → apps/mac}/Sources/Core/Actions/Intents/TileIntent.swift +0 -0
  122. /package/{app → apps/mac}/Sources/Core/Actions/PaletteCommand.swift +0 -0
  123. /package/{app → apps/mac}/Sources/Core/Actions/VoiceIntentResolver.swift +0 -0
  124. /package/{app → apps/mac}/Sources/Core/Companion/CompanionActivityLog.swift +0 -0
  125. /package/{app → apps/mac}/Sources/Core/Companion/CompanionKeyboardController.swift +0 -0
  126. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -0
  127. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -0
  128. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -0
  129. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -0
  130. /package/{app → apps/mac}/Sources/Core/Companion/LatticesDeckHost.swift +0 -0
  131. /package/{app → apps/mac}/Sources/Core/Daemon/DaemonProtocol.swift +0 -0
  132. /package/{app → apps/mac}/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -0
  133. /package/{app → apps/mac}/Sources/Core/Desktop/AppTypeClassifier.swift +0 -0
  134. /package/{app → apps/mac}/Sources/Core/Desktop/DesktopModel.swift +0 -0
  135. /package/{app → apps/mac}/Sources/Core/Desktop/DesktopModelTypes.swift +0 -0
  136. /package/{app → apps/mac}/Sources/Core/Desktop/InventoryManager.swift +0 -0
  137. /package/{app → apps/mac}/Sources/Core/Desktop/InventoryPath.swift +0 -0
  138. /package/{app → apps/mac}/Sources/Core/Desktop/MouseFinder.swift +0 -0
  139. /package/{app → apps/mac}/Sources/Core/Desktop/OcrStore.swift +0 -0
  140. /package/{app → apps/mac}/Sources/Core/Desktop/PlacementSpec.swift +0 -0
  141. /package/{app → apps/mac}/Sources/Core/Desktop/SessionWindowLocator.swift +0 -0
  142. /package/{app → apps/mac}/Sources/Core/Desktop/TilePickerView.swift +0 -0
  143. /package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewCard.swift +0 -0
  144. /package/{app → apps/mac}/Sources/Core/Desktop/WindowSelectionStore.swift +0 -0
  145. /package/{app → apps/mac}/Sources/Core/Input/KeyboardRemapConfig.swift +0 -0
  146. /package/{app → apps/mac}/Sources/Core/Input/MouseInputDeviceStore.swift +0 -0
  147. /package/{app → apps/mac}/Sources/Core/Input/MouseInputEventViewer.swift +0 -0
  148. /package/{app → apps/mac}/Sources/Core/Overlays/AppWindowShell.swift +0 -0
  149. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -0
  150. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -0
  151. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -0
  152. /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -0
  153. /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -0
  154. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -0
  155. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -0
  156. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDController.swift +0 -0
  157. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -0
  158. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -0
  159. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -0
  160. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDState.swift +0 -0
  161. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -0
  162. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -0
  163. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -0
  164. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -0
  165. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -0
  166. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -0
  167. /package/{app → apps/mac}/Sources/Core/Overlays/OverlayPanelShell.swift +0 -0
  168. /package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -0
  169. /package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -0
  170. /package/{app → apps/mac}/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -0
  171. /package/{app → apps/mac}/Sources/Core/Pi/PiAuthPromptCard.swift +0 -0
  172. /package/{app → apps/mac}/Sources/Core/Pi/PiInstallCallout.swift +0 -0
  173. /package/{app → apps/mac}/Sources/Core/System/DiagnosticLog.swift +0 -0
  174. /package/{app → apps/mac}/Sources/Core/System/EventBus.swift +0 -0
  175. /package/{app → apps/mac}/Sources/Core/System/ProcessModel.swift +0 -0
  176. /package/{app → apps/mac}/Sources/Core/System/ProcessQuery.swift +0 -0
  177. /package/{app → apps/mac}/Sources/Core/System/SystemTelemetryMonitor.swift +0 -0
  178. /package/{app → apps/mac}/Sources/Core/Voice/AdvisorLearningStore.swift +0 -0
  179. /package/{app → apps/mac}/Sources/Core/Voice/AgentSession.swift +0 -0
  180. /package/{app → apps/mac}/Sources/Core/Voice/HandsOffSession.swift +0 -0
  181. /package/{app → apps/mac}/Sources/Core/Voice/VoiceChatView.swift +0 -0
  182. /package/{app → apps/mac}/Sources/Core/Voice/VoxClient.swift +0 -0
  183. /package/{app → apps/mac}/Sources/Core/Workspace/Project.swift +0 -0
  184. /package/{app → apps/mac}/Sources/Core/Workspace/ProjectScanner.swift +0 -0
  185. /package/{app → apps/mac}/Sources/Core/Workspace/SessionLayerStore.swift +0 -0
  186. /package/{app → apps/mac}/Sources/Core/Workspace/SessionManager.swift +0 -0
  187. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/Terminal.swift +0 -0
  188. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -0
  189. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -0
  190. /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -0
  191. /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -0
  192. /package/{app → apps/mac}/Sources/Core/Workspace/WorkspaceManager.swift +0 -0
  193. /package/{app → apps/mac}/Sources/UI/ActionRow.swift +0 -0
  194. /package/{app → apps/mac}/Sources/UI/OrphanRow.swift +0 -0
  195. /package/{app → apps/mac}/Sources/UI/ProjectRow.swift +0 -0
  196. /package/{app → apps/mac}/Sources/UI/TabGroupRow.swift +0 -0
  197. /package/{app → apps/mac}/Sources/UI/Theme.swift +0 -0
  198. /package/{app → apps/mac}/Tests/StageDragTests.swift +0 -0
  199. /package/{app → apps/mac}/Tests/StageJoinTests.swift +0 -0
  200. /package/{app → apps/mac}/Tests/StageManagerTests.swift +0 -0
  201. /package/{app → apps/mac}/Tests/StageTileTests.swift +0 -0
@@ -0,0 +1,366 @@
1
+ import AppKit
2
+ import SwiftUI
3
+
4
+ /// Calm, opt-in setup surface for capabilities that require an OS permission.
5
+ /// Never opens automatically — only from explicit entry points
6
+ /// (banner button, onboarding row, Settings, feature gate).
7
+ struct PermissionsAssistantView: View {
8
+ @ObservedObject private var permChecker = PermissionChecker.shared
9
+ @ObservedObject private var prefs = Preferences.shared
10
+ @Binding var selected: Capability
11
+ var onClose: () -> Void
12
+
13
+ var body: some View {
14
+ HStack(spacing: 0) {
15
+ sidebar
16
+ .frame(width: 240, alignment: .top)
17
+
18
+ Rectangle()
19
+ .fill(Palette.border)
20
+ .frame(width: 0.5)
21
+ .frame(maxHeight: .infinity)
22
+
23
+ detail
24
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
25
+ }
26
+ .background(PanelBackground())
27
+ .preferredColorScheme(.dark)
28
+ }
29
+
30
+ // MARK: - Sidebar
31
+
32
+ private var sidebar: some View {
33
+ VStack(alignment: .leading, spacing: 14) {
34
+ VStack(alignment: .leading, spacing: 6) {
35
+ Text("CAPABILITIES")
36
+ .font(Typo.pixel(14))
37
+ .foregroundColor(Palette.textDim)
38
+ .tracking(1)
39
+ Text("Lattices works with whichever of these you turn on. Nothing here runs unless you press the button.")
40
+ .font(Typo.caption(11))
41
+ .foregroundColor(Palette.textMuted)
42
+ .fixedSize(horizontal: false, vertical: true)
43
+ }
44
+
45
+ VStack(spacing: 6) {
46
+ ForEach(Capability.allCases) { cap in
47
+ sidebarRow(cap)
48
+ }
49
+ }
50
+
51
+ Spacer(minLength: 0)
52
+ }
53
+ .padding(16)
54
+ }
55
+
56
+ private func sidebarRow(_ cap: Capability) -> some View {
57
+ let active = selected == cap
58
+ let granted = cap.isGranted
59
+
60
+ return Button {
61
+ selected = cap
62
+ } label: {
63
+ HStack(alignment: .top, spacing: 10) {
64
+ Image(systemName: cap.iconName)
65
+ .font(.system(size: 11, weight: .semibold))
66
+ .foregroundColor(active ? Palette.text : Palette.textMuted)
67
+ .frame(width: 16, alignment: .center)
68
+
69
+ VStack(alignment: .leading, spacing: 3) {
70
+ Text(cap.title)
71
+ .font(Typo.mono(11))
72
+ .foregroundColor(active ? Palette.text : Palette.textMuted)
73
+
74
+ Text(cap.requirementLabel)
75
+ .font(Typo.caption(9.5))
76
+ .foregroundColor(Palette.textMuted.opacity(active ? 0.9 : 0.7))
77
+ .lineLimit(2)
78
+ }
79
+
80
+ Spacer(minLength: 0)
81
+
82
+ statusDot(granted: granted)
83
+ .padding(.top, 2)
84
+ }
85
+ .padding(.horizontal, 10)
86
+ .padding(.vertical, 8)
87
+ .background(
88
+ RoundedRectangle(cornerRadius: 5)
89
+ .fill(active ? Palette.surfaceHov : Color.clear)
90
+ )
91
+ }
92
+ .buttonStyle(.plain)
93
+ }
94
+
95
+ private func statusDot(granted: Bool) -> some View {
96
+ Circle()
97
+ .fill(granted ? Palette.running : Palette.detach)
98
+ .frame(width: 6, height: 6)
99
+ }
100
+
101
+ // MARK: - Detail
102
+
103
+ private var detail: some View {
104
+ VStack(alignment: .leading, spacing: 0) {
105
+ hero(selected)
106
+ .padding(.horizontal, 24)
107
+ .padding(.top, 22)
108
+ .padding(.bottom, 16)
109
+
110
+ Rectangle().fill(Palette.border).frame(height: 0.5)
111
+
112
+ ScrollView {
113
+ VStack(alignment: .leading, spacing: 18) {
114
+ valueCard(selected)
115
+ statusCard(selected)
116
+ actionsCard(selected)
117
+
118
+ if prefs.isCapabilityDismissed(selected.rawValue) {
119
+ Text("You snoozed this earlier. We will not nag — opening this from a feature will surface it again.")
120
+ .font(Typo.caption(10))
121
+ .foregroundColor(Palette.textMuted)
122
+ .padding(.horizontal, 4)
123
+ }
124
+ }
125
+ .padding(.horizontal, 24)
126
+ .padding(.vertical, 18)
127
+ }
128
+ }
129
+ }
130
+
131
+ private func hero(_ cap: Capability) -> some View {
132
+ HStack(alignment: .center, spacing: 14) {
133
+ ZStack {
134
+ RoundedRectangle(cornerRadius: 8)
135
+ .fill(Palette.surface)
136
+ .frame(width: 40, height: 40)
137
+ Image(systemName: cap.iconName)
138
+ .font(.system(size: 16, weight: .semibold))
139
+ .foregroundColor(.white.opacity(0.85))
140
+ }
141
+
142
+ VStack(alignment: .leading, spacing: 4) {
143
+ Text(cap.title)
144
+ .font(Typo.heading(14))
145
+ .foregroundColor(Palette.text)
146
+ Text(cap.requirementLabel)
147
+ .font(Typo.mono(10))
148
+ .foregroundColor(Palette.textMuted)
149
+ }
150
+
151
+ Spacer()
152
+
153
+ buildChannelBadge
154
+ statusBadge(cap)
155
+
156
+ Button(action: onClose) {
157
+ Image(systemName: "xmark")
158
+ .font(.system(size: 10, weight: .bold))
159
+ .foregroundColor(Palette.textMuted)
160
+ .frame(width: 22, height: 22)
161
+ }
162
+ .buttonStyle(.plain)
163
+ }
164
+ }
165
+
166
+ private func statusBadge(_ cap: Capability) -> some View {
167
+ let granted = cap.isGranted
168
+ let label = granted ? "ON" : (prefs.isCapabilityDismissed(cap.rawValue) ? "SNOOZED" : "OFF")
169
+ let color: Color = granted ? Palette.running : Palette.detach
170
+
171
+ return Text(label)
172
+ .font(Typo.monoBold(9))
173
+ .foregroundColor(color)
174
+ .padding(.horizontal, 6)
175
+ .padding(.vertical, 3)
176
+ .background(
177
+ Capsule().fill(color.opacity(0.12))
178
+ )
179
+ }
180
+
181
+ private var buildChannelBadge: some View {
182
+ let tint = LatticesRuntime.isDevBuild ? Palette.detach : Palette.running
183
+
184
+ return Text(LatticesRuntime.buildChannelLabel)
185
+ .font(Typo.monoBold(9))
186
+ .foregroundColor(tint)
187
+ .padding(.horizontal, 6)
188
+ .padding(.vertical, 3)
189
+ .background(
190
+ Capsule()
191
+ .fill(tint.opacity(0.12))
192
+ .overlay(
193
+ Capsule()
194
+ .strokeBorder(tint.opacity(0.28), lineWidth: 0.5)
195
+ )
196
+ )
197
+ }
198
+
199
+ private func valueCard(_ cap: Capability) -> some View {
200
+ sectionCard(title: "WHAT YOU GET") {
201
+ VStack(alignment: .leading, spacing: 6) {
202
+ Text(cap.pitch)
203
+ .font(Typo.body(12))
204
+ .foregroundColor(Palette.text)
205
+ .lineSpacing(3)
206
+ Text(cap.why)
207
+ .font(Typo.caption(10))
208
+ .foregroundColor(Palette.textMuted)
209
+ .lineSpacing(2)
210
+ }
211
+ }
212
+ }
213
+
214
+ private func statusCard(_ cap: Capability) -> some View {
215
+ sectionCard(title: "STATUS") {
216
+ HStack(spacing: 8) {
217
+ Image(systemName: cap.isGranted ? "checkmark.circle.fill" : "exclamationmark.circle")
218
+ .font(.system(size: 12))
219
+ .foregroundColor(cap.isGranted ? Palette.running : Palette.detach)
220
+ Text(cap.isGranted ? cap.whenGrantedDetail : "Not enabled. Lattices works without it; the rest of the app stays usable.")
221
+ .font(Typo.mono(11))
222
+ .foregroundColor(Palette.text)
223
+ Spacer(minLength: 0)
224
+ }
225
+ }
226
+ }
227
+
228
+ private func actionsCard(_ cap: Capability) -> some View {
229
+ sectionCard(title: "ACTIONS") {
230
+ VStack(alignment: .leading, spacing: 10) {
231
+ primaryAction(cap)
232
+
233
+ HStack(spacing: 10) {
234
+ if !cap.isGranted {
235
+ Button {
236
+ prefs.dismissCapability(cap.rawValue)
237
+ } label: {
238
+ Text(prefs.isCapabilityDismissed(cap.rawValue) ? "Snoozed" : "Maybe later")
239
+ .font(Typo.monoBold(10))
240
+ .foregroundColor(Palette.textMuted)
241
+ .padding(.horizontal, 10)
242
+ .padding(.vertical, 6)
243
+ .background(
244
+ RoundedRectangle(cornerRadius: 4)
245
+ .fill(Palette.surface)
246
+ .overlay(
247
+ RoundedRectangle(cornerRadius: 4)
248
+ .strokeBorder(Palette.border, lineWidth: 0.5)
249
+ )
250
+ )
251
+ }
252
+ .buttonStyle(.plain)
253
+ .disabled(prefs.isCapabilityDismissed(cap.rawValue))
254
+ }
255
+
256
+ Button {
257
+ openSystemSettings(cap)
258
+ } label: {
259
+ HStack(spacing: 4) {
260
+ Image(systemName: "arrow.up.forward.app")
261
+ .font(.system(size: 9))
262
+ Text("Open System Settings")
263
+ .font(Typo.monoBold(10))
264
+ }
265
+ .foregroundColor(Palette.textMuted)
266
+ .padding(.horizontal, 10)
267
+ .padding(.vertical, 6)
268
+ .background(
269
+ RoundedRectangle(cornerRadius: 4)
270
+ .fill(Palette.surface)
271
+ .overlay(
272
+ RoundedRectangle(cornerRadius: 4)
273
+ .strokeBorder(Palette.border, lineWidth: 0.5)
274
+ )
275
+ )
276
+ }
277
+ .buttonStyle(.plain)
278
+ }
279
+
280
+ Text(actionFootnote(cap))
281
+ .font(Typo.caption(9.5))
282
+ .foregroundColor(Palette.textMuted)
283
+ }
284
+ }
285
+ }
286
+
287
+ @ViewBuilder
288
+ private func primaryAction(_ cap: Capability) -> some View {
289
+ if cap.isGranted {
290
+ HStack(spacing: 6) {
291
+ Image(systemName: "checkmark.circle.fill")
292
+ .foregroundColor(Palette.running)
293
+ Text("Enabled")
294
+ .font(Typo.monoBold(11))
295
+ .foregroundColor(Palette.running)
296
+ }
297
+ .padding(.vertical, 4)
298
+ } else {
299
+ Button {
300
+ triggerPrimary(cap)
301
+ } label: {
302
+ Text(primaryLabel(cap))
303
+ .angularButton(.white, filled: false)
304
+ }
305
+ .buttonStyle(.plain)
306
+ }
307
+ }
308
+
309
+ private func primaryLabel(_ cap: Capability) -> String {
310
+ switch cap {
311
+ case .windowControl: return "Request Accessibility"
312
+ case .screenSearch: return "Enable OCR"
313
+ }
314
+ }
315
+
316
+ private func actionFootnote(_ cap: Capability) -> String {
317
+ switch cap {
318
+ case .windowControl:
319
+ return "macOS will add Lattices to its Accessibility list. You finish the toggle in System Settings."
320
+ case .screenSearch:
321
+ return "Enabling this turns on OCR and asks macOS for Screen Recording on this Mac."
322
+ }
323
+ }
324
+
325
+ private func triggerPrimary(_ cap: Capability) {
326
+ // The primary action is the explicit user gesture — clear any prior snooze.
327
+ prefs.clearDismissal(cap.rawValue)
328
+
329
+ switch cap {
330
+ case .windowControl:
331
+ permChecker.requestAccessibility()
332
+ case .screenSearch:
333
+ // Turning on OCR is the moment we ask for Screen Recording.
334
+ OcrModel.shared.setEnabled(true)
335
+ }
336
+ }
337
+
338
+ private func openSystemSettings(_ cap: Capability) {
339
+ switch cap {
340
+ case .windowControl: permChecker.openAccessibilitySettings()
341
+ case .screenSearch: permChecker.openScreenRecordingSettings()
342
+ }
343
+ }
344
+
345
+ // MARK: - Section card
346
+
347
+ private func sectionCard<Content: View>(title: String, @ViewBuilder content: () -> Content) -> some View {
348
+ VStack(alignment: .leading, spacing: 8) {
349
+ Text(title)
350
+ .font(Typo.pixel(11))
351
+ .foregroundColor(Palette.textDim)
352
+ .tracking(1)
353
+ content()
354
+ .padding(12)
355
+ .frame(maxWidth: .infinity, alignment: .leading)
356
+ .background(
357
+ RoundedRectangle(cornerRadius: 6)
358
+ .fill(Palette.surface)
359
+ .overlay(
360
+ RoundedRectangle(cornerRadius: 6)
361
+ .strokeBorder(Palette.border, lineWidth: 0.5)
362
+ )
363
+ )
364
+ }
365
+ }
366
+ }
@@ -0,0 +1,70 @@
1
+ import AppKit
2
+ import SwiftUI
3
+
4
+ /// Dedicated window that hosts the Permissions Assistant. Singleton, opened
5
+ /// only on explicit user intent (banner button, onboarding row, Settings,
6
+ /// feature gate). Never shown automatically on app launch.
7
+ final class PermissionsAssistantWindowController: ObservableObject {
8
+ static let shared = PermissionsAssistantWindowController()
9
+
10
+ private var window: NSWindow?
11
+ @Published private(set) var focusedCapability: Capability = .windowControl
12
+
13
+ var isVisible: Bool { window?.isVisible ?? false }
14
+
15
+ /// Open the assistant focused on the given capability. If `cap` is nil,
16
+ /// the first missing capability is selected, falling back to `windowControl`.
17
+ func show(focus cap: Capability? = nil) {
18
+ let target = cap ?? Capability.missing.first ?? .windowControl
19
+ focusedCapability = target
20
+
21
+ if let existing = window {
22
+ existing.makeKeyAndOrderFront(nil)
23
+ NSApp.activate(ignoringOtherApps: true)
24
+ return
25
+ }
26
+
27
+ let host = HostView(controller: self)
28
+
29
+ let w = AppWindowShell.makeWindow(
30
+ config: .init(
31
+ title: "Lattices Permissions",
32
+ titleVisible: false,
33
+ initialSize: NSSize(width: 720, height: 520),
34
+ minSize: NSSize(width: 640, height: 460),
35
+ maxSize: NSSize(width: 1100, height: 800)
36
+ ),
37
+ rootView: host
38
+ )
39
+ AppWindowShell.positionCentered(w)
40
+ AppWindowShell.present(w)
41
+ self.window = w
42
+ }
43
+
44
+ func close() {
45
+ window?.orderOut(nil)
46
+ window = nil
47
+ AppDelegate.updateActivationPolicy()
48
+ }
49
+
50
+ fileprivate func selectionBinding() -> Binding<Capability> {
51
+ Binding(
52
+ get: { self.focusedCapability },
53
+ set: { self.focusedCapability = $0 }
54
+ )
55
+ }
56
+ }
57
+
58
+ // SwiftUI host that observes the controller so the assistant updates when
59
+ // `focusedCapability` is reassigned by an external caller (e.g. clicking a
60
+ // different feature gate while the window is already open).
61
+ private struct HostView: View {
62
+ @ObservedObject var controller: PermissionsAssistantWindowController
63
+
64
+ var body: some View {
65
+ PermissionsAssistantView(
66
+ selected: controller.selectionBinding(),
67
+ onClose: { controller.close() }
68
+ )
69
+ }
70
+ }
@@ -15,6 +15,8 @@ class Preferences: ObservableObject {
15
15
  static let cockpitLayout = "companion.cockpit.layout"
16
16
  }
17
17
 
18
+ private static let dismissedCapabilitiesKey = "permissions.dismissed"
19
+
18
20
  @Published var terminal: Terminal {
19
21
  didSet { UserDefaults.standard.set(terminal.rawValue, forKey: "terminal") }
20
22
  }
@@ -140,6 +142,30 @@ class Preferences: ObservableObject {
140
142
  didSet { UserDefaults.standard.set(ocrAccuracy, forKey: "ocr.accuracy") }
141
143
  }
142
144
 
145
+ // MARK: - Permissions Assistant
146
+
147
+ /// Capabilities the user has explicitly snoozed. Cleared per-capability when
148
+ /// the user re-enters the relevant feature. Persisted as raw values.
149
+ @Published var dismissedCapabilities: Set<String> {
150
+ didSet {
151
+ UserDefaults.standard.set(Array(dismissedCapabilities), forKey: Self.dismissedCapabilitiesKey)
152
+ }
153
+ }
154
+
155
+ func dismissCapability(_ rawValue: String) {
156
+ dismissedCapabilities.insert(rawValue)
157
+ }
158
+
159
+ func clearDismissal(_ rawValue: String) {
160
+ if dismissedCapabilities.contains(rawValue) {
161
+ dismissedCapabilities.remove(rawValue)
162
+ }
163
+ }
164
+
165
+ func isCapabilityDismissed(_ rawValue: String) -> Bool {
166
+ dismissedCapabilities.contains(rawValue)
167
+ }
168
+
143
169
  init() {
144
170
  if let saved = UserDefaults.standard.string(forKey: "terminal"),
145
171
  let t = Terminal(rawValue: saved), t.isInstalled {
@@ -201,8 +227,14 @@ class Preferences: ObservableObject {
201
227
  let savedBudgetUSD = UserDefaults.standard.double(forKey: "claude.advisorBudget")
202
228
  self.advisorBudgetUSD = savedBudgetUSD > 0 ? savedBudgetUSD : 0.50
203
229
 
204
- // Search & OCR
205
- self.ocrEnabled = !UserDefaults.standard.bool(forKey: "ocr.disabled")
230
+ // Search & OCR. Default off until the user explicitly enables it from
231
+ // the Permissions Assistant or Search settings. Honors any explicit
232
+ // ocr.disabled value already saved (true=off, false=on).
233
+ if UserDefaults.standard.object(forKey: "ocr.disabled") != nil {
234
+ self.ocrEnabled = !UserDefaults.standard.bool(forKey: "ocr.disabled")
235
+ } else {
236
+ self.ocrEnabled = false
237
+ }
206
238
 
207
239
  let savedInterval = UserDefaults.standard.double(forKey: "ocr.interval")
208
240
  self.ocrQuickInterval = savedInterval > 0 ? savedInterval : 60
@@ -221,6 +253,9 @@ class Preferences: ObservableObject {
221
253
 
222
254
  let savedAcc = UserDefaults.standard.string(forKey: "ocr.accuracy") ?? "accurate"
223
255
  self.ocrAccuracy = savedAcc
256
+
257
+ let dismissed = UserDefaults.standard.stringArray(forKey: Self.dismissedCapabilitiesKey) ?? []
258
+ self.dismissedCapabilities = Set(dismissed)
224
259
  }
225
260
 
226
261
  func updateCompanionCockpitSlot(