Commit e8fa626
feat: BrainBar → MenuBarExtra(.window) + live-state sparklines + full-width hero (Linear UX overhaul part 1) (#248)
* feat: rewrite BrainBar as a MenuBarExtra window
* docs: add BrainBar before-after screenshots
* feat(brainbar): integrated command bar replaces header Capture/Search 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>
* feat(brainbar): command-bar fast-follow — confirmation, Cmd+Enter, scroll-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>
* fix(brainbar): command-bar polish — honest hint, URL-action bridge, click-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>
* fix(brainbar): scope results overlay to Dashboard + dismiss on tab-switch + 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>
* fix(brainbar): re-run search on return to search mode + fade-only overlay 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>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent ec0c12b commit e8fa626
30 files changed
Lines changed: 3414 additions & 1257 deletions
File tree
- brain-bar
- Sources/BrainBar
- Dashboard
- Formatting
- Tests/BrainBarTests
- docs.local/plans/2026-04-17-brainbar-linear-ux/artifacts
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
10 | 9 | | |
11 | 10 | | |
12 | 11 | | |
| |||
67 | 66 | | |
68 | 67 | | |
69 | 68 | | |
70 | | - | |
71 | | - | |
72 | | - | |
73 | | - | |
74 | | - | |
75 | | - | |
76 | | - | |
77 | 69 | | |
78 | 70 | | |
79 | 71 | | |
| |||
84 | 76 | | |
85 | 77 | | |
86 | 78 | | |
87 | | - | |
88 | | - | |
89 | | - | |
90 | | - | |
91 | | - | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
92 | 82 | | |
93 | 83 | | |
94 | 84 | | |
| |||
109 | 99 | | |
110 | 100 | | |
111 | 101 | | |
112 | | - | |
| 102 | + | |
113 | 103 | | |
114 | 104 | | |
115 | 105 | | |
116 | 106 | | |
117 | 107 | | |
118 | 108 | | |
119 | 109 | | |
120 | | - | |
121 | | - | |
122 | 110 | | |
123 | 111 | | |
124 | 112 | | |
| |||
338 | 326 | | |
339 | 327 | | |
340 | 328 | | |
341 | | - | |
342 | | - | |
343 | | - | |
344 | | - | |
345 | | - | |
346 | | - | |
| 329 | + | |
347 | 330 | | |
348 | 331 | | |
349 | 332 | | |
| |||
355 | 338 | | |
356 | 339 | | |
357 | 340 | | |
358 | | - | |
359 | | - | |
360 | | - | |
361 | | - | |
362 | | - | |
363 | | - | |
364 | | - | |
365 | | - | |
366 | | - | |
367 | | - | |
368 | | - | |
369 | | - | |
370 | | - | |
371 | | - | |
372 | | - | |
373 | | - | |
374 | | - | |
375 | | - | |
376 | | - | |
377 | | - | |
378 | | - | |
379 | | - | |
380 | | - | |
381 | | - | |
382 | | - | |
383 | | - | |
384 | | - | |
385 | | - | |
386 | | - | |
387 | | - | |
388 | | - | |
389 | | - | |
390 | | - | |
391 | | - | |
392 | | - | |
393 | | - | |
394 | | - | |
395 | | - | |
396 | | - | |
397 | | - | |
398 | | - | |
399 | | - | |
400 | | - | |
401 | | - | |
402 | | - | |
403 | | - | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
404 | 356 | | |
405 | | - | |
406 | | - | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
407 | 360 | | |
408 | | - | |
409 | | - | |
410 | | - | |
411 | | - | |
412 | | - | |
413 | | - | |
414 | | - | |
415 | | - | |
416 | | - | |
417 | | - | |
418 | | - | |
419 | | - | |
420 | | - | |
421 | | - | |
422 | | - | |
423 | | - | |
424 | | - | |
425 | | - | |
426 | | - | |
427 | | - | |
428 | | - | |
429 | | - | |
430 | | - | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
431 | 368 | | |
432 | | - | |
433 | | - | |
434 | | - | |
435 | | - | |
436 | | - | |
437 | | - | |
438 | | - | |
439 | | - | |
440 | | - | |
441 | | - | |
442 | | - | |
443 | | - | |
444 | | - | |
445 | | - | |
| 369 | + | |
446 | 370 | | |
447 | 371 | | |
448 | 372 | | |
| |||
669 | 593 | | |
670 | 594 | | |
671 | 595 | | |
672 | | - | |
673 | | - | |
674 | | - | |
675 | | - | |
| 596 | + | |
676 | 597 | | |
677 | | - | |
678 | | - | |
| 598 | + | |
| 599 | + | |
679 | 600 | | |
680 | 601 | | |
681 | 602 | | |
| |||
0 commit comments