Skip to content

feat(linear): pin logged-in user first in assignee dropdowns - #11060

Open
andsnw wants to merge 3 commits into
stablyai:mainfrom
andsnw:feat/linear-assignee-viewer-first
Open

andsnw wants to merge 3 commits into
stablyai:mainfrom
andsnw:feat/linear-assignee-viewer-first

Conversation

@andsnw

@andsnw andsnw commented Jul 28, 2026 •

Copy link
Copy Markdown

Summary

Linear assignee dropdowns (issue drawer/page properties, new-issue composer, and assignee filters) previously listed team members in arbitrary Linear API order, so the logged-in user was buried mid-list. Team members are now sorted with the current Linear user (workspace viewer) first, and the remaining members alphabetized by display name.

The sort happens in the main-process getTeamMembers/getTeamMembersOrThrow, so every consumer — including remote/SSH runtimes, whose RPC path calls the same function on the token-owning host — inherits the order with no renderer or protocol changes. The viewer id is fetched once per workspace and cached keyed by credentialRevision, so reconnecting as a different user refreshes it. A failed viewer lookup degrades to plain alphabetical order and never fails the members fetch.

Screenshots

Assignee dropdown with the logged-in Linear user (andrew) pinned first after Unassigned:
image

Testing

  • pnpm lint
  • pnpm typecheck
  • pnpm test — full suite; 2 pre-existing failures in src/relay/agent-exec-handler.test.ts also fail on unmodified main in this local environment (env-dependent codex spawn expectations), unrelated to this change
  • pnpm build
  • Added tests: viewer pinned first + alpha rest, viewer not in team, viewer lookup failure fallback, per-credential-revision caching, deterministic tie-breaks for duplicate display names

AI Review Report

Reviewed with Claude Code and cross-checked by a second agent (Codex):

  • Cross-platform (macOS/Linux/Windows): no platform-specific APIs touched; main-process-only sorting logic.
  • SSH/remote/local: the runtime RPC path (linear.teamMembers) executes the same getTeamMembers on the remote host, so remote runtimes get identical ordering; no protocol/capability changes (result shape unchanged).
  • Integrations: Linear-only change; no provider-generic code paths altered.
  • Performance: one extra client.viewer call per workspace per credential revision (cached in-memory; fetched outside the members semaphore window to avoid nested-acquire starvation). Sorting is O(n log n) on ≤100-member pages already held in memory.
  • Flagged and addressed in review: stale viewer-cache accumulation (cache now keyed per workspace and replaced on credential-revision bump), sort non-determinism for duplicate display names (id tie-break), and semaphore nesting (viewer fetch uses its own acquire/release window).

Security Audit

  • No new IPC/RPC surface, no new inputs; viewer.id comes from the authenticated Linear SDK client already in use.
  • Viewer id is cached in main-process memory only; no credentials or tokens are read, stored, or logged.
  • Failure path returns null and logs via the existing console.warn convention without including token material.

Notes

  • Multi-team assignee filter dropdowns union per-team lists in team order (useIssueMetadata), so the union is not globally alphabetical; the viewer still surfaces at the top of the first team block after dedup.
  • No X handle to shout out for now.

@andsnw
andsnw marked this pull request as ready for review July 28, 2026 02:59
@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Linear team member retrieval now looks up and caches the viewer ID per workspace credential revision, handles viewer lookup failures with a warning and fallback, and sorts members with the viewer first followed by display name and ID ordering. Both retrieval functions use the new sorting behavior. Tests cover pagination fixtures, viewer ordering, non-member and lookup-failure fallbacks, credential rotation cache invalidation, and deterministic duplicate-name ordering.

Merge Risk: 🔵 Low · up to 094e6

If Linear credentials change while the assignee list is loading, the logged-in user could appear in the wrong position because the viewer identity and member list may come from different sessions. This is a bounded ordering issue that should receive explicit owner follow-up.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the change, rationale, testing, screenshots, security, and compatibility, but omits the required Linked Issue and checklist sections. Add the required Linked Issue, ELI5, Review, Agent skill upstream boundary, and Checklist sections, and complete or explicitly mark each applicable item.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: placing the logged-in Linear user first in assignee dropdowns.
Docstring Coverage ✅ Passed Docstring check was indeterminate for this PR — some files could not be analyzed in time. Not blocking.
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.

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 (2)
src/main/linear/teams.ts (1)

22-41: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Concurrent cold-cache calls trigger redundant viewer lookups; persistent failures re-hit the API on every call.

getCachedViewerId has two related gaps:

  1. If multiple calls for the same workspace race before the cache is populated (e.g. several UI panels requesting team members on startup), each misses the cache and issues its own entry.client.viewer call instead of sharing one in-flight lookup.
  2. On failure the result isn't cached at all, so every subsequent getTeamMembers/getTeamMembersOrThrow call for that revision re-attempts the viewer lookup — if the endpoint is down/rate-limited, this adds a redundant failing call (plus a console.warn) to every member-list fetch until it recovers.

Neither breaks correctness (fallback to alpha order still works), but both add avoidable load on the Linear API.

♻️ Dedup concurrent lookups with an in-flight promise map
+const viewerLookupInFlight = new Map<string, Promise<string | null>>()
+
 async function getCachedViewerId(entry: LinearClientForWorkspace): Promise<string | null> {
   const revision = entry.workspace.credentialRevision ?? 0
   const cached = viewerIdCache.get(entry.workspace.id)
   if (cached && cached.revision === revision) {
     return cached.viewerId
   }

-  await acquire()
-  try {
-    const viewer = await entry.client.viewer
-    viewerIdCache.set(entry.workspace.id, { revision, viewerId: viewer.id })
-    return viewer.id
-  } catch (error) {
-    // Member lists must not fail because the viewer lookup did.
-    console.warn('[linear] viewer lookup for member ordering failed:', error)
-    return null
-  } finally {
-    release()
-  }
+  const inFlight = viewerLookupInFlight.get(entry.workspace.id)
+  if (inFlight) {
+    return inFlight
+  }
+
+  const lookup = (async () => {
+    await acquire()
+    try {
+      const viewer = await entry.client.viewer
+      viewerIdCache.set(entry.workspace.id, { revision, viewerId: viewer.id })
+      return viewer.id
+    } catch (error) {
+      // Member lists must not fail because the viewer lookup did.
+      console.warn('[linear] viewer lookup for member ordering failed:', error)
+      return null
+    } finally {
+      release()
+      viewerLookupInFlight.delete(entry.workspace.id)
+    }
+  })()
+  viewerLookupInFlight.set(entry.workspace.id, lookup)
+  return lookup
 }
src/main/linear/teams.test.ts (1)

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

Assert the warning was actually logged.

The test spies on console.warn but never asserts it was called, so a regression that silently drops the warning (or changes its message) wouldn't be caught — only the fallback ordering is verified.

✅ Add a warning assertion
         await expect(getTeamMembersOrThrow('team-1', 'workspace-1')).resolves.toMatchObject([
           { id: 'user-3' },
           { id: 'user-1' },
           { id: 'user-2' },
           { id: 'user-4' }
         ])
+        expect(warn).toHaveBeenCalledWith(
+          expect.stringContaining('viewer lookup'),
+          expect.any(Error)
+        )

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8178c88e-c3c1-4a2e-b182-0e2f986fd15b

📥 Commits

Reviewing files that changed from the base of the PR and between 2cf91b8 and 41710431d6349d6eb2e76ad48fc9fbed5014e370.

📒 Files selected for processing (2)
  • src/main/linear/teams.test.ts
  • src/main/linear/teams.ts

@andsnw

andsnw commented Jul 28, 2026

Copy link
Copy Markdown
Author

Addressed both review nitpicks in f3346d4: concurrent cold-cache member fetches now share one in-flight viewer lookup (keyed by workspace + credential revision), and the fallback test asserts the warning is logged. The ordering logic moved to team-member-order.ts to stay within the repo's per-file line budget.

@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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 03350cef-18f3-410a-930e-a07a5aba4db1

📥 Commits

Reviewing files that changed from the base of the PR and between 41710431d6349d6eb2e76ad48fc9fbed5014e370 and f3346d4a376531044b43424837ad3eedab1ddd6d.

📒 Files selected for processing (3)
  • src/main/linear/team-member-order.ts
  • src/main/linear/teams.test.ts
  • src/main/linear/teams.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/linear/teams.test.ts

Comment thread src/main/linear/team-member-order.ts Resolved
andsnw and others added 3 commits August 23, 2026 17:33
Linear team members were returned in arbitrary API order, so the current
user was buried in assignee pickers. Sort members in getTeamMembers with
the workspace viewer first and the rest alphabetized, cached per
workspace credential revision. All member consumers (drawer, composer,
filters) inherit the order, including remote runtimes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Share one in-flight viewer promise per workspace credential revision so a
cold cache doesn't issue redundant API calls when several dropdowns load
at once, and assert the fallback warning in tests. Viewer-first ordering
moves to team-member-order.ts to keep teams.ts within the line budget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
localeCompare with the host default collator can order the same member
list differently across macOS, Linux, and remote hosts. Use a pinned
Intl.Collator plus an ordinal id tie-break so ordering is identical
everywhere Orca runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@andsnw
andsnw force-pushed the feat/linear-assignee-viewer-first branch from 840de6a to 094e6a4 Compare August 23, 2026 07:52
@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@andsnw

andsnw commented Aug 23, 2026

Copy link
Copy Markdown
Author

Rebased onto latest main to clear the conflicts (094e6a4).

main has since consolidated the team readers behind the shared readTeamResource helper (#13417), and moved the Linear types/concurrency modules (#14437, #14447, #14704). Rather than reinstating the old duplicated bodies, the viewer-first ordering now composes on top of the shared reader:

async function readTeamMembersViewerFirst(teamId, workspaceId, fallbackWarning?) {
  const entry = getClients(workspaceId)[0]
  if (!entry) return []
  const viewerId = await getCachedViewerId(entry)
  const members = await readTeamResource(teamId, workspaceId, fetchAllTeamMembers, fallbackWarning)
  return sortMembersViewerFirst(members, viewerId)
}

so getTeamMembers/getTeamMembersOrThrow keep their existing error policies unchanged. Net diff is now 3 files. Imports updated to the new module locations.

Re-verified after the rebase: pnpm lint, pnpm typecheck, and the Linear/runtime suites (399 files, 4860 tests) all pass, including main's new preserves the %s error policy cases. The only failures in the full run are pre-existing on unmodified main (cross-version-terminal-wire.unit.test.ts) or flaky (remote-runtime-shared-control-connection.test.ts, passes 3/3 in isolation).

@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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7d86798c-306d-4f89-8629-b227f2d45807

📥 Commits

Reviewing files that changed from the base of the PR and between 7a739c6 and 094e6a4.

📒 Files selected for processing (3)
  • src/main/linear/team-member-order.ts
  • src/main/linear/teams.test.ts
  • src/main/linear/teams.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/linear/teams.test.ts
  • src/main/linear/team-member-order.ts

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

Comment thread src/main/linear/teams.ts
Comment on lines +149 to +154
const entry = getClients(workspaceId)[0]
if (!entry) {
return []
}
const viewerId = await getCachedViewerId(entry)
const members = await readTeamResource(teamId, workspaceId, fetchAllTeamMembers, fallbackWarning)

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

Keep the viewer lookup and member fetch on one client snapshot.

entry is resolved before getCachedViewerId, but readTeamResource resolves getClients(workspaceId)[0] again after the viewer lookup awaits. If credentials rotate between these calls, the member list can use the new credential while viewerId belongs to the old revision. The current viewer can then be sorted incorrectly. Pass entry into the member read, or revalidate the credential revision before sorting.

This branch has not been deployed

No deployments
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