@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.
Files changed (111) hide show
  1. package/README.md +85 -9
  2. package/app/Info.plist +30 -0
  3. package/app/Lattices.app/Contents/Info.plist +8 -2
  4. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/app/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  6. package/app/Lattices.app/Contents/Resources/tap.wav +0 -0
  7. package/app/Lattices.app/Contents/_CodeSignature/CodeResources +139 -0
  8. package/app/Lattices.entitlements +15 -0
  9. package/app/Package.swift +8 -1
  10. package/app/Resources/tap.wav +0 -0
  11. package/app/Sources/AdvisorLearningStore.swift +90 -0
  12. package/app/Sources/AgentSession.swift +377 -0
  13. package/app/Sources/AppDelegate.swift +45 -12
  14. package/app/Sources/AppShellView.swift +81 -8
  15. package/app/Sources/AudioProvider.swift +386 -0
  16. package/app/Sources/CheatSheetHUD.swift +261 -19
  17. package/app/Sources/DaemonProtocol.swift +13 -0
  18. package/app/Sources/DaemonServer.swift +8 -0
  19. package/app/Sources/DesktopModel.swift +189 -6
  20. package/app/Sources/DesktopModelTypes.swift +2 -0
  21. package/app/Sources/DiagnosticLog.swift +104 -2
  22. package/app/Sources/EventBus.swift +1 -0
  23. package/app/Sources/HUDBottomBar.swift +279 -0
  24. package/app/Sources/HUDController.swift +1158 -0
  25. package/app/Sources/HUDLeftBar.swift +849 -0
  26. package/app/Sources/HUDMinimap.swift +179 -0
  27. package/app/Sources/HUDRightBar.swift +774 -0
  28. package/app/Sources/HUDState.swift +367 -0
  29. package/app/Sources/HUDTopBar.swift +243 -0
  30. package/app/Sources/HandsOffSession.swift +802 -0
  31. package/app/Sources/HomeDashboardView.swift +125 -0
  32. package/app/Sources/HotkeyManager.swift +2 -0
  33. package/app/Sources/HotkeyStore.swift +49 -9
  34. package/app/Sources/IntentEngine.swift +962 -0
  35. package/app/Sources/Intents/CreateLayerIntent.swift +54 -0
  36. package/app/Sources/Intents/DistributeIntent.swift +56 -0
  37. package/app/Sources/Intents/FocusIntent.swift +69 -0
  38. package/app/Sources/Intents/HelpIntent.swift +41 -0
  39. package/app/Sources/Intents/KillIntent.swift +47 -0
  40. package/app/Sources/Intents/LatticeIntent.swift +78 -0
  41. package/app/Sources/Intents/LaunchIntent.swift +67 -0
  42. package/app/Sources/Intents/ListSessionsIntent.swift +32 -0
  43. package/app/Sources/Intents/ListWindowsIntent.swift +30 -0
  44. package/app/Sources/Intents/ScanIntent.swift +52 -0
  45. package/app/Sources/Intents/SearchIntent.swift +190 -0
  46. package/app/Sources/Intents/SwitchLayerIntent.swift +50 -0
  47. package/app/Sources/Intents/TileIntent.swift +61 -0
  48. package/app/Sources/LatticesApi.swift +1275 -30
  49. package/app/Sources/LauncherHUD.swift +348 -0
  50. package/app/Sources/MainView.swift +147 -44
  51. package/app/Sources/MouseFinder.swift +222 -0
  52. package/app/Sources/OcrModel.swift +34 -1
  53. package/app/Sources/OmniSearchState.swift +99 -102
  54. package/app/Sources/OnboardingView.swift +457 -0
  55. package/app/Sources/PermissionChecker.swift +2 -12
  56. package/app/Sources/PiChatDock.swift +454 -0
  57. package/app/Sources/PiChatSession.swift +815 -0
  58. package/app/Sources/PiWorkspaceView.swift +364 -0
  59. package/app/Sources/PlacementSpec.swift +195 -0
  60. package/app/Sources/Preferences.swift +59 -0
  61. package/app/Sources/ProjectScanner.swift +58 -45
  62. package/app/Sources/ScreenMapState.swift +701 -55
  63. package/app/Sources/ScreenMapView.swift +843 -103
  64. package/app/Sources/ScreenMapWindowController.swift +22 -0
  65. package/app/Sources/SessionLayerStore.swift +285 -0
  66. package/app/Sources/SessionManager.swift +4 -1
  67. package/app/Sources/SettingsView.swift +186 -3
  68. package/app/Sources/Theme.swift +9 -8
  69. package/app/Sources/TmuxModel.swift +7 -0
  70. package/app/Sources/TmuxQuery.swift +27 -3
  71. package/app/Sources/VoiceChatView.swift +192 -0
  72. package/app/Sources/VoiceCommandWindow.swift +1594 -0
  73. package/app/Sources/VoiceIntentResolver.swift +671 -0
  74. package/app/Sources/VoxClient.swift +454 -0
  75. package/app/Sources/WindowTiler.swift +348 -87
  76. package/app/Sources/WorkspaceManager.swift +127 -18
  77. package/app/Tests/StageDragTests.swift +333 -0
  78. package/app/Tests/StageJoinTests.swift +313 -0
  79. package/app/Tests/StageManagerTests.swift +280 -0
  80. package/app/Tests/StageTileTests.swift +353 -0
  81. package/assets/AppIcon.icns +0 -0
  82. package/bin/client.ts +16 -0
  83. package/bin/{daemon-client.js → daemon-client.ts} +49 -30
  84. package/bin/handsoff-infer.ts +280 -0
  85. package/bin/handsoff-worker.ts +740 -0
  86. package/bin/lattices-app.ts +338 -0
  87. package/bin/lattices-dev +208 -0
  88. package/bin/{lattices.js → lattices.ts} +777 -140
  89. package/bin/project-twin.ts +645 -0
  90. package/docs/agent-execution-plan.md +562 -0
  91. package/docs/agent-layer-guide.md +207 -0
  92. package/docs/agents.md +142 -0
  93. package/docs/api.md +153 -34
  94. package/docs/app.md +29 -1
  95. package/docs/config.md +5 -1
  96. package/docs/handsoff-test-scenarios.md +84 -0
  97. package/docs/layers.md +20 -20
  98. package/docs/ocr.md +14 -5
  99. package/docs/overview.md +5 -1
  100. package/docs/presentation-execution-review.md +491 -0
  101. package/docs/prompts/hands-off-system.md +374 -0
  102. package/docs/prompts/hands-off-turn.md +30 -0
  103. package/docs/prompts/voice-advisor.md +31 -0
  104. package/docs/prompts/voice-fallback.md +23 -0
  105. package/docs/tiling-reference.md +167 -0
  106. package/docs/twins.md +138 -0
  107. package/docs/voice-command-protocol.md +278 -0
  108. package/docs/voice.md +219 -0
  109. package/package.json +29 -11
  110. package/bin/client.js +0 -4
  111. package/bin/lattices-app.js +0 -221
@@ -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
+ }