Skip to content

feat(linear): import Linear issues into threads + deep two-way integration (#3703)#3711

Open
maslinedwin wants to merge 16 commits into
pingdotgg:mainfrom
maslinedwin:feat/linear-issue-import
Open

feat(linear): import Linear issues into threads + deep two-way integration (#3703)#3711
maslinedwin wants to merge 16 commits into
pingdotgg:mainfrom
maslinedwin:feat/linear-issue-import

Conversation

@maslinedwin

@maslinedwin maslinedwin commented Jul 5, 2026

Copy link
Copy Markdown

Implements #3703 (I filed that issue and offered to help implement it).

Heads-up on size & scope

I've read CONTRIBUTING.md — I know this is a large feature PR and that you're not actively taking contributions. It's ~3.6k lines because it's a full two-way integration, but it's cleanly split into 5 self-contained commits (Phase 0 → Phase 3) and I'm very happy to break it into stacked PRs (e.g. land just the read-only import first) or shrink scope to whatever you'd actually consider. Just say the word. UI screenshots / a short screen recording can be added on request.

What this does & why

Today, turning a Linear ticket into an agent task means copy-pasting context by hand. This adds a native Linear integration so you can browse/search issues, bulk-import them into threads with full context pre-filled, and — as the agent works — write status back to Linear automatically. Research into Conductor's integration showed it's deliberately launch-only (no write-back), so a two-way integration is the differentiator.

Everything runs on a Linear personal API key (stored in the existing encrypted ServerSecretStore, with a T3CODE_LINEAR_API_TOKEN env fallback). No OAuth, no public webhook URL, no new dependencies.

Features

  • Connect Linear — Settings → Linear: paste a personal API key; connection status shows the authenticated account. Token is redacted/never returned to the client.
  • Import from a folder — a Linear icon on each sidebar folder row opens a browse/search popover to import 1+ issues into a new thread (title, description/acceptance criteria, labels, priority, assignee, sub-issues, linked PRs, comments, attachments), with a combine / subtasks toggle.
  • Bulk browser (/linear) — a full-page table with team / status / assignee / project / label filters, cursor pagination, select-all + multi-select. Import as one thread per issue (parallel agents, each linked) or combined into one thread; pick the destination folder.
  • Thread ↔ issue link + badge — imported threads remember their Linear issue; a status pill on the chat/sidebar row reflects the issue's live workflow state, colored by state type.
  • Status write-backLinearSyncReactor moves the issue In Progress on agent start → In Review on PR open → Done on merge (via issueUpdate, mapped per-team by workflow-state type so it survives renamed states), plus optional progress comment and an idempotent "T3 Code" attachment link. Fully configurable/toggleable in Settings; defaults on.

Architecture (follows existing conventions)

  • Server LinearApi mirrors the Bitbucket source-control provider: an Effect Context.Service using HttpClient from effect/unstable/http for GraphQL, a tagged-error family, and T3CODE_-prefixed config. Read ops (search/list/fetch/metadata) + write mutations (issueUpdate/commentCreate/attachmentCreate).
  • Contracts are effect/Schema in packages/contracts; 14 RPCs registered in the existing WsRpcGroup, handled in ws.ts with proper auth scopes (reads → read scope, writes → operate scope).
  • The thread↔issue link is threaded through the event-sourcing stack (migration 033ProjectionThreads → decider → projector → pipeline → snapshot query → read model/shell), following the existing archived_at column-add pattern.
  • LinearSyncReactor is modeled on ThreadDeletionReactor (subscribes to streamDomainEvents; watches PR state via VcsStatusBroadcaster per linked thread's worktree), registered in OrchestrationReactor and the server layer graph. It reflects state changes back to the UI via a thread.meta.update domain event, dedupes per issue, and never regresses a stage.
  • Auth is behind a small token-provider seam so OAuth + the Linear Agent SDK ("assign/@mention T3 Code in Linear") can be added later without rework.

Commits (each self-contained)

  1. feat(linear): import Linear issues into threads — Phase 0: connect + per-folder import + formatter.
  2. feat(linear): deep-integration foundation — list/filter/metadata + mutation API + LinearIssueLink contract.
  3. feat(linear): Phase 1 bulk browser — filters, pagination, multi-select, import modes.
  4. feat(linear): Phase 2 — persist thread↔issue link + sidebar badge.
  5. feat(linear): Phase 3 — status write-back (LinearSyncReactor + settings).

Testing

  • vp check and vp run typecheck pass (all 15 packages, 0 errors); no lint errors introduced.
  • Unit tests: formatLinearIssues (single/combine/subtasks/omit-empty) and the workflow-state resolver (type/name/override/fallback) — added, green.
  • Live end-to-end against a real Linear workspace: connected a PAT; browsed/filtered issues in /linear; imported an issue (one-thread-per-issue) → the thread was created & linked, the agent started, and the issue flipped Todo → In Progress in Linear, with the thread badge reflecting "In Progress" live. The issueUpdate mutation was also validated directly (state change + revert).

Known limitations / follow-ups (called out honestly)

  • PR-open → In Review and merge → Done watch a thread's worktree; a thread created without a worktree won't auto-advance from a PR (the issueUpdate path itself is validated). The live E2E confirmed the start transition; the PR/merge transitions are unit-tested + typechecked but weren't driven with a real merge.
  • Per-team workflow-state mapping is wired end-to-end but the settings page currently exposes the toggles; mapping falls back to sensible type/name-based defaults.
  • OAuth + actor=app + the Linear Agent Interaction SDK (delegate/@mention T3 Code inside Linear) are intentionally deferred; the code is structured to add them without rework.

Docs: added docs/integrations/linear.md. Happy to adjust scope, split this up, or add screenshots/video — whatever makes it reviewable for you.


Note

High Risk
Large feature touching auth-scoped external API calls, orchestration events, DB migration, and automatic third-party issue mutations; in-process sync dedupe resets on server restart.

Overview
Adds a Linear personal API key integration end-to-end: server GraphQL client, WebSocket RPCs, token storage, and clients on web and mobile.

Import: Browse/search issues from Settings, a /linear page (web), sidebar popover, or mobile import sheet. Selected issues become threads with formatted context; users can combine into one thread or start one per issue. Thread creation/bootstrap now accepts an optional linearIssue link, persisted via migration 033 (linked_linear_issue_json) through the orchestration stack and shown as badges in sidebar/thread lists. Manual mark done in Linear is available from thread menus.

Write-back: LinearSyncReactor reacts to thread lifecycle and VCS PR events to move linked issues (In Progress → In Review → Done), optionally post comments, and refresh thread metadata; toggles live under Linear settings. completeLinearThreadIssue RPC handles explicit completion.

Mobile fork builds: app.config.ts gains env overrides for iOS bundle ID, EAS project, Apple team, and Expo owner.

Reviewed by Cursor Bugbot for commit 5ab69b5. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add deep two-way Linear integration for importing issues into threads and syncing status

  • Adds a full Linear integration across web, mobile, and server: users can search/browse Linear issues, select them, and import into new threads (one per issue or combined) via LinearBrowsePopover and LinearImportRouteScreen.
  • Introduces 14 new WebSocket RPC endpoints covering Linear auth, issue search/fetch/list, team/state/project/label/user listing, issue state mutation, comment/attachment creation, and token management.
  • Adds a LinearSyncReactor that automatically transitions linked Linear issue states on thread lifecycle events (start, PR open, merge), optionally posts a comment on start, and reflects state back to thread metadata.
  • Thread rows in sidebar (web) and thread list (mobile) now display a Linear issue badge and a context menu action to mark the linked issue as done.
  • Adds Linear settings panels on web and mobile for connecting a personal API token, viewing connection status, and toggling sync behavior (autoSync, transitionOnStart, transitionOnPrOpen, transitionOnMerge, postComments).
  • Persists linked_linear_issue_json on the projection_threads table via migration 033; threads carry a linearIssue field through the full event/projection/snapshot pipeline.
  • Risk: LinearSyncReactor lifecycle transitions and comment posting are best-effort (failures logged at warning level); in-memory rank map preventing regressive transitions is lost on server restart.

Macroscope summarized 5ab69b5.

Adds a native Linear integration for the web app (issue pingdotgg#3703):

- contracts: Linear issue/auth schemas + 5 WS RPCs
- server: LinearApi Effect service (GraphQL over HttpClient), PAT stored
  via ServerSecretStore with T3CODE_LINEAR_API_TOKEN env fallback
- client-runtime/web: Linear atom family + state wiring
- Settings -> Linear page to connect/disconnect a personal API key
- per-folder Linear icon in the sidebar opening a browse/search/select
  popover with combine/subtasks toggle
- import flow: fetch issues -> format markdown -> new draft thread ->
  pre-fill composer (formatLinearIssues, unit tested)
- docs/integrations/linear.md
…+ link contract

- contracts: LinearIssueFilter, paginated LinearListIssues, team/state/project/
  label/user metadata, write-mutation inputs, LinearIssueLink; 9 new RPCs
- server LinearApi: listIssues (filter+cursor), listTeams/workflowStates/projects/
  labels/users, updateIssueState/createComment/createAttachment; ws handlers+scopes
- client-runtime: list/metadata query families + mutation commands
- orchestration contract: optional linearIssue link on thread/shell + create/meta
  commands + turn-start bootstrap (persistence wiring follows)
…t, import modes

- /linear full-page browser: team/status/assignee filters, cursor pagination
  (Load more), select-all + multi-select table
- import control: one-thread-per-issue (real started threads, each linked) or
  combine (draft) + destination-folder picker
- entry points: command-palette 'Browse Linear issues' + popover 'Browse all'
- useLinearImport gains perIssue mode via startTurn bootstrap
- migration 033 + ProjectionThreads schema/SQL store linked_linear_issue_json
- thread linearIssue flows through decider → thread.created/meta-updated →
  projector → projection pipeline → snapshot query (read model + shell + detail)
- turn-start bootstrap.createThread carries linearIssue
- LinearIssueBadge on sidebar thread rows, colored by workflow-state type
  (advances In Progress → In Review → Done as status is written back)
- ServerSettings.linear: autoSync + per-transition toggles + comment/attachment
  toggles + per-team state-mapping override (+ patch)
- linearStateMapping resolver: workflow-state type/name → target stateId, with
  per-team override (unit tested, 7 cases)
- LinearSyncReactor: agent start → In Progress; PR open → In Review; merge →
  Done (via VcsStatusBroadcaster per-thread watch); dedupes per issue, posts
  optional comment/attachment, reflects state to the thread badge via
  thread.meta.update. Registered in OrchestrationReactor + server layer graph
- Settings UI: Status write-back toggles on the Linear settings page
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c4f8dccf-b6e1-4997-bd4b-34447c7300e2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jul 5, 2026
Comment thread apps/server/src/orchestration/Layers/LinearSyncReactor.ts Outdated
Comment thread apps/server/src/orchestration/Layers/LinearSyncReactor.ts
Comment thread apps/web/src/hooks/useLinearImport.ts
Comment thread apps/server/src/orchestration/Layers/LinearSyncReactor.ts Outdated
Comment thread apps/web/src/hooks/useLinearImport.ts
Comment thread apps/web/src/components/linear/LinearBrowser.tsx Outdated
Comment thread apps/server/src/orchestration/Layers/LinearSyncReactor.ts
Comment thread apps/web/src/components/linear/LinearBrowser.tsx Outdated
Comment thread apps/server/src/linear/linearStateMapping.ts
Comment thread apps/server/src/linear/LinearApi.ts
Comment thread apps/web/src/hooks/useLinearImport.ts
Comment thread apps/web/src/components/linear/LinearBrowser.tsx
Comment thread apps/server/src/linear/LinearApi.ts
Comment thread apps/server/src/orchestration/Layers/LinearSyncReactor.ts
Comment thread apps/web/src/components/settings/LinearSettings.tsx
Comment thread apps/web/src/hooks/useLinearImport.ts
Comment thread apps/web/src/components/linear/LinearBrowser.tsx
Comment thread packages/client-runtime/src/state/linear.ts
@macroscopeapp

macroscopeapp Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

3 blocking correctness issues found. Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

- LinearSyncReactor: inline the service interface in Context.Service per Effect
  conventions (drop standalone LinearSyncReactorShape)
- reactor: key PR watchers by worktree so a later/changed worktree re-registers
  instead of being ignored (High); set appliedRank after the write + badge
  reflect so a failed issueUpdate can retry
- drop the link-attachment write-back (+ setting/UI): it linked the issue to
  itself; a real thread URL isn't available server-side without OAuth/hosting
- LinearBrowser: replace (not merge) rows when the filter changes so stale pages
  can't leak across filters; only offer 'Load more' when an endCursor exists
- perIssue import: attempt all issues and report a summary instead of bailing
  mid-loop; surface issues that couldn't be loaded
@maslinedwin

Copy link
Copy Markdown
Author

Thanks for the automated review — pushed fixes in 1f04d05.

Macroscope (Effect Service Conventions): LinearSyncReactor now inlines its interface in Context.Service and references LinearSyncReactor["Service"]["start"] instead of a standalone …Shape type.

Cursor Bugbot (6 of 8 addressed):

  • PR watcher ignores worktree changes (High) — watchers are now keyed by threadId:worktreePath, so a thread that later gets/changes a worktree registers a fresh watcher.
  • Wrong URL on link attachment — removed the attachment write-back (and its setting/UI); it linked the issue to itself, and a real thread URL isn't available server-side without OAuth/hosting. Left as a follow-up for the OAuth phase.
  • Filter change merges stale page — rows now replace (not merge) when the filter key changes, via a ref tracking which filter the accumulated rows belong to.
  • Load more without cursor repeats page — "Load more" only shows / advances when the API returned an endCursor.
  • Partial import leaves orphan threadsperIssue now attempts every issue and returns a summary (created N of M, plus any that failed / couldn't be loaded) instead of bailing mid-loop.
  • Rank set before badge updateappliedRank is now set after the write and the best-effort badge reflect.

Intentionally left:

  • Same issue rank blocks second thread — per-issue monotonic state is deliberate: a Linear issue has one workflow state, so we don't want two threads racing it backward.
  • Silent drop of missing issue IDs (combine mode)perIssue now surfaces missing issues; combine still imports what Linear returns, which is the expected behavior.

Full recursive typecheck + lint pass and unit tests are green after the changes.

Comment thread packages/contracts/src/settings.ts
Comment thread apps/web/src/components/linear/LinearBrowser.tsx
Comment thread apps/web/src/hooks/useLinearImport.ts
Partial perIssue imports (some threads created, or some issues not loaded) now
return ok:true with a `warning`, so consumers navigate and show a non-blocking
warning toast instead of a full-failure error. Only a total failure returns
ok:false. (addresses Bugbot follow-up)
Comment thread apps/web/src/components/linear/LinearBrowser.tsx
Server settings updates deepMerge (never delete missing keys), so a nested
patch could add but not remove team state-mapping overrides. applyServerSettingsPatch
now replaces linear.stateMappingByTeam wholesale (like providerInstances), so an
empty map clears stale overrides. (addresses Macroscope review)
…rtial import

- probeAuth now only swallows LinearAuthError (rejected token → unauthenticated)
  and lets LinearRequestError propagate, so a transient outage/misconfig isn't
  shown as 'Not connected' (Settings surfaces the error). Widened setToken/
  clearToken error channels + clearToken RPC union accordingly.
- useLinearImport returns failedIds; LinearBrowser/LinearBrowsePopover keep just
  the failed issues selected (and don't navigate/close) on a partial import so
  they can be retried without re-running the ones that already succeeded.
(addresses Macroscope + Bugbot follow-ups)
Comment thread apps/web/src/components/LinearBrowsePopover.tsx
Comment thread apps/web/src/components/LinearBrowsePopover.tsx
Comment thread apps/server/src/linear/LinearApi.ts Outdated
Comment thread packages/shared/src/serverSettings.ts
… on toggle

- setToken/clearToken no longer fail on a post-write probe outage (probeAuth
  stays strict for the status query; the writes swallow LinearRequestError and
  report a best-effort status). Reverted their error channels + clearToken RPC.
- LinearBrowsePopover surfaces auth query errors ('Couldn't reach Linear')
  before the 'not connected' prompt, and prunes selections that leave the
  current result set so Import can't act on invisible issues.
- Linear settings toggles send only the changed key via the server settings
  command, so stateMappingByTeam is no longer carried (and wiped) on every
  toggle; explicit map edits still replace wholesale.
(addresses Macroscope + Bugbot follow-ups on the prior fixes)
Comment thread apps/web/src/components/settings/LinearSettings.tsx
Comment thread apps/web/src/components/LinearBrowsePopover.tsx
Comment thread apps/web/src/components/LinearBrowsePopover.tsx Outdated
Comment thread apps/server/src/linear/LinearApi.ts

@macroscopeapp macroscopeapp 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.

One convention issue: recovering a single known tagged failure should use Effect.catchTags rather than Effect.catch with a manual _tag check. See inline comments.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/linear/LinearApi.ts Outdated
Comment thread apps/server/src/linear/LinearApi.ts Outdated
…ection polish

- probeAuth/probeAuthLenient use Effect.catchTags to recover specific error
  tags (Effect Service Conventions) instead of Effect.catch + manual _tag
- probeAuth only treats auth-message GraphQL errors as a rejected token;
  other 200-with-errors responses propagate as LinearRequestError (outage)
- Settings save: a stored-but-unverified token (outage) is a saved success
  with a warning, not a connect failure
- import retry-selection: keep only failed issues still visible in the current
  results/rows selected, so no un-clearable off-screen selection and the popover
  prune can't drop them
Comment thread apps/web/src/components/LinearBrowsePopover.tsx
Only narrow the selection to failed issues when the hook actually reports
failedIds; a total transient failure (e.g. fetchIssues failed, nothing
imported) now keeps the current selection so it can be retried as-is.
@maslinedwin

Copy link
Copy Markdown
Author

One more note: I'm genuinely happy to take this in whatever direction the team wants — reshape the scope, split it into smaller stacked PRs, adjust the UX/architecture, drop pieces, or rework anything to fit how you'd want a Linear integration to look in T3 Code. Just point me at the changes and I'll turn them around.

More than that, I'd love to actually collaborate on bringing this Linear integration into the platform properly, not just land a one-off PR. If there's appetite for it, I'm keen to help shape and maintain it alongside you all.

cc @juliusmarminge — would love your thoughts on whether this is a direction worth pursuing, and how you'd want it scoped.

…issue done'

- perIssue imports now start threads on Claude Opus 4.8 (claudeAgent /
  claude-opus-4-8) instead of the project/Codex default.
- New linear.completeThreadIssue RPC: resolves the team's completed state,
  writes it back via issueUpdate, and reflects it on the thread (badge → Done)
  via thread.meta.update. Surfaced as a 'Mark <ID> done in Linear' item in the
  thread right-click menu, shown only when the thread has a linked issue.
Comment thread apps/server/src/ws.ts
cause,
}),
),
);

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.

RPC fails after Linear updates

High Severity

The completeLinearThreadIssue RPC reports a failure if the internal thread.meta.update dispatch fails, even when the Linear issue has been successfully updated. This causes the client to incorrectly indicate the action failed and potentially display stale UI, despite the issue being marked as done in Linear.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 08fa28b. Configure here.

Comment thread apps/server/src/ws.ts
),
);
}
return { success: 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.

Manual done skips reactor rank

High Severity

Marking a linked issue done via completeLinearThreadIssue updates Linear but never advances LinearSyncReactor's per-issue appliedRank. A later agent start can still run the "started" transition and move a Done issue back to In Progress.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 08fa28b. Configure here.

description:
result._tag === "Failure"
? "Linear rejected the update — check the connection in Settings."
: "No completed state is configured for this team.",

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.

Complete action mislabels failures

Medium Severity

The toast messages for completing a Linear issue are too generic. The client-side logic incorrectly interprets completeLinearIssue results: it maps various server-side failures (like missing Linear links or API rejections) to "No completed state is configured," and some internal failures to "Linear rejected the update." This gives users misleading feedback.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 08fa28b. Configure here.

- share formatLinearIssues via @t3tools/client-runtime/linear-format (web re-exports)
- mobile: linearEnvironment atoms; Settings → Linear (connect token, status,
  write-back toggles); Linear import sheet (search, multi-select, per-issue /
  combine, Opus 4.8, linked threads); thread-row Linear badge + long-press
  'Mark <ID> done' via linear.completeThreadIssue; LinearIcon; screens wired
  into the native-stack navigator + settings sheet
- projectThreadStartTurn carries linearIssue through the bootstrap
export function SettingsLinearRouteScreen() {
const navigation = useNavigation();
const { environments } = useEnvironments();
const environmentId = environments[0]?.environmentId ?? null;

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.

🟡 Medium settings/SettingsLinearRouteScreen.tsx:22

SettingsLinearRouteScreen sets environmentId to environments[0]?.environmentId, so every Linear action on this screen — connect, disconnect, toggle sync settings, and open import — targets whichever environment happens to be first in presentationById iteration order. In a multi-environment setup this causes writes to the wrong server and displays the wrong auth status. Consider deriving environmentId from the user's current/selected environment instead of assuming the first entry is the intended one.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/settings/SettingsLinearRouteScreen.tsx around line 22:

`SettingsLinearRouteScreen` sets `environmentId` to `environments[0]?.environmentId`, so every Linear action on this screen — connect, disconnect, toggle sync settings, and open import — targets whichever environment happens to be first in `presentationById` iteration order. In a multi-environment setup this causes writes to the wrong server and displays the wrong auth status. Consider deriving `environmentId` from the user's current/selected environment instead of assuming the first entry is the intended one.

Comment thread apps/mobile/src/Stack.tsx

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.

🟡 Medium

const WORKSPACE_OVERLAY_ROUTES = new Set([

The new LinearImport route is presented as a formSheet overlay but is missing from WORKSPACE_OVERLAY_ROUTES. While the import sheet is open, workspacePathFromState returns /linear-import instead of the underlying workspace path, so AdaptiveWorkspaceLayout drops the active thread selection and recomputes pane behavior as if the user navigated away. Add "LinearImport" to the WORKSPACE_OVERLAY_ROUTES set alongside the other sheet routes.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/Stack.tsx around line 231:

The new `LinearImport` route is presented as a `formSheet` overlay but is missing from `WORKSPACE_OVERLAY_ROUTES`. While the import sheet is open, `workspacePathFromState` returns `/linear-import` instead of the underlying workspace path, so `AdaptiveWorkspaceLayout` drops the active thread selection and recomputes pane behavior as if the user navigated away. Add `"LinearImport"` to the `WORKSPACE_OVERLAY_ROUTES` set alongside the other sheet routes.

[iconColor, placeholderColor, selected, toggle],
);

const connected = search.error === null;

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.

🟡 Medium linear/LinearImportRouteScreen.tsx:192

connected is computed as search.error === null, but when environmentId is null the screen passes null to useEnvironmentQuery, which returns an empty AsyncResult whose error is also null. As a result, connected is true even with no environment configured, so the empty state renders No issues found. instead of the intended Connect Linear in Settings to import issues. message and hides the disconnected state from the user. Consider deriving connected from the environment being present (e.g. environmentId !== null && search.error === null).

Also found in 1 other location(s)

apps/mobile/src/features/settings/SettingsLinearRouteScreen.tsx:46

handleConnect only clears the input and refreshes authQuery when setToken returns status === &#34;authenticated&#34;. On the server, linear.setToken persists the token first and then may return status: &#34;unauthenticated&#34; when verification cannot reach Linear (&#34;Saved, but couldn&#39;t reach Linear to verify the token.&#34;). In that reachable case, the mobile UI never refreshes and continues to show Not connected, even though the token was saved successfully, so users get a false failure state and no indication that the integration may already be configured.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/linear/LinearImportRouteScreen.tsx around line 192:

`connected` is computed as `search.error === null`, but when `environmentId` is `null` the screen passes `null` to `useEnvironmentQuery`, which returns an empty `AsyncResult` whose `error` is also `null`. As a result, `connected` is `true` even with no environment configured, so the empty state renders `No issues found.` instead of the intended `Connect Linear in Settings to import issues.` message and hides the disconnected state from the user. Consider deriving `connected` from the environment being present (e.g. `environmentId !== null && search.error === null`).

Also found in 1 other location(s):
- apps/mobile/src/features/settings/SettingsLinearRouteScreen.tsx:46 -- `handleConnect` only clears the input and refreshes `authQuery` when `setToken` returns `status === "authenticated"`. On the server, `linear.setToken` persists the token first and then may return `status: "unauthenticated"` when verification cannot reach Linear (`"Saved, but couldn't reach Linear to verify the token."`). In that reachable case, the mobile UI never refreshes and continues to show `Not connected`, even though the token was saved successfully, so users get a false failure state and no indication that the integration may already be configured.

} else {
await start(formatLinearIssues(details, "combine"), null);
}
navigation.goBack();

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.

Import dismisses despite failures

High Severity

The handleImport function in LinearImportRouteScreen always navigates away after an import attempt, even if fetchIssues fails, returns fewer issues than selected, or if startTurn calls fail. This results in silent failures where the screen closes as if the import was fully successful, without user feedback.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2638ae9. Configure here.

<LinearIcon size={28} />
<Text className="text-center text-base text-foreground-muted">
{connected ? "No issues found." : "Connect Linear in Settings to import issues."}
</Text>

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.

Search errors mimic disconnect

Medium Severity

The Linear import and settings screens incorrectly conflate API errors (e.g., network issues) with an unauthenticated state. This causes misleading UI messages, like "Connect Linear in Settings" or "Not connected," when the actual problem is service unavailability.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2638ae9. Configure here.

else next.add(id);
return next;
});
}, []);

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.

Hidden selections still import

Medium Severity

The selected state for Linear issues isn't pruned when search results update. This can lead to importing issues that are no longer visible in the list, creating unexpected threads.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2638ae9. Configure here.


if (perIssue) {
for (const detail of details) {
await start(formatLinearIssues([detail], "combine"), issueLink(detail));

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.

Imported threads wrong titles

Medium Severity

Per-issue Linear imports create threads with titles derived from the full prompt text, including boilerplate, rather than the Linear issue's identifier and title, unlike the web client.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2638ae9. Configure here.

}
} finally {
setBusy(false);
}

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.

Connect ignores saved token

Medium Severity

After setToken, the handler only clears the field and refreshes auth when result.value.status === "authenticated". If the server saves the token but returns unauthenticated from a lenient post-save probe (e.g. outage message), the UI acts as if connect failed even though the token was stored.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2638ae9. Configure here.

app.config.ts honors EXPO_OWNER, EAS_PROJECT_ID, APPLE_TEAM_ID, and
T3CODE_IOS_BUNDLE_ID env overrides (all defaulting to the upstream values), so a
fork can build and ship to its own TestFlight without editing the config.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

There are 9 total unresolved issues (including 8 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5ab69b5. Configure here.

Comment thread apps/mobile/app.config.ts
const iosBundleIdentifier = process.env.T3CODE_IOS_BUNDLE_ID ?? variant.iosBundleIdentifier;
const easProjectId = process.env.EAS_PROJECT_ID ?? "d763fcb8-d37c-41ea-a773-b54a0ab4a454";
const appleTeamId = process.env.APPLE_TEAM_ID ?? "ARK85ZXQ4Z";
const expoOwner = process.env.EXPO_OWNER ?? "pingdotgg";

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.

Empty env bypasses defaults

Medium Severity

The new fork overrides use ?? on process.env, so an empty string (e.g. a placeholder in .env / .env.local merged via loadRepoEnv) is treated as set and replaces the documented upstream defaults. That can yield an empty ios.bundleIdentifier, a broken updates.url, or invalid EAS owner/project values instead of falling back like “unset”.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5ab69b5. Configure here.

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

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant