Conversation
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
📝 WalkthroughWalkthroughReplaces 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
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)
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
@codex review |
|
@greptileai review |
|
@coderabbitai review |
|
You need to increase your spend limit or enable usage-based billing to run background agents. Go to Cursor |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 422e40b. Configure here.
| totalWritten: 0, | ||
| lastProgressAt: DispatchTime.now().uptimeNanoseconds, | ||
| onDelivered: onDelivered | ||
| ) |
There was a problem hiding this comment.
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.
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 ?" |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 422e40b. Configure here.
| guard database.isOpen else { | ||
| throw BrainDatabase.DBError.notOpen | ||
| } | ||
| observerBox.store = self |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 422e40b. Configure here.
There was a problem hiding this comment.
💡 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".
| 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) |
There was a problem hiding this comment.
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 👍 / 👎.
| 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() |
There was a problem hiding this comment.
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 ?" |
There was a problem hiding this comment.
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 { | |||
There was a problem hiding this comment.
🟢 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) { |
There was a problem hiding this comment.
🟢 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 theactivityWindowMinutesparameter. 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 withactivityWindowMinutes: 60and 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+"`.
| 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) |
There was a problem hiding this comment.
🟡 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.
There was a problem hiding this comment.
🟠 High
brainlayer/brain-bar/build-app.sh
Lines 105 to 121 in 422e40b
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.
| 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 ?" |
There was a problem hiding this comment.
🟢 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-vimandproject-editorentities have the same default importance (5.0) since neither metadata contains animportancefield. ThefetchKGEntitiesquery orders byimportance DESC, but when values are equal, SQLite's row ordering is undefined. The test assumesentities.firstwill 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.
| if n < 0 { | ||
| if errno == EAGAIN || errno == EWOULDBLOCK { | ||
| eagainRetries += 1 | ||
| if eagainRetries > Self.maxWriteRetries { |
There was a problem hiding this comment.
🟠 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.
| 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) | ||
| } |
There was a problem hiding this comment.
🟠 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.enrichmentRatePerMinuteisDouble.infinity, the code path callsliveBadgeString(ratePerMinute:)which then attemptsInt(clamped)whereclampedis infinity. ConvertingDouble.infinitytoIntcauses a runtime crash in Swift. The previous implementation hadsanitizedRatePerMinutewhich checkedratePerMinute.isFiniteand returnednilfor 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) |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit aab2891. Configure here.
| recentEnrichmentBuckets: [Int], | ||
| activityWindowMinutes: Int = 30 | ||
| ) -> String { | ||
| guard !recentEnrichmentBuckets.isEmpty else { return "30m+" } |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit aab2891. Configure here.
| } | ||
| .contentShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) | ||
| .onTapGesture(perform: onSelect) | ||
| .onTapGesture(count: 2, perform: onActivate) |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit aab2891. Configure here.
| private var heroSparkline: NSImage { | ||
| SparklineRenderer.render( | ||
| state: collector.state, | ||
| values: collector.stats.recentEnrichmentBuckets, | ||
| size: NSSize(width: 360, height: 160), | ||
| accentColor: livePresentation.accentColor | ||
| ) | ||
| } |
There was a problem hiding this comment.
🟢 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
There was a problem hiding this comment.
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 | 🟡 MinorTest name contradicts assertion — rename or reassert.
testPopoverTabGraphSizeIsTallerThanDashboardasserts 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. GiventestPopoverTabsShareStablePanelSizeon 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 | 🟠 MajorUnconditional
wait_for_socketwill hard-fail whenPLIST_SRCis 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), theficloses the install block but thewait_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:
- Build and sign successfully.
- Print no "LaunchAgent installed" line.
- Spin in
wait_for_socketfor 20s.- 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 intobundle/).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 | 🔵 TrivialDuplicate
.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 | 🟠 MajorRemoving the
id DESCtiebreaker makeslistInjectionEventsnon-deterministic for same-timestamp rows.SQLite's
datetime('now')has one-second precision, so bursts of injection events easily collide ontimestamp. With the newORDER BY timestamp DESC LIMIT ?and no secondary key, which rows get truncated byLIMITis implementation-defined and can change across queries (even within the same session). This directly affectsInjectionStore.refresh(...)consistency and can re-introduce the flakiness that the formerid DESCtiebreaker 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
recentActivityBucketsandrecentEnrichmentBucketsare copy-paste — extract a shared helper.The two functions differ only in: (a) the timestamp column selected (
created_atvsenriched_at) and (b) the extraenriched_at IS NOT NULL AND TRIM(...) != ''predicate. The bucket math, date parsing, guard clauses, and offset clamping are identical. The previousrecentBuckets(...)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
recordInjectionEventreturns silent defaults whendbis nil orpreparefails — promote it tothrowsand remove the fallbacks.Two fallback paths construct a bogus
InjectionEvent(id: 0, ...):
- Line 1914–1916 when
dbis nil (startup/DB not ready)- Line 1921–1923 when
sqlite3_prepare_v2failsTests at
InjectionStoreTests.swift(lines 23, 47) andDatabaseTests.swiftalready call this astry 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 expectthrows. Silently returning a defaultInjectionEventalso 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 | 🔴 CriticalPotential use-after-free: Darwin observer outlives
InjectionStoreifstop()is not called.The observer is installed with
Unmanaged.passUnretained(self).toOpaque()(line 94) and removal is now gated solely onstop()(line 59–61). There is nodeinithook. If any caller drops the last strong reference without first callingstop()— or drops it between a Darwin notification posting and the callback running — the callback'sUnmanaged.fromOpaque(observer).takeUnretainedValue()at line 12 dereferences a freed object, which is undefined behavior.This is the scenario the removed
testScheduledDarwinRefreshDoesNotRetainStoreAfterReleasewas covering.Minimum safety net: mirror the old cleanup in
deinit. Becausedeinitcan't call a@MainActormethod 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
passRetainedon install +takeRetainedValueon remove, soselfcannot 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 | 🔴 CriticalMake
recordInjectionEventthrow explicit errors instead of silently returning zero-id events.
recordInjectionEventis declared as non-throwing (line 1913:func recordInjectionEvent(...) -> InjectionEvent) but this test and others wrap it intry, which produces a Swift compiler warning. More critically, the function silently returns fakeInjectionEvent(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
recordInjectionEventto 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
⛔ Files ignored due to path filters (2)
docs.local/plans/2026-04-17-brainbar-linear-ux/artifacts/brainbar-after-inline-dashboard.pngis excluded by!**/*.pngdocs.local/plans/2026-04-17-brainbar-linear-ux/artifacts/brainbar-before-inline-dashboard.pngis excluded by!**/*.png
📒 Files selected for processing (27)
brain-bar/Sources/BrainBar/BrainBarApp.swiftbrain-bar/Sources/BrainBar/BrainBarCommandBar.swiftbrain-bar/Sources/BrainBar/BrainBarRuntime.swiftbrain-bar/Sources/BrainBar/BrainBarServer.swiftbrain-bar/Sources/BrainBar/BrainBarWindowRootView.swiftbrain-bar/Sources/BrainBar/BrainBarWindowState.swiftbrain-bar/Sources/BrainBar/BrainDatabase.swiftbrain-bar/Sources/BrainBar/Dashboard/BrainBarLivePulse.swiftbrain-bar/Sources/BrainBar/Dashboard/DashboardMetricFormatter.swiftbrain-bar/Sources/BrainBar/Dashboard/PipelineState.swiftbrain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swiftbrain-bar/Sources/BrainBar/Dashboard/StatusPopoverView.swiftbrain-bar/Sources/BrainBar/Formatting/TextFormatter.swiftbrain-bar/Sources/BrainBar/InjectionFeedView.swiftbrain-bar/Sources/BrainBar/InjectionStore.swiftbrain-bar/Sources/BrainBar/QuickCapturePanel.swiftbrain-bar/Tests/BrainBarTests/BrainBarUXLogicTests.swiftbrain-bar/Tests/BrainBarTests/BrainBarWindowStateTests.swiftbrain-bar/Tests/BrainBarTests/DashboardTests.swiftbrain-bar/Tests/BrainBarTests/DatabaseTests.swiftbrain-bar/Tests/BrainBarTests/InjectionStoreTests.swiftbrain-bar/Tests/BrainBarTests/KnowledgeGraphTests.swiftbrain-bar/Tests/BrainBarTests/PopoverTabTests.swiftbrain-bar/Tests/BrainBarTests/SocketIntegrationTests.swiftbrain-bar/Tests/BrainBarTests/StabilityFixTests.swiftbrain-bar/Tests/BrainBarTests/TextFormatterParityTests.swiftbrain-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.swiftbrain-bar/Sources/BrainBar/InjectionFeedView.swiftbrain-bar/Sources/BrainBar/BrainBarServer.swiftbrain-bar/Sources/BrainBar/InjectionStore.swiftbrain-bar/Sources/BrainBar/QuickCapturePanel.swiftbrain-bar/Sources/BrainBar/BrainDatabase.swiftbrain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swiftbrain-bar/Sources/BrainBar/BrainBarCommandBar.swiftbrain-bar/Sources/BrainBar/Dashboard/StatusPopoverView.swiftbrain-bar/Sources/BrainBar/Dashboard/DashboardMetricFormatter.swiftbrain-bar/Sources/BrainBar/Formatting/TextFormatter.swiftbrain-bar/Sources/BrainBar/BrainBarRuntime.swiftbrain-bar/Sources/BrainBar/BrainBarWindowRootView.swiftbrain-bar/Sources/BrainBar/BrainBarWindowState.swiftbrain-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.shbrain-bar/Sources/BrainBar/BrainBarServer.swiftbrain-bar/Tests/BrainBarTests/SocketIntegrationTests.swiftbrain-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.shbrain-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.shbrain-bar/Sources/BrainBar/BrainBarServer.swiftbrain-bar/Tests/BrainBarTests/SocketIntegrationTests.swiftbrain-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.swiftbrain-bar/Sources/BrainBar/BrainBarServer.swiftbrain-bar/Sources/BrainBar/InjectionStore.swiftbrain-bar/Sources/BrainBar/QuickCapturePanel.swiftbrain-bar/Sources/BrainBar/BrainDatabase.swiftbrain-bar/Sources/BrainBar/BrainBarCommandBar.swiftbrain-bar/Sources/BrainBar/BrainBarRuntime.swiftbrain-bar/Sources/BrainBar/BrainBarWindowRootView.swiftbrain-bar/Sources/BrainBar/BrainBarWindowState.swiftbrain-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.swiftbrain-bar/Tests/BrainBarTests/SocketIntegrationTests.swiftbrain-bar/Sources/BrainBar/BrainDatabase.swiftbrain-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
BrainBarUXLogicTestsper the summary.brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift (1)
35-45: LGTM — optionalaccentColorwithstate.colorfallback is clean.Backward compatible for callers like
StatusPopoverView(no accent passed → state-driven), while the window/menu-bar callers correctly passlivePresentation.accentColorto get the active/idle distinction.brain-bar/Tests/BrainBarTests/TextFormatterParityTests.swift (1)
54-54: LGTM.Dropping the
direction:argument matches the simplifiedEntityCard.Relationinitializer; the expected→ works_on: BrainLayerline 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
testListInjectionEventsUsesIDAsTieBreakerForMatchingTimestampsaligns with the newORDER BY timestamp DESC-only SQL inBrainDatabase.swift:1944. This means events with identical timestamps now have undefined order. SinceInjectionFeedViewdisplays events in database query order andMCPRouteriterates 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 == 0while keepingrecentEnrichmentBucketscount at1, which matches the hardcodedenrichmentRateWindowMinutes = 1indashboardStats(line 786 ofBrainDatabase.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 nilimportance, tags, and multi-row separators correctly, and thetotal == 0early return prevents an empty table. No correctness concerns.
138-168:formatEntitySimplerefactor looks good.Boxed layout with bounded
prefix(…)on relations/chunks/metadata matches the arrow-direction behavior informatEntityCardand the updatedTextFormatterParityTests.brain-bar/Sources/BrainBar/BrainBarCommandBar.swift (1)
136-211:CommandBarInputbridge 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 (
titledMatchesfirst,fallbackMatchessecond) paired withBrainBarWindowPlacement.preferredMenuBarItemFrame(…)to pick by mouse proximity cleanly handles multi-display setups. Force casts onextrasBar as! AXUIElement/AXValueare 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
applicationShouldTerminateAfterLastWindowClosedreturnsfalse— 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/showQuickCapturePanelcounting ontoonSearchRequested/onQuickCaptureRequested) lock in the contract thatAppDelegatenow 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 toonToggleRequestedwhenever the window coordinator can't toggle directly, lettingAppDelegatedrive 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:ensureCommandBarViewModelis a one-shot; double-check reopen semantics.The guard
commandBarViewModel == nil, let database = runtime.databasemeans the view-model is created exactly once for the lifetime of this view. Ifruntime.databaseis ever reinstalled (e.g., after a swap or restart path) the oldQuickCaptureViewModelwill 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 changinginstall(…)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, callspanelDidAppear, clears request) matches the runtime contract covered byBrainBarWindowStateTests. TheWindowAttachmentView+BrainBarWindowObserversplit keeps AppKit work out of the SwiftUI view hierarchy.
265-270: Platforms requirement is correctly set to macOS 14.The
Package.swiftconfirms the target's minimum platform is.macOS(.v14), which correctly supports the two-argumentonChangeclosure signature used at lines 265 and 539. No action required.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| 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") | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 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.
| .contentShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) | ||
| .onTapGesture(perform: onSelect) | ||
| .onTapGesture(count: 2, perform: onActivate) | ||
| .animation(.easeInOut(duration: 0.14), value: isCopied) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat brain-bar/Sources/BrainBar/BrainBarCommandBar.swift | head -n 410 | tail -n 40Repository: EtanHey/brainlayer
Length of output: 1462
🏁 Script executed:
rg "onSelect|onActivate" brain-bar/Sources/BrainBar/BrainBarCommandBar.swift -B 2 -A 5Repository: EtanHey/brainlayer
Length of output: 1043
🏁 Script executed:
fd "\.swift$" brain-bar/Sources/BrainBar/ | head -20Repository: EtanHey/brainlayer
Length of output: 1139
🏁 Script executed:
rg "selectResult|copyResultToClipboard" brain-bar/Sources/BrainBar/ -B 2 -A 8Repository: EtanHey/brainlayer
Length of output: 6353
🏁 Script executed:
rg "double.?click|double.?tap|gesture" brain-bar/Sources/BrainBar/BrainBarCommandBar.swift -iRepository: EtanHey/brainlayer
Length of output: 155
🏁 Script executed:
rg "TODO|FIXME|NOTE|HACK" brain-bar/Sources/BrainBar/BrainBarCommandBar.swiftRepository: 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:
- 1: https://stackoverflow.com/questions/58539015/swiftui-respond-to-tap-and-double-tap-with-different-actions
- 2: https://marksinkovics.github.io/blog/2021/09/19/swiftui_detect_multiple_tap_gestures/
- 3: https://www.hackingwithswift.com/quick-start/swiftui/how-to-make-two-gestures-recognize-at-the-same-time-using-simultaneousgesture
- 4: https://stackoverflow.com/questions/68783952/swiftui-ios-adding-double-tap-cause-single-tap-called-after-a-delay
- 5: https://www.hackingwithswift.com/books/ios-swiftui/how-to-use-gestures-in-swiftui
- 6: https://developer.apple.com/documentation/SwiftUI/View/onTapGesture(count:perform:)
- 7: https://developer.apple.com/documentation/swiftui/composing-swiftui-gestures
- 8: https://developer.apple.com/documentation/swiftui/adding-interactivity-with-gestures
- 9: https://www.hackingwithswift.com/quick-start/swiftui/how-to-force-one-gesture-to-recognize-before-another-using-highprioritygesture
- 10: https://developer.apple.com/documentation/swiftui/view/highprioritygesture(_:isenabled:)?changes=_1
- 11: https://developer.apple.com/documentation/swiftui/view/highprioritygesture(_:including:)
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.
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 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.swiftRepository: EtanHey/brainlayer
Length of output: 4036
🏁 Script executed:
rg -n 'publishStoredChunk' brain-bar/Sources/BrainBar/BrainBarServer.swiftRepository: 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.swiftRepository: 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 -20Repository: EtanHey/brainlayer
Length of output: 127
🏁 Script executed:
sed -n '310,320p' brain-bar/Sources/BrainBar/BrainBarServer.swiftRepository: EtanHey/brainlayer
Length of output: 546
🏁 Script executed:
sed -n '130,160p' brain-bar/Sources/BrainBar/BrainBarServer.swiftRepository: EtanHey/brainlayer
Length of output: 998
🏁 Script executed:
# Find the function containing line 314
sed -n '280,320p' brain-bar/Sources/BrainBar/BrainBarServer.swiftRepository: 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 -A5Repository: 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.swiftRepository: 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 -A2Repository: 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 -10Repository: 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 -A5Repository: EtanHey/brainlayer
Length of output: 44
🏁 Script executed:
sed -n '257,302p' brain-bar/Sources/BrainBar/BrainBarServer.swiftRepository: EtanHey/brainlayer
Length of output: 2068
🏁 Script executed:
# Check for DispatchSourceRead setup in startOnQueue
sed -n '147,250p' brain-bar/Sources/BrainBar/BrainBarServer.swiftRepository: 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.
| 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() | ||
| } | ||
| }, | ||
| ] | ||
| } |
There was a problem hiding this comment.
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.
| 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" | ||
| ) | ||
| } |
There was a problem hiding this comment.
🧹 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) is0–5 min→ "Just now" ✓[0,1,0,0,0,0]→ bucket index 1 =20–25 minago → "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.
| 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) | ||
| } |
There was a problem hiding this comment.
🧹 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
BrainBarDashboardLayoutstatic 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") |
There was a problem hiding this comment.
🧹 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.
| 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") | ||
| } |
There was a problem hiding this comment.
🧩 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:
- 1: https://www.netmeister.org/blog/ipcbufs.html
- 2: https://stackoverflow.com/questions/72607240/setsockopt-throws-invalid-argument-error-on-macos
- 3: https://github.com/apple/darwin-xnu/blob/master/bsd/sys/socketvar.h
- 4: https://wiki.geant.org/PERTKB/MacOSXOSSpecific
🏁 Script executed:
# Search for the test to understand the initialize response size and any other context
cd brain-bar && find . -name "SocketIntegrationTests.swift" -type fRepository: 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 -30Repository: 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.
| 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) | ||
| } |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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." |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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." |
There was a problem hiding this comment.
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.
|
Review of commit
Preview of the local fast-follow in this worktree: explicit |
| 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 |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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 } |
There was a problem hiding this comment.
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 👍 / 👎.
| ensureCommandBarViewModel() | ||
| selectedTab = .dashboard | ||
| commandBarViewModel?.setMode(action == .capture ? .capture : .search) | ||
| commandBarViewModel?.panelDidAppear() | ||
| runtime.clearQuickActionRequest() |
There was a problem hiding this comment.
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 👍 / 👎.
| CFNotificationCenterAddObserver( | ||
| center, | ||
| Unmanaged.passUnretained(observerBox).toOpaque(), | ||
| Unmanaged.passUnretained(self).toOpaque(), | ||
| injectionStoreDarwinNotificationCallback, |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 8 total unresolved issues (including 7 from previous reviews).
❌ 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.
There was a problem hiding this comment.
💡 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".
| private var shouldShow: Bool { | ||
| guard viewModel.mode == .search else { return false } | ||
| return !viewModel.inputText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty |
There was a problem hiding this comment.
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 👍 / 👎.
| init(databasePath: String) throws { | ||
| self.database = BrainDatabase(path: databasePath) | ||
| self.observerBox = InjectionStoreObserverBox() | ||
| guard database.isOpen else { | ||
| throw BrainDatabase.DBError.notOpen | ||
| } | ||
| observerBox.store = self | ||
| } |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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 | 🟠 MajorSearch is still synchronous on the main actor for every keystroke — first-letter lag remains.
handleInputChange→submitSearch()runsQuickCaptureController.searchdirectly 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
📒 Files selected for processing (7)
brain-bar/Sources/BrainBar/BrainBarApp.swiftbrain-bar/Sources/BrainBar/BrainBarCommandBar.swiftbrain-bar/Sources/BrainBar/BrainBarWindowRootView.swiftbrain-bar/Sources/BrainBar/InjectionStore.swiftbrain-bar/Sources/BrainBar/QuickCapturePanel.swiftbrain-bar/Tests/BrainBarTests/BrainBarWindowStateTests.swiftbrain-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.swiftbrain-bar/Sources/BrainBar/QuickCapturePanel.swiftbrain-bar/Sources/BrainBar/BrainBarCommandBar.swiftbrain-bar/Sources/BrainBar/BrainBarApp.swiftbrain-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.swiftbrain-bar/Sources/BrainBar/QuickCapturePanel.swiftbrain-bar/Sources/BrainBar/BrainBarCommandBar.swiftbrain-bar/Sources/BrainBar/BrainBarApp.swiftbrain-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.swiftbrain-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.onTapGestureis attached first.SwiftUI resolves stacked tap gestures in attachment order, so
onSelectfires on the first click of a double-click andonActivatenever wins. Either reorder (count: 2first) 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 betweendiscoverMenuBarWindow(400×300) andisMenuBarExtraWindow(760×560).During the brief window the MenuBarExtra is being sized up,
discoverMenuBarWindowcan latchdiscoveredMenuBarWindowto a frame that thedidBecomeKeyobserver (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:BrainBarMenuBarLabelstill allocates a freshNSImageinbodyon everycollector.$statspublish.
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+onReceiveif you ever see it in Instruments.brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift (1)
615-679: WindowAttachmentView still re-resolves on every update, andattach(window:)still churns observers.
updateNSViewunconditionally dispatchesonResolve(window), andattach(window:)unconditionally callsremoveObservers()+ re-addsdidMove/didEndLiveResizeeven whenpreparedWindowNumber == window.windowNumber. The alpha-fade path is guarded byneedsPreparation, but the NotificationCenter churn is not. Short-circuit insideattachwhen the window number matches and observers are already wired (or move resolve intomakeNSViewonly).♻️ 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 {@mainactorin … }pattern capturesstorestrongly for the Task's duration, and the unconditionalCFNotificationCenterRemoveObserverindeinit(with the nicely justified comment) closes the "callback fires after owner release" gap that the old weak-box dance could still drop through. Movingdatabase.close()intostop()also aligns the socket/DB close pair with theapplicationWillTerminatepath.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 theperformKeyEquivalentpath that the old view-model-only tests couldn't reach. Asserting the "must NOT consume plain Return" branch is the key one — it protects theNSTextFieldDelegate.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.
scheduleFeedbackAutoClearcorrectly cancels the priorfeedbackResetTask, re-checks.successafter the sleep (so an intervening.errorisn't clobbered back to idle), and theisSearchOverlayDismissedflag is cleanly reset on bothsetModeandhandleInputChange. InjectablefeedbackAutoClearDelayis 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,
preferredMenuBarItemFramepicking 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.
| if launchMode == .menuBarWindow { | ||
| UserDefaults.standard.removeObject(forKey: Self.menuBarWindowAutosaveKey) | ||
| installMenuBarWindowObservers() | ||
| startMenuBarWindowSync() | ||
| } |
There was a problem hiding this comment.
🧹 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.
| private var keyboardHint: String { | ||
| switch viewModel.mode { | ||
| case .capture: return "⏎ Store · ⌘⏎ Store · ⇥ Search" | ||
| case .search: return "⏎ Copy · ⌘⏎ Capture · ⇥ Capture" | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 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>
There was a problem hiding this comment.
💡 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".
| if let preferredFrame = BrainBarWindowPlacement.preferredMenuBarItemFrame( | ||
| candidates: fallbackMatches.map(\.frame), | ||
| mouseLocation: mouseLocation | ||
| ) { | ||
| return fallbackMatches.first(where: { $0.frame == preferredFrame })?.element |
There was a problem hiding this comment.
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 👍 / 👎.
| let clamped = max(ratePerMinute, 0) | ||
| if clamped.rounded(.towardZero) == clamped { | ||
| return "\(Int(clamped))/min" | ||
| } |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (4)
brain-bar/Sources/BrainBar/BrainBarCommandBar.swift (2)
461-462:⚠️ Potential issue | 🟡 MinorTap-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 andonActivate(copy-to-clipboard) never fires from a double-click. Either swap the order so thecount: 2modifier is applied first, or compose explicitly withTapGesture(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 | 🟡 MinorCapture-mode legend still advertises Cmd+Return as a distinct action.
⏎ Store · ⌘⏎ Storeshows two separate shortcuts, but in.capturemodehandleInputReturn([])andhandleInputReturn([.command])both funnel intosubmitCapture(...)and produce the exact same user-visible outcome (store + clear). Collapse to something honest like⏎ Store · ⇥ Searchso 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:)andcoordinator.attach(window:)both own window placement.
configurecomputesBrainBarWindowPlacement.resolvedFrame(...)and callswindow.setFrame(...), and immediately afterwardscoordinator.attach(window:)(line 641) resolves against the sameBrainBarWindowFrameStoreand applies again. It's idempotent but it's two layout passes per attach and the "who places the window" responsibility is split. Letconfigurehandle 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.updateNSViewre-resolves the window on every SwiftUI update.
updateNSViewunconditionally dispatchesonResolve(window)→BrainBarWindowObserver.attach(window:)→removeObservers()+ re-addObserverfor bothdidMoveanddidEndLiveResize, andcoordinator.attach(window:)which applies the persisted frame again. ThepreparedWindowNumbercheck only prevents the alpha flicker; it does not prevent notification-center churn or the duplicatesetFrame. Guardattach(window:)to no-op when the window is already prepared and observers are live, or skip the update-time dispatch and resolve once inmakeNSView(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
📒 Files selected for processing (4)
brain-bar/Sources/BrainBar/BrainBarCommandBar.swiftbrain-bar/Sources/BrainBar/BrainBarWindowRootView.swiftbrain-bar/Sources/BrainBar/QuickCapturePanel.swiftbrain-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.swiftbrain-bar/Sources/BrainBar/BrainBarCommandBar.swiftbrain-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.swiftbrain-bar/Sources/BrainBar/BrainBarCommandBar.swiftbrain-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)
| private var heroSparkline: NSImage { | ||
| SparklineRenderer.render( | ||
| state: collector.state, | ||
| values: collector.stats.recentEnrichmentBuckets, | ||
| size: NSSize(width: 360, height: 160), | ||
| accentColor: livePresentation.accentColor | ||
| ) | ||
| } |
There was a problem hiding this comment.
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.
| 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." | ||
| ) | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
| gesture.onDoubleTap = { [weak self] in | ||
| self?.searchPanel?.show() | ||
| self?.runtime.handleToggleRequest() | ||
| } |
There was a problem hiding this comment.
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 👍 / 👎.


Summary
MenuBarExtra(.window)implementation while keepingBRAINBAR_LEGACY=1as the local escape hatchF4as a BrainBar visibility togglePlan Reference
docs.local/plans/2026-04-17-brainbar-linear-ux/README.mdPivot Note
MenuBarExtra(.window)instead ofNSStatusItem+NSPopoverBefore / After
Before
After
Verification
swift test --filter BrainBarbash brain-bar/build-app.sh/tmp/brainbar-inline-dashboard-search.pngNotes
cr review --plainwas 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 inBrainBarServerthat could affect client reliability under load.Overview
BrainBar now defaults to a SwiftUI
MenuBarExtra(.window)surface driven by a newBrainBarRuntime, withBRAINBAR_LEGACY=1preserving the priorNSStatusItem/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 fromBrainBarLivePresentation. 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 inBrainDatabase/DashboardMetricFormatter(and removal ofPipelineActivityTracks), plus safer lifecycle handling forInjectionStore’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
MenuBarExtra(.window)launch mode alongside a legacy status item mode, selected via theBRAINBAR_LEGACYenvironment variable; BrainBarApp.swift wires the new path end-to-end.@MainActorcoordinator that routes toggle/search/capture actions to the correct surface and publishes database/collector state.DashboardMetricFormattergainsindexingString,activitySummaryString, andlastCompletionStringhelpers; all rate display now uses/minunits only.BrainBarServer.sendResponseis rewritten as a synchronous retry loop (up to 10 × 1 ms EAGAIN retries) that disconnects stalled clients immediately instead of queuing partial writes.NSWindowand 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
Improvements
Tests