@lattices/cli 0.3.0 → 0.4.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.
- package/README.md +85 -9
- package/app/Info.plist +30 -0
- package/app/Lattices.app/Contents/Info.plist +8 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
- package/app/Lattices.app/Contents/Resources/tap.wav +0 -0
- package/app/Lattices.app/Contents/_CodeSignature/CodeResources +139 -0
- package/app/Lattices.entitlements +15 -0
- package/app/Package.swift +8 -1
- package/app/Resources/tap.wav +0 -0
- package/app/Sources/AdvisorLearningStore.swift +90 -0
- package/app/Sources/AgentSession.swift +377 -0
- package/app/Sources/AppDelegate.swift +45 -12
- package/app/Sources/AppShellView.swift +81 -8
- package/app/Sources/AudioProvider.swift +386 -0
- package/app/Sources/CheatSheetHUD.swift +261 -19
- package/app/Sources/DaemonProtocol.swift +13 -0
- package/app/Sources/DaemonServer.swift +8 -0
- package/app/Sources/DesktopModel.swift +189 -6
- package/app/Sources/DesktopModelTypes.swift +2 -0
- package/app/Sources/DiagnosticLog.swift +104 -2
- package/app/Sources/EventBus.swift +1 -0
- package/app/Sources/HUDBottomBar.swift +279 -0
- package/app/Sources/HUDController.swift +1158 -0
- package/app/Sources/HUDLeftBar.swift +849 -0
- package/app/Sources/HUDMinimap.swift +179 -0
- package/app/Sources/HUDRightBar.swift +774 -0
- package/app/Sources/HUDState.swift +367 -0
- package/app/Sources/HUDTopBar.swift +243 -0
- package/app/Sources/HandsOffSession.swift +802 -0
- package/app/Sources/HomeDashboardView.swift +125 -0
- package/app/Sources/HotkeyManager.swift +2 -0
- package/app/Sources/HotkeyStore.swift +49 -9
- package/app/Sources/IntentEngine.swift +962 -0
- package/app/Sources/Intents/CreateLayerIntent.swift +54 -0
- package/app/Sources/Intents/DistributeIntent.swift +56 -0
- package/app/Sources/Intents/FocusIntent.swift +69 -0
- package/app/Sources/Intents/HelpIntent.swift +41 -0
- package/app/Sources/Intents/KillIntent.swift +47 -0
- package/app/Sources/Intents/LatticeIntent.swift +78 -0
- package/app/Sources/Intents/LaunchIntent.swift +67 -0
- package/app/Sources/Intents/ListSessionsIntent.swift +32 -0
- package/app/Sources/Intents/ListWindowsIntent.swift +30 -0
- package/app/Sources/Intents/ScanIntent.swift +52 -0
- package/app/Sources/Intents/SearchIntent.swift +190 -0
- package/app/Sources/Intents/SwitchLayerIntent.swift +50 -0
- package/app/Sources/Intents/TileIntent.swift +61 -0
- package/app/Sources/LatticesApi.swift +1275 -30
- package/app/Sources/LauncherHUD.swift +348 -0
- package/app/Sources/MainView.swift +147 -44
- package/app/Sources/MouseFinder.swift +222 -0
- package/app/Sources/OcrModel.swift +34 -1
- package/app/Sources/OmniSearchState.swift +99 -102
- package/app/Sources/OnboardingView.swift +457 -0
- package/app/Sources/PermissionChecker.swift +2 -12
- package/app/Sources/PiChatDock.swift +454 -0
- package/app/Sources/PiChatSession.swift +815 -0
- package/app/Sources/PiWorkspaceView.swift +364 -0
- package/app/Sources/PlacementSpec.swift +195 -0
- package/app/Sources/Preferences.swift +59 -0
- package/app/Sources/ProjectScanner.swift +58 -45
- package/app/Sources/ScreenMapState.swift +701 -55
- package/app/Sources/ScreenMapView.swift +843 -103
- package/app/Sources/ScreenMapWindowController.swift +22 -0
- package/app/Sources/SessionLayerStore.swift +285 -0
- package/app/Sources/SessionManager.swift +4 -1
- package/app/Sources/SettingsView.swift +186 -3
- package/app/Sources/Theme.swift +9 -8
- package/app/Sources/TmuxModel.swift +7 -0
- package/app/Sources/TmuxQuery.swift +27 -3
- package/app/Sources/VoiceChatView.swift +192 -0
- package/app/Sources/VoiceCommandWindow.swift +1594 -0
- package/app/Sources/VoiceIntentResolver.swift +671 -0
- package/app/Sources/VoxClient.swift +454 -0
- package/app/Sources/WindowTiler.swift +348 -87
- package/app/Sources/WorkspaceManager.swift +127 -18
- package/app/Tests/StageDragTests.swift +333 -0
- package/app/Tests/StageJoinTests.swift +313 -0
- package/app/Tests/StageManagerTests.swift +280 -0
- package/app/Tests/StageTileTests.swift +353 -0
- package/assets/AppIcon.icns +0 -0
- package/bin/client.ts +16 -0
- package/bin/{daemon-client.js → daemon-client.ts} +49 -30
- package/bin/handsoff-infer.ts +280 -0
- package/bin/handsoff-worker.ts +740 -0
- package/bin/lattices-app.ts +338 -0
- package/bin/lattices-dev +208 -0
- package/bin/{lattices.js → lattices.ts} +777 -140
- package/bin/project-twin.ts +645 -0
- package/docs/agent-execution-plan.md +562 -0
- package/docs/agent-layer-guide.md +207 -0
- package/docs/agents.md +142 -0
- package/docs/api.md +153 -34
- package/docs/app.md +29 -1
- package/docs/config.md +5 -1
- package/docs/handsoff-test-scenarios.md +84 -0
- package/docs/layers.md +20 -20
- package/docs/ocr.md +14 -5
- package/docs/overview.md +5 -1
- package/docs/presentation-execution-review.md +491 -0
- package/docs/prompts/hands-off-system.md +374 -0
- package/docs/prompts/hands-off-turn.md +30 -0
- package/docs/prompts/voice-advisor.md +31 -0
- package/docs/prompts/voice-fallback.md +23 -0
- package/docs/tiling-reference.md +167 -0
- package/docs/twins.md +138 -0
- package/docs/voice-command-protocol.md +278 -0
- package/docs/voice.md +219 -0
- package/package.json +29 -11
- package/bin/client.js +0 -4
- package/bin/lattices-app.js +0 -221
|
@@ -12,7 +12,7 @@ final class DiagnosticLog: ObservableObject {
|
|
|
12
12
|
let message: String
|
|
13
13
|
let level: Level
|
|
14
14
|
|
|
15
|
-
enum Level { case info, success, warning, error }
|
|
15
|
+
enum Level: String { case info, success, warning, error }
|
|
16
16
|
|
|
17
17
|
var icon: String {
|
|
18
18
|
switch level {
|
|
@@ -27,14 +27,68 @@ final class DiagnosticLog: ObservableObject {
|
|
|
27
27
|
@Published var entries: [Entry] = []
|
|
28
28
|
private let maxEntries = 80
|
|
29
29
|
|
|
30
|
+
// Disk persistence
|
|
31
|
+
private let logFile: URL
|
|
32
|
+
private let fileHandle: FileHandle?
|
|
33
|
+
private let diskQueue = DispatchQueue(label: "com.lattices.log-writer")
|
|
34
|
+
private static let timeFmt: DateFormatter = {
|
|
35
|
+
let f = DateFormatter()
|
|
36
|
+
f.dateFormat = "HH:mm:ss.SSS"
|
|
37
|
+
return f
|
|
38
|
+
}()
|
|
39
|
+
|
|
40
|
+
private init() {
|
|
41
|
+
let dir = FileManager.default.homeDirectoryForCurrentUser
|
|
42
|
+
.appendingPathComponent(".lattices")
|
|
43
|
+
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
44
|
+
|
|
45
|
+
logFile = dir.appendingPathComponent("lattices.log")
|
|
46
|
+
|
|
47
|
+
// Rotate if > 1MB
|
|
48
|
+
if let attrs = try? FileManager.default.attributesOfItem(atPath: logFile.path),
|
|
49
|
+
let size = attrs[.size] as? UInt64, size > 1_000_000 {
|
|
50
|
+
let prev = dir.appendingPathComponent("lattices.log.1")
|
|
51
|
+
try? FileManager.default.removeItem(at: prev)
|
|
52
|
+
try? FileManager.default.moveItem(at: logFile, to: prev)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Create file if needed and open for appending
|
|
56
|
+
if !FileManager.default.fileExists(atPath: logFile.path) {
|
|
57
|
+
FileManager.default.createFile(atPath: logFile.path, contents: nil)
|
|
58
|
+
}
|
|
59
|
+
fileHandle = try? FileHandle(forWritingTo: logFile)
|
|
60
|
+
fileHandle?.seekToEndOfFile()
|
|
61
|
+
|
|
62
|
+
// Write session header
|
|
63
|
+
let header = "\n──── Lattices launched \(ISO8601DateFormatter().string(from: Date())) ────\n"
|
|
64
|
+
if let data = header.data(using: .utf8) {
|
|
65
|
+
fileHandle?.write(data)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
deinit {
|
|
70
|
+
fileHandle?.closeFile()
|
|
71
|
+
}
|
|
72
|
+
|
|
30
73
|
func log(_ message: String, level: Entry.Level = .info) {
|
|
31
74
|
let entry = Entry(time: Date(), message: message, level: level)
|
|
75
|
+
|
|
76
|
+
// In-memory for UI
|
|
32
77
|
DispatchQueue.main.async {
|
|
33
78
|
self.entries.append(entry)
|
|
34
79
|
if self.entries.count > self.maxEntries {
|
|
35
80
|
self.entries.removeFirst(self.entries.count - self.maxEntries)
|
|
36
81
|
}
|
|
37
82
|
}
|
|
83
|
+
|
|
84
|
+
// Disk
|
|
85
|
+
diskQueue.async { [weak self] in
|
|
86
|
+
let ts = Self.timeFmt.string(from: entry.time)
|
|
87
|
+
let line = "\(ts) \(entry.icon) [\(level.rawValue)] \(message)\n"
|
|
88
|
+
if let data = line.data(using: .utf8) {
|
|
89
|
+
self?.fileHandle?.write(data)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
38
92
|
}
|
|
39
93
|
|
|
40
94
|
func info(_ msg: String) { log(msg, level: .info) }
|
|
@@ -61,6 +115,54 @@ final class DiagnosticLog: ObservableObject {
|
|
|
61
115
|
}
|
|
62
116
|
}
|
|
63
117
|
|
|
118
|
+
// MARK: - Interaction Feedback
|
|
119
|
+
|
|
120
|
+
final class AppFeedback {
|
|
121
|
+
static let shared = AppFeedback()
|
|
122
|
+
|
|
123
|
+
private lazy var tapSound: NSSound? = {
|
|
124
|
+
guard let url = Bundle.main.url(forResource: "tap", withExtension: "wav") else { return nil }
|
|
125
|
+
return NSSound(contentsOf: url, byReference: true)
|
|
126
|
+
}()
|
|
127
|
+
|
|
128
|
+
private init() {}
|
|
129
|
+
|
|
130
|
+
@discardableResult
|
|
131
|
+
func beginTimed(_ label: String, state: HUDState? = nil, feedback: String? = nil, playSound: Bool = true) -> DiagnosticLog.TimedAction {
|
|
132
|
+
if playSound {
|
|
133
|
+
playTap()
|
|
134
|
+
}
|
|
135
|
+
if let feedback, let state {
|
|
136
|
+
state.showFeedback(feedback)
|
|
137
|
+
}
|
|
138
|
+
return DiagnosticLog.shared.startTimed(label)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func finish(_ action: DiagnosticLog.TimedAction, state: HUDState? = nil, feedback: String? = nil) {
|
|
142
|
+
if let feedback, let state {
|
|
143
|
+
state.showFeedback(feedback)
|
|
144
|
+
}
|
|
145
|
+
DiagnosticLog.shared.finish(action)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
func acknowledge(_ label: String, state: HUDState? = nil, feedback: String? = nil, playSound: Bool = true) {
|
|
149
|
+
if playSound {
|
|
150
|
+
playTap()
|
|
151
|
+
}
|
|
152
|
+
if let feedback, let state {
|
|
153
|
+
state.showFeedback(feedback)
|
|
154
|
+
}
|
|
155
|
+
DiagnosticLog.shared.info(label)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private func playTap() {
|
|
159
|
+
DispatchQueue.main.async {
|
|
160
|
+
self.tapSound?.stop()
|
|
161
|
+
self.tapSound?.play()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
64
166
|
// MARK: - Diagnostic Window
|
|
65
167
|
|
|
66
168
|
final class DiagnosticWindow {
|
|
@@ -145,7 +247,7 @@ final class DiagnosticWindow {
|
|
|
145
247
|
|
|
146
248
|
// Show running sessions
|
|
147
249
|
let task = Process()
|
|
148
|
-
task.executableURL = URL(fileURLWithPath: "/opt/homebrew/bin/tmux")
|
|
250
|
+
task.executableURL = URL(fileURLWithPath: TmuxQuery.resolvedPath ?? "/opt/homebrew/bin/tmux")
|
|
149
251
|
task.arguments = ["list-sessions", "-F", "#{session_name}"]
|
|
150
252
|
let pipe = Pipe()
|
|
151
253
|
task.standardOutput = pipe
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
// MARK: - HUDBottomBar (action playback tray)
|
|
4
|
+
|
|
5
|
+
struct HUDBottomBar: View {
|
|
6
|
+
@ObservedObject var state: HUDState
|
|
7
|
+
@ObservedObject private var handsOff = HandsOffSession.shared
|
|
8
|
+
var onDismiss: () -> Void
|
|
9
|
+
|
|
10
|
+
var body: some View {
|
|
11
|
+
HStack(spacing: 0) {
|
|
12
|
+
if state.tileMode {
|
|
13
|
+
tileModeView
|
|
14
|
+
} else if !handsOff.recentActions.isEmpty {
|
|
15
|
+
actionPlayback
|
|
16
|
+
} else if let feedback = state.feedbackMessage {
|
|
17
|
+
feedbackView(feedback)
|
|
18
|
+
} else if state.voiceActive {
|
|
19
|
+
voiceStatusView
|
|
20
|
+
} else {
|
|
21
|
+
shortcutsView
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
.frame(maxWidth: .infinity)
|
|
25
|
+
.frame(height: 48)
|
|
26
|
+
.background(Palette.bg)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// MARK: - Action playback (what just happened)
|
|
30
|
+
|
|
31
|
+
private var actionPlayback: some View {
|
|
32
|
+
HStack(spacing: 8) {
|
|
33
|
+
// Flash indicator
|
|
34
|
+
Image(systemName: "checkmark.circle.fill")
|
|
35
|
+
.font(.system(size: 12))
|
|
36
|
+
.foregroundColor(Palette.running)
|
|
37
|
+
|
|
38
|
+
// Action chips showing what was executed
|
|
39
|
+
ScrollView(.horizontal, showsIndicators: false) {
|
|
40
|
+
HStack(spacing: 6) {
|
|
41
|
+
ForEach(Array(handsOff.recentActions.enumerated()), id: \.offset) { _, action in
|
|
42
|
+
executedChip(action)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Spacer()
|
|
48
|
+
|
|
49
|
+
// Dismiss playback
|
|
50
|
+
Button {
|
|
51
|
+
handsOff.recentActions = []
|
|
52
|
+
} label: {
|
|
53
|
+
Image(systemName: "xmark")
|
|
54
|
+
.font(.system(size: 9, weight: .bold))
|
|
55
|
+
.foregroundColor(Palette.textMuted)
|
|
56
|
+
.frame(width: 20, height: 20)
|
|
57
|
+
}
|
|
58
|
+
.buttonStyle(.plain)
|
|
59
|
+
}
|
|
60
|
+
.padding(.horizontal, 16)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// MARK: - Executed action chip
|
|
64
|
+
|
|
65
|
+
private func executedChip(_ action: [String: Any]) -> some View {
|
|
66
|
+
let intent = action["intent"] as? String ?? "action"
|
|
67
|
+
let slots = action["slots"] as? [String: Any] ?? [:]
|
|
68
|
+
let summary = actionSummary(intent: intent, slots: slots)
|
|
69
|
+
|
|
70
|
+
return HStack(spacing: 5) {
|
|
71
|
+
Image(systemName: iconForIntent(intent))
|
|
72
|
+
.font(.system(size: 9))
|
|
73
|
+
.foregroundColor(Palette.running)
|
|
74
|
+
Text(summary)
|
|
75
|
+
.font(Typo.mono(10))
|
|
76
|
+
.foregroundColor(Palette.text)
|
|
77
|
+
.lineLimit(1)
|
|
78
|
+
Image(systemName: "checkmark")
|
|
79
|
+
.font(.system(size: 7, weight: .bold))
|
|
80
|
+
.foregroundColor(Palette.running)
|
|
81
|
+
}
|
|
82
|
+
.padding(.horizontal, 10)
|
|
83
|
+
.padding(.vertical, 5)
|
|
84
|
+
.background(
|
|
85
|
+
RoundedRectangle(cornerRadius: 5)
|
|
86
|
+
.fill(Palette.running.opacity(0.06))
|
|
87
|
+
.overlay(
|
|
88
|
+
RoundedRectangle(cornerRadius: 5)
|
|
89
|
+
.strokeBorder(Palette.running.opacity(0.2), lineWidth: 0.5)
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// MARK: - Voice active status
|
|
95
|
+
|
|
96
|
+
private var voiceStatusView: some View {
|
|
97
|
+
HStack(spacing: 8) {
|
|
98
|
+
// Pulsing mic
|
|
99
|
+
Image(systemName: "waveform")
|
|
100
|
+
.font(.system(size: 11))
|
|
101
|
+
.foregroundColor(voiceColor)
|
|
102
|
+
|
|
103
|
+
Text(voiceLabel)
|
|
104
|
+
.font(Typo.monoBold(10))
|
|
105
|
+
.foregroundColor(voiceColor)
|
|
106
|
+
|
|
107
|
+
if let transcript = handsOff.lastTranscript {
|
|
108
|
+
Rectangle().fill(Palette.border).frame(width: 0.5, height: 20)
|
|
109
|
+
Text(transcript)
|
|
110
|
+
.font(Typo.mono(10))
|
|
111
|
+
.foregroundColor(Palette.textMuted)
|
|
112
|
+
.lineLimit(1)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
Spacer()
|
|
116
|
+
|
|
117
|
+
if let response = handsOff.lastResponse {
|
|
118
|
+
Text(response)
|
|
119
|
+
.font(Typo.mono(9))
|
|
120
|
+
.foregroundColor(Palette.textDim)
|
|
121
|
+
.lineLimit(1)
|
|
122
|
+
.frame(maxWidth: 250, alignment: .trailing)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
.padding(.horizontal, 16)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private var voiceColor: Color {
|
|
129
|
+
switch handsOff.state {
|
|
130
|
+
case .idle: return Palette.running
|
|
131
|
+
case .connecting: return Palette.detach
|
|
132
|
+
case .listening: return Palette.running
|
|
133
|
+
case .thinking: return Palette.detach
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private var voiceLabel: String {
|
|
138
|
+
switch handsOff.state {
|
|
139
|
+
case .idle: return "ready"
|
|
140
|
+
case .connecting: return "connecting..."
|
|
141
|
+
case .listening: return "listening..."
|
|
142
|
+
case .thinking: return "thinking..."
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// MARK: - Interaction feedback
|
|
147
|
+
|
|
148
|
+
private func feedbackView(_ message: String) -> some View {
|
|
149
|
+
HStack(spacing: 8) {
|
|
150
|
+
Image(systemName: "cursorarrow.click.2")
|
|
151
|
+
.font(.system(size: 11, weight: .medium))
|
|
152
|
+
.foregroundColor(Palette.running)
|
|
153
|
+
Text(message)
|
|
154
|
+
.font(Typo.monoBold(10))
|
|
155
|
+
.foregroundColor(Palette.text)
|
|
156
|
+
.lineLimit(1)
|
|
157
|
+
Spacer()
|
|
158
|
+
Text("working")
|
|
159
|
+
.font(Typo.mono(9))
|
|
160
|
+
.foregroundColor(Palette.textDim)
|
|
161
|
+
}
|
|
162
|
+
.padding(.horizontal, 16)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// MARK: - Tile mode
|
|
166
|
+
|
|
167
|
+
private var tileModeView: some View {
|
|
168
|
+
HStack(spacing: 8) {
|
|
169
|
+
// Mode indicator
|
|
170
|
+
HStack(spacing: 4) {
|
|
171
|
+
Image(systemName: "rectangle.split.2x2")
|
|
172
|
+
.font(.system(size: 11))
|
|
173
|
+
.foregroundColor(Palette.running)
|
|
174
|
+
Text("TILE")
|
|
175
|
+
.font(Typo.monoBold(10))
|
|
176
|
+
.foregroundColor(Palette.running)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
Rectangle().fill(Palette.border).frame(width: 0.5, height: 20)
|
|
180
|
+
|
|
181
|
+
// Key hints
|
|
182
|
+
HStack(spacing: 6) {
|
|
183
|
+
tileKey("H", "←")
|
|
184
|
+
tileKey("J", "↓")
|
|
185
|
+
tileKey("K", "↑")
|
|
186
|
+
tileKey("L", "→")
|
|
187
|
+
tileKey("F", "max")
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
Rectangle().fill(Palette.border).frame(width: 0.5, height: 20)
|
|
191
|
+
|
|
192
|
+
HStack(spacing: 6) {
|
|
193
|
+
tileKey("Y", "◸")
|
|
194
|
+
tileKey("U", "◹")
|
|
195
|
+
tileKey("B", "◺")
|
|
196
|
+
tileKey("N", "◿")
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
Spacer()
|
|
200
|
+
|
|
201
|
+
Text("⎋ done")
|
|
202
|
+
.font(Typo.mono(9))
|
|
203
|
+
.foregroundColor(Palette.textMuted)
|
|
204
|
+
}
|
|
205
|
+
.padding(.horizontal, 16)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private func tileKey(_ key: String, _ hint: String) -> some View {
|
|
209
|
+
HStack(spacing: 2) {
|
|
210
|
+
Text(key)
|
|
211
|
+
.font(Typo.geistMonoBold(9))
|
|
212
|
+
.foregroundColor(Palette.text)
|
|
213
|
+
.frame(width: 16, height: 16)
|
|
214
|
+
.background(
|
|
215
|
+
RoundedRectangle(cornerRadius: 3)
|
|
216
|
+
.fill(Palette.surface)
|
|
217
|
+
.overlay(
|
|
218
|
+
RoundedRectangle(cornerRadius: 3)
|
|
219
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
Text(hint)
|
|
223
|
+
.font(Typo.mono(8))
|
|
224
|
+
.foregroundColor(Palette.textDim)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// MARK: - Shortcuts hint (default state)
|
|
229
|
+
|
|
230
|
+
private var shortcutsView: some View {
|
|
231
|
+
HStack(spacing: 8) {
|
|
232
|
+
Image(systemName: "keyboard")
|
|
233
|
+
.font(.system(size: 10))
|
|
234
|
+
.foregroundColor(Palette.textMuted.opacity(0.4))
|
|
235
|
+
Text("V voice / search 1-4 jump ⇥ tab ↵ go ⎋ close")
|
|
236
|
+
.font(Typo.mono(9))
|
|
237
|
+
.foregroundColor(Palette.textMuted.opacity(0.5))
|
|
238
|
+
Spacer()
|
|
239
|
+
}
|
|
240
|
+
.padding(.horizontal, 16)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// MARK: - Helpers
|
|
244
|
+
|
|
245
|
+
private func actionSummary(intent: String, slots: [String: Any]) -> String {
|
|
246
|
+
let target = slots["target"] as? String
|
|
247
|
+
?? slots["app"] as? String
|
|
248
|
+
?? slots["query"] as? String
|
|
249
|
+
?? ""
|
|
250
|
+
let position = slots["position"] as? String ?? ""
|
|
251
|
+
|
|
252
|
+
switch intent {
|
|
253
|
+
case "tile_window":
|
|
254
|
+
let parts = [target, position].filter { !$0.isEmpty }
|
|
255
|
+
return "Tile \(parts.joined(separator: " "))"
|
|
256
|
+
case "focus", "focus_app":
|
|
257
|
+
return "Focus \(target)"
|
|
258
|
+
case "launch", "launch_project":
|
|
259
|
+
return "Launch \(target)"
|
|
260
|
+
case "close_window":
|
|
261
|
+
return "Close \(target)"
|
|
262
|
+
case "maximize":
|
|
263
|
+
return "Maximize \(target)"
|
|
264
|
+
default:
|
|
265
|
+
return target.isEmpty ? intent : "\(intent) \(target)"
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private func iconForIntent(_ intent: String) -> String {
|
|
270
|
+
switch intent {
|
|
271
|
+
case "tile_window": return "rectangle.split.2x1"
|
|
272
|
+
case "focus", "focus_app": return "eye"
|
|
273
|
+
case "launch", "launch_project": return "play.fill"
|
|
274
|
+
case "close_window": return "xmark.circle"
|
|
275
|
+
case "maximize": return "arrow.up.left.and.arrow.down.right"
|
|
276
|
+
default: return "bolt"
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|