@lattices/cli 0.4.1 → 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/ActionRow.swift +43 -26
- package/app/Sources/App.swift +10 -0
- package/app/Sources/AppDelegate.swift +91 -30
- 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 +53 -16
- 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 +398 -186
- 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 +65 -1
- 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 +4 -2
|
@@ -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
|
-
|
|
217
|
-
|
|
238
|
+
Spacer()
|
|
239
|
+
|
|
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
|
+
}
|
|
218
268
|
|
|
219
|
-
|
|
220
|
-
|
|
269
|
+
if !filteredOrphans.isEmpty {
|
|
270
|
+
orphanSection
|
|
271
|
+
.padding(.horizontal, 10)
|
|
272
|
+
.padding(.bottom, 8)
|
|
273
|
+
}
|
|
221
274
|
}
|
|
222
275
|
}
|
|
223
276
|
|
|
@@ -283,27 +336,59 @@ struct MainView: View {
|
|
|
283
336
|
|
|
284
337
|
private var actionsSection: some View {
|
|
285
338
|
VStack(spacing: 0) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
339
|
+
HStack {
|
|
340
|
+
Text("Quick Actions")
|
|
341
|
+
.font(Typo.monoBold(10))
|
|
342
|
+
.foregroundColor(Palette.textMuted)
|
|
343
|
+
|
|
344
|
+
Spacer()
|
|
345
|
+
|
|
346
|
+
Button("Help & shortcuts") {
|
|
347
|
+
SettingsWindowController.shared.show()
|
|
348
|
+
}
|
|
349
|
+
.buttonStyle(.plain)
|
|
350
|
+
.font(Typo.mono(9))
|
|
351
|
+
.foregroundColor(Palette.textMuted)
|
|
294
352
|
}
|
|
295
|
-
|
|
296
|
-
|
|
353
|
+
.padding(.horizontal, 14)
|
|
354
|
+
.padding(.top, 10)
|
|
355
|
+
.padding(.bottom, 6)
|
|
356
|
+
|
|
357
|
+
ActionRow(
|
|
358
|
+
label: "Home",
|
|
359
|
+
detail: "Workspace overview and project launcher",
|
|
360
|
+
hotkeyTokens: hotkeyTokens(.unifiedWindow),
|
|
361
|
+
icon: "house",
|
|
362
|
+
accentColor: Palette.text
|
|
363
|
+
) {
|
|
364
|
+
ScreenMapWindowController.shared.showPage(.home)
|
|
297
365
|
}
|
|
298
|
-
ActionRow(
|
|
299
|
-
|
|
366
|
+
ActionRow(
|
|
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)
|
|
300
374
|
}
|
|
301
|
-
ActionRow(
|
|
302
|
-
|
|
375
|
+
ActionRow(
|
|
376
|
+
label: "Search",
|
|
377
|
+
detail: "Windows, projects, sessions, processes, and OCR",
|
|
378
|
+
hotkeyTokens: hotkeyTokens(.omniSearch),
|
|
379
|
+
icon: "magnifyingglass",
|
|
380
|
+
accentColor: AudioLayer.shared.isListening ? Palette.running : Palette.textDim
|
|
381
|
+
) {
|
|
382
|
+
ScreenMapWindowController.shared.showPage(.desktopInventory)
|
|
303
383
|
}
|
|
304
|
-
ActionRow(
|
|
305
|
-
|
|
306
|
-
|
|
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()
|
|
307
392
|
}
|
|
308
393
|
}
|
|
309
394
|
.padding(.vertical, 4)
|
|
@@ -317,9 +402,6 @@ struct MainView: View {
|
|
|
317
402
|
}
|
|
318
403
|
|
|
319
404
|
HStack(spacing: 4) {
|
|
320
|
-
bottomBarButton(icon: "stethoscope", label: "Diagnostics") {
|
|
321
|
-
DiagnosticWindow.shared.toggle()
|
|
322
|
-
}
|
|
323
405
|
if !permChecker.allGranted {
|
|
324
406
|
Circle()
|
|
325
407
|
.fill(Palette.detach)
|
|
@@ -351,9 +433,29 @@ struct MainView: View {
|
|
|
351
433
|
.buttonStyle(.plain)
|
|
352
434
|
}
|
|
353
435
|
|
|
354
|
-
private func
|
|
355
|
-
guard let binding = HotkeyStore.shared.bindings[action]
|
|
356
|
-
|
|
436
|
+
private func hotkeyTokens(_ action: HotkeyAction) -> [String] {
|
|
437
|
+
guard let binding = HotkeyStore.shared.bindings[action],
|
|
438
|
+
let key = binding.displayParts.last else { return [] }
|
|
439
|
+
|
|
440
|
+
let modifiers = Set(binding.displayParts.dropLast())
|
|
441
|
+
if modifiers == Set(["Ctrl", "Option", "Shift", "Cmd"]) {
|
|
442
|
+
return ["Hyper", shortenHotkeyToken(key)]
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return binding.displayParts.map(shortenHotkeyToken)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private func shortenHotkeyToken(_ token: String) -> String {
|
|
449
|
+
switch token {
|
|
450
|
+
case "Cmd": return "⌘"
|
|
451
|
+
case "Shift": return "⇧"
|
|
452
|
+
case "Option": return "⌥"
|
|
453
|
+
case "Ctrl": return "⌃"
|
|
454
|
+
case "Return": return "↩"
|
|
455
|
+
case "Escape": return "Esc"
|
|
456
|
+
case "Space": return "Space"
|
|
457
|
+
default: return token
|
|
458
|
+
}
|
|
357
459
|
}
|
|
358
460
|
|
|
359
461
|
// MARK: - Empty state
|
|
@@ -368,12 +470,33 @@ struct MainView: View {
|
|
|
368
470
|
.font(Typo.heading(14))
|
|
369
471
|
.foregroundColor(Palette.textDim)
|
|
370
472
|
|
|
371
|
-
Text("
|
|
473
|
+
Text("Choose a repo and we’ll hand off to the CLI\nin your terminal.")
|
|
372
474
|
.font(Typo.mono(11))
|
|
373
475
|
.foregroundColor(Palette.textMuted)
|
|
374
476
|
.multilineTextAlignment(.center)
|
|
375
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)
|
|
376
498
|
}
|
|
499
|
+
.padding(.horizontal, 20)
|
|
377
500
|
}
|
|
378
501
|
|
|
379
502
|
// MARK: - Permission banner
|
|
@@ -399,11 +522,11 @@ struct MainView: View {
|
|
|
399
522
|
permissionRow("Accessibility", granted: permChecker.accessibility) {
|
|
400
523
|
permChecker.requestAccessibility()
|
|
401
524
|
}
|
|
402
|
-
permissionRow("Screen
|
|
525
|
+
permissionRow("Screen Capture", granted: permChecker.screenRecording) {
|
|
403
526
|
permChecker.requestScreenRecording()
|
|
404
527
|
}
|
|
405
528
|
|
|
406
|
-
Text("Click a row to
|
|
529
|
+
Text("Click a row to continue the permission flow in macOS.")
|
|
407
530
|
.font(Typo.mono(9))
|
|
408
531
|
.foregroundColor(Palette.textMuted)
|
|
409
532
|
}
|
|
@@ -520,54 +643,6 @@ struct MainView: View {
|
|
|
520
643
|
.disabled(granted)
|
|
521
644
|
}
|
|
522
645
|
|
|
523
|
-
// MARK: - Layer Bar
|
|
524
|
-
|
|
525
|
-
private func layerBar(config: WorkspaceConfig) -> some View {
|
|
526
|
-
HStack(spacing: 6) {
|
|
527
|
-
ForEach(Array((config.layers ?? []).enumerated()), id: \.element.id) { i, layer in
|
|
528
|
-
let isActive = i == workspace.activeLayerIndex
|
|
529
|
-
let counts = workspace.layerRunningCount(index: i)
|
|
530
|
-
Button {
|
|
531
|
-
workspace.tileLayer(index: i)
|
|
532
|
-
} label: {
|
|
533
|
-
VStack(spacing: 2) {
|
|
534
|
-
HStack(spacing: 5) {
|
|
535
|
-
Circle()
|
|
536
|
-
.fill(isActive ? Palette.running : Palette.textMuted.opacity(0.4))
|
|
537
|
-
.frame(width: 6, height: 6)
|
|
538
|
-
Text(layer.label)
|
|
539
|
-
.font(Typo.mono(11))
|
|
540
|
-
.foregroundColor(isActive ? Palette.text : Palette.textDim)
|
|
541
|
-
if counts.total > 0 {
|
|
542
|
-
Text("\(counts.running)/\(counts.total)")
|
|
543
|
-
.font(Typo.mono(8))
|
|
544
|
-
.foregroundColor(counts.running > 0 ? Palette.running : Palette.textMuted)
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
Text("\u{2325}\(i + 1)")
|
|
548
|
-
.font(Typo.mono(8))
|
|
549
|
-
.foregroundColor(Palette.textMuted.opacity(0.6))
|
|
550
|
-
}
|
|
551
|
-
.padding(.horizontal, 10)
|
|
552
|
-
.padding(.vertical, 5)
|
|
553
|
-
.background(
|
|
554
|
-
RoundedRectangle(cornerRadius: 5)
|
|
555
|
-
.fill(isActive ? Palette.running.opacity(0.1) : Color.clear)
|
|
556
|
-
)
|
|
557
|
-
.overlay(
|
|
558
|
-
RoundedRectangle(cornerRadius: 5)
|
|
559
|
-
.strokeBorder(isActive ? Palette.running.opacity(0.3) : Palette.border, lineWidth: 0.5)
|
|
560
|
-
)
|
|
561
|
-
}
|
|
562
|
-
.buttonStyle(.plain)
|
|
563
|
-
.disabled(workspace.isSwitching)
|
|
564
|
-
}
|
|
565
|
-
Spacer()
|
|
566
|
-
}
|
|
567
|
-
.padding(.horizontal, 14)
|
|
568
|
-
.padding(.bottom, 8)
|
|
569
|
-
}
|
|
570
|
-
|
|
571
646
|
// MARK: - Helpers
|
|
572
647
|
|
|
573
648
|
private func headerButton(icon: String, action: @escaping () -> Void) -> some View {
|
|
@@ -579,4 +654,141 @@ struct MainView: View {
|
|
|
579
654
|
}
|
|
580
655
|
.buttonStyle(.plain)
|
|
581
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
|
+
}
|
|
582
794
|
}
|