feat: Linear integration — attach issue context from the composer#4115
feat: Linear integration — attach issue context from the composer#4115Gigioxx wants to merge 8 commits into
Conversation
Connect a Linear workspace (personal API key) in Settings > Integrations, then pick issues from a composer picker so their context (title, state, description, recent comments, URL) is attached to the outgoing prompt as a <linear_issue> block. - Server: 3 read-only RPCs (linear.getStatus/searchIssues/getIssue) via raw GraphQL over Effect HttpClient — no new dependency. Empty search returns recently updated issues. Description/comments truncated server-side. - API key stored in ServerSecretStore (never written to settings.json; legacy plaintext keys migrate on start) and redacted from all client-facing settings paths. - Composer: picker in the footer (shown only when connected), pending chips, drafts persisted with clamped payloads, context appended at send alongside the existing element/terminal contexts. - Prompt-injection hardening: Linear-authored text is sanitized so literal <linear_issue> delimiters cannot escape the untrusted block. - Tests: serializer (13), draft-store round-trip/clamp, server settings secret persistence. Full suites green (server 1423, web 1335). Claude-Session: https://claude.ai/code/session_0128WBqER3f7gKnf13iUGcgD
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
ApprovabilityVerdict: Needs human review This PR introduces a complete new Linear integration feature with new UI components, API endpoints, settings page, and external service integration. New features of this scope and complexity warrant human review. You can customize Macroscope's approvability policy. Learn more. |
- Restore attached Linear issues (chips) when a send fails, matching the other composer contexts; add setLinearIssueContexts store action - Seed auto-title from the attached issue (identifier + title) when the first message has no text - Surface Linear search failures in the picker instead of showing an empty "No issues found" state - Make draft context ids collision-resistant across reloads (random suffix; module counter alone reset on reload while ids persist) - Guard searchIssues/getIssue against an empty API key (no blank Authorization header sent to Linear) - Commit the API-key secret-store mutation only after the settings file write succeeds, so a failed persist can't strand the previous key; covered by a write-failure test Claude-Session: https://claude.ai/code/session_0128WBqER3f7gKnf13iUGcgD
|
Addressed all six review findings in 32d0177:
Suites after the fixes: server 1424 passed | 7 skipped, web 1337 passed; typechecks clean across contracts/server/client-runtime/web. |
- Keep the issue picker open until getIssue resolves: busy state while attaching, retryable error on failure, double-select guard (previously a failed fetch silently discarded the selection) - Roll settings.json back to the prior persisted state if the secret store commit fails after the file write, keeping the two stores consistent; covered by an inverse write-failure test Claude-Session: https://claude.ai/code/session_0128WBqER3f7gKnf13iUGcgD
|
Addressed the two follow-up findings in efffbae:
Server suite: 1425 passed | 7 skipped; web suite green; typechecks clean. |
- Invalidate in-flight getIssue fetches on popover close/reopen via a request-id counter, so a canceled or superseded selection can never attach late or close a newer picker session - Scope the secret-commit-failure rollback to the linear placeholder only, keeping the rest of the persisted patch consistent with provider secrets already written earlier in the flow Claude-Session: https://claude.ai/code/session_0128WBqER3f7gKnf13iUGcgD
|
Addressed the latest round in 51a5559:
Server 1425 passed | 7 skipped, web green, typechecks clean. |
There was a problem hiding this comment.
Effect Service Conventions: one finding in the new Linear service. See the inline comment on LinearApiError.
Posted via Macroscope — Effect Service Conventions
- materializeLinearApiKey no longer fails settings reads: a secret-store read error logs a warning and degrades to disconnected, so getSettings and streamChanges (and its provider-secret materialization) survive a linear-only failure; error channel removed from its type - Restructure LinearApiError per the Effect service conventions: structured reason/status/detail/issueId fields with a derived message getter, cause reserved for real underlying failures - Invalidate in-flight searches when the picker closes so late responses can't repopulate stale results on reopen Claude-Session: https://claude.ai/code/session_0128WBqER3f7gKnf13iUGcgD
|
Latest round addressed in the newest commit:
Server 1426 passed | 7 skipped, web 1337 passed, typechecks clean. |
There was a problem hiding this comment.
One Effect service-convention issue remains around LinearApiError. The earlier feedback moved the message out of a stored Schema.String, but the revised shape still models several semantically distinct failures through a single reason discriminator whose values pick the user-facing message via a switch, and it copies raw wire payloads into a caller-visible detail field. Details inline.
Posted via Macroscope — Effect Service Conventions
Follow the Effect service conventions (mirroring BitbucketApiError): seven tagged classes combined via Schema.Union, each deriving its message from structured fields; raw response bodies and GraphQL error payloads now live only in cause, never in message-visible fields. Claude-Session: https://claude.ai/code/session_0128WBqER3f7gKnf13iUGcgD
|
Addressed both Effect Service Conventions follow-ups in c287754:
Typechecks clean across contracts/server/web; server suite 1426 passed | 7 skipped. |
Truncated descriptions/comments now stay within the 3000/800 limits instead of exceeding them by the suffix length. Claude-Session: https://claude.ai/code/session_0128WBqER3f7gKnf13iUGcgD
|
Fixed the truncation off-by-suffix in the latest commit: |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d243eed. Configure here.
| materializeProviderEnvironmentSecrets(settings).pipe( | ||
| Effect.flatMap(materializeLinearApiKey), | ||
| Effect.catch((error: ServerSettingsError) => | ||
| Effect.logWarning("failed to materialize provider environment secrets", { |
There was a problem hiding this comment.
Watcher skips Linear key migration
Medium Severity
Plaintext linear.apiKey values are only migrated to the secret store during server startup. When settings.json is reloaded via the file watcher, revalidateAndEmit refreshes the cache but never runs migration, so a plaintext key reintroduced on disk (restore, hand edit, older client) can remain in the file indefinitely while the server keeps using it.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit d243eed. Configure here.
| setError(INVALID_KEY_MESSAGE); | ||
| } | ||
| } | ||
| }, [pending, status.data, status.isPending]); |
There was a problem hiding this comment.
Connect pending never clears
Medium Severity
After a successful settings persist, the connect/disconnect flow waits for linear.getStatus to return data with a new object identity. If the refetch ends in error while the query still exposes the same status.data reference as before refresh, the effect exits early and never clears pending, leaving the row stuck on “Connecting…” or “Disconnecting…”.
Reviewed by Cursor Bugbot for commit d243eed. Configure here.
| setError(null); | ||
| } else { | ||
| setError(INVALID_KEY_MESSAGE); | ||
| } |
There was a problem hiding this comment.
Transient status failure misreported
Medium Severity
When connect persists the API key successfully but the follow-up linear.getStatus refetch fails or yields connected: false without a successful validation read, the UI always shows “Invalid API key or connection failed” even though the secret may already be stored and working.
Reviewed by Cursor Bugbot for commit d243eed. Configure here.
| images: composerImagesRef.current, | ||
| terminalContexts: composerTerminalContextsRef.current, | ||
| elementContexts: composerElementContextsRef.current, | ||
| linearIssues: composerLinearIssues, |
There was a problem hiding this comment.
Send omits Linear context
Medium Severity
getSendContext reads linearIssues from render state while terminal and element attachments use refs synced for send. A send that runs before the imperative handle picks up the latest draft can omit attached issues from the outgoing message even though chips remain in the draft store until cleared.
Reviewed by Cursor Bugbot for commit d243eed. Configure here.


Problem
When working on a Linear ticket, there's no way to bring the ticket's context into a session — you copy/paste the title, description, and relevant comments by hand, and nothing links the session to the issue. Tools like conductor.build solve this with a first-class issue picker.
What this adds
Settings → Integrations (new section): connect a Linear workspace with a personal API key. The row follows the existing provider-row format (brand icon + status dot, Connected badge, "Connected as {user} · {org}"). Connect validates against Linear's API; malformed keys (
lin_api_prefix check) fail fast client-side.Composer picker: a Linear button next to the model picker (rendered only when connected) opens a search popover. Opening it lists recently updated issues ("Recent"); typing switches to Linear's issue search. Selecting an issue attaches it as a chip; chips persist with the draft across reloads.
Prompt context: at send, each attached issue is serialized into the message as a
<linear_issue>block (identifier, title, URL, state, priority/assignee/labels when present, description, up to 8 recent comments) with an explicit untrusted-content note and a 12k-char cap.Design notes
linear.getStatus/searchIssues/getIssue) call Linear's GraphQL API directly through the existing EffectHttpClientpattern (modeled onBitbucketApi). All three are registered inRPC_REQUIRED_SCOPE(read scope).ServerSecretStore— never tosettings.json(the file holds onlyapiKeySet: true); legacy plaintext keys migrate to the secret store on server start.redactServerSettingsForClientblanks the key on every client-facing path; the UI reads connection state vialinear.getStatusonly.<linear_issue>/</linear_issue>delimiters in issue descriptions/comments cannot escape the untrusted block (defanged to‹linear_issue…).elementContextspattern at every touchpoint (draft slice, persisted schema, chips, append-at-send), so no new mechanisms were introduced.Verification
tsgo --noEmitclean in contracts, server, client-runtime, web<linear_issue>block in the outgoing message, plaintext-key migration verified on a realsettings.jsonScreen.Recording.2026-07-18.at.4.12.18.AM.mov
https://claude.ai/code/session_0128WBqER3f7gKnf13iUGcgD
Note
Medium Risk
Touches secret persistence and non-atomic settings/secret pairing (mitigated by tests and rollback), plus outbound prompt injection hardening for untrusted Linear content.
Overview
Adds end-to-end Linear integration so users can connect a workspace and attach issue context from the chat composer.
Server & contracts: New
LinearApiservice calls Linear’s GraphQL API for status, search/recent issues, and issue detail (with server-side truncation). Three WebSocket RPCs (linearGetStatus,linearSearchIssues,linearGetIssue) are wired with read scope. Settings gainlinear.apiKey/apiKeySet; the real key lives inServerSecretStore(notsettings.json), with startup migration from legacy plaintext, client redaction, deferred commit after file write, rollback on secret write failure, and graceful “disconnected” when the secret can’t be read.Web: New Settings → Integrations page to connect/disconnect via personal API key. Composer gets a Linear picker (when connected), pending issue chips, draft persistence/dedupe/clamping, and
appendLinearIssuesToPromptat send (untrusted<linear_issue>block with delimiter sanitization). Plan-implementation send paths intentionally skip linear context in v1.Reviewed by Cursor Bugbot for commit d243eed. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add Linear integration to attach issue context from the composer
/settings/integrationswhere users can connect via a personal API key (lin_api_*); the key is stored in the secret store rather than plaintext insettings.json, with migration for any pre-existing plaintext keys.linearGetStatus,linearSearchIssues,linearGetIssue) backed by a new server-sideLinearApiservice that queries Linear's GraphQL API.<linear_issue>block and appended to outgoing message text; the first attached issue can seed the thread title, and failure recovery restores issues to the draft.linearplaceholder insettings.json; all other settings changes in the same update are retained.Macroscope summarized d243eed.