@lattices/cli 0.4.14 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +5 -7
  2. package/apps/mac/Info.plist +2 -2
  3. package/apps/mac/Lattices.app/Contents/Info.plist +4 -12
  4. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/bin/lattices-app.ts +110 -17
  6. package/bin/lattices-build +125 -0
  7. package/bin/lattices-dev +89 -16
  8. package/bin/lattices.ts +977 -16
  9. package/docs/agents.md +81 -4
  10. package/docs/ai-chat-ux-review.md +416 -0
  11. package/docs/api.md +135 -3
  12. package/docs/app.md +30 -8
  13. package/docs/config.md +4 -0
  14. package/docs/mouse-gestures.md +60 -1
  15. package/docs/proposals/LAT-004-interactive-overlay-actors.md +1 -1
  16. package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
  17. package/docs/proposals/LAT-006-mira-in-lattices.md +553 -0
  18. package/docs/reference/dewey.config.ts +2 -2
  19. package/docs/release.md +171 -0
  20. package/docs/repo-structure.md +4 -5
  21. package/docs/voice.md +11 -27
  22. package/package.json +9 -10
  23. package/apps/mac/Package.swift +0 -27
  24. package/apps/mac/Sources/AppShell/App.swift +0 -26
  25. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
  26. package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
  27. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
  28. package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
  29. package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
  30. package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
  31. package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
  32. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
  33. package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
  34. package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
  35. package/apps/mac/Sources/AppShell/MainView.swift +0 -847
  36. package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
  37. package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
  38. package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
  39. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
  40. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
  41. package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
  42. package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
  43. package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
  44. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
  45. package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
  46. package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
  47. package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
  48. package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
  49. package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
  50. package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
  51. package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
  52. package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
  53. package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
  54. package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
  55. package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
  56. package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
  57. package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
  58. package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
  59. package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
  60. package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
  61. package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
  62. package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
  63. package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
  64. package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
  65. package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
  66. package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
  67. package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
  68. package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
  69. package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
  70. package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
  71. package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
  72. package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
  73. package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
  74. package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
  75. package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
  76. package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
  77. package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
  78. package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
  79. package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
  80. package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
  81. package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
  82. package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
  83. package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
  84. package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
  85. package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
  86. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
  87. package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
  88. package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
  89. package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
  90. package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
  91. package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
  92. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
  93. package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
  94. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
  95. package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
  96. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
  97. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
  98. package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
  99. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
  100. package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
  101. package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
  102. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
  103. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
  104. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
  105. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
  106. package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
  107. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
  108. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
  109. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
  110. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
  111. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
  112. package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
  113. package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
  114. package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
  115. package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
  116. package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
  117. package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
  118. package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
  119. package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
  120. package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
  121. package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
  122. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
  123. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
  124. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
  125. package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
  126. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
  127. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
  128. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
  129. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
  130. package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
  131. package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
  132. package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
  133. package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
  134. package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
  135. package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
  136. package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
  137. package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
  138. package/apps/mac/Sources/Core/System/Capability.swift +0 -79
  139. package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
  140. package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
  141. package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
  142. package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
  143. package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
  144. package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
  145. package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
  146. package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
  147. package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
  148. package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
  149. package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
  150. package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
  151. package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
  152. package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
  153. package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
  154. package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
  155. package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
  156. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
  157. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
  158. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
  159. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
  160. package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
  161. package/apps/mac/Sources/UI/ActionRow.swift +0 -78
  162. package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
  163. package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
  164. package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
  165. package/apps/mac/Sources/UI/Theme.swift +0 -164
  166. package/apps/mac/Tests/StageDragTests.swift +0 -333
  167. package/apps/mac/Tests/StageJoinTests.swift +0 -313
  168. package/apps/mac/Tests/StageManagerTests.swift +0 -280
  169. package/apps/mac/Tests/StageTileTests.swift +0 -353
  170. package/swift/Package.swift +0 -20
  171. package/swift/Sources/DeckKit/DeckAction.swift +0 -51
  172. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
  173. package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
  174. package/swift/Sources/DeckKit/DeckHost.swift +0 -7
  175. package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
  176. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
  177. package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
  178. package/swift/Sources/DeckKit/DeckValue.swift +0 -93
  179. package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
  180. package/swift/Tests/DeckKitTests/DeckKitTests.swift +0 -286
@@ -1,849 +0,0 @@
1
- import SwiftUI
2
-
3
- // MARK: - HUDLeftBar
4
-
5
- struct HUDLeftBar: View {
6
- @ObservedObject var state: HUDState
7
- @ObservedObject private var scanner = ProjectScanner.shared
8
- @ObservedObject private var desktop = DesktopModel.shared
9
- @ObservedObject private var workspace = WorkspaceManager.shared
10
- @FocusState private var searchFieldFocused: Bool
11
- @State private var resizeStartWidth: CGFloat?
12
- @State private var isSearchHovered: Bool = false
13
- @State private var hoveredSectionKey: Int?
14
- @State private var hoveredItemID: String?
15
- @State private var hoveredLayerID: String?
16
- @State private var previewClearWorkItem: DispatchWorkItem?
17
- var onDismiss: () -> Void
18
-
19
- // Section definitions: (number key, title, icon, items builder)
20
- private struct SectionDef {
21
- let key: Int // number-key jump
22
- let title: String
23
- let icon: String
24
- let items: [HUDItem]
25
- }
26
-
27
- private var sections: [SectionDef] {
28
- [
29
- SectionDef(key: 1, title: "Projects", icon: "folder.fill", items: filteredProjects.map { .project($0) }),
30
- SectionDef(key: 2, title: "Windows", icon: "macwindow", items: filteredWindows.map { .window($0) }),
31
- ]
32
- }
33
-
34
- private var visibleItems: [HUDItem] {
35
- sections
36
- .filter { state.isSectionExpanded($0.key) }
37
- .flatMap(\.items)
38
- }
39
-
40
- // MARK: - Filters
41
-
42
- private var filteredProjects: [Project] {
43
- let q = state.query.lowercased()
44
- if q.isEmpty { return scanner.projects }
45
- return scanner.projects.filter {
46
- $0.name.lowercased().contains(q) ||
47
- $0.paneSummary.lowercased().contains(q)
48
- }
49
- }
50
-
51
- /// All desktop windows, sorted by z-order (front-to-back)
52
- /// Filters out: Lattices itself, windows with no title, and windows whose title is just the app name
53
- private var filteredWindows: [WindowEntry] {
54
- let q = state.query.lowercased()
55
- return desktop.allWindows()
56
- .filter { $0.app != "Lattices" }
57
- .filter { !$0.title.isEmpty }
58
- .filter { $0.title != $0.app } // skip helper windows titled "Cursor", "Codex", etc.
59
- .filter { q.isEmpty || $0.title.lowercased().contains(q) || $0.app.lowercased().contains(q) }
60
- .sorted { lhs, rhs in
61
- let lhsDate = desktop.lastInteractionDate(for: lhs.wid) ?? .distantPast
62
- let rhsDate = desktop.lastInteractionDate(for: rhs.wid) ?? .distantPast
63
- if lhsDate != rhsDate {
64
- return lhsDate > rhsDate
65
- }
66
- return lhs.zIndex < rhs.zIndex
67
- }
68
- }
69
-
70
- // MARK: - Body
71
-
72
- var body: some View {
73
- VStack(spacing: 0) {
74
- searchBar
75
-
76
- Rectangle().fill(Palette.border).frame(height: 0.5)
77
-
78
- // Tile mode banner
79
- if state.tileMode {
80
- HStack(spacing: 8) {
81
- Image(systemName: "rectangle.split.2x2")
82
- .font(.system(size: 11, weight: .medium))
83
- .foregroundColor(Palette.running)
84
- Text("TILE MODE")
85
- .font(.system(size: 10, weight: .semibold))
86
- .foregroundColor(Palette.running)
87
- Spacer()
88
- Text("H/J/K/L to place · ⎋ done")
89
- .font(.system(size: 10, weight: .medium, design: .monospaced))
90
- .foregroundColor(Color.white.opacity(0.42))
91
- }
92
- .padding(.horizontal, 14)
93
- .padding(.vertical, 8)
94
- .background(Palette.running.opacity(0.08))
95
-
96
- Rectangle().fill(Palette.running.opacity(0.3)).frame(height: 0.5)
97
- }
98
-
99
- // Scrollable sections
100
- ScrollViewReader { proxy in
101
- ScrollView {
102
- VStack(alignment: .leading, spacing: 16) {
103
- // Sections
104
- ForEach(sections, id: \.key) { sec in
105
- sectionView(sec, proxy: proxy)
106
- }
107
-
108
- // Layers (at bottom, scroll to find)
109
- if let layers = workspace.config?.layers, !layers.isEmpty {
110
- layersSection(layers)
111
- }
112
- }
113
- .padding(.vertical, 10)
114
- .padding(.horizontal, 10)
115
- }
116
- .onChange(of: state.selectedIndex) { _ in
117
- let items = visibleItems
118
- if let item = items[safe: state.selectedIndex] {
119
- proxy.scrollTo(item.id, anchor: .center)
120
- }
121
- }
122
- }
123
-
124
- Rectangle().fill(Palette.border).frame(height: 0.5)
125
-
126
- // Minimap (pinned at bottom, docked mode only)
127
- if state.minimapMode == .docked {
128
- minimapDocked
129
- }
130
-
131
- Rectangle().fill(Palette.border).frame(height: 0.5)
132
-
133
- footer
134
- }
135
- .frame(maxWidth: .infinity, maxHeight: .infinity)
136
- .background(Palette.bgSidebar)
137
- .overlay(alignment: .trailing) {
138
- resizeHandle
139
- }
140
- .onAppear { syncState() }
141
- .onChange(of: state.query) { _ in
142
- state.selectedIndex = 0
143
- syncState()
144
- }
145
- .onChange(of: state.expandedSections) { _ in
146
- syncState()
147
- }
148
- .onChange(of: state.focus) { focus in
149
- let shouldFocusSearch = focus == .search
150
- guard searchFieldFocused != shouldFocusSearch else { return }
151
- DispatchQueue.main.async {
152
- searchFieldFocused = shouldFocusSearch
153
- }
154
- }
155
- .onChange(of: searchFieldFocused) { isFocused in
156
- if isFocused {
157
- state.focus = .search
158
- } else if state.focus == .search {
159
- state.focus = .list
160
- }
161
- }
162
- .onReceive(scanner.objectWillChange) { _ in
163
- DispatchQueue.main.async { syncState() }
164
- }
165
- .onReceive(desktop.objectWillChange) { _ in
166
- DispatchQueue.main.async { syncState() }
167
- }
168
- .task {
169
- DispatchQueue.main.async {
170
- searchFieldFocused = state.focus == .search
171
- }
172
- }
173
- }
174
-
175
- /// Push flat items + section offsets to state so HUDController's key handler can use them
176
- private func syncState() {
177
- state.syncAutoSectionDefaults(hasRunningProjects: scanner.projects.contains(where: \.isRunning))
178
- let secs = sections
179
- var flat = [HUDItem]()
180
- var offsets = [Int: Int]()
181
- for sec in secs {
182
- guard state.isSectionExpanded(sec.key) else { continue }
183
- if !sec.items.isEmpty {
184
- offsets[sec.key] = flat.count
185
- }
186
- flat.append(contentsOf: sec.items)
187
- }
188
- state.flatItems = flat
189
- state.sectionOffsets = offsets
190
- state.reconcileSelection(with: flat)
191
- }
192
-
193
- private var resizeHandle: some View {
194
- ZStack {
195
- Color.clear
196
- VStack(spacing: 4) {
197
- Capsule()
198
- .fill(Palette.borderLit.opacity(0.9))
199
- .frame(width: 2, height: 28)
200
- Capsule()
201
- .fill(Palette.border.opacity(0.9))
202
- .frame(width: 2, height: 18)
203
- }
204
- }
205
- .frame(width: 10)
206
- .contentShape(Rectangle())
207
- .gesture(
208
- DragGesture(minimumDistance: 0)
209
- .onChanged { value in
210
- if resizeStartWidth == nil {
211
- resizeStartWidth = state.leftSidebarWidth
212
- }
213
- let base = resizeStartWidth ?? state.leftSidebarWidth
214
- state.setLeftSidebarWidth(base + value.translation.width)
215
- }
216
- .onEnded { _ in
217
- resizeStartWidth = nil
218
- }
219
- )
220
- }
221
-
222
- // MARK: - Search bar
223
-
224
- private var searchBar: some View {
225
- HStack(spacing: 10) {
226
- Image(systemName: "magnifyingglass")
227
- .font(.system(size: 13, weight: .medium))
228
- .foregroundColor(state.focus == .search ? Palette.text : Palette.textMuted.opacity(0.85))
229
-
230
- ZStack(alignment: .leading) {
231
- if state.query.isEmpty {
232
- Text(state.focus == .search ? "Type to search..." : "/ to search")
233
- .font(.system(size: 13, weight: .regular))
234
- .foregroundColor(Palette.textMuted)
235
- .allowsHitTesting(false)
236
- }
237
- TextField("", text: $state.query)
238
- .font(.system(size: 13, weight: .regular))
239
- .foregroundColor(Palette.text)
240
- .textFieldStyle(.plain)
241
- .focused($searchFieldFocused)
242
- .onTapGesture {
243
- state.focus = .search
244
- searchFieldFocused = true
245
- }
246
- .onSubmit { activateSelected() }
247
- }
248
- .frame(maxWidth: .infinity, alignment: .leading)
249
-
250
- if !state.query.isEmpty {
251
- Button {
252
- state.query = ""
253
- state.selectedIndex = 0
254
- } label: {
255
- Image(systemName: "xmark.circle.fill")
256
- .font(.system(size: 12))
257
- .foregroundColor(Palette.textMuted)
258
- }
259
- .buttonStyle(.plain)
260
- }
261
- }
262
- .frame(maxWidth: .infinity, alignment: .leading)
263
- .padding(.horizontal, 16)
264
- .padding(.vertical, 12)
265
- .background(searchBarBackground)
266
- .contentShape(Rectangle())
267
- .onTapGesture {
268
- state.focus = .search
269
- searchFieldFocused = true
270
- }
271
- .onHover { isHovering in
272
- isSearchHovered = isHovering
273
- }
274
- }
275
-
276
- // MARK: - Section
277
-
278
- @ViewBuilder
279
- private func sectionView(_ sec: SectionDef, proxy: ScrollViewProxy) -> some View {
280
- if !sec.items.isEmpty {
281
- let isExpanded = state.isSectionExpanded(sec.key)
282
- let isHovered = hoveredSectionKey == sec.key
283
- VStack(alignment: .leading, spacing: 4) {
284
- Button {
285
- state.toggleSection(sec.key)
286
- } label: {
287
- HStack(spacing: 8) {
288
- Image(systemName: isExpanded ? "chevron.down" : "chevron.right")
289
- .font(.system(size: 8, weight: .bold))
290
- .foregroundColor(Palette.textMuted)
291
- .frame(width: 10, alignment: .center)
292
-
293
- Image(systemName: sec.icon)
294
- .font(.system(size: 10, weight: .medium))
295
- .foregroundColor(Palette.textMuted)
296
- Text(sec.title)
297
- .font(.system(size: 11, weight: .semibold))
298
- .foregroundColor(Palette.textMuted.opacity(0.9))
299
- Text("\(sec.items.count)")
300
- .font(.system(size: 10, weight: .medium))
301
- .foregroundColor(Palette.textDim)
302
- Spacer()
303
- shortcutBadge("\(sec.key)")
304
- }
305
- .padding(.horizontal, 8)
306
- .padding(.vertical, 4)
307
- .background(
308
- RoundedRectangle(cornerRadius: 6)
309
- .fill(isHovered ? Palette.surface.opacity(0.65) : Color.clear)
310
- )
311
- .contentShape(Rectangle())
312
- }
313
- .buttonStyle(.plain)
314
- .frame(maxWidth: .infinity, alignment: .leading)
315
- .onHover { isHovering in
316
- hoveredSectionKey = isHovering ? sec.key : (hoveredSectionKey == sec.key ? nil : hoveredSectionKey)
317
- }
318
-
319
- if isExpanded {
320
- ForEach(sec.items) { item in
321
- itemRow(item)
322
- .id(item.id)
323
- }
324
- }
325
- }
326
- }
327
- }
328
-
329
- // MARK: - Item row
330
-
331
- private func itemRow(_ item: HUDItem) -> some View {
332
- let isSelected = state.selectedItem == item && state.focus == .list
333
- let isMultiSelected = state.selectedItems.contains(item.id)
334
- let isTiled = state.tileMode && {
335
- if case .window(let w) = item { return state.tiledWindows.contains(w.wid) }
336
- return false
337
- }()
338
- let subtitleText = subtitle(for: item)
339
- let isHovered = hoveredItemID == item.id
340
- let rowFill: Color = {
341
- if isTiled { return Palette.running.opacity(0.12) }
342
- if isMultiSelected { return Palette.surfaceHov.opacity(0.9) }
343
- if isSelected { return Palette.surfaceHov }
344
- if isHovered { return Palette.surface.opacity(0.92) }
345
- if state.selectedItem == item { return Palette.surface.opacity(0.8) }
346
- return Color.clear
347
- }()
348
- let rowStroke: Color = {
349
- if isTiled { return Palette.running.opacity(0.45) }
350
- if isMultiSelected { return Color.blue.opacity(0.25) }
351
- if isSelected { return Palette.borderLit }
352
- if isHovered { return Palette.border.opacity(0.9) }
353
- return Color.clear
354
- }()
355
-
356
- return Button {
357
- state.focus = .list
358
- guard let idx = visibleItems.firstIndex(of: item) else { return }
359
-
360
- let modifiers = NSEvent.modifierFlags.intersection([.shift, .command])
361
- if modifiers.contains(.shift) {
362
- state.selectRange(to: item, index: idx, in: visibleItems)
363
- } else if modifiers.contains(.command) {
364
- state.toggleSelection(item, index: idx, in: visibleItems)
365
- } else {
366
- state.selectSingle(item, index: idx)
367
- state.pinInspector(item, source: "row")
368
- }
369
- } label: {
370
- HStack(spacing: 10) {
371
- statusDot(for: item)
372
-
373
- VStack(alignment: .leading, spacing: 2) {
374
- Text(item.displayName)
375
- .font(.system(size: 13, weight: .semibold))
376
- .foregroundColor(Palette.text)
377
- .lineLimit(1)
378
-
379
- if let subtitleText {
380
- Text(subtitleText)
381
- .font(.system(size: 11, weight: .regular))
382
- .foregroundColor(Palette.textDim)
383
- .lineLimit(1)
384
- }
385
- }
386
-
387
- Spacer()
388
- }
389
- .padding(.horizontal, 10)
390
- .padding(.vertical, 8)
391
- .frame(maxWidth: .infinity, alignment: .leading)
392
- .background(
393
- RoundedRectangle(cornerRadius: 7)
394
- .fill(rowFill)
395
- .overlay(
396
- RoundedRectangle(cornerRadius: 7)
397
- .strokeBorder(rowStroke, lineWidth: 0.5)
398
- )
399
- )
400
- .contentShape(Rectangle())
401
- }
402
- .buttonStyle(.plain)
403
- .frame(maxWidth: .infinity, alignment: .leading)
404
- .onHover { isHovering in
405
- hoveredItemID = isHovering ? item.id : (hoveredItemID == item.id ? nil : hoveredItemID)
406
- if isHovering {
407
- previewClearWorkItem?.cancel()
408
- state.hoveredPreviewItem = item
409
- state.hoverPreviewAnchorScreenY = NSEvent.mouseLocation.y
410
- prefetchPreview(for: item)
411
- } else if state.hoveredPreviewItem == item {
412
- let hoveredItemID = item.id
413
- let clearWorkItem = DispatchWorkItem {
414
- guard self.state.hoveredPreviewItem?.id == hoveredItemID,
415
- !self.state.previewInteractionActive else { return }
416
- self.state.hoveredPreviewItem = nil
417
- self.state.hoverPreviewAnchorScreenY = nil
418
- }
419
- previewClearWorkItem = clearWorkItem
420
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.12, execute: clearWorkItem)
421
- }
422
- }
423
- }
424
-
425
- private func statusDot(for item: HUDItem) -> some View {
426
- Circle()
427
- .fill(dotColor(for: item))
428
- .frame(width: 7, height: 7)
429
- }
430
-
431
- private func dotColor(for item: HUDItem) -> Color {
432
- switch item {
433
- case .project(let p): return p.isRunning ? Palette.running : Palette.textMuted.opacity(0.3)
434
- case .window: return Palette.textDim
435
- }
436
- }
437
-
438
- private func subtitle(for item: HUDItem) -> String? {
439
- switch item {
440
- case .project(let p):
441
- return p.paneSummary.isEmpty ? nil : p.paneSummary
442
- case .window(let w):
443
- return w.app
444
- }
445
- }
446
-
447
- private func prefetchPreview(for item: HUDItem) {
448
- switch item {
449
- case .window(let window):
450
- WindowPreviewStore.shared.load(window: window)
451
- case .project(let project):
452
- guard project.isRunning,
453
- let window = desktop.windowForSession(project.sessionName) else { return }
454
- WindowPreviewStore.shared.load(window: window)
455
- }
456
- }
457
-
458
- // MARK: - Layers section (at bottom)
459
-
460
- private func layersSection(_ layers: [Layer]) -> some View {
461
- VStack(alignment: .leading, spacing: 4) {
462
- HStack(spacing: 6) {
463
- Image(systemName: "square.stack.3d.up")
464
- .font(.system(size: 10, weight: .medium))
465
- .foregroundColor(Palette.textMuted)
466
- Text("Layers")
467
- .font(.system(size: 11, weight: .semibold))
468
- .foregroundColor(Palette.textMuted.opacity(0.9))
469
- Text("\(layers.count)")
470
- .font(.system(size: 10, weight: .medium))
471
- .foregroundColor(Palette.textDim)
472
- Spacer()
473
- shortcutBadge("[ ]")
474
- }
475
- .padding(.horizontal, 8)
476
- .padding(.vertical, 4)
477
-
478
- ForEach(Array(layers.enumerated()), id: \.element.id) { idx, layer in
479
- let isActive = idx == workspace.activeLayerIndex
480
- let counts = workspace.layerRunningCount(index: idx)
481
- let isHovered = hoveredLayerID == layer.id
482
-
483
- Button {
484
- workspace.focusLayer(index: idx)
485
- } label: {
486
- HStack(spacing: 10) {
487
- Circle()
488
- .fill(isActive ? Palette.running : Palette.textMuted.opacity(0.2))
489
- .frame(width: 7, height: 7)
490
-
491
- VStack(alignment: .leading, spacing: 2) {
492
- Text(layer.label)
493
- .font(.system(size: 13, weight: .semibold))
494
- .foregroundColor(isActive ? Palette.text : Palette.textMuted)
495
- .lineLimit(1)
496
-
497
- Text("\(counts.running)/\(counts.total) projects")
498
- .font(.system(size: 11, weight: .regular))
499
- .foregroundColor(Palette.textDim)
500
- }
501
-
502
- Spacer()
503
-
504
- Text("\(idx + 1)")
505
- .font(.system(size: 10, weight: .semibold, design: .monospaced))
506
- .foregroundColor(isActive ? Palette.text : Palette.textDim)
507
- .frame(width: 18, height: 18)
508
- .background(
509
- RoundedRectangle(cornerRadius: 4)
510
- .fill(isActive ? Palette.running.opacity(0.15) : Palette.surface)
511
- .overlay(
512
- RoundedRectangle(cornerRadius: 4)
513
- .strokeBorder(isActive ? Palette.running.opacity(0.3) : Palette.border, lineWidth: 0.5)
514
- )
515
- )
516
- }
517
- .padding(.horizontal, 10)
518
- .padding(.vertical, 8)
519
- .frame(maxWidth: .infinity, alignment: .leading)
520
- .background(
521
- RoundedRectangle(cornerRadius: 7)
522
- .fill(isActive ? Palette.running.opacity(0.06) : (isHovered ? Palette.surface.opacity(0.75) : Color.clear))
523
- .overlay(
524
- RoundedRectangle(cornerRadius: 7)
525
- .strokeBorder(isActive ? Palette.running.opacity(0.2) : (isHovered ? Palette.border : Color.clear), lineWidth: 0.5)
526
- )
527
- )
528
- .contentShape(Rectangle())
529
- }
530
- .buttonStyle(.plain)
531
- .frame(maxWidth: .infinity, alignment: .leading)
532
- .onHover { isHovering in
533
- hoveredLayerID = isHovering ? layer.id : (hoveredLayerID == layer.id ? nil : hoveredLayerID)
534
- }
535
- }
536
- }
537
- }
538
-
539
- // MARK: - Docked minimap
540
-
541
- private var minimapDocked: some View {
542
- let screens = NSScreen.screens
543
- let screen: NSScreen? = screens.isEmpty ? nil : screens.first
544
- let mapWidth: CGFloat = 300 // full sidebar width minus padding
545
- let mapHeight: CGFloat = 140
546
-
547
- return VStack(alignment: .leading, spacing: 0) {
548
- // Header
549
- HStack(spacing: 0) {
550
- Image(systemName: "map")
551
- .font(.system(size: 9, weight: .medium))
552
- .foregroundColor(Palette.textMuted)
553
- Text("Map")
554
- .font(.system(size: 10, weight: .semibold))
555
- .foregroundColor(Palette.textMuted.opacity(0.9))
556
- .padding(.leading, 4)
557
-
558
- Spacer()
559
-
560
- // Expand out to canvas
561
- Button {
562
- state.minimapMode = .expanded
563
- } label: {
564
- Image(systemName: "arrow.up.left.and.arrow.down.right")
565
- .font(.system(size: 8, weight: .bold))
566
- .foregroundColor(Palette.textMuted)
567
- .frame(width: 18, height: 18)
568
- .background(
569
- RoundedRectangle(cornerRadius: 3)
570
- .fill(Palette.surface)
571
- )
572
- }
573
- .buttonStyle(.plain)
574
- .help("Expand map (M)")
575
-
576
- // Hide
577
- Button {
578
- state.minimapMode = .hidden
579
- } label: {
580
- Image(systemName: "chevron.down")
581
- .font(.system(size: 8, weight: .bold))
582
- .foregroundColor(Palette.textMuted)
583
- .frame(width: 18, height: 18)
584
- .background(
585
- RoundedRectangle(cornerRadius: 3)
586
- .fill(Palette.surface)
587
- )
588
- }
589
- .buttonStyle(.plain)
590
- .help("Hide map (M)")
591
- }
592
- .padding(.horizontal, 10)
593
- .padding(.vertical, 6)
594
-
595
- // Map canvas
596
- if let screen {
597
- let sw = screen.frame.width
598
- let sh = screen.frame.height
599
- let scaleX = mapWidth / sw
600
- let scaleY = mapHeight / sh
601
- let scale = min(scaleX, scaleY)
602
- let drawW = sw * scale
603
- let drawH = sh * scale
604
- let offsetX = (mapWidth - drawW) / 2
605
- let offsetY = (mapHeight - drawH) / 2
606
- let origin = screenCGOrigin(screen)
607
- let wins = windowsOnScreen(0)
608
-
609
- ZStack(alignment: .topLeading) {
610
- // Screen background
611
- RoundedRectangle(cornerRadius: 4)
612
- .fill(Palette.surface.opacity(0.4))
613
- .frame(width: drawW, height: drawH)
614
- .offset(x: offsetX, y: offsetY)
615
-
616
- // Windows (back-to-front)
617
- ForEach(wins.reversed()) { win in
618
- let rx = (CGFloat(win.frame.x) - origin.x) * scale + offsetX
619
- let ry = (CGFloat(win.frame.y) - origin.y) * scale + offsetY
620
- let rw = CGFloat(win.frame.w) * scale
621
- let rh = CGFloat(win.frame.h) * scale
622
- let isSelected = state.selectedItem == .window(win)
623
-
624
- RoundedRectangle(cornerRadius: 1.5)
625
- .fill(appColor(win.app).opacity(isSelected ? 0.5 : 0.15))
626
- .overlay(
627
- RoundedRectangle(cornerRadius: 1.5)
628
- .strokeBorder(
629
- isSelected ? Palette.running : appColor(win.app).opacity(0.35),
630
- lineWidth: isSelected ? 1.5 : 0.5
631
- )
632
- )
633
- .overlay(
634
- Group {
635
- if rw > 24 && rh > 14 {
636
- Text(String(win.app.prefix(1)))
637
- .font(.system(size: max(6, min(9, rh * 0.35)), weight: .semibold, design: .monospaced))
638
- .foregroundColor(appColor(win.app).opacity(isSelected ? 1.0 : 0.5))
639
- }
640
- }
641
- )
642
- .frame(width: max(rw, 3), height: max(rh, 2))
643
- .offset(x: rx, y: ry)
644
- .onTapGesture {
645
- state.focus = .list
646
- if let flatIdx = visibleItems.firstIndex(of: .window(win)) {
647
- state.selectSingle(.window(win), index: flatIdx)
648
- state.pinnedItem = .window(win)
649
- state.hoveredPreviewItem = nil
650
- }
651
- }
652
- }
653
- }
654
- .frame(width: mapWidth, height: mapHeight)
655
- .clipShape(RoundedRectangle(cornerRadius: 4))
656
- .padding(.horizontal, 10)
657
- .padding(.bottom, 8)
658
- }
659
- }
660
- }
661
-
662
- // MARK: - Minimap helpers
663
-
664
- private func screenCGOrigin(_ screen: NSScreen) -> (x: CGFloat, y: CGFloat) {
665
- let primaryH = NSScreen.screens.first?.frame.height ?? 900
666
- return (screen.frame.origin.x, primaryH - screen.frame.origin.y - screen.frame.height)
667
- }
668
-
669
- private func windowsOnScreen(_ screenIdx: Int) -> [WindowEntry] {
670
- let screens = NSScreen.screens
671
- guard screenIdx < screens.count else { return [] }
672
- let screen = screens[screenIdx]
673
- let origin = screenCGOrigin(screen)
674
- let sw = Double(screen.frame.width)
675
- let sh = Double(screen.frame.height)
676
-
677
- return desktop.allWindows().filter { win in
678
- let cx = win.frame.x + win.frame.w / 2
679
- let cy = win.frame.y + win.frame.h / 2
680
- return cx >= Double(origin.x) && cx < Double(origin.x) + sw &&
681
- cy >= Double(origin.y) && cy < Double(origin.y) + sh &&
682
- win.app != "Lattices"
683
- }
684
- }
685
-
686
- private func appColor(_ app: String) -> Color {
687
- if ["iTerm2", "Terminal", "WezTerm", "Alacritty", "kitty"].contains(app) {
688
- return Palette.running
689
- }
690
- if ["Google Chrome", "Safari", "Arc", "Firefox", "Brave Browser"].contains(app) {
691
- return Color.blue
692
- }
693
- if ["Xcode", "Visual Studio Code", "Cursor", "Zed"].contains(app) {
694
- return Color.purple
695
- }
696
- if app.localizedCaseInsensitiveContains("Claude") || app.localizedCaseInsensitiveContains("Codex") {
697
- return Color.orange
698
- }
699
- return Palette.textMuted
700
- }
701
-
702
- // MARK: - Footer
703
-
704
- private var footer: some View {
705
- let selectedIDs = state.effectiveSelectionIDs
706
- let selectionCount = state.multiSelectionCount
707
- let selectedProjects = visibleItems.compactMap { item -> Project? in
708
- guard selectedIDs.contains(item.id), case .project(let project) = item else { return nil }
709
- return project
710
- }
711
- let selectedWindows = visibleItems.compactMap { item -> WindowEntry? in
712
- guard selectedIDs.contains(item.id), case .window(let window) = item else { return nil }
713
- return window
714
- }
715
-
716
- return HStack(spacing: 10) {
717
- if selectionCount > 1 {
718
- Text("\(selectionCount) selected")
719
- .font(.system(size: 10, weight: .semibold))
720
- .foregroundColor(Color.white.opacity(0.86))
721
- if !selectedWindows.isEmpty || !selectedProjects.isEmpty {
722
- keyBadge("T", label: "Tile")
723
- }
724
- if !selectedProjects.isEmpty {
725
- keyBadge("D", label: "Detach")
726
- } else if selectedWindows.count > 1 {
727
- keyBadge("D", label: "Distrib")
728
- }
729
- }
730
- keyBadge("⇧↕", label: "Range")
731
- keyBadge("⌘", label: "Multi")
732
- keyBadge("⇥", label: "Focus")
733
- keyBadge("↵", label: "Go")
734
- keyBadge("[ ]", label: "Layer")
735
-
736
- Spacer()
737
-
738
- if state.minimapMode != .docked {
739
- Button {
740
- state.minimapMode = .docked
741
- } label: {
742
- HStack(spacing: 3) {
743
- Image(systemName: "map")
744
- .font(.system(size: 8))
745
- Text("M")
746
- .font(.system(size: 9, weight: .semibold, design: .monospaced))
747
- }
748
- .foregroundColor(state.minimapMode == .expanded ? Palette.running : Palette.textMuted)
749
- .padding(.horizontal, 6)
750
- .padding(.vertical, 3)
751
- .background(
752
- RoundedRectangle(cornerRadius: 3)
753
- .fill(Palette.surface)
754
- .overlay(
755
- RoundedRectangle(cornerRadius: 3)
756
- .strokeBorder(state.minimapMode == .expanded ? Palette.running.opacity(0.3) : Palette.border, lineWidth: 0.5)
757
- )
758
- )
759
- }
760
- .buttonStyle(.plain)
761
- .help("Dock map (M)")
762
- }
763
- }
764
- .padding(.horizontal, 14)
765
- .padding(.vertical, 7)
766
- }
767
-
768
- private func keyBadge(_ key: String, label: String) -> some View {
769
- HStack(spacing: 3) {
770
- Text(key)
771
- .font(.system(size: 9, weight: .semibold, design: .monospaced))
772
- .foregroundColor(Palette.text)
773
- .padding(.horizontal, 4)
774
- .padding(.vertical, 2)
775
- .background(
776
- RoundedRectangle(cornerRadius: 3)
777
- .fill(Palette.surface)
778
- .overlay(
779
- RoundedRectangle(cornerRadius: 3)
780
- .strokeBorder(Palette.border, lineWidth: 0.5)
781
- )
782
- )
783
- Text(label)
784
- .font(.system(size: 10, weight: .medium))
785
- .foregroundColor(Palette.textMuted)
786
- }
787
- }
788
-
789
- private func shortcutBadge(_ key: String) -> some View {
790
- Text(key)
791
- .font(.system(size: 9, weight: .semibold, design: .monospaced))
792
- .foregroundColor(Palette.textMuted)
793
- .padding(.horizontal, 6)
794
- .padding(.vertical, 3)
795
- .background(
796
- RoundedRectangle(cornerRadius: 4)
797
- .fill(Palette.surface)
798
- .overlay(
799
- RoundedRectangle(cornerRadius: 4)
800
- .strokeBorder(Palette.border, lineWidth: 0.5)
801
- )
802
- )
803
- }
804
-
805
- private var searchBarBackground: some View {
806
- RoundedRectangle(cornerRadius: 8)
807
- .fill(
808
- state.focus == .search
809
- ? Palette.surface.opacity(0.6)
810
- : (isSearchHovered ? Palette.surface.opacity(0.3) : Color.clear)
811
- )
812
- .overlay(
813
- RoundedRectangle(cornerRadius: 8)
814
- .strokeBorder(
815
- state.focus == .search
816
- ? Palette.borderLit
817
- : (isSearchHovered ? Palette.border.opacity(0.85) : Color.clear),
818
- lineWidth: 0.5
819
- )
820
- )
821
- }
822
-
823
- // MARK: - Actions
824
-
825
- private func activateSelected() {
826
- guard let item = state.selectedItem else { return }
827
- activate(item)
828
- }
829
-
830
- private func activate(_ item: HUDItem) {
831
- switch item {
832
- case .project(let p):
833
- SessionManager.launch(project: p)
834
- HandsOffSession.shared.playCachedCue(p.isRunning ? "Focused." : "Done.")
835
- case .window(let w):
836
- _ = WindowTiler.focusWindow(wid: w.wid, pid: w.pid)
837
- HandsOffSession.shared.playCachedCue("Focused.")
838
- }
839
- onDismiss()
840
- }
841
- }
842
-
843
- // MARK: - Safe array subscript
844
-
845
- private extension Array {
846
- subscript(safe index: Int) -> Element? {
847
- indices.contains(index) ? self[index] : nil
848
- }
849
- }