@lattices/cli 0.4.2 → 0.4.5
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 +3 -0
- package/app/Info.plist +2 -2
- package/app/Lattices.app/Contents/Info.plist +2 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Package.swift +6 -0
- package/app/Sources/App.swift +10 -0
- package/app/Sources/AppDelegate.swift +90 -34
- package/app/Sources/AppShellView.swift +2 -0
- package/app/Sources/AppTypeClassifier.swift +36 -0
- package/app/Sources/AppUpdater.swift +92 -0
- package/app/Sources/CheatSheetHUD.swift +1 -0
- package/app/Sources/CliActionLauncher.swift +50 -0
- package/app/Sources/CommandModeView.swift +4 -24
- package/app/Sources/CompanionActivityLog.swift +70 -0
- package/app/Sources/CompanionKeyboardController.swift +141 -0
- package/app/Sources/DesktopModel.swift +4 -0
- package/app/Sources/HandsOffSession.swift +15 -4
- package/app/Sources/HomeDashboardView.swift +18 -10
- package/app/Sources/HotkeyStore.swift +8 -5
- package/app/Sources/IntentEngine.swift +7 -1
- package/app/Sources/LatticesApi.swift +125 -4
- package/app/Sources/LatticesCompanionBridgeServer.swift +438 -0
- package/app/Sources/LatticesCompanionCockpit.swift +555 -0
- package/app/Sources/LatticesCompanionSecurityCoordinator.swift +594 -0
- package/app/Sources/LatticesCompanionTrackpadController.swift +204 -0
- package/app/Sources/LatticesDeckHost.swift +1463 -0
- package/app/Sources/LatticesRuntime.swift +61 -0
- package/app/Sources/MainView.swift +351 -191
- package/app/Sources/MouseFinder.swift +335 -30
- package/app/Sources/MouseGestureConfig.swift +364 -0
- package/app/Sources/MouseGestureController.swift +1203 -0
- package/app/Sources/MouseInputDeviceStore.swift +98 -0
- package/app/Sources/MouseInputEventViewer.swift +272 -0
- package/app/Sources/MouseShortcutStore.swift +107 -0
- package/app/Sources/OmniSearchView.swift +136 -2
- package/app/Sources/OmniSearchWindow.swift +65 -5
- package/app/Sources/OnboardingView.swift +30 -16
- package/app/Sources/PaletteCommand.swift +26 -6
- package/app/Sources/PermissionChecker.swift +76 -2
- package/app/Sources/PiAuthNextStepCard.swift +148 -0
- package/app/Sources/PiAuthPromptCard.swift +90 -0
- package/app/Sources/PiChatDock.swift +137 -74
- package/app/Sources/PiChatSession.swift +608 -108
- package/app/Sources/PiInstallCallout.swift +86 -0
- package/app/Sources/PiProviderSetupCallout.swift +99 -0
- package/app/Sources/PiWorkspaceView.swift +174 -77
- package/app/Sources/Preferences.swift +78 -0
- package/app/Sources/ScreenMapState.swift +91 -31
- package/app/Sources/ScreenMapView.swift +510 -524
- package/app/Sources/ScreenMapWindowController.swift +12 -4
- package/app/Sources/SettingsView.swift +869 -152
- package/app/Sources/SystemTelemetryMonitor.swift +273 -0
- package/app/Sources/VoiceCommandWindow.swift +23 -2
- package/app/Sources/WindowDragSnapController.swift +628 -0
- package/app/Sources/WindowTiler.swift +328 -65
- package/app/Sources/WorkspaceManager.swift +288 -0
- package/bin/assistant-intelligence.ts +874 -0
- package/bin/handsoff-infer.ts +16 -209
- package/bin/handsoff-worker.ts +45 -258
- package/bin/lattices-app.ts +62 -0
- package/bin/lattices-dev +4 -0
- package/bin/lattices.ts +125 -14
- package/docs/agents.md +14 -0
- package/docs/api.md +55 -0
- package/docs/app.md +3 -0
- package/docs/companion-deck.md +180 -0
- package/docs/config.md +25 -0
- package/docs/tiling-reference.md +55 -0
- package/docs/voice-error-model.md +73 -0
- package/package.json +2 -1
|
@@ -18,6 +18,12 @@ struct MainView: View {
|
|
|
18
18
|
@State private var tmuxBannerDismissed = false
|
|
19
19
|
@ObservedObject private var tmuxModel = TmuxModel.shared
|
|
20
20
|
@State private var orphanSectionCollapsed = true
|
|
21
|
+
private let embeddedProjectColumns = Array(
|
|
22
|
+
repeating: GridItem(.flexible(minimum: 0, maximum: .infinity), spacing: 10, alignment: .top),
|
|
23
|
+
count: 3
|
|
24
|
+
)
|
|
25
|
+
private let embeddedProjectCardHeight: CGFloat = 94
|
|
26
|
+
private let embeddedProjectGridSpacing: CGFloat = 10
|
|
21
27
|
private var filtered: [Project] {
|
|
22
28
|
if searchText.isEmpty { return scanner.projects }
|
|
23
29
|
return scanner.projects.filter {
|
|
@@ -34,6 +40,16 @@ struct MainView: View {
|
|
|
34
40
|
|
|
35
41
|
private var needsSetup: Bool { prefs.scanRoot.isEmpty }
|
|
36
42
|
private var runningCount: Int { scanner.projects.filter(\.isRunning).count }
|
|
43
|
+
private var hasVisibleGroups: Bool {
|
|
44
|
+
guard let groups = workspace.config?.groups else { return false }
|
|
45
|
+
return !groups.isEmpty && searchText.isEmpty
|
|
46
|
+
}
|
|
47
|
+
private var embeddedProjectGridHeight: CGFloat {
|
|
48
|
+
guard !filtered.isEmpty else { return 0 }
|
|
49
|
+
let rowCount = min(Int(ceil(Double(filtered.count) / 3.0)), 3)
|
|
50
|
+
return CGFloat(rowCount) * embeddedProjectCardHeight
|
|
51
|
+
+ CGFloat(max(0, rowCount - 1)) * embeddedProjectGridSpacing
|
|
52
|
+
}
|
|
37
53
|
|
|
38
54
|
var body: some View {
|
|
39
55
|
VStack(spacing: 0) {
|
|
@@ -43,35 +59,42 @@ struct MainView: View {
|
|
|
43
59
|
minWidth: layout == .popover ? 380 : 0,
|
|
44
60
|
idealWidth: layout == .popover ? 380 : nil,
|
|
45
61
|
maxWidth: .infinity,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
maxHeight: .infinity
|
|
62
|
+
maxHeight: .infinity,
|
|
63
|
+
alignment: .top
|
|
49
64
|
)
|
|
50
65
|
.background(PanelBackground())
|
|
51
66
|
.preferredColorScheme(.dark)
|
|
52
67
|
.onAppear {
|
|
53
|
-
let tTotal = DiagnosticLog.shared.startTimed("MainView.onAppear (total)")
|
|
54
68
|
if needsSetup && !hasCheckedSetup {
|
|
55
69
|
hasCheckedSetup = true
|
|
56
70
|
SettingsWindowController.shared.show()
|
|
57
71
|
}
|
|
58
|
-
|
|
72
|
+
runRefresh()
|
|
73
|
+
}
|
|
74
|
+
.onReceive(NotificationCenter.default.publisher(for: .latticesPopoverWillShow)) { _ in
|
|
75
|
+
guard layout == .popover else { return }
|
|
76
|
+
runRefresh()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
59
79
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
80
|
+
private func runRefresh() {
|
|
81
|
+
let tTotal = DiagnosticLog.shared.startTimed("MainView.refresh (total)")
|
|
82
|
+
scanner.updateRoot(prefs.scanRoot)
|
|
63
83
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
84
|
+
let tScan = DiagnosticLog.shared.startTimed("ProjectScanner.scan")
|
|
85
|
+
scanner.scan()
|
|
86
|
+
DiagnosticLog.shared.finish(tScan)
|
|
67
87
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
88
|
+
let tInv = DiagnosticLog.shared.startTimed("InventoryManager.refresh")
|
|
89
|
+
inventory.refresh()
|
|
90
|
+
DiagnosticLog.shared.finish(tInv)
|
|
71
91
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
let tPerm = DiagnosticLog.shared.startTimed("PermissionChecker.check")
|
|
93
|
+
permChecker.check()
|
|
94
|
+
DiagnosticLog.shared.finish(tPerm)
|
|
95
|
+
|
|
96
|
+
bannerDismissed = false
|
|
97
|
+
DiagnosticLog.shared.finish(tTotal)
|
|
75
98
|
}
|
|
76
99
|
|
|
77
100
|
private var mainContent: some View {
|
|
@@ -88,52 +111,51 @@ struct MainView: View {
|
|
|
88
111
|
(NSApp.delegate as? AppDelegate)?.dismissPopover()
|
|
89
112
|
ScreenMapWindowController.shared.showPage(.home)
|
|
90
113
|
}
|
|
91
|
-
headerButton(icon: "
|
|
114
|
+
headerButton(icon: "rectangle.3.group") {
|
|
115
|
+
(NSApp.delegate as? AppDelegate)?.dismissPopover()
|
|
116
|
+
ScreenMapWindowController.shared.showPage(.screenMap)
|
|
117
|
+
}
|
|
118
|
+
headerButton(icon: "magnifyingglass") {
|
|
92
119
|
(NSApp.delegate as? AppDelegate)?.dismissPopover()
|
|
93
|
-
ScreenMapWindowController.shared.showPage(.
|
|
120
|
+
ScreenMapWindowController.shared.showPage(.desktopInventory)
|
|
94
121
|
}
|
|
95
|
-
headerButton(icon: "
|
|
122
|
+
headerButton(icon: "command") {
|
|
96
123
|
(NSApp.delegate as? AppDelegate)?.dismissPopover()
|
|
97
|
-
|
|
124
|
+
CommandPaletteWindow.shared.toggle()
|
|
98
125
|
}
|
|
99
126
|
headerButton(icon: "arrow.clockwise") { scanner.scan(); inventory.refresh() }
|
|
100
127
|
}
|
|
101
128
|
.padding(.horizontal, 18)
|
|
102
129
|
.padding(.top, 18)
|
|
103
130
|
.padding(.bottom, 12)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
Button { searchText = "" } label: {
|
|
122
|
-
Image(systemName: "xmark.circle.fill")
|
|
123
|
-
.foregroundColor(Palette.textMuted)
|
|
124
|
-
.font(.system(size: 11))
|
|
131
|
+
} else {
|
|
132
|
+
// Search
|
|
133
|
+
HStack(spacing: 8) {
|
|
134
|
+
Image(systemName: "magnifyingglass")
|
|
135
|
+
.foregroundColor(Palette.textMuted)
|
|
136
|
+
.font(.system(size: 11))
|
|
137
|
+
TextField("Search projects...", text: $searchText)
|
|
138
|
+
.textFieldStyle(.plain)
|
|
139
|
+
.font(Typo.body(13))
|
|
140
|
+
.foregroundColor(Palette.text)
|
|
141
|
+
if !searchText.isEmpty {
|
|
142
|
+
Button { searchText = "" } label: {
|
|
143
|
+
Image(systemName: "xmark.circle.fill")
|
|
144
|
+
.foregroundColor(Palette.textMuted)
|
|
145
|
+
.font(.system(size: 11))
|
|
146
|
+
}
|
|
147
|
+
.buttonStyle(.plain)
|
|
125
148
|
}
|
|
126
|
-
.buttonStyle(.plain)
|
|
127
149
|
}
|
|
150
|
+
.padding(.horizontal, 12)
|
|
151
|
+
.padding(.vertical, 8)
|
|
152
|
+
.background(
|
|
153
|
+
RoundedRectangle(cornerRadius: 4)
|
|
154
|
+
.fill(Palette.surface)
|
|
155
|
+
)
|
|
156
|
+
.padding(.horizontal, 14)
|
|
157
|
+
.padding(.bottom, 10)
|
|
128
158
|
}
|
|
129
|
-
.padding(.horizontal, 12)
|
|
130
|
-
.padding(.vertical, 8)
|
|
131
|
-
.background(
|
|
132
|
-
RoundedRectangle(cornerRadius: 4)
|
|
133
|
-
.fill(Palette.surface)
|
|
134
|
-
)
|
|
135
|
-
.padding(.horizontal, 14)
|
|
136
|
-
.padding(.bottom, 10)
|
|
137
159
|
|
|
138
160
|
// Permission banner
|
|
139
161
|
if !permChecker.allGranted && !bannerDismissed {
|
|
@@ -145,79 +167,110 @@ struct MainView: View {
|
|
|
145
167
|
tmuxBanner
|
|
146
168
|
}
|
|
147
169
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
170
|
+
if layout == .popover {
|
|
171
|
+
Rectangle()
|
|
172
|
+
.fill(Palette.border)
|
|
173
|
+
.frame(height: 0.5)
|
|
151
174
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
175
|
+
actionsSection
|
|
176
|
+
|
|
177
|
+
Rectangle()
|
|
178
|
+
.fill(Palette.border)
|
|
179
|
+
.frame(height: 0.5)
|
|
180
|
+
|
|
181
|
+
bottomBar
|
|
157
182
|
} else {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if let groups = workspace.config?.groups, !groups.isEmpty, searchText.isEmpty {
|
|
162
|
-
ForEach(groups) { group in
|
|
163
|
-
TabGroupRow(group: group, workspace: workspace)
|
|
164
|
-
}
|
|
183
|
+
Rectangle()
|
|
184
|
+
.fill(Palette.border)
|
|
185
|
+
.frame(height: 0.5)
|
|
165
186
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
187
|
+
if filtered.isEmpty && !hasVisibleGroups {
|
|
188
|
+
Spacer()
|
|
189
|
+
emptyState
|
|
190
|
+
Spacer()
|
|
191
|
+
} else {
|
|
192
|
+
embeddedProjectsSection
|
|
193
|
+
}
|
|
173
194
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
SessionManager.launch(project: project)
|
|
178
|
-
} onDetach: {
|
|
179
|
-
SessionManager.detach(project: project)
|
|
180
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
181
|
-
scanner.refreshStatus()
|
|
182
|
-
}
|
|
183
|
-
} onKill: {
|
|
184
|
-
SessionManager.kill(project: project)
|
|
185
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
186
|
-
scanner.refreshStatus()
|
|
187
|
-
}
|
|
188
|
-
} onSync: {
|
|
189
|
-
SessionManager.sync(project: project)
|
|
190
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
|
191
|
-
scanner.refreshStatus()
|
|
192
|
-
}
|
|
193
|
-
} onRestart: { paneName in
|
|
194
|
-
SessionManager.restart(project: project, paneName: paneName)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
195
|
+
Rectangle()
|
|
196
|
+
.fill(Palette.border)
|
|
197
|
+
.frame(height: 0.5)
|
|
197
198
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
199
|
+
// Actions footer
|
|
200
|
+
actionsSection
|
|
201
|
+
|
|
202
|
+
Rectangle()
|
|
203
|
+
.fill(Palette.border)
|
|
204
|
+
.frame(height: 0.5)
|
|
205
|
+
|
|
206
|
+
// Bottom bar
|
|
207
|
+
bottomBar
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private var embeddedProjectsSection: some View {
|
|
213
|
+
VStack(spacing: 0) {
|
|
214
|
+
if hasVisibleGroups, let groups = workspace.config?.groups {
|
|
215
|
+
LazyVStack(spacing: 4) {
|
|
216
|
+
ForEach(groups) { group in
|
|
217
|
+
TabGroupRow(group: group, workspace: workspace)
|
|
202
218
|
}
|
|
203
|
-
.padding(.horizontal, 10)
|
|
204
|
-
.padding(.vertical, 8)
|
|
205
219
|
}
|
|
220
|
+
.padding(.horizontal, 10)
|
|
221
|
+
.padding(.top, 8)
|
|
222
|
+
.padding(.bottom, filtered.isEmpty ? 8 : 6)
|
|
206
223
|
}
|
|
207
224
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
225
|
+
if !filtered.isEmpty {
|
|
226
|
+
VStack(spacing: 0) {
|
|
227
|
+
HStack(spacing: 8) {
|
|
228
|
+
Text(searchText.isEmpty ? "Projects" : "Matches")
|
|
229
|
+
.font(Typo.monoBold(10))
|
|
230
|
+
.foregroundColor(Palette.textMuted)
|
|
211
231
|
|
|
212
|
-
|
|
213
|
-
|
|
232
|
+
if searchText.isEmpty {
|
|
233
|
+
Text("\(runningCount) live")
|
|
234
|
+
.font(Typo.mono(9))
|
|
235
|
+
.foregroundColor(Palette.running.opacity(0.8))
|
|
236
|
+
}
|
|
214
237
|
|
|
215
|
-
|
|
216
|
-
.fill(Palette.border)
|
|
217
|
-
.frame(height: 0.5)
|
|
238
|
+
Spacer()
|
|
218
239
|
|
|
219
|
-
|
|
220
|
-
|
|
240
|
+
Text("\(filtered.count)")
|
|
241
|
+
.font(Typo.mono(9))
|
|
242
|
+
.foregroundColor(Palette.textMuted)
|
|
243
|
+
}
|
|
244
|
+
.padding(.horizontal, 14)
|
|
245
|
+
.padding(.top, 10)
|
|
246
|
+
.padding(.bottom, 8)
|
|
247
|
+
|
|
248
|
+
ScrollView(.vertical, showsIndicators: filtered.count > 9) {
|
|
249
|
+
LazyVGrid(columns: embeddedProjectColumns, spacing: embeddedProjectGridSpacing) {
|
|
250
|
+
ForEach(filtered) { project in
|
|
251
|
+
HomeProjectCard(
|
|
252
|
+
project: project,
|
|
253
|
+
onLaunch: { launchProject(project) },
|
|
254
|
+
onDetach: { detachProject(project) },
|
|
255
|
+
onKill: { killProject(project) },
|
|
256
|
+
onSync: { syncProject(project) },
|
|
257
|
+
onRestart: { paneName in restartProject(project, paneName: paneName) }
|
|
258
|
+
)
|
|
259
|
+
.frame(height: embeddedProjectCardHeight)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
.padding(.horizontal, 14)
|
|
263
|
+
.padding(.bottom, 10)
|
|
264
|
+
}
|
|
265
|
+
.frame(maxHeight: embeddedProjectGridHeight)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if !filteredOrphans.isEmpty {
|
|
270
|
+
orphanSection
|
|
271
|
+
.padding(.horizontal, 10)
|
|
272
|
+
.padding(.bottom, 8)
|
|
273
|
+
}
|
|
221
274
|
}
|
|
222
275
|
}
|
|
223
276
|
|
|
@@ -291,7 +344,7 @@ struct MainView: View {
|
|
|
291
344
|
Spacer()
|
|
292
345
|
|
|
293
346
|
Button("Help & shortcuts") {
|
|
294
|
-
|
|
347
|
+
SettingsWindowController.shared.show()
|
|
295
348
|
}
|
|
296
349
|
.buttonStyle(.plain)
|
|
297
350
|
.font(Typo.mono(9))
|
|
@@ -302,31 +355,40 @@ struct MainView: View {
|
|
|
302
355
|
.padding(.bottom, 6)
|
|
303
356
|
|
|
304
357
|
ActionRow(
|
|
305
|
-
label: "
|
|
306
|
-
detail: "
|
|
307
|
-
hotkeyTokens: hotkeyTokens(.palette),
|
|
308
|
-
icon: "command",
|
|
309
|
-
accentColor: Palette.running
|
|
310
|
-
) {
|
|
311
|
-
CommandPaletteWindow.shared.toggle()
|
|
312
|
-
}
|
|
313
|
-
ActionRow(
|
|
314
|
-
label: "Workspace",
|
|
315
|
-
detail: "Screen map, inventory, and window context",
|
|
358
|
+
label: "Home",
|
|
359
|
+
detail: "Workspace overview and project launcher",
|
|
316
360
|
hotkeyTokens: hotkeyTokens(.unifiedWindow),
|
|
317
|
-
icon: "
|
|
361
|
+
icon: "house",
|
|
318
362
|
accentColor: Palette.text
|
|
319
363
|
) {
|
|
320
364
|
ScreenMapWindowController.shared.showPage(.home)
|
|
321
365
|
}
|
|
322
366
|
ActionRow(
|
|
323
|
-
label: "
|
|
324
|
-
detail: "
|
|
367
|
+
label: "Layout",
|
|
368
|
+
detail: "Arrange windows and layers",
|
|
369
|
+
hotkeyTokens: [],
|
|
370
|
+
icon: "rectangle.3.group",
|
|
371
|
+
accentColor: Palette.running
|
|
372
|
+
) {
|
|
373
|
+
ScreenMapWindowController.shared.showPage(.screenMap)
|
|
374
|
+
}
|
|
375
|
+
ActionRow(
|
|
376
|
+
label: "Search",
|
|
377
|
+
detail: "Windows, projects, sessions, processes, and OCR",
|
|
325
378
|
hotkeyTokens: hotkeyTokens(.omniSearch),
|
|
326
379
|
icon: "magnifyingglass",
|
|
327
380
|
accentColor: AudioLayer.shared.isListening ? Palette.running : Palette.textDim
|
|
328
381
|
) {
|
|
329
|
-
|
|
382
|
+
ScreenMapWindowController.shared.showPage(.desktopInventory)
|
|
383
|
+
}
|
|
384
|
+
ActionRow(
|
|
385
|
+
label: "Command Palette",
|
|
386
|
+
detail: "Launch, attach, and control projects",
|
|
387
|
+
hotkeyTokens: hotkeyTokens(.palette),
|
|
388
|
+
icon: "command",
|
|
389
|
+
accentColor: Palette.running
|
|
390
|
+
) {
|
|
391
|
+
CommandPaletteWindow.shared.toggle()
|
|
330
392
|
}
|
|
331
393
|
}
|
|
332
394
|
.padding(.vertical, 4)
|
|
@@ -340,9 +402,6 @@ struct MainView: View {
|
|
|
340
402
|
}
|
|
341
403
|
|
|
342
404
|
HStack(spacing: 4) {
|
|
343
|
-
bottomBarButton(icon: "stethoscope", label: "Diagnostics") {
|
|
344
|
-
DiagnosticWindow.shared.toggle()
|
|
345
|
-
}
|
|
346
405
|
if !permChecker.allGranted {
|
|
347
406
|
Circle()
|
|
348
407
|
.fill(Palette.detach)
|
|
@@ -399,15 +458,6 @@ struct MainView: View {
|
|
|
399
458
|
}
|
|
400
459
|
}
|
|
401
460
|
|
|
402
|
-
private func showAssistant() {
|
|
403
|
-
if AudioLayer.shared.isListening || VoiceCommandWindow.shared.isVisible {
|
|
404
|
-
VoiceCommandWindow.shared.toggle()
|
|
405
|
-
return
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
OmniSearchWindow.shared.show()
|
|
409
|
-
}
|
|
410
|
-
|
|
411
461
|
// MARK: - Empty state
|
|
412
462
|
|
|
413
463
|
private var emptyState: some View {
|
|
@@ -420,12 +470,33 @@ struct MainView: View {
|
|
|
420
470
|
.font(Typo.heading(14))
|
|
421
471
|
.foregroundColor(Palette.textDim)
|
|
422
472
|
|
|
423
|
-
Text("
|
|
473
|
+
Text("Choose a repo and we’ll hand off to the CLI\nin your terminal.")
|
|
424
474
|
.font(Typo.mono(11))
|
|
425
475
|
.foregroundColor(Palette.textMuted)
|
|
426
476
|
.multilineTextAlignment(.center)
|
|
427
477
|
.lineSpacing(3)
|
|
478
|
+
|
|
479
|
+
HStack(spacing: 10) {
|
|
480
|
+
Button(action: CliActionLauncher.initializeProjectInTerminal) {
|
|
481
|
+
Text("Initialize Project")
|
|
482
|
+
.angularButton(Palette.running)
|
|
483
|
+
}
|
|
484
|
+
.buttonStyle(.plain)
|
|
485
|
+
|
|
486
|
+
Button(action: CliActionLauncher.launchProjectInTerminal) {
|
|
487
|
+
Text("Launch Project")
|
|
488
|
+
.angularButton(.white, filled: false)
|
|
489
|
+
}
|
|
490
|
+
.buttonStyle(.plain)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
Text("Initialize runs lattices init && lattices in the folder you choose.")
|
|
494
|
+
.font(Typo.mono(9))
|
|
495
|
+
.foregroundColor(Palette.textMuted)
|
|
496
|
+
.multilineTextAlignment(.center)
|
|
497
|
+
.lineSpacing(2)
|
|
428
498
|
}
|
|
499
|
+
.padding(.horizontal, 20)
|
|
429
500
|
}
|
|
430
501
|
|
|
431
502
|
// MARK: - Permission banner
|
|
@@ -451,11 +522,11 @@ struct MainView: View {
|
|
|
451
522
|
permissionRow("Accessibility", granted: permChecker.accessibility) {
|
|
452
523
|
permChecker.requestAccessibility()
|
|
453
524
|
}
|
|
454
|
-
permissionRow("Screen
|
|
525
|
+
permissionRow("Screen Capture", granted: permChecker.screenRecording) {
|
|
455
526
|
permChecker.requestScreenRecording()
|
|
456
527
|
}
|
|
457
528
|
|
|
458
|
-
Text("Click a row to
|
|
529
|
+
Text("Click a row to continue the permission flow in macOS.")
|
|
459
530
|
.font(Typo.mono(9))
|
|
460
531
|
.foregroundColor(Palette.textMuted)
|
|
461
532
|
}
|
|
@@ -572,54 +643,6 @@ struct MainView: View {
|
|
|
572
643
|
.disabled(granted)
|
|
573
644
|
}
|
|
574
645
|
|
|
575
|
-
// MARK: - Layer Bar
|
|
576
|
-
|
|
577
|
-
private func layerBar(config: WorkspaceConfig) -> some View {
|
|
578
|
-
HStack(spacing: 6) {
|
|
579
|
-
ForEach(Array((config.layers ?? []).enumerated()), id: \.element.id) { i, layer in
|
|
580
|
-
let isActive = i == workspace.activeLayerIndex
|
|
581
|
-
let counts = workspace.layerRunningCount(index: i)
|
|
582
|
-
Button {
|
|
583
|
-
workspace.tileLayer(index: i)
|
|
584
|
-
} label: {
|
|
585
|
-
VStack(spacing: 2) {
|
|
586
|
-
HStack(spacing: 5) {
|
|
587
|
-
Circle()
|
|
588
|
-
.fill(isActive ? Palette.running : Palette.textMuted.opacity(0.4))
|
|
589
|
-
.frame(width: 6, height: 6)
|
|
590
|
-
Text(layer.label)
|
|
591
|
-
.font(Typo.mono(11))
|
|
592
|
-
.foregroundColor(isActive ? Palette.text : Palette.textDim)
|
|
593
|
-
if counts.total > 0 {
|
|
594
|
-
Text("\(counts.running)/\(counts.total)")
|
|
595
|
-
.font(Typo.mono(8))
|
|
596
|
-
.foregroundColor(counts.running > 0 ? Palette.running : Palette.textMuted)
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
Text("\u{2325}\(i + 1)")
|
|
600
|
-
.font(Typo.mono(8))
|
|
601
|
-
.foregroundColor(Palette.textMuted.opacity(0.6))
|
|
602
|
-
}
|
|
603
|
-
.padding(.horizontal, 10)
|
|
604
|
-
.padding(.vertical, 5)
|
|
605
|
-
.background(
|
|
606
|
-
RoundedRectangle(cornerRadius: 5)
|
|
607
|
-
.fill(isActive ? Palette.running.opacity(0.1) : Color.clear)
|
|
608
|
-
)
|
|
609
|
-
.overlay(
|
|
610
|
-
RoundedRectangle(cornerRadius: 5)
|
|
611
|
-
.strokeBorder(isActive ? Palette.running.opacity(0.3) : Palette.border, lineWidth: 0.5)
|
|
612
|
-
)
|
|
613
|
-
}
|
|
614
|
-
.buttonStyle(.plain)
|
|
615
|
-
.disabled(workspace.isSwitching)
|
|
616
|
-
}
|
|
617
|
-
Spacer()
|
|
618
|
-
}
|
|
619
|
-
.padding(.horizontal, 14)
|
|
620
|
-
.padding(.bottom, 8)
|
|
621
|
-
}
|
|
622
|
-
|
|
623
646
|
// MARK: - Helpers
|
|
624
647
|
|
|
625
648
|
private func headerButton(icon: String, action: @escaping () -> Void) -> some View {
|
|
@@ -631,4 +654,141 @@ struct MainView: View {
|
|
|
631
654
|
}
|
|
632
655
|
.buttonStyle(.plain)
|
|
633
656
|
}
|
|
657
|
+
|
|
658
|
+
private func launchProject(_ project: Project) {
|
|
659
|
+
SessionManager.launch(project: project)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
private func detachProject(_ project: Project) {
|
|
663
|
+
SessionManager.detach(project: project)
|
|
664
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
665
|
+
scanner.refreshStatus()
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
private func killProject(_ project: Project) {
|
|
670
|
+
SessionManager.kill(project: project)
|
|
671
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
672
|
+
scanner.refreshStatus()
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private func syncProject(_ project: Project) {
|
|
677
|
+
SessionManager.sync(project: project)
|
|
678
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
|
679
|
+
scanner.refreshStatus()
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private func restartProject(_ project: Project, paneName: String?) {
|
|
684
|
+
SessionManager.restart(project: project, paneName: paneName)
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private struct HomeProjectCard: View {
|
|
689
|
+
let project: Project
|
|
690
|
+
let onLaunch: () -> Void
|
|
691
|
+
let onDetach: () -> Void
|
|
692
|
+
let onKill: () -> Void
|
|
693
|
+
let onSync: () -> Void
|
|
694
|
+
let onRestart: (String?) -> Void
|
|
695
|
+
|
|
696
|
+
@State private var isHovered = false
|
|
697
|
+
|
|
698
|
+
private var summaryText: String {
|
|
699
|
+
if !project.paneSummary.isEmpty { return project.paneSummary }
|
|
700
|
+
if let cmd = project.devCommand, !cmd.isEmpty { return cmd }
|
|
701
|
+
return project.hasConfig
|
|
702
|
+
? "\(project.paneCount) pane\(project.paneCount == 1 ? "" : "s")"
|
|
703
|
+
: "No config"
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
var body: some View {
|
|
707
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
708
|
+
HStack(alignment: .top, spacing: 8) {
|
|
709
|
+
Circle()
|
|
710
|
+
.fill(project.isRunning ? Palette.running : Palette.borderLit)
|
|
711
|
+
.frame(width: 7, height: 7)
|
|
712
|
+
.padding(.top, 4)
|
|
713
|
+
|
|
714
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
715
|
+
Text(project.name)
|
|
716
|
+
.font(Typo.heading(13))
|
|
717
|
+
.foregroundColor(Palette.text)
|
|
718
|
+
.lineLimit(1)
|
|
719
|
+
|
|
720
|
+
Text(summaryText)
|
|
721
|
+
.font(Typo.mono(10))
|
|
722
|
+
.foregroundColor(Palette.textMuted)
|
|
723
|
+
.lineLimit(2)
|
|
724
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
Spacer(minLength: 0)
|
|
728
|
+
|
|
729
|
+
if project.isRunning {
|
|
730
|
+
Text("LIVE")
|
|
731
|
+
.font(Typo.monoBold(8))
|
|
732
|
+
.foregroundColor(Palette.running)
|
|
733
|
+
.padding(.horizontal, 6)
|
|
734
|
+
.padding(.vertical, 3)
|
|
735
|
+
.background(
|
|
736
|
+
Capsule()
|
|
737
|
+
.fill(Palette.running.opacity(0.10))
|
|
738
|
+
)
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
Spacer(minLength: 0)
|
|
743
|
+
|
|
744
|
+
HStack(spacing: 6) {
|
|
745
|
+
if project.isRunning {
|
|
746
|
+
Button(action: onDetach) {
|
|
747
|
+
Image(systemName: "rectangle.portrait.and.arrow.right")
|
|
748
|
+
.font(.system(size: 10, weight: .medium))
|
|
749
|
+
.angularButton(Palette.detach, filled: false)
|
|
750
|
+
}
|
|
751
|
+
.buttonStyle(.plain)
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
Spacer(minLength: 0)
|
|
755
|
+
|
|
756
|
+
Button(action: onLaunch) {
|
|
757
|
+
Text(project.isRunning ? "Attach" : "Launch")
|
|
758
|
+
.angularButton(project.isRunning ? Palette.running : Palette.launch)
|
|
759
|
+
}
|
|
760
|
+
.buttonStyle(.plain)
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
.padding(12)
|
|
764
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
765
|
+
.glassCard(hovered: isHovered)
|
|
766
|
+
.contentShape(Rectangle())
|
|
767
|
+
.onHover { isHovered = $0 }
|
|
768
|
+
.contextMenu {
|
|
769
|
+
if project.isRunning {
|
|
770
|
+
Button("Attach") { onLaunch() }
|
|
771
|
+
Button("Detach") { onDetach() }
|
|
772
|
+
Button {
|
|
773
|
+
WindowTiler.navigateToWindow(
|
|
774
|
+
session: project.sessionName,
|
|
775
|
+
terminal: Preferences.shared.terminal
|
|
776
|
+
)
|
|
777
|
+
} label: {
|
|
778
|
+
Label("Go to Window", systemImage: "macwindow")
|
|
779
|
+
}
|
|
780
|
+
Divider()
|
|
781
|
+
Button("Sync Session") { onSync() }
|
|
782
|
+
Menu("Restart Pane") {
|
|
783
|
+
ForEach(project.paneNames, id: \.self) { name in
|
|
784
|
+
Button(name) { onRestart(name) }
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
Divider()
|
|
788
|
+
Button("Kill Session") { onKill() }
|
|
789
|
+
} else {
|
|
790
|
+
Button("Launch") { onLaunch() }
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
634
794
|
}
|