diff --git a/notes/host-context-implementation-checklist.md b/notes/host-context-implementation-checklist.md index a6762ec6aa5..9ebdf991eb1 100644 --- a/notes/host-context-implementation-checklist.md +++ b/notes/host-context-implementation-checklist.md @@ -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. diff --git a/src/renderer/src/components/TaskPage.tsx b/src/renderer/src/components/TaskPage.tsx index 91d968a9356..a869159581b 100644 --- a/src/renderer/src/components/TaskPage.tsx +++ b/src/renderer/src/components/TaskPage.tsx @@ -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('issues') const [linearIssues, setLinearIssues] = useState([]) @@ -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 { setSelectedJiraIssue(null)} - sourceContext={jiraTaskSourceContext} + onClose={closeTaskDetailPage} + sourceContext={jiraDetailSourceContext} /> ) diff --git a/src/renderer/src/lib/worktree-activation.ts b/src/renderer/src/lib/worktree-activation.ts index 5dae2a53fef..b4cee8005a8 100644 --- a/src/renderer/src/lib/worktree-activation.ts +++ b/src/renderer/src/lib/worktree-activation.ts @@ -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 } })) }) diff --git a/src/renderer/src/store/slices/ui.test.ts b/src/renderer/src/store/slices/ui.test.ts index f54e0e10c58..57f05c45484 100644 --- a/src/renderer/src/store/slices/ui.test.ts +++ b/src/renderer/src/store/slices/ui.test.ts @@ -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 = {}): GitLabWork } } +function makeJiraIssue(overrides: Partial = {}): 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 { return { ...getDefaultUIState(), @@ -1627,13 +1646,16 @@ describe('createUISlice page navigation history', () => { store.setState({ recordFeatureInteraction } as Partial) 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) 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', () => { diff --git a/src/renderer/src/store/slices/ui.ts b/src/renderer/src/store/slices/ui.ts index d62426e4c58..de7aab3cdea 100644 --- a/src/renderer/src/store/slices/ui.ts +++ b/src/renderer/src/store/slices/ui.ts @@ -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) => void @@ -1097,6 +1100,9 @@ export const createUISlice: StateCreator = (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 = (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' || diff --git a/src/renderer/src/store/slices/worktree-nav-history-view-entries.test.ts b/src/renderer/src/store/slices/worktree-nav-history-view-entries.test.ts index 1f35d25ffa9..70d4475ba3a 100644 --- a/src/renderer/src/store/slices/worktree-nav-history-view-entries.test.ts +++ b/src/renderer/src/store/slices/worktree-nav-history-view-entries.test.ts @@ -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 = {}): GitLabWork } } +function makeJiraIssue(overrides: Partial = {}): 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) + }) }) diff --git a/src/renderer/src/store/slices/worktree-nav-history.ts b/src/renderer/src/store/slices/worktree-nav-history.ts index ac1b4512e34..38bd57a9870 100644 --- a/src/renderer/src/store/slices/worktree-nav-history.ts +++ b/src/renderer/src/store/slices/worktree-nav-history.ts @@ -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) diff --git a/src/shared/feature-interaction-catalog.ts b/src/shared/feature-interaction-catalog.ts index 917a9295b65..132c8af149a 100644 --- a/src/shared/feature-interaction-catalog.ts +++ b/src/shared/feature-interaction-catalog.ts @@ -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' }, diff --git a/src/shared/feature-interaction-categories.ts b/src/shared/feature-interaction-categories.ts index 72b05ceca7e..062b8821432 100644 --- a/src/shared/feature-interaction-categories.ts +++ b/src/shared/feature-interaction-categories.ts @@ -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', diff --git a/src/shared/feature-interactions.test.ts b/src/shared/feature-interactions.test.ts index 3778f979925..f9c55aee557 100644 --- a/src/shared/feature-interactions.test.ts +++ b/src/shared/feature-interactions.test.ts @@ -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')