mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 16:03:26 +00:00
Persist GitLab task detail source context
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user