@lattices/cli 0.4.5 → 0.4.6
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/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/Sources/{AppDelegate.swift → AppShell/AppDelegate.swift} +4 -0
- package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +10 -1
- package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +2 -1
- package/app/Sources/{IntentEngine.swift → Core/Actions/IntentEngine.swift} +44 -26
- package/app/Sources/Core/Actions/IntentSchema.swift +94 -0
- package/app/Sources/{Intents → Core/Actions/Intents}/LatticeIntent.swift +0 -25
- package/app/Sources/{VoiceIntentResolver.swift → Core/Actions/VoiceIntentResolver.swift} +46 -4
- package/app/Sources/{DesktopModel.swift → Core/Desktop/DesktopModel.swift} +2 -8
- package/app/Sources/Core/Desktop/SessionWindowLocator.swift +139 -0
- package/app/Sources/Core/Desktop/WindowPreviewCard.swift +100 -0
- package/app/Sources/Core/Desktop/WindowPreviewStore.swift +113 -0
- package/app/Sources/Core/Desktop/WindowSelectionStore.swift +76 -0
- package/app/Sources/{WindowTiler.swift → Core/Desktop/WindowTiler.swift} +24 -108
- package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +127 -24
- package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +488 -55
- package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -0
- package/app/Sources/{HUDRightBar.swift → Core/Overlays/HUD/HUDRightBar.swift} +23 -201
- package/app/Sources/{LauncherHUD.swift → Core/Overlays/HUD/LauncherHUD.swift} +12 -26
- package/app/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +94 -0
- package/app/Sources/Core/Overlays/OverlayPanelShell.swift +241 -0
- package/app/Sources/{ScreenMapState.swift → Core/Overlays/ScreenMap/ScreenMapState.swift} +25 -1
- package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -74
- package/docs/component-extraction-roadmap.md +392 -0
- package/package.json +3 -1
- package/app/Sources/CommandPaletteWindow.swift +0 -134
- package/app/Sources/OmniSearchWindow.swift +0 -165
- /package/app/Sources/{App.swift → AppShell/App.swift} +0 -0
- /package/app/Sources/{AppUpdater.swift → AppShell/AppUpdater.swift} +0 -0
- /package/app/Sources/{CliActionLauncher.swift → AppShell/CliActionLauncher.swift} +0 -0
- /package/app/Sources/{HomeDashboardView.swift → AppShell/HomeDashboardView.swift} +0 -0
- /package/app/Sources/{KeyRecorderView.swift → AppShell/KeyRecorderView.swift} +0 -0
- /package/app/Sources/{LatticesRuntime.swift → AppShell/LatticesRuntime.swift} +0 -0
- /package/app/Sources/{MainView.swift → AppShell/MainView.swift} +0 -0
- /package/app/Sources/{MainWindow.swift → AppShell/MainWindow.swift} +0 -0
- /package/app/Sources/{OnboardingView.swift → AppShell/OnboardingView.swift} +0 -0
- /package/app/Sources/{Preferences.swift → AppShell/Preferences.swift} +0 -0
- /package/app/Sources/{SettingsView.swift → AppShell/SettingsView.swift} +0 -0
- /package/app/Sources/{SettingsWindow.swift → AppShell/SettingsWindow.swift} +0 -0
- /package/app/Sources/{HotkeyManager.swift → Core/Actions/HotkeyManager.swift} +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/CreateLayerIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/DistributeIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/FocusIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/HelpIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/KillIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/LaunchIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ListSessionsIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ListWindowsIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ScanIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/SearchIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/SwitchLayerIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/TileIntent.swift +0 -0
- /package/app/Sources/{PaletteCommand.swift → Core/Actions/PaletteCommand.swift} +0 -0
- /package/app/Sources/{CompanionActivityLog.swift → Core/Companion/CompanionActivityLog.swift} +0 -0
- /package/app/Sources/{CompanionKeyboardController.swift → Core/Companion/CompanionKeyboardController.swift} +0 -0
- /package/app/Sources/{LatticesCompanionBridgeServer.swift → Core/Companion/LatticesCompanionBridgeServer.swift} +0 -0
- /package/app/Sources/{LatticesCompanionCockpit.swift → Core/Companion/LatticesCompanionCockpit.swift} +0 -0
- /package/app/Sources/{LatticesCompanionSecurityCoordinator.swift → Core/Companion/LatticesCompanionSecurityCoordinator.swift} +0 -0
- /package/app/Sources/{LatticesCompanionTrackpadController.swift → Core/Companion/LatticesCompanionTrackpadController.swift} +0 -0
- /package/app/Sources/{LatticesDeckHost.swift → Core/Companion/LatticesDeckHost.swift} +0 -0
- /package/app/Sources/{DaemonProtocol.swift → Core/Daemon/DaemonProtocol.swift} +0 -0
- /package/app/Sources/{DaemonServer.swift → Core/Daemon/DaemonServer.swift} +0 -0
- /package/app/Sources/{LatticesApi.swift → Core/Daemon/LatticesApi.swift} +0 -0
- /package/app/Sources/{AccessibilityTextExtractor.swift → Core/Desktop/AccessibilityTextExtractor.swift} +0 -0
- /package/app/Sources/{AppTypeClassifier.swift → Core/Desktop/AppTypeClassifier.swift} +0 -0
- /package/app/Sources/{DesktopModelTypes.swift → Core/Desktop/DesktopModelTypes.swift} +0 -0
- /package/app/Sources/{InventoryManager.swift → Core/Desktop/InventoryManager.swift} +0 -0
- /package/app/Sources/{InventoryPath.swift → Core/Desktop/InventoryPath.swift} +0 -0
- /package/app/Sources/{MouseFinder.swift → Core/Desktop/MouseFinder.swift} +0 -0
- /package/app/Sources/{OcrModel.swift → Core/Desktop/OcrModel.swift} +0 -0
- /package/app/Sources/{OcrStore.swift → Core/Desktop/OcrStore.swift} +0 -0
- /package/app/Sources/{PlacementSpec.swift → Core/Desktop/PlacementSpec.swift} +0 -0
- /package/app/Sources/{TilePickerView.swift → Core/Desktop/TilePickerView.swift} +0 -0
- /package/app/Sources/{WindowDragSnapController.swift → Core/Desktop/WindowDragSnapController.swift} +0 -0
- /package/app/Sources/{MouseGestureConfig.swift → Core/Input/MouseGestureConfig.swift} +0 -0
- /package/app/Sources/{MouseGestureController.swift → Core/Input/MouseGestureController.swift} +0 -0
- /package/app/Sources/{MouseInputDeviceStore.swift → Core/Input/MouseInputDeviceStore.swift} +0 -0
- /package/app/Sources/{MouseInputEventViewer.swift → Core/Input/MouseInputEventViewer.swift} +0 -0
- /package/app/Sources/{MouseShortcutStore.swift → Core/Input/MouseShortcutStore.swift} +0 -0
- /package/app/Sources/{AppWindowShell.swift → Core/Overlays/AppWindowShell.swift} +0 -0
- /package/app/Sources/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +0 -0
- /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
- /package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.swift} +0 -0
- /package/app/Sources/{HUDBottomBar.swift → Core/Overlays/HUD/HUDBottomBar.swift} +0 -0
- /package/app/Sources/{HUDController.swift → Core/Overlays/HUD/HUDController.swift} +0 -0
- /package/app/Sources/{HUDLeftBar.swift → Core/Overlays/HUD/HUDLeftBar.swift} +0 -0
- /package/app/Sources/{HUDMinimap.swift → Core/Overlays/HUD/HUDMinimap.swift} +0 -0
- /package/app/Sources/{HUDState.swift → Core/Overlays/HUD/HUDState.swift} +0 -0
- /package/app/Sources/{HUDTopBar.swift → Core/Overlays/HUD/HUDTopBar.swift} +0 -0
- /package/app/Sources/{LayerBezel.swift → Core/Overlays/HUD/LayerBezel.swift} +0 -0
- /package/app/Sources/{OmniSearchState.swift → Core/Overlays/OmniSearch/OmniSearchState.swift} +0 -0
- /package/app/Sources/{OmniSearchView.swift → Core/Overlays/OmniSearch/OmniSearchView.swift} +0 -0
- /package/app/Sources/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.swift} +0 -0
- /package/app/Sources/{ScreenMapWindowController.swift → Core/Overlays/ScreenMap/ScreenMapWindowController.swift} +0 -0
- /package/app/Sources/{PiAuthNextStepCard.swift → Core/Pi/PiAuthNextStepCard.swift} +0 -0
- /package/app/Sources/{PiAuthPromptCard.swift → Core/Pi/PiAuthPromptCard.swift} +0 -0
- /package/app/Sources/{PiChatDock.swift → Core/Pi/PiChatDock.swift} +0 -0
- /package/app/Sources/{PiChatSession.swift → Core/Pi/PiChatSession.swift} +0 -0
- /package/app/Sources/{PiInstallCallout.swift → Core/Pi/PiInstallCallout.swift} +0 -0
- /package/app/Sources/{PiProviderSetupCallout.swift → Core/Pi/PiProviderSetupCallout.swift} +0 -0
- /package/app/Sources/{PiWorkspaceView.swift → Core/Pi/PiWorkspaceView.swift} +0 -0
- /package/app/Sources/{DiagnosticLog.swift → Core/System/DiagnosticLog.swift} +0 -0
- /package/app/Sources/{EventBus.swift → Core/System/EventBus.swift} +0 -0
- /package/app/Sources/{PermissionChecker.swift → Core/System/PermissionChecker.swift} +0 -0
- /package/app/Sources/{ProcessModel.swift → Core/System/ProcessModel.swift} +0 -0
- /package/app/Sources/{ProcessQuery.swift → Core/System/ProcessQuery.swift} +0 -0
- /package/app/Sources/{SystemTelemetryMonitor.swift → Core/System/SystemTelemetryMonitor.swift} +0 -0
- /package/app/Sources/{AdvisorLearningStore.swift → Core/Voice/AdvisorLearningStore.swift} +0 -0
- /package/app/Sources/{AgentSession.swift → Core/Voice/AgentSession.swift} +0 -0
- /package/app/Sources/{AudioProvider.swift → Core/Voice/AudioProvider.swift} +0 -0
- /package/app/Sources/{HandsOffSession.swift → Core/Voice/HandsOffSession.swift} +0 -0
- /package/app/Sources/{VoiceChatView.swift → Core/Voice/VoiceChatView.swift} +0 -0
- /package/app/Sources/{VoxClient.swift → Core/Voice/VoxClient.swift} +0 -0
- /package/app/Sources/{Project.swift → Core/Workspace/Project.swift} +0 -0
- /package/app/Sources/{ProjectScanner.swift → Core/Workspace/ProjectScanner.swift} +0 -0
- /package/app/Sources/{SessionLayerStore.swift → Core/Workspace/SessionLayerStore.swift} +0 -0
- /package/app/Sources/{SessionManager.swift → Core/Workspace/SessionManager.swift} +0 -0
- /package/app/Sources/{Terminal.swift → Core/Workspace/Terminal/Terminal.swift} +0 -0
- /package/app/Sources/{TerminalQuery.swift → Core/Workspace/Terminal/TerminalQuery.swift} +0 -0
- /package/app/Sources/{TerminalSynthesizer.swift → Core/Workspace/Terminal/TerminalSynthesizer.swift} +0 -0
- /package/app/Sources/{TmuxModel.swift → Core/Workspace/Tmux/TmuxModel.swift} +0 -0
- /package/app/Sources/{TmuxQuery.swift → Core/Workspace/Tmux/TmuxQuery.swift} +0 -0
- /package/app/Sources/{WorkspaceManager.swift → Core/Workspace/WorkspaceManager.swift} +0 -0
- /package/app/Sources/{ActionRow.swift → UI/ActionRow.swift} +0 -0
- /package/app/Sources/{OrphanRow.swift → UI/OrphanRow.swift} +0 -0
- /package/app/Sources/{ProjectRow.swift → UI/ProjectRow.swift} +0 -0
- /package/app/Sources/{TabGroupRow.swift → UI/TabGroupRow.swift} +0 -0
- /package/app/Sources/{Theme.swift → UI/Theme.swift} +0 -0
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.6</string>
|
|
19
19
|
<key>CFBundleShortVersionString</key>
|
|
20
|
-
<string>0.4.
|
|
20
|
+
<string>0.4.6</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.6</string>
|
|
19
19
|
<key>CFBundleShortVersionString</key>
|
|
20
|
-
<string>0.4.
|
|
20
|
+
<string>0.4.6</string>
|
|
21
21
|
<key>LSMinimumSystemVersion</key>
|
|
22
22
|
<string>13.0</string>
|
|
23
23
|
<key>LSUIElement</key>
|
|
Binary file
|
|
@@ -91,6 +91,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
91
91
|
store.register(action: .unifiedWindow) { ScreenMapWindowController.shared.toggle() }
|
|
92
92
|
store.register(action: .bezel) { Self.showWorkspaceInspector() }
|
|
93
93
|
store.register(action: .cheatSheet) { SettingsWindowController.shared.show() }
|
|
94
|
+
store.register(action: .desktopInventory) {
|
|
95
|
+
DiagnosticLog.shared.info("Hotkey: desktopInventory triggered")
|
|
96
|
+
ScreenMapWindowController.shared.showPage(.desktopInventory)
|
|
97
|
+
}
|
|
94
98
|
store.register(action: .voiceCommand) {
|
|
95
99
|
DiagnosticLog.shared.info("Hotkey: voiceCommand triggered")
|
|
96
100
|
VoiceCommandWindow.shared.toggle()
|
|
@@ -56,6 +56,10 @@ struct AppShellView: View {
|
|
|
56
56
|
.background(Palette.bg)
|
|
57
57
|
.onAppear {
|
|
58
58
|
commandState.onDismiss = { windowController.activePage = .home }
|
|
59
|
+
syncPageState(windowController.activePage)
|
|
60
|
+
}
|
|
61
|
+
.onChange(of: windowController.activePage) { page in
|
|
62
|
+
syncPageState(page)
|
|
59
63
|
}
|
|
60
64
|
}
|
|
61
65
|
|
|
@@ -116,7 +120,7 @@ struct AppShellView: View {
|
|
|
116
120
|
windowController.activePage = page
|
|
117
121
|
})
|
|
118
122
|
case .desktopInventory:
|
|
119
|
-
CommandModeView(state: commandState)
|
|
123
|
+
CommandModeView(state: commandState, presentation: .embedded)
|
|
120
124
|
case .pi:
|
|
121
125
|
PiWorkspaceView()
|
|
122
126
|
case .settings:
|
|
@@ -134,4 +138,9 @@ struct AppShellView: View {
|
|
|
134
138
|
)
|
|
135
139
|
}
|
|
136
140
|
}
|
|
141
|
+
|
|
142
|
+
private func syncPageState(_ page: AppPage) {
|
|
143
|
+
if page == .screenMap { controller.enter() }
|
|
144
|
+
if page == .desktopInventory { commandState.enter() }
|
|
145
|
+
}
|
|
137
146
|
}
|
|
@@ -40,7 +40,7 @@ enum HotkeyAction: String, CaseIterable, Codable {
|
|
|
40
40
|
case .screenMap: return "Screen Map"
|
|
41
41
|
case .bezel: return "Window Bezel"
|
|
42
42
|
case .cheatSheet: return "Cheat Sheet"
|
|
43
|
-
case .desktopInventory: return "
|
|
43
|
+
case .desktopInventory: return "Window Selector"
|
|
44
44
|
case .omniSearch: return "Search"
|
|
45
45
|
case .voiceCommand: return "Voice Command"
|
|
46
46
|
case .handsOff: return "Hands-Off Mode"
|
|
@@ -231,6 +231,7 @@ class HotkeyStore: ObservableObject {
|
|
|
231
231
|
bind(.unifiedWindow, 18, hyper) // Hyper+1 (Workspace Home)
|
|
232
232
|
bind(.bezel, 19, hyper) // Hyper+2
|
|
233
233
|
bind(.hud, 20, hyper) // Hyper+3 (HUD overlay)
|
|
234
|
+
bind(.desktopInventory, 5, hyper) // Hyper+G
|
|
234
235
|
bind(.voiceCommand, 21, hyper) // Hyper+4 (moved from Hyper+3)
|
|
235
236
|
let cmdCtrl = UInt32(cmdKey | controlKey)
|
|
236
237
|
bind(.handsOff, 46, cmdCtrl) // Ctrl+Cmd+M
|
|
@@ -1,31 +1,5 @@
|
|
|
1
1
|
import AppKit
|
|
2
2
|
|
|
3
|
-
// MARK: - Intent Definition
|
|
4
|
-
|
|
5
|
-
struct IntentDef {
|
|
6
|
-
let name: String
|
|
7
|
-
let description: String
|
|
8
|
-
let examples: [String] // Example phrases that map to this intent
|
|
9
|
-
let slots: [IntentSlot] // Named parameters extracted from the utterance
|
|
10
|
-
let handler: (IntentRequest) throws -> JSON
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
struct IntentSlot {
|
|
14
|
-
let name: String
|
|
15
|
-
let type: String // "string", "int", "position", "query"
|
|
16
|
-
let required: Bool
|
|
17
|
-
let description: String
|
|
18
|
-
let enumValues: [String]? // For constrained slots like tile positions
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
struct IntentRequest {
|
|
22
|
-
let intent: String
|
|
23
|
-
let slots: [String: JSON]
|
|
24
|
-
let rawText: String? // Original transcription, for fallback matching
|
|
25
|
-
let confidence: Double? // Transcription confidence from voice service
|
|
26
|
-
let source: String? // "vox", "siri", "cli", etc.
|
|
27
|
-
}
|
|
28
|
-
|
|
29
3
|
// MARK: - Intent Engine
|
|
30
4
|
|
|
31
5
|
final class IntentEngine {
|
|
@@ -133,6 +107,8 @@ final class IntentEngine {
|
|
|
133
107
|
description: "Target window ID", enumValues: nil),
|
|
134
108
|
IntentSlot(name: "session", type: "string", required: false,
|
|
135
109
|
description: "Target session name", enumValues: nil),
|
|
110
|
+
IntentSlot(name: "selection", type: "bool", required: false,
|
|
111
|
+
description: "Apply to the active multi-window selection instead of a single window", enumValues: nil),
|
|
136
112
|
],
|
|
137
113
|
handler: { req in
|
|
138
114
|
guard let posStr = req.slots["position"]?.stringValue else {
|
|
@@ -176,6 +152,34 @@ final class IntentEngine {
|
|
|
176
152
|
throw IntentError.targetNotFound("No window found for app '\(app)'")
|
|
177
153
|
}
|
|
178
154
|
|
|
155
|
+
if req.slots["selection"]?.boolValue == true {
|
|
156
|
+
let selectionIds = WindowSelectionStore.shared.windowIds
|
|
157
|
+
guard !selectionIds.isEmpty else {
|
|
158
|
+
throw IntentError.targetNotFound("No active window selection")
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if selectionIds.count == 1,
|
|
162
|
+
let wid = selectionIds.first,
|
|
163
|
+
let entry = DesktopModel.shared.windows[wid] {
|
|
164
|
+
tileEntry(entry)
|
|
165
|
+
return .object([
|
|
166
|
+
"ok": .bool(true),
|
|
167
|
+
"target": .string("selection"),
|
|
168
|
+
"wid": .int(Int(wid)),
|
|
169
|
+
"position": .string(posStr)
|
|
170
|
+
])
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return try LatticesApi.shared.dispatch(
|
|
174
|
+
method: "space.optimize",
|
|
175
|
+
params: .object([
|
|
176
|
+
"scope": .string("selection"),
|
|
177
|
+
"windowIds": .array(selectionIds.map { .int(Int($0)) }),
|
|
178
|
+
"region": .string(posStr)
|
|
179
|
+
])
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
179
183
|
// Default: tile frontmost window
|
|
180
184
|
DispatchQueue.main.async {
|
|
181
185
|
WindowTiler.tileFrontmostViaAX(to: placement)
|
|
@@ -401,6 +405,8 @@ final class IntentEngine {
|
|
|
401
405
|
description: "Constrain the grid to a screen region. Uses tile position names.",
|
|
402
406
|
enumValues: ["left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right",
|
|
403
407
|
"left-third", "center-third", "right-third"]),
|
|
408
|
+
IntentSlot(name: "selection", type: "bool", required: false,
|
|
409
|
+
description: "Use the active selected windows instead of all visible windows", enumValues: nil),
|
|
404
410
|
],
|
|
405
411
|
handler: { req in
|
|
406
412
|
var params: [String: JSON] = [:]
|
|
@@ -413,6 +419,18 @@ final class IntentEngine {
|
|
|
413
419
|
if let region = req.slots["region"]?.stringValue {
|
|
414
420
|
params["region"] = .string(region)
|
|
415
421
|
}
|
|
422
|
+
if req.slots["selection"]?.boolValue == true {
|
|
423
|
+
let selectionIds = WindowSelectionStore.shared.windowIds
|
|
424
|
+
guard !selectionIds.isEmpty else {
|
|
425
|
+
throw IntentError.targetNotFound("No active window selection")
|
|
426
|
+
}
|
|
427
|
+
params["scope"] = .string("selection")
|
|
428
|
+
params["windowIds"] = .array(selectionIds.map { .int(Int($0)) })
|
|
429
|
+
return try LatticesApi.shared.dispatch(
|
|
430
|
+
method: "space.optimize",
|
|
431
|
+
params: .object(params)
|
|
432
|
+
)
|
|
433
|
+
}
|
|
416
434
|
return try LatticesApi.shared.dispatch(
|
|
417
435
|
method: "layout.distribute",
|
|
418
436
|
params: params.isEmpty ? nil : .object(params)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
struct IntentDef {
|
|
4
|
+
let name: String
|
|
5
|
+
let description: String
|
|
6
|
+
let examples: [String]
|
|
7
|
+
let slots: [IntentSlot]
|
|
8
|
+
let handler: (IntentRequest) throws -> JSON
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
struct IntentSlot {
|
|
12
|
+
let name: String
|
|
13
|
+
let type: String
|
|
14
|
+
let required: Bool
|
|
15
|
+
let description: String
|
|
16
|
+
let enumValues: [String]?
|
|
17
|
+
let defaultValue: JSON?
|
|
18
|
+
|
|
19
|
+
init(
|
|
20
|
+
name: String,
|
|
21
|
+
type: String,
|
|
22
|
+
required: Bool,
|
|
23
|
+
description: String,
|
|
24
|
+
enumValues: [String]? = nil,
|
|
25
|
+
defaultValue: JSON? = nil
|
|
26
|
+
) {
|
|
27
|
+
self.name = name
|
|
28
|
+
self.type = type
|
|
29
|
+
self.required = required
|
|
30
|
+
self.description = description
|
|
31
|
+
self.enumValues = enumValues
|
|
32
|
+
self.defaultValue = defaultValue
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
struct IntentRequest {
|
|
37
|
+
let intent: String
|
|
38
|
+
let slots: [String: JSON]
|
|
39
|
+
let rawText: String?
|
|
40
|
+
let confidence: Double?
|
|
41
|
+
let source: String?
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
enum SlotType {
|
|
45
|
+
case string
|
|
46
|
+
case int
|
|
47
|
+
case bool
|
|
48
|
+
case position
|
|
49
|
+
case query
|
|
50
|
+
case app
|
|
51
|
+
case session
|
|
52
|
+
case layer
|
|
53
|
+
case enumerated([String])
|
|
54
|
+
|
|
55
|
+
var typeLabel: String {
|
|
56
|
+
switch self {
|
|
57
|
+
case .string: return "string"
|
|
58
|
+
case .int: return "int"
|
|
59
|
+
case .bool: return "bool"
|
|
60
|
+
case .position: return "position"
|
|
61
|
+
case .query: return "query"
|
|
62
|
+
case .app: return "app"
|
|
63
|
+
case .session: return "session"
|
|
64
|
+
case .layer: return "layer"
|
|
65
|
+
case .enumerated: return "string"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
var enumValues: [String]? {
|
|
70
|
+
guard case .enumerated(let values) = self else { return nil }
|
|
71
|
+
return values
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
typealias SlotDef = IntentSlot
|
|
76
|
+
|
|
77
|
+
extension IntentSlot {
|
|
78
|
+
init(
|
|
79
|
+
name: String,
|
|
80
|
+
type: SlotType,
|
|
81
|
+
required: Bool = true,
|
|
82
|
+
description: String = "",
|
|
83
|
+
defaultValue: JSON? = nil
|
|
84
|
+
) {
|
|
85
|
+
self.init(
|
|
86
|
+
name: name,
|
|
87
|
+
type: type.typeLabel,
|
|
88
|
+
required: required,
|
|
89
|
+
description: description,
|
|
90
|
+
enumValues: type.enumValues,
|
|
91
|
+
defaultValue: defaultValue
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -12,31 +12,6 @@ protocol LatticeIntent {
|
|
|
12
12
|
func perform(slots: [String: JSON]) throws -> JSON
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
// MARK: - Slot Definition
|
|
16
|
-
|
|
17
|
-
enum SlotType {
|
|
18
|
-
case string // Free-form text
|
|
19
|
-
case position // Tile position (left, right, maximize, etc.)
|
|
20
|
-
case app // Running app name
|
|
21
|
-
case session // Active tmux session
|
|
22
|
-
case layer // Layer name
|
|
23
|
-
case enumerated([String]) // Fixed set of values
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
struct SlotDef {
|
|
27
|
-
let name: String
|
|
28
|
-
let type: SlotType
|
|
29
|
-
let required: Bool
|
|
30
|
-
let defaultValue: JSON?
|
|
31
|
-
|
|
32
|
-
init(name: String, type: SlotType, required: Bool = true, defaultValue: JSON? = nil) {
|
|
33
|
-
self.name = name
|
|
34
|
-
self.type = type
|
|
35
|
-
self.required = required
|
|
36
|
-
self.defaultValue = defaultValue
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
15
|
// MARK: - Compiled Phrase Template
|
|
41
16
|
|
|
42
17
|
struct CompiledPhrase {
|
|
@@ -141,10 +141,43 @@ final class VoiceIntentResolver {
|
|
|
141
141
|
private func extractSlots(for intentName: String, input: String) -> ExtractedSlots {
|
|
142
142
|
switch intentName {
|
|
143
143
|
case "tile_window":
|
|
144
|
-
|
|
144
|
+
var slots: [String: JSON] = [:]
|
|
145
|
+
var boost = 0.0
|
|
146
|
+
|
|
147
|
+
if let position = resolvePosition(in: input) {
|
|
148
|
+
slots["position"] = .string(position)
|
|
149
|
+
boost += 0.28
|
|
150
|
+
} else {
|
|
145
151
|
return ExtractedSlots(slots: [:], boost: 0)
|
|
146
152
|
}
|
|
147
|
-
|
|
153
|
+
|
|
154
|
+
if refersToSelection(in: input) {
|
|
155
|
+
slots["selection"] = .bool(true)
|
|
156
|
+
boost += 0.08
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return ExtractedSlots(slots: slots, boost: boost)
|
|
160
|
+
|
|
161
|
+
case "distribute":
|
|
162
|
+
var slots: [String: JSON] = [:]
|
|
163
|
+
var boost = 0.0
|
|
164
|
+
|
|
165
|
+
if let region = resolvePosition(in: input) {
|
|
166
|
+
slots["region"] = .string(region)
|
|
167
|
+
boost += 0.18
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if let app = detectKnownApp(in: input) {
|
|
171
|
+
slots["app"] = .string(app)
|
|
172
|
+
boost += 0.14
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if refersToSelection(in: input) {
|
|
176
|
+
slots["selection"] = .bool(true)
|
|
177
|
+
boost += 0.12
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return ExtractedSlots(slots: slots, boost: boost)
|
|
148
181
|
|
|
149
182
|
case "focus":
|
|
150
183
|
if let app = detectKnownApp(in: input) ?? extractEntity(in: input, prefixes: focusPrefixes) {
|
|
@@ -426,6 +459,15 @@ final class VoiceIntentResolver {
|
|
|
426
459
|
return nil
|
|
427
460
|
}
|
|
428
461
|
|
|
462
|
+
private func refersToSelection(in input: String) -> Bool {
|
|
463
|
+
let markers = [
|
|
464
|
+
"grid that", "grid these", "grid those",
|
|
465
|
+
"tile that", "tile these", "tile those",
|
|
466
|
+
"selected windows", "selection", "selected", "these windows", "those windows", "them"
|
|
467
|
+
]
|
|
468
|
+
return markers.contains(where: input.contains)
|
|
469
|
+
}
|
|
470
|
+
|
|
429
471
|
private func detectKnownApp(in input: String) -> String? {
|
|
430
472
|
for app in knownApps() {
|
|
431
473
|
let lower = app.lowercased()
|
|
@@ -601,7 +643,7 @@ final class VoiceIntentResolver {
|
|
|
601
643
|
"search": ["find", "search", "look for", "where is", "where d", "locate", "lost", "show me all", "windows"],
|
|
602
644
|
"list_windows": ["what s open", "list windows", "which windows", "what do i have open"],
|
|
603
645
|
"list_sessions": ["list sessions", "what s running", "which projects", "show my sessions"],
|
|
604
|
-
"distribute": ["distribute", "spread", "organize", "arrange", "tidy", "clean up", "grid"],
|
|
646
|
+
"distribute": ["distribute", "spread", "organize", "arrange", "tidy", "clean up", "grid", "selected", "selection"],
|
|
605
647
|
"create_layer": ["create layer", "save layout", "snapshot", "remember this layout"],
|
|
606
648
|
"kill": ["kill", "stop", "shut down", "close", "terminate", "end"],
|
|
607
649
|
"scan": ["scan", "rescan", "ocr", "read the screen", "what s on my screen", "screen text"],
|
|
@@ -662,7 +704,7 @@ final class VoiceIntentResolver {
|
|
|
662
704
|
"search": ["where d my slack go", "pull up everything with dewey in it", "show me all the chrome windows", "dewey"],
|
|
663
705
|
"list_windows": ["what do i have open", "what windows do i have"],
|
|
664
706
|
"list_sessions": ["show me my sessions", "which projects are active"],
|
|
665
|
-
"distribute": ["tidy up", "line everything up", "clean up the windows"],
|
|
707
|
+
"distribute": ["tidy up", "line everything up", "clean up the windows", "grid that in the bottom half", "arrange the selected windows"],
|
|
666
708
|
"create_layer": ["snapshot this", "remember this layout"],
|
|
667
709
|
"kill": ["close the dewey session", "stop my session"],
|
|
668
710
|
"scan": ["what s on my screen", "read the screen", "give me a fresh scan"],
|
|
@@ -106,8 +106,7 @@ final class DesktopModel: ObservableObject {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
func windowForSession(_ session: String) -> WindowEntry? {
|
|
109
|
-
|
|
110
|
-
return windows.values.first { $0.title.contains(tag) }
|
|
109
|
+
SessionWindowLocator.cachedWindow(forSession: session, in: windows)
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
/// Assign a layer tag to a window (in-memory only)
|
|
@@ -205,12 +204,7 @@ final class DesktopModel: ObservableObject {
|
|
|
205
204
|
|
|
206
205
|
let spaceIds = WindowTiler.getSpacesForWindow(wid)
|
|
207
206
|
|
|
208
|
-
|
|
209
|
-
var latticesSession: String?
|
|
210
|
-
if let range = title.range(of: #"\[lattices:([^\]]+)\]"#, options: .regularExpression) {
|
|
211
|
-
let match = String(title[range])
|
|
212
|
-
latticesSession = String(match.dropFirst(9).dropLast(1)) // drop "[lattices:" and "]"
|
|
213
|
-
}
|
|
207
|
+
let latticesSession = SessionWindowLocator.extractSessionName(from: title)
|
|
214
208
|
|
|
215
209
|
var entry = WindowEntry(
|
|
216
210
|
wid: wid,
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import ApplicationServices
|
|
3
|
+
import CoreGraphics
|
|
4
|
+
|
|
5
|
+
struct LocatedWindow {
|
|
6
|
+
let wid: UInt32
|
|
7
|
+
let pid: pid_t
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
enum SessionWindowLocator {
|
|
11
|
+
static func tag(for session: String) -> String {
|
|
12
|
+
Terminal.windowTag(for: session)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static func extractSessionName(from title: String) -> String? {
|
|
16
|
+
guard let range = title.range(of: #"\[lattices:([^\]]+)\]"#, options: .regularExpression) else {
|
|
17
|
+
return nil
|
|
18
|
+
}
|
|
19
|
+
let match = String(title[range])
|
|
20
|
+
return String(match.dropFirst(10).dropLast(1))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static func matches(session: String, title: String, extractedSessionName: String? = nil) -> Bool {
|
|
24
|
+
if extractedSessionName == session {
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
return title.contains(tag(for: session))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static func cachedWindow(forSession session: String, in windows: [UInt32: WindowEntry]) -> WindowEntry? {
|
|
31
|
+
windows.values.first { entry in
|
|
32
|
+
matches(session: session, title: entry.title, extractedSessionName: entry.latticesSession)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static func cachedWindow(forSession session: String, desktopModel: DesktopModel = .shared) -> WindowEntry? {
|
|
37
|
+
cachedWindow(forSession: session, in: desktopModel.windows)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static func findCGWindow(tag: String) -> LocatedWindow? {
|
|
41
|
+
guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else {
|
|
42
|
+
return nil
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for info in windowList {
|
|
46
|
+
if let name = info[kCGWindowName as String] as? String,
|
|
47
|
+
name.contains(tag),
|
|
48
|
+
let wid = info[kCGWindowNumber as String] as? UInt32,
|
|
49
|
+
let pid = info[kCGWindowOwnerPID as String] as? pid_t {
|
|
50
|
+
return LocatedWindow(wid: wid, pid: pid)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return nil
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static func findWindow(session: String, terminal: Terminal) -> LocatedWindow? {
|
|
57
|
+
findWindow(tag: tag(for: session), terminal: terminal)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static func findWindow(tag: String, terminal: Terminal) -> LocatedWindow? {
|
|
61
|
+
if let match = findCGWindow(tag: tag) {
|
|
62
|
+
return match
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if let ax = findAXWindow(terminal: terminal, tag: tag),
|
|
66
|
+
let wid = matchCGWindow(pid: ax.pid, axWindow: ax.window) {
|
|
67
|
+
return LocatedWindow(wid: wid, pid: ax.pid)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return nil
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static func findAXWindow(terminal: Terminal, tag: String) -> (pid: pid_t, window: AXUIElement)? {
|
|
74
|
+
let diag = DiagnosticLog.shared
|
|
75
|
+
guard let app = NSWorkspace.shared.runningApplications.first(where: {
|
|
76
|
+
$0.bundleIdentifier == terminal.bundleId
|
|
77
|
+
}) else {
|
|
78
|
+
diag.error("SessionWindowLocator.findAXWindow: \(terminal.rawValue) (\(terminal.bundleId)) not running")
|
|
79
|
+
return nil
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let pid = app.processIdentifier
|
|
83
|
+
let appRef = AXUIElementCreateApplication(pid)
|
|
84
|
+
var windowsRef: CFTypeRef?
|
|
85
|
+
let err = AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &windowsRef)
|
|
86
|
+
guard err == .success, let windows = windowsRef as? [AXUIElement] else {
|
|
87
|
+
diag.error("SessionWindowLocator.findAXWindow: AX error \(err.rawValue) — Accessibility not granted?")
|
|
88
|
+
return nil
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
diag.info("SessionWindowLocator.findAXWindow: \(windows.count) windows for \(terminal.rawValue), searching for \(tag)")
|
|
92
|
+
for win in windows {
|
|
93
|
+
var titleRef: CFTypeRef?
|
|
94
|
+
AXUIElementCopyAttributeValue(win, kAXTitleAttribute as CFString, &titleRef)
|
|
95
|
+
let title = titleRef as? String ?? "<no title>"
|
|
96
|
+
if title.contains(tag) {
|
|
97
|
+
diag.success("SessionWindowLocator.findAXWindow: matched \"\(title)\"")
|
|
98
|
+
return (pid, win)
|
|
99
|
+
} else {
|
|
100
|
+
diag.info(" skip: \"\(title)\"")
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
diag.warn("SessionWindowLocator.findAXWindow: no window matched tag \(tag)")
|
|
105
|
+
return nil
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static func matchCGWindow(pid: pid_t, axWindow: AXUIElement) -> UInt32? {
|
|
109
|
+
var posRef: CFTypeRef?
|
|
110
|
+
var sizeRef: CFTypeRef?
|
|
111
|
+
AXUIElementCopyAttributeValue(axWindow, kAXPositionAttribute as CFString, &posRef)
|
|
112
|
+
AXUIElementCopyAttributeValue(axWindow, kAXSizeAttribute as CFString, &sizeRef)
|
|
113
|
+
guard let pv = posRef, let sv = sizeRef else { return nil }
|
|
114
|
+
|
|
115
|
+
var pos = CGPoint.zero
|
|
116
|
+
var size = CGSize.zero
|
|
117
|
+
AXValueGetValue(pv as! AXValue, .cgPoint, &pos)
|
|
118
|
+
AXValueGetValue(sv as! AXValue, .cgSize, &size)
|
|
119
|
+
|
|
120
|
+
guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else {
|
|
121
|
+
return nil
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for info in windowList {
|
|
125
|
+
guard let wPid = info[kCGWindowOwnerPID as String] as? pid_t,
|
|
126
|
+
wPid == pid,
|
|
127
|
+
let wid = info[kCGWindowNumber as String] as? UInt32,
|
|
128
|
+
let boundsDict = info[kCGWindowBounds as String] as? NSDictionary else { continue }
|
|
129
|
+
var rect = CGRect.zero
|
|
130
|
+
if CGRectMakeWithDictionaryRepresentation(boundsDict, &rect) {
|
|
131
|
+
if abs(rect.origin.x - pos.x) < 2 && abs(rect.origin.y - pos.y) < 2 &&
|
|
132
|
+
abs(rect.width - size.width) < 2 && abs(rect.height - size.height) < 2 {
|
|
133
|
+
return wid
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return nil
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
struct WindowPreviewCardStyle {
|
|
5
|
+
var containerCornerRadius: CGFloat = 10
|
|
6
|
+
var imageCornerRadius: CGFloat = 8
|
|
7
|
+
var imagePadding: CGFloat = 8
|
|
8
|
+
var background: Color = Palette.surface.opacity(0.8)
|
|
9
|
+
var border: Color = Palette.border
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
struct WindowPreviewCard<Overlay: View>: View {
|
|
13
|
+
let image: NSImage?
|
|
14
|
+
let isLoading: Bool
|
|
15
|
+
let appName: String
|
|
16
|
+
var loadingTitle: String = "Capturing preview"
|
|
17
|
+
var unavailableTitle: String = "Preview unavailable"
|
|
18
|
+
var style: WindowPreviewCardStyle = WindowPreviewCardStyle()
|
|
19
|
+
var holdingPreviousPreview: Bool = false
|
|
20
|
+
@ViewBuilder let overlay: () -> Overlay
|
|
21
|
+
|
|
22
|
+
var body: some View {
|
|
23
|
+
ZStack {
|
|
24
|
+
RoundedRectangle(cornerRadius: style.containerCornerRadius)
|
|
25
|
+
.fill(style.background)
|
|
26
|
+
.overlay(
|
|
27
|
+
RoundedRectangle(cornerRadius: style.containerCornerRadius)
|
|
28
|
+
.strokeBorder(style.border, lineWidth: 0.5)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if let image {
|
|
32
|
+
Image(nsImage: image)
|
|
33
|
+
.resizable()
|
|
34
|
+
.aspectRatio(contentMode: .fit)
|
|
35
|
+
.clipShape(RoundedRectangle(cornerRadius: style.imageCornerRadius))
|
|
36
|
+
.padding(style.imagePadding)
|
|
37
|
+
.opacity(holdingPreviousPreview ? 0.88 : 1)
|
|
38
|
+
} else if isLoading {
|
|
39
|
+
WindowPreviewPlaceholder(
|
|
40
|
+
icon: "photo",
|
|
41
|
+
title: loadingTitle,
|
|
42
|
+
subtitle: appName
|
|
43
|
+
)
|
|
44
|
+
} else {
|
|
45
|
+
WindowPreviewPlaceholder(
|
|
46
|
+
icon: "eye.slash",
|
|
47
|
+
title: unavailableTitle,
|
|
48
|
+
subtitle: appName
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
overlay()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
extension WindowPreviewCard where Overlay == EmptyView {
|
|
58
|
+
init(
|
|
59
|
+
image: NSImage?,
|
|
60
|
+
isLoading: Bool,
|
|
61
|
+
appName: String,
|
|
62
|
+
loadingTitle: String = "Capturing preview",
|
|
63
|
+
unavailableTitle: String = "Preview unavailable",
|
|
64
|
+
style: WindowPreviewCardStyle = WindowPreviewCardStyle(),
|
|
65
|
+
holdingPreviousPreview: Bool = false
|
|
66
|
+
) {
|
|
67
|
+
self.init(
|
|
68
|
+
image: image,
|
|
69
|
+
isLoading: isLoading,
|
|
70
|
+
appName: appName,
|
|
71
|
+
loadingTitle: loadingTitle,
|
|
72
|
+
unavailableTitle: unavailableTitle,
|
|
73
|
+
style: style,
|
|
74
|
+
holdingPreviousPreview: holdingPreviousPreview,
|
|
75
|
+
overlay: { EmptyView() }
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private struct WindowPreviewPlaceholder: View {
|
|
81
|
+
let icon: String
|
|
82
|
+
let title: String
|
|
83
|
+
let subtitle: String
|
|
84
|
+
|
|
85
|
+
var body: some View {
|
|
86
|
+
VStack(spacing: 8) {
|
|
87
|
+
Image(systemName: icon)
|
|
88
|
+
.font(.system(size: 18, weight: .medium))
|
|
89
|
+
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
90
|
+
Text(title)
|
|
91
|
+
.font(Typo.monoBold(10))
|
|
92
|
+
.foregroundColor(Palette.textMuted)
|
|
93
|
+
Text(subtitle)
|
|
94
|
+
.font(Typo.mono(9))
|
|
95
|
+
.foregroundColor(Palette.textDim)
|
|
96
|
+
.lineLimit(1)
|
|
97
|
+
}
|
|
98
|
+
.padding(16)
|
|
99
|
+
}
|
|
100
|
+
}
|