Skip to content

fix(linear): surface truncation when a deduplicated status exceeds the state-id cap - #17342

Merged
nwparker merged 5 commits into
mainfrom
neil/sta-5983-linear-status-filter-truncation-warning
Aug 30, 2026
Merged

nwparker merged 5 commits into
mainfrom
neil/sta-5983-linear-status-filter-truncation-warning

Conversation

@nwparker

@nwparker nwparker commented Aug 30, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​352 $\color{#cf222e}{\Huge{\mathbf{−}}}$​32 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​320
Prod 6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​416 $\color{#cf222e}{\Huge{\mathbf{−}}}$​156 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​260

Problem

Since #16879 the Tasks filter collapses duplicate per-team status/label rows into one row that expands to one id per team. The renderer bounds that expansion to the transport cap (LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_STATE_IDS / _MAX_LABEL_IDS, both 100) at linear-issue-attribute-filter-dropdowns.tsx:290, because the IPC/RPC parser rejects an over-cap filter outright. The bound is silent: selectedLinearMetadataGroupKeys maps any surviving id back to its group, so the row stays fully checked while state.id.in (src/main/linear/issue-list-filter.ts:49) quietly excludes issues from the teams whose ids were dropped. The cap is on the whole array, not per row, so checking a second status also shrinks the coverage of the first one — 120 teams x 2 statuses ends up 50/50 with both rows checked. Team selection defaults to every team in the workspace, so ~25 teams with 4 statuses checked is already enough to trigger it.

Fix

New LinearFacetCoverageNotice (src/renderer/src/components/linear-issue-attribute-filter-coverage-notice.tsx) re-expands the currently checked group keys and compares that count with the ids the filter actually carries. When the filter carries fewer, the status and labels section details render an inline note under the list: "Filtering 100 of 120 team statuses — issues from the remaining teams are not included." Derived from the rendered value, so it stays correct no matter which caller bounded it; reuses the existing expandLinearMetadataGroupKeys / selectedLinearMetadataGroupKeys helpers, the existing translate() pattern, and the same text-xs text-muted-foreground treatment as the neighbouring team-required message. The bound itself is unchanged — no wire change, no cap raise.

Verification

$ pnpm test src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx
 Test Files  1 passed (1)
      Tests  15 passed (15)

Confirmed the new tests fail without the fix (reverted linear-issue-attribute-filter-sections.tsx only):

 Tests  3 failed | 12 passed (15)
AssertionError: expected 'Filters1Labels:bugBackClear (1)bugCle…' to contain 'Filtering 100 of 120 team labels'

(the 4th new test is the negative control asserting no notice under the cap.)

$ pnpm tc            -> exit 0, no diagnostics
$ npx oxlint <changed files>   -> no findings
$ pnpm run check:code-quality:changed
code quality: 0 new finding(s) across 3 changed file(s).
type-aware code quality: 0 new finding(s) across 3 changed file(s).
React Doctor: 0 new finding(s) across 3 changed file(s).
$ npx oxfmt --check <changed files>  -> All matched files use the correct format.
$ pnpm test src/renderer/src/components/linear-issue-attribute-filter-team-ids.test.ts src/shared/linear/issue-attribute-filter.test.ts src/renderer/src/i18n/locale-english-regression.test.ts src/renderer/src/i18n/technical-literal-catalog-values.test.ts
 Test Files  4 passed (4)
      Tests  23 passed (23)

Not done here (noted for follow-up): filtering by state/label name instead of id would remove the cap entirely, but that changes what a paired client and host exchange and needs the docs/reference/remote-wire-compatibility.md treatment.

Fixes STA-5983
https://linear.app/stably/issue/STA-5983/continuous-scan-linear-tasks-filter-deduplication-silently-caps-same

Since #16879 one status/label row expands to an id per team, and the renderer
bounds that list to the 100-id transport cap. Any surviving id kept the row
fully checked, so the picker claimed coverage the filter never had; show how
many of the row's per-team ids are actually applied.
@nwparker
nwparker requested a review from brennanb2025 as a code owner August 30, 2026 05:38
@greptile-apps

greptile-apps Bot commented Aug 30, 2026 •

Copy link
Copy Markdown

Greptile Summary

The PR makes Linear status and label truncation visible and distributes the transport ID budget across selected metadata groups.

  • Adds inline coverage notices and partial indicators to section summaries and active filter pills.
  • Replaces canonical slicing with deterministic round-robin allocation across selected groups.
  • Adds status and label coverage tests for over-cap, exactly-cap, and fully starved-row scenarios.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/renderer/src/components/linear-issue-attribute-filter-team-ids.ts Adds deterministic group-aware ID capping and conservative coverage detection that addresses the previously silent fully truncated-group cases.
src/renderer/src/components/linear-issue-attribute-filter-coverage-notice.tsx Renders explicit shortfall or exhausted-cap messaging from the bounded filter state.
src/renderer/src/components/linear-issue-attribute-filter-dropdowns.tsx Applies group-aware capping before emitting filter changes and exposes partial coverage on active pills.
src/renderer/src/components/linear-issue-attribute-filter-sections.tsx Adds partial section summaries and coverage notices for both grouped status and label pickers.
src/renderer/src/components/linear-issue-attribute-filter-pills.ts Extracts pill helpers and derives partial coverage indicators for status and label facets.
src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx Covers partial notices, pills, section summaries, and fully starved selections at the transport cap.
src/renderer/src/components/linear-issue-attribute-filter-team-ids.test.ts Verifies deterministic round-robin allocation and conservative at-cap coverage reporting.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Selected grouped rows] --> B[Expand to per-team IDs]
  B --> C[Round-robin IDs across groups]
  C --> D[Apply 100-ID transport cap]
  D --> E[Persist bounded filter]
  E --> F[Derive selected rows and coverage]
  F --> G{Coverage incomplete or cap reached?}
  G -->|Yes| H[Show notice and partial markers]
  G -->|No| I[Show normal selected state]
Loading

Reviews (5): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

@coderabbitai

coderabbitai Bot commented Aug 30, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

Next included review available in 7 minutes.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aa93b13b-02a2-47bb-a849-ca743d2f73af

📥 Commits

Reviewing files that changed from the base of the PR and between 5843daf and 16f16f2.

📒 Files selected for processing (1)
  • src/renderer/src/i18n/locales/en.json

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d46c2b3-f70f-4467-8ff3-0ed103f593c1

📥 Commits

Reviewing files that changed from the base of the PR and between 5517de4 and 5843daf.

📒 Files selected for processing (1)
  • src/renderer/src/components/linear-issue-attribute-filter-team-ids.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/src/components/linear-issue-attribute-filter-team-ids.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Linear status and label filters now cap selected metadata IDs across teams. Coverage helpers calculate intended and applied IDs. Filter sections render localized partial-coverage notices, partial summaries, and partial pill markers. Shared status and label detail rendering handles facet-specific options and limits. Tests cover capped selections, changing coverage, full coverage, partial indicators, retained IDs, and label coverage.

Merge Risk: 🔵 Low · up to 5843d

The PR adds a truncation notice for oversized status and label selections, but at the exact ID limit it may incorrectly say that selected items were omitted, misleading users about filter coverage. This is a bounded UI correctness issue suitable for explicit owner awareness or follow-up rather than a release-blocking concern.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the problem, fix, linked issue, implementation details, and verification results. However, it does not follow the required template and omits the mandatory Visual Proof sectio… Update the description to use the repository template. Add a Visual Proof section with before-and-after screenshots or videos, or state N/A with a reason only if there is no visual or interaction change. Also add or complete the ELI5, What …
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 22 functions across 7 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the primary fix: exposing truncation when deduplicated Linear status IDs exceed the transport cap. It does not mention label coverage, but it remains specific and directly …
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.
Full details: Title check

Explanation

The title clearly describes the primary fix: exposing truncation when deduplicated Linear status IDs exceed the transport cap. It does not mention label coverage, but it remains specific and directly related to the changes.

Full details: Description check

Explanation

The description explains the problem, fix, linked issue, implementation details, and verification results. However, it does not follow the required template and omits the mandatory Visual Proof section for this UI change, along with several checklist and review sections.

Resolution

Update the description to use the repository template. Add a Visual Proof section with before-and-after screenshots or videos, or state N/A with a reason only if there is no visual or interaction change. Also add or complete the ELI5, What Changed, Why, Testing checkboxes, AI Disclosure, Review, Agent skill upstream boundary, Notes, and Checklist sections as applicable.

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/renderer/src/components/linear-issue-attribute-filter-coverage-notice.tsx (1)

26-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use named interpolation placeholders instead of value0/value1.

This file introduces intent-named localization IDs (statusPartialTeamCoverage, labelsPartialTeamCoverage), which matches the repo convention for new strings. However, the interpolation placeholders still use the generic value0/value1 names instead of descriptive ones like selectedCount/totalCount.

♻️ Proposed rename to named placeholders
-  const counts = { value0: selectedIds.length, value1: groupIdCount }
+  const counts = { selectedCount: selectedIds.length, totalCount: groupIdCount }
   return (
     <p className="border-t border-border/50 px-3 py-2 text-xs text-muted-foreground">
       {facet === 'status'
         ? translate(
             'auto.components.linear-issue-attribute-filter-coverage-notice.statusPartialTeamCoverage',
-            'Filtering {{value0}} of {{value1}} team statuses — issues from the remaining teams are not included.',
+            'Filtering {{selectedCount}} of {{totalCount}} team statuses — issues from the remaining teams are not included.',
             counts
           )
         : translate(
             'auto.components.linear-issue-attribute-filter-coverage-notice.labelsPartialTeamCoverage',
-            'Filtering {{value0}} of {{value1}} team labels — issues from the remaining teams are not included.',
+            'Filtering {{selectedCount}} of {{totalCount}} team labels — issues from the remaining teams are not included.',
             counts
           )}
     </p>
   )

Fixing this file also lets the auto-synced en.json entries pick up the named placeholders on the next catalog sync.

Based on learnings: "use intent-named localization IDs with named interpolation placeholders" for newly added user-facing strings, treating hash-based IDs/placeholders as grandfathered only.

Source: Learnings


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a978716c-3f7e-4d33-916b-57d76d594932

📥 Commits

Reviewing files that changed from the base of the PR and between 73ff003 and fd51044.

📒 Files selected for processing (4)
  • src/renderer/src/components/linear-issue-attribute-filter-coverage-notice.tsx
  • src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx
  • src/renderer/src/components/linear-issue-attribute-filter-sections.tsx
  • src/renderer/src/i18n/locales/en.json

Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes — surfaces an inline notice when a deduplicated status or label filter is silently capped by the transport id limit.

  • New LinearFacetCoverageNotice component — re-expands the checked group keys and compares the would-be full count against the actual selectedIds. Renders nothing under the cap, otherwise an inline Filtering X of Y team statuses/labels note.
  • Wired into section details — rendered under the MultiSelectList for both status and labels in LinearIssueFilterSectionDetail.
  • English copy — two new translate keys with {{value0}}/{{value1}} interpolation.
  • Tests — four new cases covering status over-cap, labels over-cap, the shrunken second-status case, and a negative control under the cap.

The fix is correct and well-scoped: the notice is derived from the already-bounded value, so it stays accurate regardless of which caller applied the cap, and the selectedLinearMetadataGroupKeys → expandLinearMetadataGroupKeys round-trip guarantees it only fires when ids were actually dropped. No wire change, no cap change, and the four tests are exact enough to fail without the fix.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

The cap sliced a lexicographically sorted id list, so a whole picked row could lose
every id — reverting to unchecked with no notice, and dropping out of the coverage
denominator that was supposed to explain it. Spread the cap across the picked rows,
and carry the partial-coverage signal to the section menu and the pill, which are
what the user reads once the detail panel is closed.
@nwparker

nwparker commented Aug 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Pushed eaf5049f595 addressing the review findings.

A05-2 — dishonest coverage denominator. Fixed at the source rather than in the display. boundLinearIssueAttributeFilter slices a lexicographically sorted id array, so a picked row whose ids all sort last lost every id: it reverted to unchecked, and because selectedLinearMetadataGroupKeys only yields a key when an id survives, it also vanished from both sides of the notice's comparison. capLinearMetadataIdsAcrossGroups now spreads the transport cap round-robin over the picked rows before bounding, so every row the user checked keeps at least one id — it stays checked, and it counts toward the denominator. That fixes total suppression and the under-reporting case with the notice still derived purely from rendered state (no dropped-count threaded out of the bound). Regression test reproduces the reported scenario (Alpha a-000..a-099 + Zeta z-000); pre-fix it failed with expected [ 'a-000', … ] to include 'z-000'.

A05-1 — truncation invisible outside the detail panel. Done, not deferred. The predicate the notice already computes is now shared (linearMetadataGroupCoverage / isLinearMetadataGroupSelectionPartial), and both post-apply surfaces consume it: the collapsed section menu shows 1 selected · partial, and the ActivePill carries a partial marker with an explanatory title. The section menu now derives its count from the grouped options instead of the name maps — same count, and it removed more lines than the flag added. To stay under the 400-line max-lines cap without a disable, the near-identical section === 'status' and section === 'labels' branches of LinearIssueFilterSectionDetail were collapsed into one.

A08-1 — duplicate test scaffolding. openStatusSection is gone; its three callers now use renderDropdowns + openSectionNamed. statusRowsNamed → pickerRowsNamed, since it queries label rows too. One mount-and-open path.

Verification: pnpm test on the two touched test files — 30 passed; broader linear sweep 86 passed. pnpm tc exit 0. npx oxlint on all 6 changed files clean. pnpm run check:code-quality:changed — 0 new findings. Localization catalog + extraction verifiers pass.

One residual, worth a follow-up ticket only if it ever shows up in practice: if a user checks more distinct status names than the 100-id cap allows, round-robin still cannot seat them all and the denominator under-reports again. That needs ~100 distinct picked status names.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/renderer/src/components/linear-issue-attribute-filter-coverage-notice.tsx (1)

20-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use semantic interpolation names in new localized strings.

Generic names such as value0 and value1 hide the meaning of each value from translators. Use names such as applied, intended, and summary. Update the matching locale entries.

  • src/renderer/src/components/linear-issue-attribute-filter-coverage-notice.tsx#L20-L20: pass applied and intended to the coverage notice translations.
  • src/renderer/src/components/linear-issue-attribute-filter-sections.tsx#L151-L156: pass summary to the partial-coverage suffix translation.

Based on learnings: newly added user-facing strings must use intent-named localization IDs with named interpolation placeholders.

Source: Learnings


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 68bbdbdb-da94-4362-93f6-5410e6aa80c1

📥 Commits

Reviewing files that changed from the base of the PR and between fd51044 and eaf5049.

📒 Files selected for processing (7)
  • src/renderer/src/components/linear-issue-attribute-filter-coverage-notice.tsx
  • src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx
  • src/renderer/src/components/linear-issue-attribute-filter-dropdowns.tsx
  • src/renderer/src/components/linear-issue-attribute-filter-sections.tsx
  • src/renderer/src/components/linear-issue-attribute-filter-team-ids.test.ts
  • src/renderer/src/components/linear-issue-attribute-filter-team-ids.ts
  • src/renderer/src/i18n/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/src/i18n/locales/en.json

Included review availability: Your plan provides up to 10 included reviews per hour; 3 remain after this review.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes — since the prior review, the picker now spreads the transport id cap across the picked rows instead of slicing the sorted id list, and flags partial coverage on both the pill and the collapsed section menu.

  • Fair cap distribution — new capLinearMetadataIdsAcrossGroups round-robins one id per picked group per pass, so a picked row whose ids all sort past the cap stays checked instead of vanishing outright.
  • Shared coverage helpers — extracted linearMetadataGroupCoverage and isLinearMetadataGroupSelectionPartial, and rewired LinearFacetCoverageNotice onto them.
  • Picked-filter path — applyPickedFilter caps state/label ids across groups before canonicalizeLinearIssueAttributeFilter + boundLinearIssueAttributeFilter.
  • Partial indicators — ActivePill renders a partial marker with a tooltip, and collapsed section-menu summaries append · partial.
  • Section merge + tests — status and labels now share one detail branch; two new dropdown tests cover the all-sort-past-cap and pill/menu flag cases, plus unit tests for the cap helper.

I traced the round-robin helper, the applied/intended accounting, and the interaction with canonicalizeLinearIssueAttributeFilter + boundLinearIssueAttributeFilter — canonicalize sorts after the cap, so the bound slice is a no-op and no ids re-drop. Tests pass locally (30 component tests, 8 i18n tests).

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

More picked rows than the transport id cap cannot all be represented, and
MultiSelectList.toggle appends the clicked key last — so the starved row was
always the row the user had just clicked: it stayed unchecked, no notice fired,
and coverage still reported a full 100 of 100. Coverage now takes the cap and
reports a spent id budget as its own shortfall, so the picker says how much it
is really carrying instead of claiming teams it never covered.

Capping also bucketed by the click order the picker hands it, so the same
visible selection could resolve to different ids between renders; it now buckets
in metadata order, with ids from unloaded teams sorted after. boundLinear-
IssueAttributeFilter stays the last word on the transport bound.

The pill's `partial` marker moves from a bare title attribute to the Tooltip
primitive, which keyboard and screen-reader users can actually reach.

Pill labels and facet clearing move to their own module so sections stays under
the max-lines cap.
@nwparker

Copy link
Copy Markdown
Contributor Author

Pushed 5517de4f235 fixing two defects in this PR.

1. >100 picked rows produced a deterministic dead click that reported full coverage.

MultiSelectList.toggle (PRFilterPickers.tsx:178-186) appends the newly clicked key last, so with more picked rows than the cap, the row the user just clicked was always the one capLinearMetadataIdsAcrossGroups starved — and isLinearMetadataGroupSelectionPartial then reported {applied: 100, intended: 100}, i.e. full coverage, with no notice. That is the silent truncation this PR exists to remove.

101 single-id rows cannot be represented by 100 ids no matter how the round-robin is arranged, so the fix is on the reporting side: linearMetadataGroupCoverage now takes the cap and returns atLimit when the id budget is spent. A spent budget is the honest shortfall we can still state from the applied ids alone, so isLinearMetadataGroupSelectionPartial returns true and the picker shows "Filtering the most this can carry: 100 team statuses. Rows picked past that are left out." instead of claiming coverage it never had. The existing "N of M team statuses" notice still wins whenever the per-row shortfall is measurable.

Capping itself also went deterministic: it bucketed by the click order the picker hands it, so the same visible selection could resolve to different ids across renders (requirement 4). Buckets are now taken in metadata order, ids from unloaded teams sorted after; round 0 still gives every row one id before any row gets a second. boundLinearIssueAttributeFilter is untouched and remains the final authority on the transport bound.

Requirement 3 (don't starve the just-clicked row) is deliberately not met: the visible selection is a set, so favouring "most recent" requires click history, which is exactly the unstable ordering requirement 4 forbids. The click is still dead, but it is now announced.

2. The partial marker is now accessible.

It was a bare <span title=...> — not focusable, no route to the explanation. It is now the Tooltip primitive with a focusable trigger, per docs/STYLEGUIDE.md's trigger pattern (TooltipTrigger asChild, side="top" sideOffset={4}).

Pill labels and facet clearing moved to linear-issue-attribute-filter-pills.ts because threading the cap pushed linear-issue-attribute-filter-sections.tsx past the 400-line rule; no lint disable, no cap bump.

Regression tests (each observed failing before the fix, passing after): exactly at the cap, one id over the cap, 101 single-id rows, one wide row alongside several single-id rows, click-order determinism, plus a React-level test that clicks the 101st status row and asserts the coverage notice and the · partial summary.

Verification: pnpm test src/renderer/src/components/linear-issue-attribute src/shared/linear (7 files, 92 tests passed), pnpm tc (exit 0), npx oxlint on the changed files (clean), pnpm run check:code-quality:changed (0 new findings), verify-localization-catalog and the max-lines ratchet both green.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0631be57-35d5-4ac2-a640-68f0a183a6e9

📥 Commits

Reviewing files that changed from the base of the PR and between eaf5049 and 5517de4.

📒 Files selected for processing (8)
  • src/renderer/src/components/linear-issue-attribute-filter-coverage-notice.tsx
  • src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx
  • src/renderer/src/components/linear-issue-attribute-filter-dropdowns.tsx
  • src/renderer/src/components/linear-issue-attribute-filter-pills.ts
  • src/renderer/src/components/linear-issue-attribute-filter-sections.tsx
  • src/renderer/src/components/linear-issue-attribute-filter-team-ids.test.ts
  • src/renderer/src/components/linear-issue-attribute-filter-team-ids.ts
  • src/renderer/src/i18n/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx

Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.

Comment on lines +29 to +35
'Filtering the most this can carry: {{value0}} team statuses. Rows picked past that are left out.',
{ value0: max }
)
: translate(
'auto.components.linear-issue-attribute-filter-coverage-notice.labelsAtIdLimit',
'Filtering the most this can carry: {{value0}} team labels. Rows picked past that are left out.',
{ value0: max }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not state that rows were omitted when the limit is only reached.

A filter with exactly max selected IDs sets atLimit, even when every selected ID is retained. These messages then state that rows “are left out.” Use conditional wording, and update the matching locale entries.

Proposed fix
- 'Filtering the most this can carry: {{value0}} team statuses. Rows picked past that are left out.'
+ 'This filter can include up to {{value0}} team statuses. Some selected rows may be left out.'
...
- 'Filtering the most this can carry: {{value0}} team labels. Rows picked past that are left out.'
+ 'This filter can include up to {{value0}} team labels. Some selected rows may be left out.'

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Important

The new atLimit flag marks exactly-at-cap selections as partial even when nothing was dropped — a workspace with exactly 100 teams (or 25 teams × 4 statuses = 100 ids) gets a false "Rows picked past that are left out" notice and false "· partial" markers. Details inline.

Reviewed changes — since the prior Pullfrog review, this delta adds the at-limit coverage state, reworks the cap helper, and makes the pill partial marker keyboard/screen-reader reachable.

  • atLimit coverage state — linearMetadataGroupCoverage now returns atLimit, and every partiality check (isLinearMetadataGroupSelectionPartial, facetSummary, the notice) takes the cap as an explicit max argument.
  • At-limit message — when the budget is spent with no surviving trace of the starved row, the notice renders "Filtering the most this can carry…" instead of the shortfall copy.
  • Pill helpers extracted — countLinearIssueAttributeFilters, clearLinearIssueAttributeFacet, and linearIssueAttributeFilterPillLabels move to a new linear-issue-attribute-filter-pills.ts.
  • Accessible partial marker — the pill partial flag swaps its title span for a Tooltip + focusable <button>.
  • Deterministic cap — capLinearMetadataIdsAcrossGroups buckets by metadata order and round-robins one id per row, treating unloaded ids as their own sorted rows.
  • Tests — new unit tests for the over-subscribed-row cases and a dropdown test for the at-limit message.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

intended: expandLinearMetadataGroupKeys(groups, keys).length,
// Why: a row the cap could not fit leaves no trace in the ids, so once the budget is
// spent the honest claim is the exhausted budget itself — never full coverage (#17342).
atLimit: selectedIds.length >= max

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

atLimit: selectedIds.length >= max is true for any selection that lands exactly on the cap, so full-coverage selections are falsely reported as partial. A workspace with exactly 100 teams picking one status (or 25 teams × 4 statuses) yields applied === intended === 100, yet the notice claims "Rows picked past that are left out" and the pill/menu show "· partial". The starved-row case this flag exists for is indistinguishable from exact-fit in the bounded selectedIds alone — the fix needs the pre-cap signal from applyPickedFilter / capLinearMetadataIdsAcrossGroups, not an inference from selectedIds.length.

Technical details
# atLimit false-positives on exactly-at-cap selections

## Affected sites
- `src/renderer/src/components/linear-issue-attribute-filter-team-ids.ts:112` — `atLimit` is `selectedIds.length >= max` with no evidence anything was dropped.
- `src/renderer/src/components/linear-issue-attribute-filter-coverage-notice.tsx:51-56` — renders `idLimitMessage` ("Rows picked past that are left out") when `intended <= applied && atLimit`, which is the exact-fit case.
- `src/renderer/src/components/linear-issue-attribute-filter-pills.ts:71-75` and `src/renderer/src/components/linear-issue-attribute-filter-sections.tsx:50` — the same `atLimit` drives the `· partial` pill and section-menu suffix.

## Why it fires on exact fit
- 100 teams, one status picked: `capLinearMetadataIdsAcrossGroups` early-returns (`ids.length <= max`), `applied = 100`, `intended = 100`, `atLimit = true` → false "partial" on all three surfaces.
- 25 teams × 4 statuses = 100 ids: same result — the scenario the PR body itself calls out as triggering the cap.

## Required outcome
- No truncation → no notice and no `· partial`, even at exactly `max` ids.
- Truncation that fully starves a row (more picked rows than the cap can hold) → still surfaces, because the starved row leaves no trace in `selectedIds`.

## Suggested approach
- Carry the drop signal from where it is known: `capLinearMetadataIdsAcrossGroups` (or `applyPickedFilter`) can report whether any id was dropped (pre-cap count > post-cap count), which is exactly the "starved row" condition at the boundary. Thread that boolean into `linearMetadataGroupCoverage` / `isLinearMetadataGroupSelectionPartial` in place of `atLimit`.

## Open questions for the human
- Is a heuristic (e.g. `atLimit` only when picked-group count > max) acceptable, or should the drop flag be computed precisely at the capping step?

The exactly-on-the-cap test capped at max=4 but asserted non-partial at
max=5, so it never covered its own subject. Pin both: at the cap coverage
warns (a starved row leaves no trace in the ids), below it stays quiet.
@nwparker

Copy link
Copy Markdown
Contributor Author

Readiness review complete — known residual, deliberately not fixed

Two independent adversarial review lenses cleared the branch (0 blocking findings) with empirical backing: 50k random selections × 4 click-order shuffles produced a single result set every time (the previous click-order bucketing diverged in 41,409 of them), and a 200k-case fuzz found zero inputs where ids were dropped while the UI reported full coverage.

Both lenses independently flagged one false positive, which I am shipping as-is:

partial fires at exactly the cap even when coverage is complete. atLimit = selectedIds.length >= max (linear-issue-attribute-filter-team-ids.ts:112) cannot distinguish "exactly 100 ids, everything fit" from "100 ids, a row was starved" — a starved row leaves no trace in the surviving ids, so the information needed to tell them apart is gone by the time coverage is computed. Concretely, a workspace with 20 teams × 5 status names picking all 5 rows is exactly 100 ids with full coverage, and still shows the marker.

The bias is deliberately toward warning rather than claiming coverage we cannot prove, and the wording is true in both cases. But it does mean the marker can cry wolf, and that is worth fixing properly rather than papering over.

The real fix is to record truncation at the moment it happens — applyPickedFilter knows the pre-cap id list and can see whether cap() actually dropped anything — instead of inferring it from rendered state afterward. That needs new component state and is out of scope for this PR; follow-up ticket to come.

Also fixed in 5843daf75e6: the exactly-on-the-cap test capped at max=4 but asserted non-partial at max=5, so it never covered its own subject. It now pins both sides.

Residuals carried from earlier rounds, unchanged: the round-robin cannot honour a click once more rows are picked than the cap can hold (101 single-id rows cannot fit in 100 ids — the notice now tells the user instead of silently claiming full coverage), and the partial marker may clip under the section menu's max-w-[120px] truncate, though the pill and inline notice both still carry the signal.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Important

The new test pins a false positive, and the rationale it records is contradicted by the shipped copy.

Reviewed changes — since the prior Pullfrog review, this delta is a single test change in linear-issue-attribute-filter-team-ids.test.ts.

  • Pinned the at-cap partial flag — added an assertion that a selection sitting exactly on the cap reports isLinearMetadataGroupSelectionPartial as true, with a rationale comment stating coverage "warns rather than claim what it cannot prove."

The conservative intent is defensible — from the bounded selectedIds alone, exact-fit coverage really is indistinguishable from a starved row. But the shipped idLimitMessage copy does claim ("Rows picked past that are left out"), which is affirmatively false in the exactly-at-cap full-coverage case. Details inline.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

Comment on lines +177 to +179
// Why: on the cap, full coverage is indistinguishable from a starved row, so coverage
// warns rather than claim what it cannot prove — below the cap it stays quiet (#17342).
expect(isLinearMetadataGroupSelectionPartial(groups, ids, 4)).toBe(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This assertion codifies a false positive: a selection exactly at the cap with full coverage (the PR body's own 25-teams × 4-statuses example = 100 ids) reports partial even though nothing was dropped, so the user sees "Filtering the most this can carry: 100 team statuses. Rows picked past that are left out" — an affirmatively false statement. The rationale comment says coverage "warns rather than claim what it cannot prove," but the shipped copy does claim, which is exactly what the tooltip already avoids ("Some teams may be left out"). Either hedge idLimitMessage to match the tooltip's wording, or thread the drop signal out of capLinearMetadataIdsAcrossGroups so the notice only fires when ids were actually dropped.

Technical details
# At-cap notice copy contradicts its own rationale

## Affected sites
- src/renderer/src/components/linear-issue-attribute-filter-team-ids.test.ts:177-179 — new test asserts exact-at-cap coverage is partial; rationale comment claims it only warns without claiming.
- src/renderer/src/components/linear-issue-attribute-filter-coverage-notice.tsx:29,34 — `idLimitMessage` copy "Rows picked past that are left out." fires in the exact-fit case (`intended <= applied && atLimit`).
- src/renderer/src/components/linear-issue-attribute-filter-pills.ts:71-75 / linear-issue-attribute-filter-sections.tsx:50 — same `atLimit` drives the `· partial` marker.

## Why it fires on exact fit
- 25 teams × 4 statuses = 100 ids, or 100 teams × 1 status: `capLinearMetadataIdsAcrossGroups` early-returns (nothing dropped), `applied === intended === 100`, `atLimit = true` → notice renders the definitive left-out claim and the pill shows `· partial`.

## Required outcome
- When no id was dropped, no notice and no `· partial`, even at exactly `max` ids — or, if the conservative warning is kept, copy that hedges ("may be left out") instead of asserting ("are left out").

## Suggested approach
- Option A (copy only): change `statusAtIdLimit`/`labelsAtIdLimit` to hedge, e.g. "Filtering the most this can carry: {{value0}} team statuses. Some teams may be left out." — consistent with the existing `partialCoverageTitle`.
- Option B (precise): have `capLinearMetadataIdsAcrossGroups` (or `applyPickedFilter`) report whether it dropped ids (pre-cap count > post-cap count) and thread that boolean into `linearMetadataGroupCoverage` in place of the `selectedIds.length >= max` inference.

@nwparker
nwparker merged commit 316cb1c into main Aug 30, 2026
27 checks passed
@nwparker
nwparker deleted the neil/sta-5983-linear-status-filter-truncation-warning branch August 30, 2026 08:19
nwparker added a commit that referenced this pull request Aug 30, 2026
…ctions (#17376)

* fix(linear): stop the filter coverage warning firing on complete selections

#17342 inferred transport-cap truncation from the bounded filter after the
fact, with `atLimit = selectedIds.length >= max`. A row the cap could not fit
leaves no trace in the surviving ids, so at exactly the cap a complete
selection and a trimmed one are indistinguishable from the value alone, and
the inference biased toward always warning. A workspace with 20 teams x 5
status names expands to exactly 100 ids: picking all 5 rows is provably
untruncated, yet the menu read "100 selected · partial".

Record the trim where it happens instead. `applyPickedFilter` holds both the
pre-cap expansion and the bounded result, so it stores the surviving ids as a
truncation record; the notice, section-menu summary, and pill consume that
flag. Keying the record on the ids it describes is what keeps it fresh — the
moment the facet carries anything else (row toggle, pill clear, Clear all, the
prune effect, a workspace switch) the record no longer matches and the warning
goes away, which matters because the prune effect only ever removes ids.
`intended > applied` stays as the fallback for restored filters that carry no
record, and `boundLinearIssueAttributeFilter` is still the last word on the
cap.

Also moves the section-menu partial marker out of the `max-w-[120px] truncate`
summary span, where "100 selected · partial" could clip.

Refs STA-5996

* test(linear): cover the untouched-facet truncation guard

The guard that keeps a recorded trim alive across an unrelated facet click was
untested — the first attempt sat where intended > applied, so the value-derived
shortfall answered for it and removing the guard left every test green. Move the
scenario onto the cap, where only the record can speak.

Also stop an empty record matching an empty facet: a filter carrying nothing is
never truncated.

* test(linear): pin set equality, not subset, on a truncation record

A facet that grew past its record has refetched underneath it; matching by
subset would keep warning about a trim that no longer describes the filter.
Found by mutation: the subset mutant survived the whole suite.

* test(linear): fuzz that the coverage pill and the section notice agree

They are the same claim rendered twice; a pill reading partial over a silent
section is a lie either way round. 20k random topologies, zero divergence.
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…e state-id cap (stablyai#17342)

* fix(linear): flag partially applied status and label filters (STA-5983)

Since stablyai#16879 one status/label row expands to an id per team, and the renderer
bounds that list to the 100-id transport cap. Any surviving id kept the row
fully checked, so the picker claimed coverage the filter never had; show how
many of the row's per-team ids are actually applied.

* fix(linear): keep every picked status row inside the transport cap

The cap sliced a lexicographically sorted id list, so a whole picked row could lose
every id — reverting to unchecked with no notice, and dropping out of the coverage
denominator that was supposed to explain it. Spread the cap across the picked rows,
and carry the partial-coverage signal to the section menu and the pill, which are
what the user reads once the detail panel is closed.

* fix(linear): stop the status filter claiming coverage it cannot apply

More picked rows than the transport id cap cannot all be represented, and
MultiSelectList.toggle appends the clicked key last — so the starved row was
always the row the user had just clicked: it stayed unchecked, no notice fired,
and coverage still reported a full 100 of 100. Coverage now takes the cap and
reports a spent id budget as its own shortfall, so the picker says how much it
is really carrying instead of claiming teams it never covered.

Capping also bucketed by the click order the picker hands it, so the same
visible selection could resolve to different ids between renders; it now buckets
in metadata order, with ids from unloaded teams sorted after. boundLinear-
IssueAttributeFilter stays the last word on the transport bound.

The pill's `partial` marker moves from a bare title attribute to the Tooltip
primitive, which keyboard and screen-reader users can actually reach.

Pill labels and facet clearing move to their own module so sections stays under
the max-lines cap.

* test(linear): assert coverage at the cap it actually caps to

The exactly-on-the-cap test capped at max=4 but asserted non-partial at
max=5, so it never covered its own subject. Pin both: at the cap coverage
warns (a starved row leaves no trace in the ids), below it stays quiet.
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…ctions (stablyai#17376)

* fix(linear): stop the filter coverage warning firing on complete selections

stablyai#17342 inferred transport-cap truncation from the bounded filter after the
fact, with `atLimit = selectedIds.length >= max`. A row the cap could not fit
leaves no trace in the surviving ids, so at exactly the cap a complete
selection and a trimmed one are indistinguishable from the value alone, and
the inference biased toward always warning. A workspace with 20 teams x 5
status names expands to exactly 100 ids: picking all 5 rows is provably
untruncated, yet the menu read "100 selected · partial".

Record the trim where it happens instead. `applyPickedFilter` holds both the
pre-cap expansion and the bounded result, so it stores the surviving ids as a
truncation record; the notice, section-menu summary, and pill consume that
flag. Keying the record on the ids it describes is what keeps it fresh — the
moment the facet carries anything else (row toggle, pill clear, Clear all, the
prune effect, a workspace switch) the record no longer matches and the warning
goes away, which matters because the prune effect only ever removes ids.
`intended > applied` stays as the fallback for restored filters that carry no
record, and `boundLinearIssueAttributeFilter` is still the last word on the
cap.

Also moves the section-menu partial marker out of the `max-w-[120px] truncate`
summary span, where "100 selected · partial" could clip.

Refs STA-5996

* test(linear): cover the untouched-facet truncation guard

The guard that keeps a recorded trim alive across an unrelated facet click was
untested — the first attempt sat where intended > applied, so the value-derived
shortfall answered for it and removing the guard left every test green. Move the
scenario onto the cap, where only the record can speak.

Also stop an empty record matching an empty facet: a filter carrying nothing is
never truncated.

* test(linear): pin set equality, not subset, on a truncation record

A facet that grew past its record has refetched underneath it; matching by
subset would keep warning about a trim that no longer describes the filter.
Found by mutation: the subset mutant survived the whole suite.

* test(linear): fuzz that the coverage pill and the section notice agree

They are the same claim rendered twice; a pill reading partial over a silent
section is a lie either way round. 20k random topologies, zero divergence.
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