Preserve Linear task source context in history

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-06-12 19:43:13 -07:00
co-authored by Orca
parent 502a4bdebb
commit f9125fd949
5 changed files with 115 additions and 14 deletions
@@ -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`.
+24 -4
View File
@@ -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 ? (
<div className="mt-4 flex items-center justify-center py-14">
+58
View File
@@ -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<AppState>)
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()
+17 -7
View File
@@ -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<TaskResumeState>) => void
@@ -1108,7 +1109,8 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (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<AppState, [], [], UISlice> = (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 }
)
}
}
},
@@ -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 {