Skip to content

feat: V1 signal bars side-by-side (horizontal)#522

Merged
EtanHey merged 5 commits into
mainfrom
feat/brainbar-v1-sidebyside
Jun 19, 2026
Merged

feat: V1 signal bars side-by-side (horizontal)#522
EtanHey merged 5 commits into
mainfrom
feat/brainbar-v1-sidebyside

Conversation

@EtanHey

@EtanHey EtanHey commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Etan's iterative fix on V1 (#520): lay the 3 signal bars (Vector/FTS5/Trigram) in a horizontal row instead of stacked vertically (matches the V1.png mockup). 11-line layout change (VStack→HStack), behavior/health-colors/progressive-disclosure unchanged. NSHostingView-verified at full/compact + vector-detail; 62 DashboardTests pass.


Note

Medium Risk
Changes add SQL source filters and heuristic vector ETA math that could misread backlog if rates are noisy; scope is mostly dashboard read paths and UI with new tests.

Overview
This PR extends the BrainBar V1 dashboard with layout, styling, and richer metrics—not only the horizontal signal bars from the title.

Pipeline & signal coverage UI: The pipeline panel puts writes and signal coverage above the enrichments/queue row (queue moves beside enrichments instead of full width below). Signal coverage gets a section header, ViewThatFits horizontal/vertical columns for Vector/FTS5/Trigram, dedicated signal colors, animated coverage bars (with reduce-motion support), staggered expand animations, and Vector detail nested under the Vector column. Card styling and typography are tightened across flow lanes and overview stats.

Data & writes chart: DashboardStats now buckets writes by source into Agent stores vs JSONL watcher time series, plus computed vector net drain and backlog ETA from enrichment vs write rates (replacing hardcoded Vector detail placeholders). The ingress sparkline plots dual series with a legend, dimmed inactive sources, and a “Listening for writes…” empty state; rendering moves from Swift Charts to a custom multi-series path in SparklineRenderer.

Tests: New dashboard/sparkline tests cover source splitting, palette labels, vector ETA math, and legend/caption behavior.

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

Note

Add horizontal signal bars, multi-series sparklines, and animated coverage bars to the Brain Bar dashboard

  • Rewrites SparklineChart from Swift Charts to a custom GeometryReader/ZStack renderer supporting up to three labeled series with per-series markers, hover tooltips, and a 'Listening for writes...' caption when no activity is present.
  • Splits the ingress write lane into agent and watcher series using new recentAgentWriteBuckets / recentWatcherWriteBuckets stats, computed in BrainDatabase.swift with source-filtered SQL queries and hourly rate/backlog ETA derived values.
  • Replaces static coverage bars in BrainBarSignalCoverageRow with a new BrainBarAnimatedCoverageBar that spring-animates fill on appear and percent changes, respecting accessibilityReduceMotion.
  • Adds staggered row reveal animations and chevron rotation to BrainBarSignalCoveragePanel, and wires BrainBarVectorSignalDetail to live drain rate and ETA from dashboard stats.
  • Adds BrainBarSeriesLegend for multi-series charts and introduces new design tokens for signal and series colors in DesignTokens.swift.
  • Behavioral Change: SparklineChart no longer uses Swift Charts; hover and animation behavior are now fully custom.
📊 Macroscope summarized f1902db. 5 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

Summary by CodeRabbit

  • New Features

    • Added multi-series visualization for write activity, distinguishing agent and watcher sources with separate colors.
    • Enhanced signal coverage panel with animated, accessibility-aware reveal behavior.
    • Introduced vector drain rate and estimated backlog hours metrics to dashboard statistics.
    • Replaced static progress indicator with animated coverage bar in signal maintenance view.
  • Tests

    • Added coverage for write-source splitting and sparkline presentation logic.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Dashboard write activity is split into agent-originating and watcher-originating time-series buckets in BrainDatabase, with new rate and vector backlog ETA metrics. These buckets flow through DashboardFlowLane multi-series fields into a rewritten SparklineRenderer that draws up to three series via custom Shape rendering. BrainBarSignalCoveragePanel gains animated coverage bars and staggered accessibility-aware signal reveal.

Changes

Multi-series write split and signal coverage refactor

Layer / File(s) Summary
BrainDatabase: agent/watcher write buckets and rate/ETA metrics
brain-bar/Sources/BrainBar/BrainDatabase.swift
Two SQL where-clauses split chunk sources into agent vs watcher write buckets. DashboardStats gains stored bucket arrays, recentAgentWriteCount/recentWatcherWriteCount, recentWriteRatePerHour, vectorNetDrainRatePerHour, vectorBacklogETAHours, and a recentWriteBuckets(whereClause:) helper. withPendingStoreFlushRate forwards the new arrays.
Design tokens and PipelineState multi-series fields
brain-bar/Sources/BrainBar/DesignTokens.swift, brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift
New NSColor and SwiftUI Color tokens for signal/series styling (including dimmed variants) are added. DashboardFlowLane acquires primarySeriesLabel, secondary/tertiary values/labels/accents. The Writes lane is wired to recentAgentWriteBuckets/recentWatcherWriteBuckets; the Enrichments lane clears the new fields.
SparklineRenderer: multi-series chart with role-based drawing
brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift
SparklineLegendEntry and SparklineSeriesRole are introduced. SparklineChartPresentation stores secondary/tertiary arrays and builds role-based legend, visibility, caption, and tooltip logic. SparklineChart is rewritten with custom SparklineSeriesPathShape drawing, per-role color/stroke, emphasized point markers, hover tooltips, and reduce-motion gating.
Signal coverage UI: animated bars, staggered reveal, drain/ETA metrics
brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift (lines 694–1073)
BrainBarSignalCoverage gains optional vectorNetDrainRatePerHour/vectorBacklogETAHours fields. BrainBarSignalCoveragePanel adds reduceMotion tracking and staggered revealedSignalIDs insertions. The trigram indicator switches to BrainBarAnimatedCoverageBar. BrainBarSignalCoverageRow and the new bar gain spring-animated displayedPercent. BrainBarVectorSignalDetail computes isFalling/drainText/etaText with "n/a" fallbacks.
Dashboard layout, sparkline wiring, legend, and typography
brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift (lines 491–507, 1109–1759)
pipelinePanel branches on layout.chartColumns == 2. BrainBarHeroSparkline gains secondary/tertiary series label and accent properties wired into SparklineChartPresentation. A new BrainBarSeriesLegend view supports up to three legend items with accessibility labeling. BrainBarFlowLaneCard, BrainBarLaneMetric, BrainBarOverviewStat, and BrainBarDashboardCardStyle receive typography and configurable corner-radius/shadow/overlay updates.
Tests: write-bucket split, series labels, ETA, and sparkline legend
brain-bar/Tests/BrainBarTests/DashboardTests.swift
New tests assert agent/watcher bucket counts from fixture writes, DashboardFlowSummary series label and accent color correctness, DashboardStats vectorNetDrainRatePerHour/vectorBacklogETAHours computation, and sparkline presentation legend visibility and listening-caption behavior.

Sequence Diagram

sequenceDiagram
  participant BrainDatabase
  participant DashboardStats
  participant DashboardFlowLane
  participant SparklineChartPresentation
  participant SparklineChart

  BrainDatabase->>BrainDatabase: recentWriteBuckets(agentWriteSourceWhereClause)
  BrainDatabase->>BrainDatabase: recentWriteBuckets(watcherWriteSourceWhereClause)
  BrainDatabase->>DashboardStats: init(recentAgentWriteBuckets, recentWatcherWriteBuckets)
  DashboardStats-->>DashboardStats: vectorNetDrainRatePerHour, vectorBacklogETAHours
  DashboardStats->>DashboardFlowLane: Writes lane(secondaryValues: agentBuckets, tertiaryValues: watcherBuckets)
  DashboardFlowLane->>SparklineChartPresentation: init(secondaryValues, tertiaryValues, labels, accents)
  SparklineChartPresentation-->>SparklineChart: enabledRoles, legend entries, tooltipText
  SparklineChart->>SparklineChart: draw SparklineSeriesPathShape per role
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • EtanHey/brainlayer#514: Extends the same BrainBarSignalCoveragePanel/BrainBarSignalCoverage* components in BrainBarWindowRootView.swift and adds per-signal coverage/backlog fields to BrainDatabase dashboard stats, directly overlapping with this PR's signal coverage UI and data model changes.
  • EtanHey/brainlayer#520: Heavily refactors BrainBarSignalCoveragePanel and BrainBarVectorSignalDetail in BrainBarWindowRootView.swift, introducing progressive disclosure and vector detail metrics that this PR extends with animated bars and live drain/ETA data.
  • EtanHey/brainlayer#321: Modifies SparklineChartPresentation and SparklineChart in SparklineRenderer.swift with reduce-motion gating and rendering changes that this PR builds on for multi-series support.

Poem

🐇 Hoppity-hop through the data streams I go,
Splitting agent writes from watchers all in a row.
Three sparkline series dance with spring-animated flair,
Signals revealed one by one — accessibility-aware!
Drain rates, ETA hours, n/a when unknown,
The dashboard blooms bright — from a seed that was sown.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat: V1 signal bars side-by-side (horizontal)' clearly and concisely describes the main change: restructuring the dashboard layout to display three signal bars horizontally instead of vertically, which is the primary objective throughout all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

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

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

❤️ Share

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

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit f1902db. Configure here.

for (index, signal) in signals.enumerated() {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.06) {
withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
_ = revealedSignalIDs.insert(signal.id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Uncancelled reveal stagger callbacks

Medium Severity

updateRevealedSignals schedules per-signal updates with DispatchQueue.main.asyncAfter but never cancels them or checks isExpanded when they run. Collapsing and re-expanding quickly can leave stale callbacks that insert into revealedSignalIDs during a new stagger, so signal bars may pop in early or out of sequence.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f1902db. Configure here.

withAnimation(.easeOut(duration: 0.6)) {
lineRevealProgress = 1
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Line reveal state unused

Low Severity

SparklineChart animates lineRevealProgress in onAppear, but chartBody never reads that state, so the intended line-draw reveal never affects rendering and the chart appears at full length immediately.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f1902db. Configure here.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f1902dbf3e

ℹ️ 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".

now: now
),
values: stats.recentActivityBuckets,
values: stats.recentAgentWriteBuckets,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve all write sources in the ingress sparkline

When recent writes come only from sources outside the two new split buckets (for example Quick Capture writes use source: "quick-capture" in QuickCaptureController.swift), recentWriteCount still drives the card status/rate but the plotted primary/secondary arrays are both zero, so the Writes card can say there are recent writes while the chart shows “Listening for writes...”. Please keep an “other/all writes” series or include every counted write source in one of these buckets.

Useful? React with 👍 / 👎.

Comment on lines +203 to +208
var vectorNetDrainRatePerHour: Double {
// Vector rows do not currently carry per-vector timestamps. Until
// they do, use the real dashboard window: recent enrichment
// completions minus recent writes, which is the net backlog drain
// signal available in DashboardStats.
max(recentEnrichmentRatePerHour - recentWriteRatePerHour, 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Base vector ETA on vector progress, not enrichment

In environments where enrichment is completing but embedding/vector indexing is stuck or disabled, this reports a positive Vector drain rate even though vectorIndexedChunkCount from chunk_vectors_rowids has not moved, causing the Vector detail panel to show “falling” and a finite ETA for a backlog that is not actually draining. Please derive this from vector-index deltas or leave it unavailable until vector progress is measured.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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/Dashboard/SparklineRenderer.swift (1)

304-365: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

lineRevealProgress state is animated but never read.

The lineRevealProgress state is initialized, animated in onAppear, but never consumed in the view body or chartBody. This appears to be dead code from a planned but incomplete line-reveal animation feature.

🧹 Remove unused state if line reveal animation is not needed
-    `@State` private var lineRevealProgress: CGFloat = 1
...
-        .onAppear {
-            if reduceMotion {
-                lineRevealProgress = 1
-            } else {
-                lineRevealProgress = 0
-                withAnimation(.easeOut(duration: 0.6)) {
-                    lineRevealProgress = 1
-                }
-            }
-        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift` around lines
304 - 365, The lineRevealProgress state variable declared in SparklineRenderer
is initialized and animated in the onAppear block but is never actually used in
the view body or chartBody, making it dead code. Remove the `@State` private var
lineRevealProgress declaration and delete the animation logic in the onAppear
block that sets lineRevealProgress to 0 and animates it to 1, keeping only the
essential appearance setup if any other logic remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift`:
- Around line 847-864: The updateRevealedSignals function uses
DispatchQueue.main.asyncAfter to stagger animations, but these pending
dispatches are not cancelled if isExpanded toggles quickly or the view
disappears, causing unexpected state mutations. Replace the
DispatchQueue.main.asyncAfter approach with a Task-based implementation that
stores a reference to the task as a property, cancels any pending task before
starting a new staggered reveal sequence, and checks for task cancellation
within the animation closure to prevent modifications to revealedSignalIDs when
the task has been cancelled.

In `@brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift`:
- Around line 496-521: The computed property chartForegroundStyleScale in
SparklineRenderer is no longer used because the chart now uses custom
SparklineSeriesPathShape drawing instead of Swift Charts' built-in styling.
Remove the entire chartForegroundStyleScale computed property definition from
the class, including all its logic for building the KeyValuePairs with different
color combinations based on whether secondary and tertiary series are present.

---

Outside diff comments:
In `@brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift`:
- Around line 304-365: The lineRevealProgress state variable declared in
SparklineRenderer is initialized and animated in the onAppear block but is never
actually used in the view body or chartBody, making it dead code. Remove the
`@State` private var lineRevealProgress declaration and delete the animation logic
in the onAppear block that sets lineRevealProgress to 0 and animates it to 1,
keeping only the essential appearance setup if any other logic remains.
🪄 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: 3dae0122-369b-478a-95fc-f2fe73b133e2

📥 Commits

Reviewing files that changed from the base of the PR and between ecedfa0 and f1902db.

📒 Files selected for processing (6)
  • brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift
  • brain-bar/Sources/BrainBar/BrainDatabase.swift
  • brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift
  • brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift
  • brain-bar/Sources/BrainBar/DesignTokens.swift
  • brain-bar/Tests/BrainBarTests/DashboardTests.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). (4)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.12)
  • GitHub Check: Macroscope - Correctness Check
🧰 Additional context used
🧠 Learnings (2)
📚 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/DesignTokens.swift
  • brain-bar/Sources/BrainBar/BrainDatabase.swift
  • brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift
  • brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift
  • brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift
📚 Learning: 2026-03-29T18:45:40.988Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 133
File: brain-bar/Sources/BrainBar/BrainDatabase.swift:0-0
Timestamp: 2026-03-29T18:45:40.988Z
Learning: In the BrainBar module’s Swift database layer (notably BrainDatabase.swift), ensure that the `search()` function’s `unreadOnly=true` path orders results by the delivery frontier cursor so the watermark `maxRowID` stays contiguous. Specifically, when `unreadOnly` is enabled, the query must include `ORDER BY c.rowid ASC` (e.g., via `let orderByClause = unreadOnly ? "c.rowid ASC" : "f.rank"`). Do not replace the unread-only ordering with relevance-based sorting (e.g., `f.rank`) unconditionally or for the unread-only path, as it can introduce gaps in the watermark and incorrectly mark unseen rows as delivered. Flag any future change to the `ORDER BY` clause in this function that makes relevance sorting apply to the unread-only case.

Applied to files:

  • brain-bar/Sources/BrainBar/DesignTokens.swift
  • brain-bar/Sources/BrainBar/BrainDatabase.swift
  • brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift
🪛 SwiftLint (0.63.3)
brain-bar/Sources/BrainBar/BrainDatabase.swift

[Warning] 127-127: Prefer empty collection over optional collection

(discouraged_optional_collection)


[Warning] 128-128: Prefer empty collection over optional collection

(discouraged_optional_collection)

🔇 Additional comments (36)
brain-bar/Sources/BrainBar/BrainDatabase.swift (1)

32-33: LGTM!

Also applies to: 100-101, 127-128, 153-154, 177-183, 194-214, 268-269, 1688-1699, 1716-1717, 1906-1921

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

31-38: LGTM!

Also applies to: 172-178

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

197-203: LGTM!

Also applies to: 235-236, 333-343, 396-403

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

245-279: LGTM!


281-324: LGTM!


326-344: LGTM!


786-816: LGTM!

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

14-23: LGTM!


25-59: LGTM!


71-94: LGTM!


95-113: LGTM!


115-119: LGTM!


121-131: LGTM!


133-149: LGTM!


151-153: LGTM!


155-200: LGTM!


233-250: LGTM!


298-321: LGTM!


367-427: LGTM!


429-465: LGTM!


523-543: LGTM!


564-592: LGTM!

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

491-505: LGTM!


694-698: LGTM!


708-845: LGTM!


875-876: LGTM!


897-925: LGTM!


928-970: LGTM!


993-993: LGTM!

Also applies to: 1004-1050


1071-1074: LGTM!


1091-1138: LGTM!


1275-1329: LGTM!


1476-1485: LGTM!


1544-1546: LGTM!


1667-1696: LGTM!


1724-1759: LGTM!

Comment on lines +847 to 864
private func updateRevealedSignals(animated: Bool) {
guard isExpanded else {
revealedSignalIDs.removeAll()
return
}
if reduceMotion || !animated {
revealedSignalIDs = Set(signals.map(\.id))
return
}
revealedSignalIDs.removeAll()
for (index, signal) in signals.enumerated() {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.06) {
withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
_ = revealedSignalIDs.insert(signal.id)
}
} label: {
BrainBarSignalCoverageRow(
signal: signal,
compact: compact,
isSelected: isVectorDetailExpanded
)
}
.buttonStyle(.plain)
.help("Show Vector backlog details")
} else {
BrainBarSignalCoverageRow(signal: signal, compact: compact, isSelected: false)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Staggered reveal dispatches are not cancelled on view disappear or rapid re-toggle.

If isExpanded toggles quickly or the view disappears while the staggered asyncAfter dispatches are pending, the closures will still execute and modify revealedSignalIDs. This could cause unexpected state mutations or animation glitches.

Consider using a Task-based approach with cancellation, or add a flag to track view presence.

🛡️ Proposed fix using task cancellation
+    `@State` private var revealTask: Task<Void, Never>?
+
     private func updateRevealedSignals(animated: Bool) {
+        revealTask?.cancel()
         guard isExpanded else {
             revealedSignalIDs.removeAll()
             return
         }
         if reduceMotion || !animated {
             revealedSignalIDs = Set(signals.map(\.id))
             return
         }
         revealedSignalIDs.removeAll()
-        for (index, signal) in signals.enumerated() {
-            DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.06) {
-                withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
-                    _ = revealedSignalIDs.insert(signal.id)
+        revealTask = Task { `@MainActor` in
+            for (index, signal) in signals.enumerated() {
+                guard !Task.isCancelled else { return }
+                try? await Task.sleep(nanoseconds: UInt64(index) * 60_000_000)
+                guard !Task.isCancelled else { return }
+                withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
+                    _ = revealedSignalIDs.insert(signal.id)
                 }
             }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift` around lines 847 -
864, The updateRevealedSignals function uses DispatchQueue.main.asyncAfter to
stagger animations, but these pending dispatches are not cancelled if isExpanded
toggles quickly or the view disappears, causing unexpected state mutations.
Replace the DispatchQueue.main.asyncAfter approach with a Task-based
implementation that stores a reference to the task as a property, cancels any
pending task before starting a new staggered reveal sequence, and checks for
task cancellation within the animation closure to prevent modifications to
revealedSignalIDs when the task has been cancelled.

Comment on lines +496 to +521
private var chartForegroundStyleScale: KeyValuePairs<String, Color> {
let primaryColor = color(for: .primary)
let secondaryColor = color(for: .secondary)
let tertiaryColor = color(for: .tertiary)

if presentation.hasSecondarySeries && presentation.hasTertiarySeries {
return [
presentation.primaryLegendLabel: primaryColor,
presentation.secondaryLegendLabel: secondaryColor,
presentation.tertiaryLegendLabel: tertiaryColor,
]
}
if presentation.hasSecondarySeries {
return [
presentation.primaryLegendLabel: primaryColor,
presentation.secondaryLegendLabel: secondaryColor,
]
}
if presentation.hasTertiarySeries {
return [
presentation.primaryLegendLabel: primaryColor,
presentation.tertiaryLegendLabel: tertiaryColor,
]
}
return [presentation.primaryLegendLabel: primaryColor]
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for usages of chartForegroundStyleScale
rg -n "chartForegroundStyleScale" brain-bar/

Repository: EtanHey/brainlayer

Length of output: 201


Remove unused chartForegroundStyleScale computed property.

This property builds a KeyValuePairs for Swift Charts' foregroundStyle scale, but the chart now uses custom SparklineSeriesPathShape drawing and the property is never referenced in the codebase.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift` around lines
496 - 521, The computed property chartForegroundStyleScale in SparklineRenderer
is no longer used because the chart now uses custom SparklineSeriesPathShape
drawing instead of Swift Charts' built-in styling. Remove the entire
chartForegroundStyleScale computed property definition from the class, including
all its logic for building the KeyValuePairs with different color combinations
based on whether secondary and tertiary series are present.

Comment on lines +240 to +248
var seriesComponents = ["\(primarySeriesLabel ?? "Primary") \(primaryValue)"]
if let secondarySeriesLabel, hasSecondarySeries {
let value = secondaryValues.indices.contains(clampedBucket) ? secondaryValues[clampedBucket] : 0
seriesComponents.append("\(secondarySeriesLabel) \(value)")
}
if let tertiarySeriesLabel, hasTertiarySeries {
let value = tertiaryValues.indices.contains(clampedBucket) ? tertiaryValues[clampedBucket] : 0
seriesComponents.append("\(tertiarySeriesLabel) \(value)")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Low Dashboard/SparklineRenderer.swift:240

tooltipText(forBucket:) silently drops the secondary and tertiary series from the tooltip when secondarySeriesLabel or tertiarySeriesLabel is nil, even though the corresponding values arrays are non-empty. The if let secondarySeriesLabel guard requires a non-nil label, but legendEntries adds those same series based only on hasSecondarySeries/hasTertiarySeries and uses the fallback properties secondaryLegendLabel/tertiaryLegendLabel. The primary series in this same function already uses primarySeriesLabel ?? "Primary" as a fallback. Replace the if let bindings with if hasSecondarySeries { ... secondaryLegendLabel ... } and if hasTertiarySeries { ... tertiaryLegendLabel ... } for consistency.

-        var seriesComponents = ["\(primarySeriesLabel ?? "Primary") \(primaryValue)"]
-        if let secondarySeriesLabel, hasSecondarySeries {
+        var seriesComponents = ["\(primaryLegendLabel) \(primaryValue)"]
+        if hasSecondarySeries {
             let value = secondaryValues.indices.contains(clampedBucket) ? secondaryValues[clampedBucket] : 0
-            seriesComponents.append("\(secondarySeriesLabel) \(value)")
+            seriesComponents.append("\(secondaryLegendLabel) \(value)")
         }
-        if let tertiarySeriesLabel, hasTertiarySeries {
+        if hasTertiarySeries {
             let value = tertiaryValues.indices.contains(clampedBucket) ? tertiaryValues[clampedBucket] : 0
-            seriesComponents.append("\(tertiarySeriesLabel) \(value)")
+            seriesComponents.append("\(tertiaryLegendLabel) \(value)")
         }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift around lines 240-248:

`tooltipText(forBucket:)` silently drops the secondary and tertiary series from the tooltip when `secondarySeriesLabel` or `tertiarySeriesLabel` is `nil`, even though the corresponding values arrays are non-empty. The `if let secondarySeriesLabel` guard requires a non-nil label, but `legendEntries` adds those same series based only on `hasSecondarySeries`/`hasTertiarySeries` and uses the fallback properties `secondaryLegendLabel`/`tertiaryLegendLabel`. The primary series in this same function already uses `primarySeriesLabel ?? "Primary"` as a fallback. Replace the `if let` bindings with `if hasSecondarySeries { ... secondaryLegendLabel ... }` and `if hasTertiarySeries { ... tertiaryLegendLabel ... }` for consistency.

Evidence trail:
brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift lines 30-32 (optional labels), lines 83-89 (hasSecondarySeries/hasTertiarySeries), lines 95-105 (legendEntries uses hasSecondarySeries with fallback labels), lines 121-131 (primaryLegendLabel/secondaryLegendLabel/tertiaryLegendLabel with ?? fallbacks), lines 233-249 (tooltipText uses `if let secondarySeriesLabel` guard but primary uses `?? "Primary"` fallback).

@EtanHey EtanHey merged commit 98296e2 into main Jun 19, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant