Fix PR check full-details worktree routing (#5529)

* Fix view full details button to use correct worktree and sticky header

- Pin a sticky bar with check name and "View full logs" button inside expanded check details so the affordance stays visible while scrolling through annotations and job output
- Accept an explicit `worktreeId` prop so folder-workspace PR checks use the child worktree ID rather than the (non-existent) active worktree
- Show "View full logs" when log tail is available, "View full details" otherwise

Co-authored-by: Orca <help@stably.ai>

* fix: address review findings

* fix: address CI failures

---------

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinjing 2026-06-16 15:31:10 -07:00 committed by GitHub
parent d625079c4d
commit ca05dbf652
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1292 additions and 69 deletions

View File

@ -1,10 +1,13 @@
import React from 'react'
import { ExternalLink, LoaderCircle, RefreshCw } from 'lucide-react'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import CommentMarkdown from '@/components/sidebar/CommentMarkdown'
import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/types'
import { CheckJobLogTail } from '@/components/right-sidebar/check-job-log-tail'
import { SourceControlFixSplitButton } from '@/components/right-sidebar/source-control-fix-split-button'
import { translate } from '@/i18n/i18n'
import { useCheckRunDetailsFixWithAI } from './check-run-details-fix-with-ai'
function formatCheckTimestamp(value: string | null | undefined): string | null {
if (!value) {
@ -52,6 +55,7 @@ export function CheckRunDetailsPanel({
loading,
error,
openUrl,
worktreeId,
onRefresh
}: {
check: PRCheckDetail
@ -59,8 +63,28 @@ export function CheckRunDetailsPanel({
loading: boolean
error: string | null
openUrl: string | null | undefined
worktreeId: string | null
onRefresh?: () => void
}): React.JSX.Element {
const {
canFixWithAI,
disabledReason,
isFixing,
fixPrompt,
repoId,
connectionId,
launchPlatform,
savedAgentId,
savedCommandInputTemplate,
savedAgentArgs,
saveLaunchActionDefault,
openSourceControlAiSettings,
fixWithAI
} = useCheckRunDetailsFixWithAI({
worktreeId,
check,
details
})
const startedAt = formatCheckTimestamp(details?.startedAt)
const completedAt = formatCheckTimestamp(details?.completedAt)
const detailsStatusCheck: PRCheckDetail = {
@ -85,19 +109,86 @@ export function CheckRunDetailsPanel({
<h1 className="min-w-0 flex-1 truncate text-base font-medium text-foreground">
{check.name}
</h1>
{onRefresh && (
<Button
type="button"
variant="outline"
size="sm"
className="shrink-0"
disabled={loading}
onClick={onRefresh}
>
<RefreshCw className={`size-3.5${loading ? ' animate-spin' : ''}`} />
{translate('auto.components.editor.CheckRunDetailsPanel.b7f5e2c91a', 'Refresh')}
</Button>
)}
<div className="flex shrink-0 items-center gap-2">
{canFixWithAI && (
<SourceControlFixSplitButton
label={translate(
'auto.components.editor.CheckRunDetailsPanel.834cb3f23d',
'Fix with AI'
)}
actionId="fixChecks"
dialogTitle={translate(
'auto.components.editor.CheckRunDetailsPanel.834cb3f23d',
'Fix with AI'
)}
dialogDescription={translate(
'auto.components.editor.CheckRunDetailsPanel.c8f1a2d4e7',
'Choose the agent and edit the full command input before launch.'
)}
launchSource="task_page"
contextUnavailableLabel={translate(
'auto.components.editor.CheckRunDetailsPanel.b3e7f9a1c2',
'Check fix context unavailable'
)}
primaryTitle={translate(
'auto.components.editor.CheckRunDetailsPanel.d5a8c2f1b9',
'Start the default AI agent to fix this check'
)}
primaryAriaLabel={translate(
'auto.components.editor.CheckRunDetailsPanel.834cb3f23d',
'Fix with AI'
)}
chevronTitle={translate(
'auto.components.editor.CheckRunDetailsPanel.e2b4d7c8a1',
'Choose an agent for this check'
)}
chevronAriaLabel={translate(
'auto.components.editor.CheckRunDetailsPanel.f1c9e3a6d4',
'Choose agent to fix check'
)}
worktreeId={worktreeId}
groupId={worktreeId}
connectionId={connectionId}
repoId={repoId}
launchPlatform={launchPlatform}
prompt={fixPrompt}
isLaunching={loading || isFixing}
disabledReason={disabledReason}
variant="default"
size="sm"
iconClassName="size-3.5"
primaryClassName="rounded-r-none font-medium"
chevronClassName="rounded-l-none border-l border-primary-foreground/20 px-2"
savedAgentId={savedAgentId}
savedCommandInputTemplate={savedCommandInputTemplate}
savedAgentArgs={savedAgentArgs}
onSaveAgentDefault={saveLaunchActionDefault}
onOpenSettings={openSourceControlAiSettings}
onFixWithDefaultAgent={fixWithAI}
onPromptDelivered={() =>
toast.success(
translate(
'auto.components.editor.check.run.details.fix.with.ai.2ef90c9819',
'Started an AI agent for this check.'
)
)
}
/>
)}
{onRefresh && (
<Button
type="button"
variant="outline"
size="sm"
className="shrink-0"
disabled={loading}
onClick={onRefresh}
>
<RefreshCw className={`size-3.5${loading ? ' animate-spin' : ''}`} />
{translate('auto.components.editor.CheckRunDetailsPanel.b7f5e2c91a', 'Refresh')}
</Button>
)}
</div>
</div>
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
<span>

View File

@ -637,6 +637,7 @@ export function EditorContent({
loading={checkRunDetails.loading}
error={checkRunDetails.error}
openUrl={openUrl}
worktreeId={activeFile.worktreeId}
onRefresh={() => {
void reloadOpenCheckRunDetailsTab(activeFile.id)
}}

View File

@ -0,0 +1,153 @@
import {
buildFixBrokenChecksPrompt,
getBrokenChecks,
getCheckDetailsPromptKey
} from '@/components/pr-checks-fix-prompt'
import { gitHubPRToChecksPanelReview } from '@/components/right-sidebar/checks-panel-review'
import { getWorktreeGitIdentityDisplay } from '@/lib/worktree-git-identity-display'
import { useAppStore } from '@/store'
import { getGitHubPRCacheKey } from '@/store/slices/github-cache-key'
import { getHostedReviewCacheKey } from '@/store/slices/hosted-review-cache-identity'
import { findWorktreeById } from '@/store/slices/worktree-helpers'
import type { HostedReviewInfo } from '../../../../shared/hosted-review'
import type { PRCheckDetail, PRCheckRunDetails, Repo } from '../../../../shared/types'
import { translate } from '@/i18n/i18n'
export function resolveCheckRunDetailsFixCheck(
check: PRCheckDetail,
details: PRCheckRunDetails | null
): PRCheckDetail {
if (!details) {
return check
}
return {
...check,
status: (details.status as PRCheckDetail['status'] | undefined) ?? check.status,
conclusion: (details.conclusion as PRCheckDetail['conclusion'] | undefined) ?? check.conclusion
}
}
export function isCheckRunDetailsFixCandidate(
check: PRCheckDetail,
details: PRCheckRunDetails | null = null
): boolean {
return getBrokenChecks([resolveCheckRunDetailsFixCheck(check, details)]).length > 0
}
export function resolveHostedReviewForCheckRunDetailsFix(
worktreeId: string
): HostedReviewInfo | null {
const store = useAppStore.getState()
const worktree = findWorktreeById(store.worktreesByRepo, worktreeId)
if (!worktree) {
return null
}
const repo = store.repos.find((candidate) => candidate.id === worktree.repoId) ?? null
if (!repo) {
return null
}
const identity = getWorktreeGitIdentityDisplay(worktree)
const branch = identity?.kind === 'branch' ? identity.branchName : null
if (!branch) {
return null
}
const settings = store.settings
const prCacheKey = getGitHubPRCacheKey(
repo.path,
repo.id,
branch,
settings,
repo.connectionId,
repo.executionHostId
)
const hostedReviewCacheKey = getHostedReviewCacheKey(
repo.path,
branch,
settings,
repo.id,
repo.connectionId,
repo.executionHostId
)
const pr = prCacheKey ? (store.prCache[prCacheKey]?.data ?? null) : null
const hostedReview = hostedReviewCacheKey
? (store.hostedReviewCache[hostedReviewCacheKey]?.data ?? null)
: null
const gitLabHostedReview = hostedReview?.provider === 'gitlab' ? hostedReview : null
const linkedGitLabMR = worktree.linkedGitLabMR ?? null
if (gitLabHostedReview) {
return gitLabHostedReview
}
if (linkedGitLabMR !== null) {
return null
}
return pr ? gitHubPRToChecksPanelReview(pr) : null
}
export function buildCheckRunDetailsFixBasePrompt(args: {
worktreeId: string
check: PRCheckDetail
details: PRCheckRunDetails | null
}): string | null {
const review = resolveHostedReviewForCheckRunDetailsFix(args.worktreeId)
if (!review) {
return null
}
const resolvedCheck = resolveCheckRunDetailsFixCheck(args.check, args.details)
if (!isCheckRunDetailsFixCandidate(resolvedCheck)) {
return null
}
const checkRunDetailsByCheckKey = args.details
? { [getCheckDetailsPromptKey(resolvedCheck, 0)]: args.details }
: undefined
return buildFixBrokenChecksPrompt({
reviewKind: review.provider === 'gitlab' ? 'MR' : 'PR',
reviewNumber: review.number,
reviewTitle: review.title,
reviewUrl: review.url,
checks: [resolvedCheck],
checkRunDetailsByCheckKey
})
}
export function getCheckRunDetailsFixDisabledReason(worktreeId: string | null): string | undefined {
if (!worktreeId) {
return translate(
'auto.components.editor.check.run.details.fix.with.ai.1a8c4e2b90',
'Select a workspace before launching an AI action.'
)
}
const store = useAppStore.getState()
const worktree = findWorktreeById(store.worktreesByRepo, worktreeId)
if (!worktree) {
return translate(
'auto.components.editor.check.run.details.fix.with.ai.1a8c4e2b90',
'Select a workspace before launching an AI action.'
)
}
const repo = store.repos.find((candidate) => candidate.id === worktree.repoId) ?? null
if (!repo) {
return translate(
'auto.components.editor.check.run.details.fix.with.ai.4f2d9a8c17',
'Select a repository before launching an AI action.'
)
}
if (!resolveHostedReviewForCheckRunDetailsFix(worktreeId)) {
return translate(
'auto.components.editor.check.run.details.fix.with.ai.7c3e1b5d42',
'Open a PR or MR before launching an AI fix.'
)
}
return undefined
}
export function resolveCheckRunDetailsFixRepo(worktreeId: string | null): Repo | null {
if (!worktreeId) {
return null
}
const store = useAppStore.getState()
const worktree = findWorktreeById(store.worktreesByRepo, worktreeId)
if (!worktree) {
return null
}
return store.repos.find((candidate) => candidate.id === worktree.repoId) ?? null
}

View File

@ -0,0 +1,203 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type {
PRCheckDetail,
PRCheckRunDetails,
PRInfo,
Repo,
Worktree
} from '../../../../shared/types'
import {
getCheckRunDetailsFixDisabledReason,
isCheckRunDetailsFixCandidate,
resolveCheckRunDetailsFixCheck,
resolveHostedReviewForCheckRunDetailsFix,
startCheckRunDetailsFixWithAI
} from './check-run-details-fix-with-ai'
const startFixChecksAgent = vi.fn()
const fixtures = vi.hoisted(() => {
const repo: Repo = {
id: 'repo-1',
path: '/tmp/repo',
displayName: 'repo',
badgeColor: '#2563eb',
addedAt: 1,
connectionId: null,
executionHostId: null
}
const worktree: Worktree = {
id: 'repo-1::/tmp/repo/feature',
repoId: 'repo-1',
path: '/tmp/repo/feature',
head: 'abc123',
branch: 'feature',
isBare: false,
isMainWorktree: false,
displayName: 'feature',
comment: '',
linkedIssue: null,
linkedPR: 42,
linkedLinearIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0
}
const pr: PRInfo = {
number: 42,
title: 'Fix CI',
state: 'open',
url: 'https://github.com/acme/widgets/pull/42',
checksStatus: 'failure',
updatedAt: '2026-06-16T00:00:00Z',
mergeable: 'MERGEABLE'
}
const failingCheck: PRCheckDetail = {
name: 'verify',
status: 'completed',
conclusion: 'failure',
url: null,
checkRunId: 42
}
const checkDetails: PRCheckRunDetails = {
name: 'verify',
status: 'completed',
conclusion: 'failure',
url: null,
detailsUrl: null,
startedAt: null,
completedAt: null,
title: null,
summary: null,
text: null,
annotations: [],
jobs: [
{
id: 1,
name: 'test',
status: 'completed',
conclusion: 'failure',
startedAt: null,
completedAt: null,
url: null,
steps: [],
logTail: 'assertion failed'
}
]
}
const prCacheKey = 'repo-1::feature'
return { repo, worktree, pr, failingCheck, checkDetails, prCacheKey }
})
const storeState = vi.hoisted(() => ({
worktreesByRepo: { 'repo-1': [fixtures.worktree] } as Record<string, Worktree[]>,
repos: [fixtures.repo] as Repo[],
settings: {},
prCache: {
[fixtures.prCacheKey]: { data: fixtures.pr, fetchedAt: 1 }
} as Record<string, { data: PRInfo; fetchedAt: number }>,
hostedReviewCache: {} as Record<string, { data: unknown }>
}))
vi.mock('@/lib/worktree-git-identity-display', () => ({
getWorktreeGitIdentityDisplay: () => ({ kind: 'branch', branchName: 'feature' })
}))
vi.mock('@/store', () => ({
useAppStore: Object.assign(
(selector: (state: typeof storeState) => unknown) => selector(storeState),
{
getState: () => storeState
}
)
}))
vi.mock('@/lib/fix-checks-agent-launch', () => ({
startFixChecksAgent: (...args: unknown[]) => startFixChecksAgent(...args)
}))
vi.mock('sonner', () => ({
toast: {
message: vi.fn(),
success: vi.fn()
}
}))
beforeEach(() => {
startFixChecksAgent.mockReset()
startFixChecksAgent.mockResolvedValue(true)
storeState.worktreesByRepo = { 'repo-1': [fixtures.worktree] }
storeState.repos = [fixtures.repo]
storeState.prCache = {
[fixtures.prCacheKey]: { data: fixtures.pr, fetchedAt: 1 }
}
storeState.hostedReviewCache = {}
})
describe('check-run-details-fix-with-ai', () => {
it('detects failing checks as fix candidates', () => {
expect(isCheckRunDetailsFixCandidate(fixtures.failingCheck)).toBe(true)
expect(
isCheckRunDetailsFixCandidate({
...fixtures.failingCheck,
conclusion: 'success'
})
).toBe(false)
})
it('prefers loaded details conclusion over the list-level check', () => {
const listFailure = fixtures.failingCheck
const passingDetails: PRCheckRunDetails = {
...fixtures.checkDetails,
conclusion: 'success',
status: 'completed'
}
expect(isCheckRunDetailsFixCandidate(listFailure, passingDetails)).toBe(false)
expect(
resolveCheckRunDetailsFixCheck(
{ ...fixtures.failingCheck, conclusion: 'success' },
fixtures.checkDetails
).conclusion
).toBe('failure')
expect(
isCheckRunDetailsFixCandidate(
{ ...fixtures.failingCheck, conclusion: 'success' },
fixtures.checkDetails
)
).toBe(true)
})
it('resolves hosted review metadata from the worktree PR cache', () => {
expect(resolveHostedReviewForCheckRunDetailsFix(fixtures.worktree.id)).toMatchObject({
number: 42,
title: 'Fix CI',
url: 'https://github.com/acme/widgets/pull/42'
})
})
it('requires a hosted review before launching an AI fix', () => {
storeState.prCache = {}
expect(getCheckRunDetailsFixDisabledReason(fixtures.worktree.id)).toContain('PR or MR')
})
it('starts a single-check AI fix prompt for the owning worktree', async () => {
await startCheckRunDetailsFixWithAI({
worktreeId: fixtures.worktree.id,
check: fixtures.failingCheck,
details: fixtures.checkDetails
})
expect(startFixChecksAgent).toHaveBeenCalledWith(
expect.objectContaining({
repoId: 'repo-1',
worktreeId: fixtures.worktree.id,
groupId: fixtures.worktree.id,
launchSource: 'task_page',
basePrompt: expect.stringContaining('"name": "verify"')
})
)
expect(startFixChecksAgent.mock.calls[0]?.[0]?.basePrompt).toContain('assertion failed')
})
})

View File

@ -0,0 +1,232 @@
import { toast } from 'sonner'
import { useCallback, useMemo, useState } from 'react'
import {
buildCheckRunDetailsFixBasePrompt,
getCheckRunDetailsFixDisabledReason,
isCheckRunDetailsFixCandidate,
resolveCheckRunDetailsFixCheck,
resolveHostedReviewForCheckRunDetailsFix,
resolveCheckRunDetailsFixRepo
} from './check-run-details-fix-context'
import { openSourceControlAiSettingsTarget } from '@/components/right-sidebar/source-control-ai-settings-navigation'
import { getConnectionId } from '@/lib/connection-context'
import { startFixChecksAgent } from '@/lib/fix-checks-agent-launch'
import { readSourceControlLaunchRecipeAgentId } from '@/lib/source-control-launch-agent-selection'
import { resolveSourceControlLaunchPlatform } from '@/lib/source-control-launch-platform'
import { useAppStore } from '@/store'
import { findWorktreeById } from '@/store/slices/worktree-helpers'
import { resolveSourceControlActionRecipe } from '../../../../shared/source-control-ai'
import {
saveSourceControlActionRecipe,
type SourceControlAiWriteTarget
} from '../../../../shared/source-control-ai-recipe-save'
import type {
SourceControlActionRecipe,
SourceControlLaunchActionId
} from '../../../../shared/source-control-ai-actions'
import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/types'
import { translate } from '@/i18n/i18n'
export {
buildCheckRunDetailsFixBasePrompt,
getCheckRunDetailsFixDisabledReason,
isCheckRunDetailsFixCandidate,
resolveCheckRunDetailsFixCheck,
resolveHostedReviewForCheckRunDetailsFix
} from './check-run-details-fix-context'
export async function startCheckRunDetailsFixWithAI(args: {
worktreeId: string
check: PRCheckDetail
details: PRCheckRunDetails | null
}): Promise<boolean> {
const disabledReason = getCheckRunDetailsFixDisabledReason(args.worktreeId)
if (disabledReason) {
toast.message(disabledReason)
return false
}
const resolvedCheck = resolveCheckRunDetailsFixCheck(args.check, args.details)
if (!isCheckRunDetailsFixCandidate(resolvedCheck)) {
toast.message(
translate(
'auto.components.editor.check.run.details.fix.with.ai.9b2f6d4a81',
'This check is not failing.'
)
)
return false
}
const review = resolveHostedReviewForCheckRunDetailsFix(args.worktreeId)
if (!review) {
toast.message(
translate(
'auto.components.editor.check.run.details.fix.with.ai.7c3e1b5d42',
'Open a PR or MR before launching an AI fix.'
)
)
return false
}
const repoId = resolveCheckRunDetailsFixRepo(args.worktreeId)?.id
if (!repoId) {
return false
}
const basePrompt =
buildCheckRunDetailsFixBasePrompt({
worktreeId: args.worktreeId,
check: args.check,
details: args.details
}) ?? ''
if (!basePrompt) {
return false
}
const started = await startFixChecksAgent({
repoId,
basePrompt,
worktreeId: args.worktreeId,
groupId: args.worktreeId,
launchSource: 'task_page'
})
if (started) {
toast.success(
translate(
'auto.components.editor.check.run.details.fix.with.ai.2ef90c9819',
'Started an AI agent for this check.'
)
)
}
return started
}
export function useCheckRunDetailsFixWithAI(args: {
worktreeId: string | null
check: PRCheckDetail
details: PRCheckRunDetails | null
}): {
canFixWithAI: boolean
disabledReason: string | undefined
isFixing: boolean
fixPrompt: string | null
repoId: string | null
connectionId: string | null | undefined
launchPlatform: NodeJS.Platform | undefined
savedAgentId: ReturnType<typeof readSourceControlLaunchRecipeAgentId>
savedCommandInputTemplate: string | null
savedAgentArgs: string | null
saveLaunchActionDefault: (
target: SourceControlAiWriteTarget,
actionId: SourceControlLaunchActionId,
recipe: SourceControlActionRecipe
) => Promise<void>
openSourceControlAiSettings: () => void
fixWithAI: () => Promise<boolean>
} {
const [isFixing, setIsFixing] = useState(false)
const settings = useAppStore((state) => state.settings)
const updateSettings = useAppStore((state) => state.updateSettings)
const updateRepo = useAppStore((state) => state.updateRepo)
const openSettingsTarget = useAppStore((state) => state.openSettingsTarget)
const openSettingsPage = useAppStore((state) => state.openSettingsPage)
const repo = useMemo(() => resolveCheckRunDetailsFixRepo(args.worktreeId), [args.worktreeId])
const worktree = useMemo(() => {
if (!args.worktreeId) {
return null
}
return findWorktreeById(useAppStore.getState().worktreesByRepo, args.worktreeId)
}, [args.worktreeId])
const canFixWithAI = isCheckRunDetailsFixCandidate(args.check, args.details)
const disabledReason = getCheckRunDetailsFixDisabledReason(args.worktreeId)
const fixPrompt = useMemo(() => {
if (!args.worktreeId || !canFixWithAI) {
return null
}
return buildCheckRunDetailsFixBasePrompt({
worktreeId: args.worktreeId,
check: args.check,
details: args.details
})
}, [args.check, args.details, args.worktreeId, canFixWithAI])
const connectionId = args.worktreeId
? (getConnectionId(args.worktreeId) ?? repo?.connectionId ?? null)
: null
const launchPlatform = resolveSourceControlLaunchPlatform({
connectionId,
worktreePath: worktree?.path ?? null
})
const fixChecksRecipe = useMemo(
() =>
resolveSourceControlActionRecipe({
settings,
repo,
actionId: 'fixChecks'
}),
[repo, settings]
)
const saveLaunchActionDefault = useCallback(
async (
target: SourceControlAiWriteTarget,
actionId: SourceControlLaunchActionId,
recipe: SourceControlActionRecipe
): Promise<void> => {
const state = useAppStore.getState()
const latestSettings = state.settings
if (!latestSettings) {
throw new Error('Settings are not loaded.')
}
const latestRepo =
target.type === 'repo'
? (state.repos.find((candidate) => candidate.id === target.repoId) ?? null)
: null
const result = saveSourceControlActionRecipe({
target,
settings: latestSettings,
repo: latestRepo,
actionId,
recipe
})
if ('sourceControlAi' in result) {
await updateSettings({ sourceControlAi: result.sourceControlAi })
return
}
await updateRepo(result.target.repoId, result.update)
},
[updateRepo, updateSettings]
)
const openSourceControlAiSettings = useCallback((): void => {
openSourceControlAiSettingsTarget({
activeRepo: repo,
openSettingsTarget,
openSettingsPage
})
}, [openSettingsPage, openSettingsTarget, repo])
const fixWithAI = useCallback(async (): Promise<boolean> => {
if (!args.worktreeId || isFixing || disabledReason) {
return false
}
setIsFixing(true)
try {
return await startCheckRunDetailsFixWithAI({
worktreeId: args.worktreeId,
check: args.check,
details: args.details
})
} finally {
setIsFixing(false)
}
}, [args.check, args.details, args.worktreeId, disabledReason, isFixing])
return {
canFixWithAI,
disabledReason,
isFixing,
fixPrompt,
repoId: repo?.id ?? null,
connectionId,
launchPlatform,
savedAgentId: readSourceControlLaunchRecipeAgentId(fixChecksRecipe),
savedCommandInputTemplate: fixChecksRecipe.commandInputTemplate ?? null,
savedAgentArgs: fixChecksRecipe.agentArgs ?? null,
saveLaunchActionDefault,
openSourceControlAiSettings,
fixWithAI
}
}

View File

@ -114,6 +114,8 @@ export function FolderWorkspacePrChecksRow({
checksLoading={row.isRefreshing}
checkDetailsContextKey={row.refreshIdentity}
onLoadCheckDetails={onLoadCheckDetails}
worktreeId={row.worktree.id}
detailsStickySurface="card"
/>
</div>
) : null}

View File

@ -0,0 +1,213 @@
// @vitest-environment happy-dom
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { TooltipProvider } from '@/components/ui/tooltip'
import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/types'
import { ChecksList } from './checks-panel-content'
const openCheckRunDetails = vi.fn()
const patchOpenCheckRunDetails = vi.fn()
const activeWorktreeState = vi.hoisted(() => ({
current: null as { id: string } | null
}))
vi.mock('@/store', () => ({
useAppStore: (selector: (state: Record<string, unknown>) => unknown) =>
selector({
openCheckRunDetails,
patchOpenCheckRunDetails
})
}))
vi.mock('@/store/selectors', () => ({
useActiveWorktree: () => activeWorktreeState.current
}))
let container: HTMLDivElement
let root: Root
const failingCheck: PRCheckDetail = {
name: 'verify',
status: 'completed',
conclusion: 'failure',
url: null,
checkRunId: 42,
workflowRunId: 7
}
const checkDetails: PRCheckRunDetails = {
name: 'verify',
status: 'completed',
conclusion: 'failure',
url: null,
detailsUrl: null,
startedAt: '2026-06-16T12:00:00Z',
completedAt: '2026-06-16T12:05:00Z',
title: 'Verify failed',
summary: null,
text: null,
annotations: [],
jobs: [
{
id: 1,
name: 'test',
status: 'completed',
conclusion: 'failure',
startedAt: null,
completedAt: null,
url: null,
steps: [],
logTail: 'Error: assertion failed'
}
]
}
beforeEach(() => {
activeWorktreeState.current = null
openCheckRunDetails.mockReset()
patchOpenCheckRunDetails.mockReset()
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
})
afterEach(() => {
act(() => {
root.unmount()
})
container.remove()
})
function renderChecksList(
props: Partial<{
worktreeId: string
detailsStickySurface: 'sidebar' | 'card'
onLoadCheckDetails: (check: PRCheckDetail) => Promise<PRCheckRunDetails | null>
}> = {}
): void {
act(() => {
root.render(
<TooltipProvider>
<ChecksList
checks={[failingCheck]}
checksLoading={false}
checkDetailsContextKey="repo:42"
worktreeId={props.worktreeId}
detailsStickySurface={props.detailsStickySurface ?? 'sidebar'}
onLoadCheckDetails={
props.onLoadCheckDetails ??
(async () => {
await Promise.resolve()
return checkDetails
})
}
/>
</TooltipProvider>
)
})
}
describe('ChecksList expanded check details', () => {
it('pins a contextual full-details action with the correct sticky surface', async () => {
renderChecksList({ worktreeId: 'wt-child-1', detailsStickySurface: 'card' })
await act(async () => {
await Promise.resolve()
})
const stickyBar = container.querySelector('.sticky.top-0')
expect(stickyBar).not.toBeNull()
expect(stickyBar?.className).toContain('bg-card/95')
expect(stickyBar?.textContent).toContain('verify')
expect(stickyBar?.textContent).toContain('View full logs')
expect(container.innerHTML).toContain('lucide-panel-right')
expect(container.innerHTML).toContain('data-variant="outline"')
})
it('uses the sidebar sticky surface by default in the hosted checks panel', async () => {
activeWorktreeState.current = { id: 'wt-active-1' }
renderChecksList()
await act(async () => {
await Promise.resolve()
})
const stickyBar = container.querySelector('.sticky.top-0')
expect(stickyBar?.className).toContain('bg-sidebar/95')
})
it('falls back to the active worktree when no worktree override is provided', async () => {
activeWorktreeState.current = { id: 'wt-active-1' }
renderChecksList()
await act(async () => {
await Promise.resolve()
})
const button = [...container.querySelectorAll('button')].find((candidate) =>
candidate.textContent?.includes('View full logs')
)
expect(button).toBeDefined()
act(() => {
button!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
})
expect(openCheckRunDetails).toHaveBeenCalledWith(
'wt-active-1',
'repo:42',
failingCheck,
expect.objectContaining({
details: checkDetails,
loading: false,
error: null
})
)
})
it('opens full details on the provided worktree instead of the active worktree', async () => {
activeWorktreeState.current = { id: 'wt-active-1' }
renderChecksList({ worktreeId: 'wt-attached-9' })
await act(async () => {
await Promise.resolve()
})
const button = [...container.querySelectorAll('button')].find((candidate) =>
candidate.textContent?.includes('View full logs')
)
expect(button).toBeDefined()
act(() => {
button!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
})
expect(openCheckRunDetails).toHaveBeenCalledWith(
'wt-attached-9',
'repo:42',
failingCheck,
expect.objectContaining({
details: checkDetails,
loading: false,
error: null
})
)
})
it('keeps the generic label while inline details are still loading', async () => {
renderChecksList({
worktreeId: 'wt-child-1',
onLoadCheckDetails: () => new Promise(() => {})
})
await act(async () => {
await Promise.resolve()
})
const stickyBar = container.querySelector('.sticky.top-0')
expect(stickyBar?.textContent).toContain('View full details')
expect(stickyBar?.textContent).not.toContain('View full logs')
})
})

View File

@ -15,6 +15,7 @@ import {
Plus,
ChevronDown,
ChevronRight,
PanelRight,
SendHorizontal,
Sparkles,
RefreshCw,
@ -469,16 +470,46 @@ export function getFailedChecksForDetails(checks: PRCheckDetail[]): PRCheckDetai
return checks.filter(isFailedCheck)
}
type CheckDetailsStickySurface = 'sidebar' | 'card'
function getCheckDetailsStickySurfaceClass(surface: CheckDetailsStickySurface): string {
return surface === 'card' ? 'bg-card/95' : 'bg-sidebar/95'
}
function ViewFullCheckDetailsButton({
onClick,
label
}: {
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void
label: string
}): React.JSX.Element {
return (
<Button
type="button"
variant="outline"
size="xs"
className="h-6 min-w-[7.25rem] shrink-0 gap-1 px-1.5 text-[11px] text-muted-foreground hover:text-foreground"
onClick={onClick}
>
<PanelRight className="size-3" />
{label}
</Button>
)
}
function CheckRunDetails({
check,
state,
checkDetailsContextKey
checkDetailsContextKey,
worktreeId,
detailsStickySurface = 'sidebar'
}: {
check: PRCheckDetail
state: CheckDetailsLoadState | undefined
checkDetailsContextKey: string
worktreeId: string | null
detailsStickySurface?: CheckDetailsStickySurface
}): React.JSX.Element {
const activeWorktree = useActiveWorktree()
const openCheckRunDetails = useAppStore((s) => s.openCheckRunDetails)
const details = state?.details
const startedAt = formatCheckTimestamp(details?.startedAt)
@ -499,19 +530,49 @@ function CheckRunDetails({
const hasJobs = jobs.length > 0
const hasLogTail = jobs.some((job) => Boolean(job.logTail))
// Why: wait until inline details finish loading before switching to the logs label
// so the sticky button does not resize mid-fetch.
const fullDetailsLabel =
!state?.loading && hasLogTail
? translate('auto.components.right.sidebar.checks.panel.content.b8c4e2a1f7', 'View full logs')
: translate(
'auto.components.right.sidebar.checks.panel.content.e4e3af15ee',
'View full details'
)
const openFullDetailsTab = (): void => {
if (!activeWorktree) {
if (!worktreeId) {
return
}
openCheckRunDetails(activeWorktree.id, checkDetailsContextKey, check, {
openCheckRunDetails(worktreeId, checkDetailsContextKey, check, {
details: state?.details ?? null,
loading: state?.loading ?? false,
error: state?.error ?? null
})
}
const handleOpenFullDetails = (event: React.MouseEvent<HTMLButtonElement>): void => {
event.stopPropagation()
openFullDetailsTab()
}
return (
<div className="mb-1 ml-[26px] mr-3 min-w-0 border-l border-border pl-3">
{worktreeId && (
// Why: inline check details can be long; pinning the affordance keeps it
// visible while scrolling through annotations and job output.
<div
className={cn(
'sticky top-0 z-10 -ml-3 flex min-w-0 items-center gap-2 border-b border-border/60 py-1 pl-3 backdrop-blur-sm',
getCheckDetailsStickySurfaceClass(detailsStickySurface)
)}
>
<span className="min-w-0 flex-1 truncate text-[11px] font-medium text-foreground">
{check.name}
</span>
<ViewFullCheckDetailsButton label={fullDetailsLabel} onClick={handleOpenFullDetails} />
</div>
)}
{state?.loading ? (
<div className="flex min-w-0 flex-col gap-2 py-1.5">
<div className="flex items-center gap-2 text-[12px] text-muted-foreground">
@ -521,25 +582,6 @@ function CheckRunDetails({
'Loading check details…'
)}
</div>
{activeWorktree && (
<div className="flex justify-start">
<Button
type="button"
variant="ghost"
size="xs"
className="h-6 gap-1 px-1.5 text-[11px] text-muted-foreground hover:text-foreground"
onClick={(event) => {
event.stopPropagation()
openFullDetailsTab()
}}
>
{translate(
'auto.components.right.sidebar.checks.panel.content.e4e3af15ee',
'View full details'
)}
</Button>
</div>
)}
</div>
) : (
<div className="flex min-w-0 flex-col gap-2.5 py-1.5">
@ -744,26 +786,6 @@ function CheckRunDetails({
)}
</div>
)}
<div className="flex justify-start pt-1">
{activeWorktree && (
<Button
type="button"
variant="ghost"
size="xs"
className="h-6 gap-1 px-1.5 text-[11px] text-muted-foreground hover:text-foreground"
onClick={(event) => {
event.stopPropagation()
openFullDetailsTab()
}}
>
{translate(
'auto.components.right.sidebar.checks.panel.content.e4e3af15ee',
'View full details'
)}
</Button>
)}
</div>
</div>
)}
</div>
@ -777,14 +799,20 @@ export function ChecksList({
checks,
checksLoading,
checkDetailsContextKey,
onLoadCheckDetails
onLoadCheckDetails,
worktreeId: worktreeIdOverride,
detailsStickySurface = 'sidebar'
}: {
checks: PRCheckDetail[]
checksLoading: boolean
checkDetailsContextKey: string
onLoadCheckDetails?: (check: PRCheckDetail) => Promise<PRCheckRunDetails | null>
/** Why: folder-workspace PR checks render rows for attached worktrees, not the active one. */
worktreeId?: string
detailsStickySurface?: CheckDetailsStickySurface
}): React.JSX.Element {
const activeWorktree = useActiveWorktree()
const resolvedWorktreeId = worktreeIdOverride ?? activeWorktree?.id ?? null
const patchOpenCheckRunDetails = useAppStore((s) => s.patchOpenCheckRunDetails)
const [checksExpanded, setChecksExpanded] = useState(true)
const [expandedCheckKeys, setExpandedCheckKeys] = useState<Set<string>>(new Set())
@ -952,7 +980,7 @@ export function ChecksList({
}, [checksExpanded, detailsByCheckKey, expandedCheckKeys, requestCheckDetails, rows])
useEffect(() => {
if (!activeWorktree) {
if (!resolvedWorktreeId) {
return
}
for (const row of rows) {
@ -960,13 +988,19 @@ export function ChecksList({
if (!detailsState) {
continue
}
patchOpenCheckRunDetails(activeWorktree.id, checkDetailsContextKey, row.check, {
patchOpenCheckRunDetails(resolvedWorktreeId, checkDetailsContextKey, row.check, {
details: detailsState.details ?? null,
loading: detailsState.loading ?? false,
error: detailsState.error ?? null
})
}
}, [activeWorktree, checkDetailsContextKey, detailsByCheckKey, patchOpenCheckRunDetails, rows])
}, [
checkDetailsContextKey,
detailsByCheckKey,
patchOpenCheckRunDetails,
resolvedWorktreeId,
rows
])
const toggleCheckExpanded = useCallback(
(row: { check: PRCheckDetail; key: string }) => {
@ -1124,6 +1158,8 @@ export function ChecksList({
check={check}
state={detailsByCheckKey[row.key]}
checkDetailsContextKey={checkDetailsContextKey}
worktreeId={resolvedWorktreeId}
detailsStickySurface={detailsStickySurface}
/>
)}
</div>

View File

@ -0,0 +1,171 @@
import React, { useState } from 'react'
import { ChevronDown, RefreshCw, SlidersHorizontal, Sparkle } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { cn } from '@/lib/utils'
import type {
SourceControlActionRecipe,
SourceControlLaunchActionId
} from '../../../../shared/source-control-ai-actions'
import type { TuiAgent } from '../../../../shared/types'
import type { LaunchSource } from '../../../../shared/telemetry-events'
import type { SourceControlAiWriteTarget } from '../../../../shared/source-control-ai-recipe-save'
import { translate } from '@/i18n/i18n'
import { SourceControlAgentActionDialog } from './SourceControlAgentActionDialog'
export type SourceControlFixSplitButtonProps = {
label: string
actionId: SourceControlLaunchActionId
dialogTitle: string
dialogDescription: string
launchSource: LaunchSource
contextUnavailableLabel: string
primaryTitle: string
primaryAriaLabel: string
chevronTitle: string
chevronAriaLabel: string
worktreeId: string | null
groupId: string | null
connectionId?: string | null
repoId?: string | null
launchPlatform?: NodeJS.Platform
prompt: string | null
isLaunching: boolean
disabledReason?: string
variant: React.ComponentProps<typeof Button>['variant']
size: React.ComponentProps<typeof Button>['size']
iconClassName: string
primaryClassName?: string
chevronClassName?: string
savedAgentId?: TuiAgent | null
savedCommandInputTemplate?: string | null
savedAgentArgs?: string | null
onSaveAgentDefault?: (
target: SourceControlAiWriteTarget,
actionId: SourceControlLaunchActionId,
recipe: SourceControlActionRecipe
) => void | Promise<void>
onOpenSettings?: () => void
onFixWithDefaultAgent: (promptOverride?: string) => Promise<boolean> | boolean
onPromptDelivered?: () => void
}
export function SourceControlFixSplitButton({
label,
actionId,
dialogTitle,
dialogDescription,
launchSource,
contextUnavailableLabel,
primaryTitle,
primaryAriaLabel,
chevronTitle,
chevronAriaLabel,
worktreeId,
groupId,
connectionId,
repoId,
launchPlatform,
prompt,
isLaunching,
disabledReason,
variant,
size,
iconClassName,
primaryClassName,
chevronClassName,
savedAgentId,
savedCommandInputTemplate,
savedAgentArgs,
onSaveAgentDefault,
onOpenSettings,
onFixWithDefaultAgent,
onPromptDelivered
}: SourceControlFixSplitButtonProps): React.JSX.Element {
const [composerOpen, setComposerOpen] = useState(false)
const canLaunch = Boolean(worktreeId && groupId && prompt && !disabledReason)
const dividerClass = variant === 'default' ? 'border-primary-foreground/20' : 'border-border'
return (
<>
<DropdownMenu>
<div className="flex shrink-0 items-stretch">
<Button
type="button"
variant={variant}
size={size}
className={cn('rounded-r-none', primaryClassName)}
disabled={isLaunching || !canLaunch}
title={disabledReason ?? primaryTitle}
aria-label={primaryAriaLabel}
onClick={() => void onFixWithDefaultAgent()}
>
{isLaunching ? (
<RefreshCw className={cn(iconClassName, 'animate-spin')} />
) : (
<Sparkle className={iconClassName} />
)}
{label}
</Button>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant={variant}
size={size}
className={cn('rounded-l-none border-l', dividerClass, chevronClassName)}
disabled={isLaunching || !canLaunch}
title={chevronTitle}
aria-label={chevronAriaLabel}
>
<ChevronDown className={iconClassName} />
</Button>
</DropdownMenuTrigger>
</div>
<DropdownMenuContent align="end" className="min-w-[210px] p-1">
{worktreeId && groupId && prompt && !disabledReason ? (
<DropdownMenuItem
onSelect={() => setComposerOpen(true)}
className="gap-2 rounded-[7px] px-2 py-1.5 text-[12px] leading-5 font-medium"
>
<SlidersHorizontal className="size-4 text-muted-foreground" />
{translate(
'auto.components.right.sidebar.SourceControl.f0a2dc9e46',
'Customize launch...'
)}
</DropdownMenuItem>
) : (
<DropdownMenuItem disabled>{contextUnavailableLabel}</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
{worktreeId && groupId && prompt ? (
<SourceControlAgentActionDialog
open={composerOpen}
onOpenChange={setComposerOpen}
actionId={actionId}
title={dialogTitle}
description={dialogDescription}
baseCommandInput={prompt}
worktreeId={worktreeId}
groupId={groupId}
connectionId={connectionId}
repoId={repoId}
promptDelivery="submit-after-ready"
launchPlatform={launchPlatform}
launchSource={launchSource}
savedAgentId={savedAgentId}
savedCommandInputTemplate={savedCommandInputTemplate}
savedAgentArgs={savedAgentArgs}
onSaveAgentDefault={onSaveAgentDefault}
onOpenSettings={onOpenSettings}
onLaunched={onPromptDelivered}
/>
) : null}
</>
)
}

View File

@ -61,6 +61,7 @@ vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenuContent: () => null,
DropdownMenuItem: ({ children }: { children?: ReactNode }) => <>{children}</>,
DropdownMenuSeparator: () => null,
DropdownMenuShortcut: ({ children }: { children?: ReactNode }) => <>{children}</>,
DropdownMenuTrigger: ({ children }: { children: ReactNode }) => <>{children}</>
}))

View File

@ -8471,6 +8471,7 @@
"fd46a70f1a": "Started",
"a54ae21c6f": "Status:",
"e4e3af15ee": "View full details",
"b8c4e2a1f7": "View full logs",
"2524d1fb83": "Log tail available in full details.",
"a2fb3f4408": "Showing first 100 jobs",
"df137989b3": "Showing first 20 annotations",
@ -10438,7 +10439,30 @@
"49731703ea": "Jobs",
"ee07b33924": "unknown",
"07eccfa397": "No details are available for this check.",
"a916648574": "Open details"
"a916648574": "Open details",
"834cb3f23d": "Fix with AI",
"c8f1a2d4e7": "Choose the agent and edit the full command input before launch.",
"b3e7f9a1c2": "Check fix context unavailable",
"d5a8c2f1b9": "Start the default AI agent to fix this check",
"e2b4d7c8a1": "Choose an agent for this check",
"f1c9e3a6d4": "Choose agent to fix check"
},
"check": {
"run": {
"details": {
"fix": {
"with": {
"ai": {
"1a8c4e2b90": "Select a workspace before launching an AI action.",
"4f2d9a8c17": "Select a repository before launching an AI action.",
"7c3e1b5d42": "Open a PR or MR before launching an AI fix.",
"9b2f6d4a81": "This check is not failing.",
"2ef90c9819": "Started an AI agent for this check."
}
}
}
}
}
}
},
"diff": {

View File

@ -8505,7 +8505,8 @@
"d91f2a6c39": "Enviar {{value0}} comentarios en cola",
"a6de3e5a20": "Borrar comentarios en cola",
"49ea0937e4": "Agregar comentario a la lista de resolución",
"9fecebb29d": "Agregar"
"9fecebb29d": "Agregar",
"b8c4e2a1f7": "View full logs"
},
"empty": {
"state": {
@ -10438,7 +10439,30 @@
"49731703ea": "Jobs",
"ee07b33924": "unknown",
"07eccfa397": "No details are available for this check.",
"a916648574": "Open details"
"a916648574": "Open details",
"834cb3f23d": "Fix with AI",
"c8f1a2d4e7": "Choose the agent and edit the full command input before launch.",
"b3e7f9a1c2": "Check fix context unavailable",
"d5a8c2f1b9": "Start the default AI agent to fix this check",
"e2b4d7c8a1": "Choose an agent for this check",
"f1c9e3a6d4": "Choose agent to fix check"
},
"check": {
"run": {
"details": {
"fix": {
"with": {
"ai": {
"1a8c4e2b90": "Select a workspace before launching an AI action.",
"4f2d9a8c17": "Select a repository before launching an AI action.",
"7c3e1b5d42": "Open a PR or MR before launching an AI fix.",
"9b2f6d4a81": "This check is not failing.",
"2ef90c9819": "Started an AI agent for this check."
}
}
}
}
}
}
},
"diff": {

View File

@ -8505,7 +8505,8 @@
"d91f2a6c39": "キュー内の {{value0}} 件のコメントを送信",
"a6de3e5a20": "キュー内のコメントをクリア",
"49ea0937e4": "コメントを解決リストに追加",
"9fecebb29d": "追加"
"9fecebb29d": "追加",
"b8c4e2a1f7": "View full logs"
},
"empty": {
"state": {
@ -10438,7 +10439,30 @@
"49731703ea": "Jobs",
"ee07b33924": "unknown",
"07eccfa397": "No details are available for this check.",
"a916648574": "Open details"
"a916648574": "Open details",
"834cb3f23d": "Fix with AI",
"c8f1a2d4e7": "Choose the agent and edit the full command input before launch.",
"b3e7f9a1c2": "Check fix context unavailable",
"d5a8c2f1b9": "Start the default AI agent to fix this check",
"e2b4d7c8a1": "Choose an agent for this check",
"f1c9e3a6d4": "Choose agent to fix check"
},
"check": {
"run": {
"details": {
"fix": {
"with": {
"ai": {
"1a8c4e2b90": "Select a workspace before launching an AI action.",
"4f2d9a8c17": "Select a repository before launching an AI action.",
"7c3e1b5d42": "Open a PR or MR before launching an AI fix.",
"9b2f6d4a81": "This check is not failing.",
"2ef90c9819": "Started an AI agent for this check."
}
}
}
}
}
}
},
"diff": {

View File

@ -8505,7 +8505,8 @@
"d91f2a6c39": "대기 중인 댓글 {{value0}}개 보내기",
"a6de3e5a20": "대기 중인 댓글 지우기",
"49ea0937e4": "댓글을 해결 목록에 추가",
"9fecebb29d": "추가"
"9fecebb29d": "추가",
"b8c4e2a1f7": "View full logs"
},
"empty": {
"state": {
@ -10438,7 +10439,30 @@
"49731703ea": "Jobs",
"ee07b33924": "unknown",
"07eccfa397": "No details are available for this check.",
"a916648574": "Open details"
"a916648574": "Open details",
"834cb3f23d": "Fix with AI",
"c8f1a2d4e7": "Choose the agent and edit the full command input before launch.",
"b3e7f9a1c2": "Check fix context unavailable",
"d5a8c2f1b9": "Start the default AI agent to fix this check",
"e2b4d7c8a1": "Choose an agent for this check",
"f1c9e3a6d4": "Choose agent to fix check"
},
"check": {
"run": {
"details": {
"fix": {
"with": {
"ai": {
"1a8c4e2b90": "Select a workspace before launching an AI action.",
"4f2d9a8c17": "Select a repository before launching an AI action.",
"7c3e1b5d42": "Open a PR or MR before launching an AI fix.",
"9b2f6d4a81": "This check is not failing.",
"2ef90c9819": "Started an AI agent for this check."
}
}
}
}
}
}
},
"diff": {

View File

@ -8505,7 +8505,8 @@
"d91f2a6c39": "发送 {{value0}} 条已排队评论",
"a6de3e5a20": "清除已排队评论",
"49ea0937e4": "将评论添加到解决列表",
"9fecebb29d": "添加"
"9fecebb29d": "添加",
"b8c4e2a1f7": "View full logs"
},
"empty": {
"state": {
@ -10438,7 +10439,30 @@
"49731703ea": "Jobs",
"ee07b33924": "unknown",
"07eccfa397": "No details are available for this check.",
"a916648574": "Open details"
"a916648574": "Open details",
"834cb3f23d": "Fix with AI",
"c8f1a2d4e7": "Choose the agent and edit the full command input before launch.",
"b3e7f9a1c2": "Check fix context unavailable",
"d5a8c2f1b9": "Start the default AI agent to fix this check",
"e2b4d7c8a1": "Choose an agent for this check",
"f1c9e3a6d4": "Choose agent to fix check"
},
"check": {
"run": {
"details": {
"fix": {
"with": {
"ai": {
"1a8c4e2b90": "Select a workspace before launching an AI action.",
"4f2d9a8c17": "Select a repository before launching an AI action.",
"7c3e1b5d42": "Open a PR or MR before launching an AI fix.",
"9b2f6d4a81": "This check is not failing.",
"2ef90c9819": "Started an AI agent for this check."
}
}
}
}
}
}
},
"diff": {