@lattices/cli 0.3.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.
Files changed (74) hide show
  1. package/README.md +155 -0
  2. package/app/Lattices.app/Contents/Info.plist +24 -0
  3. package/app/Package.swift +13 -0
  4. package/app/Sources/AccessibilityTextExtractor.swift +111 -0
  5. package/app/Sources/ActionRow.swift +61 -0
  6. package/app/Sources/App.swift +10 -0
  7. package/app/Sources/AppDelegate.swift +242 -0
  8. package/app/Sources/AppShellView.swift +62 -0
  9. package/app/Sources/AppTypeClassifier.swift +70 -0
  10. package/app/Sources/AppWindowShell.swift +63 -0
  11. package/app/Sources/CheatSheetHUD.swift +332 -0
  12. package/app/Sources/CommandModeState.swift +1362 -0
  13. package/app/Sources/CommandModeView.swift +1405 -0
  14. package/app/Sources/CommandModeWindow.swift +192 -0
  15. package/app/Sources/CommandPaletteView.swift +307 -0
  16. package/app/Sources/CommandPaletteWindow.swift +134 -0
  17. package/app/Sources/DaemonProtocol.swift +101 -0
  18. package/app/Sources/DaemonServer.swift +414 -0
  19. package/app/Sources/DesktopModel.swift +149 -0
  20. package/app/Sources/DesktopModelTypes.swift +71 -0
  21. package/app/Sources/DiagnosticLog.swift +271 -0
  22. package/app/Sources/EventBus.swift +30 -0
  23. package/app/Sources/HotkeyManager.swift +254 -0
  24. package/app/Sources/HotkeyStore.swift +338 -0
  25. package/app/Sources/InventoryManager.swift +35 -0
  26. package/app/Sources/InventoryPath.swift +43 -0
  27. package/app/Sources/KeyRecorderView.swift +210 -0
  28. package/app/Sources/LatticesApi.swift +1234 -0
  29. package/app/Sources/LayerBezel.swift +203 -0
  30. package/app/Sources/MainView.swift +479 -0
  31. package/app/Sources/MainWindow.swift +83 -0
  32. package/app/Sources/OcrModel.swift +430 -0
  33. package/app/Sources/OcrStore.swift +329 -0
  34. package/app/Sources/OmniSearchState.swift +283 -0
  35. package/app/Sources/OmniSearchView.swift +288 -0
  36. package/app/Sources/OmniSearchWindow.swift +105 -0
  37. package/app/Sources/OrphanRow.swift +129 -0
  38. package/app/Sources/PaletteCommand.swift +419 -0
  39. package/app/Sources/PermissionChecker.swift +125 -0
  40. package/app/Sources/Preferences.swift +99 -0
  41. package/app/Sources/ProcessModel.swift +199 -0
  42. package/app/Sources/ProcessQuery.swift +151 -0
  43. package/app/Sources/Project.swift +28 -0
  44. package/app/Sources/ProjectRow.swift +368 -0
  45. package/app/Sources/ProjectScanner.swift +128 -0
  46. package/app/Sources/ScreenMapState.swift +2387 -0
  47. package/app/Sources/ScreenMapView.swift +2820 -0
  48. package/app/Sources/ScreenMapWindowController.swift +89 -0
  49. package/app/Sources/SessionManager.swift +72 -0
  50. package/app/Sources/SettingsView.swift +1064 -0
  51. package/app/Sources/SettingsWindow.swift +20 -0
  52. package/app/Sources/TabGroupRow.swift +178 -0
  53. package/app/Sources/Terminal.swift +259 -0
  54. package/app/Sources/TerminalQuery.swift +156 -0
  55. package/app/Sources/TerminalSynthesizer.swift +200 -0
  56. package/app/Sources/Theme.swift +163 -0
  57. package/app/Sources/TilePickerView.swift +209 -0
  58. package/app/Sources/TmuxModel.swift +53 -0
  59. package/app/Sources/TmuxQuery.swift +81 -0
  60. package/app/Sources/WindowTiler.swift +1778 -0
  61. package/app/Sources/WorkspaceManager.swift +575 -0
  62. package/bin/client.js +4 -0
  63. package/bin/daemon-client.js +187 -0
  64. package/bin/lattices-app.js +221 -0
  65. package/bin/lattices.js +1551 -0
  66. package/docs/api.md +924 -0
  67. package/docs/app.md +297 -0
  68. package/docs/concepts.md +135 -0
  69. package/docs/config.md +245 -0
  70. package/docs/layers.md +410 -0
  71. package/docs/ocr.md +185 -0
  72. package/docs/overview.md +94 -0
  73. package/docs/quickstart.md +75 -0
  74. package/package.json +42 -0
@@ -0,0 +1,329 @@
1
+ import Foundation
2
+ import SQLite3
3
+
4
+ // MARK: - Search Result
5
+
6
+ struct OcrSearchResult {
7
+ let id: Int64
8
+ let wid: UInt32
9
+ let app: String
10
+ let title: String
11
+ let frame: WindowFrame
12
+ let fullText: String
13
+ let snippet: String
14
+ let timestamp: Date
15
+ let source: TextSource
16
+ }
17
+
18
+ // MARK: - SQLite OCR Store
19
+
20
+ final class OcrStore {
21
+ static let shared = OcrStore()
22
+
23
+ private var db: OpaquePointer?
24
+ private let queue = DispatchQueue(label: "com.arach.lattices.ocrstore", qos: .background)
25
+
26
+ // Cached prepared statements
27
+ private var insertStmt: OpaquePointer?
28
+ private var cleanupStmt: OpaquePointer?
29
+
30
+ private let sqliteTransient = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
31
+
32
+ // MARK: - Open / Schema
33
+
34
+ func open() {
35
+ queue.sync {
36
+ guard db == nil else { return }
37
+
38
+ let dir = NSHomeDirectory() + "/.lattices"
39
+ try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
40
+ let path = dir + "/ocr.db"
41
+
42
+ guard sqlite3_open_v2(path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nil) == SQLITE_OK else {
43
+ DiagnosticLog.shared.error("OcrStore: failed to open \(path)")
44
+ return
45
+ }
46
+
47
+ // WAL mode for concurrent reads/writes
48
+ exec("PRAGMA journal_mode=WAL")
49
+ exec("PRAGMA synchronous=NORMAL")
50
+
51
+ // Main table
52
+ exec("""
53
+ CREATE TABLE IF NOT EXISTS ocr_entry (
54
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
55
+ wid INTEGER NOT NULL,
56
+ app TEXT NOT NULL,
57
+ title TEXT NOT NULL,
58
+ frame_x REAL,
59
+ frame_y REAL,
60
+ frame_w REAL,
61
+ frame_h REAL,
62
+ full_text TEXT NOT NULL,
63
+ timestamp REAL NOT NULL
64
+ )
65
+ """)
66
+ exec("CREATE INDEX IF NOT EXISTS idx_ocr_entry_timestamp ON ocr_entry(timestamp)")
67
+ exec("CREATE INDEX IF NOT EXISTS idx_ocr_entry_wid ON ocr_entry(wid)")
68
+
69
+ // FTS5 content-sync table
70
+ exec("""
71
+ CREATE VIRTUAL TABLE IF NOT EXISTS ocr_fts USING fts5(
72
+ full_text, app, title,
73
+ content='ocr_entry', content_rowid='id'
74
+ )
75
+ """)
76
+
77
+ // Triggers to keep FTS in sync
78
+ exec("""
79
+ CREATE TRIGGER IF NOT EXISTS ocr_fts_ai AFTER INSERT ON ocr_entry BEGIN
80
+ INSERT INTO ocr_fts(rowid, full_text, app, title)
81
+ VALUES (new.id, new.full_text, new.app, new.title);
82
+ END
83
+ """)
84
+ exec("""
85
+ CREATE TRIGGER IF NOT EXISTS ocr_fts_ad AFTER DELETE ON ocr_entry BEGIN
86
+ INSERT INTO ocr_fts(ocr_fts, rowid, full_text, app, title)
87
+ VALUES ('delete', old.id, old.full_text, old.app, old.title);
88
+ END
89
+ """)
90
+ exec("""
91
+ CREATE TRIGGER IF NOT EXISTS ocr_fts_au AFTER UPDATE ON ocr_entry BEGIN
92
+ INSERT INTO ocr_fts(ocr_fts, rowid, full_text, app, title)
93
+ VALUES ('delete', old.id, old.full_text, old.app, old.title);
94
+ INSERT INTO ocr_fts(rowid, full_text, app, title)
95
+ VALUES (new.id, new.full_text, new.app, new.title);
96
+ END
97
+ """)
98
+
99
+ // Migration: add source column if not present
100
+ migrateAddSourceColumn()
101
+
102
+ // Prepare cached statements
103
+ prepareInsert()
104
+ prepareCleanup()
105
+
106
+ // Run cleanup on open
107
+ cleanupSync(olderThanDays: 3)
108
+
109
+ DiagnosticLog.shared.info("OcrStore: opened \(path)")
110
+ }
111
+ }
112
+
113
+ // MARK: - Insert (batch, async)
114
+
115
+ func insert(results: [OcrWindowResult]) {
116
+ guard !results.isEmpty else { return }
117
+ queue.async { [weak self] in
118
+ guard let self, let db = self.db, let stmt = self.insertStmt else { return }
119
+
120
+ sqlite3_exec(db, "BEGIN TRANSACTION", nil, nil, nil)
121
+
122
+ for r in results {
123
+ let ts = r.timestamp.timeIntervalSince1970
124
+ sqlite3_bind_int(stmt, 1, Int32(r.wid))
125
+ self.bindText(stmt, 2, r.app)
126
+ self.bindText(stmt, 3, r.title)
127
+ sqlite3_bind_double(stmt, 4, r.frame.x)
128
+ sqlite3_bind_double(stmt, 5, r.frame.y)
129
+ sqlite3_bind_double(stmt, 6, r.frame.w)
130
+ sqlite3_bind_double(stmt, 7, r.frame.h)
131
+ self.bindText(stmt, 8, r.fullText)
132
+ sqlite3_bind_double(stmt, 9, ts)
133
+ self.bindText(stmt, 10, r.source.rawValue)
134
+ sqlite3_step(stmt)
135
+ sqlite3_reset(stmt)
136
+ sqlite3_clear_bindings(stmt)
137
+ }
138
+
139
+ sqlite3_exec(db, "COMMIT", nil, nil, nil)
140
+ }
141
+ }
142
+
143
+ // MARK: - Search (FTS5, synchronous)
144
+
145
+ func search(query: String, app: String? = nil, limit: Int = 50) -> [OcrSearchResult] {
146
+ guard let db else { return [] }
147
+
148
+ var sql = """
149
+ SELECT e.id, e.wid, e.app, e.title,
150
+ e.frame_x, e.frame_y, e.frame_w, e.frame_h,
151
+ e.full_text, e.timestamp,
152
+ snippet(ocr_fts, 0, '»', '«', '…', 32) AS snip,
153
+ COALESCE(e.source, 'ocr') AS source
154
+ FROM ocr_fts f
155
+ JOIN ocr_entry e ON e.id = f.rowid
156
+ WHERE ocr_fts MATCH ?1
157
+ """
158
+ if app != nil { sql += " AND e.app = ?2" }
159
+ sql += " ORDER BY rank LIMIT ?3"
160
+
161
+ var stmt: OpaquePointer?
162
+ guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else { return [] }
163
+ defer { sqlite3_finalize(stmt) }
164
+
165
+ bindText(stmt!, 1, query)
166
+ if let app { bindText(stmt!, 2, app) }
167
+ sqlite3_bind_int(stmt!, 3, Int32(limit))
168
+
169
+ var results: [OcrSearchResult] = []
170
+ while sqlite3_step(stmt) == SQLITE_ROW {
171
+ results.append(rowToSearchResult(stmt!))
172
+ }
173
+ return results
174
+ }
175
+
176
+ // MARK: - History (per-window, synchronous)
177
+
178
+ func history(wid: UInt32, limit: Int = 50) -> [OcrSearchResult] {
179
+ guard let db else { return [] }
180
+
181
+ let sql = """
182
+ SELECT id, wid, app, title,
183
+ frame_x, frame_y, frame_w, frame_h,
184
+ full_text, timestamp, '' AS snip,
185
+ COALESCE(source, 'ocr') AS source
186
+ FROM ocr_entry
187
+ WHERE wid = ?1
188
+ ORDER BY timestamp DESC
189
+ LIMIT ?2
190
+ """
191
+
192
+ var stmt: OpaquePointer?
193
+ guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else { return [] }
194
+ defer { sqlite3_finalize(stmt) }
195
+
196
+ sqlite3_bind_int(stmt!, 1, Int32(wid))
197
+ sqlite3_bind_int(stmt!, 2, Int32(limit))
198
+
199
+ var results: [OcrSearchResult] = []
200
+ while sqlite3_step(stmt) == SQLITE_ROW {
201
+ results.append(rowToSearchResult(stmt!))
202
+ }
203
+ return results
204
+ }
205
+
206
+ // MARK: - Recent (chronological, synchronous)
207
+
208
+ func recent(limit: Int = 50) -> [OcrSearchResult] {
209
+ guard let db else { return [] }
210
+
211
+ let sql = """
212
+ SELECT id, wid, app, title,
213
+ frame_x, frame_y, frame_w, frame_h,
214
+ full_text, timestamp, '' AS snip,
215
+ COALESCE(source, 'ocr') AS source
216
+ FROM ocr_entry
217
+ ORDER BY timestamp DESC
218
+ LIMIT ?1
219
+ """
220
+
221
+ var stmt: OpaquePointer?
222
+ guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else { return [] }
223
+ defer { sqlite3_finalize(stmt) }
224
+
225
+ sqlite3_bind_int(stmt!, 1, Int32(limit))
226
+
227
+ var results: [OcrSearchResult] = []
228
+ while sqlite3_step(stmt) == SQLITE_ROW {
229
+ results.append(rowToSearchResult(stmt!))
230
+ }
231
+ return results
232
+ }
233
+
234
+ // MARK: - Cleanup
235
+
236
+ private func cleanupSync(olderThanDays days: Int) {
237
+ guard let db, let stmt = cleanupStmt else { return }
238
+ let cutoff = Date().timeIntervalSince1970 - Double(days * 86400)
239
+ sqlite3_bind_double(stmt, 1, cutoff)
240
+ sqlite3_step(stmt)
241
+ sqlite3_reset(stmt)
242
+ sqlite3_clear_bindings(stmt)
243
+
244
+ let deleted = sqlite3_changes(db)
245
+ if deleted > 0 {
246
+ DiagnosticLog.shared.info("OcrStore: cleaned up \(deleted) entries older than \(days) days")
247
+ }
248
+ }
249
+
250
+ // MARK: - Helpers
251
+
252
+ private func exec(_ sql: String) {
253
+ guard let db else { return }
254
+ var err: UnsafeMutablePointer<CChar>?
255
+ if sqlite3_exec(db, sql, nil, nil, &err) != SQLITE_OK {
256
+ let msg = err.map { String(cString: $0) } ?? "unknown"
257
+ DiagnosticLog.shared.error("OcrStore SQL error: \(msg)")
258
+ sqlite3_free(err)
259
+ }
260
+ }
261
+
262
+ private func bindText(_ stmt: OpaquePointer, _ index: Int32, _ value: String) {
263
+ sqlite3_bind_text(stmt, index, value, -1, sqliteTransient)
264
+ }
265
+
266
+ private func prepareInsert() {
267
+ let sql = """
268
+ INSERT INTO ocr_entry (wid, app, title, frame_x, frame_y, frame_w, frame_h, full_text, timestamp, source)
269
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
270
+ """
271
+ sqlite3_prepare_v2(db, sql, -1, &insertStmt, nil)
272
+ }
273
+
274
+ private func migrateAddSourceColumn() {
275
+ // Check if source column exists
276
+ var tableInfoStmt: OpaquePointer?
277
+ let sql = "PRAGMA table_info(ocr_entry)"
278
+ guard sqlite3_prepare_v2(db, sql, -1, &tableInfoStmt, nil) == SQLITE_OK else { return }
279
+ defer { sqlite3_finalize(tableInfoStmt) }
280
+
281
+ var hasSource = false
282
+ while sqlite3_step(tableInfoStmt) == SQLITE_ROW {
283
+ if let name = sqlite3_column_text(tableInfoStmt, 1) {
284
+ if String(cString: name) == "source" {
285
+ hasSource = true
286
+ break
287
+ }
288
+ }
289
+ }
290
+
291
+ if !hasSource {
292
+ exec("ALTER TABLE ocr_entry ADD COLUMN source TEXT DEFAULT 'ocr'")
293
+ DiagnosticLog.shared.info("OcrStore: migrated — added source column")
294
+ }
295
+ }
296
+
297
+ private func prepareCleanup() {
298
+ let sql = "DELETE FROM ocr_entry WHERE timestamp < ?1"
299
+ sqlite3_prepare_v2(db, sql, -1, &cleanupStmt, nil)
300
+ }
301
+
302
+ private func columnText(_ stmt: OpaquePointer, _ index: Int32) -> String {
303
+ if let cStr = sqlite3_column_text(stmt, index) {
304
+ return String(cString: cStr)
305
+ }
306
+ return ""
307
+ }
308
+
309
+ private func rowToSearchResult(_ stmt: OpaquePointer) -> OcrSearchResult {
310
+ let sourceStr = columnText(stmt, 11)
311
+ let source = TextSource(rawValue: sourceStr) ?? .ocr
312
+ return OcrSearchResult(
313
+ id: sqlite3_column_int64(stmt, 0),
314
+ wid: UInt32(sqlite3_column_int(stmt, 1)),
315
+ app: columnText(stmt, 2),
316
+ title: columnText(stmt, 3),
317
+ frame: WindowFrame(
318
+ x: sqlite3_column_double(stmt, 4),
319
+ y: sqlite3_column_double(stmt, 5),
320
+ w: sqlite3_column_double(stmt, 6),
321
+ h: sqlite3_column_double(stmt, 7)
322
+ ),
323
+ fullText: columnText(stmt, 8),
324
+ snippet: columnText(stmt, 10),
325
+ timestamp: Date(timeIntervalSince1970: sqlite3_column_double(stmt, 9)),
326
+ source: source
327
+ )
328
+ }
329
+ }
@@ -0,0 +1,283 @@
1
+ import AppKit
2
+ import Combine
3
+ import Foundation
4
+
5
+ // MARK: - Result Types
6
+
7
+ enum OmniResultKind: String {
8
+ case window
9
+ case project
10
+ case session
11
+ case process
12
+ case ocrContent
13
+ }
14
+
15
+ struct OmniResult: Identifiable {
16
+ let id = UUID()
17
+ let kind: OmniResultKind
18
+ let title: String
19
+ let subtitle: String
20
+ let icon: String
21
+ let score: Int // higher = better match
22
+ let action: () -> Void
23
+
24
+ /// Group label for display
25
+ var groupLabel: String {
26
+ switch kind {
27
+ case .window: return "Windows"
28
+ case .project: return "Projects"
29
+ case .session: return "Sessions"
30
+ case .process: return "Processes"
31
+ case .ocrContent: return "Screen Text"
32
+ }
33
+ }
34
+ }
35
+
36
+ // MARK: - Activity Summary
37
+
38
+ struct ActivitySummary {
39
+ struct AppWindowCount: Identifiable {
40
+ let id: String
41
+ let appName: String
42
+ let count: Int
43
+ }
44
+
45
+ struct SessionInfo: Identifiable {
46
+ let id: String
47
+ let name: String
48
+ let paneCount: Int
49
+ let attached: Bool
50
+ }
51
+
52
+ let windowsByApp: [AppWindowCount]
53
+ let totalWindows: Int
54
+ let sessions: [SessionInfo]
55
+ let interestingProcesses: [ProcessEntry]
56
+ let lastOcrScan: Date?
57
+ let ocrWindowCount: Int
58
+ }
59
+
60
+ // MARK: - State
61
+
62
+ final class OmniSearchState: ObservableObject {
63
+ @Published var query: String = ""
64
+ @Published var results: [OmniResult] = []
65
+ @Published var selectedIndex: Int = 0
66
+ @Published var activitySummary: ActivitySummary?
67
+
68
+ private var cancellables = Set<AnyCancellable>()
69
+ private var debounceTimer: AnyCancellable?
70
+
71
+ init() {
72
+ // Debounce search by 150ms
73
+ debounceTimer = $query
74
+ .debounce(for: .milliseconds(150), scheduler: RunLoop.main)
75
+ .sink { [weak self] q in
76
+ if q.isEmpty {
77
+ self?.results = []
78
+ self?.refreshSummary()
79
+ } else {
80
+ self?.search(q)
81
+ }
82
+ }
83
+
84
+ refreshSummary()
85
+ }
86
+
87
+ // MARK: - Search
88
+
89
+ private func search(_ query: String) {
90
+ let q = query.lowercased()
91
+ var all: [OmniResult] = []
92
+
93
+ // Windows
94
+ let desktop = DesktopModel.shared
95
+ for win in desktop.allWindows() {
96
+ let score = scoreMatch(q, against: [win.app, win.title])
97
+ if score > 0 {
98
+ let wid = win.wid
99
+ let pid = win.pid
100
+ all.append(OmniResult(
101
+ kind: .window,
102
+ title: win.app,
103
+ subtitle: win.title.isEmpty ? "Window \(win.wid)" : win.title,
104
+ icon: "macwindow",
105
+ score: score
106
+ ) {
107
+ WindowTiler.focusWindow(wid: wid, pid: pid)
108
+ })
109
+ }
110
+ }
111
+
112
+ // Projects
113
+ let scanner = ProjectScanner.shared
114
+ for project in scanner.projects {
115
+ let score = scoreMatch(q, against: [project.name, project.path])
116
+ if score > 0 {
117
+ let proj = project
118
+ all.append(OmniResult(
119
+ kind: .project,
120
+ title: project.name,
121
+ subtitle: project.path,
122
+ icon: "folder",
123
+ score: score
124
+ ) {
125
+ SessionManager.launch(project: proj)
126
+ })
127
+ }
128
+ }
129
+
130
+ // Tmux Sessions
131
+ let tmux = TmuxModel.shared
132
+ for session in tmux.sessions {
133
+ let paneCommands = session.panes.map(\.currentCommand)
134
+ let score = scoreMatch(q, against: [session.name] + paneCommands)
135
+ if score > 0 {
136
+ let name = session.name
137
+ all.append(OmniResult(
138
+ kind: .session,
139
+ title: session.name,
140
+ subtitle: "\(session.windowCount) windows, \(session.panes.count) panes\(session.attached ? " (attached)" : "")",
141
+ icon: "terminal",
142
+ score: score
143
+ ) {
144
+ let terminal = Preferences.shared.terminal
145
+ terminal.focusOrAttach(session: name)
146
+ })
147
+ }
148
+ }
149
+
150
+ // Processes
151
+ let processes = ProcessModel.shared
152
+ for proc in processes.interesting {
153
+ let score = scoreMatch(q, against: [proc.comm, proc.args, proc.cwd ?? ""])
154
+ if score > 0 {
155
+ all.append(OmniResult(
156
+ kind: .process,
157
+ title: proc.comm,
158
+ subtitle: proc.cwd ?? proc.args,
159
+ icon: "gearshape",
160
+ score: score
161
+ ) {
162
+ // No direct action for processes — just informational
163
+ })
164
+ }
165
+ }
166
+
167
+ // OCR content
168
+ let ocr = OcrModel.shared
169
+ for (_, result) in ocr.results {
170
+ let ocrScore = scoreOcr(q, fullText: result.fullText)
171
+ if ocrScore > 0 {
172
+ let wid = result.wid
173
+ let pid = desktop.windows[wid]?.pid ?? 0
174
+ // Find matching line for subtitle
175
+ let matchLine = result.texts
176
+ .first { $0.text.lowercased().contains(q) }?
177
+ .text ?? String(result.fullText.prefix(80))
178
+ all.append(OmniResult(
179
+ kind: .ocrContent,
180
+ title: "\(result.app) — \(result.title)",
181
+ subtitle: matchLine,
182
+ icon: "doc.text.magnifyingglass",
183
+ score: ocrScore
184
+ ) {
185
+ WindowTiler.focusWindow(wid: wid, pid: pid)
186
+ })
187
+ }
188
+ }
189
+
190
+ // Sort by score descending
191
+ all.sort { $0.score > $1.score }
192
+
193
+ results = all
194
+ selectedIndex = 0
195
+ }
196
+
197
+ // MARK: - Scoring
198
+
199
+ private func scoreMatch(_ query: String, against fields: [String]) -> Int {
200
+ var best = 0
201
+ for field in fields {
202
+ let lower = field.lowercased()
203
+ if lower == query {
204
+ best = max(best, 100) // exact
205
+ } else if lower.hasPrefix(query) {
206
+ best = max(best, 80) // prefix
207
+ } else if lower.contains(query) {
208
+ best = max(best, 60) // contains
209
+ }
210
+ }
211
+ return best
212
+ }
213
+
214
+ private func scoreOcr(_ query: String, fullText: String) -> Int {
215
+ let lower = fullText.lowercased()
216
+ if lower.contains(query) { return 40 }
217
+ return 0
218
+ }
219
+
220
+ // MARK: - Navigation
221
+
222
+ func moveSelection(_ delta: Int) {
223
+ guard !results.isEmpty else { return }
224
+ selectedIndex = max(0, min(results.count - 1, selectedIndex + delta))
225
+ }
226
+
227
+ func activateSelected() {
228
+ guard selectedIndex >= 0, selectedIndex < results.count else { return }
229
+ results[selectedIndex].action()
230
+ }
231
+
232
+ // MARK: - Activity Summary
233
+
234
+ func refreshSummary() {
235
+ let desktop = DesktopModel.shared
236
+ let windows = desktop.allWindows()
237
+
238
+ // Group by app
239
+ var appCounts: [String: Int] = [:]
240
+ for win in windows {
241
+ appCounts[win.app, default: 0] += 1
242
+ }
243
+ let windowsByApp = appCounts
244
+ .sorted { $0.value > $1.value }
245
+ .map { ActivitySummary.AppWindowCount(id: $0.key, appName: $0.key, count: $0.value) }
246
+
247
+ // Sessions
248
+ let sessions = TmuxModel.shared.sessions.map {
249
+ ActivitySummary.SessionInfo(
250
+ id: $0.id,
251
+ name: $0.name,
252
+ paneCount: $0.panes.count,
253
+ attached: $0.attached
254
+ )
255
+ }
256
+
257
+ // Processes
258
+ let procs = ProcessModel.shared.interesting
259
+
260
+ // OCR info
261
+ let ocrResults = OcrModel.shared.results
262
+ let lastScan: Date? = ocrResults.values.map(\.timestamp).max()
263
+
264
+ activitySummary = ActivitySummary(
265
+ windowsByApp: windowsByApp,
266
+ totalWindows: windows.count,
267
+ sessions: sessions,
268
+ interestingProcesses: procs,
269
+ lastOcrScan: lastScan,
270
+ ocrWindowCount: ocrResults.count
271
+ )
272
+ }
273
+
274
+ /// Grouped results for display
275
+ var groupedResults: [(String, [OmniResult])] {
276
+ let groups = Dictionary(grouping: results) { $0.groupLabel }
277
+ let order: [String] = ["Windows", "Projects", "Sessions", "Processes", "Screen Text"]
278
+ return order.compactMap { key in
279
+ guard let items = groups[key], !items.isEmpty else { return nil }
280
+ return (key, items)
281
+ }
282
+ }
283
+ }