From 502a4bdebbab0b9e7fd9fd4d4dedfff979e355f0 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 12 Jun 2026 19:31:25 -0700 Subject: [PATCH] Route Linear and Jira tasks by source context Co-authored-by: Orca --- .../host-context-implementation-checklist.md | 5 +- .../src/components/JiraIssueWorkspace.tsx | 37 +- .../src/components/LinearIssueTextEditor.tsx | 10 +- .../src/components/LinearIssueWorkspace.tsx | 59 +++- .../src/components/LinearItemDrawer.tsx | 80 +++-- src/renderer/src/components/TaskPage.tsx | 218 +++++++++--- src/renderer/src/hooks/useIssueMetadata.ts | 17 +- src/renderer/src/store/slices/jira.test.ts | 33 ++ src/renderer/src/store/slices/jira.ts | 169 ++++++++-- src/renderer/src/store/slices/linear.test.ts | 42 +++ src/renderer/src/store/slices/linear.ts | 316 ++++++++++++------ 11 files changed, 754 insertions(+), 232 deletions(-) diff --git a/notes/host-context-implementation-checklist.md b/notes/host-context-implementation-checklist.md index 96273bbbddf..521bd87a5ab 100644 --- a/notes/host-context-implementation-checklist.md +++ b/notes/host-context-implementation-checklist.md @@ -34,8 +34,8 @@ Done means: a new agent can read the shared types and understand exactly where w - [~] Update GitHub cache keys to include provider identity and source host/runtime identity so two hosts with different GitHub accounts cannot collide. - [x] Update GitLab IPC/preload/main handlers to accept explicit project/source context instead of path-only selectors. - [~] Guard or retire GitLab path-only fallbacks where they can mutate or fetch from the wrong host. -- [~] Update Linear routing to use an explicit source account/workspace context instead of settings.activeRuntimeEnvironmentId. -- [~] Update Jira routing to use an explicit source account/site/project context instead of settings.activeRuntimeEnvironmentId. +- [x] Update Linear routing to use an explicit source account/workspace context instead of settings.activeRuntimeEnvironmentId. +- [x] Update Jira routing to use an explicit source account/site/project context instead of settings.activeRuntimeEnvironmentId. - [~] Add provider diagnostics that show which host/account/site a task source is using. - [~] Add error states for unavailable source hosts, missing provider auth, unsupported provider on host, and version-skewed remote servers. @@ -226,3 +226,4 @@ Done means: reviewers and future agents can understand the vision, implementatio - [x] 2026-06-12: Completed source-context unit coverage by adding GitLab/Linear/Jira provider identity cache-scope serialization tests and rerunning shared source-context, GitHub source/cache routing, UI open-task, and task-navigation history suites (6 files / 261 tests) plus targeted oxlint. - [x] 2026-06-12: Reconciled Add Project host-selection tests with the disabled-host UX: disconnected SSH hosts and blocked runtime hosts remain visible but disabled, the hook falls back to Local Mac instead of selecting a disconnected SSH host, and selection handlers ignore unavailable hosts; verified Add Project host selector/selection/dialog, sidebar host options, and execution-host registry tests (5 files / 34 tests) plus targeted oxlint. - [x] 2026-06-12: Re-verified project-first drag/reorder behavior across workspace manual order, drag units, workspace-board pointer drops, host-section order/rows, repo reorder host splitting, and project-header drag helpers; 9 files / 62 tests passed plus targeted oxlint. +- [x] 2026-06-12: Routed Linear and Jira Tasks reads, create flows, list/detail mutations, comments, metadata, and New Workspace handoff through explicit account-backed `TaskSourceContext` while preserving legacy focused-runtime cache keys for old callers; verified with Linear/Jira source-routing regressions, focused Linear/Jira slice tests (2 files / 48 tests), targeted oxlint, `git diff --check`, and full `pnpm run typecheck`. diff --git a/src/renderer/src/components/JiraIssueWorkspace.tsx b/src/renderer/src/components/JiraIssueWorkspace.tsx index 4be68a77b56..a5c4fdee4af 100644 --- a/src/renderer/src/components/JiraIssueWorkspace.tsx +++ b/src/renderer/src/components/JiraIssueWorkspace.tsx @@ -42,12 +42,14 @@ import type { JiraTransition, JiraUser } from '../../../shared/types' +import type { TaskSourceContext } from '../../../shared/task-source-context' import { translate } from '@/i18n/i18n' type JiraIssueWorkspaceProps = { issue: JiraIssue | null onUse: (issue: JiraIssue) => void onClose: () => void + sourceContext?: TaskSourceContext | null } const relativeFormatter = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' }) @@ -111,9 +113,11 @@ async function copyTextToClipboard(text: string, label: string): Promise { export default function JiraIssueWorkspace({ issue, onUse, - onClose + onClose, + sourceContext }: JiraIssueWorkspaceProps): React.JSX.Element { const settings = useAppStore((s) => s.settings) + const providerSettings = sourceContext ?? settings const patchJiraIssue = useAppStore((s) => s.patchJiraIssue) const [fullIssue, setFullIssue] = useState(null) const [issueLoading, setIssueLoading] = useState(false) @@ -139,7 +143,7 @@ export default function JiraIssueWorkspace({ setCommentsLoading(true) setCommentsError(null) try { - let fetched = await jiraIssueComments(settings, targetIssue.key, targetIssue.siteId) + let fetched = await jiraIssueComments(providerSettings, targetIssue.key, targetIssue.siteId) if (requestId !== requestIdRef.current) { return } @@ -159,7 +163,7 @@ export default function JiraIssueWorkspace({ } } }, - [settings] + [providerSettings] ) useEffect(() => { @@ -186,7 +190,7 @@ export default function JiraIssueWorkspace({ setCommentsError(null) setIssueLoading(true) - void jiraGetIssue(settings, issue.key, issue.siteId) + void jiraGetIssue(providerSettings, issue.key, issue.siteId) .then((result) => { if (requestId !== requestIdRef.current) { return @@ -205,9 +209,9 @@ export default function JiraIssueWorkspace({ }) void Promise.all([ - jiraListTransitions(settings, issue.key, issue.siteId), - jiraListPriorities(settings, issue.siteId), - jiraListAssignableUsers(settings, issue.key, undefined, issue.siteId) + jiraListTransitions(providerSettings, issue.key, issue.siteId), + jiraListPriorities(providerSettings, issue.siteId), + jiraListAssignableUsers(providerSettings, issue.key, undefined, issue.siteId) ]) .then(([nextTransitions, nextPriorities, nextUsers]) => { if (requestId !== requestIdRef.current) { @@ -220,14 +224,14 @@ export default function JiraIssueWorkspace({ .catch(() => {}) void loadComments(issue, requestId) - }, [issue, loadComments, settings]) + }, [issue, loadComments, providerSettings]) const refreshIssue = useCallback(async (): Promise => { if (!displayed) { return } try { - const latest = await jiraGetIssue(settings, displayed.key, displayed.siteId) + const latest = await jiraGetIssue(providerSettings, displayed.key, displayed.siteId) if (latest) { setFullIssue(latest) patchJiraIssue(latest.key, latest) @@ -235,7 +239,7 @@ export default function JiraIssueWorkspace({ } catch { // Keep the visible issue snapshot if refresh fails. } - }, [displayed, patchJiraIssue, settings]) + }, [displayed, patchJiraIssue, providerSettings]) const mutateIssue = useCallback( async ( @@ -253,7 +257,7 @@ export default function JiraIssueWorkspace({ setFullIssue({ ...displayed, ...optimistic }) patchJiraIssue(displayed.key, optimistic) } - const result = await jiraUpdateIssue(settings, displayed.key, updates, siteId) + const result = await jiraUpdateIssue(providerSettings, displayed.key, updates, siteId) if (!result.ok) { throw new Error(result.error) } @@ -273,7 +277,7 @@ export default function JiraIssueWorkspace({ setPendingField(null) } }, - [displayed, patchJiraIssue, pendingField, refreshIssue, settings, siteId] + [displayed, patchJiraIssue, pendingField, refreshIssue, providerSettings, siteId] ) const handleSaveTitle = useCallback(() => { @@ -309,7 +313,12 @@ export default function JiraIssueWorkspace({ } setCommentSubmitting(true) try { - const result = await jiraAddIssueComment(settings, displayed.key, body, displayed.siteId) + const result = await jiraAddIssueComment( + providerSettings, + displayed.key, + body, + displayed.siteId + ) if (!result.ok) { throw new Error(result.error) } @@ -331,7 +340,7 @@ export default function JiraIssueWorkspace({ } finally { setCommentSubmitting(false) } - }, [commentDraft, commentSubmitting, displayed, settings]) + }, [commentDraft, commentSubmitting, displayed, providerSettings]) const actionItems = useMemo(() => { if (!displayed) { diff --git a/src/renderer/src/components/LinearIssueTextEditor.tsx b/src/renderer/src/components/LinearIssueTextEditor.tsx index 3e97489e8df..473fa75d3c3 100644 --- a/src/renderer/src/components/LinearIssueTextEditor.tsx +++ b/src/renderer/src/components/LinearIssueTextEditor.tsx @@ -9,6 +9,7 @@ import { useAppStore } from '@/store' import { getScreenSubmitShortcutLabel, isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut' import { linearUpdateIssue } from '@/runtime/runtime-linear-client' import type { LinearIssue } from '../../../shared/types' +import type { TaskSourceContext } from '../../../shared/task-source-context' import { getLinearIssueTextSavePlan, type LinearIssueTextField @@ -24,6 +25,7 @@ type LinearIssueTextEditorProps = { onIssueChange: (patch: Pick | Pick) => void density?: 'page' | 'drawer' fields?: 'all' | 'title' | 'description' + sourceContext?: TaskSourceContext | null } function useAutosizeTextArea(value: string): React.RefObject { @@ -45,9 +47,11 @@ export function LinearIssueTextEditor({ issue, onIssueChange, density = 'page', - fields = 'all' + fields = 'all', + sourceContext }: LinearIssueTextEditorProps): React.JSX.Element { const settings = useAppStore((s) => s.settings) + const providerSettings = sourceContext ?? settings const patchLinearIssue = useAppStore((s) => s.patchLinearIssue) const [draftState, setDraftState] = useState(() => createLinearIssueTextDraftState(issue)) const [savingField, setSavingField] = useState(null) @@ -111,7 +115,7 @@ export function LinearIssueTextEditor({ onIssueChange(patch) patchLinearIssue(issue.id, patch) try { - const result = await linearUpdateIssue(settings, issue.id, patch, issue.workspaceId) + const result = await linearUpdateIssue(providerSettings, issue.id, patch, issue.workspaceId) if (!result.ok) { throw new Error(result.error) } @@ -156,7 +160,7 @@ export function LinearIssueTextEditor({ mountedRef, onIssueChange, patchLinearIssue, - settings, + providerSettings, titleDraft, updateDescriptionDraft, updateTitleDraft diff --git a/src/renderer/src/components/LinearIssueWorkspace.tsx b/src/renderer/src/components/LinearIssueWorkspace.tsx index 0272093ee01..fe8fa29b8d1 100644 --- a/src/renderer/src/components/LinearIssueWorkspace.tsx +++ b/src/renderer/src/components/LinearIssueWorkspace.tsx @@ -57,6 +57,7 @@ import type { LinearIssueChildSummary, LinearProjectSummary } from '../../../shared/types' +import type { TaskSourceContext } from '../../../shared/task-source-context' import { translate } from '@/i18n/i18n' type LinearIssueWorkspaceProps = { @@ -66,6 +67,7 @@ type LinearIssueWorkspaceProps = { onClose: () => void variant?: 'sheet' | 'page' backLabel?: string + sourceContext?: TaskSourceContext | null } async function copyTextToClipboard(text: string, label: string): Promise { @@ -111,12 +113,15 @@ function LinearIssueAvatar({ function LinearIssueSubIssueButton({ issue, - onOpenIssue + onOpenIssue, + sourceContext }: { issue: LinearIssue onOpenIssue: (issue: LinearIssue) => void + sourceContext?: TaskSourceContext | null }): React.JSX.Element { const settings = useAppStore((s) => s.settings) + const providerSettings = sourceContext ?? settings const fetchLinearIssue = useAppStore((s) => s.fetchLinearIssue) const [open, setOpen] = useState(false) const [title, setTitle] = useState('') @@ -145,7 +150,9 @@ function LinearIssueSubIssueButton({ async (subIssue: LinearIssueChildSummary) => { setOpeningSubIssueId(subIssue.id) try { - const fullIssue = await fetchLinearIssue(subIssue.id, issue.workspaceId) + const fullIssue = await fetchLinearIssue(subIssue.id, issue.workspaceId, { + sourceContext + }) if (!mountedRef.current) { return } @@ -173,7 +180,7 @@ function LinearIssueSubIssueButton({ } } }, - [fetchLinearIssue, issue.workspaceId, mountedRef, onOpenIssue] + [fetchLinearIssue, issue.workspaceId, mountedRef, onOpenIssue, sourceContext] ) const handleCreate = useCallback(async () => { @@ -183,7 +190,7 @@ function LinearIssueSubIssueButton({ } setSubmitting(true) try { - const result = await linearCreateSubIssue(settings, { + const result = await linearCreateSubIssue(providerSettings, { parentIssueId: issue.id, teamId: issue.team.id, title: trimmed, @@ -235,7 +242,7 @@ function LinearIssueSubIssueButton({ issue.subIssues, issue.team.id, issue.workspaceId, - settings, + providerSettings, title ]) @@ -311,12 +318,15 @@ function LinearIssueSubIssueButton({ function LinearIssueSidebarProjectCard({ issue, - onProjectChanged + onProjectChanged, + sourceContext }: { issue: LinearIssue onProjectChanged: (project: LinearProjectSummary) => void + sourceContext?: TaskSourceContext | null }): React.JSX.Element { const settings = useAppStore((s) => s.settings) + const providerSettings = sourceContext ?? settings const patchLinearIssue = useAppStore((s) => s.patchLinearIssue) const [open, setOpen] = useState(false) const [query, setQuery] = useState('') @@ -331,7 +341,7 @@ function LinearIssueSidebarProjectCard({ let cancelled = false const timeout = window.setTimeout(() => { setLoading(true) - void linearListProjects(settings, query, 20, issue.workspaceId) + void linearListProjects(providerSettings, query, 20, issue.workspaceId) .then((result) => { if (!cancelled) { setProjects(result.items) @@ -359,14 +369,14 @@ function LinearIssueSidebarProjectCard({ cancelled = true window.clearTimeout(timeout) } - }, [issue.workspaceId, open, query, settings]) + }, [issue.workspaceId, open, providerSettings, query]) const handleSelectProject = useCallback( async (project: LinearProjectSummary) => { setSavingProjectId(project.id) try { const result = await linearUpdateIssue( - settings, + providerSettings, issue.id, { projectId: project.id }, issue.workspaceId @@ -394,7 +404,7 @@ function LinearIssueSidebarProjectCard({ setSavingProjectId(null) } }, - [issue.id, issue.workspaceId, onProjectChanged, patchLinearIssue, settings] + [issue.id, issue.workspaceId, onProjectChanged, patchLinearIssue, providerSettings] ) return ( @@ -482,9 +492,11 @@ export default function LinearIssueWorkspace({ onOpenIssue, onClose, variant = 'sheet', - backLabel = 'Back' + backLabel = 'Back', + sourceContext }: LinearIssueWorkspaceProps): React.JSX.Element { const settings = useAppStore((s) => s.settings) + const providerSettings = sourceContext ?? settings const [fullIssue, setFullIssue] = useState(null) const [issueLoading, setIssueLoading] = useState(false) const [comments, setComments] = useState([]) @@ -519,7 +531,7 @@ export default function LinearIssueWorkspace({ } try { let fetched = (await linearIssueComments( - settings, + providerSettings, targetIssue.id, targetIssue.workspaceId )) as LinearComment[] @@ -542,7 +554,7 @@ export default function LinearIssueWorkspace({ } } }, - [mountedRef, settings] + [mountedRef, providerSettings] ) useEffect(() => { @@ -558,7 +570,7 @@ export default function LinearIssueWorkspace({ return } - const issueKey = `${settings?.activeRuntimeEnvironmentId ?? 'local'}:${issue.workspaceId ?? 'selected'}:${issue.id}` + const issueKey = `${sourceContext?.hostId ?? settings?.activeRuntimeEnvironmentId ?? 'local'}:${issue.workspaceId ?? 'selected'}:${issue.id}` if (hydratedIssueKeyRef.current === issueKey) { return } @@ -576,7 +588,7 @@ export default function LinearIssueWorkspace({ // Why: issue hydration and comments are separate surfaces; a comments // failure should not blank the issue detail the user selected. - void linearGetIssue(settings, issue.id, issue.workspaceId) + void linearGetIssue(providerSettings, issue.id, issue.workspaceId) .then((issueResult) => { if (!mountedRef.current || requestId !== requestIdRef.current) { return @@ -616,7 +628,7 @@ export default function LinearIssueWorkspace({ }) void loadComments(issue, requestId) - }, [issue, loadComments, mountedRef, settings]) + }, [issue, loadComments, mountedRef, providerSettings, settings, sourceContext?.hostId]) const displayed = fullIssue ?? issue @@ -796,9 +808,17 @@ export default function LinearIssueWorkspace({
- + - +
@@ -894,6 +914,7 @@ export default function LinearIssueWorkspace({ workspaceId={displayed.workspaceId} onCommentAdded={handleCommentAdded} variant="linear-page" + sourceContext={sourceContext} />
@@ -905,11 +926,13 @@ export default function LinearIssueWorkspace({ editState={editState} onEditStateChange={handleEditStateChange} layout="properties" + sourceContext={sourceContext} /> ) : null}
diff --git a/src/renderer/src/components/LinearItemDrawer.tsx b/src/renderer/src/components/LinearItemDrawer.tsx index 501cd5c2b8e..d11758e2ac5 100644 --- a/src/renderer/src/components/LinearItemDrawer.tsx +++ b/src/renderer/src/components/LinearItemDrawer.tsx @@ -38,6 +38,7 @@ import { } from '@/components/linear-state-pill-style' import { LinearPriorityIcon } from '@/components/linear-priority-icon' import type { LinearIssue, LinearComment } from '../../../shared/types' +import type { TaskSourceContext } from '../../../shared/task-source-context' import { linearAddIssueComment, linearGetIssue, @@ -118,6 +119,7 @@ type LinearItemDrawerProps = { issue: LinearIssue | null onUse: (issue: LinearIssue) => void onClose: () => void + sourceContext?: TaskSourceContext | null } export type LinearEditState = { @@ -134,18 +136,21 @@ type EditSectionProps = { editState: LinearEditState onEditStateChange: (patch: Partial) => void layout?: 'chips' | 'properties' + sourceContext?: TaskSourceContext | null } export function LinearIssueEditSection({ issue, editState, onEditStateChange, - layout = 'chips' + layout = 'chips', + sourceContext }: EditSectionProps): React.JSX.Element { const [labelPopoverOpen, setLabelPopoverOpen] = useState(false) const [estimatePopoverOpen, setEstimatePopoverOpen] = useState(false) const patchLinearIssue = useAppStore((s) => s.patchLinearIssue) const settings = useAppStore((s) => s.settings) + const providerSettings = sourceContext ?? settings const { isPending, run } = useImmediateMutation() const { @@ -159,9 +164,9 @@ export function LinearIssueEditSection({ const [estimateInput, setEstimateInput] = useState(() => formatLinearEstimateInput(localEstimate)) const teamId = issue.team?.id || null - const states = useTeamStates(teamId, settings, issue.workspaceId) - const labels = useTeamLabels(teamId, settings, issue.workspaceId) - const members = useTeamMembers(teamId, settings, issue.workspaceId) + const states = useTeamStates(teamId, providerSettings, issue.workspaceId) + const labels = useTeamLabels(teamId, providerSettings, issue.workspaceId) + const members = useTeamMembers(teamId, providerSettings, issue.workspaceId) const handleEstimatePopoverOpenChange = useCallback( (open: boolean) => { @@ -184,7 +189,7 @@ export function LinearIssueEditSection({ const stateValue = { name: newState.name, type: newState.type, color: newState.color } run('state', { - mutate: () => linearUpdateIssue(settings, issue.id, { stateId }, issue.workspaceId), + mutate: () => linearUpdateIssue(providerSettings, issue.id, { stateId }, issue.workspaceId), onOptimistic: () => { onEditStateChange({ state: stateValue }) patchLinearIssue(issue.id, { state: stateValue }) @@ -203,7 +208,7 @@ export function LinearIssueEditSection({ issue.id, issue.workspaceId, localState, - settings, + providerSettings, states.data, patchLinearIssue, run, @@ -216,7 +221,8 @@ export function LinearIssueEditSection({ const priority = parseInt(value, 10) const prevPriority = localPriority run('priority', { - mutate: () => linearUpdateIssue(settings, issue.id, { priority }, issue.workspaceId), + mutate: () => + linearUpdateIssue(providerSettings, issue.id, { priority }, issue.workspaceId), onOptimistic: () => { onEditStateChange({ priority }) patchLinearIssue(issue.id, { priority }) @@ -231,14 +237,23 @@ export function LinearIssueEditSection({ onError: (err) => toast.error(err) }) }, - [issue.id, issue.workspaceId, localPriority, settings, patchLinearIssue, run, onEditStateChange] + [ + issue.id, + issue.workspaceId, + localPriority, + providerSettings, + patchLinearIssue, + run, + onEditStateChange + ] ) const handleEstimateChange = useCallback( (estimate: number | null) => { const prevEstimate = localEstimate run('estimate', { - mutate: () => linearUpdateIssue(settings, issue.id, { estimate }, issue.workspaceId), + mutate: () => + linearUpdateIssue(providerSettings, issue.id, { estimate }, issue.workspaceId), onOptimistic: () => { onEditStateChange({ estimate }) patchLinearIssue(issue.id, { estimate }) @@ -254,7 +269,15 @@ export function LinearIssueEditSection({ onError: (err) => toast.error(err) }) }, - [issue.id, issue.workspaceId, localEstimate, settings, patchLinearIssue, run, onEditStateChange] + [ + issue.id, + issue.workspaceId, + localEstimate, + providerSettings, + patchLinearIssue, + run, + onEditStateChange + ] ) const handleEstimateSubmit = useCallback(() => { @@ -287,7 +310,8 @@ export function LinearIssueEditSection({ ? { id: member.id, displayName: member.displayName, avatarUrl: member.avatarUrl } : undefined run('assignee', { - mutate: () => linearUpdateIssue(settings, issue.id, { assigneeId }, issue.workspaceId), + mutate: () => + linearUpdateIssue(providerSettings, issue.id, { assigneeId }, issue.workspaceId), onOptimistic: () => { onEditStateChange({ assignee: newAssignee }) patchLinearIssue(issue.id, { assignee: newAssignee }) @@ -306,7 +330,7 @@ export function LinearIssueEditSection({ issue.id, issue.workspaceId, localAssignee, - settings, + providerSettings, members.data, patchLinearIssue, run, @@ -328,7 +352,12 @@ export function LinearIssueEditSection({ run('labels', { mutate: () => - linearUpdateIssue(settings, issue.id, { labelIds: newLabelIds }, issue.workspaceId), + linearUpdateIssue( + providerSettings, + issue.id, + { labelIds: newLabelIds }, + issue.workspaceId + ), onOptimistic: () => { onEditStateChange({ labelIds: newLabelIds, labels: newLabels }) patchLinearIssue(issue.id, { labelIds: newLabelIds, labels: newLabels }) @@ -348,7 +377,7 @@ export function LinearIssueEditSection({ issue.workspaceId, localLabelIds, localLabels, - settings, + providerSettings, labels.data, patchLinearIssue, run, @@ -994,14 +1023,17 @@ export function LinearIssueCommentFooter({ issueId, workspaceId, onCommentAdded, - variant = 'compact' + variant = 'compact', + sourceContext }: { issueId: string workspaceId?: string | null onCommentAdded: (comment: LinearLocalComment) => void variant?: 'compact' | 'linear-page' + sourceContext?: TaskSourceContext | null }): React.JSX.Element { const settings = useAppStore((s) => s.settings) + const providerSettings = sourceContext ?? settings const submitShortcutLabel = getScreenSubmitShortcutLabel() const [body, setBody] = useState('') const [submitting, setSubmitting] = useState(false) @@ -1030,7 +1062,7 @@ export function LinearIssueCommentFooter({ } setSubmitting(true) try { - const result = await linearAddIssueComment(settings, issueId, trimmed, workspaceId) + const result = await linearAddIssueComment(providerSettings, issueId, trimmed, workspaceId) const typed = result as { ok: boolean; id?: string; error?: string } if (!mountedRef.current) { return @@ -1062,7 +1094,7 @@ export function LinearIssueCommentFooter({ setSubmitting(false) } } - }, [body, issueId, onCommentAdded, settings, workspaceId]) + }, [body, issueId, onCommentAdded, providerSettings, workspaceId]) const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { @@ -1168,7 +1200,8 @@ export function initLinearIssueEditState(issue: LinearIssue): LinearEditState { export default function LinearItemDrawer({ issue, onUse, - onClose + onClose, + sourceContext }: LinearItemDrawerProps): React.JSX.Element { const [fullIssue, setFullIssue] = useState(null) const [comments, setComments] = useState([]) @@ -1178,6 +1211,7 @@ export default function LinearItemDrawer({ const hasEditedRef = useRef(false) const optimisticCommentsRef = useRef([]) const settings = useAppStore((s) => s.settings) + const providerSettings = sourceContext ?? settings const handleEditStateChange = useCallback((patch: Partial) => { hasEditedRef.current = true @@ -1214,7 +1248,7 @@ export default function LinearItemDrawer({ // Why: fetch issue and comments independently so a transient comments // failure doesn't discard the successfully-fetched issue data. - linearGetIssue(settings, issue.id, issue.workspaceId) + linearGetIssue(providerSettings, issue.id, issue.workspaceId) .then((issueResult) => { if (requestId !== requestIdRef.current) { return @@ -1231,7 +1265,7 @@ export default function LinearItemDrawer({ }) .catch(() => {}) - linearIssueComments(settings, issue.id, issue.workspaceId) + linearIssueComments(providerSettings, issue.id, issue.workspaceId) .then((commentsResult) => { if (requestId !== requestIdRef.current) { return @@ -1256,7 +1290,7 @@ export default function LinearItemDrawer({ } }) // oxlint-disable-next-line react-hooks/exhaustive-deps - }, [issue?.id, issue?.workspaceId, settings]) + }, [issue?.id, issue?.workspaceId, providerSettings]) // Why: same pointer-events fix as GitHubItemDialog — Radix may leave // pointer-events: none on body when overlays transition. @@ -1343,6 +1377,7 @@ export default function LinearItemDrawer({ onIssueChange={handleIssueTextChange} density="drawer" fields="title" + sourceContext={sourceContext} />
@@ -1400,6 +1435,7 @@ export default function LinearItemDrawer({ issue={displayed} editState={editState} onEditStateChange={handleEditStateChange} + sourceContext={sourceContext} /> )} @@ -1411,6 +1447,7 @@ export default function LinearItemDrawer({ onIssueChange={handleIssueTextChange} density="drawer" fields="description" + sourceContext={sourceContext} />
@@ -1472,6 +1509,7 @@ export default function LinearItemDrawer({ issueId={displayed.id} workspaceId={displayed.workspaceId} onCommentAdded={handleCommentAdded} + sourceContext={sourceContext} />
) @@ -8946,6 +9063,7 @@ export default function TaskPage(): React.JSX.Element { onUse={handleUseLinearItem} onOpenIssue={openRelatedLinearIssue} onClose={closeTaskDetailPage} + sourceContext={linearTaskSourceContext} /> ) : !linearStatusReady ? (
@@ -9546,7 +9664,11 @@ export default function TaskPage(): React.JSX.Element {
{effectiveLinearDisplayProperties.has('state') ? ( - + ) : null} {effectiveLinearDisplayProperties.has('assignee') ? ( @@ -9661,7 +9783,11 @@ export default function TaskPage(): React.JSX.Element {
{effectiveLinearDisplayProperties.has('state') ? ( - + ) : null} {effectiveLinearDisplayProperties.has('assignee') ? ( @@ -9703,7 +9829,11 @@ export default function TaskPage(): React.JSX.Element { {effectiveLinearDisplayProperties.has('state') ? (
- +
) : null} diff --git a/src/renderer/src/hooks/useIssueMetadata.ts b/src/renderer/src/hooks/useIssueMetadata.ts index a61b0e71da2..0ba6f1bd6d6 100644 --- a/src/renderer/src/hooks/useIssueMetadata.ts +++ b/src/renderer/src/hooks/useIssueMetadata.ts @@ -6,15 +6,16 @@ import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-cl import { linearTeamLabels, linearTeamMembers, - linearTeamStates + linearTeamStates, + type RuntimeLinearSettings } from '@/runtime/runtime-linear-client' import type { GitHubAssignableUser, - GlobalSettings, LinearWorkflowState, LinearLabel, LinearMember } from '../../../shared/types' +import { getTaskSourceRuntimeSettings } from '../../../shared/task-source-context' import { clearMetadataRequestStore, createMetadataRequestStore, @@ -193,10 +194,12 @@ const linearMemberStore = createMetadataRequestStore() function linearMetadataCacheKey( teamId: string, - settings: Pick | null | undefined, + settings: RuntimeLinearSettings, workspaceId?: string | null ): string { - const target = getActiveRuntimeTarget(settings) + const runtimeSettings = + settings && 'kind' in settings ? getTaskSourceRuntimeSettings(settings) : settings + const target = getActiveRuntimeTarget(runtimeSettings) const workspaceKey = workspaceId ?? 'selected' return target.kind === 'environment' ? `runtime:${target.environmentId}:${workspaceKey}:${teamId}` @@ -216,7 +219,7 @@ export function clearGitHubMetadataCache(): void { export function useTeamStates( teamId: string | null, - settings?: Pick | null, + settings?: RuntimeLinearSettings, workspaceId?: string | null ): MetadataState { const [state, setState] = useState>({ @@ -278,7 +281,7 @@ export function useTeamStates( export function useTeamLabels( teamId: string | null, - settings?: Pick | null, + settings?: RuntimeLinearSettings, workspaceId?: string | null ): MetadataState { const [state, setState] = useState>({ @@ -338,7 +341,7 @@ export function useTeamLabels( export function useTeamMembers( teamId: string | null, - settings?: Pick | null, + settings?: RuntimeLinearSettings, workspaceId?: string | null ): MetadataState { const [state, setState] = useState>({ diff --git a/src/renderer/src/store/slices/jira.test.ts b/src/renderer/src/store/slices/jira.test.ts index 6b320ca9c51..f045ae47412 100644 --- a/src/renderer/src/store/slices/jira.test.ts +++ b/src/renderer/src/store/slices/jira.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { create } from 'zustand' import type { AppState } from '../types' import type { JiraConnectionStatus, JiraIssue, JiraViewer } from '../../../../shared/types' +import type { TaskSourceContext } from '../../../../shared/task-source-context' import { credentialDecryptionMessage } from '../../../../shared/integration-credential-errors' import { createJiraSlice } from './jira' @@ -72,6 +73,19 @@ function issue(key: string): JiraIssue { } } +function jiraSourceContext(environmentId: string, siteId = 'site-1'): TaskSourceContext { + return { + kind: 'task-source', + provider: 'jira', + projectId: 'logical-project', + hostId: `runtime:${environmentId}`, + providerIdentity: { + provider: 'jira', + siteId + } + } +} + describe('createJiraSlice runtime context', () => { beforeEach(() => { vi.clearAllMocks() @@ -117,6 +131,25 @@ describe('createJiraSlice runtime context', () => { expect(store.getState().jiraIssueCache['selected::ORC-1']?.data?.title).toBe('Remote issue') }) + it('routes explicit source reads through their source context when focused runtime changes', async () => { + const store = createTestStore() + store.setState({ + jiraStatus: { connected: true, viewer: null, selectedSiteId: 'site-1' } + }) + const sourceContext = jiraSourceContext('source-runtime') + const sourceResult = deferred() + jiraListIssues.mockReturnValueOnce(sourceResult.promise) + + const request = store.getState().listJiraIssues('assigned', 30, { sourceContext }) + store.setState({ settings: { activeRuntimeEnvironmentId: 'focused-runtime' } as never }) + + sourceResult.resolve([{ ...issue('ALP-1'), title: 'Source issue' }]) + await expect(request).resolves.toMatchObject([{ key: 'ALP-1', title: 'Source issue' }]) + expect(jiraListIssues).toHaveBeenCalledWith(sourceContext, 'assigned', 30, 'site-1') + expect(Object.values(store.getState().jiraSearchCache)).toHaveLength(1) + expect(store.getState().jiraSearchCache['site-1::list::assigned::30']).toBeUndefined() + }) + it('returns a failed Jira connect result when the active runtime changes before completion', async () => { const store = createTestStore() const connectResult = deferred<{ ok: true; viewer: JiraViewer }>() diff --git a/src/renderer/src/store/slices/jira.ts b/src/renderer/src/store/slices/jira.ts index b34d894f5a4..628b96a19d9 100644 --- a/src/renderer/src/store/slices/jira.ts +++ b/src/renderer/src/store/slices/jira.ts @@ -24,6 +24,11 @@ import { } from '@/runtime/runtime-jira-client' import { getProviderRuntimeContextKey } from '@/lib/provider-runtime-context' import { translate } from '@/i18n/i18n' +import { + getTaskSourceCacheScope, + getTaskSourceRuntimeSettings, + type TaskSourceContext +} from '../../../../shared/task-source-context' const CACHE_TTL = 60_000 const MAX_CACHE_ENTRIES = 500 @@ -59,6 +64,15 @@ type InflightJiraReadRequest = { mutationGeneration: number } +type JiraReadOptions = { sourceContext?: TaskSourceContext | null } + +type JiraReadScope = { + settings: AppState['settings'] | TaskSourceContext | null + contextKey: string + cachePrefix: string | null + explicitSource: boolean +} + const inflightIssueRequests = new Map>() const inflightSearchRequests = new Map>() const inflightListRequests = new Map>() @@ -100,14 +114,40 @@ function isCurrentJiraRuntimeContext(contextKey: string, settings: AppState['set function canWriteJiraReadResult( contextKey: string, mutationGeneration: number, - settings: AppState['settings'] + settings: AppState['settings'], + explicitSource = false ): boolean { return ( mutationGeneration === jiraMutationGeneration && - isCurrentJiraRuntimeContext(contextKey, settings) + (explicitSource || isCurrentJiraRuntimeContext(contextKey, settings)) ) } +function getJiraReadScope( + settings: AppState['settings'], + sourceContext?: TaskSourceContext | null +): JiraReadScope { + if (!sourceContext) { + return { + settings, + contextKey: getProviderRuntimeContextKey(settings), + cachePrefix: null, + explicitSource: false + } + } + const runtimeSettings = getTaskSourceRuntimeSettings(sourceContext) + return { + settings: sourceContext, + contextKey: `${getProviderRuntimeContextKey(runtimeSettings)}::${getTaskSourceCacheScope(sourceContext)}`, + cachePrefix: getTaskSourceCacheScope(sourceContext), + explicitSource: true + } +} + +function scopedJiraCacheKey(scope: JiraReadScope, key: string): string { + return scope.cachePrefix ? `${scope.cachePrefix}::${key}` : key +} + export type JiraSlice = { jiraStatus: JiraConnectionStatus jiraStatusChecked: boolean @@ -126,9 +166,17 @@ export type JiraSlice = { ) => Promise<{ ok: true; viewer: JiraViewer } | { ok: false; error: string }> selectJiraSite: (siteId: JiraSiteSelection) => Promise disconnectJira: (siteId?: string | null) => Promise - fetchJiraIssue: (key: string, siteId?: string | null) => Promise - searchJiraIssues: (jql: string, limit?: number) => Promise - listJiraIssues: (filter?: JiraIssueFilter, limit?: number) => Promise + fetchJiraIssue: ( + key: string, + siteId?: string | null, + options?: JiraReadOptions + ) => Promise + searchJiraIssues: (jql: string, limit?: number, options?: JiraReadOptions) => Promise + listJiraIssues: ( + filter?: JiraIssueFilter, + limit?: number, + options?: JiraReadOptions + ) => Promise patchJiraIssue: (issueKey: string, patch: Partial) => void } @@ -295,9 +343,10 @@ export const createJiraSlice: StateCreator = (set, }) }, - fetchJiraIssue: async (key, siteId) => { - const contextKey = getProviderRuntimeContextKey(get().settings) - const issueCacheKey = `${siteId ?? 'selected'}::${key}` + fetchJiraIssue: async (key, siteId, options) => { + const scope = getJiraReadScope(get().settings, options?.sourceContext) + const { contextKey } = scope + const issueCacheKey = scopedJiraCacheKey(scope, `${siteId ?? 'selected'}::${key}`) const cached = get().jiraIssueCache[issueCacheKey] ?? get().jiraIssueCache[key] if (isFresh(cached)) { return cached.data @@ -312,11 +361,16 @@ export const createJiraSlice: StateCreator = (set, } let entry: InflightJiraReadRequest const requestMutationGeneration = jiraMutationGeneration - const promise = jiraGetIssue(get().settings, key, siteId) + const promise = jiraGetIssue(scope.settings, key, siteId) .then((issue) => { if ( inflightIssueRequests.get(issueCacheKey) === entry && - canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings) + canWriteJiraReadResult( + contextKey, + requestMutationGeneration, + get().settings, + scope.explicitSource + ) ) { set((s) => ({ jiraIssueCache: evictStaleEntries({ @@ -331,14 +385,24 @@ export const createJiraSlice: StateCreator = (set, console.warn('[jira] fetchJiraIssue failed:', error) if ( isIntegrationCredentialDecryptionError(error) && - canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings) + canWriteJiraReadResult( + contextKey, + requestMutationGeneration, + get().settings, + scope.explicitSource + ) ) { if (!shouldRefreshStatusAfterRead(siteId, get().jiraStatus)) { void get().checkJiraConnection() } } else if ( looksLikeAuthError(error) && - canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings) + canWriteJiraReadResult( + contextKey, + requestMutationGeneration, + get().settings, + scope.explicitSource + ) ) { set({ jiraStatus: { connected: false, viewer: null } }) } @@ -350,7 +414,12 @@ export const createJiraSlice: StateCreator = (set, } if ( shouldRefreshStatusAfterRead(siteId, get().jiraStatus) && - canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings) + canWriteJiraReadResult( + contextKey, + requestMutationGeneration, + get().settings, + scope.explicitSource + ) ) { void get().checkJiraConnection() } @@ -360,10 +429,11 @@ export const createJiraSlice: StateCreator = (set, return promise }, - searchJiraIssues: async (jql, limit = 30) => { - const contextKey = getProviderRuntimeContextKey(get().settings) + searchJiraIssues: async (jql, limit = 30, options) => { + const scope = getJiraReadScope(get().settings, options?.sourceContext) + const { contextKey } = scope const siteId = getSelectedSiteId(get().jiraStatus) - const cacheKey = `${siteId ?? 'default'}::${jql}::${limit}` + const cacheKey = scopedJiraCacheKey(scope, `${siteId ?? 'default'}::${jql}::${limit}`) const cached = get().jiraSearchCache[cacheKey] if (isFresh(cached)) { return cached.data ?? [] @@ -378,11 +448,16 @@ export const createJiraSlice: StateCreator = (set, } let entry: InflightJiraReadRequest const requestMutationGeneration = jiraMutationGeneration - const promise = jiraSearchIssues(get().settings, jql, limit, siteId) + const promise = jiraSearchIssues(scope.settings, jql, limit, siteId) .then((issues) => { if ( inflightSearchRequests.get(cacheKey) === entry && - canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings) + canWriteJiraReadResult( + contextKey, + requestMutationGeneration, + get().settings, + scope.explicitSource + ) ) { set((s) => ({ jiraSearchCache: evictStaleEntries({ @@ -397,14 +472,24 @@ export const createJiraSlice: StateCreator = (set, console.warn('[jira] searchJiraIssues failed:', error) if ( isIntegrationCredentialDecryptionError(error) && - canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings) + canWriteJiraReadResult( + contextKey, + requestMutationGeneration, + get().settings, + scope.explicitSource + ) ) { if (!shouldRefreshStatusAfterRead(siteId, get().jiraStatus)) { void get().checkJiraConnection() } } else if ( looksLikeAuthError(error) && - canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings) + canWriteJiraReadResult( + contextKey, + requestMutationGeneration, + get().settings, + scope.explicitSource + ) ) { set({ jiraStatus: { connected: false, viewer: null } }) } @@ -416,7 +501,12 @@ export const createJiraSlice: StateCreator = (set, } if ( shouldRefreshStatusAfterRead(siteId, get().jiraStatus) && - canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings) + canWriteJiraReadResult( + contextKey, + requestMutationGeneration, + get().settings, + scope.explicitSource + ) ) { void get().checkJiraConnection() } @@ -426,10 +516,11 @@ export const createJiraSlice: StateCreator = (set, return promise }, - listJiraIssues: async (filter = 'assigned', limit = 30) => { - const contextKey = getProviderRuntimeContextKey(get().settings) + listJiraIssues: async (filter = 'assigned', limit = 30, options) => { + const scope = getJiraReadScope(get().settings, options?.sourceContext) + const { contextKey } = scope const siteId = getSelectedSiteId(get().jiraStatus) - const cacheKey = `${siteId ?? 'default'}::list::${filter}::${limit}` + const cacheKey = scopedJiraCacheKey(scope, `${siteId ?? 'default'}::list::${filter}::${limit}`) const cached = get().jiraSearchCache[cacheKey] if (isFresh(cached)) { return cached.data ?? [] @@ -444,11 +535,16 @@ export const createJiraSlice: StateCreator = (set, } let entry: InflightJiraReadRequest const requestMutationGeneration = jiraMutationGeneration - const promise = jiraListIssues(get().settings, filter, limit, siteId) + const promise = jiraListIssues(scope.settings, filter, limit, siteId) .then((issues) => { if ( inflightListRequests.get(cacheKey) === entry && - canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings) + canWriteJiraReadResult( + contextKey, + requestMutationGeneration, + get().settings, + scope.explicitSource + ) ) { set((s) => ({ jiraSearchCache: evictStaleEntries({ @@ -463,14 +559,24 @@ export const createJiraSlice: StateCreator = (set, console.warn('[jira] listJiraIssues failed:', error) if ( isIntegrationCredentialDecryptionError(error) && - canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings) + canWriteJiraReadResult( + contextKey, + requestMutationGeneration, + get().settings, + scope.explicitSource + ) ) { if (!shouldRefreshStatusAfterRead(siteId, get().jiraStatus)) { void get().checkJiraConnection() } } else if ( looksLikeAuthError(error) && - canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings) + canWriteJiraReadResult( + contextKey, + requestMutationGeneration, + get().settings, + scope.explicitSource + ) ) { set({ jiraStatus: { connected: false, viewer: null } }) } @@ -482,7 +588,12 @@ export const createJiraSlice: StateCreator = (set, } if ( shouldRefreshStatusAfterRead(siteId, get().jiraStatus) && - canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings) + canWriteJiraReadResult( + contextKey, + requestMutationGeneration, + get().settings, + scope.explicitSource + ) ) { void get().checkJiraConnection() } diff --git a/src/renderer/src/store/slices/linear.test.ts b/src/renderer/src/store/slices/linear.test.ts index 725470840a1..b985c0fd6f9 100644 --- a/src/renderer/src/store/slices/linear.test.ts +++ b/src/renderer/src/store/slices/linear.test.ts @@ -12,6 +12,7 @@ import type { LinearTeam, LinearViewer } from '../../../../shared/types' +import type { TaskSourceContext } from '../../../../shared/task-source-context' import { credentialDecryptionMessage } from '../../../../shared/integration-credential-errors' import { createLinearSlice } from './linear' @@ -88,6 +89,22 @@ function project(id: string): LinearProjectSummary { return { id, name: id, workspaceId: 'workspace-1', workspaceName: 'Workspace' } } +function linearSourceContext( + environmentId: string, + workspaceId = 'workspace-1' +): TaskSourceContext { + return { + kind: 'task-source', + provider: 'linear', + projectId: 'logical-project', + hostId: `runtime:${environmentId}`, + providerIdentity: { + provider: 'linear', + workspaceId + } + } +} + function deferred() { let resolve!: (value: T) => void const promise = new Promise((res) => { @@ -752,6 +769,31 @@ describe('createLinearSlice caching', () => { expect(store.getState().getCachedLinearTeams('workspace-1')).toMatchObject([{ id: 'team-1' }]) }) + it('routes explicit source reads through their source context when focused runtime changes', async () => { + const store = createTestStore() + store.setState({ + linearStatus: { connected: true, viewer: null, selectedWorkspaceId: 'workspace-1' } + }) + const sourceContext = linearSourceContext('source-runtime') + const sourceResult = deferred>() + linearListIssues.mockReturnValueOnce(sourceResult.promise) + + const request = store.getState().listLinearIssues('all', 36, { sourceContext }) + store.setState({ settings: { activeRuntimeEnvironmentId: 'focused-runtime' } as never }) + + sourceResult.resolve({ items: [issue('LIN-SOURCE')] }) + await expect(request).resolves.toMatchObject({ items: [{ id: 'LIN-SOURCE' }] }) + expect(linearListIssues).toHaveBeenCalledWith(sourceContext, 'all', 36, 'workspace-1') + expect( + store + .getState() + .getCachedLinearIssues({ kind: 'list', filter: 'all', limit: 36 }, { sourceContext }) + ).toMatchObject({ items: [{ id: 'LIN-SOURCE' }] }) + expect( + store.getState().getCachedLinearIssues({ kind: 'list', filter: 'all', limit: 36 }) + ).toBeNull() + }) + it('patches issue-cache entries keyed by workspace-qualified ids', () => { const store = createTestStore() store.setState({ diff --git a/src/renderer/src/store/slices/linear.ts b/src/renderer/src/store/slices/linear.ts index d6a9bd58696..9d08a66c8b3 100644 --- a/src/renderer/src/store/slices/linear.ts +++ b/src/renderer/src/store/slices/linear.ts @@ -42,6 +42,11 @@ import { } from '@/runtime/runtime-linear-client' import { getProviderRuntimeContextKey } from '@/lib/provider-runtime-context' import { translate } from '@/i18n/i18n' +import { + getTaskSourceCacheScope, + getTaskSourceRuntimeSettings, + type TaskSourceContext +} from '../../../../shared/task-source-context' const CACHE_TTL = 60_000 // 60s — same as GitHub work-items revalidation TTL const TEAM_CACHE_TTL = 10 * 60_000 // Teams change rarely and block visible Linear rows. @@ -353,7 +358,14 @@ type LinearIssueReadArgs = | { kind: 'search'; query: string; limit?: number } | { kind: 'list'; filter?: 'assigned' | 'created' | 'all' | 'completed'; limit?: number } -type LinearFetchOptions = { force?: boolean } +type LinearFetchOptions = { force?: boolean; sourceContext?: TaskSourceContext | null } + +type LinearReadScope = { + settings: AppState['settings'] | TaskSourceContext | null + contextKey: string + cachePrefix: string | null + explicitSource: boolean +} function beginLinearMutation(): number { linearMutationGeneration += 1 @@ -376,15 +388,41 @@ function canWriteLinearReadResult( contextKey: string, generation: number, mutationGeneration: number, - settings: AppState['settings'] + settings: AppState['settings'], + explicitSource = false ): boolean { return ( generation === linearCacheGeneration && mutationGeneration === linearMutationGeneration && - isCurrentLinearRuntimeContext(contextKey, settings) + (explicitSource || isCurrentLinearRuntimeContext(contextKey, settings)) ) } +function getLinearReadScope( + settings: AppState['settings'], + sourceContext?: TaskSourceContext | null +): LinearReadScope { + if (!sourceContext) { + return { + settings, + contextKey: getProviderRuntimeContextKey(settings), + cachePrefix: null, + explicitSource: false + } + } + const runtimeSettings = getTaskSourceRuntimeSettings(sourceContext) + return { + settings: sourceContext, + contextKey: `${getProviderRuntimeContextKey(runtimeSettings)}::${getTaskSourceCacheScope(sourceContext)}`, + cachePrefix: getTaskSourceCacheScope(sourceContext), + explicitSource: true + } +} + +function scopedLinearCacheKey(scope: LinearReadScope, key: string): string { + return scope.cachePrefix ? `${scope.cachePrefix}::${key}` : key +} + export type LinearSlice = { linearStatus: LinearConnectionStatus linearStatusChecked: boolean @@ -414,12 +452,21 @@ export type LinearSlice = { selectLinearWorkspace: (workspaceId: LinearWorkspaceSelection) => Promise disconnectLinear: () => Promise disconnectLinearWorkspace: (workspaceId: string) => Promise - fetchLinearIssue: (id: string, workspaceId?: string | null) => Promise - refreshLinearIssue: (id: string, workspaceId?: string | null) => Promise + fetchLinearIssue: ( + id: string, + workspaceId?: string | null, + options?: LinearFetchOptions + ) => Promise + refreshLinearIssue: ( + id: string, + workspaceId?: string | null, + options?: LinearFetchOptions + ) => Promise getCachedLinearIssues: ( - args: LinearIssueReadArgs + args: LinearIssueReadArgs, + options?: Pick ) => LinearIssue[] | LinearCollectionResult | null - prefetchLinearIssues: (args: LinearIssueReadArgs) => void + prefetchLinearIssues: (args: LinearIssueReadArgs, options?: LinearFetchOptions) => void searchLinearIssues: ( query: string, limit?: number, @@ -802,9 +849,14 @@ export const createLinearSlice: StateCreator = (s }) }, - fetchLinearIssue: async (id: string, workspaceId?: string | null) => { - const contextKey = getProviderRuntimeContextKey(get().settings) - const issueCacheKey = `${workspaceId ?? 'selected'}::${id}` + fetchLinearIssue: async ( + id: string, + workspaceId?: string | null, + options?: LinearFetchOptions + ) => { + const scope = getLinearReadScope(get().settings, options?.sourceContext) + const { contextKey } = scope + const issueCacheKey = scopedLinearCacheKey(scope, `${workspaceId ?? 'selected'}::${id}`) const cached = get().linearIssueCache[issueCacheKey] ?? get().linearIssueCache[id] if (isFresh(cached)) { return cached.data @@ -822,7 +874,7 @@ export const createLinearSlice: StateCreator = (s let entry: InflightLinearIssueRequest const requestCacheGeneration = linearCacheGeneration const requestMutationGeneration = linearMutationGeneration - const promise = linearGetIssue(get().settings, id, workspaceId) + const promise = linearGetIssue(scope.settings, id, workspaceId) .then((issue) => { const data = issue as LinearIssue | null if ( @@ -831,7 +883,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { set((s) => ({ @@ -851,7 +904,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { void get().checkLinearConnection(true) @@ -868,7 +922,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { void get().checkLinearConnection(true) @@ -885,8 +940,13 @@ export const createLinearSlice: StateCreator = (s return promise }, - refreshLinearIssue: async (id: string, workspaceId?: string | null) => { - const issueCacheKey = `${workspaceId ?? 'selected'}::${id}` + refreshLinearIssue: async ( + id: string, + workspaceId?: string | null, + options?: LinearFetchOptions + ) => { + const scope = getLinearReadScope(get().settings, options?.sourceContext) + const issueCacheKey = scopedLinearCacheKey(scope, `${workspaceId ?? 'selected'}::${id}`) inflightIssueRequests.delete(issueCacheKey) clearLinearIssueCollectionRequestMaps() set((s) => { @@ -909,26 +969,37 @@ export const createLinearSlice: StateCreator = (s linearCustomViewIssueCache: {} } }) - return get().fetchLinearIssue(id, workspaceId) + return get().fetchLinearIssue(id, workspaceId, options) }, - getCachedLinearIssues: (args) => { + getCachedLinearIssues: (args, options) => { + const scope = getLinearReadScope(get().settings, options?.sourceContext) const workspaceId = getSelectedWorkspaceId(get().linearStatus) if (args.kind === 'search') { - const cacheKey = linearSearchCacheKey(workspaceId, args.query, args.limit ?? 20) + const cacheKey = scopedLinearCacheKey( + scope, + linearSearchCacheKey(workspaceId, args.query, args.limit ?? 20) + ) return get().linearSearchCache[cacheKey]?.data ?? null } const limit = clampLinearIssueListLimit(args.limit) - const cacheKey = linearListCacheKey(workspaceId, args.filter ?? 'assigned', limit) + const cacheKey = scopedLinearCacheKey( + scope, + linearListCacheKey(workspaceId, args.filter ?? 'assigned', limit) + ) return get().linearListCache[cacheKey]?.data ?? null }, - prefetchLinearIssues: (args) => { - const contextKey = getProviderRuntimeContextKey(get().settings) + prefetchLinearIssues: (args, options) => { + const scope = getLinearReadScope(get().settings, options?.sourceContext) + const { contextKey } = scope const workspaceId = getSelectedWorkspaceId(get().linearStatus) if (args.kind === 'search') { const limit = args.limit ?? 20 - const cacheKey = linearSearchCacheKey(workspaceId, args.query, limit) + const cacheKey = scopedLinearCacheKey( + scope, + linearSearchCacheKey(workspaceId, args.query, limit) + ) const inflight = inflightSearchRequests.get(cacheKey) if ( isFresh(get().linearSearchCache[cacheKey]) || @@ -939,12 +1010,15 @@ export const createLinearSlice: StateCreator = (s return } void get() - .searchLinearIssues(args.query, limit) + .searchLinearIssues(args.query, limit, options) .catch(() => {}) return } const limit = clampLinearIssueListLimit(args.limit) - const cacheKey = linearListCacheKey(workspaceId, args.filter ?? 'assigned', limit) + const cacheKey = scopedLinearCacheKey( + scope, + linearListCacheKey(workspaceId, args.filter ?? 'assigned', limit) + ) const inflight = inflightListRequests.get(cacheKey) if ( isFresh(get().linearListCache[cacheKey]) || @@ -955,14 +1029,15 @@ export const createLinearSlice: StateCreator = (s return } void get() - .listLinearIssues(args.filter, limit) + .listLinearIssues(args.filter, limit, options) .catch(() => {}) }, searchLinearIssues: async (query: string, limit = 20, options) => { - const contextKey = getProviderRuntimeContextKey(get().settings) + const scope = getLinearReadScope(get().settings, options?.sourceContext) + const { contextKey } = scope const workspaceId = getSelectedWorkspaceId(get().linearStatus) - const cacheKey = linearSearchCacheKey(workspaceId, query, limit) + const cacheKey = scopedLinearCacheKey(scope, linearSearchCacheKey(workspaceId, query, limit)) const cached = get().linearSearchCache[cacheKey] if (!options?.force && isFresh(cached)) { return cached.data ?? [] @@ -981,7 +1056,7 @@ export const createLinearSlice: StateCreator = (s let entry: InflightLinearListRequest const requestCacheGeneration = linearCacheGeneration const requestMutationGeneration = linearMutationGeneration - const promise = linearSearchIssues(get().settings, query, limit, workspaceId) + const promise = linearSearchIssues(scope.settings, query, limit, workspaceId) .then((issues) => { const data = issues as LinearIssue[] if ( @@ -990,7 +1065,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { set((s) => ({ @@ -1010,7 +1086,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { if (!shouldRefreshStatusAfterRead(workspaceId, get().linearStatus)) { @@ -1030,7 +1107,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { void get().checkLinearConnection(true) @@ -1049,10 +1127,14 @@ export const createLinearSlice: StateCreator = (s }, listLinearIssues: async (filter = 'assigned', limit = 20, options) => { - const contextKey = getProviderRuntimeContextKey(get().settings) + const scope = getLinearReadScope(get().settings, options?.sourceContext) + const { contextKey } = scope const workspaceId = getSelectedWorkspaceId(get().linearStatus) const effectiveLimit = clampLinearIssueListLimit(limit) - const cacheKey = linearListCacheKey(workspaceId, filter, effectiveLimit) + const cacheKey = scopedLinearCacheKey( + scope, + linearListCacheKey(workspaceId, filter, effectiveLimit) + ) const cached = get().linearListCache[cacheKey] if (!options?.force && isFresh(cached)) { return cached.data ?? emptyLinearCollection() @@ -1072,7 +1154,7 @@ export const createLinearSlice: StateCreator = (s const requestCacheGeneration = linearCacheGeneration const requestMutationGeneration = linearMutationGeneration const promise: Promise> = linearListIssues( - get().settings, + scope.settings, filter, effectiveLimit, workspaceId @@ -1085,7 +1167,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { set((s) => ({ @@ -1105,7 +1188,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { if (!shouldRefreshStatusAfterRead(workspaceId, get().linearStatus)) { @@ -1125,7 +1209,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { void get().checkLinearConnection(true) @@ -1149,9 +1234,10 @@ export const createLinearSlice: StateCreator = (s }, listLinearTeams: async (workspaceId, options) => { - const contextKey = getProviderRuntimeContextKey(get().settings) + const scope = getLinearReadScope(get().settings, options?.sourceContext) + const { contextKey } = scope const resolvedWorkspaceId = workspaceId ?? getSelectedWorkspaceId(get().linearStatus) - const cacheKey = linearTeamsCacheKey(resolvedWorkspaceId) + const cacheKey = scopedLinearCacheKey(scope, linearTeamsCacheKey(resolvedWorkspaceId)) const cached = get().linearTeamCache[cacheKey] if (!options?.force && isFresh(cached, TEAM_CACHE_TTL)) { return cached.data ?? [] @@ -1170,7 +1256,7 @@ export const createLinearSlice: StateCreator = (s let entry: InflightLinearTeamRequest const requestCacheGeneration = linearCacheGeneration const requestMutationGeneration = linearMutationGeneration - const promise = linearListTeams(get().settings, resolvedWorkspaceId) + const promise = linearListTeams(scope.settings, resolvedWorkspaceId) .then((teams) => { const data = teams as LinearTeam[] if ( @@ -1179,7 +1265,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { set((s) => ({ @@ -1199,7 +1286,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { if (!shouldRefreshStatusAfterRead(resolvedWorkspaceId, get().linearStatus)) { @@ -1219,7 +1307,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { void get().checkLinearConnection(true) @@ -1244,10 +1333,14 @@ export const createLinearSlice: StateCreator = (s }, listLinearProjects: async (query, limit = 20, workspaceId, options) => { - const contextKey = getProviderRuntimeContextKey(get().settings) + const scope = getLinearReadScope(get().settings, options?.sourceContext) + const { contextKey } = scope const resolvedWorkspaceId = workspaceId ?? getSelectedWorkspaceId(get().linearStatus) const trimmed = query?.trim() || undefined - const cacheKey = linearCollectionCacheKey(resolvedWorkspaceId, 'projects', trimmed, limit) + const cacheKey = scopedLinearCacheKey( + scope, + linearCollectionCacheKey(resolvedWorkspaceId, 'projects', trimmed, limit) + ) const cached = get().linearProjectCache[cacheKey] if (!options?.force && isFresh(cached)) { return cached.data ?? emptyLinearCollection() @@ -1266,7 +1359,7 @@ export const createLinearSlice: StateCreator = (s let entry: InflightLinearCollectionRequest const requestCacheGeneration = linearCacheGeneration const requestMutationGeneration = linearMutationGeneration - const promise = linearListProjects(get().settings, trimmed, limit, resolvedWorkspaceId, { + const promise = linearListProjects(scope.settings, trimmed, limit, resolvedWorkspaceId, { force: options?.force }) .then((result) => { @@ -1276,7 +1369,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { set((s) => ({ @@ -1296,7 +1390,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { void get().checkLinearConnection(true) @@ -1315,7 +1410,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { void get().checkLinearConnection(true) @@ -1334,8 +1430,12 @@ export const createLinearSlice: StateCreator = (s }, fetchLinearProject: async (id, workspaceId, options) => { - const contextKey = getProviderRuntimeContextKey(get().settings) - const cacheKey = linearCollectionCacheKey(workspaceId, 'project-detail', id) + const scope = getLinearReadScope(get().settings, options?.sourceContext) + const { contextKey } = scope + const cacheKey = scopedLinearCacheKey( + scope, + linearCollectionCacheKey(workspaceId, 'project-detail', id) + ) const cached = get().linearProjectDetailCache[cacheKey] if (!options?.force && isFresh(cached)) { return cached.data @@ -1354,7 +1454,7 @@ export const createLinearSlice: StateCreator = (s let entry: InflightLinearDetailRequest const requestCacheGeneration = linearCacheGeneration const requestMutationGeneration = linearMutationGeneration - const promise = linearGetProject(get().settings, id, workspaceId, { + const promise = linearGetProject(scope.settings, id, workspaceId, { force: options?.force }) .then((project) => { @@ -1364,7 +1464,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { set((s) => ({ @@ -1384,7 +1485,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { void get().checkLinearConnection(true) @@ -1408,7 +1510,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { void get().checkLinearConnection(true) @@ -1426,13 +1529,12 @@ export const createLinearSlice: StateCreator = (s }, listLinearProjectIssues: async (projectId, workspaceId, limit = 20, options) => { - const contextKey = getProviderRuntimeContextKey(get().settings) + const scope = getLinearReadScope(get().settings, options?.sourceContext) + const { contextKey } = scope const effectiveLimit = clampLinearIssueListLimit(limit) - const cacheKey = linearCollectionCacheKey( - workspaceId, - 'project-issues', - projectId, - effectiveLimit + const cacheKey = scopedLinearCacheKey( + scope, + linearCollectionCacheKey(workspaceId, 'project-issues', projectId, effectiveLimit) ) const cached = get().linearProjectIssueCache[cacheKey] if (!options?.force && isFresh(cached)) { @@ -1453,7 +1555,7 @@ export const createLinearSlice: StateCreator = (s const requestCacheGeneration = linearCacheGeneration const requestMutationGeneration = linearMutationGeneration const promise = linearListProjectIssues( - get().settings, + scope.settings, projectId, effectiveLimit, workspaceId, @@ -1468,7 +1570,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { set((s) => ({ @@ -1488,7 +1591,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { void get().checkLinearConnection(true) @@ -1515,7 +1619,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { void get().checkLinearConnection(true) @@ -1540,9 +1645,13 @@ export const createLinearSlice: StateCreator = (s }, listLinearCustomViews: async (model, limit = 20, workspaceId, options) => { - const contextKey = getProviderRuntimeContextKey(get().settings) + const scope = getLinearReadScope(get().settings, options?.sourceContext) + const { contextKey } = scope const resolvedWorkspaceId = workspaceId ?? getSelectedWorkspaceId(get().linearStatus) - const cacheKey = linearCollectionCacheKey(resolvedWorkspaceId, 'custom-views', model, limit) + const cacheKey = scopedLinearCacheKey( + scope, + linearCollectionCacheKey(resolvedWorkspaceId, 'custom-views', model, limit) + ) const cached = get().linearCustomViewCache[cacheKey] if (!options?.force && isFresh(cached)) { return cached.data ?? emptyLinearCollection() @@ -1561,7 +1670,7 @@ export const createLinearSlice: StateCreator = (s let entry: InflightLinearCollectionRequest const requestCacheGeneration = linearCacheGeneration const requestMutationGeneration = linearMutationGeneration - const promise = linearListCustomViews(get().settings, model, limit, resolvedWorkspaceId, { + const promise = linearListCustomViews(scope.settings, model, limit, resolvedWorkspaceId, { force: options?.force }) .then((result) => { @@ -1571,7 +1680,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { set((s) => ({ @@ -1591,7 +1701,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { void get().checkLinearConnection(true) @@ -1611,7 +1722,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { void get().checkLinearConnection(true) @@ -1630,8 +1742,12 @@ export const createLinearSlice: StateCreator = (s }, fetchLinearCustomView: async (viewId, workspaceId, model, options) => { - const contextKey = getProviderRuntimeContextKey(get().settings) - const cacheKey = linearCollectionCacheKey(workspaceId, 'custom-view-detail', model, viewId) + const scope = getLinearReadScope(get().settings, options?.sourceContext) + const { contextKey } = scope + const cacheKey = scopedLinearCacheKey( + scope, + linearCollectionCacheKey(workspaceId, 'custom-view-detail', model, viewId) + ) const cached = get().linearCustomViewDetailCache[cacheKey] if (!options?.force && isFresh(cached)) { return cached.data @@ -1650,7 +1766,7 @@ export const createLinearSlice: StateCreator = (s let entry: InflightLinearDetailRequest const requestCacheGeneration = linearCacheGeneration const requestMutationGeneration = linearMutationGeneration - const promise = linearGetCustomView(get().settings, viewId, model, workspaceId, { + const promise = linearGetCustomView(scope.settings, viewId, model, workspaceId, { force: options?.force }) .then((view) => { @@ -1660,7 +1776,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { set((s) => ({ @@ -1680,7 +1797,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { void get().checkLinearConnection(true) @@ -1704,7 +1822,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { void get().checkLinearConnection(true) @@ -1722,13 +1841,12 @@ export const createLinearSlice: StateCreator = (s }, listLinearCustomViewIssues: async (viewId, workspaceId, limit = 20, options) => { - const contextKey = getProviderRuntimeContextKey(get().settings) + const scope = getLinearReadScope(get().settings, options?.sourceContext) + const { contextKey } = scope const effectiveLimit = clampLinearIssueListLimit(limit) - const cacheKey = linearCollectionCacheKey( - workspaceId, - 'custom-view-issues', - viewId, - effectiveLimit + const cacheKey = scopedLinearCacheKey( + scope, + linearCollectionCacheKey(workspaceId, 'custom-view-issues', viewId, effectiveLimit) ) const cached = get().linearCustomViewIssueCache[cacheKey] if (!options?.force && isFresh(cached)) { @@ -1749,7 +1867,7 @@ export const createLinearSlice: StateCreator = (s const requestCacheGeneration = linearCacheGeneration const requestMutationGeneration = linearMutationGeneration const promise = linearListCustomViewIssues( - get().settings, + scope.settings, viewId, effectiveLimit, workspaceId, @@ -1764,7 +1882,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { set((s) => ({ @@ -1784,7 +1903,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { void get().checkLinearConnection(true) @@ -1811,7 +1931,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { void get().checkLinearConnection(true) @@ -1830,8 +1951,12 @@ export const createLinearSlice: StateCreator = (s }, listLinearCustomViewProjects: async (viewId, workspaceId, limit = 20, options) => { - const contextKey = getProviderRuntimeContextKey(get().settings) - const cacheKey = linearCollectionCacheKey(workspaceId, 'custom-view-projects', viewId, limit) + const scope = getLinearReadScope(get().settings, options?.sourceContext) + const { contextKey } = scope + const cacheKey = scopedLinearCacheKey( + scope, + linearCollectionCacheKey(workspaceId, 'custom-view-projects', viewId, limit) + ) const cached = get().linearCustomViewProjectCache[cacheKey] if (!options?.force && isFresh(cached)) { return cached.data ?? emptyLinearCollection() @@ -1850,7 +1975,7 @@ export const createLinearSlice: StateCreator = (s let entry: InflightLinearCollectionRequest const requestCacheGeneration = linearCacheGeneration const requestMutationGeneration = linearMutationGeneration - const promise = linearListCustomViewProjects(get().settings, viewId, limit, workspaceId, { + const promise = linearListCustomViewProjects(scope.settings, viewId, limit, workspaceId, { force: options?.force }) .then((result) => { @@ -1860,7 +1985,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { set((s) => ({ @@ -1880,7 +2006,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { void get().checkLinearConnection(true) @@ -1900,7 +2027,8 @@ export const createLinearSlice: StateCreator = (s contextKey, requestCacheGeneration, requestMutationGeneration, - get().settings + get().settings, + scope.explicitSource ) ) { void get().checkLinearConnection(true)