feat: Linear integration — attach issue context from the composer#4115
feat: Linear integration — attach issue context from the composer#4115Gigioxx wants to merge 10 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 substantial new Linear integration feature with new API endpoints, UI components, settings page, and modifications to the message send path. New features of this scope require human review. Additionally, there's an unresolved bug report about Linear context potentially being omitted during send. 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.
| 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.
…n settings reload A status refetch that fails keeps the previous data identity, so the connect/disconnect resolve effect never fired and the row stayed stuck on a pending label; it now also settles when the refetch errors. The plaintext linear.apiKey migration now runs on watcher-driven settings reloads too, so keys reintroduced on disk mid-run are secured without a restart. Claude-Session: https://claude.ai/code/session_012QZJ4RkpkjetR9G7Vwub5f
|
Addressed the latest Bugbot round in e29e269:
Verification: server suite 1431 passed | 7 skipped; web suite 1349 passed; all typechecks clean. |
The row's typed key, error, pending state, and settle refs survived a primary-environment switch, so a key entered for one environment could be persisted into another. Keying the row on the environment id resets all of it atomically on switch. Claude-Session: https://claude.ai/code/session_012QZJ4RkpkjetR9G7Vwub5f
|
Fixed the environment-switch state leak in 8f24593. The row is now keyed on the primary environment id, so switching environments remounts it — this resets not just the typed key, error, and pending state the finding calls out, but also the settle refs ( |


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 storage and non-atomic settings/secret updates for API keys, plus outbound prompt content from an external API; mitigated by rollback, redaction, and sanitization tests.
Overview
Adds a Linear integration so users can connect a workspace and attach issues to chat prompts without copy-pasting.
Server & contracts: New
LinearApicalls Linear’s GraphQL API forgetStatus,searchIssues, andgetIssue, exposed as three read-scoped WebSocket RPCs. Shared types and errors live in@t3tools/contracts; the web app gets matching query atoms via@t3tools/client-runtime/state/linear.API key handling:
settings.jsononly keepslinear.apiKeySet; the real key is written toServerSecretStore, with migration from legacy plaintext keys on startup and on settings reload. Updates defer secret commits until after the settings file write and roll back the linear placeholder if the secret write fails; unreadable secrets degrade to disconnected instead of failing all settings reads.Settings UI: New Integrations section (
/settings/integrations) to connect/disconnect with prefix validation and post-save status refetch.Composer: When connected, a footer Linear picker searches or lists recent issues, fetches details, and adds removable chips. Drafts persist
linearIssueslike element context; on send, issues append as a bounded<linear_issue>block with an untrusted-context note and delimiter sanitization. Send failures restore attached issues; plan-implementation sends ignore linear context in v1.Reviewed by Cursor Bugbot for commit 8f24593. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add Linear integration to attach issue context from the composer
LinearApiservice withgetStatus,searchIssues, andgetIssueoperations backed by the Linear GraphQL API, exposed as three new WebSocket RPC methods (linearGetStatus,linearSearchIssues,linearGetIssue).settings.json; migrates any plaintext key found on disk at startup and rolls back the settings file if the secret write fails.linearIssuesslice supporting add (with deduplication and field clamping), remove, replace, and clear; issues are persisted and hydrated across sessions.ComposerLinearIssuePickerpopover with debounced search and aComposerPendingLinearIssueschip list to theChatComposer; attached issues are serialized into a<linear_issue>block appended to the outgoing prompt inChatViewContent./settings/integrationsroute andIntegrationsSettingspage where users can connect or disconnect Linear by entering a personal API key (validated against thelin_api_prefix).getSettingsandstreamChangescall; failures are silenced and degrade to a disconnected state rather than surfacing errors.Macroscope summarized 8f24593.