@lattices/cli 0.4.14 → 0.6.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 (181) hide show
  1. package/README.md +5 -7
  2. package/apps/mac/Info.plist +4 -4
  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/proposals/LAT-007-unified-app-shell.md +128 -0
  19. package/docs/reference/dewey.config.ts +2 -2
  20. package/docs/release.md +171 -0
  21. package/docs/repo-structure.md +5 -5
  22. package/docs/voice.md +11 -27
  23. package/package.json +11 -10
  24. package/apps/mac/Package.swift +0 -27
  25. package/apps/mac/Sources/AppShell/App.swift +0 -26
  26. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
  27. package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
  28. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
  29. package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
  30. package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
  31. package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
  32. package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
  33. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
  34. package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
  35. package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
  36. package/apps/mac/Sources/AppShell/MainView.swift +0 -847
  37. package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
  38. package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
  39. package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
  40. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
  41. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
  42. package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
  43. package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
  44. package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
  45. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
  46. package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
  47. package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
  48. package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
  49. package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
  50. package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
  51. package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
  52. package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
  53. package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
  54. package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
  55. package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
  56. package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
  57. package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
  58. package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
  59. package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
  60. package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
  61. package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
  62. package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
  63. package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
  64. package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
  65. package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
  66. package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
  67. package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
  68. package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
  69. package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
  70. package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
  71. package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
  72. package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
  73. package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
  74. package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
  75. package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
  76. package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
  77. package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
  78. package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
  79. package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
  80. package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
  81. package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
  82. package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
  83. package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
  84. package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
  85. package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
  86. package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
  87. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
  88. package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
  89. package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
  90. package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
  91. package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
  92. package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
  93. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
  94. package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
  95. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
  96. package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
  97. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
  98. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
  99. package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
  100. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
  101. package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
  102. package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
  103. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
  104. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
  105. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
  106. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
  107. package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
  108. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
  109. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
  110. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
  111. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
  112. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
  113. package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
  114. package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
  115. package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
  116. package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
  117. package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
  118. package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
  119. package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
  120. package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
  121. package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
  122. package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
  123. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
  124. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
  125. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
  126. package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
  127. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
  128. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
  129. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
  130. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
  131. package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
  132. package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
  133. package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
  134. package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
  135. package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
  136. package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
  137. package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
  138. package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
  139. package/apps/mac/Sources/Core/System/Capability.swift +0 -79
  140. package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
  141. package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
  142. package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
  143. package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
  144. package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
  145. package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
  146. package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
  147. package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
  148. package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
  149. package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
  150. package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
  151. package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
  152. package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
  153. package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
  154. package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
  155. package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
  156. package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
  157. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
  158. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
  159. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
  160. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
  161. package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
  162. package/apps/mac/Sources/UI/ActionRow.swift +0 -78
  163. package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
  164. package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
  165. package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
  166. package/apps/mac/Sources/UI/Theme.swift +0 -164
  167. package/apps/mac/Tests/StageDragTests.swift +0 -333
  168. package/apps/mac/Tests/StageJoinTests.swift +0 -313
  169. package/apps/mac/Tests/StageManagerTests.swift +0 -280
  170. package/apps/mac/Tests/StageTileTests.swift +0 -353
  171. package/swift/Package.swift +0 -20
  172. package/swift/Sources/DeckKit/DeckAction.swift +0 -51
  173. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
  174. package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
  175. package/swift/Sources/DeckKit/DeckHost.swift +0 -7
  176. package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
  177. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
  178. package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
  179. package/swift/Sources/DeckKit/DeckValue.swift +0 -93
  180. package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
  181. 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
- }