Route Linear and Jira tasks by source context

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-06-12 19:31:25 -07:00
co-authored by Orca
parent 762f42a4e4
commit 502a4bdebb
11 changed files with 754 additions and 232 deletions
@@ -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`.
@@ -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<void> {
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<JiraIssue | null>(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<void> => {
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) {
@@ -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<LinearIssue, 'title'> | Pick<LinearIssue, 'description'>) => void
density?: 'page' | 'drawer'
fields?: 'all' | 'title' | 'description'
sourceContext?: TaskSourceContext | null
}
function useAutosizeTextArea(value: string): React.RefObject<HTMLTextAreaElement | null> {
@@ -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<LinearIssueTextField | null>(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
@@ -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<void> {
@@ -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<LinearIssue | null>(null)
const [issueLoading, setIssueLoading] = useState(false)
const [comments, setComments] = useState<LinearComment[]>([])
@@ -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({
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-sleek">
<div className="mx-auto grid w-full grid-cols-1 gap-10 px-7 py-10 lg:grid-cols-[minmax(0,1fr)_320px] lg:px-10 xl:px-12">
<main className="min-w-0">
<LinearIssueTextEditor issue={displayed} onIssueChange={handleIssueTextChange} />
<LinearIssueTextEditor
issue={displayed}
onIssueChange={handleIssueTextChange}
sourceContext={sourceContext}
/>
<LinearIssueSubIssueButton issue={displayed} onOpenIssue={onOpenIssue} />
<LinearIssueSubIssueButton
issue={displayed}
onOpenIssue={onOpenIssue}
sourceContext={sourceContext}
/>
<section className="mt-12 border-t border-border/60 pt-9">
<div className="mb-8 flex items-center justify-between gap-3">
@@ -894,6 +914,7 @@ export default function LinearIssueWorkspace({
workspaceId={displayed.workspaceId}
onCommentAdded={handleCommentAdded}
variant="linear-page"
sourceContext={sourceContext}
/>
</section>
</main>
@@ -905,11 +926,13 @@ export default function LinearIssueWorkspace({
editState={editState}
onEditStateChange={handleEditStateChange}
layout="properties"
sourceContext={sourceContext}
/>
) : null}
<LinearIssueSidebarProjectCard
issue={displayed}
onProjectChanged={handleProjectChanged}
sourceContext={sourceContext}
/>
<section className="rounded-xl border border-border/60 bg-card text-card-foreground shadow-xs">
<div className="flex h-10 items-center gap-1 border-b border-border/50 px-4 text-sm font-medium text-muted-foreground">
@@ -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<LinearEditState>) => 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<LinearIssue | null>(null)
const [comments, setComments] = useState<LinearComment[]>([])
@@ -1178,6 +1211,7 @@ export default function LinearItemDrawer({
const hasEditedRef = useRef(false)
const optimisticCommentsRef = useRef<LinearComment[]>([])
const settings = useAppStore((s) => s.settings)
const providerSettings = sourceContext ?? settings
const handleEditStateChange = useCallback((patch: Partial<LinearEditState>) => {
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}
/>
</div>
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted-foreground">
@@ -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}
/>
</div>
@@ -1472,6 +1509,7 @@ export default function LinearItemDrawer({
issueId={displayed.id}
workspaceId={displayed.workspaceId}
onCommentAdded={handleCommentAdded}
sourceContext={sourceContext}
/>
<div className="flex-none border-t border-border/60 bg-background/40 px-4 py-3">
<Button
+174 -44
View File
@@ -500,14 +500,17 @@ function findLinearWorkflowStateForStatus(
function LinearStateCell({
issue,
className
className,
sourceContext
}: {
issue: LinearIssue
className?: string
sourceContext?: TaskSourceContext | null
}): React.JSX.Element {
const settings = useAppStore((s) => s.settings)
const providerSettings = sourceContext ?? settings
const patchLinearIssue = useAppStore((s) => s.patchLinearIssue)
const states = useTeamStates(issue.team.id, settings, issue.workspaceId)
const states = useTeamStates(issue.team.id, providerSettings, issue.workspaceId)
const [open, setOpen] = useState(false)
const [pending, setPending] = useState(false)
const reqRef = useRef(0)
@@ -534,7 +537,7 @@ function LinearStateCell({
setPending(true)
patchLinearIssue(issue.id, { state: nextState })
void linearUpdateIssue(settings, issue.id, { stateId }, issue.workspaceId)
void linearUpdateIssue(providerSettings, issue.id, { stateId }, issue.workspaceId)
.then((result) => {
if (reqId !== reqRef.current) {
return
@@ -571,7 +574,7 @@ function LinearStateCell({
issue.workspaceId,
patchLinearIssue,
pending,
settings,
providerSettings,
states.data
]
)
@@ -2768,7 +2771,7 @@ export default function TaskPage(): React.JSX.Element {
taskSource === 'github' || taskSource === 'gitlab'
? taskSourceRepoContexts.flatMap((context) => {
const host = hostRegistryById.get(context.hostId)
if (!host || (host.health === 'local' || host.health === 'available')) {
if (!host || host.health === 'local' || host.health === 'available') {
return []
}
return [
@@ -2786,6 +2789,59 @@ export default function TaskPage(): React.JSX.Element {
() => getSettingsFocusedExecutionHostId(settings),
[settings]
)
const fallbackTaskSourceProjectId = useMemo(() => {
const firstRepoContext = selectedRepos
.map((repo) => getTaskPageRepoSourceContext(repo, 'github'))
.find((context): context is TaskSourceContext => context !== null)
return firstRepoContext?.projectId ?? 'account-backed-task-source'
}, [selectedRepos])
const linearTaskSourceContext = useMemo(
() =>
normalizeTaskSourceContext({
provider: 'linear',
projectId: fallbackTaskSourceProjectId,
hostId: accountBackedTaskSourceHostId,
providerIdentity: {
provider: 'linear',
workspaceId:
selectedLinearWorkspaceId && selectedLinearWorkspaceId !== 'all'
? selectedLinearWorkspaceId
: null,
workspaceName:
selectedLinearWorkspace?.organizationName ??
selectedLinearWorkspace?.displayName ??
null
},
accountLabel:
selectedLinearWorkspace?.organizationName ?? selectedLinearWorkspace?.displayName ?? null
}),
[
accountBackedTaskSourceHostId,
fallbackTaskSourceProjectId,
selectedLinearWorkspace,
selectedLinearWorkspaceId
]
)
const jiraTaskSourceContext = useMemo(
() =>
normalizeTaskSourceContext({
provider: 'jira',
projectId: fallbackTaskSourceProjectId,
hostId: accountBackedTaskSourceHostId,
providerIdentity: {
provider: 'jira',
siteId: selectedJiraSiteId && selectedJiraSiteId !== 'all' ? selectedJiraSiteId : null,
siteUrl: selectedJiraSite?.siteUrl ?? null
},
accountLabel: selectedJiraSite?.displayName ?? selectedJiraSite?.siteUrl ?? null
}),
[
accountBackedTaskSourceHostId,
fallbackTaskSourceProjectId,
selectedJiraSite,
selectedJiraSiteId
]
)
const accountBackedTaskSourceHostAvailability = useMemo<TaskSourceHostAvailability[]>(() => {
if (taskSource !== 'linear' && taskSource !== 'jira') {
return []
@@ -3653,7 +3709,10 @@ export default function TaskPage(): React.JSX.Element {
let cancelled = false
if (context.kind === 'project') {
void fetchLinearProject(context.id, context.workspaceId, { force: true })
void fetchLinearProject(context.id, context.workspaceId, {
force: true,
sourceContext: linearTaskSourceContext
})
.then((project) => {
if (cancelled) {
return
@@ -3689,7 +3748,8 @@ export default function TaskPage(): React.JSX.Element {
setLinearCustomViewsLoading(true)
setLinearCustomViewsError(null)
void fetchLinearCustomView(context.id, context.workspaceId, context.model, {
force: true
force: true,
sourceContext: linearTaskSourceContext
})
.then((restoredView) => {
if (cancelled) {
@@ -3722,6 +3782,7 @@ export default function TaskPage(): React.JSX.Element {
fetchLinearProject,
listLinearCustomViews,
linearConnected,
linearTaskSourceContext,
setTaskResumeState,
taskResumeApplied,
taskResumeState?.linearContext,
@@ -3748,7 +3809,7 @@ export default function TaskPage(): React.JSX.Element {
// available for new-issue creation while the replacement fetch is pending,
// but a workspace-scoped cache can keep the selector usable immediately.
setAvailableTeams(cachedTeams ?? [])
void listLinearTeams(selectedLinearWorkspaceId)
void listLinearTeams(selectedLinearWorkspaceId, { sourceContext: linearTaskSourceContext })
.then((teams) => {
if (!cancelled) {
setAvailableTeams(teams)
@@ -3770,7 +3831,8 @@ export default function TaskPage(): React.JSX.Element {
linearTeamRefreshNonce,
taskResumeApplied,
getCachedLinearTeams,
listLinearTeams
listLinearTeams,
linearTaskSourceContext
])
const [availableJiraProjects, setAvailableJiraProjects] = useState<JiraProject[]>([])
@@ -3788,7 +3850,7 @@ export default function TaskPage(): React.JSX.Element {
let cancelled = false
setAvailableJiraProjects([])
setJiraProjectsLoading(true)
void jiraListProjects(settings, selectedJiraSiteId)
void jiraListProjects(jiraTaskSourceContext ?? settings, selectedJiraSiteId)
.then((projects) => {
if (!cancelled) {
setAvailableJiraProjects(projects)
@@ -3807,7 +3869,14 @@ export default function TaskPage(): React.JSX.Element {
return () => {
cancelled = true
}
}, [settings, taskSource, jiraConnected, selectedJiraSiteId, taskResumeApplied])
}, [
settings,
taskSource,
jiraConnected,
selectedJiraSiteId,
taskResumeApplied,
jiraTaskSourceContext
])
// Why: stable key for `selectedRepos` so the GitLab fetch effect below
// doesn't re-run on every parent re-render just because the array
@@ -4419,7 +4488,11 @@ export default function TaskPage(): React.JSX.Element {
}
try {
const states = await linearTeamStates(settings, issue.team.id, issue.workspaceId)
const states = await linearTeamStates(
linearTaskSourceContext ?? settings,
issue.team.id,
issue.workspaceId
)
const workflowState = findLinearWorkflowStateForStatus(states, targetState)
if (!workflowState) {
toast.error(
@@ -4443,7 +4516,7 @@ export default function TaskPage(): React.JSX.Element {
applyFallbackState(nextState)
const result = await linearUpdateIssue(
settings,
linearTaskSourceContext ?? settings,
issue.id,
{ stateId: workflowState.id },
issue.workspaceId
@@ -4481,6 +4554,7 @@ export default function TaskPage(): React.JSX.Element {
linearStatusBoardEnabled,
patchScopedLinearIssue,
patchLinearIssue,
linearTaskSourceContext,
settings
]
)
@@ -4580,7 +4654,7 @@ export default function TaskPage(): React.JSX.Element {
const targetWorkspaceId =
newLinearIssueTargetTeam.workspaceId ||
(selectedLinearWorkspaceId !== 'all' ? selectedLinearWorkspaceId : null)
linearListProjects(settings, undefined, 100, targetWorkspaceId)
linearListProjects(linearTaskSourceContext ?? settings, undefined, 100, targetWorkspaceId)
.then((p) => {
if (!cancelled) {
setNewLinearIssueProjects(p.items)
@@ -4601,6 +4675,7 @@ export default function TaskPage(): React.JSX.Element {
linearConnected,
newLinearIssueOpen,
newLinearIssueTargetTeam,
linearTaskSourceContext,
settings,
selectedLinearWorkspaceId
])
@@ -4866,7 +4941,7 @@ export default function TaskPage(): React.JSX.Element {
setAvailableJiraIssueTypes([])
setJiraIssueTypesLoading(true)
void jiraListIssueTypes(
settings,
jiraTaskSourceContext ?? settings,
newJiraIssueTargetProject.id,
newJiraIssueTargetProject.siteId
)
@@ -4892,7 +4967,7 @@ export default function TaskPage(): React.JSX.Element {
return () => {
cancelled = true
}
}, [settings, jiraConnected, newJiraIssueOpen, newJiraIssueTargetProject])
}, [settings, jiraConnected, newJiraIssueOpen, newJiraIssueTargetProject, jiraTaskSourceContext])
useEffect(() => {
if (
@@ -4913,7 +4988,7 @@ export default function TaskPage(): React.JSX.Element {
setJiraCreateFieldsError(null)
setNewJiraIssueCustomFieldValues({})
void jiraListCreateFields(
settings,
jiraTaskSourceContext ?? settings,
newJiraIssueTargetProject.id,
newJiraIssueTargetType.id,
newJiraIssueTargetProject.siteId
@@ -4938,7 +5013,14 @@ export default function TaskPage(): React.JSX.Element {
// responses after the user switches either selector.
cancelled = true
}
}, [settings, jiraConnected, newJiraIssueOpen, newJiraIssueTargetProject, newJiraIssueTargetType])
}, [
settings,
jiraConnected,
newJiraIssueOpen,
newJiraIssueTargetProject,
newJiraIssueTargetType,
jiraTaskSourceContext
])
// Why: defense-in-depth safety net applied to the current page's items.
// The active tab scopes requests to issues or PRs, and this keeps stale
@@ -5766,7 +5848,7 @@ export default function TaskPage(): React.JSX.Element {
}
setNewLinearProjectSubmitting(true)
try {
const result = await linearCreateProject(settings, {
const result = await linearCreateProject(linearTaskSourceContext ?? settings, {
name,
description: newLinearProjectDescription.trim() || undefined,
content: newLinearProjectContent.trim() || undefined,
@@ -5840,6 +5922,7 @@ export default function TaskPage(): React.JSX.Element {
newLinearProjectTargetDate,
newLinearProjectTargetTeam,
openLinearProjectContext,
linearTaskSourceContext,
settings
])
@@ -5867,7 +5950,7 @@ export default function TaskPage(): React.JSX.Element {
setNewLinearIssueSubmitting(true)
const submitProviderRuntimeContextKey = providerRuntimeContextKey
try {
const result = await linearCreateIssue(settings, {
const result = await linearCreateIssue(linearTaskSourceContext ?? settings, {
teamId: newLinearIssueTargetTeam.id,
title,
description: newLinearIssueBody || undefined,
@@ -5914,7 +5997,11 @@ export default function TaskPage(): React.JSX.Element {
// Why: auto-select the new issue in the inline workspace so the user
// sees exactly what was filed, mirroring the GitHub create-issue flow.
void linearGetIssue(settings, result.id, newLinearIssueTargetTeam.workspaceId)
void linearGetIssue(
linearTaskSourceContext ?? settings,
result.id,
newLinearIssueTargetTeam.workspaceId
)
.then((full) => {
if (submitProviderRuntimeContextKey !== providerRuntimeContextKeyRef.current) {
return
@@ -5942,6 +6029,7 @@ export default function TaskPage(): React.JSX.Element {
providerRuntimeContextKey,
selectedLinearProject,
setSelectedLinearIssue,
linearTaskSourceContext,
settings
])
@@ -5960,7 +6048,7 @@ export default function TaskPage(): React.JSX.Element {
setNewJiraIssueSubmitting(true)
const submitProviderRuntimeContextKey = providerRuntimeContextKey
try {
const result = await jiraCreateIssue(settings, {
const result = await jiraCreateIssue(jiraTaskSourceContext ?? settings, {
siteId: newJiraIssueTargetProject.siteId,
projectId: newJiraIssueTargetProject.id,
issueTypeId: newJiraIssueTargetType.id,
@@ -5997,7 +6085,11 @@ export default function TaskPage(): React.JSX.Element {
setNewJiraIssueCustomFieldValues({})
setJiraRefreshNonce((n) => n + 1)
void jiraGetIssue(settings, result.key, newJiraIssueTargetProject.siteId)
void jiraGetIssue(
jiraTaskSourceContext ?? settings,
result.key,
newJiraIssueTargetProject.siteId
)
.then((full) => {
if (submitProviderRuntimeContextKey !== providerRuntimeContextKeyRef.current) {
return
@@ -6025,6 +6117,7 @@ export default function TaskPage(): React.JSX.Element {
newJiraIssueTargetType,
newJiraIssueTitle,
providerRuntimeContextKey,
jiraTaskSourceContext,
settings,
setSelectedJiraIssue,
visibleJiraCreateFields
@@ -6172,7 +6265,7 @@ export default function TaskPage(): React.JSX.Element {
trimmed.length > 0
? ({ kind: 'search', query: trimmed, limit: LINEAR_ITEM_LIMIT } as const)
: ({ kind: 'list', filter: 'all', limit: effectiveLinearIssueLimit } as const)
const cachedResult = getCachedLinearIssues(readArgs)
const cachedResult = getCachedLinearIssues(readArgs, { sourceContext: linearTaskSourceContext })
if (readArgs.kind === 'search') {
setLinearIssuesHasMore(false)
if (cachedResult) {
@@ -6214,10 +6307,12 @@ export default function TaskPage(): React.JSX.Element {
const request =
readArgs.kind === 'search'
? searchLinearIssues(readArgs.query, LINEAR_ITEM_LIMIT, {
force: forceRefresh || shouldProbeOnLanding
force: forceRefresh || shouldProbeOnLanding,
sourceContext: linearTaskSourceContext
})
: listLinearIssues(readArgs.filter, effectiveLinearIssueLimit, {
force: forceRefresh || shouldProbeOnLanding
force: forceRefresh || shouldProbeOnLanding,
sourceContext: linearTaskSourceContext
})
void request
@@ -6279,7 +6374,8 @@ export default function TaskPage(): React.JSX.Element {
linearIssueLimit,
linearRefreshNonce,
taskResumeApplied,
getCachedLinearIssues
getCachedLinearIssues,
linearTaskSourceContext
])
useEffect(() => {
@@ -6309,7 +6405,8 @@ export default function TaskPage(): React.JSX.Element {
setLinearProjectsLoading(force || cached === null)
setLinearProjectsError(null)
void listLinearProjectsFromStore(query || undefined, LINEAR_ITEM_LIMIT, undefined, {
force
force,
sourceContext: linearTaskSourceContext
})
.then((result) => {
if (!cancelled) {
@@ -6338,7 +6435,8 @@ export default function TaskPage(): React.JSX.Element {
selectedLinearProject,
appliedLinearProjectSearch,
linearRefreshNonce,
getCachedLinearProjects
getCachedLinearProjects,
linearTaskSourceContext
])
useEffect(() => {
@@ -6350,7 +6448,8 @@ export default function TaskPage(): React.JSX.Element {
setLinearProjectDetailLoading(true)
setLinearProjectDetailError(null)
void fetchLinearProject(selectedLinearProject.id, selectedLinearProject.workspaceId, {
force: linearRefreshNonce > 0
force: linearRefreshNonce > 0,
sourceContext: linearTaskSourceContext
})
.then((project) => {
if (!cancelled) {
@@ -6376,7 +6475,13 @@ export default function TaskPage(): React.JSX.Element {
return () => {
cancelled = true
}
}, [fetchLinearProject, linearRefreshNonce, selectedLinearProject, setTaskResumeState])
}, [
fetchLinearProject,
linearRefreshNonce,
selectedLinearProject,
setTaskResumeState,
linearTaskSourceContext
])
useEffect(() => {
if (!selectedLinearProject?.workspaceId || linearProjectTab !== 'issues') {
@@ -6390,7 +6495,7 @@ export default function TaskPage(): React.JSX.Element {
selectedLinearProject.id,
selectedLinearProject.workspaceId,
effectiveLimit,
{ force: linearRefreshNonce > 0 }
{ force: linearRefreshNonce > 0, sourceContext: linearTaskSourceContext }
)
.then((result) => {
if (!cancelled) {
@@ -6414,6 +6519,7 @@ export default function TaskPage(): React.JSX.Element {
linearProjectTab,
linearRefreshNonce,
listLinearProjectIssues,
linearTaskSourceContext,
selectedLinearProject
])
@@ -6441,7 +6547,10 @@ export default function TaskPage(): React.JSX.Element {
// models avoids a second, redundant Issues/Projects switch.
void Promise.all(
LINEAR_CUSTOM_VIEW_MODELS.map((model) =>
listLinearCustomViews(model, LINEAR_ITEM_LIMIT, undefined, { force })
listLinearCustomViews(model, LINEAR_ITEM_LIMIT, undefined, {
force,
sourceContext: linearTaskSourceContext
})
)
)
.then((result) => {
@@ -6471,7 +6580,8 @@ export default function TaskPage(): React.JSX.Element {
selectedLinearCustomView,
linearRefreshNonce,
getCachedLinearCustomViews,
listLinearCustomViews
listLinearCustomViews,
linearTaskSourceContext
])
useEffect(() => {
@@ -6490,13 +6600,13 @@ export default function TaskPage(): React.JSX.Element {
selectedLinearCustomView.id,
selectedLinearCustomView.workspaceId,
issueLimit,
{ force: linearRefreshNonce > 0 }
{ force: linearRefreshNonce > 0, sourceContext: linearTaskSourceContext }
)
: listLinearCustomViewProjects(
selectedLinearCustomView.id,
selectedLinearCustomView.workspaceId,
LINEAR_ITEM_LIMIT,
{ force: linearRefreshNonce > 0 }
{ force: linearRefreshNonce > 0, sourceContext: linearTaskSourceContext }
)
void request
.then((result) => {
@@ -6526,6 +6636,7 @@ export default function TaskPage(): React.JSX.Element {
linearCustomViewIssueLimit,
listLinearCustomViewIssues,
listLinearCustomViewProjects,
linearTaskSourceContext,
selectedLinearCustomView
])
@@ -6606,8 +6717,10 @@ export default function TaskPage(): React.JSX.Element {
const trimmed = appliedJiraSearch.trim()
const request =
trimmed.length > 0
? searchJiraIssues(trimmed, JIRA_ITEM_LIMIT)
: listJiraIssues(activeJiraPreset, JIRA_ITEM_LIMIT)
? searchJiraIssues(trimmed, JIRA_ITEM_LIMIT, { sourceContext: jiraTaskSourceContext })
: listJiraIssues(activeJiraPreset, JIRA_ITEM_LIMIT, {
sourceContext: jiraTaskSourceContext
})
void request
.then((issues) => {
@@ -6636,7 +6749,8 @@ export default function TaskPage(): React.JSX.Element {
appliedJiraSearch,
activeJiraPreset,
jiraRefreshNonce,
taskResumeApplied
taskResumeApplied,
jiraTaskSourceContext
])
useEffect(() => {
@@ -6677,11 +6791,12 @@ export default function TaskPage(): React.JSX.Element {
const linkedWorkItem = buildLinearIssueLinkedWorkItem(issue)
openModal('new-workspace-composer', {
linkedWorkItem,
taskSourceContext: linearTaskSourceContext,
prefilledName: getLinearIssueWorkspaceName(issue),
telemetrySource: 'sidebar'
})
},
[openModal]
[linearTaskSourceContext, openModal]
)
const handleUseLinearItem = useCallback(
@@ -6775,11 +6890,12 @@ export default function TaskPage(): React.JSX.Element {
}
openModal('new-workspace-composer', {
linkedWorkItem,
taskSourceContext: jiraTaskSourceContext,
prefilledName: getJiraIssueWorkspaceSeed(issue),
telemetrySource: 'sidebar'
})
},
[openModal]
[jiraTaskSourceContext, openModal]
)
const handleUseJiraItem = useCallback(
@@ -8935,6 +9051,7 @@ export default function TaskPage(): React.JSX.Element {
issue={selectedJiraIssue}
onUse={handleUseJiraItem}
onClose={() => setSelectedJiraIssue(null)}
sourceContext={jiraTaskSourceContext}
/>
</div>
)
@@ -8946,6 +9063,7 @@ export default function TaskPage(): React.JSX.Element {
onUse={handleUseLinearItem}
onOpenIssue={openRelatedLinearIssue}
onClose={closeTaskDetailPage}
sourceContext={linearTaskSourceContext}
/>
) : !linearStatusReady ? (
<div className="mt-4 flex items-center justify-center py-14">
@@ -9546,7 +9664,11 @@ export default function TaskPage(): React.JSX.Element {
</div>
<div className="mt-2 flex flex-wrap items-center gap-1.5 text-[11px] text-muted-foreground">
{effectiveLinearDisplayProperties.has('state') ? (
<LinearStateCell issue={issue} className="px-1.5 py-0.5" />
<LinearStateCell
issue={issue}
className="px-1.5 py-0.5"
sourceContext={linearTaskSourceContext}
/>
) : null}
{effectiveLinearDisplayProperties.has('assignee') ? (
<span>
@@ -9661,7 +9783,11 @@ export default function TaskPage(): React.JSX.Element {
</div>
<div className="mt-1 flex min-w-0 items-center gap-1.5 lg:!hidden">
{effectiveLinearDisplayProperties.has('state') ? (
<LinearStateCell issue={issue} className="px-1.5 py-0.5" />
<LinearStateCell
issue={issue}
className="px-1.5 py-0.5"
sourceContext={linearTaskSourceContext}
/>
) : null}
{effectiveLinearDisplayProperties.has('assignee') ? (
<span className="min-w-0 truncate text-[11px] text-muted-foreground">
@@ -9703,7 +9829,11 @@ export default function TaskPage(): React.JSX.Element {
{effectiveLinearDisplayProperties.has('state') ? (
<div className="flex min-w-0 max-lg:!hidden">
<LinearStateCell issue={issue} className="max-w-full px-2 py-0.5" />
<LinearStateCell
issue={issue}
className="max-w-full px-2 py-0.5"
sourceContext={linearTaskSourceContext}
/>
</div>
) : null}
+10 -7
View File
@@ -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<LinearMember[]>()
function linearMetadataCacheKey(
teamId: string,
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | 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<GlobalSettings, 'activeRuntimeEnvironmentId'> | null,
settings?: RuntimeLinearSettings,
workspaceId?: string | null
): MetadataState<LinearWorkflowState[]> {
const [state, setState] = useState<MetadataState<LinearWorkflowState[]>>({
@@ -278,7 +281,7 @@ export function useTeamStates(
export function useTeamLabels(
teamId: string | null,
settings?: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null,
settings?: RuntimeLinearSettings,
workspaceId?: string | null
): MetadataState<LinearLabel[]> {
const [state, setState] = useState<MetadataState<LinearLabel[]>>({
@@ -338,7 +341,7 @@ export function useTeamLabels(
export function useTeamMembers(
teamId: string | null,
settings?: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null,
settings?: RuntimeLinearSettings,
workspaceId?: string | null
): MetadataState<LinearMember[]> {
const [state, setState] = useState<MetadataState<LinearMember[]>>({
@@ -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<JiraIssue[]>()
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 }>()
+140 -29
View File
@@ -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<T> = {
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<string, InflightJiraReadRequest<JiraIssue | null>>()
const inflightSearchRequests = new Map<string, InflightJiraReadRequest<JiraIssue[]>>()
const inflightListRequests = new Map<string, InflightJiraReadRequest<JiraIssue[]>>()
@@ -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<void>
disconnectJira: (siteId?: string | null) => Promise<void>
fetchJiraIssue: (key: string, siteId?: string | null) => Promise<JiraIssue | null>
searchJiraIssues: (jql: string, limit?: number) => Promise<JiraIssue[]>
listJiraIssues: (filter?: JiraIssueFilter, limit?: number) => Promise<JiraIssue[]>
fetchJiraIssue: (
key: string,
siteId?: string | null,
options?: JiraReadOptions
) => Promise<JiraIssue | null>
searchJiraIssues: (jql: string, limit?: number, options?: JiraReadOptions) => Promise<JiraIssue[]>
listJiraIssues: (
filter?: JiraIssueFilter,
limit?: number,
options?: JiraReadOptions
) => Promise<JiraIssue[]>
patchJiraIssue: (issueKey: string, patch: Partial<JiraIssue>) => void
}
@@ -295,9 +343,10 @@ export const createJiraSlice: StateCreator<AppState, [], [], JiraSlice> = (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<AppState, [], [], JiraSlice> = (set,
}
let entry: InflightJiraReadRequest<JiraIssue | null>
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<AppState, [], [], JiraSlice> = (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<AppState, [], [], JiraSlice> = (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<AppState, [], [], JiraSlice> = (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<AppState, [], [], JiraSlice> = (set,
}
let entry: InflightJiraReadRequest<JiraIssue[]>
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<AppState, [], [], JiraSlice> = (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<AppState, [], [], JiraSlice> = (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<AppState, [], [], JiraSlice> = (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<AppState, [], [], JiraSlice> = (set,
}
let entry: InflightJiraReadRequest<JiraIssue[]>
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<AppState, [], [], JiraSlice> = (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<AppState, [], [], JiraSlice> = (set,
}
if (
shouldRefreshStatusAfterRead(siteId, get().jiraStatus) &&
canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings)
canWriteJiraReadResult(
contextKey,
requestMutationGeneration,
get().settings,
scope.explicitSource
)
) {
void get().checkJiraConnection()
}
@@ -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<T>() {
let resolve!: (value: T) => void
const promise = new Promise<T>((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<LinearCollectionResult<LinearIssue>>()
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({
+222 -94
View File
@@ -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<void>
disconnectLinear: () => Promise<void>
disconnectLinearWorkspace: (workspaceId: string) => Promise<void>
fetchLinearIssue: (id: string, workspaceId?: string | null) => Promise<LinearIssue | null>
refreshLinearIssue: (id: string, workspaceId?: string | null) => Promise<LinearIssue | null>
fetchLinearIssue: (
id: string,
workspaceId?: string | null,
options?: LinearFetchOptions
) => Promise<LinearIssue | null>
refreshLinearIssue: (
id: string,
workspaceId?: string | null,
options?: LinearFetchOptions
) => Promise<LinearIssue | null>
getCachedLinearIssues: (
args: LinearIssueReadArgs
args: LinearIssueReadArgs,
options?: Pick<LinearFetchOptions, 'sourceContext'>
) => LinearIssue[] | LinearCollectionResult<LinearIssue> | null
prefetchLinearIssues: (args: LinearIssueReadArgs) => void
prefetchLinearIssues: (args: LinearIssueReadArgs, options?: LinearFetchOptions) => void
searchLinearIssues: (
query: string,
limit?: number,
@@ -802,9 +849,14 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (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<AppState, [], [], LinearSlice> = (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<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
set((s) => ({
@@ -851,7 +904,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
void get().checkLinearConnection(true)
@@ -868,7 +922,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
void get().checkLinearConnection(true)
@@ -885,8 +940,13 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (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<AppState, [], [], LinearSlice> = (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<AppState, [], [], LinearSlice> = (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<AppState, [], [], LinearSlice> = (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<AppState, [], [], LinearSlice> = (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<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
set((s) => ({
@@ -1010,7 +1086,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
if (!shouldRefreshStatusAfterRead(workspaceId, get().linearStatus)) {
@@ -1030,7 +1107,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
void get().checkLinearConnection(true)
@@ -1049,10 +1127,14 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (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<LinearIssue>()
@@ -1072,7 +1154,7 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
const requestCacheGeneration = linearCacheGeneration
const requestMutationGeneration = linearMutationGeneration
const promise: Promise<LinearCollectionResult<LinearIssue>> = linearListIssues(
get().settings,
scope.settings,
filter,
effectiveLimit,
workspaceId
@@ -1085,7 +1167,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
set((s) => ({
@@ -1105,7 +1188,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
if (!shouldRefreshStatusAfterRead(workspaceId, get().linearStatus)) {
@@ -1125,7 +1209,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
void get().checkLinearConnection(true)
@@ -1149,9 +1234,10 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (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<AppState, [], [], LinearSlice> = (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<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
set((s) => ({
@@ -1199,7 +1286,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
if (!shouldRefreshStatusAfterRead(resolvedWorkspaceId, get().linearStatus)) {
@@ -1219,7 +1307,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
void get().checkLinearConnection(true)
@@ -1244,10 +1333,14 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (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<LinearProjectSummary>()
@@ -1266,7 +1359,7 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
let entry: InflightLinearCollectionRequest<LinearProjectSummary>
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<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
set((s) => ({
@@ -1296,7 +1390,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
void get().checkLinearConnection(true)
@@ -1315,7 +1410,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
void get().checkLinearConnection(true)
@@ -1334,8 +1430,12 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (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<AppState, [], [], LinearSlice> = (s
let entry: InflightLinearDetailRequest<LinearProjectDetail | null>
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<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
set((s) => ({
@@ -1384,7 +1485,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
void get().checkLinearConnection(true)
@@ -1408,7 +1510,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
void get().checkLinearConnection(true)
@@ -1426,13 +1529,12 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (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<AppState, [], [], LinearSlice> = (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<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
set((s) => ({
@@ -1488,7 +1591,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
void get().checkLinearConnection(true)
@@ -1515,7 +1619,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
void get().checkLinearConnection(true)
@@ -1540,9 +1645,13 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (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<LinearCustomViewSummary>()
@@ -1561,7 +1670,7 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
let entry: InflightLinearCollectionRequest<LinearCustomViewSummary>
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<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
set((s) => ({
@@ -1591,7 +1701,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
void get().checkLinearConnection(true)
@@ -1611,7 +1722,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
void get().checkLinearConnection(true)
@@ -1630,8 +1742,12 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (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<AppState, [], [], LinearSlice> = (s
let entry: InflightLinearDetailRequest<LinearCustomViewSummary | null>
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<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
set((s) => ({
@@ -1680,7 +1797,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
void get().checkLinearConnection(true)
@@ -1704,7 +1822,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
void get().checkLinearConnection(true)
@@ -1722,13 +1841,12 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (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<AppState, [], [], LinearSlice> = (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<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
set((s) => ({
@@ -1784,7 +1903,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
void get().checkLinearConnection(true)
@@ -1811,7 +1931,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
void get().checkLinearConnection(true)
@@ -1830,8 +1951,12 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (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<LinearProjectSummary>()
@@ -1850,7 +1975,7 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
let entry: InflightLinearCollectionRequest<LinearProjectSummary>
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<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
set((s) => ({
@@ -1880,7 +2006,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
void get().checkLinearConnection(true)
@@ -1900,7 +2027,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
contextKey,
requestCacheGeneration,
requestMutationGeneration,
get().settings
get().settings,
scope.explicitSource
)
) {
void get().checkLinearConnection(true)