@arach/lattices 0.2.0 → 0.6.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.
Files changed (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -86
  3. package/apps/mac/Info.plist +43 -0
  4. package/apps/mac/Lattices.app/Contents/Info.plist +43 -0
  5. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  6. package/apps/mac/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  7. package/apps/mac/Lattices.app/Contents/Resources/docs/assistant-knowledge.md +130 -0
  8. package/apps/mac/Lattices.app/Contents/Resources/tap.wav +0 -0
  9. package/apps/mac/Lattices.app/Contents/_CodeSignature/CodeResources +150 -0
  10. package/apps/mac/Lattices.entitlements +21 -0
  11. package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
  12. package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
  13. package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
  14. package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
  15. package/apps/mac/Resources/tap.wav +0 -0
  16. package/assets/AppIcon.icns +0 -0
  17. package/bin/assistant-intelligence.ts +912 -0
  18. package/bin/cli/capture.ts +252 -0
  19. package/bin/cli/daemon.ts +22 -0
  20. package/bin/cli/helpers.ts +105 -0
  21. package/bin/cli/layer.ts +178 -0
  22. package/bin/cli/runs.ts +43 -0
  23. package/bin/cli/search.ts +141 -0
  24. package/bin/cli/session.ts +32 -0
  25. package/bin/client.ts +17 -0
  26. package/bin/cua.ts +26 -0
  27. package/bin/{daemon-client.js → daemon-client.ts} +49 -30
  28. package/bin/handsoff-infer.ts +96 -0
  29. package/bin/handsoff-worker.ts +531 -0
  30. package/bin/infer.ts +424 -0
  31. package/bin/keychain.ts +75 -0
  32. package/bin/lattices-app.ts +655 -0
  33. package/bin/lattices-build +125 -0
  34. package/bin/lattices-build-env.ts +77 -0
  35. package/bin/lattices-dev +362 -0
  36. package/bin/lattices.ts +3260 -0
  37. package/bin/project-twin.ts +645 -0
  38. package/docs/agent-execution-plan.md +562 -0
  39. package/docs/agent-layer-guide.md +207 -0
  40. package/docs/agents.md +233 -0
  41. package/docs/ai-chat-ux-review.md +416 -0
  42. package/docs/api.md +1041 -47
  43. package/docs/app.md +96 -13
  44. package/docs/assistant-knowledge.md +130 -0
  45. package/docs/companion-deck.md +209 -0
  46. package/docs/component-extraction-roadmap.md +392 -0
  47. package/docs/concepts.md +13 -12
  48. package/docs/config.md +83 -10
  49. package/docs/gesture-customization-proposal.md +520 -0
  50. package/docs/handsoff-test-scenarios.md +84 -0
  51. package/docs/hyperspace-grid-snappiness.md +210 -0
  52. package/docs/layers.md +176 -28
  53. package/docs/mouse-gestures.md +244 -0
  54. package/docs/ocr.md +21 -9
  55. package/docs/overview.md +42 -23
  56. package/docs/presentation-execution-review.md +491 -0
  57. package/docs/prompts/hands-off-system.md +382 -0
  58. package/docs/prompts/hands-off-turn.md +30 -0
  59. package/docs/prompts/voice-advisor.md +31 -0
  60. package/docs/prompts/voice-fallback.md +23 -0
  61. package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
  62. package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
  63. package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
  64. package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
  65. package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
  66. package/docs/proposals/LAT-006-followup-gaps.md +103 -0
  67. package/docs/proposals/LAT-006-runs-and-capture-in-lattices.md +566 -0
  68. package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
  69. package/docs/quickstart.md +8 -12
  70. package/docs/reference/dewey.config.ts +74 -0
  71. package/docs/reference/install-agent.md +79 -0
  72. package/docs/release.md +172 -0
  73. package/docs/repo-structure.md +100 -0
  74. package/docs/terminal-kit.md +87 -0
  75. package/docs/tiling-reference.md +224 -0
  76. package/docs/twins.md +138 -0
  77. package/docs/voice-command-protocol.md +278 -0
  78. package/docs/voice-error-model.md +73 -0
  79. package/docs/voice.md +221 -0
  80. package/package.json +69 -16
  81. package/packages/npm/sdk/cua.d.mts +1 -0
  82. package/packages/npm/sdk/cua.d.ts +188 -0
  83. package/packages/npm/sdk/cua.mjs +376 -0
  84. package/app/Lattices.app/Contents/Info.plist +0 -24
  85. package/app/Package.swift +0 -13
  86. package/app/Sources/ActionRow.swift +0 -61
  87. package/app/Sources/App.swift +0 -10
  88. package/app/Sources/AppDelegate.swift +0 -234
  89. package/app/Sources/AppShellView.swift +0 -62
  90. package/app/Sources/AppTypeClassifier.swift +0 -70
  91. package/app/Sources/AppWindowShell.swift +0 -63
  92. package/app/Sources/CheatSheetHUD.swift +0 -332
  93. package/app/Sources/CommandModeState.swift +0 -1362
  94. package/app/Sources/CommandModeView.swift +0 -1405
  95. package/app/Sources/CommandModeWindow.swift +0 -192
  96. package/app/Sources/CommandPaletteView.swift +0 -307
  97. package/app/Sources/CommandPaletteWindow.swift +0 -134
  98. package/app/Sources/DaemonProtocol.swift +0 -101
  99. package/app/Sources/DaemonServer.swift +0 -414
  100. package/app/Sources/DesktopModel.swift +0 -121
  101. package/app/Sources/DesktopModelTypes.swift +0 -71
  102. package/app/Sources/DiagnosticLog.swift +0 -271
  103. package/app/Sources/EventBus.swift +0 -30
  104. package/app/Sources/HotkeyManager.swift +0 -250
  105. package/app/Sources/HotkeyStore.swift +0 -338
  106. package/app/Sources/InventoryManager.swift +0 -35
  107. package/app/Sources/InventoryPath.swift +0 -43
  108. package/app/Sources/KeyRecorderView.swift +0 -210
  109. package/app/Sources/LatticesApi.swift +0 -1125
  110. package/app/Sources/MainView.swift +0 -467
  111. package/app/Sources/MainWindow.swift +0 -83
  112. package/app/Sources/OcrModel.swift +0 -309
  113. package/app/Sources/OcrStore.swift +0 -295
  114. package/app/Sources/OmniSearchState.swift +0 -283
  115. package/app/Sources/OmniSearchView.swift +0 -288
  116. package/app/Sources/OmniSearchWindow.swift +0 -105
  117. package/app/Sources/OrphanRow.swift +0 -129
  118. package/app/Sources/PaletteCommand.swift +0 -419
  119. package/app/Sources/PermissionChecker.swift +0 -125
  120. package/app/Sources/Preferences.swift +0 -92
  121. package/app/Sources/ProcessModel.swift +0 -199
  122. package/app/Sources/ProcessQuery.swift +0 -151
  123. package/app/Sources/Project.swift +0 -28
  124. package/app/Sources/ProjectRow.swift +0 -368
  125. package/app/Sources/ProjectScanner.swift +0 -121
  126. package/app/Sources/ScreenMapState.swift +0 -2387
  127. package/app/Sources/ScreenMapView.swift +0 -2820
  128. package/app/Sources/ScreenMapWindowController.swift +0 -89
  129. package/app/Sources/SessionManager.swift +0 -72
  130. package/app/Sources/SettingsView.swift +0 -1053
  131. package/app/Sources/SettingsWindow.swift +0 -20
  132. package/app/Sources/TabGroupRow.swift +0 -178
  133. package/app/Sources/Terminal.swift +0 -259
  134. package/app/Sources/TerminalQuery.swift +0 -156
  135. package/app/Sources/TerminalSynthesizer.swift +0 -200
  136. package/app/Sources/Theme.swift +0 -163
  137. package/app/Sources/TilePickerView.swift +0 -209
  138. package/app/Sources/TmuxModel.swift +0 -53
  139. package/app/Sources/TmuxQuery.swift +0 -81
  140. package/app/Sources/WindowTiler.swift +0 -1755
  141. package/app/Sources/WorkspaceManager.swift +0 -434
  142. package/bin/lattices-app.js +0 -221
  143. package/bin/lattices.js +0 -1418
@@ -1,467 +0,0 @@
1
- import SwiftUI
2
-
3
- struct MainView: View {
4
- @ObservedObject var scanner: ProjectScanner
5
- @StateObject private var prefs = Preferences.shared
6
- @StateObject private var permChecker = PermissionChecker.shared
7
- @ObservedObject private var workspace = WorkspaceManager.shared
8
- @StateObject private var inventory = InventoryManager.shared
9
- @State private var searchText = ""
10
- @State private var hasCheckedSetup = false
11
- @State private var bannerDismissed = false
12
- @State private var orphanSectionCollapsed = true
13
- private var filtered: [Project] {
14
- if searchText.isEmpty { return scanner.projects }
15
- return scanner.projects.filter {
16
- $0.name.localizedCaseInsensitiveContains(searchText)
17
- }
18
- }
19
-
20
- private var filteredOrphans: [TmuxSession] {
21
- if searchText.isEmpty { return inventory.orphans }
22
- return inventory.orphans.filter {
23
- $0.name.localizedCaseInsensitiveContains(searchText)
24
- }
25
- }
26
-
27
- private var needsSetup: Bool { prefs.scanRoot.isEmpty }
28
- private var runningCount: Int { scanner.projects.filter(\.isRunning).count }
29
-
30
- var body: some View {
31
- VStack(spacing: 0) {
32
- mainContent
33
- }
34
- .frame(minWidth: 380, idealWidth: 380, maxWidth: 600, minHeight: 460, idealHeight: 460, maxHeight: .infinity)
35
- .background(PanelBackground())
36
- .preferredColorScheme(.dark)
37
- .onAppear {
38
- if needsSetup && !hasCheckedSetup {
39
- hasCheckedSetup = true
40
- SettingsWindowController.shared.show()
41
- }
42
- scanner.updateRoot(prefs.scanRoot)
43
- scanner.scan()
44
- inventory.refresh()
45
- permChecker.check()
46
- bannerDismissed = false
47
- }
48
- }
49
-
50
- private var mainContent: some View {
51
- VStack(spacing: 0) {
52
- // Title bar
53
- HStack {
54
- Text("lattices")
55
- .font(Typo.title())
56
- .foregroundColor(Palette.text)
57
-
58
- if runningCount > 0 || !inventory.orphans.isEmpty {
59
- let total = runningCount + inventory.orphans.count
60
- Text("\(total) session\(total == 1 ? "" : "s")")
61
- .font(Typo.mono(10))
62
- .foregroundColor(Palette.running)
63
- .padding(.leading, 4)
64
- } else {
65
- Text("None")
66
- .font(Typo.mono(10))
67
- .foregroundColor(Palette.textMuted)
68
- .padding(.leading, 4)
69
- }
70
-
71
- Spacer()
72
-
73
- headerButton(icon: "arrow.up.left.and.arrow.down.right") {
74
- (NSApp.delegate as? AppDelegate)?.dismissPopover()
75
- MainWindow.shared.show()
76
- }
77
- headerButton(icon: "arrow.clockwise") { scanner.scan(); inventory.refresh() }
78
- }
79
- .padding(.horizontal, 18)
80
- .padding(.top, 14)
81
- .padding(.bottom, 10)
82
-
83
- // Layer switcher
84
- if let config = workspace.config, let layers = config.layers, layers.count > 1 {
85
- layerBar(config: config)
86
- }
87
-
88
- // Search
89
- HStack(spacing: 8) {
90
- Image(systemName: "magnifyingglass")
91
- .foregroundColor(Palette.textMuted)
92
- .font(.system(size: 11))
93
- TextField("Search projects...", text: $searchText)
94
- .textFieldStyle(.plain)
95
- .font(Typo.body(13))
96
- .foregroundColor(Palette.text)
97
- if !searchText.isEmpty {
98
- Button { searchText = "" } label: {
99
- Image(systemName: "xmark.circle.fill")
100
- .foregroundColor(Palette.textMuted)
101
- .font(.system(size: 11))
102
- }
103
- .buttonStyle(.plain)
104
- }
105
- }
106
- .padding(.horizontal, 12)
107
- .padding(.vertical, 8)
108
- .background(
109
- RoundedRectangle(cornerRadius: 4)
110
- .fill(Palette.surface)
111
- )
112
- .padding(.horizontal, 14)
113
- .padding(.bottom, 10)
114
-
115
- // Permission banner
116
- if !permChecker.allGranted && !bannerDismissed {
117
- permissionBanner
118
- }
119
-
120
- Rectangle()
121
- .fill(Palette.border)
122
- .frame(height: 0.5)
123
-
124
- // List
125
- if filtered.isEmpty && (workspace.config?.groups ?? []).isEmpty {
126
- Spacer()
127
- emptyState
128
- Spacer()
129
- } else {
130
- ScrollView {
131
- LazyVStack(spacing: 4) {
132
- // Tab groups section
133
- if let groups = workspace.config?.groups, !groups.isEmpty, searchText.isEmpty {
134
- ForEach(groups) { group in
135
- TabGroupRow(group: group, workspace: workspace)
136
- }
137
-
138
- if !filtered.isEmpty {
139
- Rectangle()
140
- .fill(Palette.border)
141
- .frame(height: 0.5)
142
- .padding(.vertical, 4)
143
- }
144
- }
145
-
146
- // Projects
147
- ForEach(filtered) { project in
148
- ProjectRow(project: project) {
149
- SessionManager.launch(project: project)
150
- } onDetach: {
151
- SessionManager.detach(project: project)
152
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
153
- scanner.refreshStatus()
154
- }
155
- } onKill: {
156
- SessionManager.kill(project: project)
157
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
158
- scanner.refreshStatus()
159
- }
160
- } onSync: {
161
- SessionManager.sync(project: project)
162
- DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
163
- scanner.refreshStatus()
164
- }
165
- } onRestart: { paneName in
166
- SessionManager.restart(project: project, paneName: paneName)
167
- }
168
- }
169
-
170
- // Orphan sessions
171
- if !filteredOrphans.isEmpty {
172
- orphanSection
173
- }
174
- }
175
- .padding(.horizontal, 10)
176
- .padding(.vertical, 8)
177
- }
178
- }
179
-
180
- Rectangle()
181
- .fill(Palette.border)
182
- .frame(height: 0.5)
183
-
184
- // Actions footer
185
- actionsSection
186
- }
187
- }
188
-
189
- // MARK: - Orphan section
190
-
191
- private var orphanSection: some View {
192
- VStack(spacing: 4) {
193
- Rectangle()
194
- .fill(Palette.border)
195
- .frame(height: 0.5)
196
- .padding(.vertical, 4)
197
-
198
- // Section header
199
- Button {
200
- withAnimation(.easeOut(duration: 0.15)) { orphanSectionCollapsed.toggle() }
201
- } label: {
202
- HStack(spacing: 6) {
203
- Image(systemName: orphanSectionCollapsed ? "chevron.right" : "chevron.down")
204
- .font(.system(size: 9, weight: .semibold))
205
- .foregroundColor(Palette.textMuted)
206
-
207
- Text("Unmanaged Sessions")
208
- .font(Typo.caption(10))
209
- .foregroundColor(Palette.textMuted)
210
-
211
- Text("\(filteredOrphans.count)")
212
- .font(Typo.mono(9))
213
- .foregroundColor(Palette.detach)
214
- .padding(.horizontal, 5)
215
- .padding(.vertical, 1)
216
- .background(
217
- RoundedRectangle(cornerRadius: 3)
218
- .fill(Palette.detach.opacity(0.12))
219
- )
220
-
221
- Spacer()
222
- }
223
- }
224
- .buttonStyle(.plain)
225
- .padding(.horizontal, 4)
226
-
227
- if !orphanSectionCollapsed {
228
- ForEach(filteredOrphans) { session in
229
- OrphanRow(
230
- session: session,
231
- onAttach: {
232
- let terminal = Preferences.shared.terminal
233
- terminal.focusOrAttach(session: session.name)
234
- },
235
- onKill: {
236
- SessionManager.killByName(session.name)
237
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
238
- inventory.refresh()
239
- }
240
- }
241
- )
242
- }
243
- }
244
- }
245
- }
246
-
247
- // MARK: - Actions footer
248
-
249
- private var actionsSection: some View {
250
- VStack(spacing: 0) {
251
- ActionRow(shortcut: "1", label: "Command Palette", hotkey: hotkeyLabel(.palette), icon: "command", accentColor: Palette.running) {
252
- CommandPaletteWindow.shared.toggle()
253
- }
254
- ActionRow(shortcut: "2", label: "Screen Map", hotkey: hotkeyLabel(.screenMap), icon: "rectangle.3.group") {
255
- ScreenMapWindowController.shared.toggle()
256
- }
257
- ActionRow(shortcut: "3", label: "Desktop Inventory", hotkey: hotkeyLabel(.desktopInventory), icon: "rectangle.split.2x1") {
258
- CommandModeWindow.shared.toggle()
259
- }
260
- ActionRow(shortcut: "4", label: "Window Bezel", hotkey: hotkeyLabel(.bezel), icon: "macwindow") {
261
- WindowBezel.showBezelForFrontmostWindow()
262
- }
263
- ActionRow(shortcut: "5", label: "Cheat Sheet", hotkey: hotkeyLabel(.cheatSheet), icon: "keyboard") {
264
- CheatSheetHUD.shared.toggle()
265
- }
266
- ActionRow(shortcut: "6", label: "Omni Search", hotkey: hotkeyLabel(.omniSearch), icon: "magnifyingglass", accentColor: Palette.running) {
267
- OmniSearchWindow.shared.toggle()
268
- }
269
-
270
- Rectangle()
271
- .fill(Palette.border)
272
- .frame(height: 0.5)
273
- .padding(.horizontal, 10)
274
-
275
- ActionRow(shortcut: "S", label: "Settings", icon: "gearshape") {
276
- SettingsWindowController.shared.show()
277
- }
278
- HStack(spacing: 0) {
279
- ActionRow(shortcut: "D", label: "Diagnostics", icon: "stethoscope") {
280
- DiagnosticWindow.shared.toggle()
281
- }
282
- if !permChecker.allGranted {
283
- Circle()
284
- .fill(Palette.detach)
285
- .frame(width: 6, height: 6)
286
- .padding(.trailing, 14)
287
- }
288
- }
289
-
290
- Rectangle()
291
- .fill(Palette.border)
292
- .frame(height: 0.5)
293
- .padding(.horizontal, 10)
294
-
295
- ActionRow(shortcut: "Q", label: "Quit", icon: "power", accentColor: Palette.kill) {
296
- NSApp.terminate(nil)
297
- }
298
- }
299
- .padding(.vertical, 4)
300
- .background(Palette.surface.opacity(0.4))
301
- }
302
-
303
- private func hotkeyLabel(_ action: HotkeyAction) -> String? {
304
- guard let binding = HotkeyStore.shared.bindings[action] else { return nil }
305
- return binding.displayParts.joined(separator: "")
306
- }
307
-
308
- // MARK: - Empty state
309
-
310
- private var emptyState: some View {
311
- VStack(spacing: 14) {
312
- Image(systemName: "terminal")
313
- .font(.system(size: 28, weight: .light))
314
- .foregroundColor(Palette.textMuted)
315
-
316
- Text("No projects yet")
317
- .font(Typo.heading(14))
318
- .foregroundColor(Palette.textDim)
319
-
320
- Text("Run lattices init in a project\nto add it here")
321
- .font(Typo.mono(11))
322
- .foregroundColor(Palette.textMuted)
323
- .multilineTextAlignment(.center)
324
- .lineSpacing(3)
325
- }
326
- }
327
-
328
- // MARK: - Permission banner
329
-
330
- private var permissionBanner: some View {
331
- VStack(alignment: .leading, spacing: 6) {
332
- HStack {
333
- Image(systemName: "exclamationmark.triangle.fill")
334
- .font(.system(size: 10))
335
- .foregroundColor(Palette.detach)
336
- Text("PERMISSIONS NEEDED")
337
- .font(Typo.monoBold(10))
338
- .foregroundColor(Palette.detach)
339
- Spacer()
340
- Button { bannerDismissed = true } label: {
341
- Image(systemName: "xmark")
342
- .font(.system(size: 8, weight: .bold))
343
- .foregroundColor(Palette.textMuted)
344
- }
345
- .buttonStyle(.plain)
346
- }
347
-
348
- permissionRow("Accessibility", granted: permChecker.accessibility) {
349
- permChecker.requestAccessibility()
350
- }
351
- permissionRow("Screen Recording", granted: permChecker.screenRecording) {
352
- permChecker.requestScreenRecording()
353
- }
354
-
355
- Text("Click a row to request access.")
356
- .font(Typo.mono(9))
357
- .foregroundColor(Palette.textMuted)
358
- }
359
- .padding(12)
360
- .background(
361
- RoundedRectangle(cornerRadius: 5)
362
- .fill(Palette.detach.opacity(0.08))
363
- .overlay(
364
- RoundedRectangle(cornerRadius: 5)
365
- .strokeBorder(Palette.detach.opacity(0.20), lineWidth: 0.5)
366
- )
367
- )
368
- .padding(.horizontal, 14)
369
- .padding(.bottom, 10)
370
- }
371
-
372
- private func permissionRow(_ name: String, granted: Bool, open: @escaping () -> Void) -> some View {
373
- Button(action: { if !granted { open() } }) {
374
- HStack(spacing: 6) {
375
- Image(systemName: granted ? "checkmark.circle.fill" : "circle")
376
- .font(.system(size: 10))
377
- .foregroundColor(granted ? Palette.running : Palette.detach)
378
- Text(name)
379
- .font(Typo.mono(10))
380
- .foregroundColor(Palette.text)
381
- Spacer()
382
- if granted {
383
- Text("granted")
384
- .font(Typo.mono(9))
385
- .foregroundColor(Palette.running)
386
- } else {
387
- HStack(spacing: 4) {
388
- Text("not set")
389
- .font(Typo.mono(9))
390
- .foregroundColor(Palette.detach)
391
- Image(systemName: "arrow.up.forward.square")
392
- .font(.system(size: 9))
393
- .foregroundColor(Palette.detach)
394
- }
395
- }
396
- }
397
- .padding(.vertical, 4)
398
- .padding(.horizontal, 8)
399
- .background(
400
- RoundedRectangle(cornerRadius: 4)
401
- .fill(granted ? Color.clear : Palette.detach.opacity(0.06))
402
- )
403
- }
404
- .buttonStyle(.plain)
405
- .disabled(granted)
406
- }
407
-
408
- // MARK: - Layer Bar
409
-
410
- private func layerBar(config: WorkspaceConfig) -> some View {
411
- HStack(spacing: 6) {
412
- ForEach(Array((config.layers ?? []).enumerated()), id: \.element.id) { i, layer in
413
- let isActive = i == workspace.activeLayerIndex
414
- let counts = workspace.layerRunningCount(index: i)
415
- Button {
416
- workspace.tileLayer(index: i)
417
- } label: {
418
- VStack(spacing: 2) {
419
- HStack(spacing: 5) {
420
- Circle()
421
- .fill(isActive ? Palette.running : Palette.textMuted.opacity(0.4))
422
- .frame(width: 6, height: 6)
423
- Text(layer.label)
424
- .font(Typo.mono(11))
425
- .foregroundColor(isActive ? Palette.text : Palette.textDim)
426
- if counts.total > 0 {
427
- Text("\(counts.running)/\(counts.total)")
428
- .font(Typo.mono(8))
429
- .foregroundColor(counts.running > 0 ? Palette.running : Palette.textMuted)
430
- }
431
- }
432
- Text("\u{2325}\(i + 1)")
433
- .font(Typo.mono(8))
434
- .foregroundColor(Palette.textMuted.opacity(0.6))
435
- }
436
- .padding(.horizontal, 10)
437
- .padding(.vertical, 5)
438
- .background(
439
- RoundedRectangle(cornerRadius: 5)
440
- .fill(isActive ? Palette.running.opacity(0.1) : Color.clear)
441
- )
442
- .overlay(
443
- RoundedRectangle(cornerRadius: 5)
444
- .strokeBorder(isActive ? Palette.running.opacity(0.3) : Palette.border, lineWidth: 0.5)
445
- )
446
- }
447
- .buttonStyle(.plain)
448
- .disabled(workspace.isSwitching)
449
- }
450
- Spacer()
451
- }
452
- .padding(.horizontal, 14)
453
- .padding(.bottom, 8)
454
- }
455
-
456
- // MARK: - Helpers
457
-
458
- private func headerButton(icon: String, action: @escaping () -> Void) -> some View {
459
- Button(action: action) {
460
- Image(systemName: icon)
461
- .font(.system(size: 12, weight: .medium))
462
- .foregroundColor(Palette.textDim)
463
- .frame(width: 28, height: 28)
464
- }
465
- .buttonStyle(.plain)
466
- }
467
- }
@@ -1,83 +0,0 @@
1
- import AppKit
2
- import SwiftUI
3
-
4
- /// Manages the main lattices window as a standalone NSWindow.
5
- /// Menu bar icon toggles this window open/closed.
6
- final class MainWindow {
7
- static let shared = MainWindow()
8
-
9
- private var window: NSWindow?
10
- private var keyMonitor: Any?
11
-
12
- var isVisible: Bool { window?.isVisible ?? false }
13
-
14
- func toggle() {
15
- if let w = window, w.isVisible {
16
- w.orderOut(nil)
17
- AppDelegate.updateActivationPolicy()
18
- } else {
19
- show()
20
- }
21
- }
22
-
23
- func show() {
24
- if let existing = window {
25
- existing.makeKeyAndOrderFront(nil)
26
- NSApp.activate(ignoringOtherApps: true)
27
- return
28
- }
29
-
30
- let view = MainView(scanner: ProjectScanner.shared)
31
- .preferredColorScheme(.dark)
32
-
33
- let hostingView = NSHostingView(rootView: view)
34
- hostingView.frame = NSRect(x: 0, y: 0, width: 380, height: 460)
35
-
36
- let w = NSWindow(
37
- contentRect: NSRect(x: 0, y: 0, width: 380, height: 460),
38
- styleMask: [.titled, .closable, .resizable, .miniaturizable],
39
- backing: .buffered,
40
- defer: false
41
- )
42
- w.contentView = hostingView
43
- w.title = "lattices"
44
- w.titlebarAppearsTransparent = true
45
- w.titleVisibility = .hidden
46
- w.isReleasedWhenClosed = false
47
- w.backgroundColor = NSColor(red: 0.11, green: 0.11, blue: 0.12, alpha: 1.0)
48
- w.appearance = NSAppearance(named: .darkAqua)
49
- w.minSize = NSSize(width: 340, height: 380)
50
- w.maxSize = NSSize(width: 600, height: 800)
51
-
52
- // Position near top-right of screen (close to menu bar area)
53
- if let screen = NSScreen.main {
54
- let visibleFrame = screen.visibleFrame
55
- let x = visibleFrame.maxX - 380 - 20
56
- let y = visibleFrame.maxY - 460 - 10
57
- w.setFrameOrigin(NSPoint(x: x, y: y))
58
- }
59
-
60
- w.makeKeyAndOrderFront(nil)
61
- NSApp.activate(ignoringOtherApps: true)
62
-
63
- window = w
64
- AppDelegate.updateActivationPolicy()
65
-
66
- // Escape key → close
67
- keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
68
- guard event.keyCode == 53,
69
- self?.window?.isKeyWindow == true else { return event }
70
- self?.close()
71
- return nil
72
- }
73
- }
74
-
75
- func close() {
76
- window?.orderOut(nil)
77
- if let monitor = keyMonitor {
78
- NSEvent.removeMonitor(monitor)
79
- keyMonitor = nil
80
- }
81
- AppDelegate.updateActivationPolicy()
82
- }
83
- }