mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 08:03:22 +00:00
Preserve Linear task source context in history
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user