feat(linear): add multi-workspace support - #30314
Conversation
Adds multi-workspace support to the Linear extension. See PR description for the full feature list and design notes. Builds on raycast/utils#80 via a temporary manual OAuth service construction behind a code-level switch.
|
Thank you for your first contribution! 🎉 🔔 @thomaslombart @thomaspaulmann @peduarte @pernielsentikaer @clemb8 @jfkisafk @filipszczepanski @JoeBoulton @itsmingjie @xmok @0xdhrv @erikjs14 @alexibuild you might want to have a look. You can use this guide to learn how to check out the Pull Request locally in order to test it. 📋 Quick checkout commandsBRANCH="feat/linear-multi-workspace"
FORK_URL="https://github.com/smorin/extensions.git"
EXTENSION_NAME="linear"
REPO_NAME="extensions"
git clone -n --depth=1 --filter=tree:0 -b $BRANCH $FORK_URL
cd $REPO_NAME
git sparse-checkout set --no-cone "extensions/$EXTENSION_NAME"
git checkout
cd "extensions/$EXTENSION_NAME"
npm install && npm run devWe're currently experiencing a high volume of incoming requests. As a result, the initial review may take up to 15 business days. |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds multi-workspace support for the Linear Raycast extension, enabling multiple concurrent OAuth sessions (including same workspace across different accounts) with consistent workspace switching across UI surfaces, menu bar aggregation, and AI tool routing.
Changes:
- Introduces a workspace registry + per-(workspace, account) OAuth providerId strategy, plus a new Manage Workspaces command.
- Adds workspace switchers (list dropdown + form field), per-workspace persisted UI defaults, and menu bar unread notifications aggregated across workspaces.
- Routes AI tools per call via
workspaceId, addsget-workspaces, and updates tool auth to be non-interactive.
Reviewed changes
Copilot reviewed 99 out of 99 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| extensions/linear/src/unread-notifications.tsx | Aggregates unread notifications across all connected workspaces in the menu bar. |
| extensions/linear/src/tools/update-project-milestone.ts | Adds optional workspaceId routing + non-interactive tool auth. |
| extensions/linear/src/tools/update-issue.ts | Adds optional workspaceId routing + workspace-aware confirmations. |
| extensions/linear/src/tools/update-document.ts | Adds optional workspaceId routing + workspace-aware confirmations. |
| extensions/linear/src/tools/update-comment.ts | Adds optional workspaceId routing + workspace-aware confirmations and attachment uploads. |
| extensions/linear/src/tools/resolveToolWorkspace.ts | Implements per-call workspace routing for AI tools and non-interactive auth wrapper. |
| extensions/linear/src/tools/remove-label.ts | Adds optional workspaceId routing + workspace-aware confirmations. |
| extensions/linear/src/tools/get-workspaces.ts | New tool to list connected workspaces without triggering OAuth. |
| extensions/linear/src/tools/get-teams.ts | Adds optional workspaceId routing for AI tools. |
| extensions/linear/src/tools/get-projects.ts | Adds optional workspaceId routing for AI tools. |
| extensions/linear/src/tools/get-project-updates.ts | Adds optional workspaceId routing for AI tools. |
| extensions/linear/src/tools/get-project-statuses.ts | Adds optional workspaceId routing for AI tools. |
| extensions/linear/src/tools/get-notifications.ts | Adds optional workspaceId routing for AI tools. |
| extensions/linear/src/tools/get-members.ts | Adds optional workspaceId routing for AI tools. |
| extensions/linear/src/tools/get-labels.ts | Adds optional workspaceId routing for AI tools. |
| extensions/linear/src/tools/get-issue-states.ts | Adds optional workspaceId routing for AI tools. |
| extensions/linear/src/tools/get-initiatives.ts | Adds optional workspaceId routing and exposes API helper with client override. |
| extensions/linear/src/tools/get-documents.ts | Adds optional workspaceId routing and exposes API helper with client override. |
| extensions/linear/src/tools/get-document-content.ts | Adds optional workspaceId routing and exposes API helper with client override. |
| extensions/linear/src/tools/get-current-user.ts | Adds optional workspaceId routing for AI tools. |
| extensions/linear/src/tools/full-text-search-issues.ts | Adds optional workspaceId routing for AI tools. |
| extensions/linear/src/tools/formatConfirmation.ts | Makes confirmations workspace-aware via explicit client parameter. |
| extensions/linear/src/tools/filter-issues.ts | Adds optional workspaceId routing for AI tools. |
| extensions/linear/src/tools/create-project-update.ts | Adds optional workspaceId routing + workspace-aware confirmations. |
| extensions/linear/src/tools/create-issue.ts | Adds optional workspaceId routing + workspace-aware confirmations. |
| extensions/linear/src/tools/create-document.ts | Adds optional workspaceId routing + workspace-aware confirmations. |
| extensions/linear/src/tools/create-comment.ts | Adds optional workspaceId routing + workspace-aware confirmations and attachment uploads. |
| extensions/linear/src/tools/add-label.ts | Adds optional workspaceId routing + workspace-aware confirmations. |
| extensions/linear/src/search-issues.tsx | Adds workspace dropdown accessory to list search UI. |
| extensions/linear/src/search-custom-views.tsx | Adds workspace dropdown accessory to custom view search UI. |
| extensions/linear/src/quick-add-comment-to-issue.ts | Adds one-shot workspace argument resolution + subtitle refresh mechanism. |
| extensions/linear/src/notifications.tsx | Adds workspace dropdown accessory and makes menu bar refresh best-effort. |
| extensions/linear/src/manage-workspaces.tsx | New UI to add/switch/re-auth/logout workspaces with staging-token recovery. |
| extensions/linear/src/hooks/useWorkspaceCachedState.ts | New helper to scope persisted UI state by workspace key. |
| extensions/linear/src/hooks/useUsers.ts | Scopes cached users queries by workspace. |
| extensions/linear/src/hooks/useTeams.ts | Scopes cached teams queries by workspace. |
| extensions/linear/src/hooks/useStates.ts | Scopes cached workflow states queries by workspace. |
| extensions/linear/src/hooks/useSearchIssues.ts | Scopes cached issue search by workspace. |
| extensions/linear/src/hooks/useProjects.ts | Scopes cached projects pagination by workspace. |
| extensions/linear/src/hooks/useProjectUpdates.ts | Scopes cached project updates by workspace. |
| extensions/linear/src/hooks/useProjectStatuses.ts | Scopes cached project statuses by workspace. |
| extensions/linear/src/hooks/usePriorities.ts | Scopes cached priority values by workspace. |
| extensions/linear/src/hooks/useNotifications.ts | Scopes cached notifications by workspace. |
| extensions/linear/src/hooks/useMilestones.ts | Scopes cached milestones by workspace. |
| extensions/linear/src/hooks/useMe.ts | Scopes cached viewer by workspace. |
| extensions/linear/src/hooks/useLabels.ts | Scopes cached labels by workspace. |
| extensions/linear/src/hooks/useIssues.ts | Scopes cached issue lists by workspace. |
| extensions/linear/src/hooks/useIssueTemplates.ts | Scopes cached issue templates by workspace. |
| extensions/linear/src/hooks/useIssueDetail.ts | Scopes cached issue detail by workspace. |
| extensions/linear/src/hooks/useIssueComments.ts | Scopes cached issue comments by workspace. |
| extensions/linear/src/hooks/useInitiatives.ts | Scopes cached initiatives by workspace. |
| extensions/linear/src/hooks/useDocuments.ts | Scopes cached documents + content by workspace. |
| extensions/linear/src/hooks/useCycles.ts | Scopes cached cycles queries by workspace. |
| extensions/linear/src/hooks/useCustomViews.ts | Scopes cached custom views + issues by workspace. |
| extensions/linear/src/hooks/useAllWorkspaceNotifications.ts | New hook to fetch notifications per workspace for menu bar aggregation. |
| extensions/linear/src/helpers/workspaceArgument.ts | Implements workspace argument matching + command subtitle updates. |
| extensions/linear/src/helpers/refreshQuickSubtitles.ts | Adds best-effort background launches to refresh quick-command subtitles. |
| extensions/linear/src/helpers/createIssueForMyself.ts | Extracts “create issue for myself” into a reusable helper supporting client overrides. |
| extensions/linear/src/favorites.tsx | Adds workspace dropdown and scopes favorites caching by workspace. |
| extensions/linear/src/created-issues.tsx | Adds workspace dropdown accessory. |
| extensions/linear/src/create-project.tsx | Adds draft-time workspace pinning and loading gating for project creation flow. |
| extensions/linear/src/create-issue.tsx | Adds draft-time workspace pinning and loading gating for issue creation flow. |
| extensions/linear/src/create-issue-for-myself.ts | Adds workspace subtitle refresh + workspace-aware auth wrapper. |
| extensions/linear/src/create-issue-for-myself-in-workspace.ts | New quick command to create an issue explicitly in a named workspace. |
| extensions/linear/src/components/docs/DocumentList.tsx | Adds workspace switch section to entity dropdown + per-workspace persisted entity selection. |
| extensions/linear/src/components/WorkspaceDropdown.tsx | New list/form workspace dropdown components + embedded section for existing dropdowns. |
| extensions/linear/src/components/WorkspaceContext.tsx | New context/provider handling workspace switching and remount boundary. |
| extensions/linear/src/components/View.tsx | Wraps commands in WorkspaceProvider and updates auth handling for multi-workspace. |
| extensions/linear/src/components/ProjectList.tsx | Adds workspace switch section to initiative dropdown + per-workspace persisted initiative. |
| extensions/linear/src/components/ProjectIssues.tsx | Scopes milestone filter persistence by workspace. |
| extensions/linear/src/components/EditIssueForm.tsx | Removes storeValue from milestone dropdown (workspace-scoped persistence now handled elsewhere). |
| extensions/linear/src/components/CreateProjectForm.tsx | Adds workspace form field + per-workspace persisted defaults (status/lead). |
| extensions/linear/src/components/CreateMilestoneForm.tsx | Adds per-workspace persisted project selection and removes storeValue. |
| extensions/linear/src/components/CreateIssueForm.tsx | Adds workspace form field + per-workspace persisted defaults with readiness semantics. |
| extensions/linear/src/assigned-issues.tsx | Embeds workspace switch section into view dropdown. |
| extensions/linear/src/api/workspaces.ts | New workspace registry (LocalStorage) with migration/adoption of existing single-login slot. |
| extensions/linear/src/api/withWorkspaceAuth.ts | New wrapper reusing withAccessToken but bootstrapping workspace auth. |
| extensions/linear/src/api/updateNotification.ts | Adds optional client override for workspace routing. |
| extensions/linear/src/api/updateIssue.ts | Adds optional client override for workspace routing. |
| extensions/linear/src/api/oauth.ts | Adds providerId-aware OAuth services + staging token flow + non-destructive refresh. |
| extensions/linear/src/api/linearClient.ts | Reworks client bootstrap/caching for multi-workspace + active workspace snapshot. |
| extensions/linear/src/api/initiatives.ts | Adds optional client override for workspace routing. |
| extensions/linear/src/api/getTeams.ts | Adds optional client override for workspace routing. |
| extensions/linear/src/api/getProjects.ts | Adds optional client override for workspace routing. |
| extensions/linear/src/api/getNotifications.ts | Adds optional client override for workspace routing. |
| extensions/linear/src/api/getMilestones.ts | Adds optional client override for workspace routing. |
| extensions/linear/src/api/getLabels.ts | Adds optional client override for workspace routing. |
| extensions/linear/src/api/getIssues.ts | Adds optional client override for workspace routing for all issue queries. |
| extensions/linear/src/api/getIssueTemplates.ts | Adds optional client override for workspace routing. |
| extensions/linear/src/api/getCustomViews.ts | Adds optional client override for workspace routing. |
| extensions/linear/src/api/favorites.ts | Adds optional client override for workspace routing. |
| extensions/linear/src/api/documents.ts | Adds optional client override for workspace routing. |
| extensions/linear/src/api/deleteNotification.ts | Adds optional client override for workspace routing. |
| extensions/linear/src/api/createIssue.ts | Adds optional client override for workspace routing (issue + sub-issue). |
| extensions/linear/src/api/attachments.ts | Adds optional client override to keep uploads routed to correct workspace. |
| extensions/linear/src/active-cycle.tsx | Adds embedded workspace switching + per-workspace persisted selected team. |
| extensions/linear/package.json | Adds new commands/tools and updates AI instructions + evals for workspace routing. |
| extensions/linear/README.md | Documents multi-workspace usage and new management/switching surfaces. |
| extensions/linear/CHANGELOG.md | Adds changelog entry describing multi-workspace support. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| export async function refreshNonDestructive( | ||
| service: OAuthService, | ||
| ): Promise<{ status: "ok"; accessToken: string } | { status: "failed" }> { | ||
| const tokens = await service.client.getTokens(); | ||
| if (!tokens?.refreshToken) return { status: "failed" }; | ||
| try { | ||
| const response = await fetch(`${LINEAR_PROXY}/refresh-token`, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| client_id: LINEAR_PROXY_CLIENT_ID, | ||
| refresh_token: tokens.refreshToken, | ||
| grant_type: "refresh_token", | ||
| }), | ||
| }); | ||
| if (!response.ok) return { status: "failed" }; | ||
| const next = (await response.json()) as { access_token: string; refresh_token?: string; expires_in?: number }; | ||
| await service.client.setTokens(next); | ||
| return { status: "ok", accessToken: next.access_token }; | ||
| } catch { | ||
| return { status: "failed" }; | ||
| } | ||
| } |
| } | ||
| } | ||
|
|
||
| if (!teamId) { |
| {rows.map((row) => ( | ||
| <MenuBarExtra.Section | ||
| key={entryKey(row.entry)} | ||
| {...(multi | ||
| ? { | ||
| title: | ||
| row.entry.orgName + | ||
| (rows.filter((r) => r.entry.orgId === row.entry.orgId).length > 1 ? ` (${row.entry.userEmail})` : ""), | ||
| } | ||
| : {})} | ||
| > |
| async function logOutAll() { | ||
| const confirmed = await confirmAlert({ | ||
| title: "Log Out of All Workspaces?", | ||
| message: | ||
| "Removes every workspace login from Raycast (tokens are deleted locally; each access token expires at Linear on its own within 24 hours).", | ||
| primaryAction: { title: "Log Out of All", style: Alert.ActionStyle.Destructive }, | ||
| }); | ||
| if (!confirmed) return; | ||
| for (const row of rows) { | ||
| const service = getServiceForProviderId(row.entry.providerId, undefined, `Linear — ${row.entry.orgName}`); | ||
| await service.client.removeTokens(); | ||
| await removeWorkspaceEntry({ orgId: row.entry.orgId, userId: row.entry.userId }); | ||
| } | ||
| await showToast({ style: Toast.Style.Success, title: "Logged out of all workspaces" }); | ||
| refreshQuickCommandSubtitles(); // fire-and-forget: workspace membership changed, subtitles listing "others" are now stale | ||
| await reload(); | ||
| } |
Greptile SummaryAdds multi-workspace OAuth registration and routing throughout the Linear extension.
Confidence Score: 3/5The PR should not merge until complete multi-workspace notification payloads are kept out of the unencrypted cache or stored using an appropriately protected mechanism. The new menu-bar hook serializes private notification content from every connected Linear account into plaintext cache storage and leaves no removal-time invalidation path. Files Needing Attention: extensions/linear/src/hooks/useAllWorkspaceNotifications.ts and extensions/linear/src/manage-workspaces.tsx
|
| Filename | Overview |
|---|---|
| extensions/linear/src/hooks/useAllWorkspaceNotifications.ts | Adds all-workspace notification aggregation but caches complete OAuth-protected notification payloads in storage documented as unencrypted. |
| extensions/linear/src/api/linearClient.ts | Introduces provider-specific client creation, token migration, activation, and caching for workspace/account pairs. |
| extensions/linear/src/api/workspaces.ts | Adds the persistent workspace registry, legacy-login migration, entry identity, and active-workspace management. |
| extensions/linear/src/components/WorkspaceContext.tsx | Provides workspace state and remounts command content after switching to prevent stale workspace-bound UI state. |
| extensions/linear/src/tools/resolveToolWorkspace.ts | Resolves explicit per-call AI workspace identifiers and consistently supplies matching clients to checked confirmations and executions. |
| extensions/linear/src/manage-workspaces.tsx | Adds workspace add, switch, reauthentication, and logout workflows, but logout does not clear cached notification data. |
| extensions/linear/package.json | Declares the new workspace commands and AI tool while extending existing quick-command and AI routing interfaces. |
| extensions/linear/CHANGELOG.md | Adds a correctly positioned multi-workspace release entry using the required merge-date placeholder. |
Prompt To Fix All With AI
### Issue 1
extensions/linear/src/hooks/useAllWorkspaceNotifications.ts:23
**Notification payloads enter plaintext cache**
When users connect additional workspaces, `useCachedPromise` serializes each workspace's complete notification rows into the unencrypted cache, exposing private issue details, comment bodies, organization metadata, and account emails to a local cache reader even after the corresponding workspace is removed. **How this was verified:** The hook returns full `NotificationResult` arrays through a cache explicitly documented here as unencrypted, while the workspace-removal flow contains no cache invalidation or redaction.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "feat(linear): add multi-workspace suppor..." | Re-trigger Greptile
| // background-safe token access only (never authorize(), §4.3). One row per ENTRY — | ||
| // two entries can share an orgId (D10), so grouping is by entryKey. | ||
| export default function useAllWorkspaceNotifications() { | ||
| const { data, isLoading, mutate } = useCachedPromise(async (): Promise<WorkspaceNotificationRow[]> => { |
There was a problem hiding this comment.
Notification payloads enter plaintext cache
When users connect additional workspaces, useCachedPromise serializes each workspace's complete notification rows into the unencrypted cache, exposing private issue details, comment bodies, organization metadata, and account emails to a local cache reader even after the corresponding workspace is removed. How this was verified: The hook returns full NotificationResult arrays through a cache explicitly documented here as unencrypted, while the workspace-removal flow contains no cache invalidation or redaction.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/linear/src/hooks/useAllWorkspaceNotifications.ts
Line: 23
Comment:
**Notification payloads enter plaintext cache**
When users connect additional workspaces, `useCachedPromise` serializes each workspace's complete notification rows into the unencrypted cache, exposing private issue details, comment bodies, organization metadata, and account emails to a local cache reader even after the corresponding workspace is removed. **How this was verified:** The hook returns full `NotificationResult` arrays through a cache explicitly documented here as unencrypted, while the workspace-removal flow contains no cache invalidation or redaction.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Description
Adds multi-workspace support to the Linear extension: connect any number of workspaces
(including the same workspace under two accounts), switch via a consistent pattern on
every surface (list dropdowns, a form field, a dedicated Create Issue for Myself in
Workspace quick command plus a one-shot
workspaceargument on Quick Add Comment), anew Manage Workspaces command, an aggregated menu bar, and workspace-routable AI tools
with a new
get-workspacestool. The quick commands surface their target via a liveroot-search subtitle (hidden for single-workspace users).
Single-workspace users see no behavior change: every switcher is invisible until a
second workspace is connected, and an existing login is adopted silently (no re-auth).
The only visible additions are the new Manage Workspaces and Create Issue for Myself
in Workspace commands and an optional
workspaceargument on Quick Add Comment.Closes #24297. Supersedes the PAT-based approach in #25646 — this stays entirely on
Raycast's OAuth proxy and token storage.
Approach
(
linear-ws-<orgId>-<userId>); the existinglinearslot is adopted unchanged.providerId/extraParameterstoOAuthService.linear(); untilit ships in a release, a small factory (
src/api/oauth.ts,AUTH_BACKENDswitch)constructs the service manually with the same proxy + clientId — the copied constants
are temporary by design and deleted when the utils option lands.
prompt=consent(without it Linear silently skips consent foran already-granted app). Linear's consent page has no workspace picker — the grant
follows the active workspace at linear.app, so the add flow instructs the user to
switch there first (verified empirically).
token refresh, so a transient network failure can never log a workspace out.
(revocation is authorization-scoped and eventually consistent, so an unconditional
revoke could affect the primary login).
workspaceId(each tool call runs in a fresh process, soper-call routing is the only reliable design); confirmations resolve the same client
as executions and display the target workspace.
Testing notes & known limitations
macOS only (no Windows hardware available to the author) — flagging for reviewers.
including the same workspace under two accounts): silent adoption on upgrade, add /
switch / re-authenticate / log out flows, per-workspace remembered form defaults,
cross-workspace drafts (draft pins its workspace; the global default is untouched),
menu-bar aggregation with per-workspace re-auth rows, and AI-tool workspace routing.
workspaceIdon AI tools is optional by design (absent → active workspace). If themodel never calls
get-workspacesand the user switches the default between aconfirmation and its execution, routing follows the default; mitigations are the AI
instructions, a Workspace row on every mutating confirmation, and routing evals.
dropdowns require a static list; workspaces are per-user runtime data). It matches
URL key, account email, or a unique name prefix, and never changes the default
workspace; Create Issue for Myself is deliberately pegged to the active workspace,
with the dedicated in-Workspace command for explicit targeting.
create-project-update.tsandget-initiatives.tsexist undeclared intools[]; team switch clears the CreateIssue title via the template-reset effect (upstream behavior, kept for parity).
Manage Workspaces is the primary management surface and labels each login row
(
providerName: "Linear — <org>").a failed staged-add verification matches on error text ("HTTP 401/403") — an SDK-path
auth failure in a rare race is treated as transient (retried) rather than permanent.
Screencast
<attach the Manage Workspaces add/switch screencast>