// Agent search: a Spotlight-style window (global shortcut) that fuzzy-finds // any agent and jumps to it. // // - Opens centered on the screen with the mouse, takes the keyboard without // activating dingger (a non-activating key panel), so focus returns to // where it was when it closes. // - Empty query: every agent in the menu's order, so Enter jumps to the top // waiting agent, like the jump shortcut. // - Typing filters fzf-style: each word must match, letters in order, one of // the agent's name, session, window, harness, status, or directory. // - The selection follows the agent, not the position, while states change. // - ↑↓ (or ⌃N ⌃P, ⌃J ⌃K) select, ↩ or click jumps, esc closes, ⌃U clears. import AppKit // MARK: matching (pure, tested by --selftest) private func lowered(_ text: String) -> [Character] { text.map { Character(String($0).lowercased().first.map(String.init) ?? String($0)) } } /// fzf-style subsequence match of `term` (already lowercased) in `text`: /// nil when the letters do not all appear in order; otherwise a score and /// the matched character positions. Word starts and runs of consecutive /// letters score higher, gaps lower. func fuzzyMatch(_ term: [Character], in text: String) -> (score: Int, positions: [Int])? { guard !term.isEmpty else { return (0, []) } let original = Array(text) let lower = lowered(text) func boundary(_ index: Int) -> Bool { if index == 0 { return true } let previous = original[index - 1], current = original[index] if !(previous.isLetter || previous.isNumber) { return true } return current.isUppercase && previous.isLowercase } // fzf v2-style: the best-scoring alignment, not the first one found. // score[i][j]: best score with term[0...i] matched and term[i] at j. let n = lower.count, m = term.count guard m <= n else { return nil } let none = Int.min var score = Array(repeating: Array(repeating: none, count: n), count: m) var from = Array(repeating: Array(repeating: -1, count: n), count: m) for i in 0.. score[i][j] { score[i][j] = candidate; from[i][j] = k } } } } guard let end = (0.. 0 { j = from[i][j]; i -= 1; positions.insert(j, at: 0) } return (score[m - 1][end], positions) } struct SearchMatch: Equatable { let row: Row let score: Int /// Matched character positions in the row's title (`row.label`). let labelPositions: [Int] } /// What a row can be found by; the title first. func searchFields(_ row: Row) -> [String] { [row.name.isEmpty ? row.agent : row.name, row.session, row.window, row.agent, row.status.rawValue, row.path] } /// Rows matching every word of `query`, best first; ties keep the given /// order. An empty query returns every row in order. A word also matches /// the draft in an agent's input box, as written (not fuzzily: a long text /// would match almost anything), ranking below the other fields. func search(_ rows: [Row], _ query: String) -> [SearchMatch] { let terms = query.split(whereSeparator: \.isWhitespace).map { lowered(String($0)) } var matches: [(match: SearchMatch, order: Int)] = [] for (order, row) in rows.enumerated() { let fields = searchFields(row) let draft = String(lowered(row.draft)) var total = 0 var labelPositions = Set() var matchedAll = true for term in terms { var best: (score: Int, field: Int, positions: [Int])? for (field, text) in fields.enumerated() { if let found = fuzzyMatch(term, in: text), best == nil || found.score > best!.score { best = (found.score, field, found.positions) } } if best == nil, draft.contains(String(term)) { best = (term.count * 8, fields.count, []) } guard let best else { matchedAll = false; break } total += best.score if best.field == 0 { labelPositions.formUnion(best.positions) } } guard matchedAll else { continue } matches.append((SearchMatch(row: row, score: total, labelPositions: labelPositions.sorted()), order)) } matches.sort { $0.match.score != $1.match.score ? $0.match.score > $1.match.score : $0.order < $1.order } return matches.map(\.match) } /// The search window's state: query, results, and the selected agent. struct SearchState: Equatable { static let visibleRows = 10 private(set) var rows: [Row] private(set) var query = "" private(set) var results: [SearchMatch] = [] /// The selected agent's pane, so the selection survives reordering. private(set) var selected: String? /// First result shown; the list scrolls to keep the selection visible. private(set) var offset = 0 init(rows: [Row]) { self.rows = rows recompute(keepSelection: false) } var selectedIndex: Int? { results.firstIndex { $0.row.paneId == selected } } var visible: ArraySlice { results[offset..= offset + Self.visibleRows { offset = index - Self.visibleRows + 1 } offset = max(0, min(offset, max(0, results.count - Self.visibleRows))) } var selectedRow: Row? { results.first { $0.row.paneId == selected }?.row } var footer: String { let count = results.count == rows.count ? "\(rows.count) agents" : "\(results.count) of \(rows.count)" let unread = selectedRow?.canMarkUnread == true ? " ⌘U mark unread" : "" return "\(count) ↑↓ select ↩ jump\(unread) esc close" } /// Printed by --print-search, and compared to skip needless re-renders. var plainText: String { var lines: [String] = [] if results.isEmpty { lines.append(rows.isEmpty ? "No agents" : "No agents match '\(query)'") } for match in visible { let row = match.row let marker = row.paneId == selected ? "›" : " " lines.append("\(marker) \(row.panelText)") } lines.append(footer) return lines.joined(separator: "\n") } } // MARK: window private let searchWidth: CGFloat = 580 /// A borderless panel that can take the keyboard without activating dingger. private final class KeyPanel: NSPanel { override var canBecomeKey: Bool { true } } /// Title with the matched letters highlighted. private func highlightedTitle(_ match: SearchMatch) -> NSAttributedString { let text = NSMutableAttributedString( attributedString: rowTitle(match.row, primary: mochaText, secondary: mochaSubtext)) guard !match.labelPositions.isEmpty else { return text } let prefix = rowTitlePrefix(match.row).utf16.count let font = NSFont.boldSystemFont(ofSize: NSFont.menuFont(ofSize: 0).pointSize) let label = Array(match.row.label) var utf16Offset = prefix let wanted = Set(match.labelPositions) for (index, character) in label.enumerated() { let length = String(character).utf16.count if wanted.contains(index) { let range = NSRange(location: utf16Offset, length: length) text.addAttributes([.foregroundColor: mochaBlue, .font: font], range: range) } utf16Offset += length } return text } final class SearchController: NSObject, NSWindowDelegate, NSTextFieldDelegate { private let panel: KeyPanel private let field = NSTextField() private let list = NSStackView() private let onJump: (String) -> Void private let onMarkUnread: (String) -> Void private var state = SearchState(rows: []) private var rendered: String? private var keyMonitor: Any? init(onJump: @escaping (String) -> Void, onMarkUnread: @escaping (String) -> Void = { _ in }) { self.onJump = onJump self.onMarkUnread = onMarkUnread panel = KeyPanel(contentRect: NSRect(x: 0, y: 0, width: searchWidth, height: 80), styleMask: [.nonactivatingPanel, .borderless], backing: .buffered, defer: false) super.init() panel.isFloatingPanel = true panel.level = NSWindow.Level(rawValue: NSWindow.Level.floating.rawValue + 1) panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] panel.hidesOnDeactivate = false panel.becomesKeyOnlyIfNeeded = false panel.backgroundColor = .clear panel.isOpaque = false panel.hasShadow = true panel.appearance = NSAppearance(named: .darkAqua) panel.delegate = self let background = darkHUD() panel.contentView = background field.isBezeled = false field.drawsBackground = false field.focusRingType = .none field.font = .systemFont(ofSize: 20) field.textColor = mochaText field.placeholderAttributedString = NSAttributedString( string: "Search agents", attributes: [.font: NSFont.systemFont(ofSize: 20), .foregroundColor: mochaOverlay]) field.cell?.usesSingleLineMode = true field.cell?.isScrollable = true field.delegate = self let line = NSView() line.wantsLayer = true line.layer?.backgroundColor = mochaOverlay.withAlphaComponent(0.4).cgColor list.orientation = .vertical list.alignment = .leading list.spacing = 0 for view in [field, line, list] { view.translatesAutoresizingMaskIntoConstraints = false background.addSubview(view) } NSLayoutConstraint.activate([ field.leadingAnchor.constraint(equalTo: background.leadingAnchor, constant: 18), field.trailingAnchor.constraint(equalTo: background.trailingAnchor, constant: -18), field.topAnchor.constraint(equalTo: background.topAnchor, constant: 14), line.heightAnchor.constraint(equalToConstant: 1), line.leadingAnchor.constraint(equalTo: background.leadingAnchor), line.trailingAnchor.constraint(equalTo: background.trailingAnchor), line.topAnchor.constraint(equalTo: field.bottomAnchor, constant: 12), list.leadingAnchor.constraint(equalTo: background.leadingAnchor, constant: 8), list.trailingAnchor.constraint(equalTo: background.trailingAnchor, constant: -8), list.topAnchor.constraint(equalTo: line.bottomAnchor, constant: 6), ]) } var isOpen: Bool { panel.isVisible } func open(rows: [Row]) { state = SearchState(rows: rows) field.stringValue = "" rendered = nil render() // Spotlight's spot: centered, a fifth of the way down the screen the // mouse is on. let mouse = NSEvent.mouseLocation let screen = NSScreen.screens.first { $0.frame.contains(mouse) } ?? NSScreen.main if let area = screen?.visibleFrame { panel.setFrameTopLeftPoint(NSPoint(x: area.midX - searchWidth / 2, y: area.maxY - area.height / 5)) render(force: true) } panel.makeKeyAndOrderFront(nil) panel.makeFirstResponder(field) installKeyMonitor() log("search: opened (key: \(panel.isKeyWindow), \(rows.count) agents)") } func close(_ reason: String) { guard isOpen else { return } if let keyMonitor { NSEvent.removeMonitor(keyMonitor) } keyMonitor = nil panel.orderOut(nil) log("search: closed by \(reason) (query '\(state.query)', \(state.results.count) results, " + "selected \(state.selected ?? "none"))") } func update(rows: [Row]) { guard isOpen else { return } state.update(rows: rows) render() } private func jumpToSelection() { guard let paneId = state.selected else { NSSound.beep(); return } jump(to: paneId) } private func jump(to paneId: String) { let query = state.query close("jump") log("search: jump to \(paneId) (query '\(query)')") onJump(paneId) } /// ⌘U: mark the selected agent unread; search stays open, and the agent /// moves to the done ones as the list refreshes. private func markSelectionUnread() { guard let row = state.selectedRow, row.canMarkUnread else { NSSound.beep(); return } log("search: mark \(row.paneId) unread") onMarkUnread(row.paneId) } /// Debug: render `query` over `rows` offscreen to a PNG. func snapshot(rows: [Row], query: String, down: Int, to path: String) -> Bool { state = SearchState(rows: rows) field.stringValue = query state.setQuery(query) state.move(down) render(force: true) guard let view = panel.contentView else { return false } return snapshotView(view, to: path) } // MARK: keys /// ⌃N ⌃P ⌃J ⌃K move, ⌃U clears: fzf's keys, before the text field's own /// Emacs bindings see them. ⌘U marks the selected agent unread. private func installKeyMonitor() { guard keyMonitor == nil else { return } keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in guard let self, event.window === self.panel else { return event } let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) if flags == .command, event.charactersIgnoringModifiers == "u" { self.markSelectionUnread() return nil } guard flags == .control else { return event } switch event.charactersIgnoringModifiers { case "n", "j": self.move(1) case "p", "k": self.move(-1) case "u": self.field.stringValue = "" self.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification)) default: return event } return nil } } func control(_ control: NSControl, textView: NSTextView, doCommandBy selector: Selector) -> Bool { switch selector { case #selector(NSResponder.moveDown(_:)): move(1) case #selector(NSResponder.moveUp(_:)): move(-1) case #selector(NSResponder.insertNewline(_:)): jumpToSelection() case #selector(NSResponder.cancelOperation(_:)): close("esc") default: return false } return true } func controlTextDidChange(_ notification: Notification) { state.setQuery(field.stringValue) render() } private func move(_ delta: Int) { state.move(delta) render() } func windowDidResignKey(_ notification: Notification) { close("clicking elsewhere") } // MARK: rendering private func render(force: Bool = false) { let text = state.plainText guard force || text != rendered else { return } rendered = text list.arrangedSubviews.forEach { view in list.removeArrangedSubview(view) view.removeFromSuperview() } if state.results.isEmpty { list.addArrangedSubview(searchCaption( state.rows.isEmpty ? "No agents" : "No agents match '\(state.query)'", color: mochaSubtext)) } for match in state.visible { let view = PanelRowView(row: match.row, title: highlightedTitle(match), selected: match.row.paneId == state.selected, onClick: { [weak self] paneId in self?.jump(to: paneId) }, onMarkUnread: { [weak self] paneId in self?.onMarkUnread(paneId) }) list.addArrangedSubview(view) view.widthAnchor.constraint(equalTo: list.widthAnchor).isActive = true } list.addArrangedSubview(searchCaption(state.footer, color: mochaOverlay)) // Grow or shrink downward from the top edge. panel.contentView?.layoutSubtreeIfNeeded() let height = 14 + field.fittingSize.height + 12 + 1 + 6 + list.fittingSize.height + 8 let top = panel.frame.maxY panel.setFrame(NSRect(x: panel.frame.minX, y: top - height, width: searchWidth, height: height), display: true) } private func searchCaption(_ text: String, color: NSColor) -> NSView { let label = NSTextField(labelWithString: text) label.font = .systemFont(ofSize: 11) label.textColor = color let box = NSView() label.translatesAutoresizingMaskIntoConstraints = false box.addSubview(label) NSLayoutConstraint.activate([ label.leadingAnchor.constraint(equalTo: box.leadingAnchor, constant: 8), label.trailingAnchor.constraint(lessThanOrEqualTo: box.trailingAnchor, constant: -8), label.topAnchor.constraint(equalTo: box.topAnchor, constant: 6), label.bottomAnchor.constraint(equalTo: box.bottomAnchor, constant: -2), ]) return box } } // MARK: selftest func searchSelftest() -> [String] { var failures: [String] = [] func check(_ condition: Bool, _ message: String) { if !condition { failures.append(message) } } func row(_ id: String, _ name: String, _ status: Status = .idle, session: String = "s", window: String = "w", path: String = "") -> Row { Row(paneId: id, agent: "pi", name: name, status: status, session: session, window: window, since: 1, path: path) } func ids(_ matches: [SearchMatch]) -> [String] { matches.map(\.row.paneId) } check(fuzzyMatch(lowered("lgsc"), in: "LangGraph feature scanner") != nil, "match: letters in order") check(fuzzyMatch(lowered("longer"), in: "lon") == nil && fuzzyMatch(lowered("a"), in: "") == nil, "match: a term longer than the text") check(fuzzyMatch(lowered("csgl"), in: "LangGraph feature scanner") == nil, "match: order matters") check(fuzzyMatch(lowered("LANG"), in: "langgraph")?.positions == [0, 1, 2, 3], "match: case-insensitive") check(fuzzyMatch(lowered("lg"), in: "LangGraph")?.positions == [0, 4], "match: prefers word starts (camel case)") check(fuzzyMatch(lowered("scan"), in: "a secret scanner")?.positions == [9, 10, 11, 12], "match: prefers a consecutive run at a word start over the first letters") let rows = [ row("%1", "Review LangGraph feature scanner", .done, session: "scout"), row("%2", "Temporal crash-resume demo", .working, session: "temporal"), row("%3", "Deploy the widget", session: "tools", path: "~/src/langgraph"), row("%4", "", session: "sidequests", window: "notes"), ] check(ids(search(rows, "")) == ["%1", "%2", "%3", "%4"], "empty query: every agent, in order") check(ids(search(rows, "lgsc")) == ["%1"], "query: fuzzy name") check(ids(search(rows, "langgraph")) == ["%1", "%3"], "query: directories match too") check(ids(search(rows, "temporal demo")) == ["%2"], "query: every word must match") var drafted = rows drafted[3].draft = "also check the Sidecar restart" check(ids(search(drafted, "sidecar")) == ["%4"], "query: a draft's words match") check(ids(search(drafted, "scr")).first == "%1" && !ids(search(drafted, "sdcr")).contains("%4"), "query: a draft matches as written, not fuzzily") drafted[0].draft = "deploy" check(ids(search(drafted, "deploy")) == ["%3", "%1"], "query: a draft ranks below a name") check(ids(search(rows, "done")) == ["%1"], "query: status word") check(ids(search(rows, "notes")) == ["%4"], "query: window") check(ids(search(rows, "zzz")).isEmpty, "query: no match") check(search(rows, "lgsc").first?.labelPositions == [7, 11, 25, 26], "highlight: matched title letters") check(search(rows, "langgraph").last?.labelPositions == [], "highlight: none for a directory match") var state = SearchState(rows: rows) check(state.selected == "%1", "state: the first agent is selected") state.move(1) check(state.selected == "%2", "state: down") state.move(-5) check(state.selected == "%1", "state: stops at the top") state.move(2) state.update(rows: [rows[2], rows[0], rows[1], rows[3]]) check(state.selected == "%3", "state: selection follows the agent when rows reorder") state.update(rows: [rows[0], rows[1], rows[3]]) check(state.selected == "%1", "state: when the selected agent goes, the row now in its place") state.setQuery("temporal") check(state.selected == "%2" && state.results.count == 1, "state: a new query selects the best match") state.setQuery("zzz") check(state.selected == nil && state.plainText.hasPrefix("No agents match 'zzz'"), "state: no results") let many = (0..<25).map { row("%\($0)", "agent \($0)") } var scroll = SearchState(rows: many) scroll.move(12) check(scroll.visible.count == SearchState.visibleRows && scroll.visible.last?.row.paneId == "%12", "state: the list scrolls to keep the selection visible") scroll.move(-12) check(scroll.offset == 0, "state: and back to the top") check(scroll.footer.hasPrefix("25 agents"), "footer: agent count") let mixed = SearchState(rows: [row("%1", "a", .idle), row("%2", "b", .done)]) var onDone = mixed onDone.move(1) check(mixed.selectedRow?.paneId == "%1" && mixed.footer.contains("⌘U mark unread"), "footer: ⌘U offered for an idle agent") check(onDone.selectedRow?.paneId == "%2" && !onDone.footer.contains("⌘U"), "footer: not for a done one") return failures }