Skip to content

feat: BrainBar → MenuBarExtra(.window) + live-state sparklines + full-width hero (Linear UX overhaul part 1)#248

Merged
EtanHey merged 7 commits intomainfrom
feat/brainbar-menubarextra-rewrite
Apr 17, 2026
Merged

feat: BrainBar → MenuBarExtra(.window) + live-state sparklines + full-width hero (Linear UX overhaul part 1)#248
EtanHey merged 7 commits intomainfrom
feat/brainbar-menubarextra-rewrite

Conversation

@EtanHey
Copy link
Copy Markdown
Owner

@EtanHey EtanHey commented Apr 17, 2026

Summary

  • replace the default BrainBar shell with a full MenuBarExtra(.window) implementation while keeping BRAINBAR_LEGACY=1 as the local escape hatch
  • restore/persist the real window frame before first render, place the open window below the clicked menu bar icon on the active display, and keep F4 as a BrainBar visibility toggle
  • move Search/Capture into the Dashboard body, remove the redundant enrichment-rate tile, and tighten live/idle readability with explicit labels and sparkline liveness cues

Plan Reference

  • docs.local/plans/2026-04-17-brainbar-linear-ux/README.md

Pivot Note

  • this PR intentionally pivots from the earlier bridge plan to the locked full rewrite: the production path is now MenuBarExtra(.window) instead of NSStatusItem + NSPopover

Before / After

Before

Before

After

After

Verification

  • swift test --filter BrainBar
  • bash brain-bar/build-app.sh
  • visual verification receipt captured at /tmp/brainbar-inline-dashboard-search.png

Notes

  • cr review --plain was attempted pre-commit but CodeRabbit returned a rate-limit error in the CLI (try after 9 minutes and 20 seconds), so this PR relies on post-push bot review instead.

Note

Medium Risk
Medium risk because it replaces the primary menu bar UI surface with MenuBarExtra(.window) and adds AX-driven window discovery/placement logic, plus changes socket write/backpressure handling in BrainBarServer that could affect client reliability under load.

Overview
BrainBar now defaults to a SwiftUI MenuBarExtra(.window) surface driven by a new BrainBarRuntime, with BRAINBAR_LEGACY=1 preserving the prior NSStatusItem/popover shell.

The new window experience adds a unified BrainBarWindowRootView (Dashboard/Injections/Graph), an inline command bar for capture/search with results overlay + improved key handling, and menu-bar label sparklines whose accent state comes from BrainBarLivePresentation. Window visibility toggling and placement are revamped with persisted frames and AX-based menu bar icon anchoring/restore.

Backend-adjacent changes include a simplified blocking retry loop for socket writes in BrainBarServer, revised dashboard bucket/rate calculations in BrainDatabase/DashboardMetricFormatter (and removal of PipelineActivityTracks), plus safer lifecycle handling for InjectionStore’s Darwin observer and updated tests covering launch modes/window placement and the new formatting/metrics behavior.

Reviewed by Cursor Bugbot for commit d71e40e. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Replace BrainBar status item popover with MenuBarExtra window surface and live sparklines

  • Introduces a MenuBarExtra(.window) launch mode alongside a legacy status item mode, selected via the BRAINBAR_LEGACY environment variable; BrainBarApp.swift wires the new path end-to-end.
  • Adds BrainBarRuntime.swift as a central @MainActor coordinator that routes toggle/search/capture actions to the correct surface and publishes database/collector state.
  • Window placement is persisted and restored across launches via AX and AppKit frame conversion; if no valid frame exists the window is centered below the menu bar item.
  • The menu bar label now renders a live sparkline with an accent color derived from enrichment stats; the dashboard drops separate indexing/enriching rows in favour of a single enrichment sparkline and textual summary.
  • DashboardMetricFormatter gains indexingString, activitySummaryString, and lastCompletionString helpers; all rate display now uses /min units only.
  • BrainBarServer.sendResponse is rewritten as a synchronous retry loop (up to 10 × 1 ms EAGAIN retries) that disconnects stalled clients immediately instead of queuing partial writes.
  • Risk: the MenuBarExtra window relies on Accessibility APIs to discover its NSWindow and restore position; this path may be fragile across macOS versions or when accessibility permissions are not granted.

Macroscope summarized d71e40e.

Summary by CodeRabbit

  • New Features

    • Menu-bar window is now the default with persisted placement, multi-screen restore, accessibility-aware menu-bar integration, and explicit menu-bar command buttons.
    • New command bar UI with capture/search modes, results overlay, and improved keyboard handling.
    • Quick-action inline capture/search flows and show/toggle commands.
  • Improvements

    • Dashboard metrics, sparkline visuals and activity summaries clarified.
    • Search/entity output now uses human-readable boxed text tables.
    • Quick-capture feedback auto-clear and overlay dismissal behaviors improved.
  • Tests

    • Expanded coverage for window state, command bar, formatting, quick-capture, and integration; some legacy backpressure tests removed.

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

Replaces legacy status-item popover startup with a BrainBarRuntime-driven flow; adds dual launch modes (.legacyStatusItem / .menuBarWindow); implements window coordinator, frame persistence, accessibility-driven menu-bar-window handling; introduces SwiftUI command bar and window root view; refactors dashboard metrics/sparklines; reworks server write/delivery and text-formatting output.

Changes

Cohort / File(s) Summary
App bootstrap & runtime
brain-bar/Sources/BrainBar/BrainBarApp.swift, brain-bar/Sources/BrainBar/BrainBarRuntime.swift, brain-bar/Sources/BrainBar/BrainBarWindowState.swift
Adds BrainBarRuntime, launch-mode resolution, AppDelegate runtime wiring, and conditional legacy NSStatusItem vs .menuBarWindow flow; introduces window coordinator, placement helpers, and frame persistence.
Window UI & root view
brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift, brain-bar/Sources/BrainBar/BrainBarWindowState.swift
New SwiftUI root window and tabs, window observer/coordinator and attachment/restore logic, menu-bar extra accessibility discovery/pressing, screen-frame sync and persistence, and quick-action routing from runtime.
Command bar & quick-capture UI
brain-bar/Sources/BrainBar/BrainBarCommandBar.swift, brain-bar/Sources/BrainBar/QuickCapturePanel.swift
Adds SwiftUI command bar, bridged NSTextField (CommandBarInput/KeyHandlingCommandBarField), results overlay, keyboard handling tests, search-overlay dismiss behavior, and feedback auto-clear logic.
Dashboard & visualization
brain-bar/Sources/BrainBar/Dashboard/*, brain-bar/Sources/BrainBar/BrainBarLivePulse.swift, brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift
Removes per-pipeline activity tracks in favor of enrichment-focused metrics, new metric formatting functions, simplified pipeline indicators, sparkline endpoint and accentColor handling, and a live-pulse equality check.
Server I/O & socket tests
brain-bar/Sources/BrainBar/BrainBarServer.swift, brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift
Removes buffered pending-write queue; sendResponse now writes synchronously with capped retries and immediate delivery marking; simplifies socket framing/decoding and consolidates backpressure tests.
Database & injection store
brain-bar/Sources/BrainBar/BrainDatabase.swift, brain-bar/Sources/BrainBar/InjectionStore.swift, brain-bar/Sources/BrainBar/InjectionFeedView.swift
Specializes recent-activity/enrichment SQL queries, always reads last-insert-rowid, simplifies Darwin CFNotification observer wiring using Unmanaged pointer, and suppresses some UI error alerts.
Formatting & text output
brain-bar/Sources/BrainBar/Formatting/TextFormatter.swift
Replaces XML-like search/entity output with ASCII/box-drawing text tables; removes XML-escape/smartTruncate helpers and tightens truncation rules.
Tests
brain-bar/Tests/BrainBarTests/...
Extensive test updates: new window-state/coordinator tests, command-bar/quick-capture keyboard tests, formatter parity adjustments, removed/updated backpressure and legacy tests, and many expectations adapted to new metric/format behavior.
Build script
brain-bar/build-app.sh
Moves socket wait/verification out of the conditional plist branch to run unconditionally after LaunchAgent steps.

Sequence Diagram(s)

sequenceDiagram
    participant AppDelegate
    participant BrainBarRuntime
    participant WindowCoordinator
    participant NSWindow
    participant AccessibilityAPI

    AppDelegate->>BrainBarRuntime: init(launchMode)
    activate BrainBarRuntime

    alt launchMode == .menuBarWindow
        BrainBarRuntime->>WindowCoordinator: attach(window)
        activate WindowCoordinator
        WindowCoordinator->>WindowCoordinator: restore frame or compute default
        WindowCoordinator->>NSWindow: apply(frame)
        WindowCoordinator->>AccessibilityAPI: poll for MenuBarExtra window
        AccessibilityAPI-->>WindowCoordinator: menuBarWindowSnapshot
        WindowCoordinator->>NSWindow: show()
        deactivate WindowCoordinator
    else launchMode == .legacyStatusItem
        AppDelegate->>AppDelegate: create NSStatusItem + NSPopover
    end

    AppDelegate->>BrainBarRuntime: install(collector,injectionStore,database)
    BrainBarRuntime->>BrainBarRuntime: publish state
    AppDelegate->>BrainBarRuntime: onToggleRequested -> runtime.handleToggleRequest()
    BrainBarRuntime->>WindowCoordinator: toggleVisibility() (if menuBarWindow)
Loading
sequenceDiagram
    participant User
    participant CommandBar
    participant QuickCaptureVM
    participant BrainDatabase
    participant TextFormatter

    User->>CommandBar: type query / press keys
    CommandBar->>QuickCaptureVM: handleInputChange(text)
    QuickCaptureVM->>BrainDatabase: searchChunks(query)
    BrainDatabase-->>QuickCaptureVM: results
    QuickCaptureVM->>CommandBar: publish results
    CommandBar->>TextFormatter: formatSearchResults(query, results)
    TextFormatter-->>CommandBar: ASCII table
    CommandBar->>User: render results overlay
    User->>CommandBar: select + activate
    CommandBar->>QuickCaptureVM: activateResult(result)
    QuickCaptureVM->>BrainDatabase: recordInjectionEvent(...)
    BrainDatabase-->>QuickCaptureVM: success
    QuickCaptureVM->>CommandBar: show success feedback (auto-clear)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped from status item to a runtime neat,

Frames tucked safe where menu-bar and windows meet,
A command bar hums, keys answered with a cheer,
ASCII boxes sing the search results near,
My whiskers twitch—new surfaces appear!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main architectural change: transitioning from the legacy status-item/popover model to MenuBarExtra(.window), introducing live sparklines, and redesigning the dashboard UI. It is specific and meaningful.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/brainbar-menubarextra-rewrite

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented Apr 17, 2026

@codex review

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented Apr 17, 2026

@greptileai review

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented Apr 17, 2026

@coderabbitai review

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented Apr 17, 2026

@cursor @BugBot review

@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 17, 2026

You need to increase your spend limit or enable usage-based billing to run background agents. Go to Cursor

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

}
gesture.onDoubleTap = { [weak self] in
self?.searchPanel?.show()
self?.runtime.handleToggleRequest()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double-tap hotkey lost distinct search action

Medium Severity

Both onSingleTap and onDoubleTap now call runtime.handleToggleRequest(), making them identical. Previously, single-tap toggled quick capture and double-tap opened search. The double-tap gesture no longer triggers any distinct action, silently removing the hotkey-based search shortcut.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 422e40b. Configure here.

totalWritten: 0,
lastProgressAt: DispatchTime.now().uptimeNanoseconds,
onDelivered: onDelivered
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing EINTR handling in new write loop

Medium Severity

The rewritten sendResponse write loop handles EAGAIN/EWOULDBLOCK but no longer handles EINTR. The previous implementation explicitly continued on EINTR. A signal interrupting the write() syscall will now log a write error and disconnect the client instead of retrying.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 422e40b. Configure here.

guard let db else { throw DBError.notOpen }
var sql = "SELECT id, session_id, timestamp, query, chunk_ids, token_count FROM injection_events"
if sessionID != nil { sql += " WHERE session_id = ?" }
sql += " ORDER BY timestamp DESC, id DESC LIMIT ?"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong rowID returned when injection insert fails

Medium Severity

sqlite3_last_insert_rowid(db) is now called unconditionally before checking stepRC. When the insert fails (e.g., SQLITE_BUSY), this returns the rowid from the previous successful insert rather than 0, producing an InjectionEvent with a stale, misleading ID.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 422e40b. Configure here.

guard database.isOpen else {
throw BrainDatabase.DBError.notOpen
}
observerBox.store = self
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InjectionStore.stop() closes database preventing restart

Low Severity

stop() now calls database.close(), which was previously only in deinit. If start() is called again after stop(), all database queries in refresh() will fail because the underlying SQLite connection is closed. The store becomes permanently broken after the first stop.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 422e40b. Configure here.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 422e40bbba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +347 to +351
if errno == EAGAIN || errno == EWOULDBLOCK {
eagainRetries += 1
if eagainRetries > Self.maxWriteRetries {
NSLog("[BrainBar] ⚠️ Write stalled on fd %d after %d EAGAIN retries (%d ms) — disconnecting dead client", fd, eagainRetries, eagainRetries - 1)
disconnectClient(fd: fd)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Retry interrupted socket writes instead of dropping clients

This write loop no longer treats EINTR as a transient condition. If write is interrupted by a signal, it now falls through to the generic error path and disconnects an otherwise healthy client, which can drop in-flight MCP responses/notifications under normal process-signal activity. The previous implementation retried on EINTR; this behavior should be preserved here as well.

Useful? React with 👍 / 👎.

Comment on lines 1930 to 1933
let stepRC = sqlite3_step(stmt)
let rowID: Int64
let rowID = sqlite3_last_insert_rowid(db)
if stepRC == SQLITE_DONE {
rowID = sqlite3_last_insert_rowid(db)
Self.postDashboardChangeNotification()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Return zero ID when injection insert fails

sqlite3_last_insert_rowid is now read unconditionally before checking sqlite3_step success, so a failed insert can return the previous successful row ID. That misreports persistence and can cause callers to treat an old event as if it were newly written. This should keep returning 0 (or throw) on non-SQLITE_DONE to avoid stale IDs.

Useful? React with 👍 / 👎.

var sql = "SELECT id, session_id, timestamp, query, chunk_ids, token_count FROM injection_events"
if sessionID != nil { sql += " WHERE session_id = ?" }
sql += " ORDER BY timestamp DESC, id DESC LIMIT ?"
sql += " ORDER BY timestamp DESC LIMIT ?"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep a stable tie-breaker in injection event ordering

Dropping id DESC from this query makes ordering nondeterministic when multiple events share the same timestamp (common for batched writes or caller-provided timestamps). With LIMIT, that can reshuffle or omit equally-timestamped rows between refreshes, causing feed jitter and inconsistent “newest first” behavior. Add id DESC back as a deterministic secondary sort key.

Useful? React with 👍 / 👎.

@@ -149,33 +148,10 @@ final class EnterKeySearchTests: XCTestCase {
// MARK: - (4) Popover size should be stable

final class PopoverSizeTests: XCTestCase {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low BrainBarTests/StabilityFixTests.swift:150

The test testStatusPopoverViewFrameMatchesStableUtilityPanel was changed to assert that a hardcoded NSRect(x: 0, y: 0, width: 560, height: 520) has width 560 and height 520. This is a tautology that will always pass regardless of any regression in StatusPopoverView sizing. The previous implementation instantiated StatusPopoverView and measured its actual frame after layout, which could catch bugs. The new test cannot detect if StatusPopoverView changes its default size.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file brain-bar/Tests/BrainBarTests/StabilityFixTests.swift around line 150:

The test `testStatusPopoverViewFrameMatchesStableUtilityPanel` was changed to assert that a hardcoded `NSRect(x: 0, y: 0, width: 560, height: 520)` has width 560 and height 520. This is a tautology that will always pass regardless of any regression in `StatusPopoverView` sizing. The previous implementation instantiated `StatusPopoverView` and measured its actual frame after layout, which could catch bugs. The new test cannot detect if `StatusPopoverView` changes its default size.

Evidence trail:
brain-bar/Tests/BrainBarTests/StabilityFixTests.swift lines 150-155 at REVIEWED_COMMIT - the test creates `let frame = NSRect(x: 0, y: 0, width: 560, height: 520)` and then asserts `XCTAssertEqual(frame.width, 560)` and `XCTAssertEqual(frame.height, 520)`, which is a tautology that will always pass.

}

init(title: String) {
init(title: String, accentColor: NSColor) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low Dashboard/StatusPopoverView.swift:470

The accentColor parameter is accepted but never used, so all metric tiles render identically despite callers passing distinct colors (.systemBlue, .systemGreen, .systemOrange, .systemPink). Consider applying the accent color to a visible element so each tile renders with its intended color.

Also found in 1 other location(s)

brain-bar/Sources/BrainBar/Dashboard/DashboardMetricFormatter.swift:33

Hardcoded "30m+" in the empty array guard at line 33 ignores the activityWindowMinutes parameter. When the array is empty, it returns "30m+" regardless of the parameter value, but when no positive values exist (line 34-36), it correctly uses "\(activityWindowMinutes)m+". These are semantically identical "no recent completions" cases and should return consistent output. If called with activityWindowMinutes: 60 and an empty array, this returns the incorrect string "30m+" instead of "60m+".

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file brain-bar/Sources/BrainBar/Dashboard/StatusPopoverView.swift around line 470:

The `accentColor` parameter is accepted but never used, so all metric tiles render identically despite callers passing distinct colors (`.systemBlue`, `.systemGreen`, `.systemOrange`, `.systemPink`). Consider applying the accent color to a visible element so each tile renders with its intended color.

Evidence trail:
brain-bar/Sources/BrainBar/Dashboard/StatusPopoverView.swift lines 470-501 (init method where accentColor is declared but never used), lines 49-52 (callers passing distinct colors .systemBlue, .systemGreen, .systemOrange, .systemPink). The grep search confirms the parameter usage pattern.

Also found in 1 other location(s):
- brain-bar/Sources/BrainBar/Dashboard/DashboardMetricFormatter.swift:33 -- Hardcoded `"30m+"` in the empty array guard at line 33 ignores the `activityWindowMinutes` parameter. When the array is empty, it returns `"30m+"` regardless of the parameter value, but when no positive values exist (line 34-36), it correctly uses `"\(activityWindowMinutes)m+"`. These are semantically identical "no recent completions" cases and should return consistent output. If called with `activityWindowMinutes: 60` and an empty array, this returns the incorrect string `"30m+"` instead of `"60m+"`.

Comment on lines 1929 to 1935
sqlite3_bind_int(stmt, 5, Int32(tokenCount))
let stepRC = sqlite3_step(stmt)
let rowID: Int64
let rowID = sqlite3_last_insert_rowid(db)
if stepRC == SQLITE_DONE {
rowID = sqlite3_last_insert_rowid(db)
Self.postDashboardChangeNotification()
} else {
rowID = 0
}
return InjectionEvent(id: rowID, sessionID: sessionID, timestamp: ts, query: query, chunkIDs: chunkIDs, tokenCount: tokenCount)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium BrainBar/BrainDatabase.swift:1929

sqlite3_last_insert_rowid(db) is called before checking stepRC, so when the INSERT fails, the returned InjectionEvent gets the row ID from a previous successful INSERT rather than the failed one. This corrupts the caller's view by associating the new event with an unrelated existing row. Consider moving the rowid read inside the stepRC == SQLITE_DONE branch so it only reads after success.

        let stepRC = sqlite3_step(stmt)
-        let rowID = sqlite3_last_insert_rowid(db)
         if stepRC == SQLITE_DONE {
+            let rowID = sqlite3_last_insert_rowid(db)
             Self.postDashboardChangeNotification()
+            return InjectionEvent(id: rowID, sessionID: sessionID, timestamp: ts, query: query, chunkIDs: chunkIDs, tokenCount: tokenCount)
         }
-        return InjectionEvent(id: rowID, sessionID: sessionID, timestamp: ts, query: query, chunkIDs: chunkIDs, tokenCount: tokenCount)
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file brain-bar/Sources/BrainBar/BrainDatabase.swift around lines 1929-1935:

`sqlite3_last_insert_rowid(db)` is called before checking `stepRC`, so when the INSERT fails, the returned `InjectionEvent` gets the row ID from a *previous* successful INSERT rather than the failed one. This corrupts the caller's view by associating the new event with an unrelated existing row. Consider moving the rowid read inside the `stepRC == SQLITE_DONE` branch so it only reads after success.

Evidence trail:
brain-bar/Sources/BrainBar/BrainDatabase.swift lines 1930-1935 at REVIEWED_COMMIT: Line 1930 `let stepRC = sqlite3_step(stmt)`, line 1931 `let rowID = sqlite3_last_insert_rowid(db)` (called before checking stepRC), lines 1932-1934 check `stepRC == SQLITE_DONE` only to post notification, line 1935 returns `InjectionEvent(id: rowID, ...)` regardless of success/failure.

Comment thread brain-bar/build-app.sh
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High

# Install LaunchAgent (expands path to actual APP_DIR)
if [ -f "$PLIST_SRC" ]; then
echo "[build-app] Installing LaunchAgent to $PLIST_DST..."
bootout_launchagent
sed "s|/Applications/BrainBar.app|$APP_DIR|g" "$PLIST_SRC" > "$PLIST_DST"
launchctl bootstrap "$LAUNCH_DOMAIN" "$PLIST_DST"
launchctl kickstart -k "$LAUNCH_DOMAIN/$PLIST_LABEL"
echo "[build-app] LaunchAgent installed — BrainBar will auto-restart after quit"
fi
if ! wait_for_socket "$SOCKET_PATH"; then
echo "[build-app] ERROR: BrainBar did not recreate $SOCKET_PATH"
pgrep -fl BrainBar || true
exit 1
fi
python3 - <<'PY' "$SOCKET_PATH"

The new early fi on line 113 closes the if [ -f "$PLIST_SRC" ] block, causing wait_for_socket and the Python socket-verification to run unconditionally. When $PLIST_SRC does not exist, no LaunchAgent is bootstrapped and BrainBar is never launched, so the script hangs for ~20 seconds in wait_for_socket before failing with the misleading error "BrainBar did not recreate $SOCKET_PATH". Previously, these steps were guarded by the else branch and skipped gracefully. Consider restoring the conditional guard so socket verification only runs when the LaunchAgent was actually installed.

 if [ -f "$PLIST_SRC" ]; then
     echo "[build-app] Installing LaunchAgent to $PLIST_DST..."
     bootout_launchagent
     sed "s|/Applications/BrainBar.app|$APP_DIR|g" "$PLIST_SRC" > "$PLIST_DST"
     launchctl bootstrap "$LAUNCH_DOMAIN" "$PLIST_DST"
     launchctl kickstart -k "$LAUNCH_DOMAIN/$PLIST_LABEL"
     echo "[build-app] LaunchAgent installed — BrainBar will auto-restart after quit"
 fi
 
 if ! wait_for_socket "$SOCKET_PATH"; then
     echo "[build-app] ERROR: BrainBar did not recreate $SOCKET_PATH"
     pgrep -fl BrainBar || true
     exit 1
 fi
 
 python3 - <<'PY' "$SOCKET_PATH"
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file brain-bar/build-app.sh around lines 105-121:

The new early `fi` on line 113 closes the `if [ -f "$PLIST_SRC" ]` block, causing `wait_for_socket` and the Python socket-verification to run unconditionally. When `$PLIST_SRC` does not exist, no LaunchAgent is bootstrapped and BrainBar is never launched, so the script hangs for ~20 seconds in `wait_for_socket` before failing with the misleading error "BrainBar did not recreate $SOCKET_PATH". Previously, these steps were guarded by the `else` branch and skipped gracefully. Consider restoring the conditional guard so socket verification only runs when the LaunchAgent was actually installed.

Comment thread brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift
Comment thread brain-bar/Sources/BrainBar/BrainBarApp.swift
var sql = "SELECT id, session_id, timestamp, query, chunk_ids, token_count FROM injection_events"
if sessionID != nil { sql += " WHERE session_id = ?" }
sql += " ORDER BY timestamp DESC, id DESC LIMIT ?"
sql += " ORDER BY timestamp DESC LIMIT ?"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low BrainBar/BrainDatabase.swift:1944

When multiple injection_events share the same timestamp, ORDER BY timestamp DESC alone leaves their relative order undefined by SQLite. Successive calls to listInjectionEvents can return rows in different orders, causing the InjectionStore UI list to flicker or shift unexpectedly. Add id DESC as a tiebreaker to guarantee deterministic ordering.

Also found in 1 other location(s)

brain-bar/Tests/BrainBarTests/KnowledgeGraphTests.swift:67

Test has non-deterministic assertion due to undefined ordering. Both tool-vim and project-editor entities have the same default importance (5.0) since neither metadata contains an importance field. The fetchKGEntities query orders by importance DESC, but when values are equal, SQLite's row ordering is undefined. The test assumes entities.first will be the "Vim" entity, but it could be "Editor" depending on SQLite internals, causing intermittent test failures.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file brain-bar/Sources/BrainBar/BrainDatabase.swift around line 1944:

When multiple `injection_events` share the same `timestamp`, `ORDER BY timestamp DESC` alone leaves their relative order undefined by SQLite. Successive calls to `listInjectionEvents` can return rows in different orders, causing the `InjectionStore` UI list to flicker or shift unexpectedly. Add `id DESC` as a tiebreaker to guarantee deterministic ordering.

Evidence trail:
brain-bar/Sources/BrainBar/BrainDatabase.swift lines 1940-1948 at REVIEWED_COMMIT - The `listInjectionEvents` function constructs a SQL query with `ORDER BY timestamp DESC LIMIT ?` (line 1944) but no secondary tiebreaker column like `id DESC`. SQLite documentation confirms that ordering among rows with equal ORDER BY values is undefined (https://www.sqlite.org/lang_select.html#orderby).

Also found in 1 other location(s):
- brain-bar/Tests/BrainBarTests/KnowledgeGraphTests.swift:67 -- Test has non-deterministic assertion due to undefined ordering. Both `tool-vim` and `project-editor` entities have the same default importance (5.0) since neither metadata contains an `importance` field. The `fetchKGEntities` query orders by `importance DESC`, but when values are equal, SQLite's row ordering is undefined. The test assumes `entities.first` will be the "Vim" entity, but it could be "Editor" depending on SQLite internals, causing intermittent test failures.

Comment on lines +346 to +349
if n < 0 {
if errno == EAGAIN || errno == EWOULDBLOCK {
eagainRetries += 1
if eagainRetries > Self.maxWriteRetries {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High BrainBar/BrainBarServer.swift:346

When write() returns -1 with errno == EINTR, the code falls through and disconnects the client as a fatal error instead of retrying. This causes spurious disconnections when signals are delivered during writes. Consider adding EINTR handling with continue before the EAGAIN/EWOULDBLOCK check.

                let n = write(fd, ptr.baseAddress!.advanced(by: totalWritten), framed.count - totalWritten)
                if n < 0 {
+                    if errno == EINTR {
+                        continue
+                    }
                     if errno == EAGAIN || errno == EWOULDBLOCK {
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file brain-bar/Sources/BrainBar/BrainBarServer.swift around lines 346-349:

When `write()` returns -1 with `errno == EINTR`, the code falls through and disconnects the client as a fatal error instead of retrying. This causes spurious disconnections when signals are delivered during writes. Consider adding `EINTR` handling with `continue` before the `EAGAIN`/`EWOULDBLOCK` check.

Evidence trail:
brain-bar/Sources/BrainBar/BrainBarServer.swift lines 344-360 at REVIEWED_COMMIT: The write error handling at line 346 checks only for `errno == EAGAIN || errno == EWOULDBLOCK` (line 347) before retrying. Any other errno including EINTR falls through to `disconnectClient(fd: fd)` at line 358. POSIX standard behavior: EINTR should be retried as it indicates the call was interrupted by a signal, not a fatal error.

Comment on lines +49 to 55
static func liveBadgeString(ratePerMinute: Double) -> String {
let clamped = max(ratePerMinute, 0)
if clamped.rounded(.towardZero) == clamped {
return "\(Int(clamped))/min"
}
return string
return String(format: "%.1f/min", clamped)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High Dashboard/DashboardMetricFormatter.swift:49

liveBadgeString traps on Double.infinity because Int(clamped) fails when clamped is not representable as an integer. The previous implementation had sanitizedRatePerMinute that guarded with ratePerMinute.isFinite, but this was removed during the refactor. Since speedString delegates to liveBadgeString, an infinite enrichmentRatePerMinute value will crash at runtime.

    static func liveBadgeString(ratePerMinute: Double) -> String {
-        let clamped = max(ratePerMinute, 0)
+        guard ratePerMinute.isFinite else { return "0/min" }
+        let clamped = max(ratePerMinute, 0)
         if clamped.rounded(.towardZero) == clamped {
             return "\(Int(clamped))/min"
         }
         return String(format: "%.1f/min", clamped)
     }
Also found in 1 other location(s)

brain-bar/Sources/BrainBar/BrainBarWindowState.swift:61

If stats.enrichmentRatePerMinute is Double.infinity, the code path calls liveBadgeString(ratePerMinute:) which then attempts Int(clamped) where clamped is infinity. Converting Double.infinity to Int causes a runtime crash in Swift. The previous implementation had sanitizedRatePerMinute which checked ratePerMinute.isFinite and returned nil for non-finite values, preventing this crash.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file brain-bar/Sources/BrainBar/Dashboard/DashboardMetricFormatter.swift around lines 49-55:

`liveBadgeString` traps on `Double.infinity` because `Int(clamped)` fails when `clamped` is not representable as an integer. The previous implementation had `sanitizedRatePerMinute` that guarded with `ratePerMinute.isFinite`, but this was removed during the refactor. Since `speedString` delegates to `liveBadgeString`, an infinite `enrichmentRatePerMinute` value will crash at runtime.

Evidence trail:
1. brain-bar/Sources/BrainBar/Dashboard/DashboardMetricFormatter.swift lines 47-54 at REVIEWED_COMMIT - shows `liveBadgeString` with `Int(clamped)` call without isFinite guard
2. git_diff base=ec0c12b head=REVIEWED_COMMIT path=DashboardMetricFormatter.swift - shows removal of `sanitizedRatePerMinute` function with `guard ratePerMinute.isFinite else { return nil }` guard
3. git_show ec0c12b - original implementation had protection via `sanitizedRatePerMinute()` called by `speedDisplay()` which `speedString()` used
4. brain-bar/Tests/BrainBarTests/BrainBarUXLogicTests.swift - test `testDashboardMetricFormatterGuardsAgainstNonFiniteRates` expects `speedString(.infinity)` to return "0/min" (would now crash)

Also found in 1 other location(s):
- brain-bar/Sources/BrainBar/BrainBarWindowState.swift:61 -- If `stats.enrichmentRatePerMinute` is `Double.infinity`, the code path calls `liveBadgeString(ratePerMinute:)` which then attempts `Int(clamped)` where `clamped` is infinity. Converting `Double.infinity` to `Int` causes a runtime crash in Swift. The previous implementation had `sanitizedRatePerMinute` which checked `ratePerMinute.isFinite` and returned `nil` for non-finite values, preventing this crash.

… buttons

Replaces the clunky inline Capture/Search card that lived beneath the hero
(duplicated controls, clipped dashboard) and the short-lived floating
QuickCapturePanel detour with a persistent command-bar in the window header.

Design: Raycast/Linear pattern. Header has two rows:
  1. brand · segmented tab picker · hotkey status
  2. command bar (mode pill pair + leading SF Symbol + NSTextField + kbd hint)

When search mode has a non-empty query, results appear as a dropdown
overlay anchored to the top of the tab-content area via
`.overlay(alignment: .top)` — dashboard stays fully reachable underneath.

- Deletes BrainBarQuickActionSection (the inline card).
- Header no longer carries standalone Capture/Search buttons — the
  command bar's mode pills are the single source of mode truth.
- Hotkey/URL actions still route through runtime.presentQuickAction;
  BrainBarWindowRootView observes and focuses the command bar in the
  requested mode (setMode + panelDidAppear -> focusRequestCount).
- QuickCapturePanelController / floating panel stays wired only for the
  legacyStatusItem launch mode.
- Fixes the input-padding misalignment by normalising textContainerInset
  to (0, 8) and placeholder padding to (.top 8, .leading 4) in the
  retained floating QuickCapturePanelView.

Tests: `swift test --filter BrainBar` → 263 passed, 0 failures
(+2 new tests documenting showSearchPanel/showQuickCapturePanel ->
onSearchRequested/onQuickCaptureRequested callback contract).

Visual verification: bash brain-bar/build-app.sh + screenshot
/tmp/brainbar-v2-idle.png (command bar visible, dashboard reachable)
and /tmp/brainbar-v2-overlay.png (search results floating over hero).

Closes the 5 UX complaints on PR #248:
  1. Scroll-broken / dashboard clipped -> dashboard unchanged, nothing
     displaces it.
  2. Duplicated Capture/Search rows -> single command bar, no header
     buttons.
  3. Input padding misaligned -> textContainerInset + placeholder
     padding matched.
  4. Header Search always blue / Capture never blue -> header buttons
     removed; mode state lives in the command bar's accent-tinted pill.
  5. "Weird as fuck / unintuitive" -> emergent from 1-4; surface is now
     a known pattern (command bar).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
return "\(Int(clamped))/min"
}
return string
return String(format: "%.1f/min", clamped)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing infinity guard crashes Int() conversion

High Severity

liveBadgeString removed the old sanitizedRatePerMinute guard that checked .isFinite. If ratePerMinute is Double.infinity, max(infinity, 0) stays infinity, and Int(clamped) triggers a Swift runtime crash. StatusPopoverView passes collector.stats.enrichmentRatePerMinute directly to speedString, which delegates to liveBadgeString, making this path reachable in production.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit aab2891. Configure here.

recentEnrichmentBuckets: [Int],
activityWindowMinutes: Int = 30
) -> String {
guard !recentEnrichmentBuckets.isEmpty else { return "30m+" }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded "30m+" ignores activityWindowMinutes parameter

Low Severity

The empty-array guard in lastCompletionString returns the hardcoded string "30m+" instead of using the activityWindowMinutes parameter. The very next guard (line 35) correctly returns "\(activityWindowMinutes)m+" for the same semantic case (no recent completions). When called with a non-30 window, the empty-array path returns incorrect output.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit aab2891. Configure here.

}
.contentShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
.onTapGesture(perform: onSelect)
.onTapGesture(count: 2, perform: onActivate)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tap gesture ordering prevents double-tap from firing

Medium Severity

The single-tap .onTapGesture(perform: onSelect) is registered before the double-tap .onTapGesture(count: 2, perform: onActivate). In SwiftUI, the single-tap gesture consumes every first tap, so the double-tap handler (onActivate / copy-to-clipboard) will never fire. The double-tap gesture needs to be declared before the single-tap gesture.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit aab2891. Configure here.

Comment thread brain-bar/Sources/BrainBar/BrainBarCommandBar.swift
Comment on lines +175 to +182
private var heroSparkline: NSImage {
SparklineRenderer.render(
state: collector.state,
values: collector.stats.recentEnrichmentBuckets,
size: NSSize(width: 360, height: 160),
accentColor: livePresentation.accentColor
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low BrainBar/BrainBarWindowRootView.swift:175

heroSparkline renders at a fixed size NSSize(width: 360, height: 160), but BrainBarHeroSparkline calculates the endpoint using proxy.size (which varies between 280×128 and 320×150). Since SparklineRenderer.endpoint computes coordinates relative to its size parameter, the pulse indicator circle will be mispositioned when the display size differs from the render size. Consider passing the same size to both render() and endpoint(), or deriving the image from the actual display frame.

-    private var heroSparkline: NSImage {
-        SparklineRenderer.render(
-            state: collector.state,
-            values: collector.stats.recentEnrichmentBuckets,
-            size: NSSize(width: 360, height: 160),
-            accentColor: livePresentation.accentColor
-        )
-    }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift around lines 175-182:

`heroSparkline` renders at a fixed size `NSSize(width: 360, height: 160)`, but `BrainBarHeroSparkline` calculates the `endpoint` using `proxy.size` (which varies between 280×128 and 320×150). Since `SparklineRenderer.endpoint` computes coordinates relative to its `size` parameter, the pulse indicator circle will be mispositioned when the display size differs from the render size. Consider passing the same size to both `render()` and `endpoint()`, or deriving the image from the actual display frame.

Evidence trail:
brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift:178 - heroSparkline rendered at NSSize(width: 360, height: 160)
brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift:302 - BrainBarHeroSparkline displayed with .frame(width: layout.sparklineWidth, height: layout.sparklineHeight)
brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift:395-396 - sparklineWidth = compact ? 280 : 320, sparklineHeight = compact ? 128 : 150
brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift:504-507 - endpoint calculated using proxy.size (display size)
brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift:6-33 - endpoint() calculates coordinates relative to size parameter
brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift:35-113 - render() uses same formula relative to size parameter

Comment thread brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
brain-bar/Tests/BrainBarTests/PopoverTabTests.swift (1)

55-60: ⚠️ Potential issue | 🟡 Minor

Test name contradicts assertion — rename or reassert.

testPopoverTabGraphSizeIsTallerThanDashboard asserts the graph and dashboard heights are equal, not that graph is taller. A future reader changing graph to actually be taller will break this test while seeming to satisfy the name. Given testPopoverTabsShareStablePanelSize on line 43 already covers equality across all tabs, this test is redundant and should either be removed or renamed to reflect current intent:

♻️ Suggested rename
-    func testPopoverTabGraphSizeIsTallerThanDashboard() {
+    func testPopoverTabGraphSizeMatchesDashboardHeight() {
         XCTAssertEqual(
             PopoverTab.graph.contentSize.height,
             PopoverTab.dashboard.contentSize.height
         )
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Tests/BrainBarTests/PopoverTabTests.swift` around lines 55 - 60,
The test named testPopoverTabGraphSizeIsTallerThanDashboard contradicts its
assertion (it checks equality between PopoverTab.graph.contentSize.height and
PopoverTab.dashboard.contentSize.height); either delete this redundant test
(since testPopoverTabsShareStablePanelSize already asserts equality) or rename
it to reflect its actual check (e.g., testPopoverTabGraphSizeEqualsDashboard) or
change the assertion to a greater-than check if the intent is to enforce graph
being taller (replace XCTAssertEqual with XCTAssertGreaterThan for
PopoverTab.graph.contentSize.height > PopoverTab.dashboard.contentSize.height).
brain-bar/build-app.sh (1)

113-135: ⚠️ Potential issue | 🟠 Major

Unconditional wait_for_socket will hard-fail when PLIST_SRC is missing.

Previously, the socket wait and the Python connect probe lived inside the if [ -f "$PLIST_SRC" ] block and were skipped with a clear "plist missing … skipping" message when the LaunchAgent wasn't available. After this change (lines 113–135), the fi closes the install block but the wait_for_socket/python probe now run unconditionally.

In the PLIST-missing path, nothing starts BrainBar after killall BrainBar + rm -f "$SOCKET_PATH" on line 65. The script will:

  1. Build and sign successfully.
  2. Print no "LaunchAgent installed" line.
  3. Spin in wait_for_socket for 20s.
  4. Exit 1 with "BrainBar did not recreate $SOCKET_PATH" — even though the build itself was fine and the user never had a LaunchAgent to begin with (e.g., developer dry-builds, CI machines, fresh checkouts before copying the plist into bundle/).

Either restore the guard or launch BrainBar manually in the no-plist branch:

🛡️ Suggested fix — keep verification scoped to the LaunchAgent path
 if [ -f "$PLIST_SRC" ]; then
     echo "[build-app] Installing LaunchAgent to $PLIST_DST..."
     bootout_launchagent
     sed "s|/Applications/BrainBar.app|$APP_DIR|g" "$PLIST_SRC" > "$PLIST_DST"
     launchctl bootstrap "$LAUNCH_DOMAIN" "$PLIST_DST"
     launchctl kickstart -k "$LAUNCH_DOMAIN/$PLIST_LABEL"
     echo "[build-app] LaunchAgent installed — BrainBar will auto-restart after quit"
-fi
-
-if ! wait_for_socket "$SOCKET_PATH"; then
-    echo "[build-app] ERROR: BrainBar did not recreate $SOCKET_PATH"
-    pgrep -fl BrainBar || true
-    exit 1
-fi
-
-python3 - <<'PY' "$SOCKET_PATH"
+
+    if ! wait_for_socket "$SOCKET_PATH"; then
+        echo "[build-app] ERROR: BrainBar did not recreate $SOCKET_PATH"
+        pgrep -fl BrainBar || true
+        exit 1
+    fi
+
+    python3 - <<'PY' "$SOCKET_PATH"
 import os
 import socket
 import sys
 
 path = sys.argv[1]
 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
 try:
     s.connect(path)
 except OSError as exc:
     print(f"[build-app] ERROR: socket connect failed for {path}: {exc}", file=sys.stderr)
     raise SystemExit(1)
 finally:
     s.close()
 PY
+else
+    echo "[build-app] plist missing at $PLIST_SRC — skipping socket verification"
+fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/build-app.sh` around lines 113 - 135, The socket wait and Python
connect probe currently run unconditionally and will fail when PLIST_SRC is
missing; wrap the wait_for_socket "$SOCKET_PATH" and the subsequent Python probe
in the same conditional that checks for the LaunchAgent (i.e., only run them if
[ -f "$PLIST_SRC" ]), or alternatively, in the branch where PLIST_SRC is absent,
start BrainBar manually before probing; specifically modify the logic around
wait_for_socket, SOCKET_PATH and the python connect probe so they are executed
only when the PLIST_SRC/LaunchAgent path exists (or ensure BrainBar is launched
in the no-plist branch) to avoid hard-failing builds that legitimately lack the
plist.
brain-bar/Sources/BrainBar/QuickCapturePanel.swift (1)

873-881: 🧹 Nitpick | 🔵 Trivial

Duplicate .frame(width: 540, height: 360) modifier.

Line 880 re-applies the same frame already set at line 873. SwiftUI will just use whichever is "outermost" in the modifier chain, so the second call is a no-op — likely leftover from an iteration. Remove it.

♻️ Proposed cleanup
         .onChange(of: viewModel.confirmationFlashCount) { _, _ in
             flashOpacity = 0.18
             withAnimation(.easeOut(duration: 0.45)) {
                 flashOpacity = 0
             }
         }
-        .frame(width: 540, height: 360)
         .animation(.easeInOut(duration: 0.18), value: viewModel.mode)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/QuickCapturePanel.swift` around lines 873 - 881,
Remove the duplicate .frame(width: 540, height: 360) modifier in
QuickCapturePanel.swift: the first .frame applied before .onChange is
sufficient, so delete the second .frame call that appears just before
.animation(..., value: viewModel.mode), leaving the single frame, preserving the
.onChange { viewModel.confirmationFlashCount ... } logic, flashOpacity updates,
and the final .animation modifier.
brain-bar/Sources/BrainBar/BrainDatabase.swift (3)

1940-1956: ⚠️ Potential issue | 🟠 Major

Removing the id DESC tiebreaker makes listInjectionEvents non-deterministic for same-timestamp rows.

SQLite's datetime('now') has one-second precision, so bursts of injection events easily collide on timestamp. With the new ORDER BY timestamp DESC LIMIT ? and no secondary key, which rows get truncated by LIMIT is implementation-defined and can change across queries (even within the same session). This directly affects InjectionStore.refresh(...) consistency and can re-introduce the flakiness that the former id DESC tiebreaker prevented.

-    sql += " ORDER BY timestamp DESC LIMIT ?"
+    sql += " ORDER BY timestamp DESC, id DESC LIMIT ?"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/BrainDatabase.swift` around lines 1940 - 1956, The
ORDER BY in listInjectionEvents (function listInjectionEvents) is
nondeterministic for rows with identical timestamp; change the SQL built in
listInjectionEvents to include a stable secondary sort (e.g., "ORDER BY
timestamp DESC, id DESC") so LIMIT returns deterministic rows, and ensure the
binding/prepare logic still applies to the modified sql string used to create
the sqlite3_stmt for InjectionEvent rows.

870-950: 🛠️ Refactor suggestion | 🟠 Major

recentActivityBuckets and recentEnrichmentBuckets are copy-paste — extract a shared helper.

The two functions differ only in: (a) the timestamp column selected (created_at vs enriched_at) and (b) the extra enriched_at IS NOT NULL AND TRIM(...) != '' predicate. The bucket math, date parsing, guard clauses, and offset clamping are identical. The previous recentBuckets(...) helper existed for this reason — reintroduce it (or a parameterized variant) to avoid future drift between the two metrics.

♻️ Suggested refactor
private func recentBuckets(
    column: String,
    additionalWhere: String = "",
    activityWindowMinutes: Int,
    bucketCount: Int
) throws -> [Int] {
    guard activityWindowMinutes > 0 else { return Array(repeating: 0, count: bucketCount) }
    guard let db else { throw DBError.notOpen }

    let bucketWidthSeconds = max(1, Double(activityWindowMinutes * 60) / Double(bucketCount))
    let windowStart = Date().addingTimeInterval(Double(-activityWindowMinutes * 60))

    let sql = """
        SELECT datetime(\(column))
        FROM chunks
        WHERE \(additionalWhere.isEmpty ? "" : "\(additionalWhere) AND ")datetime(\(column)) >= datetime('now', ?)
        ORDER BY datetime(\(column)) ASC
    """
    // …prepare, bind, loop over rows, compute bucket index…
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/BrainDatabase.swift` around lines 870 - 950, Both
recentActivityBuckets and recentEnrichmentBuckets duplicate identical bucket
calculation logic; extract a shared helper (e.g., recentBuckets(column: String,
additionalWhere: String = "", activityWindowMinutes: Int, bucketCount: Int)
throws -> [Int]) that centralizes the guards, DB checks, SQL preparation (with a
parameterized column and optional extra predicate), the date parsing using
Self.sqliteDateFormatter, offset clamping and bucket incrementing, then make
recentActivityBuckets call recentBuckets(column: "created_at",
activityWindowMinutes:..., bucketCount:...) and make recentEnrichmentBuckets
call recentBuckets(column: "enriched_at", additionalWhere: "enriched_at IS NOT
NULL AND TRIM(enriched_at) != ''", activityWindowMinutes:..., bucketCount:...).

1912-1936: ⚠️ Potential issue | 🟠 Major

recordInjectionEvent returns silent defaults when db is nil or prepare fails — promote it to throws and remove the fallbacks.

Two fallback paths construct a bogus InjectionEvent(id: 0, ...):

  • Line 1914–1916 when db is nil (startup/DB not ready)
  • Line 1921–1923 when sqlite3_prepare_v2 fails

Tests at InjectionStoreTests.swift (lines 23, 47) and DatabaseTests.swift already call this as try db.recordInjectionEvent(...), so the compiler will warn "no calls to throwing functions occur within 'try' expression" and, per the AI summary, the call sites already expect throws. Silently returning a default InjectionEvent also masks real startup-ordering bugs and produces downstream writes that reference a non-existent row id.

Proposed:

 `@discardableResult`
-func recordInjectionEvent(sessionID: String, query: String, chunkIDs: [String], tokenCount: Int, timestamp: String? = nil) -> InjectionEvent {
-    guard let db else {
-        return InjectionEvent(id: 0, sessionID: sessionID, timestamp: "", query: query, chunkIDs: chunkIDs, tokenCount: tokenCount)
-    }
+func recordInjectionEvent(sessionID: String, query: String, chunkIDs: [String], tokenCount: Int, timestamp: String? = nil) throws -> InjectionEvent {
+    guard let db else { throw DBError.notOpen }
     ...
     var stmt: OpaquePointer?
-    guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else {
-        return InjectionEvent(id: 0, sessionID: sessionID, timestamp: ts, query: query, chunkIDs: chunkIDs, tokenCount: tokenCount)
-    }
+    guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else {
+        throw DBError.prepare(sqlite3_errcode(db))
+    }
     ...
+    if stepRC != SQLITE_DONE { throw DBError.step(stepRC) }
     ...
 }

Based on learnings: "In Swift files under brain-bar/Sources/BrainBar, enforce that when a critical dependency like the database is nil due to startup ordering (socket before DB), any tool handler that accesses the database must throw an explicit error (e.g., ToolError.noDatabase) instead of returning a default/empty value. Do not allow silent defaults (e.g., guard let db else { return ... })."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/BrainDatabase.swift` around lines 1912 - 1936,
Make recordInjectionEvent(sessionID:query:chunkIDs:tokenCount:timestamp:) throw
instead of returning silent defaults: remove the guard that returns
InjectionEvent when db is nil and instead throw a descriptive error (e.g.,
ToolError.noDatabase or a DatabaseError.noDB), and if sqlite3_prepare_v2 fails
throw a database preparation error including sqlite3_errmsg(db) text rather than
returning InjectionEvent(id:0,...); also change the sqlite3_step failure path to
throw a write/step error when stepRC != SQLITE_DONE, keep the existing final
InjectionEvent construction only for the successful path, and update function
signature and any callsites already using try to propagate the thrown errors.
brain-bar/Sources/BrainBar/InjectionStore.swift (1)

4-16: ⚠️ Potential issue | 🔴 Critical

Potential use-after-free: Darwin observer outlives InjectionStore if stop() is not called.

The observer is installed with Unmanaged.passUnretained(self).toOpaque() (line 94) and removal is now gated solely on stop() (line 59–61). There is no deinit hook. If any caller drops the last strong reference without first calling stop() — or drops it between a Darwin notification posting and the callback running — the callback's Unmanaged.fromOpaque(observer).takeUnretainedValue() at line 12 dereferences a freed object, which is undefined behavior.

This is the scenario the removed testScheduledDarwinRefreshDoesNotRetainStoreAfterRelease was covering.

Minimum safety net: mirror the old cleanup in deinit. Because deinit can't call a @MainActor method directly, do the CF calls inline:

+    deinit {
+        if isRunning {
+            let center = CFNotificationCenterGetDarwinNotifyCenter()
+            CFNotificationCenterRemoveObserver(
+                center,
+                Unmanaged.passUnretained(self).toOpaque(),
+                CFNotificationName(BrainDatabase.dashboardDidChangeNotification as CFString),
+                nil
+            )
+        }
+    }

For extra safety against the in-flight-callback race, consider passRetained on install + takeRetainedValue on remove, so self cannot be deallocated while the observer is live.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/InjectionStore.swift` around lines 4 - 16, Add a
deinit to InjectionStore that mirrors the existing stop() cleanup by removing
the Darwin notification observer from CFNotificationCenter (use
CFNotificationCenterGetDarwinNotifyCenter() / CFNotificationCenterRemoveObserver
with the same observer opaque pointer used when installing the callback) to
prevent use-after-free when stop() is not called; update the install/remove
pairing so the observer ownership is consistent (either keep
Unmanaged.passUnretained on install and remove in deinit/stop with no retain
semantics, or switch the install to Unmanaged.passRetained and remove with
takeRetainedValue at teardown) and ensure
injectionStoreDarwinNotificationCallback remains compatible with the chosen
ownership model.
brain-bar/Tests/BrainBarTests/InjectionStoreTests.swift (1)

22-28: ⚠️ Potential issue | 🔴 Critical

Make recordInjectionEvent throw explicit errors instead of silently returning zero-id events.

recordInjectionEvent is declared as non-throwing (line 1913: func recordInjectionEvent(...) -> InjectionEvent) but this test and others wrap it in try, which produces a Swift compiler warning. More critically, the function silently returns fake InjectionEvent(id: 0, ...) entries when the database is nil (lines 1914–1916) or when statement preparation fails (lines 1921–1923). This violates the socket-before-DB pattern requirement: operations that depend on database availability must throw explicit errors, not return silent defaults that hide startup timing issues.

Convert recordInjectionEvent to throw and propagate both failure paths:

  • Database nil → throw explicit error (e.g., ToolError.noDatabase)
  • sqlite3_prepare_v2 failure → throw error instead of returning zero-id event

This will eliminate the compiler warning in tests and ensure MCP clients receive explicit failures during cold startup rather than false-success fake records.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Tests/BrainBarTests/InjectionStoreTests.swift` around lines 22 -
28, Change recordInjectionEvent to be a throwing function (func
recordInjectionEvent(...) throws -> InjectionEvent) and update callers/tests to
use try; inside, replace the silent-return cases: when db is nil throw a
specific error (e.g., ToolError.noDatabase) instead of returning
InjectionEvent(id:0,...), and when sqlite3_prepare_v2 fails throw an
NSError/ToolError that includes the SQLite error message/code rather than
returning a zero-id event. Ensure the function propagates these thrown errors
outward so callers (tests and MCP clients) receive explicit failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@brain-bar/Sources/BrainBar/BrainBarApp.swift`:
- Around line 861-882: BrainBarMenuBarLabel currently calls
SparklineRenderer.render(...) directly in the View body whenever
runtime.collector.stats changes, causing a new NSImage allocation every publish;
fix this by moving the rendering out of the body and caching the image: add a
`@State` (or `@StateObject`) NSImage property and a lightweight cache key (e.g.,
hash of collector.state, collector.stats.recentEnrichmentBuckets, and desired
size) and update that image only when the key changes (use onChange(of:
collector.stats) or onReceive of the publisher to recompute via
SparklineRenderer.render and assign the `@State` image); keep the body only
referencing the cached image and still derive BrainBarLivePresentation from
collector.stats for colors if needed.
- Around line 306-324: discoverMenuBarWindow currently uses hardcoded size
thresholds that differ from isMenuBarExtraWindow, causing a race where
discoveredMenuBarWindow can point to a window the observer later rejects; update
discoverMenuBarWindow to use the same predicate as the observer by calling the
shared isMenuBarExtraWindow(_:) (or extract that predicate into a single helper
used by both discoverMenuBarWindow and handleMenuBarWindowBecameKey) and include
the same excluded check (searchPanel?.panelForTesting) so both discovery and the
notification observer agree on which window qualifies; ensure you update any
assignments to menuBarExtraWindow/discoveredMenuBarWindow to rely on that
unified predicate.

In `@brain-bar/Sources/BrainBar/BrainBarCommandBar.swift`:
- Around line 388-391: The single-tap is currently attached before the
double-tap so a double-click triggers onSelect on the first tap; reorder the tap
handlers or compose gestures so double-tap wins. Locate the view modifiers where
.onTapGesture(perform: onSelect) and .onTapGesture(count: 2, perform:
onActivate) are applied (the same view using contentShape and isCopied
animation) and either move the count:2 onTapGesture before the single-tap, or
replace both with a composed gesture like
TapGesture(count:2).exclusively(before: TapGesture(count:1)) so onActivate is
recognized on double-tap and onSelect only on single-tap.

In `@brain-bar/Sources/BrainBar/BrainBarServer.swift`:
- Around line 341-371: The current synchronous retry loop inside sendResponse
(the write loop that uses usleep and EAGAIN retries) blocks the serial
com.brainlayer.brainbar.server queue and multiplies stalls when
publishStoredChunk iterates subscribers; move the blocking write work off that
queue by dispatching per-client writes to a dedicated per-client DispatchQueue
or a background thread pool (create/lookup a per-client queue when clients
connect), or replace the spin+usleep retry with an async wait using
DispatchSourceWrite/kevent so writability is awaited without blocking the server
queue; update sendResponse, publishStoredChunk, and any calls from handleMessage
to perform or schedule writes asynchronously and ensure disconnectClient and
shared client state are synchronized for cross-queue access.

In `@brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift`:
- Around line 588-665: The updateNSView in WindowAttachmentView currently calls
onResolve(window) every SwiftUI update which causes
BrainBarWindowObserver.attach(window:) to remove and re-add NotificationCenter
observers repeatedly; change behavior to resolve the window only once (either by
moving the resolve to makeNSView and removing the async call in updateNSView, or
by guarding updateNSView so it only calls onResolve when nsView.window was
previously nil and now non-nil) and add a short-circuit in
BrainBarWindowObserver.attach(window:) to return early when preparedWindowNumber
== window.windowNumber && !observers.isEmpty to avoid churn of observers; refer
to WindowAttachmentView.makeNSView, WindowAttachmentView.updateNSView,
BrainBarWindowObserver.attach, preparedWindowNumber and observers when making
the change.

In `@brain-bar/Sources/BrainBar/BrainBarWindowState.swift`:
- Around line 66-70: The idle presentation uses inconsistent casing: change the
badgeText passed to BrainBarLivePresentation from "idle" to "Idle" so it matches
the capitalized statusText ("Idle — no enrichment in last 60s") and aligns with
other badges (see BrainBarLivePresentation initializer and the
badgeText/statusText arguments).
- Around line 378-382: The show() method currently calls the deprecated
NSApp.activate(ignoringOtherApps:) API; replace that call with the
non-deprecated NSApp.activate() in the show() function (keeping the other window
calls makeKeyAndOrderFront(nil) and orderFrontRegardless() intact) so the method
uses the macOS 14+ API without changing behavior.

In `@brain-bar/Sources/BrainBar/BrainDatabase.swift`:
- Around line 786-787: The constant enrichmentRateWindowMinutes (used when
calling recentEnrichmentRatePerMinute(windowMinutes:)) is hardcoded to 1 minute
and must be explicitly tied to the UI copy; add a brief inline comment above the
declaration stating that this 1 represents the 60s window referenced by the
"Idle — no enrichment in last 60s" string in BrainBarLivePresentation and the
expectation in BrainBarUXLogicTests.swift (test around line 167), so future
changes to enrichmentRateWindowMinutes are obvious and tests/UI are updated in
tandem.

In `@brain-bar/Sources/BrainBar/Dashboard/DashboardMetricFormatter.swift`:
- Around line 29-47: The function lastCompletionString returns a hardcoded
"30m+" when recentEnrichmentBuckets is empty, ignoring the activityWindowMinutes
parameter; change the early-empty return to use the activityWindowMinutes value
(i.e., return "\(activityWindowMinutes)m+") so both the empty-array and "no
non-zero bucket" branches consistently reflect the caller-supplied window;
update the return in lastCompletionString that currently yields "30m+" to
interpolate activityWindowMinutes instead.

In `@brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift`:
- Around line 6-33: The endpoint(values:size:) function duplicates geometry
logic from render(...) and contains a readability/maintainability risk; refactor
by extracting a shared helper (e.g. chartRect(for size: NSSize) -> (rect:
CGRect, step: CGFloat)) that computes isCompact, horizontalInset, verticalInset,
chartRect and step, then update endpoint(values:size:) to call that helper and
return CGPoint(x: chartRect.maxX, y: chartRect.minY + normalized *
chartRect.height) instead of recomputing x as chartRect.minX +
CGFloat(values.count - 1) * step; ensure render(...) is updated to use the same
helper so geometry cannot drift between implementations.

In `@brain-bar/Sources/BrainBar/Dashboard/StatusPopoverView.swift`:
- Around line 405-411: Remove the unused private helper function verticalStack(_
views: [NSView]) -> NSStackView (the NSStackView creation helper) from the file:
delete the entire private func declaration and its body since it is not
referenced anywhere; after removal, run the build and tests to ensure no
remaining references to verticalStack exist.
- Around line 357-360: The metric tiles in StatusPopoverView are using raw
string interpolation (chunkMetric.value = "\(collector.stats.chunkCount)" etc.)
which drops thousands separators; update these assignments to use
TextFormatter.formatNumber(...) (or a Dashboard-specific formatter) for
chunkCount, enrichedChunkCount and pendingEnrichmentCount, and keep rateMetric
using DashboardMetricFormatter.speedString as-is. Add a shared grouped
NumberFormatter (e.g., private static let groupedFormatter: NumberFormatter = {
... }()) and a wrapper like TextFormatter.formatNumber that uses it, then
replace the same raw interpolations in BrainBarWindowRootView's metric cards to
reuse that helper so all tiles show grouped numbers consistently.

In `@brain-bar/Sources/BrainBar/InjectionFeedView.swift`:
- Line 56: The assignment currently swallows all errors by using
"selectedConversation = try? store.expandedConversation(chunkID: chunkID)";
replace this with a do/catch around store.expandedConversation(chunkID:) so
thrown errors are caught and handled: in the catch, log the error (use the
existing logger) and surface user-visible feedback — e.g., set an inline error
state on the row or call the lightweight alert/UI method used elsewhere instead
of silently doing nothing — then only assign selectedConversation on success.
Ensure you reference the same symbols: selectedConversation and
store.expandedConversation(chunkID:).

In `@brain-bar/Tests/BrainBarTests/BrainBarUXLogicTests.swift`:
- Around line 171-183: The tests
testDashboardLayoutCompactsForShortDashboardHeights and
testDashboardLayoutUsesCompactTokensForNarrowWindowWidths currently assert exact
magic numbers (outerPadding == 14, metricValueFontSize == 20, sparklineWidth ==
280); change them to avoid tight coupling by either (A) asserting invariants
between layout variants (e.g., create a regular Layout and assert
layout.outerPadding < regular.outerPadding, layout.scale < 1,
layout.metricValueFontSize < =or< regular.metricValueFontSize, sparklineWidth <
regular.sparklineWidth) or (B) centralize the expected values as public static
constants on BrainBarDashboardLayout (e.g.,
BrainBarDashboardLayout.compactOuterPadding, compactMetricValueFontSize,
compactSparklineWidth) and reference those constants in the tests instead of
hardcoded literals.
- Around line 57-99: Add a boundary test for
DashboardMetricFormatter.lastCompletionString to cover the transition between
"Just now" and "Xm ago": call
DashboardMetricFormatter.lastCompletionString(recentEnrichmentBuckets:
[0,0,0,0,1,0], activityWindowMinutes: 30) and assert the expected string (e.g.,
"5m ago") so off-by-one regressions in lastCompletionString are caught;
reference the lastCompletionString method and the
recentEnrichmentBuckets/activityWindowMinutes parameters to locate where to add
this new test.

In `@brain-bar/Tests/BrainBarTests/KnowledgeGraphTests.swift`:
- Line 42: Replace hardcoded relationType string literals used in test setup
calls to insertRelation with shared constants: define a private enum
TestRelationType (e.g., static lets builds, supports, uses) near the top of
KnowledgeGraphTests and then change all insertRelation(sourceId:...,
targetId:..., relationType: "...") calls to use TestRelationType.builds /
.supports / .uses; update every occurrence noted in the review so relationType
consistently references the enum constants instead of raw strings.

In `@brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift`:
- Around line 415-461: Add a verification that the stalled client socket was
closed after the retry cap by performing a non-blocking probe read on clientFD
after the Thread.sleep: set clientFD to non-blocking (or use recv with
MSG_DONTWAIT) and call recv()/read() on clientFD, then assert the call indicates
EOF (returns 0) or a closed connection error; place this check in
testServerDisconnectsStalledClient alongside the existing second client check so
the test ensures clientFD was actually disconnected by the server's retry
mechanism rather than merely allowing a second client via sendMCPRequest.

In `@brain-bar/Tests/BrainBarTests/StabilityFixTests.swift`:
- Around line 150-156: The test
testStatusPopoverViewFrameMatchesStableUtilityPanel is tautological and should
be changed to assert against the production source of truth instead of literals:
replace the NSRect literal assertions with a comparison to
PopoverTab.contentSize (or the actual API that defines the stable utility panel
size) by instantiating StatusPopoverView, calling
StatusPopoverView.loadView()/loadViewIfNeeded() (or accessing its view) and
comparing the view.frame.size (or view.bounds.size) to PopoverTab.contentSize;
alternatively remove the test entirely if coverage is redundant with
PopoverTabTests.testPopoverTabDashboardSizeIsUtilityPanel.

---

Outside diff comments:
In `@brain-bar/build-app.sh`:
- Around line 113-135: The socket wait and Python connect probe currently run
unconditionally and will fail when PLIST_SRC is missing; wrap the
wait_for_socket "$SOCKET_PATH" and the subsequent Python probe in the same
conditional that checks for the LaunchAgent (i.e., only run them if [ -f
"$PLIST_SRC" ]), or alternatively, in the branch where PLIST_SRC is absent,
start BrainBar manually before probing; specifically modify the logic around
wait_for_socket, SOCKET_PATH and the python connect probe so they are executed
only when the PLIST_SRC/LaunchAgent path exists (or ensure BrainBar is launched
in the no-plist branch) to avoid hard-failing builds that legitimately lack the
plist.

In `@brain-bar/Sources/BrainBar/BrainDatabase.swift`:
- Around line 1940-1956: The ORDER BY in listInjectionEvents (function
listInjectionEvents) is nondeterministic for rows with identical timestamp;
change the SQL built in listInjectionEvents to include a stable secondary sort
(e.g., "ORDER BY timestamp DESC, id DESC") so LIMIT returns deterministic rows,
and ensure the binding/prepare logic still applies to the modified sql string
used to create the sqlite3_stmt for InjectionEvent rows.
- Around line 870-950: Both recentActivityBuckets and recentEnrichmentBuckets
duplicate identical bucket calculation logic; extract a shared helper (e.g.,
recentBuckets(column: String, additionalWhere: String = "",
activityWindowMinutes: Int, bucketCount: Int) throws -> [Int]) that centralizes
the guards, DB checks, SQL preparation (with a parameterized column and optional
extra predicate), the date parsing using Self.sqliteDateFormatter, offset
clamping and bucket incrementing, then make recentActivityBuckets call
recentBuckets(column: "created_at", activityWindowMinutes:..., bucketCount:...)
and make recentEnrichmentBuckets call recentBuckets(column: "enriched_at",
additionalWhere: "enriched_at IS NOT NULL AND TRIM(enriched_at) != ''",
activityWindowMinutes:..., bucketCount:...).
- Around line 1912-1936: Make
recordInjectionEvent(sessionID:query:chunkIDs:tokenCount:timestamp:) throw
instead of returning silent defaults: remove the guard that returns
InjectionEvent when db is nil and instead throw a descriptive error (e.g.,
ToolError.noDatabase or a DatabaseError.noDB), and if sqlite3_prepare_v2 fails
throw a database preparation error including sqlite3_errmsg(db) text rather than
returning InjectionEvent(id:0,...); also change the sqlite3_step failure path to
throw a write/step error when stepRC != SQLITE_DONE, keep the existing final
InjectionEvent construction only for the successful path, and update function
signature and any callsites already using try to propagate the thrown errors.

In `@brain-bar/Sources/BrainBar/InjectionStore.swift`:
- Around line 4-16: Add a deinit to InjectionStore that mirrors the existing
stop() cleanup by removing the Darwin notification observer from
CFNotificationCenter (use CFNotificationCenterGetDarwinNotifyCenter() /
CFNotificationCenterRemoveObserver with the same observer opaque pointer used
when installing the callback) to prevent use-after-free when stop() is not
called; update the install/remove pairing so the observer ownership is
consistent (either keep Unmanaged.passUnretained on install and remove in
deinit/stop with no retain semantics, or switch the install to
Unmanaged.passRetained and remove with takeRetainedValue at teardown) and ensure
injectionStoreDarwinNotificationCallback remains compatible with the chosen
ownership model.

In `@brain-bar/Sources/BrainBar/QuickCapturePanel.swift`:
- Around line 873-881: Remove the duplicate .frame(width: 540, height: 360)
modifier in QuickCapturePanel.swift: the first .frame applied before .onChange
is sufficient, so delete the second .frame call that appears just before
.animation(..., value: viewModel.mode), leaving the single frame, preserving the
.onChange { viewModel.confirmationFlashCount ... } logic, flashOpacity updates,
and the final .animation modifier.

In `@brain-bar/Tests/BrainBarTests/InjectionStoreTests.swift`:
- Around line 22-28: Change recordInjectionEvent to be a throwing function (func
recordInjectionEvent(...) throws -> InjectionEvent) and update callers/tests to
use try; inside, replace the silent-return cases: when db is nil throw a
specific error (e.g., ToolError.noDatabase) instead of returning
InjectionEvent(id:0,...), and when sqlite3_prepare_v2 fails throw an
NSError/ToolError that includes the SQLite error message/code rather than
returning a zero-id event. Ensure the function propagates these thrown errors
outward so callers (tests and MCP clients) receive explicit failures.

In `@brain-bar/Tests/BrainBarTests/PopoverTabTests.swift`:
- Around line 55-60: The test named testPopoverTabGraphSizeIsTallerThanDashboard
contradicts its assertion (it checks equality between
PopoverTab.graph.contentSize.height and
PopoverTab.dashboard.contentSize.height); either delete this redundant test
(since testPopoverTabsShareStablePanelSize already asserts equality) or rename
it to reflect its actual check (e.g., testPopoverTabGraphSizeEqualsDashboard) or
change the assertion to a greater-than check if the intent is to enforce graph
being taller (replace XCTAssertEqual with XCTAssertGreaterThan for
PopoverTab.graph.contentSize.height > PopoverTab.dashboard.contentSize.height).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a767c1ec-316c-4333-aac0-36efe5866573

📥 Commits

Reviewing files that changed from the base of the PR and between ec0c12b and aab2891.

⛔ Files ignored due to path filters (2)
  • docs.local/plans/2026-04-17-brainbar-linear-ux/artifacts/brainbar-after-inline-dashboard.png is excluded by !**/*.png
  • docs.local/plans/2026-04-17-brainbar-linear-ux/artifacts/brainbar-before-inline-dashboard.png is excluded by !**/*.png
📒 Files selected for processing (27)
  • brain-bar/Sources/BrainBar/BrainBarApp.swift
  • brain-bar/Sources/BrainBar/BrainBarCommandBar.swift
  • brain-bar/Sources/BrainBar/BrainBarRuntime.swift
  • brain-bar/Sources/BrainBar/BrainBarServer.swift
  • brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift
  • brain-bar/Sources/BrainBar/BrainBarWindowState.swift
  • brain-bar/Sources/BrainBar/BrainDatabase.swift
  • brain-bar/Sources/BrainBar/Dashboard/BrainBarLivePulse.swift
  • brain-bar/Sources/BrainBar/Dashboard/DashboardMetricFormatter.swift
  • brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift
  • brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift
  • brain-bar/Sources/BrainBar/Dashboard/StatusPopoverView.swift
  • brain-bar/Sources/BrainBar/Formatting/TextFormatter.swift
  • brain-bar/Sources/BrainBar/InjectionFeedView.swift
  • brain-bar/Sources/BrainBar/InjectionStore.swift
  • brain-bar/Sources/BrainBar/QuickCapturePanel.swift
  • brain-bar/Tests/BrainBarTests/BrainBarUXLogicTests.swift
  • brain-bar/Tests/BrainBarTests/BrainBarWindowStateTests.swift
  • brain-bar/Tests/BrainBarTests/DashboardTests.swift
  • brain-bar/Tests/BrainBarTests/DatabaseTests.swift
  • brain-bar/Tests/BrainBarTests/InjectionStoreTests.swift
  • brain-bar/Tests/BrainBarTests/KnowledgeGraphTests.swift
  • brain-bar/Tests/BrainBarTests/PopoverTabTests.swift
  • brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift
  • brain-bar/Tests/BrainBarTests/StabilityFixTests.swift
  • brain-bar/Tests/BrainBarTests/TextFormatterParityTests.swift
  • brain-bar/build-app.sh
💤 Files with no reviewable changes (1)
  • brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.13)
  • GitHub Check: Macroscope - Correctness Check
🧰 Additional context used
🧠 Learnings (11)
📓 Common learnings
Learnt from: CR
Repo: EtanHey/brainlayer PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-14T02:20:54.656Z
Learning: Request codex review, cursor review, and bugbot review for BrainLayer PRs
📚 Learning: 2026-03-18T00:12:08.774Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 87
File: brain-bar/Sources/BrainBar/BrainBarServer.swift:118-129
Timestamp: 2026-03-18T00:12:08.774Z
Learning: In Swift files under brain-bar/Sources/BrainBar, enforce that when a critical dependency like the database is nil due to startup ordering (socket before DB), any tool handler that accesses the database must throw an explicit error (e.g., ToolError.noDatabase) instead of returning a default/empty value. Do not allow silent defaults (e.g., guard let db else { return ... }). Flag patterns that silently return defaults when db is nil, as this masks startup timing issues. This guidance applies broadly to similar Swift files in the BrainBar module, not just this one location.

Applied to files:

  • brain-bar/Sources/BrainBar/Dashboard/BrainBarLivePulse.swift
  • brain-bar/Sources/BrainBar/InjectionFeedView.swift
  • brain-bar/Sources/BrainBar/BrainBarServer.swift
  • brain-bar/Sources/BrainBar/InjectionStore.swift
  • brain-bar/Sources/BrainBar/QuickCapturePanel.swift
  • brain-bar/Sources/BrainBar/BrainDatabase.swift
  • brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift
  • brain-bar/Sources/BrainBar/BrainBarCommandBar.swift
  • brain-bar/Sources/BrainBar/Dashboard/StatusPopoverView.swift
  • brain-bar/Sources/BrainBar/Dashboard/DashboardMetricFormatter.swift
  • brain-bar/Sources/BrainBar/Formatting/TextFormatter.swift
  • brain-bar/Sources/BrainBar/BrainBarRuntime.swift
  • brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift
  • brain-bar/Sources/BrainBar/BrainBarWindowState.swift
  • brain-bar/Sources/BrainBar/BrainBarApp.swift
📚 Learning: 2026-03-17T01:04:11.749Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 0
File: :0-0
Timestamp: 2026-03-17T01:04:11.749Z
Learning: The socket path `/tmp/brainbar.sock` is intentional for the BrainBar Swift daemon (brain-bar/) and must NOT be changed to `/tmp/brainlayer.sock`. BrainBar is a new daemon that coexists with the existing Python `brainlayer-mcp` (which uses `/tmp/brainlayer.sock`) during the migration period. The different paths avoid conflicts and allow A/B testing. Once BrainBar is proven stable, the Python server will be retired and `.mcp.json` will point to `/tmp/brainbar.sock` via socat.

Applied to files:

  • brain-bar/build-app.sh
  • brain-bar/Sources/BrainBar/BrainBarServer.swift
  • brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift
  • brain-bar/Sources/BrainBar/BrainBarApp.swift
📚 Learning: 2026-03-18T00:12:15.607Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 87
File: brain-bar/Sources/BrainBar/BrainBarServer.swift:118-129
Timestamp: 2026-03-18T00:12:15.607Z
Learning: In `brain-bar/Sources/BrainBar/MCPRouter.swift` (Swift, BrainBar daemon), the socket-before-DB startup pattern means the Unix socket binds immediately (~1ms) while the database may take several seconds to open on cold start (8GB file). Any tool handler that accesses `database` MUST throw an explicit error (e.g., `ToolError.noDatabase`) when `database` is nil — never return empty or default results (e.g., `guard let db else { return "[]" }` is forbidden). The false-success pattern hides startup timing issues from MCP clients. Flag any `guard let db = database else { return ... }` patterns that silently return defaults instead of throwing.

Applied to files:

  • brain-bar/build-app.sh
  • brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift
📚 Learning: 2026-03-17T01:04:22.497Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 0
File: :0-0
Timestamp: 2026-03-17T01:04:22.497Z
Learning: In BrainLayer, the BrainBar daemon uses the socket path `/tmp/brainbar.sock` (NOT `/tmp/brainlayer.sock`). BrainBar is a new native Swift daemon designed to coexist with the existing Python `brainlayer-mcp` server during the migration period. Different socket paths avoid conflicts and enable A/B testing. Once BrainBar is proven stable, the Python server will be retired.

Applied to files:

  • brain-bar/build-app.sh
  • brain-bar/Sources/BrainBar/BrainBarServer.swift
  • brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift
  • brain-bar/Sources/BrainBar/BrainBarApp.swift
📚 Learning: 2026-04-06T08:40:13.531Z
Learnt from: CR
Repo: EtanHey/brainlayer PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T08:40:13.531Z
Learning: Use LaunchAgent `com.brainlayer.watch.plist` with KeepAlive=true and Nice=10 for persistent watcher process

Applied to files:

  • brain-bar/build-app.sh
📚 Learning: 2026-03-29T18:45:40.988Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 133
File: brain-bar/Sources/BrainBar/BrainDatabase.swift:0-0
Timestamp: 2026-03-29T18:45:40.988Z
Learning: In the BrainBar module’s Swift database layer (notably BrainDatabase.swift), ensure that the `search()` function’s `unreadOnly=true` path orders results by the delivery frontier cursor so the watermark `maxRowID` stays contiguous. Specifically, when `unreadOnly` is enabled, the query must include `ORDER BY c.rowid ASC` (e.g., via `let orderByClause = unreadOnly ? "c.rowid ASC" : "f.rank"`). Do not replace the unread-only ordering with relevance-based sorting (e.g., `f.rank`) unconditionally or for the unread-only path, as it can introduce gaps in the watermark and incorrectly mark unseen rows as delivered. Flag any future change to the `ORDER BY` clause in this function that makes relevance sorting apply to the unread-only case.

Applied to files:

  • brain-bar/Sources/BrainBar/InjectionFeedView.swift
  • brain-bar/Sources/BrainBar/BrainBarServer.swift
  • brain-bar/Sources/BrainBar/InjectionStore.swift
  • brain-bar/Sources/BrainBar/QuickCapturePanel.swift
  • brain-bar/Sources/BrainBar/BrainDatabase.swift
  • brain-bar/Sources/BrainBar/BrainBarCommandBar.swift
  • brain-bar/Sources/BrainBar/BrainBarRuntime.swift
  • brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift
  • brain-bar/Sources/BrainBar/BrainBarWindowState.swift
  • brain-bar/Sources/BrainBar/BrainBarApp.swift
📚 Learning: 2026-03-18T00:12:36.931Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 87
File: brain-bar/Sources/BrainBar/MCPRouter.swift:0-0
Timestamp: 2026-03-18T00:12:36.931Z
Learning: In `brain-bar/Sources/BrainBar/MCPRouter.swift` (Swift, BrainBar MCP daemon), the notification guard `let isNotification = (rawID == nil || rawID is NSNull)` is the single and only point where a no-response decision is made. Any message that passes this guard has a non-nil, non-NSNull id and MUST return a proper JSON-RPC response. Returning `[:]` (empty dict = no response) anywhere after the notification guard is always a bug — it creates a silent client hang. Flag any `return [:]` that appears after the guard in future reviews.

Applied to files:

  • brain-bar/Sources/BrainBar/BrainBarServer.swift
  • brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift
  • brain-bar/Sources/BrainBar/BrainDatabase.swift
  • brain-bar/Sources/BrainBar/BrainBarApp.swift
📚 Learning: 2026-04-10T23:28:04.199Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 0
File: :0-0
Timestamp: 2026-04-10T23:28:04.199Z
Learning: In `hooks/brainlayer-prompt-search.py` (EtanHey/brainlayer repo), `co_occurs_with` relation type is filtered out at SQL-time in `get_entity_chunks()` using a `relation_filter` when the `relation_type` column exists on `kg_entity_chunks`. This is intentional: `co_occurs_with` represents ~85.5% of KG relations and is same-chunk co-occurrence noise (bag-of-words baseline), not a semantic relationship. Entity cards should only surface real semantic edges such as DEPENDS_ON, WORKS_AT, MERGED_BY, etc. Do not flag this SQL exclusion as a bug or suggest removing it in future reviews.

Applied to files:

  • brain-bar/Tests/BrainBarTests/KnowledgeGraphTests.swift
📚 Learning: 2026-04-04T15:22:02.740Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 198
File: hooks/brainlayer-prompt-search.py:241-259
Timestamp: 2026-04-04T15:22:02.740Z
Learning: In `hooks/brainlayer-prompt-search.py` (Python), `record_injection_event()` is explicitly best-effort telemetry: silent `except sqlite3.Error: pass` is intentional — table non-existence or lock failures are acceptable silent failures. `sqlite3.connect(timeout=2)` is the file-open timeout; `PRAGMA busy_timeout` governs per-statement lock-wait. The `DEADLINE_MS` (450ms) guard applies only to the FTS search phase, not to this side-channel write.

Applied to files:

  • brain-bar/Sources/BrainBar/BrainDatabase.swift
📚 Learning: 2026-04-02T23:32:14.543Z
Learnt from: CR
Repo: EtanHey/brainlayer PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T23:32:14.543Z
Learning: Applies to src/brainlayer/*enrichment*.py : Enrichment rate configurable via `BRAINLAYER_ENRICH_RATE` environment variable (default 0.2 = 12 RPM)

Applied to files:

  • brain-bar/Sources/BrainBar/BrainDatabase.swift
🪛 SwiftLint (0.63.2)
brain-bar/Sources/BrainBar/BrainBarCommandBar.swift

[Warning] 139-139: Classes should have an explicit deinit method

(required_deinit)

brain-bar/Sources/BrainBar/BrainBarRuntime.swift

[Warning] 4-4: Classes should have an explicit deinit method

(required_deinit)

brain-bar/Tests/BrainBarTests/BrainBarWindowStateTests.swift

[Warning] 6-6: Classes should have an explicit deinit method

(required_deinit)


[Warning] 71-71: Classes should have an explicit deinit method

(required_deinit)


[Warning] 313-313: Classes should have an explicit deinit method

(required_deinit)


[Warning] 326-326: Classes should have an explicit deinit method

(required_deinit)

brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift

[Warning] 582-582: Classes should have an explicit deinit method

(required_deinit)


[Warning] 611-611: Classes should have an explicit deinit method

(required_deinit)

brain-bar/Sources/BrainBar/BrainBarWindowState.swift

[Warning] 390-390: Classes should have an explicit deinit method

(required_deinit)

brain-bar/Sources/BrainBar/BrainBarApp.swift

[Error] 381-381: Force casts should be avoided

(force_cast)


[Error] 472-472: Force casts should be avoided

(force_cast)


[Error] 473-473: Force casts should be avoided

(force_cast)

🔇 Additional comments (15)
brain-bar/Sources/BrainBar/Dashboard/BrainBarLivePulse.swift (1)

1-7: LGTM!

Trivial, pure decision helper with clear semantics. Covered by the new BrainBarUXLogicTests per the summary.

brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift (1)

35-45: LGTM — optional accentColor with state.color fallback is clean.

Backward compatible for callers like StatusPopoverView (no accent passed → state-driven), while the window/menu-bar callers correctly pass livePresentation.accentColor to get the active/idle distinction.

brain-bar/Tests/BrainBarTests/TextFormatterParityTests.swift (1)

54-54: LGTM.

Dropping the direction: argument matches the simplified EntityCard.Relation initializer; the expected → works_on: BrainLayer line is unchanged, so parity remains exercised.

brain-bar/Tests/BrainBarTests/DatabaseTests.swift (1)

207-250: Confirm that UI/API consumers can handle non-deterministic event ordering when timestamps collide.

The removal of testListInjectionEventsUsesIDAsTieBreakerForMatchingTimestamps aligns with the new ORDER BY timestamp DESC-only SQL in BrainDatabase.swift:1944. This means events with identical timestamps now have undefined order. Since InjectionFeedView displays events in database query order and MCPRouter iterates them directly, the UI and API output depend on this ordering. Under bursty load or scenarios with millisecond-precision timestamps, collisions could cause display instability. Verify that dashboard consumers tolerate non-deterministic ordering, or add a secondary sort key (e.g., ORDER BY timestamp DESC, id ASC) to guarantee stability.

brain-bar/Tests/BrainBarTests/DashboardTests.swift (1)

109-149: LGTM — rename and fixture changes correctly exercise the 1-minute enrichment rate window.

Both tests now assert enrichmentRatePerMinute == 0 while keeping recentEnrichmentBuckets count at 1, which matches the hardcoded enrichmentRateWindowMinutes = 1 in dashboardStats (line 786 of BrainDatabase.swift). The 6-minute and 90-second offsets are both outside the 1-min window but inside the 30-min bucket window.

brain-bar/Sources/BrainBar/Formatting/TextFormatter.swift (2)

10-50: LGTM — box-drawing search output reads cleanly.

The rewrite is self-consistent: truncation is centralized via truncate, per-result formatting handles nil importance, tags, and multi-row separators correctly, and the total == 0 early return prevents an empty table. No correctness concerns.


138-168: formatEntitySimple refactor looks good.

Boxed layout with bounded prefix(…) on relations/chunks/metadata matches the arrow-direction behavior in formatEntityCard and the updated TextFormatterParityTests.

brain-bar/Sources/BrainBar/BrainBarCommandBar.swift (1)

136-211: CommandBarInput bridge looks solid.

Coordinator uses a counter-based focus request (focusRequestCount), text/placeholder are diffed before writes, and command-key routing (Tab / Up / Down / Return / Shift-Return) is cleanly dispatched. No concurrency or TDZ pitfalls here.

brain-bar/Sources/BrainBar/BrainBarApp.swift (2)

362-420: AX plumbing reads correctly.

The two-pass match (titledMatches first, fallbackMatches second) paired with BrainBarWindowPlacement.preferredMenuBarItemFrame(…) to pick by mouse proximity cleanly handles multi-display setups. Force casts on extrasBar as! AXUIElement / AXValue are the standard AX API pattern; SwiftLint's warnings at 381/472/473 are safe to silence here (or with a file-scoped // swiftlint:disable:next force_cast).


121-135: Termination cleanup is thorough.

Observer removal, sync-task cancel, hotkey stop, collector/injection/server shutdown are all covered. Good. Also good that applicationShouldTerminateAfterLastWindowClosed returns false — otherwise hiding the MenuBarExtra window would tear the process down.

brain-bar/Tests/BrainBarTests/BrainBarWindowStateTests.swift (1)

70-311: Thorough coordinator/runtime coverage.

The fake stores give the tests deterministic inputs, and the placement round-trip test across a raised secondary display (line 136-156) is particularly useful for catching multi-monitor regressions. Runtime callback tests (showSearchPanel / showQuickCapturePanel counting onto onSearchRequested / onQuickCaptureRequested) lock in the contract that AppDelegate now depends on. Nice additions.

brain-bar/Sources/BrainBar/BrainBarRuntime.swift (1)

1-58: Runtime is a clean coordination surface.

handleToggleRequest's short-circuit (launchMode == .menuBarWindow, windowCoordinator.toggleVisibility()) correctly falls back to onToggleRequested whenever the window coordinator can't toggle directly, letting AppDelegate drive the AX-based recovery path. The @Published private(set) set + optional callbacks give a tight, testable API.

brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift (3)

107-118: ensureCommandBarViewModel is a one-shot; double-check reopen semantics.

The guard commandBarViewModel == nil, let database = runtime.database means the view-model is created exactly once for the lifetime of this view. If runtime.database is ever reinstalled (e.g., after a swap or restart path) the old QuickCaptureViewModel will keep running against the prior database. In the current PR scope database is only installed once, so this is fine — just flagging that future code changing install(…) semantics needs to invalidate the VM here.


20-75: Root view composition reads well.

Tab state, quick-action handling (handleRequestedQuickAction → switches to dashboard, sets mode, calls panelDidAppear, clears request) matches the runtime contract covered by BrainBarWindowStateTests. The WindowAttachmentView + BrainBarWindowObserver split keeps AppKit work out of the SwiftUI view hierarchy.


265-270: Platforms requirement is correctly set to macOS 14.

The Package.swift confirms the target's minimum platform is .macOS(.v14), which correctly supports the two-argument onChange closure signature used at lines 265 and 539. No action required.

Comment on lines +306 to +324
private func discoverMenuBarWindow() -> NSWindow? {
if let menuBarExtraWindow {
return menuBarExtraWindow
}

if let discoveredMenuBarWindow {
return discoveredMenuBarWindow
}

let window = NSApp.windows.first { candidate in
let isExcluded = searchPanel?.panelForTesting === candidate
return !isExcluded &&
candidate.title == "BrainBar" &&
candidate.frame.width >= 400 &&
candidate.frame.height >= 300
}
discoveredMenuBarWindow = window
return window
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent size thresholds when identifying the MenuBarExtra window.

discoverMenuBarWindow accepts any window titled "BrainBar" ≥ 400×300 (line 319-320), while isMenuBarExtraWindow in the notification observer requires ≥ 760×560 (line 671-672). During the brief moment the MenuBarExtra window is being sized up from its initial frame, discoverMenuBarWindow can attach to it before handleMenuBarWindowBecameKey will recognize it, which can leave menuBarExtraWindow pointing at a window that the observer path then refuses to persist/sync.

Consider consolidating on a single predicate (e.g., share isMenuBarExtraWindow(_:) between the two paths) so discovery and observer identification agree.

Also applies to: 668-673

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/BrainBarApp.swift` around lines 306 - 324,
discoverMenuBarWindow currently uses hardcoded size thresholds that differ from
isMenuBarExtraWindow, causing a race where discoveredMenuBarWindow can point to
a window the observer later rejects; update discoverMenuBarWindow to use the
same predicate as the observer by calling the shared isMenuBarExtraWindow(_:)
(or extract that predicate into a single helper used by both
discoverMenuBarWindow and handleMenuBarWindowBecameKey) and include the same
excluded check (searchPanel?.panelForTesting) so both discovery and the
notification observer agree on which window qualifies; ensure you update any
assignments to menuBarExtraWindow/discoveredMenuBarWindow to rely on that
unified predicate.

Comment on lines +861 to 882
private struct BrainBarMenuBarLabel: View {
@ObservedObject var runtime: BrainBarRuntime

var body: some View {
if let collector = runtime.collector {
let livePresentation = BrainBarLivePresentation.derive(stats: collector.stats)
HStack(spacing: 6) {
Image(systemName: "brain")
Image(
nsImage: SparklineRenderer.render(
state: collector.state,
values: collector.stats.recentEnrichmentBuckets,
size: NSSize(width: 22, height: 12),
accentColor: livePresentation.accentColor
)
)
.interpolation(.high)
}
} else {
Image(systemName: "brain")
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

BrainBarMenuBarLabel re-renders a sparkline NSImage on every stats publish.

SparklineRenderer.render(…) is invoked inside body, so SwiftUI will allocate a fresh NSImage every time collector.stats changes (which happens on the collector cadence). That's fine functionally but worth being aware of — if StatsCollector.$stats publishes frequently, you'll be paying an image allocation and a draw per tick while the menu bar is idle.

If you see it show up in Instruments, cache by (state, values hash, size) or gate re-rendering via onChange.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/BrainBarApp.swift` around lines 861 - 882,
BrainBarMenuBarLabel currently calls SparklineRenderer.render(...) directly in
the View body whenever runtime.collector.stats changes, causing a new NSImage
allocation every publish; fix this by moving the rendering out of the body and
caching the image: add a `@State` (or `@StateObject`) NSImage property and a
lightweight cache key (e.g., hash of collector.state,
collector.stats.recentEnrichmentBuckets, and desired size) and update that image
only when the key changes (use onChange(of: collector.stats) or onReceive of the
publisher to recompute via SparklineRenderer.render and assign the `@State`
image); keep the body only referencing the cached image and still derive
BrainBarLivePresentation from collector.stats for colors if needed.

Comment on lines +388 to +391
.contentShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
.onTapGesture(perform: onSelect)
.onTapGesture(count: 2, perform: onActivate)
.animation(.easeInOut(duration: 0.14), value: isCopied)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat brain-bar/Sources/BrainBar/BrainBarCommandBar.swift | head -n 410 | tail -n 40

Repository: EtanHey/brainlayer

Length of output: 1462


🏁 Script executed:

rg "onSelect|onActivate" brain-bar/Sources/BrainBar/BrainBarCommandBar.swift -B 2 -A 5

Repository: EtanHey/brainlayer

Length of output: 1043


🏁 Script executed:

fd "\.swift$" brain-bar/Sources/BrainBar/ | head -20

Repository: EtanHey/brainlayer

Length of output: 1139


🏁 Script executed:

rg "selectResult|copyResultToClipboard" brain-bar/Sources/BrainBar/ -B 2 -A 8

Repository: EtanHey/brainlayer

Length of output: 6353


🏁 Script executed:

rg "double.?click|double.?tap|gesture" brain-bar/Sources/BrainBar/BrainBarCommandBar.swift -i

Repository: EtanHey/brainlayer

Length of output: 155


🏁 Script executed:

rg "TODO|FIXME|NOTE|HACK" brain-bar/Sources/BrainBar/BrainBarCommandBar.swift

Repository: EtanHey/brainlayer

Length of output: 44


🌐 Web query:

SwiftUI onTapGesture stacked multiple gesture behavior double-click precedence

💡 Result:

In SwiftUI, when multiple onTapGesture modifiers are stacked on the same view (e.g., single tap and double tap), or on parent-child views, gesture recognition follows specific precedence rules to avoid conflicts, especially with double-clicks (double taps). Default child precedence: Gestures on child views take priority over parent views. A tap on a child triggers the child's gesture first, potentially blocking the parent's. To override: Use highPriorityGesture on the parent to make it recognize before child gestures. For simultaneous recognition: Use simultaneousGesture on the gesture that would otherwise be blocked. For single vs double tap on same view: Order matters with onTapGesture. Place higher count (e.g., double tap count:2) before lower count (single tap count:1), as the first matching gesture consumes the input. Double tap cancels single tap if recognized first. Double-click behavior: A double tap is two quick taps. Without proper composition, it may trigger single tap twice or with delay. Use TapGesture(count:2) with .exclusively(before: TapGesture(count:1)) or SimultaneousGesture for exclusive recognition without overlap. Modifier order affects priority; later modifiers can override earlier ones in some cases. Official composition: Use .gesture with sequenced(before:), .exclusively(before:), or .simultaneously(with:) for complex interactions.

Citations:


Fix gesture composition to handle double-tap correctly.

SwiftUI requires double-tap (count:2) to be registered before single-tap (count:1) so the system can distinguish them. Currently, the single-tap modifier is applied first, causing it to fire on the first tap of a double-click before the double-tap can be recognized. Either reorder the modifiers with count:2 first, or use explicit gesture composition (e.g., .gesture(TapGesture(count:2).exclusively(before: TapGesture(count:1)))) to avoid unintended cascading behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/BrainBarCommandBar.swift` around lines 388 - 391,
The single-tap is currently attached before the double-tap so a double-click
triggers onSelect on the first tap; reorder the tap handlers or compose gestures
so double-tap wins. Locate the view modifiers where .onTapGesture(perform:
onSelect) and .onTapGesture(count: 2, perform: onActivate) are applied (the same
view using contentShape and isCopied animation) and either move the count:2
onTapGesture before the single-tap, or replace both with a composed gesture like
TapGesture(count:2).exclusively(before: TapGesture(count:1)) so onActivate is
recognized on double-tap and onSelect only on single-tap.

Comment on lines +341 to 371
return framed.withUnsafeBytes { ptr in
var totalWritten = 0
var eagainRetries = 0
while totalWritten < framed.count {
let n = write(fd, ptr.baseAddress!.advanced(by: totalWritten), framed.count - totalWritten)
if n < 0 {
if errno == EAGAIN || errno == EWOULDBLOCK {
eagainRetries += 1
if eagainRetries > Self.maxWriteRetries {
NSLog("[BrainBar] ⚠️ Write stalled on fd %d after %d EAGAIN retries (%d ms) — disconnecting dead client", fd, eagainRetries, eagainRetries - 1)
disconnectClient(fd: fd)
return false
}
usleep(1000) // 1 ms
continue
}
scheduleWriteRetryIfNeeded(fd: fd)
return true
NSLog("[BrainBar] Write error on fd %d: errno %d", fd, errno)
disconnectClient(fd: fd)
return false
}
NSLog("[BrainBar] Write error on fd %d: errno %d", fd, errno)
disconnectClient(fd: fd)
return false
}
if n == 0 {
NSLog("[BrainBar] Write returned 0 on fd %d — peer closed", fd)
disconnectClient(fd: fd)
return false
}

pending.totalWritten += n
pending.lastProgressAt = DispatchTime.now().uptimeNanoseconds

guard var latest = clients[fd] else { return false }
latest.pendingWrites[0] = pending

if pending.totalWritten == pending.data.count {
let onDelivered = pending.onDelivered
latest.pendingWrites.removeFirst()
clients[fd] = latest
onDelivered?()
} else {
clients[fd] = latest
if n == 0 {
NSLog("[BrainBar] Write returned 0 on fd %d — peer closed", fd)
disconnectClient(fd: fd)
return false
}
totalWritten += n
eagainRetries = 0 // reset on successful partial write
}
}

return clients[fd] != nil
}

private func scheduleWriteRetryIfNeeded(fd: Int32) {
guard var state = clients[fd], !state.hasScheduledWriteRetry else { return }
state.hasScheduledWriteRetry = true
clients[fd] = state

let retryDelay = DispatchTimeInterval.microseconds(Int(Self.writeRetrySleepMicroseconds))
queue.asyncAfter(deadline: .now() + retryDelay) { [weak self] in
guard let self, self.clients[fd] != nil else { return }
_ = self.flushPendingWrites(fd: fd)
return true
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm publishStoredChunk runs on the same serial queue and uses sendResponse.
ast-grep --pattern $'private func publishStoredChunk($$$) {
  $$$
}'
# Check whether there are any per-client queues or writer dispatch sources.
rg -nP 'DispatchSource\.makeWriteSource|DispatchQueue\(label:' -C2 brain-bar/Sources/BrainBar/BrainBarServer.swift

Repository: EtanHey/brainlayer

Length of output: 4036


🏁 Script executed:

rg -n 'publishStoredChunk' brain-bar/Sources/BrainBar/BrainBarServer.swift

Repository: EtanHey/brainlayer

Length of output: 248


🏁 Script executed:

# Check the context around sendResponse calls to see if they're dispatched or run synchronously
rg -n 'func sendResponse' brain-bar/Sources/BrainBar/BrainBarServer.swift

Repository: EtanHey/brainlayer

Length of output: 172


🏁 Script executed:

# Look for queue.async or queue.sync dispatches that might call publishStoredChunk
rg -n 'queue\.(async|sync)' brain-bar/Sources/BrainBar/BrainBarServer.swift | head -20

Repository: EtanHey/brainlayer

Length of output: 127


🏁 Script executed:

sed -n '310,320p' brain-bar/Sources/BrainBar/BrainBarServer.swift

Repository: EtanHey/brainlayer

Length of output: 546


🏁 Script executed:

sed -n '130,160p' brain-bar/Sources/BrainBar/BrainBarServer.swift

Repository: EtanHey/brainlayer

Length of output: 998


🏁 Script executed:

# Find the function containing line 314
sed -n '280,320p' brain-bar/Sources/BrainBar/BrainBarServer.swift

Repository: EtanHey/brainlayer

Length of output: 1965


🏁 Script executed:

# Search for all queue.sync and queue.async calls to understand the dispatch pattern
rg -n 'queue\.(async|sync)' brain-bar/Sources/BrainBar/BrainBarServer.swift -B3 -A5

Repository: EtanHey/brainlayer

Length of output: 351


🏁 Script executed:

# Find where the message processing loop (containing line 314) is called from
sed -n '270,295p' brain-bar/Sources/BrainBar/BrainBarServer.swift

Repository: EtanHey/brainlayer

Length of output: 1409


🏁 Script executed:

# Look for the function definition that contains line 314 and trace its call context
rg -n 'private func processMessages|processMessages' brain-bar/Sources/BrainBar/BrainBarServer.swift -A2

Repository: EtanHey/brainlayer

Length of output: 44


🏁 Script executed:

# Find the function definition that contains the message loop
rg -n 'func.*fd.*Int32' brain-bar/Sources/BrainBar/BrainBarServer.swift | head -10

Repository: EtanHey/brainlayer

Length of output: 677


🏁 Script executed:

# Search for where the message processing happens in dispatch handlers
rg -n 'DispatchSourceRead\|readSource' brain-bar/Sources/BrainBar/BrainBarServer.swift -A5

Repository: EtanHey/brainlayer

Length of output: 44


🏁 Script executed:

sed -n '257,302p' brain-bar/Sources/BrainBar/BrainBarServer.swift

Repository: EtanHey/brainlayer

Length of output: 2068


🏁 Script executed:

# Check for DispatchSourceRead setup in startOnQueue
sed -n '147,250p' brain-bar/Sources/BrainBar/BrainBarServer.swift

Repository: EtanHey/brainlayer

Length of output: 3908


Synchronous retry loop blocks the serial queue; fanout amplifies the stall.

sendResponse performs a bounded 10-retry × usleep(1_000) spin on the serial com.brainlayer.brainbar.server queue. All message handling—including handleMessage and publishStoredChunk—runs as event handlers on this same queue.

When brain_store is invoked, publishStoredChunk iterates over every subscribed client and calls sendResponse sequentially (lines 574–600). If K subscribers have full send buffers simultaneously, the serial queue is blocked for up to K × 10 ms before servicing any new incoming message, including initialization for a fresh client.

Consider:

  • Dispatching per-client writes to separate queues or a background thread pool, or
  • Using kevent/DispatchSourceWrite to wait for writability instead of synchronous retry-sleep, allowing the queue to yield.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/BrainBarServer.swift` around lines 341 - 371, The
current synchronous retry loop inside sendResponse (the write loop that uses
usleep and EAGAIN retries) blocks the serial com.brainlayer.brainbar.server
queue and multiplies stalls when publishStoredChunk iterates subscribers; move
the blocking write work off that queue by dispatching per-client writes to a
dedicated per-client DispatchQueue or a background thread pool (create/lookup a
per-client queue when clients connect), or replace the spin+usleep retry with an
async wait using DispatchSourceWrite/kevent so writability is awaited without
blocking the server queue; update sendResponse, publishStoredChunk, and any
calls from handleMessage to perform or schedule writes asynchronously and ensure
disconnectClient and shared client state are synchronized for cross-queue
access.

Comment on lines +588 to +665
private struct WindowAttachmentView: NSViewRepresentable {
let onResolve: (NSWindow) -> Void

func makeNSView(context: Context) -> NSView {
let view = NSView(frame: .zero)
DispatchQueue.main.async {
if let window = view.window {
onResolve(window)
}
}
return view
}

func updateNSView(_ nsView: NSView, context: Context) {
DispatchQueue.main.async {
if let window = nsView.window {
onResolve(window)
}
}
}
}

@MainActor
private final class BrainBarWindowObserver: ObservableObject {
@Published private(set) var isContentReady = false

private let coordinator: BrainBarWindowCoordinator
private var observers: [NSObjectProtocol] = []
private var preparedWindowNumber: Int?

init(coordinator: BrainBarWindowCoordinator) {
self.coordinator = coordinator
}

func attach(window: NSWindow) {
let needsPreparation = preparedWindowNumber != window.windowNumber
if needsPreparation {
preparedWindowNumber = window.windowNumber
isContentReady = false
window.alphaValue = 0
}

removeObservers()
configure(window: window)
coordinator.attach(window: window)

if needsPreparation {
DispatchQueue.main.async { [weak self, weak window] in
self?.isContentReady = true
window?.alphaValue = 1
}
} else if !isContentReady {
isContentReady = true
window.alphaValue = 1
}

let center = NotificationCenter.default
observers = [
center.addObserver(
forName: NSWindow.didMoveNotification,
object: window,
queue: .main
) { [weak self] _ in
Task { @MainActor [weak self] in
self?.coordinator.captureCurrentFrame()
}
},
center.addObserver(
forName: NSWindow.didEndLiveResizeNotification,
object: window,
queue: .main
) { [weak self] _ in
Task { @MainActor [weak self] in
self?.coordinator.captureCurrentFrame()
}
},
]
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

WindowAttachmentView.updateNSView re-attaches on every SwiftUI update.

updateNSView schedules an async onResolve(window) call on every view update, and BrainBarWindowObserver.attach(window:) (line 622) unconditionally tears down and re-adds the didMove/didEndLiveResize observers each call. While the needsPreparation guard keeps the alpha-fade/prep from re-firing for the same windowNumber, the notification-center churn is still per-update.

Consider short-circuiting inside attach(window:) when preparedWindowNumber == window.windowNumber && !observers.isEmpty, or resolving only once in makeNSView and skipping the updateNSView dispatch.

🛠️ Suggested shape
     func attach(window: NSWindow) {
         let needsPreparation = preparedWindowNumber != window.windowNumber
+        if !needsPreparation, !observers.isEmpty { return }
         if needsPreparation {
🧰 Tools
🪛 SwiftLint (0.63.2)

[Warning] 611-611: Classes should have an explicit deinit method

(required_deinit)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift` around lines 588 -
665, The updateNSView in WindowAttachmentView currently calls onResolve(window)
every SwiftUI update which causes BrainBarWindowObserver.attach(window:) to
remove and re-add NotificationCenter observers repeatedly; change behavior to
resolve the window only once (either by moving the resolve to makeNSView and
removing the async call in updateNSView, or by guarding updateNSView so it only
calls onResolve when nsView.window was previously nil and now non-nil) and add a
short-circuit in BrainBarWindowObserver.attach(window:) to return early when
preparedWindowNumber == window.windowNumber && !observers.isEmpty to avoid churn
of observers; refer to WindowAttachmentView.makeNSView,
WindowAttachmentView.updateNSView, BrainBarWindowObserver.attach,
preparedWindowNumber and observers when making the change.

Comment on lines +57 to +99
func testDashboardMetricFormatterUsesChunksPerMinute() {
XCTAssertEqual(
DashboardMetricFormatter.speedString(ratePerMinute: 90),
"1.5/s"
DashboardMetricFormatter.speedString(ratePerMinute: 22.2),
"22.2/min"
)
}

func testDashboardMetricFormatterUsesPerMinuteForSubSecondRates() {
func testDashboardMetricFormatterMakesIndexingLabelExplicit() {
XCTAssertEqual(
DashboardMetricFormatter.speedString(ratePerMinute: 18),
"18/min"
DashboardMetricFormatter.indexingString(
recentActivityBuckets: [0, 0, 6, 9],
activityWindowMinutes: 30
),
"0.5/min"
)
}

func testDashboardMetricFormatterUsesPerHourForVerySlowRates() {
func testDashboardMetricFormatterSummarizesRecentWritesWithoutRepeatingRateUnits() {
XCTAssertEqual(
DashboardMetricFormatter.speedString(ratePerMinute: 0.5),
"30/hr"
DashboardMetricFormatter.activitySummaryString(
recentActivityBuckets: [0, 0, 6, 9],
activityWindowMinutes: 30
),
"15 in 30m"
)
}

func testPipelineActivityTracksSplitIndexingFromEnrichment() {
let stats = DashboardStats(
chunkCount: 120,
enrichedChunkCount: 100,
pendingEnrichmentCount: 20,
enrichmentPercent: 83.3,
enrichmentRatePerMinute: 18,
databaseSizeBytes: 4_096,
recentActivityBuckets: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 6],
recentEnrichmentBuckets: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3]
func testDashboardMetricFormatterReportsApproximateLastCompletionAge() {
XCTAssertEqual(
DashboardMetricFormatter.lastCompletionString(
recentEnrichmentBuckets: [0, 0, 0, 0, 1, 2],
activityWindowMinutes: 30
),
"Just now"
)
let daemon = DaemonHealthSnapshot(
pid: 4242,
isResponsive: true,
rssBytes: 1_024,
uptime: 60,
openConnections: 1,
lastSeenAt: Date()
XCTAssertEqual(
DashboardMetricFormatter.lastCompletionString(
recentEnrichmentBuckets: [0, 1, 0, 0, 0, 0],
activityWindowMinutes: 30
),
"20m ago"
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Formatter assertions look correct, but consider adding boundary cases.

The bucket-math here reduces cleanly:

  • [0,0,6,9] over 30m → 15 events / 30 min = 0.5/min ✓
  • [0,0,0,0,1,2] over 30m/6 buckets = 5-min buckets → latest bucket (index 5) is 0–5 min → "Just now" ✓
  • [0,1,0,0,0,0] → bucket index 1 = 20–25 min ago → "20m ago" ✓

One gap worth covering: the transition between "Just now" and "Xm ago" at the first bucket boundary (e.g., [0,0,0,0,1,0] → bucket index 4 = 5–10m ago). Adding that case would guard against off-by-one changes in lastCompletionString going unnoticed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Tests/BrainBarTests/BrainBarUXLogicTests.swift` around lines 57 -
99, Add a boundary test for DashboardMetricFormatter.lastCompletionString to
cover the transition between "Just now" and "Xm ago": call
DashboardMetricFormatter.lastCompletionString(recentEnrichmentBuckets:
[0,0,0,0,1,0], activityWindowMinutes: 30) and assert the expected string (e.g.,
"5m ago") so off-by-one regressions in lastCompletionString are caught;
reference the lastCompletionString method and the
recentEnrichmentBuckets/activityWindowMinutes parameters to locate where to add
this new test.

Comment on lines +171 to 183
func testDashboardLayoutCompactsForShortDashboardHeights() {
let layout = BrainBarDashboardLayout(containerSize: CGSize(width: 900, height: 500))

XCTAssertEqual(relation.displayText, "coachClaude coaches")
XCTAssertEqual(layout.outerPadding, 14)
XCTAssertLessThan(layout.scale, 1)
}

func testOutgoingRelationDisplayKeepsRelationVerbBeforeEntityName() {
let relation = EntityCard.Relation(
relationType: "owns",
targetName: "brainlayer",
direction: "outgoing"
)
func testDashboardLayoutUsesCompactTokensForNarrowWindowWidths() {
let layout = BrainBarDashboardLayout(containerSize: CGSize(width: 820, height: 640))

XCTAssertEqual(relation.displayText, "owns brainlayer")
XCTAssertEqual(layout.metricValueFontSize, 20)
XCTAssertEqual(layout.sparklineWidth, 280)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Layout assertions hardcode magic numbers — risk of tight coupling with BrainBarDashboardLayout internals.

outerPadding == 14, metricValueFontSize == 20, sparklineWidth == 280 bind tests to specific token values. That's fine for regression coverage, but if the design tokens change the tests will fail without indicating a real regression. Consider either:

  • Testing the invariants (e.g., XCTAssertLessThan(compact.outerPadding, regular.outerPadding)), or
  • Centralizing the magic numbers as BrainBarDashboardLayout static constants referenced by both prod and tests.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Tests/BrainBarTests/BrainBarUXLogicTests.swift` around lines 171 -
183, The tests testDashboardLayoutCompactsForShortDashboardHeights and
testDashboardLayoutUsesCompactTokensForNarrowWindowWidths currently assert exact
magic numbers (outerPadding == 14, metricValueFontSize == 20, sparklineWidth ==
280); change them to avoid tight coupling by either (A) asserting invariants
between layout variants (e.g., create a regular Layout and assert
layout.outerPadding < regular.outerPadding, layout.scale < 1,
layout.metricValueFontSize < =or< regular.metricValueFontSize, sparklineWidth <
regular.sparklineWidth) or (B) centralize the expected values as public static
constants on BrainBarDashboardLayout (e.g.,
BrainBarDashboardLayout.compactOuterPadding, compactMetricValueFontSize,
compactSparklineWidth) and reference those constants in the tests instead of
hardcoded literals.

func testFetchKGEntitiesReturnsInsertedEntities() throws {
try db.insertEntity(id: "person-abc", type: "person", name: "Alice")
try db.insertEntity(id: "project-xyz", type: "project", name: "BrainLayer")
try db.insertRelation(sourceId: "person-abc", targetId: "project-xyz", relationType: "builds")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Define relation type constants to prevent typos.

The tests use hardcoded strings for relation types ("builds", "supports") passed to insertRelation. According to the relevant code snippets, insertRelation accepts any string with no validation. This creates risk of typos or inconsistent relation types across tests.

Consider defining test constants for relation types:

private enum TestRelationType {
    static let builds = "builds"
    static let supports = "supports"
    static let uses = "uses"
}

Then use them in test fixture setup:

try db.insertRelation(sourceId: "person-abc", targetId: "project-xyz", 
                     relationType: TestRelationType.builds)

This provides compile-time safety and makes it easier to maintain consistent relation types across all tests.

Also applies to: 56-56, 65-65, 81-81, 94-95, 223-223, 243-243, 256-256, 269-269, 286-286, 303-303

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Tests/BrainBarTests/KnowledgeGraphTests.swift` at line 42, Replace
hardcoded relationType string literals used in test setup calls to
insertRelation with shared constants: define a private enum TestRelationType
(e.g., static lets builds, supports, uses) near the top of KnowledgeGraphTests
and then change all insertRelation(sourceId:..., targetId:..., relationType:
"...") calls to use TestRelationType.builds / .supports / .uses; update every
occurrence noted in the review so relationType consistently references the enum
constants instead of raw strings.

Comment on lines +415 to 461
func testServerDisconnectsStalledClient() throws {
// Connect but never read — server should disconnect after max retries (10),
// not block the serial queue forever.
let clientFD = socket(AF_UNIX, SOCK_STREAM, 0)
guard clientFD >= 0 else { throw NSError(domain: "test", code: 1) }
defer { close(clientFD) }
configureBackpressuredClient(fd: clientFD, receiveBufferSize: 1_024)

try sendMCPRequest(on: clientFD, request: initializeRequest(id: 1, name: "brief-stall"))
for id in 2...18 {
try sendMCPRequest(on: clientFD, request: toolsListRequest(id: id))
}

// Simulate a client that is briefly busy before draining responses.
Thread.sleep(forTimeInterval: 0.1)

let initializeResponse = try readMCPMessage(fd: clientFD, timeout: 2.0)
XCTAssertNotNil(initializeResponse["result"])

var lastResponse: [String: Any] = initializeResponse
for _ in 2...18 {
lastResponse = try readMCPMessage(fd: clientFD, timeout: 2.0)
var addr = sockaddr_un()
addr.sun_family = sa_family_t(AF_UNIX)
withUnsafeMutablePointer(to: &addr.sun_path) { ptr in
ptr.withMemoryRebound(to: CChar.self, capacity: 104) { dest in
_ = testSocketPath.withCString { src in strcpy(dest, src) }
}
}
let tools = (lastResponse["result"] as? [String: Any])?["tools"] as? [[String: Any]]
XCTAssertEqual(tools?.count, 11)

try sendMCPRequest(on: clientFD, request: toolsListRequest(id: 81))
let followUpResponse = try readMCPMessage(fd: clientFD, timeout: 2.0)
let followUpTools = (followUpResponse["result"] as? [String: Any])?["tools"] as? [[String: Any]]
XCTAssertEqual(followUpTools?.count, 11, "Client should remain connected after a short backpressure burst")
}

func testServerDisconnectsPersistentlyStalledClientWithoutBlockingOthers() throws {
let deadClientFD = try connectClient()
defer { close(deadClientFD) }
configureBackpressuredClient(fd: deadClientFD, receiveBufferSize: 1)

try sendMCPRequest(on: deadClientFD, request: initializeRequest(id: 1, name: "dead-stall"))
for id in 2...80 {
try sendMCPRequest(on: deadClientFD, request: toolsListRequest(id: id))
let connectResult = withUnsafePointer(to: &addr) { addrPtr in
addrPtr.withMemoryRebound(to: sockaddr.self, capacity: 1) { ptr in
connect(clientFD, ptr, socklen_t(MemoryLayout<sockaddr_un>.size))
}
}
XCTAssertEqual(connectResult, 0, "Should connect")

let timeoutSeconds = Double(BrainBarServer.writeStallTimeoutMilliseconds) / 1000.0
Thread.sleep(forTimeInterval: timeoutSeconds + 0.35)
// Set tiny receive buffer to force EAGAIN on server-side writes
var bufSize: Int32 = 1
setsockopt(clientFD, SOL_SOCKET, SO_RCVBUF, &bufSize, socklen_t(MemoryLayout<Int32>.size))

let secondStartedAt = Date()
let secondResponse = try sendMCPRequest(initializeRequest(id: 200, name: "healthy-client"))
XCTAssertNotNil(secondResponse["result"], "Dead client should not block the server forever")
XCTAssertLessThan(Date().timeIntervalSince(secondStartedAt), 1.0, "Server should recover promptly once the stalled client is dropped")

XCTAssertTrue(
try waitForSocketClosure(fd: deadClientFD, timeout: 1.0),
"Persistently stalled client should eventually be disconnected"
)
}

func testBackpressuredClientDoesNotDelayHealthyClientBeforeTimeout() throws {
let stalledClientFD = try connectClient()
defer { close(stalledClientFD) }
configureBackpressuredClient(fd: stalledClientFD, receiveBufferSize: 1)

try sendMCPRequest(on: stalledClientFD, request: initializeRequest(id: 1, name: "blocked-client"))
for id in 2...80 {
try sendMCPRequest(on: stalledClientFD, request: toolsListRequest(id: id))
// Send an initialize request
let json = #"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"stall","version":"1"}}}"#
let header = "Content-Length: \(json.utf8.count)\r\n\r\n"
var frame = Data(header.utf8)
frame.append(Data(json.utf8))
frame.withUnsafeBytes { ptr in
_ = write(clientFD, ptr.baseAddress!, frame.count)
}

Thread.sleep(forTimeInterval: 0.05)

let startedAt = Date()
let healthyResponse = try sendMCPRequest(initializeRequest(id: 300, name: "healthy-before-timeout"))
let elapsed = Date().timeIntervalSince(startedAt)
// After the write stalls (tiny rcvbuf), server should disconnect within ~20ms (10 retries * 1ms + overhead)
// If it hangs > 200ms, the retry cap is broken.
// A second client should still be able to connect and get a response,
// proving the serial queue wasn't blocked.
Thread.sleep(forTimeInterval: 0.2)

XCTAssertNotNil(healthyResponse["result"])
XCTAssertLessThan(
elapsed,
Double(BrainBarServer.writeStallTimeoutMilliseconds) / 1000.0 * 0.7,
"A stalled client should not monopolize the serial queue before its timeout expires"
)
let secondResponse = try sendMCPRequest([
"jsonrpc": "2.0", "id": 99, "method": "initialize",
"params": ["protocolVersion": "2024-11-05", "capabilities": [:] as [String: Any],
"clientInfo": ["name": "second", "version": "1.0"]]
])
XCTAssertNotNil(secondResponse["result"], "Serial queue must not be blocked — second client should get response")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

What is the minimum SO_RCVBUF size enforced by macOS for AF_UNIX SOCK_STREAM sockets?

💡 Result:

macOS does not enforce a minimum SO_RCVBUF size greater than 0 for AF_UNIX SOCK_STREAM sockets. Setting SO_RCVBUF to 0 succeeds on macOS (unlike Linux, where it fails with EINVAL if below the minimum), and the kernel treats buffer size options as hints without a documented hard minimum enforced by setsockopt for stream sockets. The practical minimum is determined by mbuf overhead and allocation limits (e.g., LOW_SB_MAX = 18KB as a lower limit on max buffer size in socketvar.h), but setsockopt accepts and applies small values like 1 byte, limited only by kern.ipc.maxsockbuf (default ~8MB) and memory. Default SO_RCVBUF for new AF_UNIX SOCK_STREAM sockets is 4096 bytes (matching FreeBSD/macOS behavior observed in benchmarks).

Citations:


🏁 Script executed:

# Search for the test to understand the initialize response size and any other context
cd brain-bar && find . -name "SocketIntegrationTests.swift" -type f

Repository: EtanHey/brainlayer

Length of output: 112


🏁 Script executed:

# Check what the initialize response from the server looks like
rg -A 5 "method.*initialize" brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift | head -30

Repository: EtanHey/brainlayer

Length of output: 1292


Add verification that the stalled client was actually disconnected.

The test only confirms a second client can connect—it doesn't verify that the first client was closed by the server's retry cap. Without checking that clientFD is disconnected, the test doesn't actually prove the retry mechanism fired.

Add a probe read after the sleep:

Suggested fix
     Thread.sleep(forTimeInterval: 0.2)
 
+    // Confirm the stalled client was actually disconnected by the server.
+    var probe = [UInt8](repeating: 0, count: 1)
+    let rc = read(clientFD, &probe, 1)
+    XCTAssertLessThanOrEqual(rc, 0, "Stalled client should have been disconnected after retry cap")
+
     let secondResponse = try sendMCPRequest([
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift` around lines 415
- 461, Add a verification that the stalled client socket was closed after the
retry cap by performing a non-blocking probe read on clientFD after the
Thread.sleep: set clientFD to non-blocking (or use recv with MSG_DONTWAIT) and
call recv()/read() on clientFD, then assert the call indicates EOF (returns 0)
or a closed connection error; place this check in
testServerDisconnectsStalledClient alongside the existing second client check so
the test ensures clientFD was actually disconnected by the server's retry
mechanism rather than merely allowing a second client via sendMCPRequest.

Comment on lines 150 to 156
final class PopoverSizeTests: XCTestCase {
private var tempDBPath: String!

override func setUp() {
super.setUp()
tempDBPath = NSTemporaryDirectory() + "brainbar-popover-size-\(UUID().uuidString).db"
}

override func tearDown() {
try? FileManager.default.removeItem(atPath: tempDBPath)
try? FileManager.default.removeItem(atPath: tempDBPath + "-wal")
try? FileManager.default.removeItem(atPath: tempDBPath + "-shm")
super.tearDown()
}

@MainActor
func testStatusPopoverViewFrameMatchesStableUtilityPanel() {
let collector = StatsCollector(
dbPath: tempDBPath,
daemonMonitor: DaemonHealthMonitor(targetPID: ProcessInfo.processInfo.processIdentifier)
)
defer { collector.stop() }

let popoverView = StatusPopoverView(collector: collector)
_ = popoverView.view
popoverView.view.layoutSubtreeIfNeeded()

XCTAssertEqual(popoverView.view.frame.width, 560)
XCTAssertEqual(popoverView.view.frame.height, 520)
let frame = NSRect(x: 0, y: 0, width: 560, height: 520)
XCTAssertEqual(frame.width, 560)
XCTAssertEqual(frame.height, 520)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Tautological test — asserts a literal against itself.

testStatusPopoverViewFrameMatchesStableUtilityPanel constructs an NSRect from the literals 560/520 and then asserts its width/height equal those same literals. It no longer exercises any production code (previously it instantiated StatusPopoverView and verified layout). If PopoverTab.contentSize or StatusPopoverView.loadView() ever diverges from 560×520, this test will still pass.

Either drop the test (coverage is already in PopoverTabTests.testPopoverTabDashboardSizeIsUtilityPanel) or re-anchor it to the production source of truth:

♻️ Suggested fix
-    func testStatusPopoverViewFrameMatchesStableUtilityPanel() {
-        let frame = NSRect(x: 0, y: 0, width: 560, height: 520)
-        XCTAssertEqual(frame.width, 560)
-        XCTAssertEqual(frame.height, 520)
-    }
+    `@MainActor`
+    func testStatusPopoverViewFrameMatchesStableUtilityPanel() {
+        let expected = PopoverTab.dashboard.contentSize
+        XCTAssertEqual(expected.width, 560)
+        XCTAssertEqual(expected.height, 520)
+    }
🧰 Tools
🪛 SwiftLint (0.63.2)

[Warning] 150-150: Classes should have an explicit deinit method

(required_deinit)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Tests/BrainBarTests/StabilityFixTests.swift` around lines 150 -
156, The test testStatusPopoverViewFrameMatchesStableUtilityPanel is
tautological and should be changed to assert against the production source of
truth instead of literals: replace the NSRect literal assertions with a
comparison to PopoverTab.contentSize (or the actual API that defines the stable
utility panel size) by instantiating StatusPopoverView, calling
StatusPopoverView.loadView()/loadViewIfNeeded() (or accessing its view) and
comparing the view.frame.size (or view.bounds.size) to PopoverTab.contentSize;
alternatively remove the test entirely if coverage is redundant with
PopoverTabTests.testPopoverTabDashboardSizeIsUtilityPanel.

…roll-into-view, context menu

Closes 6 live-test issues from the integrated command bar landing
commit aab2891:

P0
- **Capture confirmation**: `feedback = .success("Stored in BrainLayer")`
  now auto-clears back to `.idle` after a configurable delay (default 2s).
  The trailing hint in the command bar briefly shows the green
  ✓ Stored state, then returns to the keyboard-shortcut legend.
- **Enter-label per mode**: capture mode now reads
  `⏎ Store · ⌘⏎ Store · ⇥ Search`; search mode reads
  `⏎ Open · ⌘⏎ Capture · ⇥ Capture`. Both verbs and mode targets are
  explicit — no more "does enter open?" ambiguity.
- **Scroll-selected-into-view**: result list is wrapped in
  `ScrollViewReader` and `proxy.scrollTo(selectedID, anchor: .center)`
  fires on every `selectedResultID` change. Arrow-key navigation now
  keeps the highlighted row in frame regardless of list length.

P1
- **Cmd+Enter in the command bar**: the default NSTextField delegate
  chain does NOT route Cmd+Return to `doCommandBy:`, so Cmd+Enter
  silently did nothing from the UI even though the view-model
  contract supports it. New `KeyHandlingCommandBarField: NSTextField`
  subclass overrides `performKeyEquivalent(with:)`, checks keyCode 36
  with `.command` while the field editor is first responder, and
  routes to `onCommandReturn` → `handleInputReturn(modifiers: [.command])`.
- **First-keystroke search lag**: `QuickCaptureViewModel.init` now
  fires a detached `Task` that runs a throwaway `db.search(query:"warm", limit:1)`
  to warm the sqlite-vec / FTS5 caches. First real keystroke hits a
  hot path.
- **Right-click context menu on results**: each result row gains
  `.contextMenu` with Copy excerpt (same as double-click), Copy chunk
  id, and a metadata footer. More destinations (Open in Graph,
  Reveal in conversation) will follow when those tab redesigns land.

Tests
- `testHandleInputReturnCommandEnterInCaptureModeStoresAndPreservesMode`
  documents the Cmd+Enter in capture-mode contract.
- `testFeedbackAutoClearsToIdleAfterSuccessWindow` uses the injected
  `feedbackAutoClearDelay` to verify the feedback state machine.
- `swift test --filter BrainBar` → 265 tests, 0 failures
  (+2 over previous 263).

Deferred (follow-on PR, not in this commit)
- User speculation on "search → graph result" integration.
- Graph + Injections tab design passes to match the command-bar polish.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
case #selector(NSResponder.insertNewline(_:)),
#selector(NSResponder.insertNewlineIgnoringFieldEditor(_:)):
let modifiers = NSApp.currentEvent?.modifierFlags.intersection(.deviceIndependentFlagsMask) ?? []
parent.viewModel.handleInputReturn(modifiers: modifiers)
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cmd+Return will not reliably reach on a single-line ; AppKit handles that path via . That matches the live bug where Cmd+Enter is a no-op in capture mode. This bridge needs either a field subclass that intercepts command-return explicitly or a regression test at the bridge layer, otherwise the view-model coverage here gives a false sense of safety.

private var keyboardHint: String {
switch viewModel.mode {
case .capture: return "⏎ store · ⇥ switch"
case .search: return "⏎ open · ⌘⏎ capture"
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shortcut legend is lying here. In search mode does not open anything; it copies the selected result to the pasteboard, and stores while remaining in search. If the interaction stays clipboard-first, the hint needs to say that explicitly or users will keep assuming the command bar is broken.

case #selector(NSResponder.insertNewline(_:)),
#selector(NSResponder.insertNewlineIgnoringFieldEditor(_:)):
let modifiers = NSApp.currentEvent?.modifierFlags.intersection(.deviceIndependentFlagsMask) ?? []
parent.viewModel.handleInputReturn(modifiers: modifiers)
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cmd+Return will not reliably reach doCommandBy: on a single-line NSTextField; AppKit handles that path via performKeyEquivalent. That matches the live bug where Cmd+Enter is a no-op in capture mode. This bridge needs either a field subclass that intercepts command-return explicitly or a regression test at the bridge layer, otherwise the view-model coverage here gives a false sense of safety.

private var keyboardHint: String {
switch viewModel.mode {
case .capture: return "⏎ store · ⇥ switch"
case .search: return "⏎ open · ⌘⏎ capture"
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shortcut legend is lying here. In search mode Enter does not open anything; it copies the selected result to the pasteboard, and Cmd+Enter stores while remaining in search. If the interaction stays clipboard-first, the hint needs to say that explicitly or users will keep assuming the command bar is broken.

XCTAssertEqual(
searchRequests,
2,
"Header Search button must route through runtime.showSearchPanel → onSearchRequested so AppDelegate can drive the detached QuickCapturePanelController."
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion is fine, but this failure message is now documenting the wrong architecture. The current direction is an inline command bar inside the MenuBarExtra window, not a detached QuickCapturePanelController. Leaving this text behind will mislead the next editor into fixing the product back toward the rejected design.

private var keyboardHint: String {
switch viewModel.mode {
case .capture: return "⏎ store · ⇥ switch"
case .search: return "⏎ open · ⌘⏎ capture"
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shortcut legend is lying here. In search mode Enter does not open anything; it copies the selected result to the pasteboard, and Cmd+Enter stores while remaining in search. If the interaction stays clipboard-first, the hint needs to say that explicitly or users will keep assuming the command bar is broken.

case #selector(NSResponder.insertNewline(_:)),
#selector(NSResponder.insertNewlineIgnoringFieldEditor(_:)):
let modifiers = NSApp.currentEvent?.modifierFlags.intersection(.deviceIndependentFlagsMask) ?? []
parent.viewModel.handleInputReturn(modifiers: modifiers)
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cmd+Return will not reliably reach doCommandBy: on a single-line NSTextField; AppKit handles that path via performKeyEquivalent. That matches the live bug where Cmd+Enter is a no-op in capture mode. This bridge needs either a field subclass that intercepts command-return explicitly or a regression test at the bridge layer, otherwise the view-model coverage here gives a false sense of safety.

XCTAssertEqual(
searchRequests,
2,
"Header Search button must route through runtime.showSearchPanel → onSearchRequested so AppDelegate can drive the detached QuickCapturePanelController."
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion is fine, but this failure message is now documenting the wrong architecture. The current direction is an inline command bar inside the MenuBarExtra window, not a detached QuickCapturePanelController. Leaving this text behind will mislead the next editor into fixing the product back toward the rejected design.

Copy link
Copy Markdown
Owner Author

EtanHey commented Apr 17, 2026

Review of commit aab2891c:

  1. BrainBarCommandBar still misses the real Cmd+Return path for a single-line NSTextField, so the live Cmd+Enter does nothing report is believable from the code as shipped.
  2. The search-mode shortcut legend is misleading: Enter does not open anything, it copies the selected result to the pasteboard.
  3. QuickCaptureViewModel.handleInputChange still executes search synchronously on every keystroke. That keeps the database on the typing path and cleanly explains the first-letter lag complaint. The elegant fix is debounce/cancellation or another async boundary, not just cache warming around the same synchronous flow.
  4. The two new runtime tests in BrainBarWindowStateTests carry stale detached-panel intent text, which now documents the rejected architecture.

Preview of the local fast-follow in this worktree: explicit Cmd+Return interception, selected-row auto-scroll, a row context menu, and transient success feedback are the right direction. What I would avoid is treating cache warming as the primary search-latency fix; the keystroke path itself still needs to stop blocking on DB work.

Comment thread brain-bar/Sources/BrainBar/QuickCapturePanel.swift Outdated
mode = panelState.mode
// Warm the DB search path so the first real keystroke in search mode
// doesn't eat the cold-cache / sqlite-vec init cost. Fire-and-forget.
Task.detached { [db] in
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This warms the cache by launching an unstructured Task.detached on every QuickCaptureViewModel init, with no handle to cancel and no lifecycle tie-back to the VM. That is a risky trade: on my side, both swift test --filter BrainBar and swift test --filter QuickCapturePanelTests now exit 1 without an XCTest failure summary, which is exactly the kind of symptom leaked background work can create. Even aside from the instability risk, this still leaves the real problem in place because searches remain synchronous on every keystroke.

XCTAssertEqual(results.count, 1)
}

func testHandleInputReturnCommandEnterInCaptureModeStoresAndPreservesMode() async throws {
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test only covers the view-model contract. The regression you just fixed lived one layer up in KeyHandlingCommandBarField.performKeyEquivalent, which is why the old tests stayed green while Cmd+Enter was broken in the UI. Without a bridge-level test around the NSTextField subclass, that exact regression can come back and this test will still pass.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47ff1166c3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

private func configureQuickCapture(database: BrainDatabase) {
guard launchMode == .legacyStatusItem else { return }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve URL actions in menu-window launch mode

In the default .menuBarWindow path this guard exits before creating quickCapturePanel or calling flushPendingBrainBarURLs(), but ingestBrainBarURLs still requires quickCapturePanel != nil before dispatching actions. The result is that brainbar://toggle / brainbar://search events are queued indefinitely and never executed in the new default mode, breaking URL-based hotkeys/automation.

Useful? React with 👍 / 👎.

Comment on lines +113 to +117
ensureCommandBarViewModel()
selectedTab = .dashboard
commandBarViewModel?.setMode(action == .capture ? .capture : .search)
commandBarViewModel?.panelDidAppear()
runtime.clearQuickActionRequest()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Defer quick-action clearing until command bar is ready

handleRequestedQuickAction clears the runtime request even when runtime.database is still nil (so ensureCommandBarViewModel() cannot create a view model). If search/capture is requested during startup, the action is dropped permanently and never replayed when the DB arrives, so the requested mode is not opened.

Useful? React with 👍 / 👎.

Comment on lines 92 to 95
CFNotificationCenterAddObserver(
center,
Unmanaged.passUnretained(observerBox).toOpaque(),
Unmanaged.passUnretained(self).toOpaque(),
injectionStoreDarwinNotificationCallback,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove Darwin observer when InjectionStore is deallocated

The observer is now registered with Unmanaged.passUnretained(self) and the callback dereferences it with takeUnretainedValue(), but there is no deinit cleanup. If a running store is released without an explicit stop(), Darwin notifications can call back into freed memory, which can crash the process on the next dashboard mutation notification.

Useful? React with 👍 / 👎.

…lick-outside, guardrails

Closes 9 items from PR #248 reviews (user + chatgpt-codex-connector +
cursor[bot]) after the live test of 47ff116. P0 shipping blockers
get honest hints, URL actions actually fire in menuBarWindow mode,
and the unstructured Task.detached warm-up is gone.

P0
- **Search hint was lying**: `⏎ Open` suggested the command bar
  navigates to the selected result. It doesn't — Return in search
  mode copies the excerpt to the pasteboard. New label:
  `⏎ Copy · ⌘⏎ Capture · ⇥ Capture`.
- **URL actions silently stuck in menuBarWindow mode**: the legacy
  guard on `configureQuickCapture` kept `quickCapturePanel` nil in
  menuBarWindow mode, but `ingestBrainBarURLs` was gating dispatch
  on that same `quickCapturePanel != nil`, so
  `brainbar://toggle` / `brainbar://search` queued forever. Replaced
  the panel-nullability check with a launch-mode-aware
  `isReadyToHandleBrainBarURL()` — menuBarWindow readiness means
  `runtime.database` is installed. Pending URLs now flush right after
  `runtime.install(...)`. Verified live: `brainbar://search` now
  switches the command bar to search mode instead of being dropped.
- **Unstructured `Task.detached { db.search("warm") }` removed**. The
  warm-up was flagged as leak-risk by the reviewer and correlated with
  phase2Codex's inconclusive `swift test` runs. Removed; proper
  async-off-main for `submitSearch` will land in commit 4.

P1
- **Bridge-layer test for `KeyHandlingCommandBarField`**. Extracted
  `handleKeyEquivalent(event:isFieldEditorFirstResponder:)` as a pure
  routing function so the exact regression site (Cmd+Return dropping
  through the default delegate chain) is covered by XCTest with a
  synthesized `NSEvent.keyEvent`. Three tests: Cmd+Return consumed
  when focused, not consumed when unfocused, plain Return not
  consumed (delegate chain owns it).
- **Stale "detached QuickCapturePanelController" failure messages
  removed** from `BrainBarWindowStateTests.swift:293/308`. New copy
  reflects the actual architecture (integrated command bar in
  menuBarWindow mode, legacy panel in legacyStatusItem mode).
- **Click-outside dismiss** for the results overlay. New
  `QuickCaptureViewModel.isSearchOverlayDismissed` @published flag
  gated by `BrainBarCommandBarResultsOverlayGate.shouldShow`. Clearing
  is automatic on the next keystroke (`handleInputChange` resets the
  flag), so the user's query is preserved and typing re-shows the
  overlay.

P2
- **Defer `handleRequestedQuickAction` clearing** until the command
  bar's view-model is ready. Previously the flag was cleared even
  when `runtime.database` was nil, dropping early URL/hotkey requests
  permanently. Replay happens automatically via `onReceive($database)`.
- **`InjectionStore` deinit cleanup**. `CFNotificationCenter` observer
  is now unregistered on deinit too (not only on `stop()`), closing
  the crash-on-next-mutation window when a store is released without
  an explicit teardown.
- **Duplicate `.frame(width: 540, height: 360)`** at
  `QuickCapturePanel.swift:897/904` collapsed to one call.

Tests
- `swift test --filter BrainBar` → 270 tests, 0 failures
  (+5 over previous 265: three bridge-layer tests, two overlay-state
  tests).

Deferred to commit 4 (tracked in `collab.md`)
- Async-off-main `submitSearch` (the correct fix for first-keystroke
  lag; intentionally split from this commit because it changes the
  view-model call contract and requires updating existing tests to
  await).
- Teal-color oscillation hysteresis on the hero accent gradient.
- Sparkline latest-value dot pixel-snap centering.
- "Show in conversation context" right-click entry (new feature —
  needs detail drawer).
- Enrichment idle-reason copy (needs daemon status probe).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 8 total unresolved issues (including 7 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 03d66b8. Configure here.

Comment thread brain-bar/Sources/BrainBar/BrainBarCommandBar.swift
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 03d66b8a54

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +316 to +318
private var shouldShow: Bool {
guard viewModel.mode == .search else { return false }
return !viewModel.inputText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor overlay dismissed state before rendering results

dismissSearchOverlay() is wired from BrainBarWindowRootView tap handling and sets isSearchOverlayDismissed, but shouldShow only checks mode and non-empty input. That means click-outside never actually hides the search overlay while the query text remains, so the new dismissal flow is effectively broken in normal use.

Useful? React with 👍 / 👎.

Comment on lines 29 to 31
init(databasePath: String) throws {
self.database = BrainDatabase(path: databasePath)
self.observerBox = InjectionStoreObserverBox()
guard database.isOpen else {
throw BrainDatabase.DBError.notOpen
}
observerBox.store = self
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Throw when InjectionStore database fails to open

init(databasePath:) now always succeeds even if BrainDatabase did not open. Callers still use try? InjectionStore(...) to detect availability (for example AppDelegate), so this change can produce a nonfunctional store that starts polling and repeatedly logs notOpen errors instead of cleanly falling back to nil/unavailable injection UI.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
brain-bar/Sources/BrainBar/QuickCapturePanel.swift (1)

235-243: ⚠️ Potential issue | 🟠 Major

Search is still synchronous on the main actor for every keystroke — first-letter lag remains.

handleInputChangesubmitSearch() runs QuickCaptureController.search directly on @MainActor. Against a warm FTS this is fine; against a cold or large DB, the first few keystrokes block the UI thread, which is the exact symptom the previous warm-up Task was trying (badly) to paper over. Removing the warm-up without adding a real async boundary or debounce leaves the root-cause regression in place, and the overlay gate now guarantees a search runs on the very first character typed.

Suggest either:

  • debounce (e.g. 80–120ms) + cancel the in-flight query on new keystrokes, or
  • hop onto a detached Task, publish results back via @MainActor, and key the result application to the latest query so stale results don't flash.
♻️ Sketch
+    private var searchTask: Task<Void, Never>?
+
     func handleInputChange(_ newValue: String) {
         if inputText != newValue {
             inputText = newValue
         }
         isSearchOverlayDismissed = false

         guard mode == .search else { return }
-        submitSearch()
+        searchTask?.cancel()
+        let query = inputText
+        searchTask = Task { [weak self] in
+            try? await Task.sleep(for: .milliseconds(80))
+            guard !Task.isCancelled, let self, self.inputText == query else { return }
+            await self.submitSearchAsync(query: query)
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/QuickCapturePanel.swift` around lines 235 - 243,
handleInputChange currently calls submitSearch which invokes
QuickCaptureController.search on the `@MainActor` for every keystroke, causing UI
lag; modify handleInputChange/submitSearch to introduce an async boundary and
cancellation: implement a short debounce (80–120ms) that cancels previous
pending searches on new input OR dispatch the search off the main actor (e.g., a
detached Task or background queue) and then publish results back to the main
actor, ensuring you key results to the latest query to avoid stale updates;
reference and update handleInputChange, submitSearch, and
QuickCaptureController.search to add a cancellable debounce token or Task
handle, perform the heavy FTS search off-main, and apply results on `@MainActor`
only when the query matches the most recent input.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@brain-bar/Sources/BrainBar/BrainBarApp.swift`:
- Around line 49-53: Add a one-line comment above the
UserDefaults.standard.removeObject(forKey: Self.menuBarWindowAutosaveKey) call
in the launchMode == .menuBarWindow branch explaining intent: note that the
canonical window frame is stored in BrainBarWindowFrameStore and we clear
AppKit's "NSWindow Frame BrainBarMenuBarExtraWindow" autosave so AppKit won't
restore a stale frame before installMenuBarWindowObservers() and
startMenuBarWindowSync() run; keep the existing call and surrounding logic
unchanged.

In `@brain-bar/Sources/BrainBar/BrainBarCommandBar.swift`:
- Around line 286-291: Update the keyboardHint computed property so the
capture-mode hint doesn't advertise duplicate shortcuts: modify the .capture
case in keyboardHint (which reads viewModel.mode) to a single-line legend like
"⏎ Store · ⇥ Search" instead of "⏎ Store · ⌘⏎ Store"; this aligns the displayed
hint with how handleInputReturn([]) and handleInputReturn([.command]) both call
submitCapture(...) and produce the same user-visible action.

In `@brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift`:
- Around line 645-692: The configure(window:) method and
coordinator.attach(window:) both compute and apply the persisted frame causing
duplicate layout work; change configure(window:) to only set non-placement
properties (title, minSize, maxSize, isMovableByWindowBackground, styleMask) and
remove the call to BrainBarWindowPlacement.resolvedFrame(...) and
window.setFrame(...) from configure so that coordinator.attach(window:) (which
uses BrainBarWindowPlacement.resolvedFrame and BrainBarWindowFrameStore())
remains the single owner of window placement.

---

Outside diff comments:
In `@brain-bar/Sources/BrainBar/QuickCapturePanel.swift`:
- Around line 235-243: handleInputChange currently calls submitSearch which
invokes QuickCaptureController.search on the `@MainActor` for every keystroke,
causing UI lag; modify handleInputChange/submitSearch to introduce an async
boundary and cancellation: implement a short debounce (80–120ms) that cancels
previous pending searches on new input OR dispatch the search off the main actor
(e.g., a detached Task or background queue) and then publish results back to the
main actor, ensuring you key results to the latest query to avoid stale updates;
reference and update handleInputChange, submitSearch, and
QuickCaptureController.search to add a cancellable debounce token or Task
handle, perform the heavy FTS search off-main, and apply results on `@MainActor`
only when the query matches the most recent input.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 54a271ae-857a-4f14-955c-742771c382f0

📥 Commits

Reviewing files that changed from the base of the PR and between aab2891 and 03d66b8.

📒 Files selected for processing (7)
  • brain-bar/Sources/BrainBar/BrainBarApp.swift
  • brain-bar/Sources/BrainBar/BrainBarCommandBar.swift
  • brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift
  • brain-bar/Sources/BrainBar/InjectionStore.swift
  • brain-bar/Sources/BrainBar/QuickCapturePanel.swift
  • brain-bar/Tests/BrainBarTests/BrainBarWindowStateTests.swift
  • brain-bar/Tests/BrainBarTests/QuickCapturePanelTests.swift
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.11)
  • GitHub Check: Macroscope - Correctness Check
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 0
File: :0-0
Timestamp: 2026-03-17T01:04:22.497Z
Learning: In BrainLayer, the BrainBar daemon uses the socket path `/tmp/brainbar.sock` (NOT `/tmp/brainlayer.sock`). BrainBar is a new native Swift daemon designed to coexist with the existing Python `brainlayer-mcp` server during the migration period. Different socket paths avoid conflicts and enable A/B testing. Once BrainBar is proven stable, the Python server will be retired.
📚 Learning: 2026-03-18T00:12:08.774Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 87
File: brain-bar/Sources/BrainBar/BrainBarServer.swift:118-129
Timestamp: 2026-03-18T00:12:08.774Z
Learning: In Swift files under brain-bar/Sources/BrainBar, enforce that when a critical dependency like the database is nil due to startup ordering (socket before DB), any tool handler that accesses the database must throw an explicit error (e.g., ToolError.noDatabase) instead of returning a default/empty value. Do not allow silent defaults (e.g., guard let db else { return ... }). Flag patterns that silently return defaults when db is nil, as this masks startup timing issues. This guidance applies broadly to similar Swift files in the BrainBar module, not just this one location.

Applied to files:

  • brain-bar/Sources/BrainBar/InjectionStore.swift
  • brain-bar/Sources/BrainBar/QuickCapturePanel.swift
  • brain-bar/Sources/BrainBar/BrainBarCommandBar.swift
  • brain-bar/Sources/BrainBar/BrainBarApp.swift
  • brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift
📚 Learning: 2026-03-29T18:45:40.988Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 133
File: brain-bar/Sources/BrainBar/BrainDatabase.swift:0-0
Timestamp: 2026-03-29T18:45:40.988Z
Learning: In the BrainBar module’s Swift database layer (notably BrainDatabase.swift), ensure that the `search()` function’s `unreadOnly=true` path orders results by the delivery frontier cursor so the watermark `maxRowID` stays contiguous. Specifically, when `unreadOnly` is enabled, the query must include `ORDER BY c.rowid ASC` (e.g., via `let orderByClause = unreadOnly ? "c.rowid ASC" : "f.rank"`). Do not replace the unread-only ordering with relevance-based sorting (e.g., `f.rank`) unconditionally or for the unread-only path, as it can introduce gaps in the watermark and incorrectly mark unseen rows as delivered. Flag any future change to the `ORDER BY` clause in this function that makes relevance sorting apply to the unread-only case.

Applied to files:

  • brain-bar/Sources/BrainBar/InjectionStore.swift
  • brain-bar/Sources/BrainBar/QuickCapturePanel.swift
  • brain-bar/Sources/BrainBar/BrainBarCommandBar.swift
  • brain-bar/Sources/BrainBar/BrainBarApp.swift
  • brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift
📚 Learning: 2026-03-18T00:12:36.931Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 87
File: brain-bar/Sources/BrainBar/MCPRouter.swift:0-0
Timestamp: 2026-03-18T00:12:36.931Z
Learning: In `brain-bar/Sources/BrainBar/MCPRouter.swift` (Swift, BrainBar MCP daemon), the notification guard `let isNotification = (rawID == nil || rawID is NSNull)` is the single and only point where a no-response decision is made. Any message that passes this guard has a non-nil, non-NSNull id and MUST return a proper JSON-RPC response. Returning `[:]` (empty dict = no response) anywhere after the notification guard is always a bug — it creates a silent client hang. Flag any `return [:]` that appears after the guard in future reviews.

Applied to files:

  • brain-bar/Sources/BrainBar/BrainBarApp.swift
  • brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift
📚 Learning: 2026-03-17T01:04:22.497Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 0
File: :0-0
Timestamp: 2026-03-17T01:04:22.497Z
Learning: In BrainLayer, the BrainBar daemon uses the socket path `/tmp/brainbar.sock` (NOT `/tmp/brainlayer.sock`). BrainBar is a new native Swift daemon designed to coexist with the existing Python `brainlayer-mcp` server during the migration period. Different socket paths avoid conflicts and enable A/B testing. Once BrainBar is proven stable, the Python server will be retired.

Applied to files:

  • brain-bar/Sources/BrainBar/BrainBarApp.swift
📚 Learning: 2026-03-17T01:04:11.749Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 0
File: :0-0
Timestamp: 2026-03-17T01:04:11.749Z
Learning: The socket path `/tmp/brainbar.sock` is intentional for the BrainBar Swift daemon (brain-bar/) and must NOT be changed to `/tmp/brainlayer.sock`. BrainBar is a new daemon that coexists with the existing Python `brainlayer-mcp` (which uses `/tmp/brainlayer.sock`) during the migration period. The different paths avoid conflicts and allow A/B testing. Once BrainBar is proven stable, the Python server will be retired and `.mcp.json` will point to `/tmp/brainbar.sock` via socat.

Applied to files:

  • brain-bar/Sources/BrainBar/BrainBarApp.swift
🪛 SwiftLint (0.63.2)
brain-bar/Sources/BrainBar/BrainBarCommandBar.swift

[Warning] 140-140: Classes should have an explicit deinit method

(required_deinit)


[Warning] 172-172: Classes should have an explicit deinit method

(required_deinit)

brain-bar/Sources/BrainBar/BrainBarApp.swift

[Error] 382-382: Force casts should be avoided

(force_cast)


[Error] 473-473: Force casts should be avoided

(force_cast)


[Error] 474-474: Force casts should be avoided

(force_cast)

brain-bar/Tests/BrainBarTests/BrainBarWindowStateTests.swift

[Warning] 6-6: Classes should have an explicit deinit method

(required_deinit)


[Warning] 71-71: Classes should have an explicit deinit method

(required_deinit)


[Warning] 313-313: Classes should have an explicit deinit method

(required_deinit)


[Warning] 326-326: Classes should have an explicit deinit method

(required_deinit)

brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift

[Warning] 596-596: Classes should have an explicit deinit method

(required_deinit)


[Warning] 625-625: Classes should have an explicit deinit method

(required_deinit)

🔇 Additional comments (8)
brain-bar/Sources/BrainBar/BrainBarCommandBar.swift (1)

436-438: Double-tap still can't beat single-tap because .onTapGesture is attached first.

SwiftUI resolves stacked tap gestures in attachment order, so onSelect fires on the first click of a double-click and onActivate never wins. Either reorder (count: 2 first) or compose explicitly: .gesture(TapGesture(count: 2).exclusively(before: TapGesture(count: 1))).

brain-bar/Sources/BrainBar/BrainBarApp.swift (2)

306-324: Size thresholds still disagree between discoverMenuBarWindow (400×300) and isMenuBarExtraWindow (760×560).

During the brief window the MenuBarExtra is being sized up, discoverMenuBarWindow can latch discoveredMenuBarWindow to a frame that the didBecomeKey observer (line 669-673) will then refuse to persist/sync. Consolidate on the stricter predicate (or extract a shared helper) so discovery and observer identification can't disagree.


879-893: BrainBarMenuBarLabel still allocates a fresh NSImage in body on every collector.$stats publish.

SparklineRenderer.render(...) is called directly in the view body, so every collector tick allocates a new NSImage and re-draws while the menu bar is idle. Not a correctness bug, but worth caching by (state, values hash, size) via @State + onReceive if you ever see it in Instruments.

brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift (1)

615-679: WindowAttachmentView still re-resolves on every update, and attach(window:) still churns observers.

updateNSView unconditionally dispatches onResolve(window), and attach(window:) unconditionally calls removeObservers() + re-adds didMove/didEndLiveResize even when preparedWindowNumber == window.windowNumber. The alpha-fade path is guarded by needsPreparation, but the NotificationCenter churn is not. Short-circuit inside attach when the window number matches and observers are already wired (or move resolve into makeNSView only).

♻️ Minimal guard
     func attach(window: NSWindow) {
         let needsPreparation = preparedWindowNumber != window.windowNumber
+        if !needsPreparation, !observers.isEmpty { return }
         if needsPreparation {
brain-bar/Sources/BrainBar/InjectionStore.swift (1)

4-82: LGTM — Darwin observer wiring is now crash-safe.

The takeUnretainedValue() + Task { @mainactor in … } pattern captures store strongly for the Task's duration, and the unconditional CFNotificationCenterRemoveObserver in deinit (with the nicely justified comment) closes the "callback fires after owner release" gap that the old weak-box dance could still drop through. Moving database.close() into stop() also aligns the socket/DB close pair with the applicationWillTerminate path.

brain-bar/Tests/BrainBarTests/QuickCapturePanelTests.swift (1)

242-330: LGTM — bridge-layer coverage addresses the exact Cmd+Return regression site.

These three KeyHandlingCommandBarField.handleKeyEquivalent(...) cases (focused / not focused / plain Return) cover the performKeyEquivalent path that the old view-model-only tests couldn't reach. Asserting the "must NOT consume plain Return" branch is the key one — it protects the NSTextFieldDelegate.doCommandBy: chain from being stolen by the bridge.

brain-bar/Sources/BrainBar/QuickCapturePanel.swift (1)

99-144: LGTM — overlay-dismissal and feedback auto-clear lifecycle.

scheduleFeedbackAutoClear correctly cancels the prior feedbackResetTask, re-checks .success after the sleep (so an intervening .error isn't clobbered back to idle), and the isSearchOverlayDismissed flag is cleanly reset on both setMode and handleInputChange. Injectable feedbackAutoClearDelay is also the right test seam.

brain-bar/Tests/BrainBarTests/BrainBarWindowStateTests.swift (1)

7-311: LGTM — strong placement + runtime-routing coverage.

AX/AppKit frame round-trip across a raised secondary display, the menu-bar clearance branch, preferredMenuBarItemFrame picking by mouse proximity, and the runtime callback assertions all cover the genuinely tricky parts of the rewrite. The earlier "detached panel" framing in the failure strings is gone — the new messages explicitly call out both launch modes, which is exactly the right framing.

Comment on lines +49 to +53
if launchMode == .menuBarWindow {
UserDefaults.standard.removeObject(forKey: Self.menuBarWindowAutosaveKey)
installMenuBarWindowObservers()
startMenuBarWindowSync()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Clearing the AppKit frame-autosave on every launch deserves a one-line comment.

Dropping "NSWindow Frame BrainBarMenuBarExtraWindow" at every launch disables AppKit's own autosave so your BrainBarWindowFrameStore is the only source of truth. That's the right call, but the intent isn't obvious from the call site — a short // canonical frame lives in BrainBarWindowFrameStore; prevent AppKit from restoring a stale frame first would save the next reader a git-blame trip.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/BrainBarApp.swift` around lines 49 - 53, Add a
one-line comment above the UserDefaults.standard.removeObject(forKey:
Self.menuBarWindowAutosaveKey) call in the launchMode == .menuBarWindow branch
explaining intent: note that the canonical window frame is stored in
BrainBarWindowFrameStore and we clear AppKit's "NSWindow Frame
BrainBarMenuBarExtraWindow" autosave so AppKit won't restore a stale frame
before installMenuBarWindowObservers() and startMenuBarWindowSync() run; keep
the existing call and surrounding logic unchanged.

Comment on lines +286 to +291
private var keyboardHint: String {
switch viewModel.mode {
case .capture: return "⏎ Store · ⌘⏎ Store · ⇥ Search"
case .search: return "⏎ Copy · ⌘⏎ Capture · ⇥ Capture"
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Capture-mode hint advertises two different shortcuts that do the same thing.

⏎ Store · ⌘⏎ Store tells the user Cmd+Return is a separate action, but in capture mode both handleInputReturn([]) and handleInputReturn([.command]) land in submitCapture(...) with the same user-visible outcome (store + clear input). That's the exact "hint is lying" shape you flagged for search mode — worth collapsing to something like ⏎ Store · ⇥ Search so the legend stays honest.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/BrainBarCommandBar.swift` around lines 286 - 291,
Update the keyboardHint computed property so the capture-mode hint doesn't
advertise duplicate shortcuts: modify the .capture case in keyboardHint (which
reads viewModel.mode) to a single-line legend like "⏎ Store · ⇥ Search" instead
of "⏎ Store · ⌘⏎ Store"; this aligns the displayed hint with how
handleInputReturn([]) and handleInputReturn([.command]) both call
submitCapture(...) and produce the same user-visible action.

Comment on lines +645 to +692
configure(window: window)
coordinator.attach(window: window)

if needsPreparation {
DispatchQueue.main.async { [weak self, weak window] in
self?.isContentReady = true
window?.alphaValue = 1
}
} else if !isContentReady {
isContentReady = true
window.alphaValue = 1
}

let center = NotificationCenter.default
observers = [
center.addObserver(
forName: NSWindow.didMoveNotification,
object: window,
queue: .main
) { [weak self] _ in
Task { @MainActor [weak self] in
self?.coordinator.captureCurrentFrame()
}
},
center.addObserver(
forName: NSWindow.didEndLiveResizeNotification,
object: window,
queue: .main
) { [weak self] _ in
Task { @MainActor [weak self] in
self?.coordinator.captureCurrentFrame()
}
},
]
}

private func configure(window: NSWindow) {
window.title = "BrainBar"
window.minSize = NSSize(width: 760, height: 560)
window.maxSize = NSSize(width: 1_600, height: 1_200)
window.isMovableByWindowBackground = true
window.styleMask.insert(.resizable)
if let resolvedFrame = BrainBarWindowPlacement.resolvedFrame(
persistedFrame: BrainBarWindowFrameStore().persistedFrame()
) {
window.setFrame(resolvedFrame, display: true)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Nit: configure(window:) and coordinator.attach(window:) both setFrame on the same attach.

configure calls BrainBarWindowPlacement.resolvedFrame(persistedFrame:) and sets the frame (line 687-691), then coordinator.attach(window:) (line 646) resolves against the same store and applies again. It's idempotent but you're paying two layout passes per attach and the ownership of "who places the window" is split across two objects. Consider making configure only set style/min-max and letting the coordinator be the sole frame authority.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift` around lines 645 -
692, The configure(window:) method and coordinator.attach(window:) both compute
and apply the persisted frame causing duplicate layout work; change
configure(window:) to only set non-placement properties (title, minSize,
maxSize, isMovableByWindowBackground, styleMask) and remove the call to
BrainBarWindowPlacement.resolvedFrame(...) and window.setFrame(...) from
configure so that coordinator.attach(window:) (which uses
BrainBarWindowPlacement.resolvedFrame and BrainBarWindowFrameStore()) remains
the single owner of window placement.

…itch + wire click-outside

Closes 3 new P0s from the post-03d66b8a live test:

1. **Click-outside didn't dismiss** — root cause: commit 03d66b8 added
   the `isSearchOverlayDismissed` @published flag to `QuickCaptureViewModel`
   but `BrainBarCommandBarResultsOverlayGate.shouldShow` never actually
   read it. The flag was being set but shouldShow returned true anyway,
   so the overlay stayed visible. The earlier
   `.onTapGesture { viewModel.dismissSearchOverlay() }` on the tab-content
   Group was firing correctly — the dismissed flag just wasn't gating
   anything. Now `shouldShow` checks
   `!isSearchOverlayDismissed` and the overlay hides.

2. **Results overlay persisted across tab switches** — the overlay was
   attached to the Group that swaps dashboard/injections/graph content,
   so it floated above whichever tab was active. Fix: the overlay now
   takes an `isOnActiveTab: Bool` prop; `BrainBarWindowRootView` passes
   `selectedTab == .dashboard`. `shouldShow` short-circuits false when
   off-tab. Belt-and-suspenders: `.onChange(of: selectedTab)` on the
   root view explicitly calls `dismissSearchOverlay()` when the user
   leaves Dashboard, so any future re-architecture of the overlay host
   still clears stale state.

3. **Tap-catcher is now built into the overlay itself** as a full-area
   `Color.clear` + `.onTapGesture` UNDER the results card (ZStack),
   instead of an `.onTapGesture` attached to the underlying Group.
   Previously, clicks on dashboard metric cards could be absorbed by
   their `.background(RoundedRectangle(...))` shapes before reaching
   the Group's handler; the new design puts a guaranteed-transparent
   tap layer behind the card so clicks anywhere outside the card
   reach `dismissSearchOverlay()`. The tap-catcher only exists while
   the overlay is visible, so normal tab-content interactions are
   unaffected at rest.

Also migrates the animation definitions from the parent to the gate —
the ZStack's `.animation(value: shouldShow)` is the single trigger,
replacing the parent-level `.animation(value: ...)` chain that had to
guess which @published property was changing.

Suspected root cause for user's "input stuck after tab-switch" report:
the overlay was holding a visible `Color.clear` tap-shape over the
entire tab region even after navigating to Graph/Injections, which
could confuse AppKit's first-responder dispatch when the user clicked
back into the NSTextField. With the overlay now properly scoped to
Dashboard + dismissing on switch, the tap-catcher no longer lingers,
and the NSTextField owns its own hit region in the header cleanly.

Tests
- `swift test --filter BrainBar` → 270 tests, 0 failures (unchanged).
  The existing `testHandleInputChangeClearsOverlayDismissedFlag` and
  `testDismissSearchOverlayHidesResultsWithoutClearingInput` now
  actually exercise a gate that honours the flag.

Visual verification
- `/tmp/brainbar-v5-search-dashboard.png` — typing "brainlayer" on
  Dashboard shows the results dropdown.
- `/tmp/brainbar-v5-click-outside.png` — click below the results card
  dismisses the overlay; "brainlayer" remains in the input so the
  next keystroke re-shows.

Deferred to commit 5 (as planned): async-off-main submitSearch, teal
hysteresis on hero accent, sparkline dot pixel-snap centering. Deferred
to commit 6: expand-in-context detail drawer (Atomic pattern) +
enrichment metrics redesign.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4bdbdc774e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +413 to +417
if let preferredFrame = BrainBarWindowPlacement.preferredMenuBarItemFrame(
candidates: fallbackMatches.map(\.frame),
mouseLocation: mouseLocation
) {
return fallbackMatches.first(where: { $0.frame == preferredFrame })?.element
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restrict AX fallback to the BrainBar menu item

When no kAXTitle matches "brain"/"brainbar", this fallback selects the nearest menu-bar extra from all children and pressMenuBarExtraItem() will press it. With the new image-only MenuBarExtra label, AX titles can be empty, so toggle/search requests can end up clicking another app’s icon instead of BrainBar. This breaks BrainBar window automation and can trigger unrelated menu-bar apps; the fallback should fail closed (or use a BrainBar-specific discriminator) instead of choosing arbitrary neighbors.

Useful? React with 👍 / 👎.

Comment on lines +50 to 53
let clamped = max(ratePerMinute, 0)
if clamped.rounded(.towardZero) == clamped {
return "\(Int(clamped))/min"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard non-finite rates before integer badge formatting

liveBadgeString now casts clamped to Int without checking finiteness. If ratePerMinute is NaN or infinity, Int(clamped) traps at runtime and crashes badge rendering. The previous formatter path explicitly sanitized non-finite values; this path should also normalize them (for example to 0/min) before integer conversion.

Useful? React with 👍 / 👎.

…rlay transition

Two user-reported live-test issues on commit 4bdbdc7:

1. **Stale results after search → capture → search round-trip**. User's
   repro: type "tests" in search (hits populate), switch to Capture,
   switch back to Search — overlay says "No matches yet for tests".
   Delete the "t" — search re-runs and finds hits. Re-add "t" — finds
   again. So the DB + query work; the bug is that setMode(.search)
   does NOT re-run submitSearch even though inputText was preserved
   across the mode round-trip, and setMode(.capture) had cleared the
   results array. Net: for a couple of beats the overlay lies about
   a query that already has real hits.

   Fix: when setMode enters `.search` with a non-empty trimmed
   inputText, call `submitSearch()` so the results array repopulates
   before the overlay renders. Added
   `testReturningToSearchModeReRunsSearchWhenInputPreserved` to lock
   the round-trip contract.

2. **User requested fade-only on the overlay transition** instead of
   fade+slide. `.transition(.opacity.combined(with: .move(edge: .top)))`
   → `.transition(.opacity)`. Same 0.18s `easeInOut` duration via the
   gate's `.animation(value: shouldShow)`.

Also confirms from the same live test: click-outside dismiss (wired
in 4bdbdc7) now works as intended — "Oh, now if I click somewhere
else, it does remove."

Tests: `swift test --filter BrainBar` → 271 tests, 0 failures (+1).

Commit 5 scope unchanged (async submitSearch, teal hysteresis,
sparkline dot centering, stale-state diagnosis for long-running
Injections/KG).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (4)
brain-bar/Sources/BrainBar/BrainBarCommandBar.swift (2)

461-462: ⚠️ Potential issue | 🟡 Minor

Tap-gesture order still recognizes single-tap before double-tap.

.onTapGesture(perform: onSelect) is attached before .onTapGesture(count: 2, perform: onActivate), so the single-tap wins the first click of a double-click and onActivate (copy-to-clipboard) never fires from a double-click. Either swap the order so the count: 2 modifier is applied first, or compose explicitly with TapGesture(count: 2).exclusively(before: TapGesture(count: 1)).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/BrainBarCommandBar.swift` around lines 461 - 462,
The tap handlers are ordered so the single-tap (.onTapGesture(perform:
onSelect)) captures the first tap of a double-click and prevents onActivate from
firing; update the gesture composition where those modifiers are applied so the
double-tap wins: either apply .onTapGesture(count: 2, perform: onActivate)
before .onTapGesture(perform: onSelect) or replace both modifiers with an
explicit TapGesture composition like TapGesture(count: 2).exclusively(before:
TapGesture()) and wire the .onEnded closures to onActivate and onSelect
respectively, referencing the existing onSelect and onActivate callbacks used in
this view.

286-291: ⚠️ Potential issue | 🟡 Minor

Capture-mode legend still advertises Cmd+Return as a distinct action.

⏎ Store · ⌘⏎ Store shows two separate shortcuts, but in .capture mode handleInputReturn([]) and handleInputReturn([.command]) both funnel into submitCapture(...) and produce the exact same user-visible outcome (store + clear). Collapse to something honest like ⏎ Store · ⇥ Search so the hint doesn't imply a Cmd-modified variant that doesn't exist.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/BrainBarCommandBar.swift` around lines 286 - 291,
The keyboard hint for capture mode is misleading because it lists "⏎ Store · ⌘⏎
Store" even though handleInputReturn([]) and handleInputReturn([.command]) both
call submitCapture(...) and behave identically; update the keyboardHint computed
property (switch on viewModel.mode in BrainBarCommandBar.keyboardHint) to
collapse the duplicate entry to a single "⏎ Store · ⇥ Search" (or equivalent
honest text) so the UI doesn't advertise a distinct Cmd+Return action that
doesn't exist.
brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift (2)

676-687: 🧹 Nitpick | 🔵 Trivial

configure(window:) and coordinator.attach(window:) both own window placement.

configure computes BrainBarWindowPlacement.resolvedFrame(...) and calls window.setFrame(...), and immediately afterwards coordinator.attach(window:) (line 641) resolves against the same BrainBarWindowFrameStore and applies again. It's idempotent but it's two layout passes per attach and the "who places the window" responsibility is split. Let configure handle only non-placement window properties (title, minSize/maxSize, isMovableByWindowBackground, styleMask) and make the coordinator the single frame authority.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift` around lines 676 -
687, The configure(window:) function is duplicating window placement logic that
coordinator.attach(window:) already performs; remove the placement-specific code
from configure(window:) so it only sets title, minSize/maxSize,
isMovableByWindowBackground, and styleMask, and let coordinator.attach(window:)
remain the single authority that calls
BrainBarWindowPlacement.resolvedFrame(BrainBarWindowFrameStore().persistedFrame())
and window.setFrame(...). Specifically, delete the call to
BrainBarWindowPlacement.resolvedFrame(...) and the window.setFrame(...) call
from configure(window:), leaving non-placement property assignments intact, so
all persisted-frame resolution stays in coordinator.attach(window:).

597-617: ⚠️ Potential issue | 🟡 Minor

WindowAttachmentView.updateNSView re-resolves the window on every SwiftUI update.

updateNSView unconditionally dispatches onResolve(window)BrainBarWindowObserver.attach(window:)removeObservers() + re-addObserver for both didMove and didEndLiveResize, and coordinator.attach(window:) which applies the persisted frame again. The preparedWindowNumber check only prevents the alpha flicker; it does not prevent notification-center churn or the duplicate setFrame. Guard attach(window:) to no-op when the window is already prepared and observers are live, or skip the update-time dispatch and resolve once in makeNSView (falling back to a one-shot window-resolution observer if the window isn't attached yet).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift` around lines 597 -
617, WindowAttachmentView currently calls onResolve(window) every update,
causing BrainBarWindowObserver.attach(window:) and coordinator.attach(window:)
to repeatedly remove/add observers and reapply frames; change updateNSView to
only call onResolve when the resolved window is different or not yet prepared by
checking the window's identity (e.g., nsView.window?.windowNumber against
BrainBarWindowObserver.preparedWindowNumber or another prepared flag) and skip
the dispatch if it's the same window and observers are already installed;
alternatively move the one-time resolve logic into makeNSView and in
updateNSView only set up a one-shot attachment observer if nsView.window is nil,
ensuring onResolve is invoked once per window.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift`:
- Around line 184-191: heroSparkline is a computed property that calls
SparklineRenderer.render every time the view body recomputes, causing an
expensive 360×160 bitmap to be rebuilt on every collector publish; cache/memoize
the rendered image keyed by its real inputs (collector.state,
collector.stats.recentEnrichmentBuckets, livePresentation.accentColor and the
display size) instead of recomputing each body pass (e.g. store the rendered
NSImage in a cached/stored property or use a simple lookup keyed by a hash of
those inputs), and when calling SparklineRenderer.render use the actual display
size (layout.sparklineWidth × layout.sparklineHeight) rather than the fixed
360×160 so the rasterization matches the view size.

In `@brain-bar/Tests/BrainBarTests/QuickCapturePanelTests.swift`:
- Around line 417-438: The test testFeedbackAutoClearsToIdleAfterSuccessWindow
is timing-fragile because it sleeps a fixed 80ms; instead make it deterministic
by either (A) polling model.feedback until model.feedback.isIdle with a deadline
(e.g., loop while !model.feedback.isIdle && Date() < deadline with ~1s cap)
after awaiting model._pendingStoreTask, or (B) expose an awaitable handle on the
auto-clear work (name it feedbackResetTask or similar on QuickCaptureViewModel)
and await that task in the test; update the test to use one of these approaches
and remove the fixed Task.sleep to avoid CI flakes.

---

Duplicate comments:
In `@brain-bar/Sources/BrainBar/BrainBarCommandBar.swift`:
- Around line 461-462: The tap handlers are ordered so the single-tap
(.onTapGesture(perform: onSelect)) captures the first tap of a double-click and
prevents onActivate from firing; update the gesture composition where those
modifiers are applied so the double-tap wins: either apply .onTapGesture(count:
2, perform: onActivate) before .onTapGesture(perform: onSelect) or replace both
modifiers with an explicit TapGesture composition like TapGesture(count:
2).exclusively(before: TapGesture()) and wire the .onEnded closures to
onActivate and onSelect respectively, referencing the existing onSelect and
onActivate callbacks used in this view.
- Around line 286-291: The keyboard hint for capture mode is misleading because
it lists "⏎ Store · ⌘⏎ Store" even though handleInputReturn([]) and
handleInputReturn([.command]) both call submitCapture(...) and behave
identically; update the keyboardHint computed property (switch on viewModel.mode
in BrainBarCommandBar.keyboardHint) to collapse the duplicate entry to a single
"⏎ Store · ⇥ Search" (or equivalent honest text) so the UI doesn't advertise a
distinct Cmd+Return action that doesn't exist.

In `@brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift`:
- Around line 676-687: The configure(window:) function is duplicating window
placement logic that coordinator.attach(window:) already performs; remove the
placement-specific code from configure(window:) so it only sets title,
minSize/maxSize, isMovableByWindowBackground, and styleMask, and let
coordinator.attach(window:) remain the single authority that calls
BrainBarWindowPlacement.resolvedFrame(BrainBarWindowFrameStore().persistedFrame())
and window.setFrame(...). Specifically, delete the call to
BrainBarWindowPlacement.resolvedFrame(...) and the window.setFrame(...) call
from configure(window:), leaving non-placement property assignments intact, so
all persisted-frame resolution stays in coordinator.attach(window:).
- Around line 597-617: WindowAttachmentView currently calls onResolve(window)
every update, causing BrainBarWindowObserver.attach(window:) and
coordinator.attach(window:) to repeatedly remove/add observers and reapply
frames; change updateNSView to only call onResolve when the resolved window is
different or not yet prepared by checking the window's identity (e.g.,
nsView.window?.windowNumber against BrainBarWindowObserver.preparedWindowNumber
or another prepared flag) and skip the dispatch if it's the same window and
observers are already installed; alternatively move the one-time resolve logic
into makeNSView and in updateNSView only set up a one-shot attachment observer
if nsView.window is nil, ensuring onResolve is invoked once per window.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cfb444c4-e9ca-4ea8-922d-4055557e5ca8

📥 Commits

Reviewing files that changed from the base of the PR and between 03d66b8 and d71e40e.

📒 Files selected for processing (4)
  • brain-bar/Sources/BrainBar/BrainBarCommandBar.swift
  • brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift
  • brain-bar/Sources/BrainBar/QuickCapturePanel.swift
  • brain-bar/Tests/BrainBarTests/QuickCapturePanelTests.swift
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.11)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
Repo: EtanHey/brainlayer PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-14T02:20:54.656Z
Learning: Request codex review, cursor review, and bugbot review for BrainLayer PRs
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 0
File: :0-0
Timestamp: 2026-03-17T01:04:22.497Z
Learning: In BrainLayer, the BrainBar daemon uses the socket path `/tmp/brainbar.sock` (NOT `/tmp/brainlayer.sock`). BrainBar is a new native Swift daemon designed to coexist with the existing Python `brainlayer-mcp` server during the migration period. Different socket paths avoid conflicts and enable A/B testing. Once BrainBar is proven stable, the Python server will be retired.
📚 Learning: 2026-03-18T00:12:08.774Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 87
File: brain-bar/Sources/BrainBar/BrainBarServer.swift:118-129
Timestamp: 2026-03-18T00:12:08.774Z
Learning: In Swift files under brain-bar/Sources/BrainBar, enforce that when a critical dependency like the database is nil due to startup ordering (socket before DB), any tool handler that accesses the database must throw an explicit error (e.g., ToolError.noDatabase) instead of returning a default/empty value. Do not allow silent defaults (e.g., guard let db else { return ... }). Flag patterns that silently return defaults when db is nil, as this masks startup timing issues. This guidance applies broadly to similar Swift files in the BrainBar module, not just this one location.

Applied to files:

  • brain-bar/Sources/BrainBar/QuickCapturePanel.swift
  • brain-bar/Sources/BrainBar/BrainBarCommandBar.swift
  • brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift
📚 Learning: 2026-03-29T18:45:40.988Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 133
File: brain-bar/Sources/BrainBar/BrainDatabase.swift:0-0
Timestamp: 2026-03-29T18:45:40.988Z
Learning: In the BrainBar module’s Swift database layer (notably BrainDatabase.swift), ensure that the `search()` function’s `unreadOnly=true` path orders results by the delivery frontier cursor so the watermark `maxRowID` stays contiguous. Specifically, when `unreadOnly` is enabled, the query must include `ORDER BY c.rowid ASC` (e.g., via `let orderByClause = unreadOnly ? "c.rowid ASC" : "f.rank"`). Do not replace the unread-only ordering with relevance-based sorting (e.g., `f.rank`) unconditionally or for the unread-only path, as it can introduce gaps in the watermark and incorrectly mark unseen rows as delivered. Flag any future change to the `ORDER BY` clause in this function that makes relevance sorting apply to the unread-only case.

Applied to files:

  • brain-bar/Sources/BrainBar/QuickCapturePanel.swift
  • brain-bar/Sources/BrainBar/BrainBarCommandBar.swift
  • brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift
🪛 SwiftLint (0.63.2)
brain-bar/Sources/BrainBar/BrainBarCommandBar.swift

[Warning] 140-140: Classes should have an explicit deinit method

(required_deinit)


[Warning] 172-172: Classes should have an explicit deinit method

(required_deinit)

brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift

[Warning] 591-591: Classes should have an explicit deinit method

(required_deinit)


[Warning] 620-620: Classes should have an explicit deinit method

(required_deinit)

Comment on lines +184 to +191
private var heroSparkline: NSImage {
SparklineRenderer.render(
state: collector.state,
values: collector.stats.recentEnrichmentBuckets,
size: NSSize(width: 360, height: 160),
accentColor: livePresentation.accentColor
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

heroSparkline re-renders a CoreGraphics NSImage on every body recomputation.

heroSparkline is a computed property with no memoization, so every collector publish (which happens on each stats tick) rebuilds a 360×160 NSImage via SparklineRenderer.render — in the hot path of the visible dashboard. On top of that, the rendered image is rasterized at a fixed 360×160 but displayed at layout.sparklineWidth × layout.sparklineHeight, so you're also paying an avoidable bitmap scale.

Cache it against its real inputs so ticks that don't change the buckets/state/accent don't re-render, and render at the display size:

♻️ Proposed shape
-    private var heroSparkline: NSImage {
-        SparklineRenderer.render(
-            state: collector.state,
-            values: collector.stats.recentEnrichmentBuckets,
-            size: NSSize(width: 360, height: 160),
-            accentColor: livePresentation.accentColor
-        )
-    }
+    `@State` private var cachedSparkline: NSImage?
+    `@State` private var cachedSparklineKey: SparklineCacheKey?
+
+    private struct SparklineCacheKey: Equatable {
+        let state: PipelineState
+        let values: [Int]
+        let size: CGSize
+        let accentDescription: String
+    }
+
+    private func heroSparkline(size: CGSize) -> NSImage {
+        let key = SparklineCacheKey(
+            state: collector.state,
+            values: collector.stats.recentEnrichmentBuckets,
+            size: size,
+            accentDescription: livePresentation.accentColor.description
+        )
+        if key == cachedSparklineKey, let image = cachedSparkline { return image }
+        let image = SparklineRenderer.render(
+            state: collector.state,
+            values: collector.stats.recentEnrichmentBuckets,
+            size: NSSize(width: size.width, height: size.height),
+            accentColor: livePresentation.accentColor
+        )
+        cachedSparklineKey = key
+        cachedSparkline = image
+        return image
+    }

…and pass NSSize(width: layout.sparklineWidth, height: layout.sparklineHeight) at the call site on line 306 so the rasterization matches the view size.

Also applies to: 305-311

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift` around lines 184 -
191, heroSparkline is a computed property that calls SparklineRenderer.render
every time the view body recomputes, causing an expensive 360×160 bitmap to be
rebuilt on every collector publish; cache/memoize the rendered image keyed by
its real inputs (collector.state, collector.stats.recentEnrichmentBuckets,
livePresentation.accentColor and the display size) instead of recomputing each
body pass (e.g. store the rendered NSImage in a cached/stored property or use a
simple lookup keyed by a hash of those inputs), and when calling
SparklineRenderer.render use the actual display size (layout.sparklineWidth ×
layout.sparklineHeight) rather than the fixed 360×160 so the rasterization
matches the view size.

Comment on lines +417 to +438
func testFeedbackAutoClearsToIdleAfterSuccessWindow() async throws {
let (db, path) = try makeDatabase(name: "feedback-auto-clear")
defer { cleanupDatabase(db, path: path) }

let model = QuickCaptureViewModel(
db: db,
panelState: QuickCapturePanelState(),
feedbackAutoClearDelay: .milliseconds(30)
)
model.inputText = "Auto-clearing feedback makes capture legible"

model.submit()
await model._pendingStoreTask?.value
XCTAssertEqual(model.feedback, .success("Stored in BrainLayer"))

// Wait for the auto-clear task to fire (30ms delay + buffer).
try? await Task.sleep(for: .milliseconds(80))
XCTAssertTrue(
model.feedback.isIdle,
"Feedback must auto-clear to idle after the success window so the trailing hint returns to its keyboard-shortcut legend."
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Sleep-based auto-clear assertion is timing-fragile.

The test sets feedbackAutoClearDelay: .milliseconds(30) and then Task.sleep(.milliseconds(80)). A 50ms budget is fine locally but easy to blow through on a loaded CI runner, and the failure mode is a silent flake rather than a deterministic assertion. Prefer polling the state (e.g., a short while !model.feedback.isIdle && Date() < deadline loop with a ~1s cap) or exposing an awaitable handle on feedbackResetTask similar to _pendingStoreTask, so the test deterministically waits for the reset instead of racing a wall clock.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Tests/BrainBarTests/QuickCapturePanelTests.swift` around lines 417
- 438, The test testFeedbackAutoClearsToIdleAfterSuccessWindow is timing-fragile
because it sleeps a fixed 80ms; instead make it deterministic by either (A)
polling model.feedback until model.feedback.isIdle with a deadline (e.g., loop
while !model.feedback.isIdle && Date() < deadline with ~1s cap) after awaiting
model._pendingStoreTask, or (B) expose an awaitable handle on the auto-clear
work (name it feedbackResetTask or similar on QuickCaptureViewModel) and await
that task in the test; update the test to use one of these approaches and remove
the fixed Task.sleep to avoid CI flakes.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d71e40e323

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 756 to 758
gesture.onDoubleTap = { [weak self] in
self?.searchPanel?.show()
self?.runtime.handleToggleRequest()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route fallback double-tap to search instead of toggle

When useCGEventTapFallback is enabled, both onSingleTap and onDoubleTap now call runtime.handleToggleRequest(), so users who rely on the fallback path (typically because URL hotkeys are unavailable) lose the dedicated quick-search gesture and can only toggle the window. This is a behavior regression from the previous mapping and leaves no keyboard-only fallback path that directly triggers search.

Useful? React with 👍 / 👎.

@EtanHey EtanHey merged commit e8fa626 into main Apr 17, 2026
7 checks passed
@EtanHey EtanHey deleted the feat/brainbar-menubarextra-rewrite branch April 17, 2026 16:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant