@lattices/cli 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -9
- package/app/Info.plist +30 -0
- package/app/Lattices.app/Contents/Info.plist +8 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
- package/app/Lattices.app/Contents/Resources/tap.wav +0 -0
- package/app/Lattices.app/Contents/_CodeSignature/CodeResources +139 -0
- package/app/Lattices.entitlements +15 -0
- package/app/Package.swift +8 -1
- package/app/Resources/tap.wav +0 -0
- package/app/Sources/AdvisorLearningStore.swift +90 -0
- package/app/Sources/AgentSession.swift +377 -0
- package/app/Sources/AppDelegate.swift +45 -12
- package/app/Sources/AppShellView.swift +81 -8
- package/app/Sources/AudioProvider.swift +386 -0
- package/app/Sources/CheatSheetHUD.swift +261 -19
- package/app/Sources/DaemonProtocol.swift +13 -0
- package/app/Sources/DaemonServer.swift +8 -0
- package/app/Sources/DesktopModel.swift +189 -6
- package/app/Sources/DesktopModelTypes.swift +2 -0
- package/app/Sources/DiagnosticLog.swift +104 -2
- package/app/Sources/EventBus.swift +1 -0
- package/app/Sources/HUDBottomBar.swift +279 -0
- package/app/Sources/HUDController.swift +1158 -0
- package/app/Sources/HUDLeftBar.swift +849 -0
- package/app/Sources/HUDMinimap.swift +179 -0
- package/app/Sources/HUDRightBar.swift +774 -0
- package/app/Sources/HUDState.swift +367 -0
- package/app/Sources/HUDTopBar.swift +243 -0
- package/app/Sources/HandsOffSession.swift +802 -0
- package/app/Sources/HomeDashboardView.swift +125 -0
- package/app/Sources/HotkeyManager.swift +2 -0
- package/app/Sources/HotkeyStore.swift +49 -9
- package/app/Sources/IntentEngine.swift +962 -0
- package/app/Sources/Intents/CreateLayerIntent.swift +54 -0
- package/app/Sources/Intents/DistributeIntent.swift +56 -0
- package/app/Sources/Intents/FocusIntent.swift +69 -0
- package/app/Sources/Intents/HelpIntent.swift +41 -0
- package/app/Sources/Intents/KillIntent.swift +47 -0
- package/app/Sources/Intents/LatticeIntent.swift +78 -0
- package/app/Sources/Intents/LaunchIntent.swift +67 -0
- package/app/Sources/Intents/ListSessionsIntent.swift +32 -0
- package/app/Sources/Intents/ListWindowsIntent.swift +30 -0
- package/app/Sources/Intents/ScanIntent.swift +52 -0
- package/app/Sources/Intents/SearchIntent.swift +190 -0
- package/app/Sources/Intents/SwitchLayerIntent.swift +50 -0
- package/app/Sources/Intents/TileIntent.swift +61 -0
- package/app/Sources/LatticesApi.swift +1275 -30
- package/app/Sources/LauncherHUD.swift +348 -0
- package/app/Sources/MainView.swift +147 -44
- package/app/Sources/MouseFinder.swift +222 -0
- package/app/Sources/OcrModel.swift +34 -1
- package/app/Sources/OmniSearchState.swift +99 -102
- package/app/Sources/OnboardingView.swift +457 -0
- package/app/Sources/PermissionChecker.swift +2 -12
- package/app/Sources/PiChatDock.swift +454 -0
- package/app/Sources/PiChatSession.swift +815 -0
- package/app/Sources/PiWorkspaceView.swift +364 -0
- package/app/Sources/PlacementSpec.swift +195 -0
- package/app/Sources/Preferences.swift +59 -0
- package/app/Sources/ProjectScanner.swift +58 -45
- package/app/Sources/ScreenMapState.swift +701 -55
- package/app/Sources/ScreenMapView.swift +843 -103
- package/app/Sources/ScreenMapWindowController.swift +22 -0
- package/app/Sources/SessionLayerStore.swift +285 -0
- package/app/Sources/SessionManager.swift +4 -1
- package/app/Sources/SettingsView.swift +186 -3
- package/app/Sources/Theme.swift +9 -8
- package/app/Sources/TmuxModel.swift +7 -0
- package/app/Sources/TmuxQuery.swift +27 -3
- package/app/Sources/VoiceChatView.swift +192 -0
- package/app/Sources/VoiceCommandWindow.swift +1594 -0
- package/app/Sources/VoiceIntentResolver.swift +671 -0
- package/app/Sources/VoxClient.swift +454 -0
- package/app/Sources/WindowTiler.swift +348 -87
- package/app/Sources/WorkspaceManager.swift +127 -18
- package/app/Tests/StageDragTests.swift +333 -0
- package/app/Tests/StageJoinTests.swift +313 -0
- package/app/Tests/StageManagerTests.swift +280 -0
- package/app/Tests/StageTileTests.swift +353 -0
- package/assets/AppIcon.icns +0 -0
- package/bin/client.ts +16 -0
- package/bin/{daemon-client.js → daemon-client.ts} +49 -30
- package/bin/handsoff-infer.ts +280 -0
- package/bin/handsoff-worker.ts +740 -0
- package/bin/lattices-app.ts +338 -0
- package/bin/lattices-dev +208 -0
- package/bin/{lattices.js → lattices.ts} +777 -140
- package/bin/project-twin.ts +645 -0
- package/docs/agent-execution-plan.md +562 -0
- package/docs/agent-layer-guide.md +207 -0
- package/docs/agents.md +142 -0
- package/docs/api.md +153 -34
- package/docs/app.md +29 -1
- package/docs/config.md +5 -1
- package/docs/handsoff-test-scenarios.md +84 -0
- package/docs/layers.md +20 -20
- package/docs/ocr.md +14 -5
- package/docs/overview.md +5 -1
- package/docs/presentation-execution-review.md +491 -0
- package/docs/prompts/hands-off-system.md +374 -0
- package/docs/prompts/hands-off-turn.md +30 -0
- package/docs/prompts/voice-advisor.md +31 -0
- package/docs/prompts/voice-fallback.md +23 -0
- package/docs/tiling-reference.md +167 -0
- package/docs/twins.md +138 -0
- package/docs/voice-command-protocol.md +278 -0
- package/docs/voice.md +219 -0
- package/package.json +29 -11
- package/bin/client.js +0 -4
- package/bin/lattices-app.js +0 -221
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import AppKit
|
|
3
|
+
|
|
4
|
+
// MARK: - Onboarding Flow
|
|
5
|
+
|
|
6
|
+
/// A step-by-step welcome screen shown on first launch.
|
|
7
|
+
/// Walks the user through granting Accessibility, Screen Recording,
|
|
8
|
+
/// choosing a project root, and optionally installing tmux.
|
|
9
|
+
struct OnboardingView: View {
|
|
10
|
+
@ObservedObject private var permChecker = PermissionChecker.shared
|
|
11
|
+
@ObservedObject private var prefs = Preferences.shared
|
|
12
|
+
@ObservedObject private var tmux = TmuxModel.shared
|
|
13
|
+
@State private var step: Step = .welcome
|
|
14
|
+
var onComplete: () -> Void
|
|
15
|
+
|
|
16
|
+
enum Step: Int, CaseIterable {
|
|
17
|
+
case welcome
|
|
18
|
+
case accessibility
|
|
19
|
+
case screenRecording
|
|
20
|
+
case projectRoot
|
|
21
|
+
case tmux
|
|
22
|
+
case done
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var body: some View {
|
|
26
|
+
VStack(spacing: 0) {
|
|
27
|
+
// Progress dots
|
|
28
|
+
HStack(spacing: 6) {
|
|
29
|
+
ForEach(Step.allCases, id: \.rawValue) { s in
|
|
30
|
+
Circle()
|
|
31
|
+
.fill(s.rawValue <= step.rawValue ? Color.white : Color.white.opacity(0.20))
|
|
32
|
+
.frame(width: 6, height: 6)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
.padding(.top, 28)
|
|
36
|
+
.padding(.bottom, 24)
|
|
37
|
+
|
|
38
|
+
// Step content
|
|
39
|
+
Group {
|
|
40
|
+
switch step {
|
|
41
|
+
case .welcome: welcomeStep
|
|
42
|
+
case .accessibility: accessibilityStep
|
|
43
|
+
case .screenRecording: screenRecordingStep
|
|
44
|
+
case .projectRoot: projectRootStep
|
|
45
|
+
case .tmux: tmuxStep
|
|
46
|
+
case .done: doneStep
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
50
|
+
.padding(.horizontal, 40)
|
|
51
|
+
|
|
52
|
+
Spacer(minLength: 0)
|
|
53
|
+
|
|
54
|
+
// Navigation
|
|
55
|
+
HStack {
|
|
56
|
+
if step != .welcome {
|
|
57
|
+
Button("Back") { withAnimation(.easeInOut(duration: 0.2)) { goBack() } }
|
|
58
|
+
.buttonStyle(.plain)
|
|
59
|
+
.font(Typo.mono(11))
|
|
60
|
+
.foregroundColor(Palette.textMuted)
|
|
61
|
+
}
|
|
62
|
+
Spacer()
|
|
63
|
+
if step == .done {
|
|
64
|
+
Button(action: { onComplete() }) {
|
|
65
|
+
Text("Get started")
|
|
66
|
+
.angularButton(Palette.running)
|
|
67
|
+
}
|
|
68
|
+
.buttonStyle(.plain)
|
|
69
|
+
} else {
|
|
70
|
+
Button(action: { withAnimation(.easeInOut(duration: 0.2)) { advance() } }) {
|
|
71
|
+
Text(nextLabel)
|
|
72
|
+
.angularButton(.white)
|
|
73
|
+
}
|
|
74
|
+
.buttonStyle(.plain)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
.padding(.horizontal, 40)
|
|
78
|
+
.padding(.bottom, 28)
|
|
79
|
+
}
|
|
80
|
+
.frame(width: 480, height: 420)
|
|
81
|
+
.background(Palette.bg)
|
|
82
|
+
.preferredColorScheme(.dark)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// MARK: - Steps
|
|
86
|
+
|
|
87
|
+
private var welcomeStep: some View {
|
|
88
|
+
VStack(spacing: 16) {
|
|
89
|
+
latticesIcon
|
|
90
|
+
Text("Welcome to Lattices")
|
|
91
|
+
.font(Typo.title(18))
|
|
92
|
+
.foregroundColor(Palette.text)
|
|
93
|
+
Text("Workspace control plane for macOS.\nLet's get you set up in under a minute.")
|
|
94
|
+
.font(Typo.body(12))
|
|
95
|
+
.foregroundColor(Palette.textDim)
|
|
96
|
+
.multilineTextAlignment(.center)
|
|
97
|
+
.lineSpacing(3)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private var accessibilityStep: some View {
|
|
102
|
+
permissionStep(
|
|
103
|
+
icon: "hand.raised.fill",
|
|
104
|
+
title: "Accessibility",
|
|
105
|
+
description: "Lattices needs Accessibility access to read window titles, move and resize windows, and tile your workspace.",
|
|
106
|
+
granted: permChecker.accessibility,
|
|
107
|
+
action: { permChecker.requestAccessibility() }
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private var screenRecordingStep: some View {
|
|
112
|
+
permissionStep(
|
|
113
|
+
icon: "rectangle.dashed.badge.record",
|
|
114
|
+
title: "Screen Recording",
|
|
115
|
+
description: "Allows Lattices to index on-screen text with OCR so you can search across all your windows.",
|
|
116
|
+
granted: permChecker.screenRecording,
|
|
117
|
+
action: { permChecker.requestScreenRecording() }
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private var projectRootStep: some View {
|
|
122
|
+
VStack(spacing: 16) {
|
|
123
|
+
Image(systemName: "folder.fill")
|
|
124
|
+
.font(.system(size: 28))
|
|
125
|
+
.foregroundColor(.white.opacity(0.7))
|
|
126
|
+
|
|
127
|
+
Text("Project directory")
|
|
128
|
+
.font(Typo.title(16))
|
|
129
|
+
.foregroundColor(Palette.text)
|
|
130
|
+
|
|
131
|
+
Text("Where do your projects live? Lattices scans this folder to find workspaces.")
|
|
132
|
+
.font(Typo.body(12))
|
|
133
|
+
.foregroundColor(Palette.textDim)
|
|
134
|
+
.multilineTextAlignment(.center)
|
|
135
|
+
.lineSpacing(3)
|
|
136
|
+
|
|
137
|
+
HStack(spacing: 8) {
|
|
138
|
+
Text(prefs.scanRoot.isEmpty ? "Not set" : abbreviatePath(prefs.scanRoot))
|
|
139
|
+
.font(Typo.mono(11))
|
|
140
|
+
.foregroundColor(prefs.scanRoot.isEmpty ? Palette.textMuted : Palette.text)
|
|
141
|
+
.lineLimit(1)
|
|
142
|
+
.truncationMode(.middle)
|
|
143
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
144
|
+
.padding(.horizontal, 10)
|
|
145
|
+
.padding(.vertical, 8)
|
|
146
|
+
.background(
|
|
147
|
+
RoundedRectangle(cornerRadius: 5)
|
|
148
|
+
.fill(Palette.surface)
|
|
149
|
+
.overlay(
|
|
150
|
+
RoundedRectangle(cornerRadius: 5)
|
|
151
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
Button("Browse") {
|
|
156
|
+
let panel = NSOpenPanel()
|
|
157
|
+
panel.canChooseFiles = false
|
|
158
|
+
panel.canChooseDirectories = true
|
|
159
|
+
panel.allowsMultipleSelection = false
|
|
160
|
+
panel.directoryURL = URL(fileURLWithPath: prefs.scanRoot.isEmpty ? NSHomeDirectory() : prefs.scanRoot)
|
|
161
|
+
if panel.runModal() == .OK, let url = panel.url {
|
|
162
|
+
prefs.scanRoot = url.path
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
.buttonStyle(.plain)
|
|
166
|
+
.font(Typo.monoBold(10))
|
|
167
|
+
.foregroundColor(.white)
|
|
168
|
+
.padding(.horizontal, 10)
|
|
169
|
+
.padding(.vertical, 6)
|
|
170
|
+
.background(
|
|
171
|
+
RoundedRectangle(cornerRadius: 5)
|
|
172
|
+
.fill(Palette.surface)
|
|
173
|
+
.overlay(
|
|
174
|
+
RoundedRectangle(cornerRadius: 5)
|
|
175
|
+
.strokeBorder(Palette.borderLit, lineWidth: 0.5)
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if !prefs.scanRoot.isEmpty {
|
|
181
|
+
HStack(spacing: 4) {
|
|
182
|
+
Image(systemName: "checkmark.circle.fill")
|
|
183
|
+
.font(.system(size: 10))
|
|
184
|
+
.foregroundColor(Palette.running)
|
|
185
|
+
Text(abbreviatePath(prefs.scanRoot))
|
|
186
|
+
.font(Typo.mono(10))
|
|
187
|
+
.foregroundColor(Palette.running)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private var tmuxStep: some View {
|
|
194
|
+
VStack(spacing: 16) {
|
|
195
|
+
Image(systemName: "terminal.fill")
|
|
196
|
+
.font(.system(size: 28))
|
|
197
|
+
.foregroundColor(.white.opacity(0.7))
|
|
198
|
+
|
|
199
|
+
Text("Terminal sessions")
|
|
200
|
+
.font(Typo.title(16))
|
|
201
|
+
.foregroundColor(Palette.text)
|
|
202
|
+
|
|
203
|
+
if tmux.isAvailable {
|
|
204
|
+
HStack(spacing: 6) {
|
|
205
|
+
Image(systemName: "checkmark.circle.fill")
|
|
206
|
+
.foregroundColor(Palette.running)
|
|
207
|
+
Text("tmux is installed")
|
|
208
|
+
.font(Typo.mono(12))
|
|
209
|
+
.foregroundColor(Palette.running)
|
|
210
|
+
}
|
|
211
|
+
Text("Lattices can manage tmux sessions, pane layouts, and terminal workspaces for you.")
|
|
212
|
+
.font(Typo.body(12))
|
|
213
|
+
.foregroundColor(Palette.textDim)
|
|
214
|
+
.multilineTextAlignment(.center)
|
|
215
|
+
.lineSpacing(3)
|
|
216
|
+
} else {
|
|
217
|
+
Text("tmux is optional but recommended. It enables managed terminal sessions with persistent pane layouts.")
|
|
218
|
+
.font(Typo.body(12))
|
|
219
|
+
.foregroundColor(Palette.textDim)
|
|
220
|
+
.multilineTextAlignment(.center)
|
|
221
|
+
.lineSpacing(3)
|
|
222
|
+
|
|
223
|
+
Button(action: {
|
|
224
|
+
let task = Process()
|
|
225
|
+
task.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
|
226
|
+
task.arguments = ["-lc", "brew install tmux"]
|
|
227
|
+
task.standardOutput = FileHandle.nullDevice
|
|
228
|
+
task.standardError = FileHandle.nullDevice
|
|
229
|
+
try? task.run()
|
|
230
|
+
}) {
|
|
231
|
+
HStack(spacing: 6) {
|
|
232
|
+
Image(systemName: "arrow.down.circle")
|
|
233
|
+
.font(.system(size: 11))
|
|
234
|
+
Text("brew install tmux")
|
|
235
|
+
.font(Typo.monoBold(11))
|
|
236
|
+
}
|
|
237
|
+
.angularButton(.white, filled: false)
|
|
238
|
+
}
|
|
239
|
+
.buttonStyle(.plain)
|
|
240
|
+
|
|
241
|
+
Text("You can always install it later. Window tiling, search, and OCR work without tmux.")
|
|
242
|
+
.font(Typo.mono(10))
|
|
243
|
+
.foregroundColor(Palette.textMuted)
|
|
244
|
+
.multilineTextAlignment(.center)
|
|
245
|
+
.lineSpacing(2)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private var doneStep: some View {
|
|
251
|
+
VStack(spacing: 16) {
|
|
252
|
+
Image(systemName: "checkmark.circle.fill")
|
|
253
|
+
.font(.system(size: 36))
|
|
254
|
+
.foregroundColor(Palette.running)
|
|
255
|
+
|
|
256
|
+
Text("You're all set")
|
|
257
|
+
.font(Typo.title(18))
|
|
258
|
+
.foregroundColor(Palette.text)
|
|
259
|
+
|
|
260
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
261
|
+
statusRow("Accessibility", granted: permChecker.accessibility)
|
|
262
|
+
statusRow("Screen Recording", granted: permChecker.screenRecording)
|
|
263
|
+
statusRow("Project root", granted: !prefs.scanRoot.isEmpty,
|
|
264
|
+
detail: prefs.scanRoot.isEmpty ? "not set" : abbreviatePath(prefs.scanRoot))
|
|
265
|
+
statusRow("tmux", granted: tmux.isAvailable,
|
|
266
|
+
detail: tmux.isAvailable ? "installed" : "skipped")
|
|
267
|
+
}
|
|
268
|
+
.padding(16)
|
|
269
|
+
.background(
|
|
270
|
+
RoundedRectangle(cornerRadius: 6)
|
|
271
|
+
.fill(Palette.surface)
|
|
272
|
+
.overlay(
|
|
273
|
+
RoundedRectangle(cornerRadius: 6)
|
|
274
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// MARK: - Shared helpers
|
|
281
|
+
|
|
282
|
+
private func permissionStep(icon: String, title: String, description: String, granted: Bool, action: @escaping () -> Void) -> some View {
|
|
283
|
+
VStack(spacing: 16) {
|
|
284
|
+
Image(systemName: icon)
|
|
285
|
+
.font(.system(size: 28))
|
|
286
|
+
.foregroundColor(.white.opacity(0.7))
|
|
287
|
+
|
|
288
|
+
Text(title)
|
|
289
|
+
.font(Typo.title(16))
|
|
290
|
+
.foregroundColor(Palette.text)
|
|
291
|
+
|
|
292
|
+
Text(description)
|
|
293
|
+
.font(Typo.body(12))
|
|
294
|
+
.foregroundColor(Palette.textDim)
|
|
295
|
+
.multilineTextAlignment(.center)
|
|
296
|
+
.lineSpacing(3)
|
|
297
|
+
|
|
298
|
+
if granted {
|
|
299
|
+
HStack(spacing: 6) {
|
|
300
|
+
Image(systemName: "checkmark.circle.fill")
|
|
301
|
+
.foregroundColor(Palette.running)
|
|
302
|
+
Text("Granted")
|
|
303
|
+
.font(Typo.monoBold(11))
|
|
304
|
+
.foregroundColor(Palette.running)
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
Button(action: action) {
|
|
308
|
+
Text("Grant \(title)")
|
|
309
|
+
.angularButton(.white, filled: false)
|
|
310
|
+
}
|
|
311
|
+
.buttonStyle(.plain)
|
|
312
|
+
|
|
313
|
+
Text("macOS will ask you to toggle this on in System Settings.")
|
|
314
|
+
.font(Typo.mono(10))
|
|
315
|
+
.foregroundColor(Palette.textMuted)
|
|
316
|
+
.multilineTextAlignment(.center)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private func statusRow(_ label: String, granted: Bool, detail: String? = nil) -> some View {
|
|
322
|
+
HStack(spacing: 8) {
|
|
323
|
+
Image(systemName: granted ? "checkmark.circle.fill" : "circle")
|
|
324
|
+
.font(.system(size: 11))
|
|
325
|
+
.foregroundColor(granted ? Palette.running : Palette.detach)
|
|
326
|
+
Text(label)
|
|
327
|
+
.font(Typo.mono(11))
|
|
328
|
+
.foregroundColor(Palette.text)
|
|
329
|
+
Spacer()
|
|
330
|
+
if let detail {
|
|
331
|
+
Text(detail)
|
|
332
|
+
.font(Typo.mono(10))
|
|
333
|
+
.foregroundColor(granted ? Palette.textDim : Palette.detach)
|
|
334
|
+
} else {
|
|
335
|
+
Text(granted ? "granted" : "not set")
|
|
336
|
+
.font(Typo.mono(10))
|
|
337
|
+
.foregroundColor(granted ? Palette.running : Palette.detach)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private var latticesIcon: some View {
|
|
343
|
+
// 3x3 grid — L-shape pattern
|
|
344
|
+
let cells = [true, false, false, true, false, false, true, true, true]
|
|
345
|
+
let size: CGFloat = 40
|
|
346
|
+
let pad: CGFloat = 4
|
|
347
|
+
let gap: CGFloat = 2.5
|
|
348
|
+
let cell = (size - 2 * pad - 2 * gap) / 3
|
|
349
|
+
return Canvas { context, _ in
|
|
350
|
+
for (i, bright) in cells.enumerated() {
|
|
351
|
+
let row = i / 3
|
|
352
|
+
let col = i % 3
|
|
353
|
+
let rect = CGRect(
|
|
354
|
+
x: pad + CGFloat(col) * (cell + gap),
|
|
355
|
+
y: pad + CGFloat(row) * (cell + gap),
|
|
356
|
+
width: cell, height: cell
|
|
357
|
+
)
|
|
358
|
+
context.fill(
|
|
359
|
+
RoundedRectangle(cornerRadius: 2).path(in: rect),
|
|
360
|
+
with: .color(bright ? .white : .white.opacity(0.18))
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
.frame(width: size, height: size)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// MARK: - Navigation
|
|
368
|
+
|
|
369
|
+
private var nextLabel: String {
|
|
370
|
+
switch step {
|
|
371
|
+
case .accessibility where !permChecker.accessibility: return "Continue anyway"
|
|
372
|
+
case .screenRecording where !permChecker.screenRecording: return "Continue anyway"
|
|
373
|
+
case .projectRoot where prefs.scanRoot.isEmpty: return "Skip for now"
|
|
374
|
+
case .tmux where !tmux.isAvailable: return "Skip"
|
|
375
|
+
default: return "Continue"
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private func advance() {
|
|
380
|
+
guard let next = Step(rawValue: step.rawValue + 1) else { return }
|
|
381
|
+
step = next
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private func goBack() {
|
|
385
|
+
guard let prev = Step(rawValue: step.rawValue - 1) else { return }
|
|
386
|
+
step = prev
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private func abbreviatePath(_ path: String) -> String {
|
|
390
|
+
let home = NSHomeDirectory()
|
|
391
|
+
if path.hasPrefix(home) {
|
|
392
|
+
return "~" + path.dropFirst(home.count)
|
|
393
|
+
}
|
|
394
|
+
return path
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// MARK: - Window Controller
|
|
399
|
+
|
|
400
|
+
final class OnboardingWindowController {
|
|
401
|
+
static let shared = OnboardingWindowController()
|
|
402
|
+
|
|
403
|
+
private var window: NSWindow?
|
|
404
|
+
private static let completedKey = "onboarding.completed"
|
|
405
|
+
|
|
406
|
+
var hasCompleted: Bool {
|
|
407
|
+
UserDefaults.standard.bool(forKey: Self.completedKey)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/// Show the onboarding window if not yet completed.
|
|
411
|
+
/// Returns true if onboarding was shown.
|
|
412
|
+
@discardableResult
|
|
413
|
+
func showIfNeeded() -> Bool {
|
|
414
|
+
guard !hasCompleted else { return false }
|
|
415
|
+
show()
|
|
416
|
+
return true
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
func show() {
|
|
420
|
+
if let w = window {
|
|
421
|
+
w.makeKeyAndOrderFront(nil)
|
|
422
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
let view = OnboardingView {
|
|
427
|
+
self.complete()
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let w = AppWindowShell.makeWindow(
|
|
431
|
+
config: .init(
|
|
432
|
+
title: "Welcome to Lattices",
|
|
433
|
+
titleVisible: false,
|
|
434
|
+
initialSize: NSSize(width: 480, height: 420),
|
|
435
|
+
minSize: NSSize(width: 480, height: 420),
|
|
436
|
+
maxSize: NSSize(width: 480, height: 420),
|
|
437
|
+
miniaturizable: false
|
|
438
|
+
),
|
|
439
|
+
rootView: view
|
|
440
|
+
)
|
|
441
|
+
w.styleMask.remove(.resizable)
|
|
442
|
+
AppWindowShell.positionCentered(w)
|
|
443
|
+
AppWindowShell.present(w)
|
|
444
|
+
self.window = w
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private func complete() {
|
|
448
|
+
UserDefaults.standard.set(true, forKey: Self.completedKey)
|
|
449
|
+
window?.orderOut(nil)
|
|
450
|
+
window = nil
|
|
451
|
+
AppDelegate.updateActivationPolicy()
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
func reset() {
|
|
455
|
+
UserDefaults.standard.removeObject(forKey: Self.completedKey)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
@@ -13,14 +13,14 @@ final class PermissionChecker: ObservableObject {
|
|
|
13
13
|
|
|
14
14
|
var allGranted: Bool { accessibility && screenRecording }
|
|
15
15
|
|
|
16
|
-
/// Check current permission state
|
|
16
|
+
/// Check current permission state without prompting.
|
|
17
17
|
func check() {
|
|
18
18
|
let diag = DiagnosticLog.shared
|
|
19
19
|
|
|
20
20
|
let ax = AXIsProcessTrusted()
|
|
21
21
|
let sr = CGPreflightScreenCaptureAccess()
|
|
22
22
|
|
|
23
|
-
// First check: log identity info
|
|
23
|
+
// First check: log identity info only
|
|
24
24
|
if !hasLoggedInitial {
|
|
25
25
|
hasLoggedInitial = true
|
|
26
26
|
let bundleId = Bundle.main.bundleIdentifier ?? "<no bundle id>"
|
|
@@ -30,16 +30,6 @@ final class PermissionChecker: ObservableObject {
|
|
|
30
30
|
diag.info("PermissionChecker: exec=\(execPath)")
|
|
31
31
|
diag.info("AXIsProcessTrusted() → \(ax)")
|
|
32
32
|
diag.info("CGPreflightScreenCaptureAccess() → \(sr)")
|
|
33
|
-
|
|
34
|
-
// Prompt for missing permissions on first check
|
|
35
|
-
if !ax {
|
|
36
|
-
requestAccessibility()
|
|
37
|
-
return
|
|
38
|
-
}
|
|
39
|
-
if !sr {
|
|
40
|
-
requestScreenRecording()
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
33
|
}
|
|
44
34
|
|
|
45
35
|
// Log on state changes
|