@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,348 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
// MARK: - LauncherHUD (singleton window controller)
|
|
5
|
+
|
|
6
|
+
final class LauncherHUD {
|
|
7
|
+
static let shared = LauncherHUD()
|
|
8
|
+
|
|
9
|
+
private var panel: NSPanel?
|
|
10
|
+
private var localMonitor: Any?
|
|
11
|
+
private var globalMonitor: Any?
|
|
12
|
+
|
|
13
|
+
var isVisible: Bool { panel?.isVisible ?? false }
|
|
14
|
+
|
|
15
|
+
func toggle() {
|
|
16
|
+
if isVisible { dismiss() } else { show() }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func show() {
|
|
20
|
+
guard panel == nil else { return }
|
|
21
|
+
|
|
22
|
+
// Ensure projects are fresh
|
|
23
|
+
ProjectScanner.shared.scan()
|
|
24
|
+
|
|
25
|
+
let view = LauncherView(dismiss: { [weak self] in self?.dismiss() })
|
|
26
|
+
.preferredColorScheme(.dark)
|
|
27
|
+
|
|
28
|
+
let hosting = NSHostingView(rootView: view)
|
|
29
|
+
|
|
30
|
+
let p = NSPanel(
|
|
31
|
+
contentRect: NSRect(x: 0, y: 0, width: 420, height: 480),
|
|
32
|
+
styleMask: [.borderless, .nonactivatingPanel],
|
|
33
|
+
backing: .buffered,
|
|
34
|
+
defer: false
|
|
35
|
+
)
|
|
36
|
+
p.isOpaque = false
|
|
37
|
+
p.backgroundColor = .clear
|
|
38
|
+
p.level = .floating
|
|
39
|
+
p.hasShadow = true
|
|
40
|
+
p.hidesOnDeactivate = false
|
|
41
|
+
p.isReleasedWhenClosed = false
|
|
42
|
+
p.isMovableByWindowBackground = false
|
|
43
|
+
p.contentView = hosting
|
|
44
|
+
|
|
45
|
+
// Center on mouse screen
|
|
46
|
+
let mouseLocation = NSEvent.mouseLocation
|
|
47
|
+
let screen = NSScreen.screens.first(where: { $0.frame.contains(mouseLocation) }) ?? NSScreen.main ?? NSScreen.screens.first!
|
|
48
|
+
let screenFrame = screen.visibleFrame
|
|
49
|
+
let x = screenFrame.midX - 210
|
|
50
|
+
let y = screenFrame.midY - 240 + (screenFrame.height * 0.08)
|
|
51
|
+
p.setFrameOrigin(NSPoint(x: x, y: y))
|
|
52
|
+
|
|
53
|
+
p.alphaValue = 0
|
|
54
|
+
p.orderFrontRegardless()
|
|
55
|
+
|
|
56
|
+
NSAnimationContext.runAnimationGroup { ctx in
|
|
57
|
+
ctx.duration = 0.12
|
|
58
|
+
p.animator().alphaValue = 1.0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
self.panel = p
|
|
62
|
+
installMonitors()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func dismiss() {
|
|
66
|
+
guard let p = panel else { return }
|
|
67
|
+
removeMonitors()
|
|
68
|
+
|
|
69
|
+
NSAnimationContext.runAnimationGroup({ ctx in
|
|
70
|
+
ctx.duration = 0.15
|
|
71
|
+
p.animator().alphaValue = 0
|
|
72
|
+
}) { [weak self] in
|
|
73
|
+
p.orderOut(nil)
|
|
74
|
+
self?.panel = nil
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// MARK: - Event monitors
|
|
79
|
+
|
|
80
|
+
private func installMonitors() {
|
|
81
|
+
localMonitor = NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { [weak self] event in
|
|
82
|
+
if event.keyCode == 53 { // Escape
|
|
83
|
+
self?.dismiss()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { [weak self] event in
|
|
87
|
+
// Don't dismiss if clicking inside the panel
|
|
88
|
+
guard let panel = self?.panel else { return }
|
|
89
|
+
let loc = event.locationInWindow
|
|
90
|
+
if !panel.frame.contains(NSEvent.mouseLocation) {
|
|
91
|
+
self?.dismiss()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private func removeMonitors() {
|
|
97
|
+
if let m = localMonitor { NSEvent.removeMonitor(m); localMonitor = nil }
|
|
98
|
+
if let m = globalMonitor { NSEvent.removeMonitor(m); globalMonitor = nil }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// MARK: - LauncherView
|
|
103
|
+
|
|
104
|
+
struct LauncherView: View {
|
|
105
|
+
var dismiss: () -> Void
|
|
106
|
+
|
|
107
|
+
@ObservedObject private var scanner = ProjectScanner.shared
|
|
108
|
+
@ObservedObject private var tmux = TmuxModel.shared
|
|
109
|
+
@State private var query = ""
|
|
110
|
+
@State private var selectedIndex = 0
|
|
111
|
+
@State private var hoveredId: String?
|
|
112
|
+
|
|
113
|
+
private var filtered: [Project] {
|
|
114
|
+
if query.isEmpty { return scanner.projects }
|
|
115
|
+
let q = query.lowercased()
|
|
116
|
+
return scanner.projects.filter {
|
|
117
|
+
$0.name.lowercased().contains(q) ||
|
|
118
|
+
($0.paneSummary ?? "").lowercased().contains(q)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
var body: some View {
|
|
123
|
+
VStack(spacing: 0) {
|
|
124
|
+
// Search bar
|
|
125
|
+
HStack(spacing: 10) {
|
|
126
|
+
Image(systemName: "magnifyingglass")
|
|
127
|
+
.font(.system(size: 13))
|
|
128
|
+
.foregroundColor(Palette.textMuted)
|
|
129
|
+
|
|
130
|
+
ZStack(alignment: .leading) {
|
|
131
|
+
if query.isEmpty {
|
|
132
|
+
Text("Launch a project...")
|
|
133
|
+
.font(Typo.mono(13))
|
|
134
|
+
.foregroundColor(Palette.textMuted)
|
|
135
|
+
}
|
|
136
|
+
TextField("", text: $query)
|
|
137
|
+
.font(Typo.mono(13))
|
|
138
|
+
.foregroundColor(Palette.text)
|
|
139
|
+
.textFieldStyle(.plain)
|
|
140
|
+
.onSubmit { launchSelected() }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if !query.isEmpty {
|
|
144
|
+
Button {
|
|
145
|
+
query = ""
|
|
146
|
+
selectedIndex = 0
|
|
147
|
+
} label: {
|
|
148
|
+
Image(systemName: "xmark.circle.fill")
|
|
149
|
+
.font(.system(size: 12))
|
|
150
|
+
.foregroundColor(Palette.textMuted)
|
|
151
|
+
}
|
|
152
|
+
.buttonStyle(.plain)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
.padding(.horizontal, 16)
|
|
156
|
+
.padding(.vertical, 12)
|
|
157
|
+
|
|
158
|
+
Rectangle().fill(Palette.border).frame(height: 0.5)
|
|
159
|
+
|
|
160
|
+
// Project list
|
|
161
|
+
if filtered.isEmpty {
|
|
162
|
+
Spacer()
|
|
163
|
+
VStack(spacing: 8) {
|
|
164
|
+
Image(systemName: "folder.badge.questionmark")
|
|
165
|
+
.font(.system(size: 24))
|
|
166
|
+
.foregroundColor(Palette.textMuted)
|
|
167
|
+
Text(scanner.projects.isEmpty ? "No projects found" : "No matches")
|
|
168
|
+
.font(Typo.mono(12))
|
|
169
|
+
.foregroundColor(Palette.textMuted)
|
|
170
|
+
if scanner.projects.isEmpty {
|
|
171
|
+
Text("Add .lattices.json to your projects")
|
|
172
|
+
.font(Typo.mono(10))
|
|
173
|
+
.foregroundColor(Palette.textMuted.opacity(0.6))
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
Spacer()
|
|
177
|
+
} else {
|
|
178
|
+
ScrollViewReader { proxy in
|
|
179
|
+
ScrollView {
|
|
180
|
+
LazyVStack(spacing: 2) {
|
|
181
|
+
ForEach(Array(filtered.enumerated()), id: \.element.id) { index, project in
|
|
182
|
+
projectRow(project, index: index)
|
|
183
|
+
.id(project.id)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
.padding(.vertical, 6)
|
|
187
|
+
.padding(.horizontal, 8)
|
|
188
|
+
}
|
|
189
|
+
.onChange(of: selectedIndex) { newVal in
|
|
190
|
+
if let project = filtered[safe: newVal] {
|
|
191
|
+
proxy.scrollTo(project.id, anchor: .center)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
Rectangle().fill(Palette.border).frame(height: 0.5)
|
|
198
|
+
|
|
199
|
+
// Footer
|
|
200
|
+
HStack(spacing: 16) {
|
|
201
|
+
HStack(spacing: 4) {
|
|
202
|
+
keyBadge("↑↓")
|
|
203
|
+
Text("Navigate")
|
|
204
|
+
.font(Typo.mono(10))
|
|
205
|
+
.foregroundColor(Palette.textMuted)
|
|
206
|
+
}
|
|
207
|
+
HStack(spacing: 4) {
|
|
208
|
+
keyBadge("↵")
|
|
209
|
+
Text("Launch")
|
|
210
|
+
.font(Typo.mono(10))
|
|
211
|
+
.foregroundColor(Palette.textMuted)
|
|
212
|
+
}
|
|
213
|
+
HStack(spacing: 4) {
|
|
214
|
+
keyBadge("ESC")
|
|
215
|
+
Text("Close")
|
|
216
|
+
.font(Typo.mono(10))
|
|
217
|
+
.foregroundColor(Palette.textMuted)
|
|
218
|
+
}
|
|
219
|
+
Spacer()
|
|
220
|
+
Text("\(filtered.count) project\(filtered.count == 1 ? "" : "s")")
|
|
221
|
+
.font(Typo.mono(10))
|
|
222
|
+
.foregroundColor(Palette.textMuted)
|
|
223
|
+
}
|
|
224
|
+
.padding(.horizontal, 16)
|
|
225
|
+
.padding(.vertical, 8)
|
|
226
|
+
}
|
|
227
|
+
.frame(width: 420, height: 480)
|
|
228
|
+
.background(
|
|
229
|
+
RoundedRectangle(cornerRadius: 12)
|
|
230
|
+
.fill(Palette.bg)
|
|
231
|
+
.overlay(
|
|
232
|
+
RoundedRectangle(cornerRadius: 12)
|
|
233
|
+
.strokeBorder(Palette.borderLit, lineWidth: 0.5)
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
237
|
+
.onChange(of: query) { _ in selectedIndex = 0 }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// MARK: - Project row
|
|
241
|
+
|
|
242
|
+
private func projectRow(_ project: Project, index: Int) -> some View {
|
|
243
|
+
let isSelected = index == selectedIndex
|
|
244
|
+
let isHovered = hoveredId == project.id
|
|
245
|
+
|
|
246
|
+
return Button {
|
|
247
|
+
launch(project)
|
|
248
|
+
} label: {
|
|
249
|
+
HStack(spacing: 10) {
|
|
250
|
+
// Status dot
|
|
251
|
+
Circle()
|
|
252
|
+
.fill(project.isRunning ? Palette.running : Palette.textMuted.opacity(0.3))
|
|
253
|
+
.frame(width: 7, height: 7)
|
|
254
|
+
|
|
255
|
+
// Name + pane info
|
|
256
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
257
|
+
Text(project.name)
|
|
258
|
+
.font(Typo.monoBold(12))
|
|
259
|
+
.foregroundColor(Palette.text)
|
|
260
|
+
.lineLimit(1)
|
|
261
|
+
|
|
262
|
+
HStack(spacing: 6) {
|
|
263
|
+
if !project.paneSummary.isEmpty {
|
|
264
|
+
let summary = project.paneSummary
|
|
265
|
+
Text(summary)
|
|
266
|
+
.font(Typo.mono(10))
|
|
267
|
+
.foregroundColor(Palette.textDim)
|
|
268
|
+
.lineLimit(1)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
Spacer()
|
|
274
|
+
|
|
275
|
+
// Status badge
|
|
276
|
+
if project.isRunning {
|
|
277
|
+
Text("running")
|
|
278
|
+
.font(Typo.mono(9))
|
|
279
|
+
.foregroundColor(Palette.running)
|
|
280
|
+
.padding(.horizontal, 6)
|
|
281
|
+
.padding(.vertical, 2)
|
|
282
|
+
.background(
|
|
283
|
+
RoundedRectangle(cornerRadius: 3)
|
|
284
|
+
.fill(Palette.running.opacity(0.10))
|
|
285
|
+
)
|
|
286
|
+
} else {
|
|
287
|
+
Image(systemName: "play.fill")
|
|
288
|
+
.font(.system(size: 9))
|
|
289
|
+
.foregroundColor(isSelected || isHovered ? Palette.text : Palette.textMuted)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
.padding(.horizontal, 10)
|
|
293
|
+
.padding(.vertical, 8)
|
|
294
|
+
.background(
|
|
295
|
+
RoundedRectangle(cornerRadius: 6)
|
|
296
|
+
.fill(isSelected ? Palette.surfaceHov : (isHovered ? Palette.surface : Color.clear))
|
|
297
|
+
.overlay(
|
|
298
|
+
RoundedRectangle(cornerRadius: 6)
|
|
299
|
+
.strokeBorder(isSelected ? Palette.borderLit : Color.clear, lineWidth: 0.5)
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
.buttonStyle(.plain)
|
|
304
|
+
.onHover { over in hoveredId = over ? project.id : nil }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// MARK: - Actions
|
|
308
|
+
|
|
309
|
+
private func moveSelection(_ delta: Int) {
|
|
310
|
+
let count = filtered.count
|
|
311
|
+
guard count > 0 else { return }
|
|
312
|
+
selectedIndex = (selectedIndex + delta + count) % count
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private func launchSelected() {
|
|
316
|
+
guard let project = filtered[safe: selectedIndex] else { return }
|
|
317
|
+
launch(project)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private func launch(_ project: Project) {
|
|
321
|
+
SessionManager.launch(project: project)
|
|
322
|
+
dismiss()
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private func keyBadge(_ key: String) -> some View {
|
|
326
|
+
Text(key)
|
|
327
|
+
.font(Typo.geistMonoBold(9))
|
|
328
|
+
.foregroundColor(Palette.text)
|
|
329
|
+
.padding(.horizontal, 5)
|
|
330
|
+
.padding(.vertical, 2)
|
|
331
|
+
.background(
|
|
332
|
+
RoundedRectangle(cornerRadius: 3)
|
|
333
|
+
.fill(Palette.surface)
|
|
334
|
+
.overlay(
|
|
335
|
+
RoundedRectangle(cornerRadius: 3)
|
|
336
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// MARK: - Safe array subscript
|
|
343
|
+
|
|
344
|
+
private extension Array {
|
|
345
|
+
subscript(safe index: Int) -> Element? {
|
|
346
|
+
indices.contains(index) ? self[index] : nil
|
|
347
|
+
}
|
|
348
|
+
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
2
|
|
|
3
|
+
enum MainViewLayout {
|
|
4
|
+
case popover
|
|
5
|
+
case embedded
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
struct MainView: View {
|
|
4
9
|
@ObservedObject var scanner: ProjectScanner
|
|
10
|
+
var layout: MainViewLayout = .popover
|
|
5
11
|
@StateObject private var prefs = Preferences.shared
|
|
6
12
|
@StateObject private var permChecker = PermissionChecker.shared
|
|
7
13
|
@ObservedObject private var workspace = WorkspaceManager.shared
|
|
@@ -9,6 +15,8 @@ struct MainView: View {
|
|
|
9
15
|
@State private var searchText = ""
|
|
10
16
|
@State private var hasCheckedSetup = false
|
|
11
17
|
@State private var bannerDismissed = false
|
|
18
|
+
@State private var tmuxBannerDismissed = false
|
|
19
|
+
@ObservedObject private var tmuxModel = TmuxModel.shared
|
|
12
20
|
@State private var orphanSectionCollapsed = true
|
|
13
21
|
private var filtered: [Project] {
|
|
14
22
|
if searchText.isEmpty { return scanner.projects }
|
|
@@ -31,7 +39,14 @@ struct MainView: View {
|
|
|
31
39
|
VStack(spacing: 0) {
|
|
32
40
|
mainContent
|
|
33
41
|
}
|
|
34
|
-
.frame(
|
|
42
|
+
.frame(
|
|
43
|
+
minWidth: layout == .popover ? 380 : 0,
|
|
44
|
+
idealWidth: layout == .popover ? 380 : nil,
|
|
45
|
+
maxWidth: .infinity,
|
|
46
|
+
minHeight: layout == .popover ? 520 : 0,
|
|
47
|
+
idealHeight: layout == .popover ? 560 : nil,
|
|
48
|
+
maxHeight: .infinity
|
|
49
|
+
)
|
|
35
50
|
.background(PanelBackground())
|
|
36
51
|
.preferredColorScheme(.dark)
|
|
37
52
|
.onAppear {
|
|
@@ -61,36 +76,32 @@ struct MainView: View {
|
|
|
61
76
|
|
|
62
77
|
private var mainContent: some View {
|
|
63
78
|
VStack(spacing: 0) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
79
|
+
if layout == .popover {
|
|
80
|
+
HStack {
|
|
81
|
+
Text("Lattices")
|
|
82
|
+
.font(Typo.mono(14))
|
|
83
|
+
.foregroundColor(Palette.text)
|
|
69
84
|
|
|
70
|
-
|
|
71
|
-
let total = runningCount + inventory.orphans.count
|
|
72
|
-
Text("\(total) session\(total == 1 ? "" : "s")")
|
|
73
|
-
.font(Typo.mono(10))
|
|
74
|
-
.foregroundColor(Palette.running)
|
|
75
|
-
.padding(.leading, 4)
|
|
76
|
-
} else {
|
|
77
|
-
Text("None")
|
|
78
|
-
.font(Typo.mono(10))
|
|
79
|
-
.foregroundColor(Palette.textMuted)
|
|
80
|
-
.padding(.leading, 4)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
Spacer()
|
|
85
|
+
Spacer()
|
|
84
86
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
headerButton(icon: "house") {
|
|
88
|
+
(NSApp.delegate as? AppDelegate)?.dismissPopover()
|
|
89
|
+
ScreenMapWindowController.shared.showPage(.home)
|
|
90
|
+
}
|
|
91
|
+
headerButton(icon: "terminal") {
|
|
92
|
+
(NSApp.delegate as? AppDelegate)?.dismissPopover()
|
|
93
|
+
ScreenMapWindowController.shared.showPage(.pi)
|
|
94
|
+
}
|
|
95
|
+
headerButton(icon: "arrow.up.left.and.arrow.down.right") {
|
|
96
|
+
(NSApp.delegate as? AppDelegate)?.dismissPopover()
|
|
97
|
+
MainWindow.shared.show()
|
|
98
|
+
}
|
|
99
|
+
headerButton(icon: "arrow.clockwise") { scanner.scan(); inventory.refresh() }
|
|
88
100
|
}
|
|
89
|
-
|
|
101
|
+
.padding(.horizontal, 18)
|
|
102
|
+
.padding(.top, 18)
|
|
103
|
+
.padding(.bottom, 12)
|
|
90
104
|
}
|
|
91
|
-
.padding(.horizontal, 18)
|
|
92
|
-
.padding(.top, 14)
|
|
93
|
-
.padding(.bottom, 10)
|
|
94
105
|
|
|
95
106
|
// Layer switcher
|
|
96
107
|
if let config = workspace.config, let layers = config.layers, layers.count > 1 {
|
|
@@ -129,6 +140,11 @@ struct MainView: View {
|
|
|
129
140
|
permissionBanner
|
|
130
141
|
}
|
|
131
142
|
|
|
143
|
+
// tmux not-found banner
|
|
144
|
+
if !tmuxModel.isAvailable && !tmuxBannerDismissed {
|
|
145
|
+
tmuxBanner
|
|
146
|
+
}
|
|
147
|
+
|
|
132
148
|
Rectangle()
|
|
133
149
|
.fill(Palette.border)
|
|
134
150
|
.frame(height: 0.5)
|
|
@@ -195,6 +211,13 @@ struct MainView: View {
|
|
|
195
211
|
|
|
196
212
|
// Actions footer
|
|
197
213
|
actionsSection
|
|
214
|
+
|
|
215
|
+
Rectangle()
|
|
216
|
+
.fill(Palette.border)
|
|
217
|
+
.frame(height: 0.5)
|
|
218
|
+
|
|
219
|
+
// Bottom bar
|
|
220
|
+
bottomBar
|
|
198
221
|
}
|
|
199
222
|
}
|
|
200
223
|
|
|
@@ -278,38 +301,54 @@ struct MainView: View {
|
|
|
278
301
|
ActionRow(shortcut: "6", label: "Omni Search", hotkey: hotkeyLabel(.omniSearch), icon: "magnifyingglass", accentColor: Palette.running) {
|
|
279
302
|
OmniSearchWindow.shared.toggle()
|
|
280
303
|
}
|
|
304
|
+
ActionRow(shortcut: "7", label: "Voice Command", hotkey: hotkeyLabel(.voiceCommand), icon: "mic", accentColor: AudioLayer.shared.isListening ? Palette.running : Palette.textDim) {
|
|
305
|
+
let audio = AudioLayer.shared
|
|
306
|
+
if audio.isListening { audio.stopVoiceCommand() } else { audio.startVoiceCommand() }
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
.padding(.vertical, 4)
|
|
310
|
+
.background(Palette.surface.opacity(0.4))
|
|
311
|
+
}
|
|
281
312
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
.padding(.horizontal, 10)
|
|
286
|
-
|
|
287
|
-
ActionRow(shortcut: "S", label: "Settings", icon: "gearshape") {
|
|
313
|
+
private var bottomBar: some View {
|
|
314
|
+
HStack(spacing: 16) {
|
|
315
|
+
bottomBarButton(icon: "gearshape", label: "Settings") {
|
|
288
316
|
SettingsWindowController.shared.show()
|
|
289
317
|
}
|
|
290
|
-
|
|
291
|
-
|
|
318
|
+
|
|
319
|
+
HStack(spacing: 4) {
|
|
320
|
+
bottomBarButton(icon: "stethoscope", label: "Diagnostics") {
|
|
292
321
|
DiagnosticWindow.shared.toggle()
|
|
293
322
|
}
|
|
294
323
|
if !permChecker.allGranted {
|
|
295
324
|
Circle()
|
|
296
325
|
.fill(Palette.detach)
|
|
297
|
-
.frame(width:
|
|
298
|
-
.padding(.trailing, 14)
|
|
326
|
+
.frame(width: 5, height: 5)
|
|
299
327
|
}
|
|
300
328
|
}
|
|
301
329
|
|
|
302
|
-
|
|
303
|
-
.fill(Palette.border)
|
|
304
|
-
.frame(height: 0.5)
|
|
305
|
-
.padding(.horizontal, 10)
|
|
330
|
+
Spacer()
|
|
306
331
|
|
|
307
|
-
|
|
332
|
+
bottomBarButton(icon: "power", label: "Quit", color: Palette.kill) {
|
|
308
333
|
NSApp.terminate(nil)
|
|
309
334
|
}
|
|
310
335
|
}
|
|
311
|
-
.padding(.
|
|
312
|
-
.
|
|
336
|
+
.padding(.horizontal, 16)
|
|
337
|
+
.padding(.vertical, 8)
|
|
338
|
+
.background(Palette.bg)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private func bottomBarButton(icon: String, label: String, color: Color = Palette.textMuted, action: @escaping () -> Void) -> some View {
|
|
342
|
+
Button(action: action) {
|
|
343
|
+
HStack(spacing: 4) {
|
|
344
|
+
Image(systemName: icon)
|
|
345
|
+
.font(.system(size: 10, weight: .medium))
|
|
346
|
+
Text(label)
|
|
347
|
+
.font(Typo.mono(9))
|
|
348
|
+
}
|
|
349
|
+
.foregroundColor(color)
|
|
350
|
+
}
|
|
351
|
+
.buttonStyle(.plain)
|
|
313
352
|
}
|
|
314
353
|
|
|
315
354
|
private func hotkeyLabel(_ action: HotkeyAction) -> String? {
|
|
@@ -381,6 +420,70 @@ struct MainView: View {
|
|
|
381
420
|
.padding(.bottom, 10)
|
|
382
421
|
}
|
|
383
422
|
|
|
423
|
+
// MARK: - tmux banner
|
|
424
|
+
|
|
425
|
+
private var tmuxBanner: some View {
|
|
426
|
+
VStack(alignment: .leading, spacing: 6) {
|
|
427
|
+
HStack {
|
|
428
|
+
Image(systemName: "terminal")
|
|
429
|
+
.font(.system(size: 10))
|
|
430
|
+
.foregroundColor(Palette.detach)
|
|
431
|
+
Text("TMUX NOT FOUND")
|
|
432
|
+
.font(Typo.monoBold(10))
|
|
433
|
+
.foregroundColor(Palette.detach)
|
|
434
|
+
Spacer()
|
|
435
|
+
Button { tmuxBannerDismissed = true } label: {
|
|
436
|
+
Image(systemName: "xmark")
|
|
437
|
+
.font(.system(size: 8, weight: .bold))
|
|
438
|
+
.foregroundColor(Palette.textMuted)
|
|
439
|
+
}
|
|
440
|
+
.buttonStyle(.plain)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
Text("Session management requires tmux. Install it with Homebrew:")
|
|
444
|
+
.font(Typo.mono(10))
|
|
445
|
+
.foregroundColor(Palette.text)
|
|
446
|
+
|
|
447
|
+
Button {
|
|
448
|
+
let task = Process()
|
|
449
|
+
task.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
|
450
|
+
task.arguments = ["-lc", "brew install tmux"]
|
|
451
|
+
task.standardOutput = FileHandle.nullDevice
|
|
452
|
+
task.standardError = FileHandle.nullDevice
|
|
453
|
+
try? task.run()
|
|
454
|
+
} label: {
|
|
455
|
+
HStack(spacing: 6) {
|
|
456
|
+
Image(systemName: "arrow.down.circle")
|
|
457
|
+
.font(.system(size: 10))
|
|
458
|
+
Text("brew install tmux")
|
|
459
|
+
.font(Typo.monoBold(10))
|
|
460
|
+
}
|
|
461
|
+
.padding(.vertical, 4)
|
|
462
|
+
.padding(.horizontal, 8)
|
|
463
|
+
.background(
|
|
464
|
+
RoundedRectangle(cornerRadius: 4)
|
|
465
|
+
.fill(Palette.detach.opacity(0.06))
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
.buttonStyle(.plain)
|
|
469
|
+
|
|
470
|
+
Text("Window tiling, search, and OCR work without tmux.")
|
|
471
|
+
.font(Typo.mono(9))
|
|
472
|
+
.foregroundColor(Palette.textMuted)
|
|
473
|
+
}
|
|
474
|
+
.padding(12)
|
|
475
|
+
.background(
|
|
476
|
+
RoundedRectangle(cornerRadius: 5)
|
|
477
|
+
.fill(Palette.detach.opacity(0.08))
|
|
478
|
+
.overlay(
|
|
479
|
+
RoundedRectangle(cornerRadius: 5)
|
|
480
|
+
.strokeBorder(Palette.detach.opacity(0.20), lineWidth: 0.5)
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
.padding(.horizontal, 14)
|
|
484
|
+
.padding(.bottom, 10)
|
|
485
|
+
}
|
|
486
|
+
|
|
384
487
|
private func permissionRow(_ name: String, granted: Bool, open: @escaping () -> Void) -> some View {
|
|
385
488
|
Button(action: { if !granted { open() } }) {
|
|
386
489
|
HStack(spacing: 6) {
|