Persist GitLab task detail source context

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-06-12 20:41:58 -07:00
co-authored by Orca
parent ea127e0924
commit 88d71b9950
7 changed files with 198 additions and 14 deletions
@@ -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`.
+39 -6
View File
@@ -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"
+31 -3
View File
@@ -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
}
}))
})
+44
View File
@@ -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> = {}): LinearIssue {
} as LinearIssue
}
function makeGitLabWorkItem(overrides: Partial<GitLabWorkItem> = {}): 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> = {}): 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()
+18 -5
View File
@@ -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<AppState, [], [], UISlice> = (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<AppState, [], [], UISlice> = (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' ||
@@ -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<GitHubWorkItem> = {}): GitHubWork
}
}
function makeGitLabWorkItem(overrides: Partial<GitLabWorkItem> = {}): 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)
})
})
@@ -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)