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 {