diff --git a/notes/host-context-implementation-checklist.md b/notes/host-context-implementation-checklist.md index 6815ea0b46c..389ef0d3257 100644 --- a/notes/host-context-implementation-checklist.md +++ b/notes/host-context-implementation-checklist.md @@ -220,6 +220,7 @@ Done means: reviewers and future agents can understand the vision, implementatio - [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: 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. - [x] 2026-06-12: Updated PR #5071 with the current verified scope, evidence docs, validation commands, explicit out-of-scope cloud/onboarding items, and remaining provider/version-skew risk. - [x] 2026-06-12: Added load-time backfill for legacy automations and automation runs that predate `runContext`/`sourceContext`, deriving explicit project host and provider source contexts from the saved repo id/path; verified with focused persistence migration coverage, automation service/precheck tests, daemon/settings merge-repair tests, and full `pnpm run typecheck`. - [x] 2026-06-12: Repaired the merge-repair lint gate without disabling max-lines by extracting automation run-target/usage modules and settings subcomponents, fixed stale source-context hook dependencies, and tightened GitHub PR checks cache ownership so `noCache` bypasses cache/in-flight requests while head-specific checks are reused during refresh events; verified targeted oxlint, focused automation/settings/GitHub cache tests (10 files / 157 tests), `github-checks-cache.test.ts`, `git diff --check`, and full `pnpm run typecheck`. diff --git a/src/renderer/src/components/TaskPage.tsx b/src/renderer/src/components/TaskPage.tsx index 5531540e886..91d968a9356 100644 --- a/src/renderer/src/components/TaskPage.tsx +++ b/src/renderer/src/components/TaskPage.tsx @@ -3221,10 +3221,22 @@ export default function TaskPage(): React.JSX.Element { : null, [gitlabDialogItem, primaryRepo, selectedRepos] ) - const gitlabDialogSourceContext = useMemo( - () => getTaskPageRepoSourceContext(gitlabDialogRepo, 'gitlab'), - [gitlabDialogRepo] - ) + const gitlabDialogSourceContext = useMemo(() => { + if ( + gitlabDialogItem && + pageData.openGitLabSourceContext?.provider === 'gitlab' && + pageData.openGitLabWorkItem?.id === gitlabDialogItem.id && + pageData.openGitLabWorkItem.repoId === gitlabDialogItem.repoId + ) { + return pageData.openGitLabSourceContext + } + return getTaskPageRepoSourceContext(gitlabDialogRepo, 'gitlab') + }, [ + gitlabDialogItem, + gitlabDialogRepo, + pageData.openGitLabSourceContext, + pageData.openGitLabWorkItem + ]) const setDialogWorkItem = useCallback( (item: GitHubWorkItem | null, initialTab: ItemDialogTab = 'conversation') => { @@ -3243,6 +3255,10 @@ export default function TaskPage(): React.JSX.Element { setDialogWorkItem(pageData.openGitHubWorkItem, pageData.openGitHubInitialTab) }, [pageData.openGitHubInitialTab, pageData.openGitHubWorkItem, setDialogWorkItem]) + useEffect(() => { + setGitlabDialogItem(pageData.openGitLabWorkItem ?? null) + }, [pageData.openGitLabWorkItem]) + const openGitHubDetailPage = useCallback( (item: GitHubWorkItem, initialTab: ItemDialogTab = 'conversation') => { openTaskPage( @@ -3259,6 +3275,21 @@ export default function TaskPage(): React.JSX.Element { [openTaskPage, repoMap] ) + const openGitLabDetailPage = useCallback( + (item: GitLabWorkItem) => { + openTaskPage( + { + taskSource: 'gitlab', + preselectedRepoId: item.repoId, + openGitLabWorkItem: item, + openGitLabSourceContext: getTaskPageRepoSourceContext(repoMap.get(item.repoId), 'gitlab') + }, + { recordTasksInteraction: false } + ) + }, + [openTaskPage, repoMap] + ) + const patchTaskPageWorkItemRows = useCallback( ( itemKey: { id: string; repoId: string }, @@ -3545,6 +3576,8 @@ export default function TaskPage(): React.JSX.Element { openGitHubWorkItem: undefined, openGitHubSourceContext: undefined, openGitHubInitialTab: undefined, + openGitLabWorkItem: undefined, + openGitLabSourceContext: undefined, openLinearIssue: undefined, openLinearSourceContext: undefined } @@ -8848,13 +8881,13 @@ export default function TaskPage(): React.JSX.Element { key={item.id} onClick={() => { useAppStore.getState().recordFeatureInteraction('gitlab-tasks') - setGitlabDialogItem(item) + openGitLabDetailPage(item) }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() useAppStore.getState().recordFeatureInteraction('gitlab-tasks') - setGitlabDialogItem(item) + openGitLabDetailPage(item) } }} className="grid w-full cursor-pointer gap-3 px-3 py-2 text-left grid-cols-[80px_minmax(0,3fr)_120px_110px_50px] hover:bg-muted/50" diff --git a/src/renderer/src/lib/worktree-activation.ts b/src/renderer/src/lib/worktree-activation.ts index d1793d0edae..5dae2a53fef 100644 --- a/src/renderer/src/lib/worktree-activation.ts +++ b/src/renderer/src/lib/worktree-activation.ts @@ -584,7 +584,10 @@ setWorktreeNavViewActivator((entry) => { openGitHubWorkItem: undefined, openGitHubSourceContext: undefined, openGitHubInitialTab: undefined, - openLinearIssue: undefined + openGitLabWorkItem: undefined, + openGitLabSourceContext: undefined, + openLinearIssue: undefined, + openLinearSourceContext: undefined } })) return @@ -599,7 +602,29 @@ setWorktreeNavViewActivator((entry) => { openGitHubWorkItem: entry.workItem, openGitHubSourceContext: entry.sourceContext, openGitHubInitialTab: entry.initialTab, - openLinearIssue: undefined + openGitLabWorkItem: undefined, + openGitLabSourceContext: undefined, + openLinearIssue: undefined, + openLinearSourceContext: undefined + } + })) + return + } + if (entry.source === 'gitlab') { + useAppStore.setState((state) => ({ + activeView: 'tasks', + githubTaskDrawerWorkItem: null, + taskPageData: { + ...state.taskPageData, + taskSource: 'gitlab', + preselectedRepoId: entry.workItem.repoId, + openGitHubWorkItem: undefined, + openGitHubSourceContext: undefined, + openGitHubInitialTab: undefined, + openGitLabWorkItem: entry.workItem, + openGitLabSourceContext: entry.sourceContext, + openLinearIssue: undefined, + openLinearSourceContext: undefined } })) return @@ -613,7 +638,10 @@ setWorktreeNavViewActivator((entry) => { openGitHubWorkItem: undefined, openGitHubSourceContext: undefined, openGitHubInitialTab: undefined, - openLinearIssue: entry.issue + openGitLabWorkItem: undefined, + openGitLabSourceContext: undefined, + openLinearIssue: entry.issue, + openLinearSourceContext: entry.sourceContext } })) }) diff --git a/src/renderer/src/store/slices/ui.test.ts b/src/renderer/src/store/slices/ui.test.ts index 073570e5aa5..f54e0e10c58 100644 --- a/src/renderer/src/store/slices/ui.test.ts +++ b/src/renderer/src/store/slices/ui.test.ts @@ -10,6 +10,7 @@ import type { Worktree, WorktreeCardProperty } from '../../../../shared/types' +import type { GitLabWorkItem } from '../../../../shared/gitlab-types' import { createUISlice } from './ui' import { createWorktreeNavHistorySlice } from './worktree-nav-history' import { createSettingsSearchState } from './settings-search-state' @@ -141,6 +142,22 @@ function makeLinearIssue(overrides: Partial = {}): LinearIssue { } as LinearIssue } +function makeGitLabWorkItem(overrides: Partial = {}): GitLabWorkItem { + return { + id: 'mr-12', + type: 'mr', + number: 12, + title: 'Fix runner routing', + state: 'opened', + url: 'https://gitlab.com/acme/repo/-/merge_requests/12', + labels: [], + updatedAt: '2026-05-30T00:00:00.000Z', + author: 'gitlab-user', + repoId: 'repo-1', + ...overrides + } +} + function makePersistedUI(overrides: Partial = {}): PersistedUIState { return { ...getDefaultUIState(), @@ -1672,6 +1689,33 @@ describe('createUISlice page navigation history', () => { }) }) + it('preserves GitLab task detail source context in navigation history', () => { + const store = createUIStore() + const workItem = makeGitLabWorkItem({ repoId: 'repo-remote' }) + const sourceContext: TaskSourceContext = { + kind: 'task-source', + provider: 'gitlab', + projectId: 'project-1', + hostId: 'ssh:devbox', + projectHostSetupId: 'setup-1', + repoId: 'repo-remote', + providerIdentity: { provider: 'gitlab', projectId: '1234' } + } + + store.getState().openTaskPage({ + taskSource: 'gitlab', + openGitLabWorkItem: workItem, + openGitLabSourceContext: sourceContext + }) + + expect(store.getState().worktreeNavHistory.at(-1)).toEqual({ + kind: 'task-detail', + source: 'gitlab', + workItem, + 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 c78bd162c1d..d62426e4c58 100644 --- a/src/renderer/src/store/slices/ui.ts +++ b/src/renderer/src/store/slices/ui.ts @@ -27,6 +27,7 @@ import type { WorkspaceHostScope, VisibleWorkspaceHostIds } from '../../../../shared/types' +import type { GitLabWorkItem } from '../../../../shared/gitlab-types' import type { LaunchSource } from '../../../../shared/telemetry-events' import type { TaskSourceContext } from '../../../../shared/task-source-context' import { tuiAgentToAgentKind } from '../../../../shared/agent-kind' @@ -605,6 +606,8 @@ export type UISlice = { openGitHubWorkItem?: GitHubWorkItem openGitHubSourceContext?: TaskSourceContext | null openGitHubInitialTab?: 'conversation' | 'checks' | 'files' + openGitLabWorkItem?: GitLabWorkItem + openGitLabSourceContext?: TaskSourceContext | null openLinearIssue?: LinearIssue openLinearSourceContext?: TaskSourceContext | null } @@ -1088,6 +1091,9 @@ export const createUISlice: StateCreator = (set, get) if (data.openGitHubWorkItem) { get().recordFeatureInteraction?.('github-tasks') } + if (data.openGitLabWorkItem) { + get().recordFeatureInteraction?.('gitlab-tasks') + } if (data.openLinearIssue) { get().recordFeatureInteraction?.('linear-tasks') } @@ -1105,14 +1111,21 @@ export const createUISlice: StateCreator = (set, get) sourceContext: data.openGitHubSourceContext, initialTab: data.openGitHubInitialTab } as const) - : data.openLinearIssue + : data.openGitLabWorkItem ? ({ kind: 'task-detail', - source: 'linear', - issue: data.openLinearIssue, - sourceContext: data.openLinearSourceContext + source: 'gitlab', + workItem: data.openGitLabWorkItem, + sourceContext: data.openGitLabSourceContext } as const) - : null + : data.openLinearIssue + ? ({ + kind: 'task-detail', + source: 'linear', + issue: data.openLinearIssue, + sourceContext: data.openLinearSourceContext + } 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 9bee048f17d..1f35d25ffa9 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 @@ -2,6 +2,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 { GitLabWorkItem } from '../../../../shared/gitlab-types' import type { TaskSourceContext } from '../../../../shared/task-source-context' import { createWorktreeNavHistorySlice, @@ -62,6 +63,22 @@ function makeGitHubWorkItem(overrides: Partial = {}): GitHubWork } } +function makeGitLabWorkItem(overrides: Partial = {}): GitLabWorkItem { + return { + id: 'mr-12', + type: 'mr', + number: 12, + title: 'Fix runner routing', + state: 'opened', + url: 'https://gitlab.com/acme/repo/-/merge_requests/12', + labels: [], + updatedAt: '2026-05-20T00:00:00.000Z', + author: 'gitlab-user', + repoId: 'repo-1', + ...overrides + } +} + describe('worktree-nav-history slice: view entries', () => { afterEach(() => { setWorktreeNavActivator(null) @@ -208,4 +225,38 @@ describe('worktree-nav-history slice: view entries', () => { expect(store.getState().worktreeNavHistory).toHaveLength(2) expect(store.getState().worktreeNavHistoryIndex).toBe(1) }) + + it('keeps same GitLab item details separate when the source host differs', () => { + const store = createHistoryStore(['a']) + const workItem = makeGitLabWorkItem() + const localSource: TaskSourceContext = { + kind: 'task-source', + provider: 'gitlab', + projectId: 'project-1', + hostId: 'local', + repoId: 'repo-1', + providerIdentity: { provider: 'gitlab', projectId: '1234' } + } + const sshSource: TaskSourceContext = { + ...localSource, + hostId: 'ssh:devbox', + projectHostSetupId: 'setup-ssh' + } + + store.getState().recordViewVisit({ + kind: 'task-detail', + source: 'gitlab', + workItem, + sourceContext: localSource + }) + store.getState().recordViewVisit({ + kind: 'task-detail', + source: 'gitlab', + workItem, + sourceContext: sshSource + }) + + 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 cda30db3d66..ac1b4512e34 100644 --- a/src/renderer/src/store/slices/worktree-nav-history.ts +++ b/src/renderer/src/store/slices/worktree-nav-history.ts @@ -2,6 +2,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 { GitLabWorkItem } from '../../../../shared/gitlab-types' import { getTaskSourceCacheScope, type TaskSourceContext @@ -33,6 +34,12 @@ export type WorktreeNavHistoryTaskDetailEntry = issue: LinearIssue sourceContext?: TaskSourceContext | null } + | { + kind: 'task-detail' + source: 'gitlab' + workItem: GitLabWorkItem + sourceContext?: TaskSourceContext | null + } export type WorktreeNavHistoryViewEntry = | WorktreeNavHistorySimpleViewEntry | WorktreeNavHistoryTaskDetailEntry @@ -98,6 +105,13 @@ function getHistoryEntryKey(entry: WorktreeNavHistoryEntry): string { : 'legacy' return `view:task-detail:github:${sourceScope}:${entry.workItem.repoId}:${entry.workItem.type}:${entry.workItem.number}:${entry.initialTab ?? 'conversation'}` } + if (entry.source === 'gitlab') { + const sourceScope = + entry.sourceContext?.provider === 'gitlab' + ? getTaskSourceCacheScope(entry.sourceContext) + : 'legacy' + return `view:task-detail:gitlab:${sourceScope}:${entry.workItem.repoId}:${entry.workItem.type}:${entry.workItem.number}` + } const sourceScope = entry.sourceContext?.provider === 'linear' ? getTaskSourceCacheScope(entry.sourceContext)