Skip to content

feat: branch filtering, activity rollups, and usage grouping#1017

Open
wesm wants to merge 32 commits into
mainfrom
feat/branch-filtering
Open

feat: branch filtering, activity rollups, and usage grouping#1017
wesm wants to merge 32 commits into
mainfrom
feat/branch-filtering

Conversation

@wesm

@wesm wesm commented Jul 7, 2026

Copy link
Copy Markdown
Member

Combines and supersedes #974, #978, and #979 (branch filtering, activity
rollups, and usage grouping), rebased onto current main with the original
commits and authorship preserved, plus integration fixes and review-driven
refinements. Part of #928.

What the branch does

  • Session surfaces (feat(filter): expose branch filters in client surfaces #974): --branch on session list/session search/activity report (requires --project), git_branch on the MCP
    tools, and a sidebar Branch multi-select with search, chips, badge, and URL
    round-trip that flows into Analytics and Usage filtering. Branch metadata
    sorts by most recent session activity in all three backends.
  • Activity (feat(activity): group activity by branch #978): by_branch rollups keyed by typed project/branch
    fields across SQLite/PostgreSQL/DuckDB, a By branch CLI section, and a
    branch typeahead plus Branch breakdown panel on the Activity page.
  • Usage (feat(usage): group usage by branch and make branches excludable #979): range-wide branchTotals and per-day branchBreakdowns,
    a Branch group-by for the cost chart and Attribution panel, and branch
    exclusion (exclude_git_branch) persisted in URL and localStorage.

Integration with current main

Rebasing over the kit-ui migration (#957), semantic search, --since, the
usage schema fields (#989), and exclude-agent filters required merge
resolutions in the CLI flag surfaces, MCP tools, activity report struct, usage
summary structs, and the Activity/Usage Svelte components. Golden fixtures
were regenerated for the new fields, the FilterDropdown tests were ported to
the kit-ui DOM, and the new branch message keys were added to the ko locale
(added on main after these PRs were cut).

Review-driven refinements

Frontend performance was the review focus:

  • The sidebar filter dropdown no longer re-sorts the uncapped branch list on
    every search keystroke: ranking and search filtering are separate deriveds,
    and selection membership is hoisted into Sets instead of splitting CSV
    strings inside sort comparators.
  • The attribution treemap caps at the top 40 tiles by cost (beyond that tiles
    are sub-pixel); the scrollable rail and list views remain complete so every
    branch stays clickable.
  • Activity Breakdowns tokenizes branch rows once per report instead of on
    every metric toggle.

Correctness fixes:

  • The usage store's branch options are invalidated on sync completion
    (matching the sessions and activity stores), so newly imported branches
    appear in the exclude dropdown without a reload.
  • The Activity branch typeahead is scoped to the active project filter, and a
    project change clears a branch scoped to another project — previously that
    contradiction silently produced an all-zero report.
  • The MCP get_usage_summary tool rejects a comma-separated project list
    combined with git_branch instead of silently returning zeros.
  • docs/session-api.md again documents the git_branch HTTP param as the
    opaque token from GET /api/v1/branches (the tables had been rewritten with
    CLI flag semantics).

Deduplication, preserving backend parity by construction: db.UsageBucket/
db.AddUsageBucket, db.SortBranchBreakdowns, and
UsageFilter.RequiresSessionScope() replace byte-identical copies in the
SQLite/PostgreSQL/DuckDB usage paths, and the sessions/usage stores share one
toggleListValue helper.

Known limitations and notes for reviewers

  • Branch filters travel as an uncapped token list, matching the existing
    CSV-style filters (carried over from feat(filter): expose branch filters in client surfaces #974).
  • Excluding a branch drops all Cursor-imported usage events from totals, not
    just that branch's share. This mirrors the pre-existing exclude_project
    behavior, is tested, and is consistent across backends — but the semantics
    are debatable for an exclude filter and worth a product call.
  • Branch breakdowns are computed and serialized on every breakdown-mode usage
    summary fetch, mirroring the existing bundle-all-breakdowns design that
    keeps the group-by toggle refetch-free; payload grows with per-day branch
    churn.
  • Pre-existing, surfaced during dedupe: the cursor bail-out's termination
    sub-condition differs subtly between SQLite and PG/DuckDB for unrecognized
    termination values; each backend's current behavior was preserved.

Reviewers should look at the merge-resolution surfaces (cmd/agentsview CLI
flags, internal/mcp/tools.go, internal/activity/activity.go, the usage summary
structs) and the shared helpers in internal/db.

🤖 Generated with Claude Code

@roborev-ci

roborev-ci Bot commented Jul 7, 2026

Copy link
Copy Markdown

roborev: Combined Review (8a0c66b)

Branch filtering looks mostly solid, but there are two medium-severity parity/UI gaps to fix before merge.

Medium

  • frontend/src/lib/components/filters/SessionFilterControl.svelte:104
    The branch dropdown offers every branch even when a project filter is active. Because the backend ANDs the project filter with opaque (project, branch) branch tokens, selecting a branch from another project creates a contradictory filter and returns an empty view.
    Fix: Scope visible branch options to the active project, and clear or disable selected branch tokens whose project no longer matches.

  • cmd/agentsview/usage.go:557
    The daemon path for dailyUsageQuery serializes project, machine, model, and other filters but drops GitBranch and the new ExcludeGitBranch. The local backend passes those fields through, so branch-filtered daily usage can return different results depending on whether a daemon is used.
    Fix: Add git_branch and exclude_git_branch query params in fetchHTTPDailyUsage and cover the serialization in the HTTP daily usage tests.


Reviewers: 2 done | Synthesis: codex, 8s | Total: 12m13s

@roborev-ci

roborev-ci Bot commented Jul 7, 2026

Copy link
Copy Markdown

roborev: Combined Review (d1653e1)

Summary verdict: One medium issue should be addressed before merging.

Medium

  • frontend/src/lib/stores/activity.svelte.ts:237
    The new Activity branch filter loads options from /api/v1/branches, but that endpoint is backed by GetBranches, which filters out subagent and fork sessions. Activity rollups include both subagents and forks, and usage totals include fork usage, so branch rollups can contain branches that never appear in the dropdown. In Activity they cannot be selected or excluded, and in Usage a fork-only branch can become impossible to re-enable individually after it is hidden.

    Suggested fix: Add a branch metadata mode or endpoint for Activity/Usage that includes the same relationship scope as those reports, while keeping the existing root-only list for the sidebar.


Reviewers: 2 done | Synthesis: codex, 7s | Total: 13m13s

@roborev-ci

roborev-ci Bot commented Jul 7, 2026

Copy link
Copy Markdown

roborev: Combined Review (d316550)

Summary verdict: One medium-severity issue should be addressed before merge.

Medium

  • frontend/src/lib/components/filters/SessionFilterControl.svelte:96
    • Problem: The sidebar branch dropdown ranks and shows all branches even when a project filter is active. Users can select a branch token from another project, silently combining contradictory predicates and producing an empty session list.
    • Fix: Scope visible branch options to sessions.filters.project when set, and clear or reject incompatible selected branch tokens when project state changes.

Reviewers: 2 done | Synthesis: codex, 6s | Total: 15m56s

@roborev-ci

roborev-ci Bot commented Jul 8, 2026

Copy link
Copy Markdown

roborev: Combined Review (480664b)

No blocking security issues found, but one medium backend correctness issue should be fixed before merge.

Medium

  • internal/duckdb/store.go:636 - GetBranches calls s.duck.QueryContext directly, bypassing the store’s queryContext wrapper used for DuckDB remote/quack connections. In that mode, the branch metadata query can run against the wrong connection path and fail instead of using the attached remote query mechanism.
    • Fix: Use s.queryContext(ctx, ...), matching GetMachines and the other DuckDB store query methods.

Reviewers: 2 done | Synthesis: codex, 6s | Total: 17m18s

@roborev-ci

roborev-ci Bot commented Jul 8, 2026

Copy link
Copy Markdown

roborev: Combined Review (5bcdfc8)

Summary verdict: one medium issue remains; no high or critical findings were reported.

Medium

  • frontend/src/lib/stores/sessions.svelte.ts:711: The sidebar loads branch metadata with scope: "all", so it can offer branches that exist only on subagent/fork sessions. The sidebar session query still applies git_branch predicates to root candidates, then includes descendants unfiltered, so selecting a sub-session-only branch can produce an empty sidebar even though the option was offered.
    • Fix: Keep the sidebar branch list scoped to root sessions, or update sidebar root selection to include roots whose descendant tree contains a matching branch while still returning the full matched tree for display.

Reviewers: 2 done | Synthesis: codex, 7s | Total: 15m3s

@wesm

wesm commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

This branch needs more work (I have over 1000 branches in my session database, and there are performance sharp edges) so I'm going to ship 0.37.0 and then get this merged when I can

@roborev-ci

roborev-ci Bot commented Jul 9, 2026

Copy link
Copy Markdown

roborev: Combined Review (b13fce9)

Summary verdict: Two medium issues remain; no high or critical findings were reported.

Medium

  • Location: frontend/src/lib/stores/sessions.svelte.ts:710
    Problem: The sidebar requests branch options with scope: "all", so branches that exist only on subagent/fork sessions are offered, but the sidebar session query still applies git_branch only to root candidates. Selecting one of those child-only branch options can return an empty sidebar even though the dropdown offered it as valid.
    Fix: Either request root-scoped branches for the sidebar, or update sidebar-index filtering across backends to match roots whose descendants have the selected branch.

  • Location: frontend/src/lib/stores/usage.svelte.ts:338
    Problem: When the sidebar branch filter and usage page branch selection do not overlap, effectiveGitBranch silently falls back to the sidebar branch, so the usage page can show a selected branch that is not actually applied to the API request.
    Fix: Make the effective filter reflect the true intersection, or clear/scope the local usage branch selection when it conflicts with the sidebar filter.


Reviewers: 2 done | Synthesis: codex, 8s | Total: 13m53s

@mariusvniekerk
mariusvniekerk force-pushed the feat/branch-filtering branch from b13fce9 to 2682399 Compare July 10, 2026 02:57
@roborev-ci

roborev-ci Bot commented Jul 10, 2026

Copy link
Copy Markdown

roborev: Combined Review (2682399)

Changes requested: two medium-severity branch filtering issues can produce misleading usage results.

Medium

  • internal/db/usage.go:932 — Cursor usage is assigned the synthetic pair ("", ""), making it appear as a clickable “no branch” attribution. Selecting it filters out Cursor rows and may show unrelated session-backed usage with empty project and branch. Represent Cursor usage as explicitly unattributed and non-filterable, or add a special filter token handled consistently across SQLite, PostgreSQL, and DuckDB. Add a cursor-only branch attribution test.

  • frontend/src/lib/stores/usage.svelte.ts:328 — When sidebar and usage-local branch selections do not overlap, effectiveGitBranch ignores the usage-local selection while both controls remain visibly active. Use the true intersection and show no results, or clear/reconcile incompatible selections.


Reviewers: 2 done | Synthesis: codex, 7s | Total: 5m43s

@roborev-ci

roborev-ci Bot commented Jul 10, 2026

Copy link
Copy Markdown

roborev: Combined Review (52a78ed)

The changes are generally sound, but one medium-severity UI behavior mismatch should be fixed.

Medium

  • Misleading branch-row actionfrontend/src/lib/components/usage/AttributionPanel.svelte:103
    The panel says “click to hide,” but clicking a branch uses the new include-based toggleBranch, filtering to only that branch and hiding all others—the opposite of the advertised action. Update the copy and behavior to accurately represent drill-down/selection, or maintain separate exclusion state for attribution-row clicks.

Reviewers: 2 done | Synthesis: codex, 7s | Total: 4m12s

@roborev-ci

roborev-ci Bot commented Jul 12, 2026

Copy link
Copy Markdown

roborev: Combined Review (34b3e7f)

No issues found.


Reviewers: 2 done | Synthesis: codex | Total: 3m39s

@mariusvniekerk
mariusvniekerk force-pushed the feat/branch-filtering branch from 34b3e7f to 117e099 Compare July 12, 2026 19:30
@roborev-ci

roborev-ci Bot commented Jul 12, 2026

Copy link
Copy Markdown

roborev: Combined Review (117e099)

Medium-severity issues remain in branch attribution and sync-time scalability.

Medium

  • Arbitrary branch attribution for deduplicated usageinternal/db/usage.go:1846-1883, internal/activity/activity.go:750-760
    Branch costs are assigned after global first-seen usage deduplication. When resumed or forked sessions contain the same deduplication key on different branches, lexicographic session-ID ordering can charge shared historical usage to an arbitrary branch. This can make activity and usage totals disagree with the branch where usage originated. Define a branch-aware survivor policy—such as preferring the original/root session—and apply it consistently across SQLite, PostgreSQL, DuckDB, and activity aggregation. Add a parent/fork fixture covering duplicate usage keys across branches.

  • Archive-wide branch refresh after every syncinternal/db/sessions.go:2654-2673, frontend/src/lib/stores/activity.svelte.ts:485-487, frontend/src/lib/stores/sessions.svelte.ts:1213-1231
    Once branch controls are loaded, every sync completion reruns GetBranches, grouping and sorting all qualifying sessions by activity. Updates to one active session therefore trigger work proportional to the entire archive. Refresh metadata only when affected sessions introduce or change a (project, branch) pair, or maintain an incremental branch catalog. At minimum, debounce and lazily invalidate the query, with a regression test comparing small and large archives.


Reviewers: 2 done | Synthesis: codex, 11s | Total: 4m48s

@mariusvniekerk
mariusvniekerk force-pushed the feat/branch-filtering branch from 117e099 to aa5196a Compare July 12, 2026 19:57
@roborev-ci

roborev-ci Bot commented Jul 12, 2026

Copy link
Copy Markdown

roborev: Combined Review (aa5196a)

Changes look sound overall, but the Usage branch filter can create conflicting project/branch selections.

Medium

  • frontend/src/lib/components/usage/UsagePage.svelte:59 — The branch dropdown includes branches from all projects even when a project filter is active. Selecting or restoring a branch from another project applies incompatible predicates and produces an unexplained empty report. The persisted selectedGitBranch also survives project changes. Scope branch options to sessions.filters.project and reconcile selected branch tokens when the project changes, retaining only branches belonging to that project or clearing the selection.

Reviewers: 2 done | Synthesis: codex, 6s | Total: 6m43s

@roborev-ci

roborev-ci Bot commented Jul 12, 2026

Copy link
Copy Markdown

roborev: Combined Review (7ce4693)

The PR has one medium-severity issue: project filtering can conflict with branch selection.

Medium

  • frontend/src/lib/components/usage/UsagePage.svelte:59 — The branch dropdown includes branches from every project even when a project filter is active. Selecting another project’s branch sends contradictory project and git_branch predicates, unexpectedly producing an empty report. Scope branch options to sessions.filters.project, and clear or reconcile selected branch tokens when the project changes.

Reviewers: 2 done | Synthesis: codex, 6s | Total: 4m52s

@roborev-ci

roborev-ci Bot commented Jul 12, 2026

Copy link
Copy Markdown

roborev: Combined Review (73adffb)

Medium-severity performance issue found in branch dropdown rendering.

Medium

  • Eager rendering of all branch rowsfrontend/src/lib/components/filters/SessionFilterControl.svelte:450, frontend/src/lib/components/usage/FilterDropdown.svelte:127

    Both dropdowns render every branch eagerly. Archives containing thousands of branches will create thousands of buttons when opened; scrolling limits panel height but does not constrain DOM creation or reactive work.

    Suggested fix: Virtualize the rows, or initially render a bounded recent subset with search or on-demand expansion.


Reviewers: 2 done | Synthesis: codex, 6s | Total: 5m31s

@roborev-ci

roborev-ci Bot commented Jul 14, 2026

Copy link
Copy Markdown

roborev: Combined Review (b4a6c0e)

The PR needs changes: two medium-severity issues can merge distinct projects and misrepresent branch-attributed usage.

Medium

  • Opaque project identity missing from branch rollupsinternal/db/usage.go:1610, internal/activity/activity.go:194

    Sanitization converts path-based project labels to "". As a result, distinct projects on the same branch can be merged in usage totals, collide as frontend keys, and generate unusable empty-project filter tokens.

    Fix: Propagate project_key through branch breakdowns and totals. Use it for aggregation, rendering identity, and filter resolution while retaining the sanitized label for display.

  • Branch attribution excludes non-session usagefrontend/src/lib/components/usage/AttributionPanel.svelte:54

    Cursor events and other non-session usage are omitted, and percentages are calculated only from branch-attributed cost. Cursor-only periods therefore show “no data,” while mixed periods can misleadingly show a branch as 100% of usage.

    Fix: Add a non-filterable “Unattributed” row for the difference between total cost and branch totals, and calculate percentages against the complete total.


Reviewers: 2 done | Synthesis: codex, 10s | Total: 7m26s

@mariusvniekerk
mariusvniekerk force-pushed the feat/branch-filtering branch from b4a6c0e to 04adf03 Compare July 15, 2026 01:31
@roborev-ci

roborev-ci Bot commented Jul 15, 2026

Copy link
Copy Markdown

roborev: Combined Review (04adf03)

Code review identified two medium-severity branch-filter identity and reconciliation issues; no critical or high-severity findings.

Medium

  • internal/db/usage.go:1674 — Branch rows sanitize Project without retaining an opaque identity. Absolute-path projects become "", causing foldBranchTotals to merge distinct projects that share a branch. AttributionPanel.svelte:56 then constructs filters such as "\x1fmain", which match only an empty project. Activity branch rows can likewise lose identity and produce duplicate keyed rows.

    • Fix: Carry a stable project key and opaque branch-filter identity alongside the display label. Use these fields for aggregation, frontend keys, and filtering.
  • frontend/src/lib/branchFilters.ts:59 — Legacy selection reconciliation keeps only the first project-qualified token for a shared branch name. For example, selecting another branch while both project-a/main and project-b/main are active silently removes project-b/main.

    • Fix: Preserve every current token whose normalized branch remains selected, or deliberately migrate the entire equivalence class to the plain branch-name representation.

Reviewers: 2 done | Synthesis: codex, 13s | Total: 8m36s

@mariusvniekerk
mariusvniekerk force-pushed the feat/branch-filtering branch from 04adf03 to 63ca296 Compare July 19, 2026 15:01
@roborev-ci

roborev-ci Bot commented Jul 19, 2026

Copy link
Copy Markdown

roborev: Combined Review (7cfd1c7)

Review found three medium-severity issues involving branch identity, API compatibility, and unattributed usage costs.

Medium

  • internal/db/usage.go:1650, internal/activity/activity.go:196 — Branch rollups lack an opaque project key. Sanitization blanks path- or URL-shaped project labels, so distinct projects on the same branch can collide in frontend identities, merge in foldBranchTotals, or create duplicate Svelte keys. Clicking these rows creates a token scoped to an empty project and returns no matching data. Carry a pre-sanitization project_key or opaque filter token through branch breakdowns and totals, use it for frontend keys, and resolve it to the raw project when filtering.

  • internal/server/huma_routes_metadata.go:50/api/v1/branches replaces the existing response contract with deduplicated {branch} entries, removing project and token. This breaks released clients expecting scoped tokens, while the documentation still directs callers to use the removed token. Preserve the existing endpoint and add a separate bounded branch-name search endpoint, or version the breaking response and update documentation and clients together.

  • frontend/src/lib/components/usage/AttributionPanel.svelte:54 — Branch attribution uses branchTotals, which excludes cursor usage, and calculates percentages against that partial subtotal. Branches may therefore appear to represent 100% of cost while a substantial portion of totals.totalCost remains unattributed. Add a non-filterable “Unattributed” row for totalCost - sum(branchTotals.cost) and include it in the percentage calculation.


Reviewers: 2 done | Synthesis: codex, 13s | Total: 12m34s

@roborev-ci

roborev-ci Bot commented Jul 20, 2026

Copy link
Copy Markdown

roborev: Combined Review (d34459c)

Branch attribution has three medium-severity correctness issues involving lost project identity and incomplete cost visualization.

Medium

  • Stable project identity is discarded

    • Location: internal/db/usage.go:1712, internal/activity/activity.go:108
    • Absolute-path projects become empty display labels, causing same-named branches to merge, duplicate frontend keys, and attribution links that cannot match sessions.
    • Fix: Carry an opaque project_key or branch token alongside the display label and use it for aggregation, keyed rendering, and filtering.
  • Cursor usage is omitted from branch attribution

    • Location: frontend/src/lib/components/usage/AttributionPanel.svelte:54
    • Cursor-only ranges incorrectly show “No data,” while mixed ranges calculate percentages against an incomplete subtotal.
    • Fix: Add a non-filterable “Unattributed” row for the difference from summary.totals.totalCost, and calculate percentages against the complete total.
  • Treemap truncation misrepresents omitted costs

    • Location: frontend/src/lib/components/usage/AttributionPanel.svelte:95
    • Limiting the treemap to 40 rows causes the visible subset to fill the chart, making omitted branches appear to contribute no cost.
    • Fix: Aggregate omitted entries into an “Other” tile or reserve proportional area for them.

Reviewers: 2 done | Synthesis: codex, 10s | Total: 10m4s

@mariusvniekerk
mariusvniekerk force-pushed the feat/branch-filtering branch from d34459c to c69d89b Compare July 20, 2026 20:49
@roborev-ci

roborev-ci Bot commented Jul 20, 2026

Copy link
Copy Markdown

roborev: Combined Review (c69d89b)

The PR needs changes: three medium-severity issues affect branch identity, API compatibility, and usage-page scalability.

Medium

  • Branch identities collide across projectsinternal/db/usage.go:1712, internal/activity/activity.go:108
    Branch rollups sanitize the project label without retaining an opaque project key. Absolute-path and URL projects sanitize to "", causing same-named branches from different projects to merge in usage totals, duplicate Activity UI keys, and produce empty-project filter tokens that cannot match the original session.
    Fix: Carry a project_key or opaque compound branch identity through branch breakdowns and use it for aggregation, UI keys, and filter resolution. Use the sanitized project string only for display.

  • Existing branches API contract is brokeninternal/server/huma_routes_metadata.go:50, docs/session-api.md:123
    /api/v1/branches changes from unbounded (project, branch, token) rows to at most 100 deduplicated branch names. Existing consumers lose required fields and cannot enumerate the full catalog, while the documentation still directs callers to retrieve opaque tokens from this endpoint.
    Fix: Preserve the existing response by default and introduce a separate/versioned search endpoint or opt-in bounded mode. Update the documentation and generated client accordingly.

  • Default usage summaries process and render an unbounded branch dimensioninternal/db/usage.go:1990, frontend/src/lib/components/usage/AttributionPanel.svelte:225
    Because breakdowns defaults to true, every usage-summary request partitions, aggregates, and serializes all branches even when grouping by project, model, or agent. Although the treemap is capped, the side rail and list still render every branch, so routine usage-page work scales poorly for archives with thousands of branches.
    Fix: Request branch breakdowns only when branch grouping is selected, or load them lazily, and virtualize or bound the rendered branch list.


Reviewers: 2 done | Synthesis: codex, 12s | Total: 12m53s

@roborev-ci

roborev-ci Bot commented Jul 21, 2026

Copy link
Copy Markdown

roborev: Combined Review (2228d33)

Branch filtering is generally sound, but four medium-severity correctness and performance issues should be addressed.

Medium

  • frontend/src/lib/stores/usage.svelte.ts:749 — The lazy branch-breakdown refresh replaces this.summary with a response lacking comparison, despite preserveRelatedState, and aborts any in-flight comparison without restarting it. Selecting Branch therefore removes the “vs prior” metric until another full refresh. Preserve and merge the existing comparison into the branch-rich response, or refetch it when unavailable; add a regression test.

  • frontend/src/lib/branchFilters.ts:57-104 — Branch reconciliation and intersection collapse project-qualified tokens by branch name. Retaining both project-a/main and project-b/main, or intersecting plain main with both qualified filters, keeps only the first and silently excludes valid project/branch pairs. Preserve all compatible qualified tokens and deduplicate only exact token identities; add multi-project, same-branch tests for both helpers.

  • internal/db/usage.go:2037, internal/db/usage.go:2377, internal/postgres/usage.go:1395, internal/postgres/usage.go:1710 — With breakdowns=false&branch_breakdowns=true, SQLite and PostgreSQL take the breakdown path without populating agent breakdowns. CopilotAICredits is then calculated from empty agent slices and incorrectly becomes zero. Calculate AI credits directly from the accumulator, consistent with the fast and DuckDB paths, and test branch-only requests on both supported backends.

  • frontend/src/lib/stores/usage.svelte.ts:319baseParams() enables branch breakdowns for every Usage endpoint while Branch grouping is active. Full refreshes make prior-period comparison and pairwise queries build large branch maps that their responses discard, multiplying work on branch-heavy archives. Enable branchBreakdowns only for summary requests and defensively clear it from comparison and pairwise filters before calling GetDailyUsage.


Reviewers: 2 done | Synthesis: codex, 13s | Total: 14m49s

Exposes the scoped (project, branch) filter everywhere sessions are
browsed: --branch flags on session list, session search, and activity
report (rejected without --project, since a branch name only identifies
work within a project); git_branch parameters on the MCP tools; and a
Branch section in the web sidebar filter with multi-select, search,
selected entries floated to the top, removable chips, a numeric
active-filter badge on the filter button, and URL round-trip. The
sidebar selection also drives the Analytics dashboard, and the usage
endpoints receive the same session-scoped branch filter.

Branch filters travel as opaque (project, branch) tokens because the
shared filter field carries multiple pairs and project or branch names
can contain commas; CLI and MCP accept two plain parameters and encode
internally, so callers never see the token format. Branch metadata is
ordered by most recent session activity so pickers surface current
work first, and cmd/testfixture seeds git branches so e2e suites and
screenshots exercise the branch surfaces with synthetic data.
wesm and others added 27 commits July 21, 2026 23:35
The runner image's packages.microsoft.com repos intermittently fail
clearsign validation and abort apt-get update; the docs job only needs
ripgrep from the Ubuntu archive.
GET /api/v1/branches gains a scope param: roots (default) keeps the
sidebar's root-only list, all also counts subagent and fork sessions.
Activity rollups and usage totals include those sessions, so their
branch controls previously omitted branches the breakdowns could
surface - in Usage a fork-only branch hidden via a chart click had no
unhide row. The Activity typeahead and Usage exclude dropdown now
request scope=all across SQLite, PostgreSQL, and DuckDB.
Sibling worktrees share the default golangci-lint cache, and cached
suggested-fixes resolve through worktree-relative paths, so a run with
--fix can apply byte offsets computed against another checkout's version
of a file and corrupt it in place. Pointing GOLANGCI_LINT_CACHE at a
gitignored directory inside the checkout removes the shared state.
With a project filter pinned, the sidebar branch dropdown offered every
project's branches; selecting a foreign one combined contradictory
predicates and emptied the session list. Branch options now scope to the
active project, matching the Activity page. Selecting a project already
clears any selected branch via setProjectFilter's filter reset.
The sidebar branch dropdown loaded /api/v1/branches with the default
roots scope, hiding branches that exist only on subagent or fork
sessions (common with worktree subagents). The sidebar index matches
those sessions against the branch filter, so the options list now
requests scope=all like the Activity and Usage pages. Automated-only
branches still appear only when automated sessions are included.
The db slow-write diagnostics (InsertMessages and friends) fire on
slow CI runners while a benchmark's result line is half-printed,
splicing into it and corrupting the capture; GetDailyUsage lost 4 of
6 samples that way, which benchgate treats as fatal. Slow-op logs are
now suppressed under go test via testing.Testing().

benchgate also treated baseline corruption as exit 2, but the
merge-base run executes code a PR cannot fix, and a benchmark
corrupted only in the baseline still parses on the candidate side,
where it takes the missing-baseline not-gated path. Baseline
corruption now warns instead of failing; candidate corruption stays
fatal.
Deselect-all in the usage branch dropdown excludes every known
(project, branch) pair; BranchPairPredicate joined those as a flat OR
chain, which parses left-deep and exceeds SQLite's 1000-deep
expression cap, so usage summary and top-sessions returned 500
(Expression tree is too large). The predicate now nests as a balanced
OR tree, keeping parse depth logarithmic with identical semantics on
all backends.

The kit-ui dropdown panel also had no height cap, so the uncapped
branch list grew past the viewport with no way to scroll; the app
wrapper now clamps the panel and scrolls it until a cap lands
upstream in kit-ui.
The branch dropdown was exclusion-based, so focusing on one branch
encoded as excluding every other known (project, branch) pair — a
predicate that grows with the branch catalog rather than the
selection, and at real catalog sizes produced enormous queries and
URLs. The dropdown now uses the include mode the Model filter already
uses: empty selection means all branches, and picking branches sends
a git_branch include list sized by the selection.

The sidebar branch filter and the local selection share one API
param, so they compose by intersection; a persisted local selection
with no overlap defers to the sidebar filter. The usage URL key is
branch (session params own git_branch when the two merge), and saved
exclude-mode state resets to all branches since an exclusion set has
no include equivalent. The include-mode trigger label now resolves
the item's display label instead of echoing the raw token.
Cursor usage has no session-backed project or branch, but the usage breakdown treated its empty pair as a selectable branch attribution. That made a Cursor-only cost row look filterable while the filter itself cannot recover those rows. Keep Cursor costs in totals/model/agent attribution while leaving branch attribution to session-backed usage across SQLite, PostgreSQL, and DuckDB.

When the sidebar branch filter and usage-local branch selection have no overlap, both controls are visibly active. Send a fail-closed branch token for that empty intersection so usage charts reflect the active filters instead of falling back to sidebar-only results.
The attribution panel advertised every row click as "click to hide",
but branch and model clicks feed include-based selections
(selectedGitBranch/selectedModels), so clicking filters the dashboard
to the clicked item — the opposite of hiding it. Only project and
agent clicks are exclusions, so the hint, row tooltips, and treemap
aria labels now describe the actual action per grouping, and a
selected row's copy flips to "clear the filter" since include-mode
rows stay visible while filtered.

Treemap no longer hard-codes the hide copy; the caller passes the
tooltip/aria formatters, per the shared-component localization rule
in DESIGN.md.
Rebasing onto main's session-evidence work (#1075) crossed two
refactors: main added toggleProjectKey calling the store's toggleCsv
helper, while this branch had already replaced toggleCsv with the
shared toggleListValue utility. The merged tip kept main's call to
the deleted method, so excluding a project by key threw at click
time. toggleProjectKey now uses toggleListValue like every other
toggle, and the buildUsageUrlParams test literal gains the
excludedProjectKeys field main's UsageUrlState requires.
…copy

The cost time series fell back to a single day-total series whenever
the active grouping had no breakdowns, so in branch grouping a
Cursor-only archive rendered its unattributable cost as a series while
the attribution panel correctly reported no branch data. Branch mode
now shows the empty state instead, keeping both branch views telling
the same story; other groupings keep the total fallback.

The include-mode attribution hint promised only filtering, but once a
row is selected its click clears that filter, so the hint flips to
neutral add-or-remove copy in all locales. Coverage extends to model
mode, treemap tile titles/aria labels in both selection states, and a
SQLite contract test that a separator-less git_branch token (the
frontend's disjoint-filter sentinel) fails closed to zero rows rather
than broadening or erroring.
The usage page's branch dropdown offered every project's branches
even with a sidebar project filter pinned, so picking another
project's branch ANDed contradictory project and git_branch
predicates into an empty report. Branch options now scope to the
active project filter, mirroring the sidebar dropdown, and the query
path drops off-project tokens from the stored selection at request
time rather than mutating it — a selection made before pinning a
project defers to the project filter and takes effect again when the
filter clears.
The branch-filter work predated the French catalog added on main, so rebasing left the new locale without 19 user-facing keys and broke the catalog parity test. Add native French copy for the branch picker, active-filter, activity, and usage messages so every configured locale exposes the same interface.
The branch picker duplicated input chrome that kit-ui already owns, causing the frontend policy check to reject the PR and letting this control drift from the rest of the app. Adopt the shared search input while retaining the picker's remote multi-select behavior, and pass localized clear-action copy through its reusable component contract.
Branch attribution added git_branch to every daily-usage row, including totals-only requests that discard it. Scanning that unused text column across large histories pushed GetDailyUsage allocation volume past the benchmark gate. Select branch and machine dimensions only for breakdown requests in both SQLite and PostgreSQL, matching DuckDB's existing query shape.
The remaining hosted review findings span private project identity, usage-summary loading, and large-list rendering. Record the agreed in-place design and its non-goals before changing those cross-backend and frontend contracts.
The remaining same-head findings cross Activity identity, three usage backends, generated API contracts, store refresh behavior, and high-cardinality rendering. Record the approved task boundaries and test-first sequence so those changes preserve backend parity and can be reviewed as focused commits.
Sanitized private project labels can collapse distinct branch rows onto the same display text. Carry the opaque project key alongside that safe label so downstream branch selection and keyed rendering can remain exact without exposing raw paths.
Private project labels can sanitize to the same display value, which made range totals and branch filters collapse distinct repositories. Carry the opaque project key through daily and range-wide rows, and resolve key-qualified tokens back to raw labels only inside the service boundary.
Most Usage views need project, model, agent, and machine attribution but never consume per-branch rows. Separate branch scan columns, accumulator keys, and folds behind an explicit request flag so ordinary summaries avoid cardinality work while all three storage backends keep matching behavior.
Branch display labels are intentionally sanitized and can collide, so they cannot serve as stable chart, list, or filter identities. Expose the opaque key through the server/OpenAPI seam and use it only for identity while keeping safe project and branch labels visible.
Branch attribution can contain thousands of rows but is irrelevant to the default Usage grouping. Request it only for an active Branch view, retain the rich response across grouping switches, and coalesce rapid repeated selections without disturbing related panel state.
Branch attribution remains intentionally uncapped, so eager DOM rendering grows with archive cardinality even though only a viewport-sized slice is visible. Reuse the existing TanStack wrapper for both scrolling row surfaces while preserving the treemap cap and row interactions.
The branches endpoint now returns bounded branch-name search results rather than opaque project-qualified tokens. Document the actual unshipped contract so clients use search, project scope, limit, and has_more correctly without inventing a compatibility path.
Activity JSON now exposes opaque branch project keys, while ordinary daily usage breakdowns intentionally omit branch rows unless separately requested. Keep the public golden contracts aligned with those reviewed response semantics.
@mariusvniekerk
mariusvniekerk force-pushed the feat/branch-filtering branch from 2228d33 to bf872a3 Compare July 22, 2026 03:35
@roborev-ci

roborev-ci Bot commented Jul 22, 2026

Copy link
Copy Markdown

roborev: Combined Review (bf872a3)

Review found three medium-severity correctness issues involving branch-filter identity, unattributed costs, and backend parity.

Medium

  • frontend/src/lib/stores/usage.svelte.ts:351, 537 — Exact (project_key, branch) selections are not preserved. Project scoping compares the opaque project key with the raw project label and drops the branch filter, while selection logic normalizes qualified tokens to branch names, conflating same-named branches from different projects. Keep project-key-qualified tokens exact, restrict normalization to plain picker values, and add coverage for active project filters and duplicate branch names across projects.

  • frontend/src/lib/components/usage/AttributionPanel.svelte:55 — Branch attribution renders only branchTotals, although cursor usage is excluded from branch breakdowns. Normalizing percentages over that partial sum makes the panel misleadingly account for 100% of total cost. Add a non-filterable “Unattributed” remainder calculated from summary.totals.totalCost - sum(branchTotals).

  • internal/db/usage.go:2473; internal/postgres/usage.go:1834 — With branch_breakdowns=true&breakdowns=false, SQLite and PostgreSQL calculate Copilot AI credits from empty agent breakdowns and return zero, while DuckDB uses the accumulator correctly. Calculate AI credits directly from accumulated agent/cost keys in the branch-breakdown path to preserve independent flags and backend parity.


Reviewers: 2 done | Synthesis: codex, 10s | Total: 10m41s

@roborev-ci

roborev-ci Bot commented Jul 24, 2026

Copy link
Copy Markdown

roborev: Combined Review (bf872a3)

Medium-severity issues remain in branch usage loading, credit accounting, and attribution display.

Medium

  • Race-prone lazy branch loadingfrontend/src/lib/stores/usage.svelte.ts:818
    Selecting Branch while the initial summary is loading returns immediately and is never retried, leaving branch views empty until refresh. When a summary already exists, enrichment via fetchSummary aborts in-flight comparison and pairwise requests without restarting them because loadComparison is false.
    Fix: Queue branch enrichment until initial loading completes and merge it through a dedicated request that does not invalidate unrelated panels, or explicitly restart aborted requests.

  • Incorrect Copilot AI credits in branch-only modeinternal/db/usage.go:2473, internal/postgres/usage.go:1834
    With breakdowns=false&branch_breakdowns=true, SQLite and PostgreSQL calculate credits from empty AgentBreakdowns, yielding zero despite nonzero Copilot usage. DuckDB calculates directly from the accumulator, breaking backend parity.
    Fix: Calculate credits from the agent-bearing accumulator independently of emitted breakdown rows, and add a branch-only regression test.

  • Branch attribution hides unattributed costfrontend/src/lib/components/usage/AttributionPanel.svelte:55
    Cursor or otherwise unattributable cost is omitted, so the panel can show “No data” despite real cost or display visible branches as 100% while hiding part of the total.
    Fix: Add an “Unattributed” row for summary.totals.totalCost - sum(branchTotals.cost) and include it in percentages, treemap, rail, and list rendering.


Reviewers: 2 done | Synthesis: codex, 11s | Total: 7m52s

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants