Preserve Jira task source context

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-06-12 20:58:45 -07:00
co-authored by Orca
parent 44f61b1aee
commit f91d83dfcd
10 changed files with 214 additions and 12 deletions
@@ -218,6 +218,7 @@ Done means: reviewers and future agents can understand the vision, implementatio
- [x] 2026-06-12: Polished sidebar host headers from card-like rows into quieter sidebar grouping rows while keeping explicit SSH/status/count text; verified with focused sidebar tests, web typecheck, and live Electron screenshot `notes/artifacts/host-context-sidebar-calm-host-headers.png`.
- [x] 2026-06-12: Added a narrow `.gitignore` rule for `notes/artifacts/` so verification screenshots stay local while the HTML report can reference them; verified with `git check-ignore`.
- [x] 2026-06-12: Added `taskSourceContext` and `workspaceRunContext` to retryable pending worktree creation requests, seeded them from the Composer's selected task source and project-host setup, and verified the pending store preserves both identities with focused worktree tests plus web typecheck.
- [x] 2026-06-12: Preserved Jira task detail source context in Tasks open payloads, back/forward history entries, and replay state so a reopened Jira issue does not fall back to the ambient site/host; added a same-issue/different-source-host history regression and verified focused UI/history/feature-interaction tests, oxlint, and full `pnpm run typecheck`.
- [x] 2026-06-12: Threaded GitHub Tasks row-level mutations through `TaskSourceContext` for issue state, assignees, reviewer requests/removals, PR merge/auto-merge, and new issue creation/follow-up reads; verified with focused GitHub/TaskPage tests, focused oxlint, `pnpm run typecheck`, and `git diff --check`.
- [x] 2026-06-12: Added provider account-scope diagnostics to GitHub/GitLab CLI integration cards so code-host credential checks show whether they belong to the local desktop client or selected remote server, matching the existing Linear/Jira ownership copy; verified with focused integration-card scope tests and oxlint.
- [x] 2026-06-12: Extended task-detail navigation state to GitLab and fixed Linear replay so saved `TaskSourceContext` is restored through back/forward and direct detail reopens; GitLab detail history keys now include source scope so identical MRs from different hosts stay distinct. Verified with focused UI/history/static-boundary tests and oxlint.
+41 -5
View File
@@ -3579,7 +3579,9 @@ export default function TaskPage(): React.JSX.Element {
openGitLabWorkItem: undefined,
openGitLabSourceContext: undefined,
openLinearIssue: undefined,
openLinearSourceContext: undefined
openLinearSourceContext: undefined,
openJiraIssue: undefined,
openJiraSourceContext: undefined
}
}))
}, [clearSelectedLinearIssue, setDialogWorkItem])
@@ -3600,12 +3602,46 @@ export default function TaskPage(): React.JSX.Element {
const selectedJiraIssue = selectedJiraIssueKey
? (cachedSelectedJiraIssue ?? selectedJiraIssueFallback)
: null
const jiraDetailSourceContext = useMemo(() => {
if (
selectedJiraIssue &&
pageData.openJiraSourceContext?.provider === 'jira' &&
pageData.openJiraIssue?.key === selectedJiraIssue.key &&
pageData.openJiraIssue.siteId === selectedJiraIssue.siteId
) {
return pageData.openJiraSourceContext
}
return jiraTaskSourceContext
}, [
jiraTaskSourceContext,
pageData.openJiraIssue,
pageData.openJiraSourceContext,
selectedJiraIssue
])
const setSelectedJiraIssue = useCallback((issue: JiraIssue | null) => {
setSelectedJiraIssueKey(issue?.key ?? null)
setSelectedJiraIssueFallback(issue)
}, [])
useEffect(() => {
setSelectedJiraIssue(pageData.openJiraIssue ?? null)
}, [pageData.openJiraIssue, setSelectedJiraIssue])
const openJiraDetailPage = useCallback(
(issue: JiraIssue) => {
openTaskPage(
{
taskSource: 'jira',
openJiraIssue: issue,
openJiraSourceContext: jiraTaskSourceContext
},
{ recordTasksInteraction: false }
)
},
[jiraTaskSourceContext, openTaskPage]
)
// Linear tab state
const [linearMode, setLinearMode] = useState<LinearMode>('issues')
const [linearIssues, setLinearIssues] = useState<LinearIssue[]>([])
@@ -9070,14 +9106,14 @@ export default function TaskPage(): React.JSX.Element {
tabIndex={0}
aria-current={selected ? 'true' : undefined}
data-current={selected ? 'true' : undefined}
onClick={() => setSelectedJiraIssue(issue)}
onClick={() => openJiraDetailPage(issue)}
onKeyDown={(e) => {
if (e.target !== e.currentTarget) {
return
}
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
setSelectedJiraIssue(issue)
openJiraDetailPage(issue)
}
}}
className={cn(
@@ -9238,8 +9274,8 @@ export default function TaskPage(): React.JSX.Element {
<JiraIssueWorkspace
issue={selectedJiraIssue}
onUse={handleUseJiraItem}
onClose={() => setSelectedJiraIssue(null)}
sourceContext={jiraTaskSourceContext}
onClose={closeTaskDetailPage}
sourceContext={jiraDetailSourceContext}
/>
</div>
)
+32 -4
View File
@@ -587,7 +587,9 @@ setWorktreeNavViewActivator((entry) => {
openGitLabWorkItem: undefined,
openGitLabSourceContext: undefined,
openLinearIssue: undefined,
openLinearSourceContext: undefined
openLinearSourceContext: undefined,
openJiraIssue: undefined,
openJiraSourceContext: undefined
}
}))
return
@@ -605,7 +607,9 @@ setWorktreeNavViewActivator((entry) => {
openGitLabWorkItem: undefined,
openGitLabSourceContext: undefined,
openLinearIssue: undefined,
openLinearSourceContext: undefined
openLinearSourceContext: undefined,
openJiraIssue: undefined,
openJiraSourceContext: undefined
}
}))
return
@@ -624,7 +628,29 @@ setWorktreeNavViewActivator((entry) => {
openGitLabWorkItem: entry.workItem,
openGitLabSourceContext: entry.sourceContext,
openLinearIssue: undefined,
openLinearSourceContext: undefined
openLinearSourceContext: undefined,
openJiraIssue: undefined,
openJiraSourceContext: undefined
}
}))
return
}
if (entry.source === 'jira') {
useAppStore.setState((state) => ({
activeView: 'tasks',
githubTaskDrawerWorkItem: null,
taskPageData: {
...state.taskPageData,
taskSource: 'jira',
openGitHubWorkItem: undefined,
openGitHubSourceContext: undefined,
openGitHubInitialTab: undefined,
openGitLabWorkItem: undefined,
openGitLabSourceContext: undefined,
openLinearIssue: undefined,
openLinearSourceContext: undefined,
openJiraIssue: entry.issue,
openJiraSourceContext: entry.sourceContext
}
}))
return
@@ -641,7 +667,9 @@ setWorktreeNavViewActivator((entry) => {
openGitLabWorkItem: undefined,
openGitLabSourceContext: undefined,
openLinearIssue: entry.issue,
openLinearSourceContext: entry.sourceContext
openLinearSourceContext: entry.sourceContext,
openJiraIssue: undefined,
openJiraSourceContext: undefined
}
}))
})
+56
View File
@@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { getDefaultUIState } from '../../../../shared/constants'
import type {
GitHubWorkItem,
JiraIssue,
LinearIssue,
PersistedUIState,
TerminalTab,
@@ -158,6 +159,24 @@ function makeGitLabWorkItem(overrides: Partial<GitLabWorkItem> = {}): GitLabWork
}
}
function makeJiraIssue(overrides: Partial<JiraIssue> = {}): JiraIssue {
return {
id: 'ORC-1',
key: 'ORC-1',
title: 'Fix task source context',
url: 'https://example.atlassian.net/browse/ORC-1',
siteId: 'site-1',
siteName: 'Example Jira',
project: { id: '10000', key: 'ORC', name: 'Orca', siteId: 'site-1' },
issueType: { id: '10001', name: 'Bug' },
status: { id: '1', name: 'Todo', categoryKey: 'new', categoryName: 'To Do' },
labels: [],
createdAt: '2026-05-30T00:00:00.000Z',
updatedAt: '2026-05-30T00:00:00.000Z',
...overrides
}
}
function makePersistedUI(overrides: Partial<PersistedUIState> = {}): PersistedUIState {
return {
...getDefaultUIState(),
@@ -1627,13 +1646,16 @@ describe('createUISlice page navigation history', () => {
store.setState({ recordFeatureInteraction } as Partial<AppState>)
const workItem = makeGitHubWorkItem()
const linearIssue = makeLinearIssue()
const jiraIssue = makeJiraIssue()
store.getState().openTaskPage({ taskSource: 'github', openGitHubWorkItem: workItem })
store.getState().openTaskPage({ taskSource: 'linear', openLinearIssue: linearIssue })
store.getState().openTaskPage({ taskSource: 'jira', openJiraIssue: jiraIssue })
expect(recordFeatureInteraction).toHaveBeenCalledWith('tasks')
expect(recordFeatureInteraction).toHaveBeenCalledWith('github-tasks')
expect(recordFeatureInteraction).toHaveBeenCalledWith('linear-tasks')
expect(recordFeatureInteraction).toHaveBeenCalledWith('jira-tasks')
})
it('preserves GitHub task detail source context in navigation history', () => {
@@ -1716,12 +1738,39 @@ describe('createUISlice page navigation history', () => {
})
})
it('preserves Jira task detail source context in navigation history', () => {
const store = createUIStore()
const issue = makeJiraIssue()
const sourceContext: TaskSourceContext = {
kind: 'task-source',
provider: 'jira',
projectId: 'project-1',
hostId: 'runtime:remote-server',
providerIdentity: { provider: 'jira', siteId: 'site-1' },
accountLabel: 'Example Jira'
}
store.getState().openTaskPage({
taskSource: 'jira',
openJiraIssue: issue,
openJiraSourceContext: sourceContext
})
expect(store.getState().worktreeNavHistory.at(-1)).toEqual({
kind: 'task-detail',
source: 'jira',
issue,
sourceContext
})
})
it('can suppress the Tasks surface interaction for in-page provider navigation', () => {
const store = createUIStore()
const recordFeatureInteraction = vi.fn()
store.setState({ recordFeatureInteraction } as Partial<AppState>)
const workItem = makeGitHubWorkItem()
const linearIssue = makeLinearIssue()
const jiraIssue = makeJiraIssue()
store
.getState()
@@ -1735,10 +1784,17 @@ describe('createUISlice page navigation history', () => {
{ taskSource: 'linear', openLinearIssue: linearIssue },
{ recordTasksInteraction: false }
)
store
.getState()
.openTaskPage(
{ taskSource: 'jira', openJiraIssue: jiraIssue },
{ recordTasksInteraction: false }
)
expect(recordFeatureInteraction).not.toHaveBeenCalledWith('tasks')
expect(recordFeatureInteraction).toHaveBeenCalledWith('github-tasks')
expect(recordFeatureInteraction).toHaveBeenCalledWith('linear-tasks')
expect(recordFeatureInteraction).toHaveBeenCalledWith('jira-tasks')
})
it('skips the whole Tasks detail stack on close', () => {
+14 -1
View File
@@ -10,6 +10,7 @@ import type {
ChangelogData,
CustomPet,
GitHubWorkItem,
JiraIssue,
LinearIssue,
PersistedTrustedOrcaHooks,
PersistedUIState,
@@ -610,6 +611,8 @@ export type UISlice = {
openGitLabSourceContext?: TaskSourceContext | null
openLinearIssue?: LinearIssue
openLinearSourceContext?: TaskSourceContext | null
openJiraIssue?: JiraIssue
openJiraSourceContext?: TaskSourceContext | null
}
taskResumeState: TaskResumeState | undefined
setTaskResumeState: (updates: Partial<TaskResumeState>) => void
@@ -1097,6 +1100,9 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (set, get)
if (data.openLinearIssue) {
get().recordFeatureInteraction?.('linear-tasks')
}
if (data.openJiraIssue) {
get().recordFeatureInteraction?.('jira-tasks')
}
// Why: record a Tasks visit in the shared back/forward history so the
// titlebar Back/Forward buttons can return to Tasks. All task-source
// variants (github/linear presets) collapse to a single 'tasks' entry;
@@ -1125,7 +1131,14 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (set, get)
issue: data.openLinearIssue,
sourceContext: data.openLinearSourceContext
} as const)
: null
: data.openJiraIssue
? ({
kind: 'task-detail',
source: 'jira',
issue: data.openJiraIssue,
sourceContext: data.openJiraSourceContext
} as const)
: null
const currentEntry = get().worktreeNavHistory[get().worktreeNavHistoryIndex]
const currentIsTaskStack =
currentEntry === 'tasks' ||
@@ -1,7 +1,7 @@
import { createStore, type StoreApi } from 'zustand/vanilla'
import { afterEach, describe, expect, it } from 'vitest'
import type { AppState } from '../types'
import type { GitHubWorkItem, Worktree } from '../../../../shared/types'
import type { GitHubWorkItem, JiraIssue, Worktree } from '../../../../shared/types'
import type { GitLabWorkItem } from '../../../../shared/gitlab-types'
import type { TaskSourceContext } from '../../../../shared/task-source-context'
import {
@@ -79,6 +79,24 @@ function makeGitLabWorkItem(overrides: Partial<GitLabWorkItem> = {}): GitLabWork
}
}
function makeJiraIssue(overrides: Partial<JiraIssue> = {}): JiraIssue {
return {
id: 'ORC-1',
key: 'ORC-1',
title: 'Fix task source context',
url: 'https://example.atlassian.net/browse/ORC-1',
siteId: 'site-1',
siteName: 'Example Jira',
project: { id: '10000', key: 'ORC', name: 'Orca', siteId: 'site-1' },
issueType: { id: '10001', name: 'Bug' },
status: { id: '1', name: 'Todo', categoryKey: 'new', categoryName: 'To Do' },
labels: [],
createdAt: '2026-05-30T00:00:00.000Z',
updatedAt: '2026-05-30T00:00:00.000Z',
...overrides
}
}
describe('worktree-nav-history slice: view entries', () => {
afterEach(() => {
setWorktreeNavActivator(null)
@@ -259,4 +277,36 @@ describe('worktree-nav-history slice: view entries', () => {
expect(store.getState().worktreeNavHistory).toHaveLength(2)
expect(store.getState().worktreeNavHistoryIndex).toBe(1)
})
it('keeps same Jira issue details separate when the source host differs', () => {
const store = createHistoryStore(['a'])
const issue = makeJiraIssue()
const localSource: TaskSourceContext = {
kind: 'task-source',
provider: 'jira',
projectId: 'project-1',
hostId: 'local',
providerIdentity: { provider: 'jira', siteId: 'site-1' }
}
const remoteSource: TaskSourceContext = {
...localSource,
hostId: 'runtime:remote-server'
}
store.getState().recordViewVisit({
kind: 'task-detail',
source: 'jira',
issue,
sourceContext: localSource
})
store.getState().recordViewVisit({
kind: 'task-detail',
source: 'jira',
issue,
sourceContext: remoteSource
})
expect(store.getState().worktreeNavHistory).toHaveLength(2)
expect(store.getState().worktreeNavHistoryIndex).toBe(1)
})
})
@@ -1,7 +1,7 @@
import type { StateCreator } from 'zustand'
import type { AppState } from '../types'
import { findWorktreeById } from './worktree-helpers'
import type { GitHubWorkItem, LinearIssue } from '../../../../shared/types'
import type { GitHubWorkItem, JiraIssue, LinearIssue } from '../../../../shared/types'
import type { GitLabWorkItem } from '../../../../shared/gitlab-types'
import {
getTaskSourceCacheScope,
@@ -40,6 +40,12 @@ export type WorktreeNavHistoryTaskDetailEntry =
workItem: GitLabWorkItem
sourceContext?: TaskSourceContext | null
}
| {
kind: 'task-detail'
source: 'jira'
issue: JiraIssue
sourceContext?: TaskSourceContext | null
}
export type WorktreeNavHistoryViewEntry =
| WorktreeNavHistorySimpleViewEntry
| WorktreeNavHistoryTaskDetailEntry
@@ -112,6 +118,13 @@ function getHistoryEntryKey(entry: WorktreeNavHistoryEntry): string {
: 'legacy'
return `view:task-detail:gitlab:${sourceScope}:${entry.workItem.repoId}:${entry.workItem.type}:${entry.workItem.number}`
}
if (entry.source === 'jira') {
const sourceScope =
entry.sourceContext?.provider === 'jira'
? getTaskSourceCacheScope(entry.sourceContext)
: 'legacy'
return `view:task-detail:jira:${sourceScope}:${entry.issue.siteId ?? 'selected'}:${entry.issue.key}`
}
const sourceScope =
entry.sourceContext?.provider === 'linear'
? getTaskSourceCacheScope(entry.sourceContext)
@@ -14,6 +14,7 @@ export type FeatureInteractionId =
| 'github-tasks'
| 'gitlab-tasks'
| 'linear-tasks'
| 'jira-tasks'
| 'automations'
| 'automation-created'
| 'automation-run'
@@ -80,6 +81,7 @@ export const FEATURE_INTERACTIONS = [
{ id: 'github-tasks', interaction: 'GitHub task item workflow used' },
{ id: 'gitlab-tasks', interaction: 'GitLab task item workflow used' },
{ id: 'linear-tasks', interaction: 'Linear task item workflow used' },
{ id: 'jira-tasks', interaction: 'Jira task item workflow used' },
{ id: 'automations', interaction: 'Automations page opened' },
{ id: 'automation-created', interaction: 'automation created' },
{ id: 'automation-run', interaction: 'automation run queued' },
@@ -35,6 +35,7 @@ export const FEATURE_INTERACTION_CATEGORY_BY_ID = {
'github-tasks': 'task_management',
'gitlab-tasks': 'task_management',
'linear-tasks': 'task_management',
'jira-tasks': 'task_management',
automations: 'automation',
'automation-created': 'automation',
'automation-run': 'automation',
+2
View File
@@ -46,6 +46,7 @@ describe('feature interactions', () => {
'github-tasks',
'gitlab-tasks',
'linear-tasks',
'jira-tasks',
'automations',
'automation-created',
'automation-run',
@@ -171,6 +172,7 @@ describe('feature interactions', () => {
)
expect(FEATURE_INTERACTION_CATEGORY_BY_ID.tasks).toBe('task_management')
expect(FEATURE_INTERACTION_CATEGORY_BY_ID['github-tasks']).toBe('task_management')
expect(FEATURE_INTERACTION_CATEGORY_BY_ID['jira-tasks']).toBe('task_management')
expect(FEATURE_INTERACTION_CATEGORY_BY_ID['markdown-file-created']).toBe('notes')
expect(FEATURE_INTERACTION_CATEGORY_BY_ID['agent-browser-setup']).toBe('setup')
expect(FEATURE_INTERACTION_CATEGORY_BY_ID['terminal-tabs']).toBe('terminal')