Skip to content

feat(linear): add issue presets and status/assignee/label filters#7388

Open
daniphant wants to merge 5 commits into
stablyai:mainfrom
daniphant:feature/linear-filtering
Open

feat(linear): add issue presets and status/assignee/label filters#7388
daniphant wants to merge 5 commits into
stablyai:mainfrom
daniphant:feature/linear-filtering

Conversation

@daniphant

@daniphant daniphant commented Jul 5, 2026

Copy link
Copy Markdown

Description

The Linear issues tab fetched one hardcoded list (filter: 'all') and offered only search plus the team scope selector. This adds the two filtering layers the GitHub toolbar already has:

Server-side presets. An Assigned / Created / All / Completed radio drives listLinearIssues — the preset is part of the request signature (so switching presets refetches without a nonce bump), persists via taskResumeState.linearPreset, and the warm prefetch in ui.ts now reads the resumed preset instead of unconditionally warming all, so the cache entry it prepares is the one the page actually requests on mount.

Client-side row filters. A collapsed Filters popover with drill-in sections for Status, Assignee, and Label, rendered as closeable pills when active. Options are derived from the fetched rows (Linear has no cheap per-workspace endpoint for these): statuses keep Linear's workflow ordering (triage → backlog → unstarted → started → completed → canceled) with their state colors, assignees get the viewer pinned on top with a Me tag plus an Unassigned sentinel (__unassigned__, can't collide with a real UUID). Empty selection means "all"; reconcileTaskFilterSelection drops ids that vanish after a preset/workspace switch so a stale selection can't silently filter everything out, and returns the same Set reference when nothing changed so the reconcile effect never causes an extra render. The team dimension intentionally stays with the existing header scope selector — the popover does not duplicate it, so each filter has exactly one UI path.

Rather than copying the GitHub toolbar's markup, its chrome moved into provider-neutral components consumed by both: PRFilterPickerstask-filter-pickers (out of components/github/, lists now generic over the option type so callers carry color/avatar on the option instead of re-joining by key per row), task-filter-section-menu (drill-in menu + back header), task-filter-pill, and task-preset-buttons — the existing Jira preset row now renders through that last one too.

ELI5

The Linear tab used to be one long list you could only search. Now it has quick tabs — Assigned, Created, All, Completed — that ask Linear for different lists, and a Filters button to narrow what's on screen by status, assignee, or label, exactly like the GitHub PRs toolbar. Under the hood the GitHub toolbar's filter widgets were turned into shared parts instead of being copy-pasted, so the two toolbars can't drift apart.

Screenshots

1 image

Evidence

New unit tests pin the behavior that matters: option collection (dedupe, sort, workflow-state ordering, unassigned appended last), match semantics (empty = all, sentinel matching), reconciliation (drops stale ids, falls back to all when every id is stale, returns the same Set reference when nothing changed — the contract the reconcile hook relies on), and the picker query-size guards after the move/rename.

npx vitest run --config config/vitest.config.ts \
  src/renderer/src/components/linear-assignee-filter.test.ts \
  src/renderer/src/components/linear-label-filter.test.ts \
  src/renderer/src/components/linear-status-filter.test.ts \
  src/renderer/src/components/task-filter-pickers.test.ts

Full gates green locally: pnpm lint (incl. locale parity + coverage verifiers), pnpm typecheck (node/cli/web), pnpm test, pnpm build.

Trade-offs

  • Row filters are client-side over the fetched page, not server queries. They can only narrow what's already fetched; an issue outside the current preset/limit won't appear by selecting its label. That's inherent to deriving options from rows, and the zero-match empty state says so and points at refresh. Presets remain the server-side dimension.
  • Viewer pin matches by display name, because issue rows don't carry viewer ids. Two users sharing a display name would both pin. Cosmetic worst case, noted in the code.
  • components/github/PRFilterPickers moved and renamed. Blast radius is the GitHub PR toolbar, which now consumes the shared components; its picker tests moved with the module and the affected i18n keys were migrated to the new namespaces with existing translations preserved. One deliberate visual delta outside Linear: the Jira active preset pill drops backdrop-blur-md (a no-op on an opaque background), and the GitHub filter menu heading gains the space it was missing ("Filterpull requests").
  • New strings ship translated for es/ja/ko/zh, reusing existing catalog terminology (担当者/负责人/etc.) rather than inventing new terms.

Scope

Renderer-only plus one line in the ui.ts store slice; no new IPC channels, no main-process changes, no dependencies. Nothing platform-, SSH-, or provider-specific: no shortcuts, paths, or shell behavior touched, and filtering runs over rows already fetched through the existing Linear IPC. Preset ids are double-validated (renderer resume-state whitelist + existing VALID_FILTERS in the main process), so IPC can't be driven with arbitrary filter strings. Avatar URLs come from the Linear API and render through the same <img> pattern the Linear UI already uses.

AI review: audited with Claude Code (repo-convention pass + four-agent reuse/simplification/efficiency/altitude review); confirmed findings were applied before opening — shared-chrome extraction instead of a second markup copy, one reconcile hook instead of three copy-pasted effects, generic pickers instead of per-row Array.find, one preset-row component for Linear and Jira.

X: @pinchyNeb

daniphant added 2 commits July 4, 2026 22:17
Adds a server-side preset radio (Assigned/Created/All/Completed) and a
client-side Filters popover (status, team, assignee, label) to the Linear
issues tab. Presets flow through the fetch request signature, resume state,
and warm prefetch; row filters derive their options from fetched issues and
reconcile stale selections on preset or workspace switches.

Extracts the GitHub PR toolbar's filter chrome into provider-neutral shared
components (task-filter-pickers, task-filter-section-menu, task-filter-pill,
task-preset-buttons) consumed by both toolbars, and localizes all new
strings for es/ja/ko/zh.

Claude-Session: https://claude.ai/code/session_01EigB95j2H5tPwtQ41CGrUp
# Conflicts:
#	src/renderer/src/i18n/locales/es.json
#	src/renderer/src/i18n/locales/ja.json
#	src/renderer/src/i18n/locales/ko.json
#	src/renderer/src/i18n/locales/zh.json
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

This PR adds shared task-filter primitives, Linear-specific assignee/label/status filter helpers, and a new Linear filter toolbar with preset-driven issue fetching. TaskPage now restores and applies the active Linear preset during fetches and persistence. GitHub PR filter pills and section controls now use shared components. Locale files were updated for the new filter labels, menu text, and empty-state messages.

Sequence Diagram(s)

Included within the hidden review stack artifact.

Related Issues: Not specified in provided information.

Related PRs: Not specified in provided information.

Suggested labels: review_needed_senior_swe, review_depth_deep

Suggested reviewers: Not specified in provided information.

Poem
A rabbit hopped through filter trees,
Pills and pickers, shared with ease,
Linear presets now remembered well,
Assignees sorted, labels spell,
One tidy hop, less code to weed. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title accurately summarizes the main Linear presets and issue-filtering change.
Description check ✅ Passed The description covers the main change, screenshots, testing, review, security, and notes, though the headings differ from the template.

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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/renderer/src/components/task-filter-pickers.tsx (2)

87-93: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fallback strings bypass localization.

'Loading…', 'Search text is too large.', 'Press Enter to use the typed value.', and 'No matches' are hardcoded, while every other user-facing string in this file goes through translate(...). Given the PR's goal of localizing filter UI strings for es/ja/ko/zh, these will silently stay English.

🌐 Proposed fix
-  const fallback = loading
-    ? 'Loading…'
-    : queryTooLarge
-      ? 'Search text is too large.'
-      : showCustom
-        ? 'Press Enter to use the typed value.'
-        : (error ?? emptyText ?? 'No matches')
+  const fallback = loading
+    ? translate('auto.components.task.filter.pickers.loading', 'Loading…')
+    : queryTooLarge
+      ? translate('auto.components.task.filter.pickers.query.too.large', 'Search text is too large.')
+      : showCustom
+        ? translate('auto.components.task.filter.pickers.press.enter', 'Press Enter to use the typed value.')
+        : (error ?? emptyText ?? translate('auto.components.task.filter.pickers.no.matches', 'No matches'))

Apply the equivalent change to the MultiSelectList fallback (Lines 173-177).

Also applies to: 173-177


199-207: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

"Clear (N)" label splits translation across concatenation.

translate(...) only wraps 'Clear ('; the count and closing paren are appended as raw JSX. Translators can't reorder the count relative to the verb, so this breaks correct phrasing in es/ja/ko/zh — the exact locales this PR targets.

🌐 Proposed fix using interpolation
-            {translate('auto.components.task.filter.pickers.2534e82b7d', 'Clear (')}
-            {selected.length})
+            {translate('auto.components.task.filter.pickers.2534e82b7d', 'Clear ({{count}})', {
+              count: selected.length
+            })}

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ea964f87-7b9a-4834-93e9-c6b93171f6e0

📥 Commits

Reviewing files that changed from the base of the PR and between eb2b894 and ec88c43.

📒 Files selected for processing (25)
  • src/renderer/src/components/TaskPage.tsx
  • src/renderer/src/components/github/PRFilterDropdowns.tsx
  • src/renderer/src/components/github/PRFilterPickers.test.ts
  • src/renderer/src/components/github/PRFilterSections.tsx
  • src/renderer/src/components/linear-assignee-filter.test.ts
  • src/renderer/src/components/linear-assignee-filter.ts
  • src/renderer/src/components/linear-issue-filters.tsx
  • src/renderer/src/components/linear-label-filter.test.ts
  • src/renderer/src/components/linear-label-filter.ts
  • src/renderer/src/components/linear-status-filter.test.ts
  • src/renderer/src/components/linear-status-filter.ts
  • src/renderer/src/components/task-filter-pickers.test.ts
  • src/renderer/src/components/task-filter-pickers.tsx
  • src/renderer/src/components/task-filter-pill.tsx
  • src/renderer/src/components/task-filter-section-menu.tsx
  • src/renderer/src/components/task-filter-selection.ts
  • src/renderer/src/components/task-page-localized-options.tsx
  • src/renderer/src/components/task-preset-buttons.tsx
  • src/renderer/src/hooks/useReconciledFilterSelection.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/store/slices/ui.ts
💤 Files with no reviewable changes (1)
  • src/renderer/src/components/github/PRFilterPickers.test.ts

Comment thread src/renderer/src/components/TaskPage.tsx
daniphant added 3 commits July 5, 2026 11:02
The header scope selector already owns the team dimension, so the popover
offered a second UI path to the same filter. Team filtering itself is
unchanged — the scope selector still drives it.
Fall back to the connection viewer's display name when no single
workspace is selected. The workspace displayName is already the viewer's
per-org name (not the org label), but it is null under "all workspaces",
which silently dropped the Me pin. Addresses CodeRabbit review on stablyai#7388.
The single/multi select fallback messages (Loading, query-too-large,
custom-value hint, no matches) were hardcoded English, and the Clear (N)
label split the count outside translate(), so translators could not
reorder it. Both flagged by CodeRabbit on stablyai#7388.
@daniphant daniphant changed the title feat(linear): add issue presets and status/team/assignee/label filters feat(linear): add issue presets and status/assignee/label filters Jul 5, 2026
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.

2 participants