@lattices/cli 0.4.2 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/app/Info.plist +2 -2
- package/app/Lattices.app/Contents/Info.plist +2 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Package.swift +6 -0
- package/app/Sources/App.swift +10 -0
- package/app/Sources/AppDelegate.swift +90 -34
- package/app/Sources/AppShellView.swift +2 -0
- package/app/Sources/AppTypeClassifier.swift +36 -0
- package/app/Sources/AppUpdater.swift +92 -0
- package/app/Sources/CheatSheetHUD.swift +1 -0
- package/app/Sources/CliActionLauncher.swift +50 -0
- package/app/Sources/CommandModeView.swift +4 -24
- package/app/Sources/CompanionActivityLog.swift +70 -0
- package/app/Sources/CompanionKeyboardController.swift +141 -0
- package/app/Sources/DesktopModel.swift +4 -0
- package/app/Sources/HandsOffSession.swift +15 -4
- package/app/Sources/HomeDashboardView.swift +18 -10
- package/app/Sources/HotkeyStore.swift +8 -5
- package/app/Sources/IntentEngine.swift +7 -1
- package/app/Sources/LatticesApi.swift +125 -4
- package/app/Sources/LatticesCompanionBridgeServer.swift +438 -0
- package/app/Sources/LatticesCompanionCockpit.swift +555 -0
- package/app/Sources/LatticesCompanionSecurityCoordinator.swift +594 -0
- package/app/Sources/LatticesCompanionTrackpadController.swift +204 -0
- package/app/Sources/LatticesDeckHost.swift +1463 -0
- package/app/Sources/LatticesRuntime.swift +61 -0
- package/app/Sources/MainView.swift +351 -191
- package/app/Sources/MouseFinder.swift +335 -30
- package/app/Sources/MouseGestureConfig.swift +364 -0
- package/app/Sources/MouseGestureController.swift +1203 -0
- package/app/Sources/MouseInputDeviceStore.swift +98 -0
- package/app/Sources/MouseInputEventViewer.swift +272 -0
- package/app/Sources/MouseShortcutStore.swift +107 -0
- package/app/Sources/OmniSearchView.swift +136 -2
- package/app/Sources/OmniSearchWindow.swift +65 -5
- package/app/Sources/OnboardingView.swift +30 -16
- package/app/Sources/PaletteCommand.swift +26 -6
- package/app/Sources/PermissionChecker.swift +76 -2
- package/app/Sources/PiAuthNextStepCard.swift +148 -0
- package/app/Sources/PiAuthPromptCard.swift +90 -0
- package/app/Sources/PiChatDock.swift +137 -74
- package/app/Sources/PiChatSession.swift +608 -108
- package/app/Sources/PiInstallCallout.swift +86 -0
- package/app/Sources/PiProviderSetupCallout.swift +99 -0
- package/app/Sources/PiWorkspaceView.swift +174 -77
- package/app/Sources/Preferences.swift +78 -0
- package/app/Sources/ScreenMapState.swift +91 -31
- package/app/Sources/ScreenMapView.swift +510 -524
- package/app/Sources/ScreenMapWindowController.swift +12 -4
- package/app/Sources/SettingsView.swift +869 -152
- package/app/Sources/SystemTelemetryMonitor.swift +273 -0
- package/app/Sources/VoiceCommandWindow.swift +23 -2
- package/app/Sources/WindowDragSnapController.swift +628 -0
- package/app/Sources/WindowTiler.swift +328 -65
- package/app/Sources/WorkspaceManager.swift +288 -0
- package/bin/assistant-intelligence.ts +874 -0
- package/bin/handsoff-infer.ts +16 -209
- package/bin/handsoff-worker.ts +45 -258
- package/bin/lattices-app.ts +62 -0
- package/bin/lattices-dev +4 -0
- package/bin/lattices.ts +125 -14
- package/docs/agents.md +14 -0
- package/docs/api.md +55 -0
- package/docs/app.md +3 -0
- package/docs/companion-deck.md +180 -0
- package/docs/config.md +25 -0
- package/docs/tiling-reference.md +55 -0
- package/docs/voice-error-model.md +73 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -58,6 +58,9 @@ To build a signed, notarized DMG for distribution:
|
|
|
58
58
|
```sh
|
|
59
59
|
# Requires a Developer ID certificate and notarytool keychain profile
|
|
60
60
|
./scripts/build-dmg.sh
|
|
61
|
+
|
|
62
|
+
# Update v<package.json version> and upload the DMG to GitHub Releases
|
|
63
|
+
./scripts/ship.sh
|
|
61
64
|
```
|
|
62
65
|
|
|
63
66
|
## Quick start
|
package/app/Info.plist
CHANGED
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
<key>CFBundlePackageType</key>
|
|
16
16
|
<string>APPL</string>
|
|
17
17
|
<key>CFBundleVersion</key>
|
|
18
|
-
<string>0.4.
|
|
18
|
+
<string>0.4.5</string>
|
|
19
19
|
<key>CFBundleShortVersionString</key>
|
|
20
|
-
<string>0.4.
|
|
20
|
+
<string>0.4.5</string>
|
|
21
21
|
<key>LSMinimumSystemVersion</key>
|
|
22
22
|
<string>13.0</string>
|
|
23
23
|
<key>LSUIElement</key>
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
<key>CFBundlePackageType</key>
|
|
16
16
|
<string>APPL</string>
|
|
17
17
|
<key>CFBundleVersion</key>
|
|
18
|
-
<string>0.4.
|
|
18
|
+
<string>0.4.5</string>
|
|
19
19
|
<key>CFBundleShortVersionString</key>
|
|
20
|
-
<string>0.4.
|
|
20
|
+
<string>0.4.5</string>
|
|
21
21
|
<key>LSMinimumSystemVersion</key>
|
|
22
22
|
<string>13.0</string>
|
|
23
23
|
<key>LSUIElement</key>
|
|
Binary file
|
package/app/Package.swift
CHANGED
|
@@ -4,9 +4,15 @@ import PackageDescription
|
|
|
4
4
|
let package = Package(
|
|
5
5
|
name: "Lattices",
|
|
6
6
|
platforms: [.macOS(.v13)],
|
|
7
|
+
dependencies: [
|
|
8
|
+
.package(path: "../swift")
|
|
9
|
+
],
|
|
7
10
|
targets: [
|
|
8
11
|
.executableTarget(
|
|
9
12
|
name: "Lattices",
|
|
13
|
+
dependencies: [
|
|
14
|
+
.product(name: "DeckKit", package: "swift")
|
|
15
|
+
],
|
|
10
16
|
path: "Sources",
|
|
11
17
|
resources: [
|
|
12
18
|
.copy("../Resources/tap.wav"),
|
package/app/Sources/App.swift
CHANGED
|
@@ -6,5 +6,15 @@ struct LatticesApp: App {
|
|
|
6
6
|
|
|
7
7
|
var body: some Scene {
|
|
8
8
|
Settings { EmptyView() }
|
|
9
|
+
.commands {
|
|
10
|
+
CommandGroup(after: .appInfo) {
|
|
11
|
+
Button("Update Lattices…") {
|
|
12
|
+
AppUpdater.shared.promptForUpdate()
|
|
13
|
+
}
|
|
14
|
+
.disabled(!AppUpdater.shared.canUpdate)
|
|
15
|
+
|
|
16
|
+
Divider()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
9
19
|
}
|
|
10
20
|
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import AppKit
|
|
2
2
|
import SwiftUI
|
|
3
3
|
|
|
4
|
+
extension Notification.Name {
|
|
5
|
+
static let latticesPopoverWillShow = Notification.Name("latticesPopoverWillShow")
|
|
6
|
+
}
|
|
7
|
+
|
|
4
8
|
/// Manages the NSStatusItem (menu bar icon), left-click popover, and right-click context menu.
|
|
5
9
|
/// Replaces the previous SwiftUI MenuBarExtra approach for full click-event control.
|
|
6
10
|
class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
11
|
+
private static weak var shared: AppDelegate?
|
|
7
12
|
|
|
8
13
|
private var statusItem: NSStatusItem!
|
|
9
14
|
private var popover: NSPopover?
|
|
@@ -45,6 +50,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
45
50
|
/// based on whether any managed windows are open.
|
|
46
51
|
static func updateActivationPolicy() {
|
|
47
52
|
let hasVisibleWindow =
|
|
53
|
+
(Self.shared?.popover?.isShown == true) ||
|
|
48
54
|
CommandModeWindow.shared.isVisible ||
|
|
49
55
|
CommandPaletteWindow.shared.isVisible ||
|
|
50
56
|
MainWindow.shared.isVisible ||
|
|
@@ -60,6 +66,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
69
|
+
Self.shared = self
|
|
63
70
|
NSApp.setActivationPolicy(.accessory)
|
|
64
71
|
NSApp.appearance = NSAppearance(named: .darkAqua)
|
|
65
72
|
|
|
@@ -82,8 +89,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
82
89
|
let store = HotkeyStore.shared
|
|
83
90
|
store.register(action: .palette) { CommandPaletteWindow.shared.toggle() }
|
|
84
91
|
store.register(action: .unifiedWindow) { ScreenMapWindowController.shared.toggle() }
|
|
85
|
-
store.register(action: .bezel) {
|
|
86
|
-
store.register(action: .cheatSheet) {
|
|
92
|
+
store.register(action: .bezel) { Self.showWorkspaceInspector() }
|
|
93
|
+
store.register(action: .cheatSheet) { SettingsWindowController.shared.show() }
|
|
87
94
|
store.register(action: .voiceCommand) {
|
|
88
95
|
DiagnosticLog.shared.info("Hotkey: voiceCommand triggered")
|
|
89
96
|
VoiceCommandWindow.shared.toggle()
|
|
@@ -103,7 +110,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
103
110
|
|
|
104
111
|
// Pre-render HUD panels off-screen for instant first open
|
|
105
112
|
DispatchQueue.main.async { HUDController.shared.warmUp() }
|
|
113
|
+
// Pre-build the menu bar popover so the first click doesn't pay the SwiftUI mount cost.
|
|
114
|
+
// Touching `.view` forces NSHostingController to materialize the SwiftUI view tree.
|
|
115
|
+
DispatchQueue.main.async { [weak self] in
|
|
116
|
+
guard let self = self else { return }
|
|
117
|
+
let p = self.makePopover()
|
|
118
|
+
_ = p.contentViewController?.view
|
|
119
|
+
}
|
|
106
120
|
store.register(action: .omniSearch) { OmniSearchWindow.shared.toggle() }
|
|
121
|
+
WindowDragSnapController.shared.start()
|
|
122
|
+
MouseGestureController.shared.start()
|
|
107
123
|
|
|
108
124
|
// Session layer cycling
|
|
109
125
|
store.register(action: .layerNext) { SessionLayerStore.shared.cycleNext() }
|
|
@@ -140,7 +156,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
140
156
|
for (action, position) in tileMap {
|
|
141
157
|
store.register(action: action) { WindowTiler.tileFrontmostViaAX(to: position) }
|
|
142
158
|
}
|
|
143
|
-
store.register(action: .tileDistribute) { WindowTiler.distributeVisible() }
|
|
159
|
+
store.register(action: .tileDistribute) { WindowTiler.distributeVisible(reactivateLattices: false) }
|
|
160
|
+
store.register(action: .tileTypeGrid) { WindowTiler.distributeVisibleByFrontmostType(reactivateLattices: false) }
|
|
144
161
|
|
|
145
162
|
// Onboarding on first launch; otherwise just check permissions
|
|
146
163
|
if !OnboardingWindowController.shared.showIfNeeded() {
|
|
@@ -157,6 +174,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
157
174
|
ProcessModel.shared.start()
|
|
158
175
|
LatticesApi.setup()
|
|
159
176
|
DaemonServer.shared.start()
|
|
177
|
+
LatticesCompanionBridgeServer.shared.start()
|
|
160
178
|
AgentPool.shared.start()
|
|
161
179
|
diag.finish(tBoot)
|
|
162
180
|
|
|
@@ -167,14 +185,19 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
167
185
|
}
|
|
168
186
|
}
|
|
169
187
|
|
|
170
|
-
// --screen-map flag: auto-open
|
|
188
|
+
// --screen-map flag: auto-open layout on launch
|
|
171
189
|
if CommandLine.arguments.contains("--screen-map") {
|
|
172
190
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
173
|
-
ScreenMapWindowController.shared.
|
|
191
|
+
ScreenMapWindowController.shared.showPage(.screenMap)
|
|
174
192
|
}
|
|
175
193
|
}
|
|
176
194
|
}
|
|
177
195
|
|
|
196
|
+
func applicationWillTerminate(_ notification: Notification) {
|
|
197
|
+
LatticesCompanionBridgeServer.shared.stop()
|
|
198
|
+
DaemonServer.shared.stop()
|
|
199
|
+
}
|
|
200
|
+
|
|
178
201
|
// MARK: - Status item click handler
|
|
179
202
|
|
|
180
203
|
@objc private func statusItemClicked(_ sender: Any?) {
|
|
@@ -184,13 +207,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
184
207
|
// Right-click → context menu
|
|
185
208
|
contextMenu.popUp(positioning: nil, at: NSPoint(x: 0, y: button.bounds.height + 4), in: button)
|
|
186
209
|
} else {
|
|
187
|
-
// Left-click → toggle popover
|
|
210
|
+
// Left-click → toggle the menu bar projects popover.
|
|
188
211
|
if let shown = popover, shown.isShown {
|
|
189
212
|
shown.performClose(sender)
|
|
190
213
|
} else {
|
|
191
|
-
|
|
192
|
-
p.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
|
|
193
|
-
p.contentViewController?.view.window?.makeKey()
|
|
214
|
+
showProjectsPopover()
|
|
194
215
|
}
|
|
195
216
|
}
|
|
196
217
|
}
|
|
@@ -200,14 +221,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
200
221
|
popover?.performClose(nil)
|
|
201
222
|
}
|
|
202
223
|
|
|
203
|
-
///
|
|
204
|
-
///
|
|
224
|
+
/// Cached popover — built lazily on first click, reused on every subsequent open.
|
|
225
|
+
/// Keeping the SwiftUI view tree alive avoids rebuilding on each click (slow first paint).
|
|
226
|
+
/// Data refresh is driven from `popoverWillShow` + a notification MainView listens to.
|
|
205
227
|
private func makePopover() -> NSPopover {
|
|
228
|
+
if let p = popover { return p }
|
|
206
229
|
let t = DiagnosticLog.shared.startTimed("makePopover")
|
|
207
230
|
let p = NSPopover()
|
|
208
231
|
p.contentViewController = NSHostingController(rootView: MainView(scanner: ProjectScanner.shared))
|
|
209
232
|
p.behavior = .transient
|
|
210
|
-
p.contentSize = NSSize(width: 380, height:
|
|
233
|
+
p.contentSize = NSSize(width: 380, height: 300)
|
|
211
234
|
p.appearance = NSAppearance(named: .darkAqua)
|
|
212
235
|
p.delegate = self
|
|
213
236
|
popover = p
|
|
@@ -215,10 +238,20 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
215
238
|
return p
|
|
216
239
|
}
|
|
217
240
|
|
|
241
|
+
private func showProjectsPopover() {
|
|
242
|
+
guard let button = statusItem.button else { return }
|
|
243
|
+
let p = makePopover()
|
|
244
|
+
p.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
|
|
245
|
+
p.contentViewController?.view.window?.makeKey()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
func popoverWillShow(_ notification: Notification) {
|
|
249
|
+
Self.updateActivationPolicy()
|
|
250
|
+
NotificationCenter.default.post(name: .latticesPopoverWillShow, object: nil)
|
|
251
|
+
}
|
|
252
|
+
|
|
218
253
|
func popoverDidClose(_ notification: Notification) {
|
|
219
|
-
|
|
220
|
-
popover?.contentViewController = nil
|
|
221
|
-
popover = nil
|
|
254
|
+
Self.updateActivationPolicy()
|
|
222
255
|
}
|
|
223
256
|
|
|
224
257
|
// MARK: - Context menu
|
|
@@ -227,10 +260,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
227
260
|
let menu = NSMenu()
|
|
228
261
|
|
|
229
262
|
let actions: [(String, String, Selector)] = [
|
|
263
|
+
("Home", "", #selector(menuWorkspace)),
|
|
264
|
+
("Layout", "", #selector(menuLayout)),
|
|
265
|
+
("Search", "", #selector(menuSearch)),
|
|
230
266
|
("Command Palette", "⌘⇧M", #selector(menuCommandPalette)),
|
|
231
|
-
("Workspace", "", #selector(menuWorkspace)),
|
|
232
|
-
("Assistant", "", #selector(menuAssistant)),
|
|
233
|
-
("Help & Shortcuts", "", #selector(menuDocs)),
|
|
234
267
|
]
|
|
235
268
|
for (title, shortcut, action) in actions {
|
|
236
269
|
let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
|
|
@@ -243,14 +276,29 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
243
276
|
|
|
244
277
|
menu.addItem(.separator())
|
|
245
278
|
|
|
246
|
-
let
|
|
279
|
+
let cliActions: [(String, Selector)] = [
|
|
280
|
+
("Projects…", #selector(menuProjects)),
|
|
281
|
+
("Initialize Project in Terminal…", #selector(menuInitializeProject)),
|
|
282
|
+
("Launch Project in Terminal…", #selector(menuLaunchProject)),
|
|
283
|
+
]
|
|
284
|
+
for (title, action) in cliActions {
|
|
285
|
+
let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
|
|
286
|
+
item.target = self
|
|
287
|
+
menu.addItem(item)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
menu.addItem(.separator())
|
|
291
|
+
|
|
292
|
+
let update = NSMenuItem(title: "Update Lattices…", action: #selector(menuUpdate), keyEquivalent: "")
|
|
293
|
+
update.target = self
|
|
294
|
+
menu.addItem(update)
|
|
295
|
+
|
|
296
|
+
menu.addItem(.separator())
|
|
297
|
+
|
|
298
|
+
let settings = NSMenuItem(title: "Help & Settings…", action: #selector(menuSettings), keyEquivalent: ",")
|
|
247
299
|
settings.target = self
|
|
248
300
|
menu.addItem(settings)
|
|
249
301
|
|
|
250
|
-
let diag = NSMenuItem(title: "Diagnostics", action: #selector(menuDiagnostics), keyEquivalent: "")
|
|
251
|
-
diag.target = self
|
|
252
|
-
menu.addItem(diag)
|
|
253
|
-
|
|
254
302
|
menu.addItem(.separator())
|
|
255
303
|
|
|
256
304
|
let quit = NSMenuItem(title: "Quit Lattices", action: #selector(menuQuit), keyEquivalent: "q")
|
|
@@ -262,19 +310,27 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
262
310
|
|
|
263
311
|
@objc private func menuCommandPalette() { CommandPaletteWindow.shared.toggle() }
|
|
264
312
|
@objc private func menuWorkspace() { ScreenMapWindowController.shared.showPage(.home) }
|
|
265
|
-
@objc private func
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
@objc private func menuDocs() { ScreenMapWindowController.shared.showPage(.docs) }
|
|
313
|
+
@objc private func menuLayout() { ScreenMapWindowController.shared.showPage(.screenMap) }
|
|
314
|
+
@objc private func menuSearch() { ScreenMapWindowController.shared.showPage(.desktopInventory) }
|
|
315
|
+
@objc private func menuDocs() { SettingsWindowController.shared.show() }
|
|
316
|
+
@objc private func menuProjects() { DispatchQueue.main.async { self.showProjectsPopover() } }
|
|
317
|
+
@objc private func menuInitializeProject() { CliActionLauncher.initializeProjectInTerminal() }
|
|
318
|
+
@objc private func menuLaunchProject() { CliActionLauncher.launchProjectInTerminal() }
|
|
273
319
|
@objc private func menuHUD() { HUDController.shared.toggle() }
|
|
274
|
-
@objc private func menuWindowBezel() {
|
|
275
|
-
@objc private func menuCheatSheet() {
|
|
320
|
+
@objc private func menuWindowBezel() { Self.showWorkspaceInspector() }
|
|
321
|
+
@objc private func menuCheatSheet() { SettingsWindowController.shared.show() }
|
|
276
322
|
@objc private func menuOmniSearch() { OmniSearchWindow.shared.toggle() }
|
|
323
|
+
@MainActor @objc private func menuUpdate() { AppUpdater.shared.promptForUpdate() }
|
|
277
324
|
@objc private func menuSettings() { SettingsWindowController.shared.show() }
|
|
278
|
-
@objc private func menuDiagnostics() { DiagnosticWindow.shared.toggle() }
|
|
279
325
|
@objc private func menuQuit() { NSApp.terminate(nil) }
|
|
326
|
+
|
|
327
|
+
private static func showWorkspaceInspector() {
|
|
328
|
+
guard let entry = DesktopModel.shared.frontmostWindow(),
|
|
329
|
+
entry.app != "Lattices" else {
|
|
330
|
+
ScreenMapWindowController.shared.showPage(.screenMap)
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
ScreenMapWindowController.shared.showWindow(wid: entry.wid)
|
|
335
|
+
}
|
|
280
336
|
}
|
|
@@ -66,6 +66,7 @@ struct AppShellView: View {
|
|
|
66
66
|
ForEach(AppPage.primaryTabs, id: \.rawValue) { tab in
|
|
67
67
|
tabButton(tab)
|
|
68
68
|
}
|
|
69
|
+
|
|
69
70
|
Spacer()
|
|
70
71
|
}
|
|
71
72
|
.padding(.horizontal, 12)
|
|
@@ -90,6 +91,7 @@ struct AppShellView: View {
|
|
|
90
91
|
.foregroundColor(isActive ? Palette.text : Palette.textMuted)
|
|
91
92
|
.padding(.horizontal, 12)
|
|
92
93
|
.padding(.vertical, 6)
|
|
94
|
+
.contentShape(Rectangle())
|
|
93
95
|
.background(
|
|
94
96
|
RoundedRectangle(cornerRadius: 6)
|
|
95
97
|
.fill(isActive ? Palette.surfaceHov : Color.clear)
|
|
@@ -13,6 +13,20 @@ enum AppType: String, CaseIterable {
|
|
|
13
13
|
var label: String { rawValue }
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
enum AppGrouping {
|
|
17
|
+
case type(AppType)
|
|
18
|
+
case exactApp(String)
|
|
19
|
+
|
|
20
|
+
var label: String {
|
|
21
|
+
switch self {
|
|
22
|
+
case .type(let type):
|
|
23
|
+
return type.label
|
|
24
|
+
case .exactApp(let appName):
|
|
25
|
+
return appName
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
enum AppTypeClassifier {
|
|
17
31
|
private static let nameMap: [String: AppType] = [
|
|
18
32
|
// Terminals
|
|
@@ -67,4 +81,26 @@ enum AppTypeClassifier {
|
|
|
67
81
|
if lower.contains("slack") || lower.contains("discord") || lower.contains("chat") || lower.contains("teams") { return .chat }
|
|
68
82
|
return .other
|
|
69
83
|
}
|
|
84
|
+
|
|
85
|
+
static func grouping(for appName: String) -> AppGrouping {
|
|
86
|
+
switch classify(appName) {
|
|
87
|
+
case .system, .other:
|
|
88
|
+
return .exactApp(appName)
|
|
89
|
+
case let type:
|
|
90
|
+
return .type(type)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static func matches(_ appName: String, grouping: AppGrouping) -> Bool {
|
|
95
|
+
switch grouping {
|
|
96
|
+
case .type(let type):
|
|
97
|
+
return classify(appName) == type
|
|
98
|
+
case .exactApp(let exactApp):
|
|
99
|
+
return appName.localizedCaseInsensitiveCompare(exactApp) == .orderedSame
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static func matches(_ appName: String, type: AppType) -> Bool {
|
|
104
|
+
classify(appName) == type
|
|
105
|
+
}
|
|
70
106
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import Combine
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
@MainActor
|
|
6
|
+
final class AppUpdater: ObservableObject {
|
|
7
|
+
static let shared = AppUpdater()
|
|
8
|
+
|
|
9
|
+
@Published private(set) var isUpdating = false
|
|
10
|
+
@Published private(set) var statusMessage: String?
|
|
11
|
+
|
|
12
|
+
private init() {}
|
|
13
|
+
|
|
14
|
+
var currentVersion: String { LatticesRuntime.appVersion }
|
|
15
|
+
|
|
16
|
+
var canUpdate: Bool {
|
|
17
|
+
LatticesRuntime.bunPath != nil && LatticesRuntime.appHelperScriptPath != nil
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
var unavailableReason: String? {
|
|
21
|
+
if LatticesRuntime.bunPath == nil {
|
|
22
|
+
return "Install Bun to enable in-app updates."
|
|
23
|
+
}
|
|
24
|
+
if LatticesRuntime.appHelperScriptPath == nil {
|
|
25
|
+
return "Launch Lattices via `lattices app` so the updater can find the CLI bundle."
|
|
26
|
+
}
|
|
27
|
+
return nil
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func promptForUpdate() {
|
|
31
|
+
guard canUpdate else {
|
|
32
|
+
presentAlert(
|
|
33
|
+
title: "Update Unavailable",
|
|
34
|
+
message: unavailableReason ?? "Lattices could not locate its updater."
|
|
35
|
+
)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let alert = NSAlert()
|
|
40
|
+
alert.alertStyle = .informational
|
|
41
|
+
alert.messageText = "Install the latest Lattices app update?"
|
|
42
|
+
alert.informativeText = "Lattices will download the latest released app bundle, close, and relaunch when the update is ready."
|
|
43
|
+
alert.addButton(withTitle: "Update")
|
|
44
|
+
alert.addButton(withTitle: "Cancel")
|
|
45
|
+
|
|
46
|
+
guard alert.runModal() == .alertFirstButtonReturn else { return }
|
|
47
|
+
startDetachedUpdate()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private func startDetachedUpdate() {
|
|
51
|
+
guard !isUpdating else { return }
|
|
52
|
+
guard let bunPath = LatticesRuntime.bunPath,
|
|
53
|
+
let scriptPath = LatticesRuntime.appHelperScriptPath else {
|
|
54
|
+
presentAlert(
|
|
55
|
+
title: "Update Unavailable",
|
|
56
|
+
message: unavailableReason ?? "Lattices could not locate its updater."
|
|
57
|
+
)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let proc = Process()
|
|
62
|
+
proc.executableURL = URL(fileURLWithPath: bunPath)
|
|
63
|
+
proc.arguments = [scriptPath, "update", "--detach", "--launch"]
|
|
64
|
+
if let cliRoot = LatticesRuntime.cliRoot {
|
|
65
|
+
proc.currentDirectoryURL = URL(fileURLWithPath: cliRoot)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
var env = ProcessInfo.processInfo.environment
|
|
69
|
+
env.removeValue(forKey: "CLAUDECODE")
|
|
70
|
+
proc.environment = env
|
|
71
|
+
|
|
72
|
+
do {
|
|
73
|
+
try proc.run()
|
|
74
|
+
isUpdating = true
|
|
75
|
+
statusMessage = "Updating to the latest release. Lattices will relaunch when it's ready."
|
|
76
|
+
} catch {
|
|
77
|
+
presentAlert(
|
|
78
|
+
title: "Update Failed",
|
|
79
|
+
message: "Lattices could not start the updater.\n\n\(error.localizedDescription)"
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private func presentAlert(title: String, message: String) {
|
|
85
|
+
let alert = NSAlert()
|
|
86
|
+
alert.alertStyle = .warning
|
|
87
|
+
alert.messageText = title
|
|
88
|
+
alert.informativeText = message
|
|
89
|
+
alert.addButton(withTitle: "OK")
|
|
90
|
+
alert.runModal()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -254,6 +254,7 @@ struct CheatSheetView: View {
|
|
|
254
254
|
// Center + Distribute
|
|
255
255
|
shortcutRow(action: .tileCenter)
|
|
256
256
|
shortcutRow(action: .tileDistribute)
|
|
257
|
+
shortcutRow(action: .tileTypeGrid)
|
|
257
258
|
|
|
258
259
|
// Hovered shortcut detail
|
|
259
260
|
if let hovered = hoveredAction, let binding = hotkeyStore.bindings[hovered] {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
|
|
3
|
+
enum CliActionLauncher {
|
|
4
|
+
private static var defaultDirectory: String {
|
|
5
|
+
let root = Preferences.shared.scanRoot
|
|
6
|
+
return root.isEmpty ? NSHomeDirectory() : root
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
private static func chooseProjectDirectory(message: String, prompt: String) -> String? {
|
|
10
|
+
let panel = NSOpenPanel()
|
|
11
|
+
panel.message = message
|
|
12
|
+
panel.prompt = prompt
|
|
13
|
+
panel.canChooseFiles = false
|
|
14
|
+
panel.canChooseDirectories = true
|
|
15
|
+
panel.allowsMultipleSelection = false
|
|
16
|
+
panel.directoryURL = URL(fileURLWithPath: defaultDirectory)
|
|
17
|
+
return panel.runModal() == .OK ? panel.url?.path : nil
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static func initializeProjectInTerminal() {
|
|
21
|
+
guard let directory = chooseProjectDirectory(
|
|
22
|
+
message: "Choose a project folder to initialize with Lattices.",
|
|
23
|
+
prompt: "Initialize"
|
|
24
|
+
) else { return }
|
|
25
|
+
|
|
26
|
+
Preferences.shared.terminal.launch(
|
|
27
|
+
command: "lattices init && lattices",
|
|
28
|
+
in: directory
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static func launchProjectInTerminal() {
|
|
33
|
+
guard let directory = chooseProjectDirectory(
|
|
34
|
+
message: "Choose a project folder to launch with Lattices.",
|
|
35
|
+
prompt: "Launch"
|
|
36
|
+
) else { return }
|
|
37
|
+
|
|
38
|
+
Preferences.shared.terminal.launch(
|
|
39
|
+
command: "lattices",
|
|
40
|
+
in: directory
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static func installTmuxInTerminal() {
|
|
45
|
+
Preferences.shared.terminal.launch(
|
|
46
|
+
command: "brew install tmux",
|
|
47
|
+
in: defaultDirectory
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -121,23 +121,6 @@ struct CommandModeView: View {
|
|
|
121
121
|
|
|
122
122
|
Spacer()
|
|
123
123
|
|
|
124
|
-
if let layer = state.inventory.activeLayer {
|
|
125
|
-
HStack(spacing: 4) {
|
|
126
|
-
Text("Layer: \(layer)")
|
|
127
|
-
.font(Typo.mono(10))
|
|
128
|
-
.foregroundColor(Palette.running)
|
|
129
|
-
|
|
130
|
-
Text("[\(state.inventory.layerCount > 0 ? "\(WorkspaceManager.shared.activeLayerIndex + 1)/\(state.inventory.layerCount)" : "—")]")
|
|
131
|
-
.font(Typo.mono(10))
|
|
132
|
-
.foregroundColor(Palette.textMuted)
|
|
133
|
-
}
|
|
134
|
-
.padding(.horizontal, 6)
|
|
135
|
-
.padding(.vertical, 2)
|
|
136
|
-
.background(
|
|
137
|
-
RoundedRectangle(cornerRadius: 3)
|
|
138
|
-
.fill(Palette.running.opacity(0.10))
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
124
|
}
|
|
142
125
|
.padding(.horizontal, 16)
|
|
143
126
|
.padding(.vertical, 10)
|
|
@@ -155,15 +138,12 @@ struct CommandModeView: View {
|
|
|
155
138
|
private var inventoryGrid: some View {
|
|
156
139
|
ScrollView {
|
|
157
140
|
LazyVStack(alignment: .leading, spacing: 0) {
|
|
158
|
-
let
|
|
159
|
-
if
|
|
141
|
+
let items = state.inventory.items
|
|
142
|
+
if items.isEmpty {
|
|
160
143
|
emptyState
|
|
161
144
|
} else {
|
|
162
|
-
ForEach(
|
|
163
|
-
|
|
164
|
-
ForEach(Array(items.enumerated()), id: \.offset) { _, item in
|
|
165
|
-
inventoryRow(item)
|
|
166
|
-
}
|
|
145
|
+
ForEach(Array(items.enumerated()), id: \.offset) { _, item in
|
|
146
|
+
inventoryRow(item)
|
|
167
147
|
}
|
|
168
148
|
}
|
|
169
149
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import DeckKit
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
final class CompanionActivityLog {
|
|
5
|
+
static let shared = CompanionActivityLog()
|
|
6
|
+
|
|
7
|
+
private let lock = NSLock()
|
|
8
|
+
private var entries: [DeckActivityLogEntry] = []
|
|
9
|
+
private let maxEntries = 120
|
|
10
|
+
|
|
11
|
+
private init() {
|
|
12
|
+
EventBus.shared.subscribe { [weak self] event in
|
|
13
|
+
self?.record(event)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func record(tag: String, tint: String?, text: String) {
|
|
18
|
+
let entry = DeckActivityLogEntry(
|
|
19
|
+
id: UUID().uuidString,
|
|
20
|
+
createdAt: Date(),
|
|
21
|
+
tag: tag,
|
|
22
|
+
tint: tint,
|
|
23
|
+
text: text
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
lock.lock()
|
|
27
|
+
entries.append(entry)
|
|
28
|
+
if entries.count > maxEntries {
|
|
29
|
+
entries.removeFirst(entries.count - maxEntries)
|
|
30
|
+
}
|
|
31
|
+
lock.unlock()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func snapshot(limit: Int = 80) -> [DeckActivityLogEntry] {
|
|
35
|
+
lock.lock()
|
|
36
|
+
let copy = entries
|
|
37
|
+
lock.unlock()
|
|
38
|
+
|
|
39
|
+
return Array(copy.suffix(limit).reversed())
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private extension CompanionActivityLog {
|
|
44
|
+
func record(_ event: ModelEvent) {
|
|
45
|
+
switch event {
|
|
46
|
+
case .windowsChanged(let windows, let added, let removed):
|
|
47
|
+
let delta = [added.isEmpty ? nil : "+\(added.count)", removed.isEmpty ? nil : "-\(removed.count)"]
|
|
48
|
+
.compactMap { $0 }
|
|
49
|
+
.joined(separator: " ")
|
|
50
|
+
let suffix = delta.isEmpty ? "" : " (\(delta))"
|
|
51
|
+
record(tag: "WIN", tint: "blue", text: "\(windows.count) desktop windows\(suffix)")
|
|
52
|
+
|
|
53
|
+
case .tmuxChanged(let sessions):
|
|
54
|
+
record(tag: "TMUX", tint: "green", text: "\(sessions.count) tmux sessions indexed")
|
|
55
|
+
|
|
56
|
+
case .layerSwitched(let index):
|
|
57
|
+
record(tag: "LAYER", tint: "violet", text: "Switched workspace layer \(index + 1)")
|
|
58
|
+
|
|
59
|
+
case .processesChanged(let interesting):
|
|
60
|
+
record(tag: "PROC", tint: "amber", text: "\(interesting.count) terminal processes changed")
|
|
61
|
+
|
|
62
|
+
case .ocrScanComplete(let windowCount, let totalBlocks):
|
|
63
|
+
record(tag: "OCR", tint: "teal", text: "Scanned \(totalBlocks) text blocks across \(windowCount) windows")
|
|
64
|
+
|
|
65
|
+
case .voiceCommand(let text, let confidence):
|
|
66
|
+
let pct = Int((confidence * 100).rounded())
|
|
67
|
+
record(tag: "VOICE", tint: "red", text: "\"\(text)\" · \(pct)%")
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|