@lattices/cli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -9
- package/app/Package.swift +8 -1
- package/app/Sources/AdvisorLearningStore.swift +90 -0
- package/app/Sources/AgentSession.swift +377 -0
- package/app/Sources/AppDelegate.swift +44 -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 +164 -5
- 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 +733 -0
- package/app/Sources/HomeDashboardView.swift +125 -0
- package/app/Sources/HotkeyManager.swift +2 -0
- package/app/Sources/HotkeyStore.swift +45 -9
- package/app/Sources/IntentEngine.swift +925 -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 +1235 -30
- package/app/Sources/LauncherHUD.swift +348 -0
- package/app/Sources/MainView.swift +147 -44
- 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 +1 -1
- 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/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 +731 -0
- package/bin/{lattices-app.js → lattices-app.ts} +67 -32
- package/bin/lattices-dev +160 -0
- package/bin/{lattices.js → lattices.ts} +600 -137
- package/bin/project-twin.ts +645 -0
- package/docs/agent-execution-plan.md +562 -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 +21 -10
- package/bin/client.js +0 -4
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import Foundation
|
|
3
|
+
import NaturalLanguage
|
|
4
|
+
|
|
5
|
+
private struct IntentCandidate {
|
|
6
|
+
let intent: IntentDef
|
|
7
|
+
let slots: [String: JSON]
|
|
8
|
+
let score: Double
|
|
9
|
+
let semanticScore: Double
|
|
10
|
+
let keywordBoost: Double
|
|
11
|
+
let slotBoost: Double
|
|
12
|
+
let matchedExample: String
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private struct ExtractedSlots {
|
|
16
|
+
let slots: [String: JSON]
|
|
17
|
+
let boost: Double
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
final class VoiceIntentResolver {
|
|
21
|
+
static let shared = VoiceIntentResolver()
|
|
22
|
+
|
|
23
|
+
private let embedding = NLEmbedding.sentenceEmbedding(for: .english)
|
|
24
|
+
|
|
25
|
+
private init() {}
|
|
26
|
+
|
|
27
|
+
func match(text: String) -> IntentMatch? {
|
|
28
|
+
let input = normalizeUtterance(text)
|
|
29
|
+
guard !input.isEmpty else { return nil }
|
|
30
|
+
|
|
31
|
+
if let direct = directMatch(for: input) {
|
|
32
|
+
return direct
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
var candidates = IntentEngine.shared.definitions().compactMap { candidate(for: $0, input: input) }
|
|
36
|
+
candidates.sort { lhs, rhs in
|
|
37
|
+
if lhs.score == rhs.score {
|
|
38
|
+
return lhs.semanticScore > rhs.semanticScore
|
|
39
|
+
}
|
|
40
|
+
return lhs.score > rhs.score
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
guard let best = candidates.first else { return nil }
|
|
44
|
+
|
|
45
|
+
let minimumScore = best.intent.slots.contains(where: \.required) ? 0.42 : 0.36
|
|
46
|
+
guard best.score >= minimumScore else { return nil }
|
|
47
|
+
|
|
48
|
+
if let runnerUp = candidates.dropFirst().first,
|
|
49
|
+
best.score - runnerUp.score < 0.05,
|
|
50
|
+
best.keywordBoost < 0.18,
|
|
51
|
+
best.slotBoost < 0.12 {
|
|
52
|
+
return nil
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return IntentMatch(
|
|
56
|
+
intentName: best.intent.name,
|
|
57
|
+
slots: best.slots,
|
|
58
|
+
confidence: min(0.98, max(0.35, best.score)),
|
|
59
|
+
matchedPhrase: best.matchedExample
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func execute(_ match: IntentMatch) throws -> JSON {
|
|
64
|
+
try IntentEngine.shared.execute(IntentRequest(
|
|
65
|
+
intent: match.intentName,
|
|
66
|
+
slots: match.slots,
|
|
67
|
+
rawText: nil,
|
|
68
|
+
confidence: match.confidence,
|
|
69
|
+
source: "voice-local"
|
|
70
|
+
))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func catalog() -> JSON {
|
|
74
|
+
IntentEngine.shared.catalog()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private func candidate(for intent: IntentDef, input: String) -> IntentCandidate? {
|
|
78
|
+
let extracted = extractSlots(for: intent.name, input: input)
|
|
79
|
+
let requiredMissing = intent.slots.contains { $0.required && extracted.slots[$0.name] == nil }
|
|
80
|
+
let exampleMatch = bestExampleMatch(for: intent, input: input)
|
|
81
|
+
let keywordBoost = keywordBoost(for: intent.name, input: input)
|
|
82
|
+
let exactBoost = normalizeUtterance(exampleMatch.example) == input ? 0.20 : 0.0
|
|
83
|
+
let missingPenalty = requiredMissing ? 0.22 : 0.0
|
|
84
|
+
let score = exampleMatch.score + keywordBoost + extracted.boost + exactBoost - missingPenalty
|
|
85
|
+
|
|
86
|
+
if score <= 0 {
|
|
87
|
+
return nil
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return IntentCandidate(
|
|
91
|
+
intent: intent,
|
|
92
|
+
slots: extracted.slots,
|
|
93
|
+
score: score,
|
|
94
|
+
semanticScore: exampleMatch.score,
|
|
95
|
+
keywordBoost: keywordBoost,
|
|
96
|
+
slotBoost: extracted.boost,
|
|
97
|
+
matchedExample: exampleMatch.example
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private func bestExampleMatch(for intent: IntentDef, input: String) -> (example: String, score: Double) {
|
|
102
|
+
let examples = intent.examples + supplementalExamples[intent.name, default: []]
|
|
103
|
+
guard !examples.isEmpty else { return ("", 0) }
|
|
104
|
+
|
|
105
|
+
let best = examples
|
|
106
|
+
.map { example -> (String, Double) in
|
|
107
|
+
let normalizedExample = normalizeUtterance(example)
|
|
108
|
+
if normalizedExample == input {
|
|
109
|
+
return (example, 0.62)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let distance = semanticDistance(between: input, and: normalizedExample)
|
|
113
|
+
let semantic = max(0, 1.18 - distance) * 0.48
|
|
114
|
+
let overlap = tokenOverlap(input, normalizedExample) * 0.24
|
|
115
|
+
return (example, semantic + overlap)
|
|
116
|
+
}
|
|
117
|
+
.max { $0.1 < $1.1 } ?? ("", 0)
|
|
118
|
+
|
|
119
|
+
return best
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private func semanticDistance(between lhs: String, and rhs: String) -> Double {
|
|
123
|
+
guard let embedding else {
|
|
124
|
+
return lhs == rhs ? 0 : 2
|
|
125
|
+
}
|
|
126
|
+
return Double(embedding.distance(between: lhs, and: rhs))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private func keywordBoost(for intentName: String, input: String) -> Double {
|
|
130
|
+
guard let keywords = intentKeywords[intentName] else { return 0 }
|
|
131
|
+
|
|
132
|
+
var boost = 0.0
|
|
133
|
+
for keyword in keywords {
|
|
134
|
+
if input.contains(keyword) {
|
|
135
|
+
boost += keyword.contains(" ") ? 0.12 : 0.08
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return min(boost, 0.28)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private func extractSlots(for intentName: String, input: String) -> ExtractedSlots {
|
|
142
|
+
switch intentName {
|
|
143
|
+
case "tile_window":
|
|
144
|
+
guard let position = resolvePosition(in: input) else {
|
|
145
|
+
return ExtractedSlots(slots: [:], boost: 0)
|
|
146
|
+
}
|
|
147
|
+
return ExtractedSlots(slots: ["position": .string(position)], boost: 0.28)
|
|
148
|
+
|
|
149
|
+
case "focus":
|
|
150
|
+
if let app = detectKnownApp(in: input) ?? extractEntity(in: input, prefixes: focusPrefixes) {
|
|
151
|
+
let resolved = resolveApp(app)
|
|
152
|
+
guard !resolved.isEmpty else { return ExtractedSlots(slots: [:], boost: 0) }
|
|
153
|
+
return ExtractedSlots(slots: ["app": .string(resolved)], boost: 0.18)
|
|
154
|
+
}
|
|
155
|
+
return ExtractedSlots(slots: [:], boost: 0)
|
|
156
|
+
|
|
157
|
+
case "launch":
|
|
158
|
+
if let project = extractEntity(in: input, prefixes: launchPrefixes) {
|
|
159
|
+
let cleaned = cleanEntity(project)
|
|
160
|
+
guard !cleaned.isEmpty else { return ExtractedSlots(slots: [:], boost: 0) }
|
|
161
|
+
return ExtractedSlots(slots: ["project": .string(cleaned)], boost: 0.16)
|
|
162
|
+
}
|
|
163
|
+
return ExtractedSlots(slots: [:], boost: 0)
|
|
164
|
+
|
|
165
|
+
case "switch_layer":
|
|
166
|
+
if let layer = extractLayer(in: input) {
|
|
167
|
+
guard !layer.isEmpty else { return ExtractedSlots(slots: [:], boost: 0) }
|
|
168
|
+
return ExtractedSlots(slots: ["layer": .string(layer)], boost: 0.18)
|
|
169
|
+
}
|
|
170
|
+
return ExtractedSlots(slots: [:], boost: 0)
|
|
171
|
+
|
|
172
|
+
case "search":
|
|
173
|
+
if let query = extractSearchQuery(from: input) {
|
|
174
|
+
let cleaned = cleanQuery(query)
|
|
175
|
+
guard !cleaned.isEmpty else { return ExtractedSlots(slots: [:], boost: 0) }
|
|
176
|
+
return ExtractedSlots(slots: ["query": .string(cleaned)], boost: 0.16)
|
|
177
|
+
}
|
|
178
|
+
return ExtractedSlots(slots: [:], boost: 0)
|
|
179
|
+
|
|
180
|
+
case "create_layer":
|
|
181
|
+
if let name = extractLayerName(from: input) {
|
|
182
|
+
let cleaned = cleanEntity(name)
|
|
183
|
+
guard !cleaned.isEmpty else { return ExtractedSlots(slots: [:], boost: 0) }
|
|
184
|
+
return ExtractedSlots(slots: ["name": .string(cleaned)], boost: 0.14)
|
|
185
|
+
}
|
|
186
|
+
return ExtractedSlots(slots: [:], boost: 0)
|
|
187
|
+
|
|
188
|
+
case "kill":
|
|
189
|
+
if let session = extractEntity(in: input, prefixes: killPrefixes) {
|
|
190
|
+
let cleaned = cleanEntity(session)
|
|
191
|
+
guard !cleaned.isEmpty else { return ExtractedSlots(slots: [:], boost: 0) }
|
|
192
|
+
return ExtractedSlots(slots: ["session": .string(cleaned)], boost: 0.16)
|
|
193
|
+
}
|
|
194
|
+
return ExtractedSlots(slots: [:], boost: 0)
|
|
195
|
+
|
|
196
|
+
default:
|
|
197
|
+
return ExtractedSlots(slots: [:], boost: 0)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private func extractSearchQuery(from input: String) -> String? {
|
|
202
|
+
let prefixes = [
|
|
203
|
+
"find all the ", "find all ", "find ",
|
|
204
|
+
"search for all ", "search for ", "search ",
|
|
205
|
+
"look for ", "look up ", "locate all ", "locate ",
|
|
206
|
+
"where is ", "where s ", "where does it say ", "where did i see ",
|
|
207
|
+
"which window has ", "which window shows ",
|
|
208
|
+
"help me find ", "can you find ",
|
|
209
|
+
"show me all the ", "show me all ", "show all the ", "show all ",
|
|
210
|
+
"open up all the ", "open up all ", "open all the ", "open all ",
|
|
211
|
+
"pull up everything with ", "pull up all ", "pull up ",
|
|
212
|
+
"bring up all the ", "bring up all ", "bring up my ",
|
|
213
|
+
"where d my ", "i lost my ", "i lost ", "where the hell is ",
|
|
214
|
+
"see all my ", "see all ", "see where s ", "see where is "
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
if let entity = extractEntity(in: input, prefixes: prefixes) {
|
|
218
|
+
return entity
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if input.hasSuffix(" windows") || input.hasSuffix(" window") {
|
|
222
|
+
return cleanQuery(input)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let wordCount = input.split(separator: " ").count
|
|
226
|
+
if wordCount <= 3, !genericNonCommandPhrases.contains(input) {
|
|
227
|
+
return input
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return nil
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private func extractLayerName(from input: String) -> String? {
|
|
234
|
+
if let called = extractEntity(in: input, prefixes: [
|
|
235
|
+
"create a layer called ", "create layer called ", "make a layer called ",
|
|
236
|
+
"make layer called ", "new layer called ", "name this layer "
|
|
237
|
+
]) {
|
|
238
|
+
return called
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return extractEntity(in: input, prefixes: [
|
|
242
|
+
"save this layout as ", "save layout as ", "save as layer ", "save as ",
|
|
243
|
+
"create a layer ", "create layer ", "create new layer ",
|
|
244
|
+
"make a layer ", "make layer ", "make a new layer ", "new layer "
|
|
245
|
+
])
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private func extractLayer(in input: String) -> String? {
|
|
249
|
+
if input == "next layer" || input == "previous layer" {
|
|
250
|
+
return input
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if let literal = [
|
|
254
|
+
"layer one": "1",
|
|
255
|
+
"layer two": "2",
|
|
256
|
+
"layer three": "3",
|
|
257
|
+
"first layer": "1",
|
|
258
|
+
"second layer": "2",
|
|
259
|
+
"third layer": "3",
|
|
260
|
+
][input] {
|
|
261
|
+
return literal
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if let entity = extractEntity(in: input, prefixes: [
|
|
265
|
+
"switch to layer ", "switch to the ", "switch to ",
|
|
266
|
+
"go to layer ", "go to the ", "go to ",
|
|
267
|
+
"activate layer ", "activate the ", "change to layer ",
|
|
268
|
+
"change layer to ", "layer "
|
|
269
|
+
]) {
|
|
270
|
+
return cleanLayer(entity)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return nil
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private func extractEntity(in input: String, prefixes: [String]) -> String? {
|
|
277
|
+
for prefix in prefixes.sorted(by: { $0.count > $1.count }) {
|
|
278
|
+
if input.hasPrefix(prefix) {
|
|
279
|
+
return String(input.dropFirst(prefix.count))
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return nil
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private func normalizeUtterance(_ text: String) -> String {
|
|
286
|
+
var input = text.lowercased()
|
|
287
|
+
.replacingOccurrences(of: #"[^\w\s-]"#, with: " ", options: .regularExpression)
|
|
288
|
+
.split(separator: " ").joined(separator: " ")
|
|
289
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
290
|
+
|
|
291
|
+
var changed = true
|
|
292
|
+
while changed {
|
|
293
|
+
changed = false
|
|
294
|
+
for prefix in leadingNoise {
|
|
295
|
+
if input.hasPrefix(prefix) {
|
|
296
|
+
input = String(input.dropFirst(prefix.count)).trimmingCharacters(in: .whitespaces)
|
|
297
|
+
changed = true
|
|
298
|
+
break
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
var stripped = true
|
|
304
|
+
while stripped {
|
|
305
|
+
stripped = false
|
|
306
|
+
for suffix in trailingNoise {
|
|
307
|
+
if input.hasSuffix(suffix) {
|
|
308
|
+
input = String(input.dropLast(suffix.count)).trimmingCharacters(in: .whitespaces)
|
|
309
|
+
stripped = true
|
|
310
|
+
break
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return input
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private func resolvePosition(in input: String) -> String? {
|
|
319
|
+
let map: [(keywords: [String], position: String)] = [
|
|
320
|
+
(["top left", "upper left", "top-left"], "top-left"),
|
|
321
|
+
(["top right", "upper right", "top-right"], "top-right"),
|
|
322
|
+
(["bottom left", "lower left", "bottom-left"], "bottom-left"),
|
|
323
|
+
(["bottom right", "lower right", "bottom-right"], "bottom-right"),
|
|
324
|
+
(["left half", "left side", "the left", "left"], "left"),
|
|
325
|
+
(["right half", "right side", "the right", "right"], "right"),
|
|
326
|
+
(["maximize", "full screen", "full", "big", "max"], "maximize"),
|
|
327
|
+
(["center", "middle", "centre"], "center"),
|
|
328
|
+
(["top half", "top"], "top"),
|
|
329
|
+
(["bottom half", "bottom"], "bottom"),
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
for entry in map {
|
|
333
|
+
if entry.keywords.contains(where: input.contains) {
|
|
334
|
+
return entry.position
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return nil
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private func directMatch(for input: String) -> IntentMatch? {
|
|
341
|
+
if let intent = exactIntentMatches[input] {
|
|
342
|
+
return IntentMatch(intentName: intent, slots: [:], confidence: 0.99, matchedPhrase: input)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if let query = exactSearchQuery(for: input) {
|
|
346
|
+
return IntentMatch(
|
|
347
|
+
intentName: "search",
|
|
348
|
+
slots: ["query": .string(query)],
|
|
349
|
+
confidence: 0.98,
|
|
350
|
+
matchedPhrase: input
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if let app = exactFocusApp(for: input) {
|
|
355
|
+
return IntentMatch(
|
|
356
|
+
intentName: "focus",
|
|
357
|
+
slots: ["app": .string(app)],
|
|
358
|
+
confidence: 0.98,
|
|
359
|
+
matchedPhrase: input
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if let position = exactTilePosition(for: input) {
|
|
364
|
+
return IntentMatch(
|
|
365
|
+
intentName: "tile_window",
|
|
366
|
+
slots: ["position": .string(position)],
|
|
367
|
+
confidence: 0.99,
|
|
368
|
+
matchedPhrase: input
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if let session = exactKillSession(for: input) {
|
|
373
|
+
return IntentMatch(
|
|
374
|
+
intentName: "kill",
|
|
375
|
+
slots: ["session": .string(session)],
|
|
376
|
+
confidence: 0.98,
|
|
377
|
+
matchedPhrase: input
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return nil
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private func exactSearchQuery(for input: String) -> String? {
|
|
385
|
+
if let query = extractSearchQuery(from: input), !query.isEmpty,
|
|
386
|
+
searchPrefixes.contains(where: input.hasPrefix) {
|
|
387
|
+
return cleanQuery(query)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if input.hasSuffix(" windows"), input != "list windows" {
|
|
391
|
+
let query = cleanQuery(input)
|
|
392
|
+
return query.isEmpty ? nil : query
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return nil
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private func exactFocusApp(for input: String) -> String? {
|
|
399
|
+
if input.hasPrefix("see "), let app = detectKnownApp(in: input) ?? extractEntity(in: input, prefixes: ["see "]) {
|
|
400
|
+
let resolved = resolveApp(app)
|
|
401
|
+
return resolved.isEmpty ? nil : resolved
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if input.hasSuffix(" on screen"), input.hasPrefix("get "),
|
|
405
|
+
let app = extractEntity(in: input, prefixes: ["get "]) {
|
|
406
|
+
let resolved = resolveApp(cleanEntity(app.replacingOccurrences(of: " on screen", with: "")))
|
|
407
|
+
return resolved.isEmpty ? nil : resolved
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return nil
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private func exactTilePosition(for input: String) -> String? {
|
|
414
|
+
if input == "right side" { return "right" }
|
|
415
|
+
if input == "left side" { return "left" }
|
|
416
|
+
return nil
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private func exactKillSession(for input: String) -> String? {
|
|
420
|
+
if input.hasPrefix("kill "), let session = extractEntity(in: input, prefixes: ["kill "]) {
|
|
421
|
+
return session
|
|
422
|
+
}
|
|
423
|
+
if input.hasPrefix("stop "), let session = extractEntity(in: input, prefixes: ["stop "]) {
|
|
424
|
+
return session
|
|
425
|
+
}
|
|
426
|
+
return nil
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
private func detectKnownApp(in input: String) -> String? {
|
|
430
|
+
for app in knownApps() {
|
|
431
|
+
let lower = app.lowercased()
|
|
432
|
+
if input.contains(lower) {
|
|
433
|
+
return app
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return nil
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private func knownApps() -> [String] {
|
|
440
|
+
var names = Set(DesktopModel.shared.windows.values.map(\.app))
|
|
441
|
+
for app in NSWorkspace.shared.runningApplications {
|
|
442
|
+
if let name = app.localizedName, !name.isEmpty {
|
|
443
|
+
names.insert(name)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return names.sorted { $0.count > $1.count }
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private func resolveApp(_ raw: String) -> String {
|
|
450
|
+
let trimmed = cleanEntity(raw)
|
|
451
|
+
let knownAliases: [String: String] = [
|
|
452
|
+
"visual studio code": "Visual Studio Code",
|
|
453
|
+
"vs code": "Visual Studio Code",
|
|
454
|
+
"vscode": "Visual Studio Code",
|
|
455
|
+
"google chrome": "Google Chrome",
|
|
456
|
+
"iterm2": "iTerm2",
|
|
457
|
+
"iterm": "iTerm2",
|
|
458
|
+
]
|
|
459
|
+
|
|
460
|
+
if let alias = knownAliases[trimmed.lowercased()] {
|
|
461
|
+
return alias
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return trimmed
|
|
465
|
+
.split(separator: " ")
|
|
466
|
+
.map { $0.prefix(1).uppercased() + $0.dropFirst() }
|
|
467
|
+
.joined(separator: " ")
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private func cleanLayer(_ raw: String) -> String {
|
|
471
|
+
cleanEntity(raw)
|
|
472
|
+
.replacingOccurrences(of: " layer", with: "")
|
|
473
|
+
.trimmingCharacters(in: .whitespaces)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private func cleanEntity(_ raw: String) -> String {
|
|
477
|
+
var value = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
478
|
+
let leading = ["the ", "my ", "a ", "an ", "this ", "that ", "like ", "um ", "uh "]
|
|
479
|
+
let trailing = [
|
|
480
|
+
" please", " for me", " right now", " real quick", " quickly",
|
|
481
|
+
" session", " project", " app", " windows", " window", " layer"
|
|
482
|
+
]
|
|
483
|
+
|
|
484
|
+
var changed = true
|
|
485
|
+
while changed {
|
|
486
|
+
changed = false
|
|
487
|
+
|
|
488
|
+
for prefix in leading {
|
|
489
|
+
if value.hasPrefix(prefix) {
|
|
490
|
+
value = String(value.dropFirst(prefix.count)).trimmingCharacters(in: .whitespaces)
|
|
491
|
+
changed = true
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
for suffix in trailing {
|
|
496
|
+
if value.hasSuffix(suffix) {
|
|
497
|
+
value = String(value.dropLast(suffix.count)).trimmingCharacters(in: .whitespaces)
|
|
498
|
+
changed = true
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private func cleanQuery(_ raw: String) -> String {
|
|
507
|
+
var query = raw
|
|
508
|
+
let noise = [
|
|
509
|
+
"all instances of ", "all of the ", "all the ", "all ",
|
|
510
|
+
"instances of ", "of the ",
|
|
511
|
+
"that mentioned ", "that mention ", "that say ", "that says ",
|
|
512
|
+
"that have ", "that has ", "that are ", "that is ",
|
|
513
|
+
"everything with ", "everything that has ",
|
|
514
|
+
"windows ", "window ", "windows", "window",
|
|
515
|
+
"stuff ", "stuff", "project ", "project", "app ", "app",
|
|
516
|
+
"with the name ", "named ", "called ",
|
|
517
|
+
"in the title", "in my title", "in the name", "on my screen", "on screen",
|
|
518
|
+
" in it", " in there", " at", " go"
|
|
519
|
+
]
|
|
520
|
+
|
|
521
|
+
for item in noise {
|
|
522
|
+
query = query.replacingOccurrences(of: item, with: " ")
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return query
|
|
526
|
+
.split(separator: " ")
|
|
527
|
+
.joined(separator: " ")
|
|
528
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private func tokenOverlap(_ lhs: String, _ rhs: String) -> Double {
|
|
532
|
+
let lhsTokens = Set(lhs.split(separator: " ").map(String.init))
|
|
533
|
+
let rhsTokens = Set(rhs.split(separator: " ").map(String.init))
|
|
534
|
+
guard !lhsTokens.isEmpty, !rhsTokens.isEmpty else { return 0 }
|
|
535
|
+
let intersection = lhsTokens.intersection(rhsTokens).count
|
|
536
|
+
let union = lhsTokens.union(rhsTokens).count
|
|
537
|
+
return Double(intersection) / Double(union)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private let leadingNoise = [
|
|
541
|
+
"alright let s go ahead and ", "alright let s go ahead ", "let s go ahead and ",
|
|
542
|
+
"alright let s ", "all right let s ",
|
|
543
|
+
"okay let s ", "ok let s ",
|
|
544
|
+
"can you please ", "could you please ", "would you please ",
|
|
545
|
+
"i think i want to ", "i think i need to ",
|
|
546
|
+
"can you ", "could you ", "would you ", "will you ",
|
|
547
|
+
"i want to ", "i d like to ", "i would like to ",
|
|
548
|
+
"i want you to ", "i need to ", "i need you to ",
|
|
549
|
+
"i wanna ", "i think ", "i need ",
|
|
550
|
+
"let s ", "let me ", "please ", "go ahead and ", "just ", "now ",
|
|
551
|
+
"alright ", "all right ",
|
|
552
|
+
"no sorry ", "sorry ", "no wait ", "wait ",
|
|
553
|
+
"actually ", "okay ", "ok ",
|
|
554
|
+
"um ", "uh ", "like ", "hmm ", "yeah ", "hey ", "yo ", "so "
|
|
555
|
+
]
|
|
556
|
+
|
|
557
|
+
private let trailingNoise = [
|
|
558
|
+
" please", " for me", " real quick", " right now", " quickly",
|
|
559
|
+
" if you can", " when you get a chance", " at", " up"
|
|
560
|
+
]
|
|
561
|
+
|
|
562
|
+
private let focusPrefixes = [
|
|
563
|
+
"show me the ", "show me ", "show ", "focus on ", "focus the ", "focus ",
|
|
564
|
+
"switch over to ", "switch to ", "go back to ", "go to ",
|
|
565
|
+
"bring up the ", "bring up ", "bring forward ", "raise the ", "raise ",
|
|
566
|
+
"pull up the ", "pull up ", "i want to see ", "let me see ",
|
|
567
|
+
"take me to ", "give me the ", "give me ", "activate the ", "activate ",
|
|
568
|
+
"jump to ", "can i get ", "see "
|
|
569
|
+
]
|
|
570
|
+
|
|
571
|
+
private let launchPrefixes = [
|
|
572
|
+
"open up ", "open my ", "open the ", "open ",
|
|
573
|
+
"launch the ", "launch my ", "launch ",
|
|
574
|
+
"start working on ", "start up ", "start the ", "start my ", "start ",
|
|
575
|
+
"work on the ", "work on ",
|
|
576
|
+
"begin working on ", "begin ",
|
|
577
|
+
"fire up the ", "fire up ", "spin up ", "boot up ",
|
|
578
|
+
"load up ", "load ", "run the ", "run "
|
|
579
|
+
]
|
|
580
|
+
|
|
581
|
+
private let killPrefixes = [
|
|
582
|
+
"kill the ", "kill ", "stop the ", "stop ",
|
|
583
|
+
"shut down the ", "shut down ", "close the ", "close ",
|
|
584
|
+
"terminate the ", "terminate ", "end the ", "end "
|
|
585
|
+
]
|
|
586
|
+
|
|
587
|
+
private let genericNonCommandPhrases: Set<String> = [
|
|
588
|
+
"what time is it",
|
|
589
|
+
"tell me a joke",
|
|
590
|
+
"how are you doing",
|
|
591
|
+
"the weather today",
|
|
592
|
+
"play some music",
|
|
593
|
+
"set a timer for five minutes"
|
|
594
|
+
]
|
|
595
|
+
|
|
596
|
+
private let intentKeywords: [String: [String]] = [
|
|
597
|
+
"tile_window": ["tile", "snap", "move", "put", "throw", "left", "right", "top", "bottom", "center", "maximize", "full screen"],
|
|
598
|
+
"focus": ["show", "focus", "switch to", "go to", "bring up", "pull up", "activate", "jump to"],
|
|
599
|
+
"launch": ["open", "launch", "start", "begin", "fire up", "boot up", "work on", "run"],
|
|
600
|
+
"switch_layer": ["layer", "switch to", "next layer", "previous layer"],
|
|
601
|
+
"search": ["find", "search", "look for", "where is", "where d", "locate", "lost", "show me all", "windows"],
|
|
602
|
+
"list_windows": ["what s open", "list windows", "which windows", "what do i have open"],
|
|
603
|
+
"list_sessions": ["list sessions", "what s running", "which projects", "show my sessions"],
|
|
604
|
+
"distribute": ["distribute", "spread", "organize", "arrange", "tidy", "clean up", "grid"],
|
|
605
|
+
"create_layer": ["create layer", "save layout", "snapshot", "remember this layout"],
|
|
606
|
+
"kill": ["kill", "stop", "shut down", "close", "terminate", "end"],
|
|
607
|
+
"scan": ["scan", "rescan", "ocr", "read the screen", "what s on my screen", "screen text"],
|
|
608
|
+
"help": ["help", "what can i do", "what can you do", "commands", "options"]
|
|
609
|
+
]
|
|
610
|
+
|
|
611
|
+
private let searchPrefixes = [
|
|
612
|
+
"find all the ", "find all ", "find ",
|
|
613
|
+
"search for all ", "search for ", "search ",
|
|
614
|
+
"look for ", "look up ", "locate all ", "locate ",
|
|
615
|
+
"where is ", "where s ", "where does it say ", "where did i see ",
|
|
616
|
+
"which window has ", "which window shows ",
|
|
617
|
+
"help me find ", "can you find ",
|
|
618
|
+
"show me all the ", "show me all ", "show all the ", "show all ",
|
|
619
|
+
"open up all the ", "open up all ", "open all the ", "open all ",
|
|
620
|
+
"pull up everything with ", "pull up all ", "pull up ",
|
|
621
|
+
"bring up all the ", "bring up all ", "bring up my ",
|
|
622
|
+
"where d my ", "i lost my ", "i lost ", "where the hell is ",
|
|
623
|
+
"see all my ", "see all "
|
|
624
|
+
]
|
|
625
|
+
|
|
626
|
+
private let exactIntentMatches: [String: String] = [
|
|
627
|
+
"help": "help",
|
|
628
|
+
"help me": "help",
|
|
629
|
+
"what can i do": "help",
|
|
630
|
+
"what can you do": "help",
|
|
631
|
+
"how does this work": "help",
|
|
632
|
+
"what can i say": "help",
|
|
633
|
+
"what are my options": "help",
|
|
634
|
+
"show me the commands": "help",
|
|
635
|
+
"list windows": "list_windows",
|
|
636
|
+
"what windows do i have": "list_windows",
|
|
637
|
+
"list sessions": "list_sessions",
|
|
638
|
+
"show me my sessions": "list_sessions",
|
|
639
|
+
"rescan": "scan",
|
|
640
|
+
"do a scan": "scan",
|
|
641
|
+
"do a quick scan": "scan",
|
|
642
|
+
"scan the screen": "scan",
|
|
643
|
+
"read the screen": "scan",
|
|
644
|
+
"refresh the screen text": "scan",
|
|
645
|
+
"what s on my screen": "scan",
|
|
646
|
+
"what s on the screen": "scan",
|
|
647
|
+
"show me what s on the screen": "scan",
|
|
648
|
+
"organize": "distribute",
|
|
649
|
+
"organize my windows": "distribute",
|
|
650
|
+
"line everything up": "distribute",
|
|
651
|
+
"let s get everything organized": "distribute",
|
|
652
|
+
"get everything organized": "distribute",
|
|
653
|
+
"clean up the windows": "distribute",
|
|
654
|
+
"tidy up": "distribute"
|
|
655
|
+
]
|
|
656
|
+
|
|
657
|
+
private let supplementalExamples: [String: [String]] = [
|
|
658
|
+
"tile_window": ["put this on the left side", "move this over to the right", "maximize", "center it"],
|
|
659
|
+
"focus": ["i need to see chrome", "can i get safari up", "show me visual studio code"],
|
|
660
|
+
"launch": ["fire up vox", "start working on lattices", "open my notes app"],
|
|
661
|
+
"switch_layer": ["next layer", "previous layer", "switch to review"],
|
|
662
|
+
"search": ["where d my slack go", "pull up everything with dewey in it", "show me all the chrome windows", "dewey"],
|
|
663
|
+
"list_windows": ["what do i have open", "what windows do i have"],
|
|
664
|
+
"list_sessions": ["show me my sessions", "which projects are active"],
|
|
665
|
+
"distribute": ["tidy up", "line everything up", "clean up the windows"],
|
|
666
|
+
"create_layer": ["snapshot this", "remember this layout"],
|
|
667
|
+
"kill": ["close the dewey session", "stop my session"],
|
|
668
|
+
"scan": ["what s on my screen", "read the screen", "give me a fresh scan"],
|
|
669
|
+
"help": ["what can i say", "show me the commands"]
|
|
670
|
+
]
|
|
671
|
+
}
|