Skip to content

Linear quick-view caching, OAuth token refresh, and project-scoped autolinks#395

Merged
arul28 merged 4 commits into
mainfrom
ade/linear-issues-514a19d3
May 29, 2026
Merged

Linear quick-view caching, OAuth token refresh, and project-scoped autolinks#395
arul28 merged 4 commits into
mainfrom
ade/linear-issues-514a19d3

Conversation

@arul28
Copy link
Copy Markdown
Owner

@arul28 arul28 commented May 29, 2026

Summary

Three Linear improvements across the issue browser and connection layer.

1. Quick-view caching — LinearIssueBrowser.tsx, LinearQuickViewButton.tsx

Per-project stale-while-revalidate cache so the top-bar quick view opens instantly and survives reopen. Quick view, filter catalog, and issue search each render cached data immediately, dedupe in-flight requests, and guard against stale responses with per-loader request-id checks — including loadCatalog, which previously had none. Larger modal + redesigned issue-detail pane.

2. OAuth token refresh — linearTokenRefresh.ts, credential services, linearClient.ts

Linear OAuth access tokens expire ~24h after sign-in and Linear issues a rotating refresh token, but ADE never refreshed — so OAuth connections (the primary sign-in path) silently broke after a day. Now:

  • ensureFreshToken() on both the desktop and headless (ade serve) credential services refreshes via grant_type=refresh_token when at/near expiry, rotates the refresh token, clears the connection on invalid_grant (prompts reconnect), and keeps the token on transient failures. Concurrency-deduped.
  • linearClient refreshes proactively before each GraphQL request + the quick-view SDK call, and reactively once on a 401 (re-reading the token per attempt).
  • Grounded in Linear's OAuth docs (expires_in: 86399; the refresh-token system has been mandatory since 2026-04-01).

3. Project-scoped autolink setup — LinearSection.tsx

Settings → Linear "GitHub reference links" now reloads connection / repo / team-keys when the active project changes, so the generated gh repo autolink commands target the current project's repo + Linear workspace (previously stale on project switch). Clearer copy: explains what autolinks do, that they apply to this project's repo, and that Create configures it automatically while the shown gh command is the manual equivalent.

Verification

  • Typecheck (desktop + ade-cli) clean; ESLint clean on changed files.
  • New tests: linearTokenRefresh helper (7) + linearAuth OAuth-refresh cases (4: near-expiry rotation, no-refresh for manual/fresh, invalid_grant→clear, transient→keep).
  • Full cto suite (151) + renderer suite (1916) green; headless (22).

Note: project-scoped Linear credentials shipped separately in #392; this PR carries no credential-storage changes.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Automatic OAuth token refresh for Linear connections (keeps sessions valid and retries requests when needed).
  • Improvements

    • In-memory caching in the Linear browser for snappier navigation and reduced load times.
    • Quick view overlay styling made more responsive and visually consistent.
    • Linear settings now reload per-project and autolinks copy/creation guidance clarified.
  • Bug Fixes

    • Stale/invalid OAuth connections are properly cleared when refresh is rejected.

Review Change Stack

Greptile Summary

This PR fixes a real-world session expiry bug (OAuth tokens silently dying after ~24 h), adds stale-while-revalidate caching to the Linear issue browser, and makes the Settings → Linear autolink section project-aware. All three changes are well-scoped and correctly guarded.

  • Token refresh (linearTokenRefresh.ts, linearCredentialService.ts, linearClient.ts): shared refreshLinearOAuthAccessToken helper correctly narrows invalidGrant to error === "invalid_grant" only (per RFC 6749 §5.2); proactive + reactive refresh paths in linearClient deduplicate in-flight refreshes via refreshInFlight and prevent infinite loops with the didAuthRefresh flag.
  • Browser caching (LinearIssueBrowser.tsx): module-level linearIssueBrowserCache implements stale-while-revalidate for quick-view, catalog, and search results with per-loader request-ID guards and in-flight deduplication; state is pre-seeded from cache on mount.
  • Project-scoped autolinks (LinearSection.tsx): subscribes to useAppStore for projectRoot and wires it into both load effects, with an autolinksRequestIdRef guard preventing stale detectRepo/listRepoAutolinks responses from populating the wrong project.

Confidence Score: 5/5

The PR is safe to merge. The token-refresh logic correctly handles all error cases (invalid_grant vs. transient), the cache uses request-ID guards to prevent stale data from crossing project boundaries, and the existing test suite was extended to cover the new refresh paths.

All three change areas are logically sound: the invalidGrant detection was narrowed to only error === invalid_grant (fixing the previously flagged overly-broad 400/401 check), the React caching is properly scoped per project root and CTO bridge instance, and the project-switch reload in LinearSection is guarded against in-flight race conditions. No data-loss or incorrect-auth paths were found in the new code.

No files require special attention. The headless credential service's ensureFreshToken does not support fetchImpl injection (flagged as a suggestion), but this does not affect correctness in production.

Important Files Changed

Filename Overview
apps/desktop/src/main/services/cto/linearTokenRefresh.ts New shared helper implementing the RFC 6749 refresh-token exchange; correctly restricts invalidGrant to error === invalid_grant only, handles missing expires_in gracefully, and is fully unit-tested.
apps/desktop/src/main/services/cto/linearCredentialService.ts Adds ensureFreshToken with concurrency deduplication via refreshInFlight; correctly guards against invalid_grant, calls invalidateCache() after persistence, and injects fetchImpl for testability.
apps/desktop/src/main/services/cto/linearClient.ts Adds proactive token refresh before each GraphQL request and getQuickView, and a one-shot reactive refresh on isAuthError; didAuthRefresh flag prevents infinite retry loops, token is re-read per attempt after a refresh.
apps/ade-cli/src/headlessLinearServices.ts Mirrors desktop ensureFreshToken with correct credential-write semantics, but unlike the desktop service the implementation does not accept a fetchImpl parameter, so the headless refresh path always uses the global fetch and cannot be unit-tested without mocking globals.
apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx Adds module-level stale-while-revalidate cache with per-loader request-ID guards and in-flight deduplication; project-switch effect correctly populates from cache before fresh fetches; redesigned issue-detail pane with responsive column sizing.
apps/desktop/src/renderer/components/settings/LinearSection.tsx Adds projectRoot from useAppStore and wires it into both load effects so autolinks and connection status reload on project switch; autolinksRequestIdRef correctly discards stale in-flight responses.

Sequence Diagram

sequenceDiagram
    participant R as Renderer
    participant LC as linearClient
    participant CS as CredentialService
    participant TR as linearTokenRefresh
    participant LA as Linear API

    R->>LC: getQuickView() / request()
    LC->>CS: ensureFreshToken() [proactive]
    CS-->>CS: check expiresAt vs now - 2min buffer
    alt token near expiry
        CS->>TR: refreshLinearOAuthAccessToken()
        TR->>LA: POST /oauth/token (refresh_token)
        LA-->>TR: "{access_token, refresh_token, expires_in}"
        TR-->>CS: "{ok:true, accessToken, refreshToken, expiresAt}"
        CS-->>CS: persistToken() + invalidateCache()
    else token fresh or manual
        CS-->>LC: (no-op)
    end
    LC->>LA: GraphQL / SDK request
    alt 401 received
        LA-->>LC: 401 Unauthorized
        LC->>CS: "ensureFreshToken({force:true}) [reactive, once]"
        alt invalid_grant
            CS-->>CS: persistToken(null) — clear connection
            LC-->>R: throw token missing error
        else transient failure
            CS-->>CS: keep existing token
            LC->>LA: retry with same token
        else success
            CS-->>CS: persistToken(newToken)
            LC->>LA: retry with new token
            LA-->>LC: 200 OK
            LC-->>R: data
        end
    else success
        LA-->>LC: 200 OK
        LC-->>R: data
    end
Loading

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/ade-cli/src/headlessLinearServices.ts:1144-1146
The headless `ensureFreshToken` always falls through to the global `fetch` because `createHeadlessLinearCredentialService` accepts no `fetchImpl` parameter. The desktop service accepts one — tests inject a `vi.fn()` and validate the full refresh cycle. Without injection, testing the headless path requires mocking the global `fetch`, and the four `linearAuth.test.ts` cases only exercise the desktop service. Consider threading `fetchImpl` through so the headless refresh is testable on the same terms.

```suggestion
function createHeadlessLinearCredentialService(args: {
  adeDir: string;
  fetchImpl?: typeof fetch;
}): HeadlessLinearCredentialService {
```

Reviews (5): Last reviewed commit: "Only treat invalid_grant as a dead Linea..." | Re-trigger Greptile

@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview May 29, 2026 8:49am

@capy-ai
Copy link
Copy Markdown

capy-ai Bot commented May 29, 2026

Capy auto-review is paused for this organization because the monthly auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

📝 Walkthrough

Walkthrough

This PR adds proactive and reactive OAuth token auto-refresh for Linear credentials and implements per-project in-memory caching for the Linear issue browser. Token refresh utilities, persistence, and concurrency guards are added and integrated into desktop/headless credential services and the GraphQL client with tests. The browser hydrates state from cache, deduplicates in-flight requests, and receives layout/styling updates.

Changes

Linear OAuth token auto-refresh infrastructure

Layer / File(s) Summary
Shared token refresh utilities
apps/desktop/src/main/services/cto/linearTokenRefresh.ts, apps/desktop/src/main/services/cto/linearTokenRefresh.test.ts
linearTokenNeedsRefresh determines token staleness via expiry + configurable buffer; refreshLinearOAuthAccessToken performs a grant_type=refresh_token exchange with injectable fetch, parses responses safely, and classifies failures as invalidGrant (dead token) or transient (retryable). Tests cover freshness, request/response formats, token rotation, expiry computation, and error classification.
Desktop credential service token refresh
apps/desktop/src/main/services/cto/linearCredentialService.ts, apps/desktop/src/main/services/cto/linearAuth.test.ts
ensureFreshToken checks OAuth tokens for staleness, refreshes using injected fetch, deduplicates concurrent attempts, persists refreshed tokens, clears credentials on invalidGrant, and logs warnings on transient failures. Exported on returned service. Tests validate refresh/rotation, no-op for manual/fresh tokens, clearing on invalid grants, and preservation on transient errors.
Headless services token refresh
apps/ade-cli/src/headlessLinearServices.ts
Headless credential service gains ensureFreshToken: imports refresh utilities, adds method to interface, performs best-effort refresh with in-flight deduplication, persists rotated tokens on success, and clears auth state on invalidGrant. Exported on returned service object.
Linear GraphQL client auth refresh
apps/desktop/src/main/services/cto/linearClient.ts
Proactive refresh via ensureFreshAuth before each request and recomputing Authorization per retry. Reactive refresh on auth failures (HTTP 401, GraphQL AUTHENTICATION_ERROR, message patterns): forces a one-time refresh per request and retries with the refreshed token. getQuickView calls ensureFreshAuth before SDK/GraphQL operations.

Linear issue browser caching and layout restructuring

Layer / File(s) Summary
Cache infrastructure and state initialization
apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx
In-memory cache scoped by window.ade.cto instance and project+environment keys; tracks freshness, stores in-flight promises for deduplication, enforces max search entries with LRU eviction. Component state (quick view, catalog, filters, issues, pageInfo) hydrates from cache and localStorage on mount and re-hydrates when cacheKey/projectRoot change.
Cache-aware data loading
apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx
loadQuickView, loadCatalog, searchIssues consult cache before fetching, reuse fresh cached results and in-flight promises for deduplication, gate state updates via request-id refs to avoid stale overwrites, and update cache on success; pagination cursor derived from cached pageInfo for append behavior.
UI layout restructuring and styling
apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx, apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx
Main grid columns updated for responsive layout; panes/action docks gain data-linear-pane / data-linear-action-dock attributes; issue details converted to aside/card wrapper; info section switched to 2-column grid; resolve buttons redesigned with grid icon/label layout; InfoRow items changed to bordered-card style with uppercase labels; quick-view backdrop darkened and modal sized responsively.
Settings panel project-scoped loading
apps/desktop/src/renderer/components/settings/LinearSection.tsx
LinearSection derives projectRoot from app store and re-executes credential status and GitHub autolinks loaders when active project changes. Autolinks loader uses request-id refs to avoid stale updates; UI copy updated to clarify repo-link creation and gh command instructions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • arul28/ADE#133: Both PRs modify Linear credential handling in linearCredentialService.ts and relate to stored-token vs OAuth refresh flows.
  • arul28/ADE#274: Overlaps with client/quick-view/search pipeline changes in linearClient.ts; related to request/auth integration.

Suggested labels

desktop

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the three main changes: Linear quick-view caching, OAuth token refresh, and project-scoped autolinks, matching the PR's core deliverables.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ade/linear-issues-514a19d3

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

❤️ Share

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

@arul28 arul28 changed the title Linear Issues Scope Linear credentials per project and harden the issue browser May 29, 2026
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 29, 2026

@copilot review but do not make fixes

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

PR Review

Scope: 11 file(s), +792 / −462
Verdict: Minor issues

This PR moves Linear auth to per-project machine credential keys (linear.project.<hash>.*), wipes legacy global/file stores on service construction, aligns headless/CLI readiness checks, and adds stale-while-revalidate caching plus layout polish in LinearIssueBrowser / the quick-view dialog. The credential isolation is a clear win; the main user-facing risk is the intentional no-migration upgrade path and a few browser edge cases around errors and shared env tokens.


🐛 Functionality

[High] Upgrade clears legacy Linear credentials without migration

File: apps/desktop/src/main/services/cto/linearCredentialService.ts:L226-L234 (also L979-L1033)
Issue: On every createLinearCredentialService construction, clearLegacyLinearCredentials() deletes the global machine keys (linear.token.v1, …), project-local linear-token.v1.bin, OAuth config files, and the linear stanza in local.secret.yaml without copying them into the new per-project keys. Anyone upgrading from a build that stored a single shared token will see Linear as disconnected until they reconnect in each project.
Repro: Store a token under legacy linear.token.v1 or .ade/secrets/linear-token.v1.bin, upgrade to this branch, open the project — getStatus().tokenStored is false and CTO/Linear surfaces show disconnected.
Fix: On first construction per project, migrate legacy material into createLinearProjectCredentialKeys(projectRoot) (or gate deletion behind a one-time migration flag), and surface an in-app “Reconnect Linear” banner when legacy material was removed.

[Medium] ADE_LINEAR_* / LINEAR_* env tokens still apply to every project

File: apps/desktop/src/main/services/cto/linearCredentialService.ts:L231-L240
Issue: After scoping machine-store keys per project, readEnvToken() still fills in a token whenever the project-scoped store is empty. A headless or desktop session with ADE_LINEAR_API set will attach the same Linear workspace to all projects, undermining per-project isolation the PR introduces.
Repro: Set ADE_LINEAR_API globally; open project A (no stored token) and project B (no stored token) — both resolve the same env token and workspace.
Fix: Either document env overrides as an explicit global escape hatch, or namespace env lookup (e.g. per-project suffix) and only honor global env when no project-scoped key exists and the operator opts in.


🎨 UI/UX

[Medium] Linear browser error banner can persist after switching projects

File: apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx:L426-L435
Issue: When projectRoot / cacheKey changes, the effect reloads filters, quick view, catalog, and issues from cache but never calls setError(null). A failed load in project A can leave a red error banner visible while project B data renders underneath.
Repro: Open project A’s Linear browser with Linear disconnected or offline to trigger an error; switch to project B with a healthy connection — stale error text remains until another fetch fails or succeeds with force.
Fix: Clear error (and optionally reset loading flags) at the start of the [cacheKey, projectRoot] effect, matching what loadQuickView / loadCatalog do when they start a fetch.

[Low] Issue list can stay stale for up to 90s after external Linear edits

File: apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx:L64-L65, L308-L310, L538-L541
Issue: Search/quick-view/catalog caches treat data as fresh for LINEAR_BROWSER_CACHE_STALE_MS (90s) and skip network refresh unless force is true. Edits made directly in Linear (state, assignee, title) may not appear until the user triggers refresh or waits out the TTL.
Fix: Invalidate the relevant cache entry on refreshKey, window focus, or after lane/resolve actions that mutate Linear issues; or shorten TTL for the issues pane.


⚡ Performance

[Low] Module-level Linear browser cache never evicts old project entries

File: apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx:L113, L251-L254
Issue: linearIssueBrowserCache is a process-lifetime Map keyed by projectRoot::cto:scope. Each distinct project opened in a long session retains quick-view/catalog/search maps (search capped at 16 keys per project, but entries themselves are not removed).
Impact: Slow memory growth when switching among many projects in one Electron session (mostly retained issue arrays and catalog metadata).
Fix: LRU-cap the outer Map (e.g. keep last 4–8 cacheKeys) or delete the entry when projectRoot changes.


Notes

  • Security / isolation: Project-scoped keys plus legacy global key deletion fix the prior cross-project token bleed; headless tests for A/B isolation are a good guardrail.
  • OAuth: Docs correctly call out ~24h access-token expiry with refresh not wired yet; that limitation predates this diff but is now more visible with OAuth persisted per project.
  • Upgrade UX: Commit message and feature docs already say “reconnect once”; consider a dedicated disconnected state in LinearConnectionPanel / top-bar quick view so the wipe is not mistaken for a transient network failure.
  • Positive: realpathSync canonicalization in linearCredentialKeys.ts avoids symlinked /var vs /private/var splits; CLI readiness now probes the real store instead of removed linear-token.v1.bin.
Open in Web View Automation 

Sent by Cursor Automation: BUGBOT in Versic

Comment thread apps/desktop/src/main/services/cto/linearCredentialService.ts Outdated
Comment thread apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx
Comment thread apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
apps/ade-cli/src/cli.ts (1)

9065-9090: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve credential-store read failures in the readiness output.

This catch turns storage errors into “no token present,” so ade doctor / ade auth status will misdiagnose a broken machine credential store as a missing Linear login. Keep the check non-fatal, but surface the read failure in message and/or details.

Proposed fix
 function checkLinearReadiness(projectRoot: string): ReadinessCheck {
   // Linear tokens are stored under a project-scoped key in the shared machine
   // credential store (linear.project.<hash>.token.v1). The legacy project-local
   // linear-token.v1.bin file is no longer written (and is actively removed on
   // service construction), so probe the real source of truth instead.
   let storedTokenPresent = false;
+  let storeReadError: string | null = null;
   try {
     const machineStore = new EncryptedFileCredentialStore();
     const keys = createLinearProjectCredentialKeys(projectRoot);
     storedTokenPresent = Boolean(machineStore.getSync(keys.token)?.trim());
-  } catch {
-    storedTokenPresent = false;
+  } catch (error) {
+    storeReadError =
+      error instanceof Error ? error.message : String(error);
   }
   const envTokenPresent = Boolean(
     process.env.ADE_LINEAR_API?.trim() ||
     process.env.LINEAR_API_KEY?.trim() ||
     process.env.ADE_LINEAR_TOKEN?.trim() ||
     process.env.LINEAR_TOKEN?.trim(),
   );
   const ready = storedTokenPresent || envTokenPresent;
   return {
     ready,
     status: ready ? "ready" : "warning",
     message: ready
       ? "Linear credentials are present locally."
-      : "No Linear token was detected in local stores or environment variables.",
+      : storeReadError
+        ? "Linear credential storage could not be read."
+        : "No Linear token was detected in local stores or environment variables.",
     nextAction: ready
       ? undefined
       : "Configure Linear in ADE desktop or set ADE_LINEAR_API/LINEAR_API_KEY for headless mode.",
     details: {
       storedTokenPresent,
       tokenEnvPresent: envTokenPresent,
+      ...(storeReadError ? { storeReadError } : {}),
     },
   };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ade-cli/src/cli.ts` around lines 9065 - 9090, The current catch in the
credential-read block swallows storage errors and treats them as "no token", so
update the logic around EncryptedFileCredentialStore/getSync and
createLinearProjectCredentialKeys to capture read failures: instead of only
setting storedTokenPresent=false in the catch, set a storedTokenReadError flag
(or store the caught Error) and preserve storedTokenPresent=false; then include
that flag/error in the returned readiness object (e.g., in details as
storedTokenReadError or storedTokenReadErrorMessage) and adjust message/status
to surface a read-failure case (for example, when storedTokenReadError is
truthy, set status to "warning" and message to indicate a credential-store read
error). Ensure the variables referenced are storedTokenPresent,
storedTokenReadError, EncryptedFileCredentialStore, and
createLinearProjectCredentialKeys so callers can detect the read-failure vs.
missing-token cases.
🧹 Nitpick comments (2)
apps/desktop/src/main/services/cto/linearCredentialService.ts (1)

143-148: ⚡ Quick win

Order writes so a partial failure can't leave a token with stale metadata.

writeMachineCredential rethrows, and the store persists each key independently. If the token write succeeds but a subsequent metadata write throws, the store ends up with a fresh token alongside stale authMode/refreshToken/expiresAt (e.g. an OAuth-labeled manual token with a leftover refresh token). Writing the token last on persist — and deleting it first on clear — guarantees the token is only "present" once its metadata is consistent.

♻️ Proposed reordering
 const persistMachineToken = (record: StoredLinearToken | null): void => {
-  writeMachineCredential(machineKeys.token, record?.token ?? null);
-  writeMachineCredential(machineKeys.authMode, record?.authMode ?? null);
-  writeMachineCredential(machineKeys.refreshToken, record?.refreshToken ?? null);
-  writeMachineCredential(machineKeys.tokenExpiresAt, record?.expiresAt ?? null);
+  if (!record) {
+    // Delete token first: a later failure still leaves no usable token.
+    writeMachineCredential(machineKeys.token, null);
+    writeMachineCredential(machineKeys.authMode, null);
+    writeMachineCredential(machineKeys.refreshToken, null);
+    writeMachineCredential(machineKeys.tokenExpiresAt, null);
+    return;
+  }
+  // Write metadata first, token last: a partial failure never yields a token with stale metadata.
+  writeMachineCredential(machineKeys.authMode, record.authMode ?? null);
+  writeMachineCredential(machineKeys.refreshToken, record.refreshToken ?? null);
+  writeMachineCredential(machineKeys.tokenExpiresAt, record.expiresAt ?? null);
+  writeMachineCredential(machineKeys.token, record.token);
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/cto/linearCredentialService.ts` around lines
143 - 148, persistMachineToken currently writes token before its metadata which
can leave a fresh token paired with stale metadata if a subsequent write throws;
reorder writes so writeMachineCredential calls for machineKeys.authMode,
machineKeys.refreshToken, and machineKeys.tokenExpiresAt happen first and
writeMachineCredential(machineKeys.token, ...) is executed last in
persistMachineToken, and mirror the opposite behavior in the clear routine
(delete token first) so the token is only present when its metadata is
consistent; use the existing function names persistMachineToken,
writeMachineCredential and the machineKeys identifiers to locate and update the
logic.
apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx (1)

109-111: 💤 Low value

Consider adding entry limit or cleanup for the outer cache Map.

The linearIssueBrowserCache Map accumulates entries keyed by projectRoot::cto:scope but never evicts old project entries (only search results within entries are limited). Over extended desktop sessions with multiple project switches, memory could grow unboundedly. Consider adding a max-entry limit with LRU eviction, or cleanup on project/CTO change.

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

In `@apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx` around lines
109 - 111, The outer cache linearIssueBrowserCache currently never evicts
entries and can grow unbounded; add a bounded-entry policy or explicit cleanup:
introduce a MAX_LINEAR_CACHE_ENTRIES constant and on every insertion into
linearIssueBrowserCache evict least-recently-used entries (use Map iteration
order as LRU by deleting the first key when size > MAX) or provide a cleanup
function that removes all entries for a given projectRoot::cto:scope when
ctoCacheScopes or nextCtoCacheScope change; update creation/access code for
LinearIssueBrowserCacheEntry to move touched keys to the Map tail to maintain
LRU ordering and ensure ctoCacheScopes/nextCtoCacheScope updates trigger removal
of stale keys.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/ade-cli/src/cli.ts`:
- Around line 9065-9090: The current catch in the credential-read block swallows
storage errors and treats them as "no token", so update the logic around
EncryptedFileCredentialStore/getSync and createLinearProjectCredentialKeys to
capture read failures: instead of only setting storedTokenPresent=false in the
catch, set a storedTokenReadError flag (or store the caught Error) and preserve
storedTokenPresent=false; then include that flag/error in the returned readiness
object (e.g., in details as storedTokenReadError or storedTokenReadErrorMessage)
and adjust message/status to surface a read-failure case (for example, when
storedTokenReadError is truthy, set status to "warning" and message to indicate
a credential-store read error). Ensure the variables referenced are
storedTokenPresent, storedTokenReadError, EncryptedFileCredentialStore, and
createLinearProjectCredentialKeys so callers can detect the read-failure vs.
missing-token cases.

---

Nitpick comments:
In `@apps/desktop/src/main/services/cto/linearCredentialService.ts`:
- Around line 143-148: persistMachineToken currently writes token before its
metadata which can leave a fresh token paired with stale metadata if a
subsequent write throws; reorder writes so writeMachineCredential calls for
machineKeys.authMode, machineKeys.refreshToken, and machineKeys.tokenExpiresAt
happen first and writeMachineCredential(machineKeys.token, ...) is executed last
in persistMachineToken, and mirror the opposite behavior in the clear routine
(delete token first) so the token is only present when its metadata is
consistent; use the existing function names persistMachineToken,
writeMachineCredential and the machineKeys identifiers to locate and update the
logic.

In `@apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx`:
- Around line 109-111: The outer cache linearIssueBrowserCache currently never
evicts entries and can grow unbounded; add a bounded-entry policy or explicit
cleanup: introduce a MAX_LINEAR_CACHE_ENTRIES constant and on every insertion
into linearIssueBrowserCache evict least-recently-used entries (use Map
iteration order as LRU by deleting the first key when size > MAX) or provide a
cleanup function that removes all entries for a given projectRoot::cto:scope
when ctoCacheScopes or nextCtoCacheScope change; update creation/access code for
LinearIssueBrowserCacheEntry to move touched keys to the Map tail to maintain
LRU ordering and ensure ctoCacheScopes/nextCtoCacheScope updates trigger removal
of stale keys.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4cce70e2-38d7-45f4-aeea-bd532513eca4

📥 Commits

Reviewing files that changed from the base of the PR and between 547b0f5 and cd3e94a.

⛔ Files ignored due to path filters (3)
  • docs/features/cto/linear-integration.md is excluded by !docs/**
  • docs/features/linear-integration/README.md is excluded by !docs/**
  • docs/features/onboarding-and-settings/README.md is excluded by !docs/**
📒 Files selected for processing (8)
  • apps/ade-cli/src/cli.ts
  • apps/ade-cli/src/headlessLinearServices.test.ts
  • apps/ade-cli/src/headlessLinearServices.ts
  • apps/desktop/src/main/services/cto/linearAuth.test.ts
  • apps/desktop/src/main/services/cto/linearCredentialKeys.ts
  • apps/desktop/src/main/services/cto/linearCredentialService.ts
  • apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx
  • apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx

Add a per-project stale-while-revalidate cache to the Linear issue browser
so the quick-view popover opens instantly and survives reopen. Quick view,
filter catalog, and issue search each show cached data immediately, dedupe
in-flight requests, and guard against stale responses with per-loader
request-id checks (incl. loadCatalog, which previously lacked one — this
prevents a stale catalog overwrite when the active project changes while the
overlay is open). Cache is bounded (90s freshness, 16 searches per scope).

Also enlarge the quick-view modal and refresh the issue detail pane (card
header, metadata pills, branch box, two-column info grid, sticky action
dock).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@arul28 arul28 force-pushed the ade/linear-issues-514a19d3 branch from cd3e94a to 0f55a82 Compare May 29, 2026 06:57
@arul28 arul28 changed the title Scope Linear credentials per project and harden the issue browser Cache and polish the Linear issue browser quick view May 29, 2026
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 29, 2026

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

…ject

Linear OAuth access tokens expire ~24h after sign-in and Linear issues a
rotating refresh token, but ADE never refreshed — so OAuth connections
(the primary sign-in path) silently broke after a day. Add automatic
refresh:

- linearTokenRefresh.ts: shared grant_type=refresh_token exchange + an
  expiry/near-expiry check.
- linearCredentialService.ensureFreshToken() (desktop) and the headless
  ade-serve credential service: refresh when at/near expiry, rotate the
  refresh token, clear the connection on invalid_grant (prompt reconnect),
  keep the token on transient failures. Concurrency-deduped.
- linearClient: refresh proactively before each GraphQL request and the
  quick-view SDK call, and reactively once on a 401, re-reading the token
  per attempt.

Also fix the Settings > Linear "GitHub reference links" (autolink) panel:

- Reload connection, repo, and team keys when the active project changes,
  so the generated `gh repo autolink` commands target the current
  project's repo and Linear workspace instead of a stale one.
- Clarify the confusing copy: explain what autolinks do, that they apply
  to this project's repo, and that Create configures it automatically
  while the shown gh command is the manual equivalent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@arul28 arul28 changed the title Cache and polish the Linear issue browser quick view Linear quick-view caching, OAuth token refresh, and project-scoped autolinks May 29, 2026
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 29, 2026

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@apps/desktop/src/renderer/components/settings/LinearSection.tsx`:
- Around line 204-206: The async loadGithubAutolinks flow can set
githubRepo/githubAutolinks from stale responses when switching projects quickly;
add a cancellation/request-id guard: create a requestId (useRef counter or local
const within the useEffect) and pass it into loadGithubAutolinks or have
loadGithubAutolinks capture it; inside detectRepo() and listRepoAutolinks()
response handlers only call setGithubRepo/setGithubAutolinks if the current
requestId matches (or if an AbortSignal is not aborted), and increment/abort the
previous request on cleanup of the effect so older promises are ignored—update
the useEffect and loadGithubAutolinks/detectRepo/listRepoAutolinks logic to
perform these checks using the request-id or AbortController to prevent stale
updates.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f8b298b5-3fac-42bb-a320-694736a63975

📥 Commits

Reviewing files that changed from the base of the PR and between cd3e94a and 7be9f36.

⛔ Files ignored due to path filters (1)
  • docs/features/cto/linear-integration.md is excluded by !docs/**
📒 Files selected for processing (9)
  • apps/ade-cli/src/headlessLinearServices.ts
  • apps/desktop/src/main/services/cto/linearAuth.test.ts
  • apps/desktop/src/main/services/cto/linearClient.ts
  • apps/desktop/src/main/services/cto/linearCredentialService.ts
  • apps/desktop/src/main/services/cto/linearTokenRefresh.test.ts
  • apps/desktop/src/main/services/cto/linearTokenRefresh.ts
  • apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx
  • apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx
  • apps/desktop/src/renderer/components/settings/LinearSection.tsx
✅ Files skipped from review due to trivial changes (1)
  • apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx

Comment thread apps/desktop/src/renderer/components/settings/LinearSection.tsx
loadGithubAutolinks lacked the request-id guard that loadStatus uses, so a
rapid active-project switch could let an older detectRepo()/listRepoAutolinks()
response land last and repopulate the repo/autolinks for the previous project —
making the displayed repo and generated `gh repo autolink` commands wrong for
the active project. Add an autolinksRequestIdRef and bail out of the stale
setState calls, mirroring the existing loadStatus pattern. (CodeRabbit review.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 29, 2026

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

Comment thread apps/desktop/src/main/services/cto/linearTokenRefresh.ts
The refresh helper flagged invalidGrant on any 400/401 or invalid_request,
which would clear (persistToken(null)) a still-valid refresh token when the
failure was actually a client-config error (invalid_client, also 400/401) or
a malformed request — forcing a needless full reconnect. Per RFC 6749 §5.2,
only error === "invalid_grant" means the refresh token itself is dead; every
other 4xx/5xx/network failure is kept and retried. Add tests covering
invalid_client (401) and invalid_request (400) -> invalidGrant false. (Greptile review.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 29, 2026

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@arul28 arul28 merged commit 01a72a8 into main May 29, 2026
28 checks passed
@arul28 arul28 deleted the ade/linear-issues-514a19d3 branch May 29, 2026 16:18
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