diff --git a/notes/host-context-implementation-checklist.md b/notes/host-context-implementation-checklist.md
index 521bd87a5ab..99f1821798f 100644
--- a/notes/host-context-implementation-checklist.md
+++ b/notes/host-context-implementation-checklist.md
@@ -227,3 +227,4 @@ Done means: reviewers and future agents can understand the vision, implementatio
- [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`.
+- [x] 2026-06-12: Preserved Linear task detail source context in `openTaskPage` data, back/forward history entries, history de-dupe keys, and Linear warm prefetch so direct detail opens replay the same account/host source instead of recomputing from focused settings; verified with UI/history tests (2 files / 131 tests) and full `pnpm run typecheck`.
diff --git a/src/renderer/src/components/TaskPage.tsx b/src/renderer/src/components/TaskPage.tsx
index 5c19554cf06..292c580309a 100644
--- a/src/renderer/src/components/TaskPage.tsx
+++ b/src/renderer/src/components/TaskPage.tsx
@@ -3357,6 +3357,21 @@ export default function TaskPage(): React.JSX.Element {
const selectedLinearIssue = selectedLinearIssueId
? (cachedSelectedLinearIssue ?? selectedLinearIssueFallback)
: null
+ const linearDetailSourceContext = useMemo(() => {
+ if (
+ selectedLinearIssue &&
+ pageData.openLinearSourceContext?.provider === 'linear' &&
+ pageData.openLinearIssue?.id === selectedLinearIssue.id
+ ) {
+ return pageData.openLinearSourceContext
+ }
+ return linearTaskSourceContext
+ }, [
+ linearTaskSourceContext,
+ pageData.openLinearIssue,
+ pageData.openLinearSourceContext,
+ selectedLinearIssue
+ ])
const setSelectedLinearIssue = useCallback(
(issue: LinearIssue | null, options?: { allowOutsideList?: boolean }) => {
@@ -3384,11 +3399,15 @@ export default function TaskPage(): React.JSX.Element {
const openLinearDetailPage = useCallback(
(issue: LinearIssue) => {
openTaskPage(
- { taskSource: 'linear', openLinearIssue: issue },
+ {
+ taskSource: 'linear',
+ openLinearIssue: issue,
+ openLinearSourceContext: linearTaskSourceContext
+ },
{ recordTasksInteraction: false }
)
},
- [openTaskPage]
+ [linearTaskSourceContext, openTaskPage]
)
const openRelatedLinearIssue = useCallback(
@@ -3417,7 +3436,8 @@ export default function TaskPage(): React.JSX.Element {
openGitHubWorkItem: undefined,
openGitHubSourceContext: undefined,
openGitHubInitialTab: undefined,
- openLinearIssue: undefined
+ openLinearIssue: undefined,
+ openLinearSourceContext: undefined
}
}))
}, [clearSelectedLinearIssue, setDialogWorkItem])
@@ -9063,7 +9083,7 @@ export default function TaskPage(): React.JSX.Element {
onUse={handleUseLinearItem}
onOpenIssue={openRelatedLinearIssue}
onClose={closeTaskDetailPage}
- sourceContext={linearTaskSourceContext}
+ sourceContext={linearDetailSourceContext}
/>
) : !linearStatusReady ? (
diff --git a/src/renderer/src/store/slices/ui.test.ts b/src/renderer/src/store/slices/ui.test.ts
index 6e563f69392..073570e5aa5 100644
--- a/src/renderer/src/store/slices/ui.test.ts
+++ b/src/renderer/src/store/slices/ui.test.ts
@@ -1429,6 +1429,39 @@ describe('createUISlice settings navigation', () => {
)
})
+ it('prefetches direct Linear task opens with their source context', () => {
+ const store = createUIStore()
+ const prefetchLinearIssues = vi.fn()
+ const linearIssue = makeLinearIssue()
+ const sourceContext: TaskSourceContext = {
+ kind: 'task-source',
+ provider: 'linear',
+ projectId: 'project-1',
+ hostId: 'runtime:remote-server',
+ providerIdentity: { provider: 'linear', workspaceId: 'workspace-1' }
+ }
+
+ store.setState({
+ settings: {
+ visibleTaskProviders: ['linear'],
+ defaultTaskSource: 'linear'
+ } as unknown as AppState['settings'],
+ linearStatus: { connected: true } as AppState['linearStatus'],
+ prefetchLinearIssues
+ } as unknown as Partial
)
+
+ store.getState().openTaskPage({
+ taskSource: 'linear',
+ openLinearIssue: linearIssue,
+ openLinearSourceContext: sourceContext
+ })
+
+ expect(prefetchLinearIssues).toHaveBeenCalledWith(
+ { kind: 'list', filter: 'all', limit: expect.any(Number) },
+ { sourceContext }
+ )
+ })
+
it('returns to the tasks page after visiting settings from an in-progress draft', () => {
const store = createUIStore()
@@ -1614,6 +1647,31 @@ describe('createUISlice page navigation history', () => {
})
})
+ it('preserves Linear task detail source context in navigation history', () => {
+ const store = createUIStore()
+ const linearIssue = makeLinearIssue()
+ const sourceContext: TaskSourceContext = {
+ kind: 'task-source',
+ provider: 'linear',
+ projectId: 'project-1',
+ hostId: 'runtime:remote-server',
+ providerIdentity: { provider: 'linear', workspaceId: 'workspace-1' }
+ }
+
+ store.getState().openTaskPage({
+ taskSource: 'linear',
+ openLinearIssue: linearIssue,
+ openLinearSourceContext: sourceContext
+ })
+
+ expect(store.getState().worktreeNavHistory.at(-1)).toEqual({
+ kind: 'task-detail',
+ source: 'linear',
+ issue: linearIssue,
+ sourceContext
+ })
+ })
+
it('can suppress the Tasks surface interaction for in-page provider navigation', () => {
const store = createUIStore()
const recordFeatureInteraction = vi.fn()
diff --git a/src/renderer/src/store/slices/ui.ts b/src/renderer/src/store/slices/ui.ts
index 25a61e6b6e7..c78bd162c1d 100644
--- a/src/renderer/src/store/slices/ui.ts
+++ b/src/renderer/src/store/slices/ui.ts
@@ -606,6 +606,7 @@ export type UISlice = {
openGitHubSourceContext?: TaskSourceContext | null
openGitHubInitialTab?: 'conversation' | 'checks' | 'files'
openLinearIssue?: LinearIssue
+ openLinearSourceContext?: TaskSourceContext | null
}
taskResumeState: TaskResumeState | undefined
setTaskResumeState: (updates: Partial) => void
@@ -1108,7 +1109,8 @@ export const createUISlice: StateCreator = (set, get)
? ({
kind: 'task-detail',
source: 'linear',
- issue: data.openLinearIssue
+ issue: data.openLinearIssue,
+ sourceContext: data.openLinearSourceContext
} as const)
: null
const currentEntry = get().worktreeNavHistory[get().worktreeNavHistoryIndex]
@@ -1191,16 +1193,24 @@ export const createUISlice: StateCreator = (set, get)
if (resolvedSource === 'linear' && typeof state.prefetchLinearIssues === 'function') {
const resume = state.taskResumeState
const query = (resume?.linearQuery ?? '').trim()
+ const sourceContext =
+ data.openLinearSourceContext?.provider === 'linear' ? data.openLinearSourceContext : null
if (query) {
- state.prefetchLinearIssues({ kind: 'search', query, limit: LINEAR_TASK_PREFETCH_LIMIT })
+ state.prefetchLinearIssues(
+ { kind: 'search', query, limit: LINEAR_TASK_PREFETCH_LIMIT },
+ { sourceContext }
+ )
} else {
// Why: TaskPage no longer exposes Linear preset filters; keep warm
// prefetch aligned with the default unsearched issue list.
- state.prefetchLinearIssues({
- kind: 'list',
- filter: 'all',
- limit: LINEAR_TASK_PREFETCH_LIMIT
- })
+ state.prefetchLinearIssues(
+ {
+ kind: 'list',
+ filter: 'all',
+ limit: LINEAR_TASK_PREFETCH_LIMIT
+ },
+ { sourceContext }
+ )
}
}
},
diff --git a/src/renderer/src/store/slices/worktree-nav-history.ts b/src/renderer/src/store/slices/worktree-nav-history.ts
index dc4e9a802dc..cda30db3d66 100644
--- a/src/renderer/src/store/slices/worktree-nav-history.ts
+++ b/src/renderer/src/store/slices/worktree-nav-history.ts
@@ -2,7 +2,10 @@ import type { StateCreator } from 'zustand'
import type { AppState } from '../types'
import { findWorktreeById } from './worktree-helpers'
import type { GitHubWorkItem, LinearIssue } from '../../../../shared/types'
-import { getTaskSourceCacheScope, type TaskSourceContext } from '../../../../shared/task-source-context'
+import {
+ getTaskSourceCacheScope,
+ type TaskSourceContext
+} from '../../../../shared/task-source-context'
// Why: cap the per-session history so a long-lived workspace with many
// worktree jumps cannot grow the array unbounded. 50 is generous enough
@@ -24,7 +27,12 @@ export type WorktreeNavHistoryTaskDetailEntry =
sourceContext?: TaskSourceContext | null
initialTab?: 'conversation' | 'checks' | 'files'
}
- | { kind: 'task-detail'; source: 'linear'; issue: LinearIssue }
+ | {
+ kind: 'task-detail'
+ source: 'linear'
+ issue: LinearIssue
+ sourceContext?: TaskSourceContext | null
+ }
export type WorktreeNavHistoryViewEntry =
| WorktreeNavHistorySimpleViewEntry
| WorktreeNavHistoryTaskDetailEntry
@@ -90,7 +98,11 @@ function getHistoryEntryKey(entry: WorktreeNavHistoryEntry): string {
: 'legacy'
return `view:task-detail:github:${sourceScope}:${entry.workItem.repoId}:${entry.workItem.type}:${entry.workItem.number}:${entry.initialTab ?? 'conversation'}`
}
- return `view:task-detail:linear:${entry.issue.workspaceId ?? 'selected'}:${entry.issue.id}`
+ const sourceScope =
+ entry.sourceContext?.provider === 'linear'
+ ? getTaskSourceCacheScope(entry.sourceContext)
+ : 'legacy'
+ return `view:task-detail:linear:${sourceScope}:${entry.issue.workspaceId ?? 'selected'}:${entry.issue.id}`
}
function isLiveEntry(entry: WorktreeNavHistoryEntry, state: AppState): boolean {