mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
Fix Checks panel local branch empty state (#2768)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -42,6 +42,33 @@ import {
|
||||
shouldCommitChecksPanelAsyncResult
|
||||
} from './checks-panel-async-result-key'
|
||||
import { installWindowVisibilityTimeoutPoller } from '@/lib/window-visibility-timeout-poller'
|
||||
import {
|
||||
getChecksPanelEmptyStateCopy,
|
||||
shouldShowChecksPanelPublishBranchAction
|
||||
} from './checks-panel-empty-state'
|
||||
import { getRuntimeGitStatus, getRuntimeGitUpstreamStatus } from '@/runtime/runtime-git-client'
|
||||
import {
|
||||
buildChecksPanelGitStatusContextKey,
|
||||
readChecksPanelPublishActionGitStatus,
|
||||
readChecksPanelGitStatusSnapshot,
|
||||
shouldClearChecksPanelGitStatusSnapshot,
|
||||
shouldCoalesceChecksPanelGitStatusSnapshotRefresh,
|
||||
shouldCommitChecksPanelGitStatusSnapshot,
|
||||
shouldPollChecksPanelRuntimeSshStatus,
|
||||
type ChecksPanelGitStatusSnapshot
|
||||
} from './checks-panel-git-status-snapshot'
|
||||
import { installWindowVisibilityInterval } from '@/lib/window-visibility-interval'
|
||||
|
||||
const RUNTIME_SSH_STATUS_REFRESH_MS = 3000
|
||||
const GIT_STATUS_FAILURE_RETRY_MS = 3000
|
||||
|
||||
type HostedReviewCreationSnapshot = {
|
||||
requestKey: string
|
||||
repoId: string
|
||||
worktreeId: string | null
|
||||
branch: string
|
||||
data: HostedReviewCreationEligibility
|
||||
}
|
||||
|
||||
export default function ChecksPanel(): React.JSX.Element {
|
||||
const activeWorktree = useActiveWorktree()
|
||||
@@ -58,12 +85,13 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
const conflictOperation = useAppStore((s) =>
|
||||
activeWorktreeId ? (s.gitConflictOperationByWorktree[activeWorktreeId] ?? 'unknown') : 'unknown'
|
||||
)
|
||||
const hasUncommittedChanges = useAppStore((s) =>
|
||||
activeWorktreeId ? (s.gitStatusByWorktree[activeWorktreeId]?.length ?? 0) > 0 : false
|
||||
const gitStatusInvalidation = useAppStore((s) =>
|
||||
activeWorktreeId ? s.gitStatusByWorktree[activeWorktreeId] : undefined
|
||||
)
|
||||
const remoteStatus = useAppStore((s) =>
|
||||
const remoteStatusInvalidation = useAppStore((s) =>
|
||||
activeWorktreeId ? s.remoteStatusesByWorktree[activeWorktreeId] : undefined
|
||||
)
|
||||
const isRemoteOperationActive = useAppStore((s) => s.isRemoteOperationActive)
|
||||
const pushBranch = useAppStore((s) => s.pushBranch)
|
||||
const fetchUpstreamStatus = useAppStore((s) => s.fetchUpstreamStatus)
|
||||
const setRightSidebarOpen = useAppStore((s) => s.setRightSidebarOpen)
|
||||
@@ -90,10 +118,15 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
const [conflictDetailsRefreshing, setConflictDetailsRefreshing] = useState(false)
|
||||
const [createPrDialogOpen, setCreatePrDialogOpen] = useState(false)
|
||||
const [createPrPushFirst, setCreatePrPushFirst] = useState(false)
|
||||
const [isPublishingBranch, setIsPublishingBranch] = useState(false)
|
||||
const [isResolvingConflictsWithAI, setIsResolvingConflictsWithAI] = useState(false)
|
||||
const [isFixingChecksWithAI, setIsFixingChecksWithAI] = useState(false)
|
||||
const [hostedReviewCreation, setHostedReviewCreation] =
|
||||
useState<HostedReviewCreationEligibility | null>(null)
|
||||
const [hostedReviewCreationSnapshot, setHostedReviewCreationSnapshot] =
|
||||
useState<HostedReviewCreationSnapshot | null>(null)
|
||||
const [gitStatusSnapshot, setGitStatusSnapshot] = useState<ChecksPanelGitStatusSnapshot | null>(
|
||||
null
|
||||
)
|
||||
const [gitStatusRefreshNonce, setGitStatusRefreshNonce] = useState(0)
|
||||
const [editingTitle, setEditingTitle] = useState(false)
|
||||
const [titleDraft, setTitleDraft] = useState('')
|
||||
const [titleSaving, setTitleSaving] = useState(false)
|
||||
@@ -104,31 +137,67 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
const asyncResultKeyRef = useRef<string>('')
|
||||
const refreshRequestKeyRef = useRef<string | null>(null)
|
||||
const refreshContextKeyRef = useRef<string | null>(null)
|
||||
const gitStatusSnapshotInFlightContextRef = useRef<string | null>(null)
|
||||
const gitStatusSnapshotRerunContextRef = useRef<string | null>(null)
|
||||
const gitStatusSnapshotRetryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const branch = activeWorktree ? activeWorktree.branch.replace(/^refs\/heads\//, '') : ''
|
||||
const activeWorktreePath = activeWorktree?.path ?? null
|
||||
const activeWorktreePushTarget = activeWorktree?.pushTarget ?? null
|
||||
const runtimeEnvironmentId = settings?.activeRuntimeEnvironmentId?.trim() || null
|
||||
const repoConnectionId = repo?.connectionId?.trim() || null
|
||||
const sshConnectionStatus = useAppStore((s) =>
|
||||
repoConnectionId ? s.sshConnectionStates.get(repoConnectionId)?.status : undefined
|
||||
)
|
||||
const panelContextKey = buildChecksPanelGitStatusContextKey({
|
||||
repoId: repo?.id,
|
||||
worktreeId: activeWorktreeId,
|
||||
worktreePath: activeWorktreePath,
|
||||
branch,
|
||||
runtimeEnvironmentId,
|
||||
repoConnectionId,
|
||||
pushTarget: activeWorktreePushTarget
|
||||
})
|
||||
const panelContextKeyRef = useRef(panelContextKey)
|
||||
panelContextKeyRef.current = panelContextKey
|
||||
|
||||
// Why: the sidebar no longer uses key={activeWorktreeId} to force a full
|
||||
// remount on worktree switch (that caused an IPC storm on Windows).
|
||||
// Reset worktree-specific local state so stale UI from the previous
|
||||
// worktree doesn't leak (e.g. mid-edit title, stale loading indicators).
|
||||
// remount on worktree switch (that caused an IPC storm on Windows). Reset
|
||||
// branch-specific local state so stale UI from the previous context doesn't
|
||||
// leak (e.g. mid-edit title, stale loading indicators, PR dialog fields).
|
||||
// Done during render (not useEffect) so the reset takes effect on the same
|
||||
// paint as the worktree change — useEffect would leave one render with the
|
||||
// previous worktree's stale title/loading state visible.
|
||||
const [prevActiveWorktreeId, setPrevActiveWorktreeId] = useState(activeWorktreeId)
|
||||
if (activeWorktreeId !== prevActiveWorktreeId) {
|
||||
setPrevActiveWorktreeId(activeWorktreeId)
|
||||
// paint as the context change; useEffect would leave one stale render.
|
||||
const [prevPanelContextKey, setPrevPanelContextKey] = useState(panelContextKey)
|
||||
if (panelContextKey !== prevPanelContextKey) {
|
||||
setPrevPanelContextKey(panelContextKey)
|
||||
setEditingTitle(false)
|
||||
setTitleDraft('')
|
||||
setTitleSaving(false)
|
||||
setChecks([])
|
||||
setChecksLoading(false)
|
||||
setComments([])
|
||||
setCommentsLoading(false)
|
||||
setIsRefreshing(false)
|
||||
setEmptyRefreshing(false)
|
||||
setConflictDetailsRefreshing(false)
|
||||
setCreatePrDialogOpen(false)
|
||||
setCreatePrPushFirst(false)
|
||||
setIsPublishingBranch(false)
|
||||
setIsResolvingConflictsWithAI(false)
|
||||
setIsFixingChecksWithAI(false)
|
||||
setHostedReviewCreationSnapshot(null)
|
||||
setGitStatusSnapshot(null)
|
||||
setGitStatusRefreshNonce((value) => value + 1)
|
||||
pollIntervalRef.current = 30_000
|
||||
prevChecksRef.current = ''
|
||||
conflictSummaryRefreshKeyRef.current = null
|
||||
refreshRequestKeyRef.current = null
|
||||
if (gitStatusSnapshotRetryTimerRef.current) {
|
||||
clearTimeout(gitStatusSnapshotRetryTimerRef.current)
|
||||
gitStatusSnapshotRetryTimerRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
// Find active worktree and repo
|
||||
const branch = activeWorktree ? activeWorktree.branch.replace(/^refs\/heads\//, '') : ''
|
||||
const isFolder = repo ? isFolderRepo(repo) : false
|
||||
const prCacheKey =
|
||||
repo && branch
|
||||
@@ -184,7 +253,58 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
const linkedPR = activeWorktree?.linkedPR ?? null
|
||||
const fallbackGitHubPRNumber = linkedPR == null ? (pr?.number ?? null) : null
|
||||
const linkedGitLabMR = activeWorktree?.linkedGitLabMR ?? null
|
||||
const activeWorktreePath = activeWorktree?.path ?? null
|
||||
const hostedReviewCreationRequestKey =
|
||||
repo && branch
|
||||
? JSON.stringify({
|
||||
repoId: repo.id,
|
||||
repoPath: repo.path,
|
||||
worktreeId: activeWorktreeId ?? null,
|
||||
worktreePath: activeWorktreePath,
|
||||
runtimeEnvironmentId,
|
||||
connectionId: repoConnectionId,
|
||||
branch,
|
||||
base: repo.worktreeBaseRef ?? null,
|
||||
hasUncommittedChanges:
|
||||
gitStatusSnapshot?.contextKey === panelContextKey
|
||||
? gitStatusSnapshot.hasUncommittedChanges
|
||||
: null,
|
||||
hasUpstream:
|
||||
gitStatusSnapshot?.contextKey === panelContextKey
|
||||
? (gitStatusSnapshot.remoteStatus?.hasUpstream ?? null)
|
||||
: null,
|
||||
ahead:
|
||||
gitStatusSnapshot?.contextKey === panelContextKey
|
||||
? (gitStatusSnapshot.remoteStatus?.ahead ?? null)
|
||||
: null,
|
||||
behind:
|
||||
gitStatusSnapshot?.contextKey === panelContextKey
|
||||
? (gitStatusSnapshot.remoteStatus?.behind ?? null)
|
||||
: null,
|
||||
linkedGitHubPR: linkedPR,
|
||||
fallbackGitHubPR: fallbackGitHubPRNumber,
|
||||
linkedGitLabMR
|
||||
})
|
||||
: ''
|
||||
const gitStatusInputs = readChecksPanelGitStatusSnapshot(gitStatusSnapshot, panelContextKey)
|
||||
const gitStatusReadyForPanelContext = gitStatusInputs.hasUncommittedChanges !== undefined
|
||||
const hasUncommittedChanges = gitStatusInputs.hasUncommittedChanges
|
||||
const remoteStatus = gitStatusInputs.remoteStatus
|
||||
// Why: Create PR eligibility waits for the stricter panel snapshot, but the
|
||||
// Publish affordance can use the active worktree poller when SSH snapshot
|
||||
// refresh is delayed; publishing is still blocked for dirty fallback status.
|
||||
const publishActionGitStatusInputs = readChecksPanelPublishActionGitStatus({
|
||||
snapshot: gitStatusSnapshot,
|
||||
contextKey: panelContextKey,
|
||||
fallbackEntries: gitStatusInvalidation,
|
||||
fallbackRemoteStatus: remoteStatusInvalidation
|
||||
})
|
||||
const publishActionHasUncommittedChanges =
|
||||
publishActionGitStatusInputs.hasUncommittedChanges ?? true
|
||||
const publishActionRemoteStatus = publishActionGitStatusInputs.remoteStatus
|
||||
const hostedReviewCreation =
|
||||
hostedReviewCreationSnapshot?.requestKey === hostedReviewCreationRequestKey
|
||||
? hostedReviewCreationSnapshot.data
|
||||
: null
|
||||
const stateRequestKey =
|
||||
repo && branch
|
||||
? checksPanelAsyncResultKey(prCacheKey, branch, prNumber, pr?.prRepo, pr?.headSha)
|
||||
@@ -197,16 +317,185 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
[]
|
||||
)
|
||||
useEffect(() => {
|
||||
if (isPanelVisible && repo && !isFolder && branch) {
|
||||
if (isPanelVisible && repo && !isFolder && branch && prCacheKey) {
|
||||
if (activeWorktreeId) {
|
||||
enqueueGitHubPRRefresh(activeWorktreeId, 'swr', 30)
|
||||
}
|
||||
}
|
||||
}, [repo, isFolder, branch, activeWorktreeId, enqueueGitHubPRRefresh, isPanelVisible])
|
||||
}, [repo, isFolder, branch, prCacheKey, activeWorktreeId, enqueueGitHubPRRefresh, isPanelVisible])
|
||||
|
||||
useEffect(() => {
|
||||
if (!repo || isFolder || !branch || !isPanelVisible) {
|
||||
setHostedReviewCreation(null)
|
||||
if (
|
||||
!shouldPollChecksPanelRuntimeSshStatus({
|
||||
isPanelVisible,
|
||||
runtimeEnvironmentId,
|
||||
repoConnectionId
|
||||
})
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
let skippedInitialRun = false
|
||||
return installWindowVisibilityInterval({
|
||||
run: () => {
|
||||
if (!skippedInitialRun) {
|
||||
skippedInitialRun = true
|
||||
return
|
||||
}
|
||||
const currentContextKey = panelContextKeyRef.current
|
||||
if (
|
||||
shouldCoalesceChecksPanelGitStatusSnapshotRefresh(
|
||||
gitStatusSnapshotInFlightContextRef.current,
|
||||
currentContextKey
|
||||
)
|
||||
) {
|
||||
gitStatusSnapshotRerunContextRef.current = currentContextKey
|
||||
return
|
||||
}
|
||||
setGitStatusRefreshNonce((value) => value + 1)
|
||||
},
|
||||
intervalMs: RUNTIME_SSH_STATUS_REFRESH_MS
|
||||
})
|
||||
}, [isPanelVisible, repoConnectionId, runtimeEnvironmentId])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!repo ||
|
||||
isFolder ||
|
||||
!branch ||
|
||||
!isPanelVisible ||
|
||||
!activeWorktreeId ||
|
||||
!activeWorktreePath ||
|
||||
(!runtimeEnvironmentId && repoConnectionId && sshConnectionStatus !== 'connected')
|
||||
) {
|
||||
if (gitStatusSnapshotRetryTimerRef.current) {
|
||||
clearTimeout(gitStatusSnapshotRetryTimerRef.current)
|
||||
gitStatusSnapshotRetryTimerRef.current = null
|
||||
}
|
||||
// Why: hiding the panel or temporarily losing SSH should stop new work,
|
||||
// not erase same-context Create PR eligibility that can still be retried.
|
||||
return
|
||||
}
|
||||
let stale = false
|
||||
const requestContextKey = panelContextKey
|
||||
const connectionId = getConnectionId(activeWorktreeId) ?? undefined
|
||||
if (
|
||||
shouldCoalesceChecksPanelGitStatusSnapshotRefresh(
|
||||
gitStatusSnapshotInFlightContextRef.current,
|
||||
requestContextKey
|
||||
)
|
||||
) {
|
||||
gitStatusSnapshotRerunContextRef.current = requestContextKey
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}
|
||||
gitStatusSnapshotInFlightContextRef.current = requestContextKey
|
||||
// Why: global status maps are keyed only by worktree. Use their changes as
|
||||
// invalidation signals, then fetch a local snapshot for the active boundary.
|
||||
if (gitStatusSnapshotRetryTimerRef.current) {
|
||||
clearTimeout(gitStatusSnapshotRetryTimerRef.current)
|
||||
gitStatusSnapshotRetryTimerRef.current = null
|
||||
}
|
||||
setGitStatusSnapshot((snapshot) =>
|
||||
shouldClearChecksPanelGitStatusSnapshot(snapshot, requestContextKey) ? null : snapshot
|
||||
)
|
||||
const context = {
|
||||
settings: useAppStore.getState().settings,
|
||||
worktreeId: activeWorktreeId,
|
||||
worktreePath: activeWorktreePath,
|
||||
connectionId
|
||||
}
|
||||
void (async () => {
|
||||
const status = await getRuntimeGitStatus(context)
|
||||
let freshRemoteStatus = status.upstreamStatus
|
||||
if (activeWorktreePushTarget) {
|
||||
freshRemoteStatus = await getRuntimeGitUpstreamStatus(context, activeWorktreePushTarget)
|
||||
} else if (
|
||||
!freshRemoteStatus ||
|
||||
(freshRemoteStatus.ahead > 0 &&
|
||||
freshRemoteStatus.behind > 0 &&
|
||||
freshRemoteStatus.behindCommitsArePatchEquivalent === undefined)
|
||||
) {
|
||||
freshRemoteStatus = await getRuntimeGitUpstreamStatus(context)
|
||||
}
|
||||
return { status, remoteStatus: freshRemoteStatus }
|
||||
})()
|
||||
.then(({ status, remoteStatus }) => {
|
||||
if (
|
||||
!stale &&
|
||||
shouldCommitChecksPanelGitStatusSnapshot(panelContextKeyRef.current, requestContextKey)
|
||||
) {
|
||||
setGitStatusSnapshot({
|
||||
contextKey: requestContextKey,
|
||||
hasUncommittedChanges: status.entries.length > 0,
|
||||
remoteStatus
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('[ChecksPanel] git status refresh before eligibility failed', error)
|
||||
if (!stale) {
|
||||
// Why: transient SSH/runtime flakes should not hide an already-valid
|
||||
// Create PR state for this same branch; retry while the panel stays visible.
|
||||
setGitStatusSnapshot((snapshot) =>
|
||||
shouldClearChecksPanelGitStatusSnapshot(snapshot, requestContextKey) ? null : snapshot
|
||||
)
|
||||
gitStatusSnapshotRetryTimerRef.current = setTimeout(() => {
|
||||
gitStatusSnapshotRetryTimerRef.current = null
|
||||
if (
|
||||
shouldCommitChecksPanelGitStatusSnapshot(
|
||||
panelContextKeyRef.current,
|
||||
requestContextKey
|
||||
)
|
||||
) {
|
||||
setGitStatusRefreshNonce((value) => value + 1)
|
||||
}
|
||||
}, GIT_STATUS_FAILURE_RETRY_MS)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (gitStatusSnapshotInFlightContextRef.current === requestContextKey) {
|
||||
gitStatusSnapshotInFlightContextRef.current = null
|
||||
}
|
||||
if (gitStatusSnapshotRerunContextRef.current === requestContextKey) {
|
||||
gitStatusSnapshotRerunContextRef.current = null
|
||||
if (
|
||||
shouldCommitChecksPanelGitStatusSnapshot(panelContextKeyRef.current, requestContextKey)
|
||||
) {
|
||||
setGitStatusRefreshNonce((value) => value + 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
stale = true
|
||||
if (gitStatusSnapshotRetryTimerRef.current) {
|
||||
clearTimeout(gitStatusSnapshotRetryTimerRef.current)
|
||||
gitStatusSnapshotRetryTimerRef.current = null
|
||||
}
|
||||
}
|
||||
}, [
|
||||
activeWorktreePushTarget,
|
||||
activeWorktreeId,
|
||||
activeWorktreePath,
|
||||
branch,
|
||||
gitStatusInvalidation,
|
||||
gitStatusRefreshNonce,
|
||||
isFolder,
|
||||
isPanelVisible,
|
||||
panelContextKey,
|
||||
repo,
|
||||
repoConnectionId,
|
||||
remoteStatusInvalidation,
|
||||
runtimeEnvironmentId,
|
||||
sshConnectionStatus
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!repo || isFolder || !branch) {
|
||||
setHostedReviewCreationSnapshot(null)
|
||||
return
|
||||
}
|
||||
if (!isPanelVisible || !gitStatusReadyForPanelContext) {
|
||||
return
|
||||
}
|
||||
let stale = false
|
||||
@@ -228,22 +517,31 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
})
|
||||
.then((result) => {
|
||||
if (!stale) {
|
||||
setHostedReviewCreation(result)
|
||||
setHostedReviewCreationSnapshot({
|
||||
requestKey: hostedReviewCreationRequestKey,
|
||||
repoId: repo.id,
|
||||
worktreeId: activeWorktreeId,
|
||||
branch,
|
||||
data: result
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!stale) {
|
||||
setHostedReviewCreation(null)
|
||||
setHostedReviewCreationSnapshot(null)
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}, [
|
||||
activeWorktreeId,
|
||||
activeWorktreePath,
|
||||
branch,
|
||||
getHostedReviewCreationEligibility,
|
||||
gitStatusReadyForPanelContext,
|
||||
hasUncommittedChanges,
|
||||
hostedReviewCreationRequestKey,
|
||||
isFolder,
|
||||
isPanelVisible,
|
||||
linkedPR,
|
||||
@@ -509,6 +807,7 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
refreshRequestKeyRef.current = refreshRequestKey
|
||||
const isCurrentRequest = (): boolean => refreshRequestKeyRef.current === refreshRequestKey
|
||||
setIsRefreshing(true)
|
||||
setGitStatusRefreshNonce((value) => value + 1)
|
||||
try {
|
||||
const refreshedPR = await fetchPRForBranch(repo.path, branch, {
|
||||
force: true,
|
||||
@@ -663,9 +962,7 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
// duplicate fetches from rapid show/hide toggles. See
|
||||
// docs/refresh-on-checks-tab.md.
|
||||
const entryKey =
|
||||
isPanelVisible && repo && !isFolder && branch
|
||||
? `${activeWorktreeId ?? ''}::${repo.id}::${branch}`
|
||||
: ''
|
||||
isPanelVisible && repo && !isFolder && branch ? `${activeWorktreeId ?? ''}::${prCacheKey}` : ''
|
||||
const lastEntryKeyRef = useRef<string>('')
|
||||
useEffect(() => {
|
||||
if (!entryKey) {
|
||||
@@ -935,6 +1232,48 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
}
|
||||
}, [activeWorktree, activeWorktreeId, fetchUpstreamStatus, pushBranch])
|
||||
|
||||
const handlePublishBranch = useCallback(async (): Promise<void> => {
|
||||
if (
|
||||
!activeWorktreeId ||
|
||||
!activeWorktree?.path ||
|
||||
isPublishingBranch ||
|
||||
isRemoteOperationActive
|
||||
) {
|
||||
return
|
||||
}
|
||||
const connectionId = getConnectionId(activeWorktreeId) ?? undefined
|
||||
setIsPublishingBranch(true)
|
||||
try {
|
||||
await pushBranch(
|
||||
activeWorktreeId,
|
||||
activeWorktree.path,
|
||||
true,
|
||||
connectionId,
|
||||
activeWorktree.pushTarget
|
||||
)
|
||||
await fetchUpstreamStatus(
|
||||
activeWorktreeId,
|
||||
activeWorktree.path,
|
||||
connectionId,
|
||||
activeWorktree.pushTarget
|
||||
)
|
||||
} catch {
|
||||
// Store remote actions already surface the publish failure toast.
|
||||
} finally {
|
||||
// Why: publishing changes the upstream boundary the Checks panel uses to
|
||||
// decide between Publish, Create PR, and Push & Create PR.
|
||||
setGitStatusRefreshNonce((value) => value + 1)
|
||||
setIsPublishingBranch(false)
|
||||
}
|
||||
}, [
|
||||
activeWorktree,
|
||||
activeWorktreeId,
|
||||
fetchUpstreamStatus,
|
||||
isPublishingBranch,
|
||||
isRemoteOperationActive,
|
||||
pushBranch
|
||||
])
|
||||
|
||||
const handleBranchChangedByPullRequestGeneration = useCallback(async (): Promise<void> => {
|
||||
if (!activeWorktreeId || !activeWorktree?.path) {
|
||||
return
|
||||
@@ -1132,13 +1471,21 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
: conflictOperation === 'cherry-pick'
|
||||
? 'Cherry-pick'
|
||||
: null
|
||||
const isQueuedPRRefresh = prRefreshState?.status === 'queued'
|
||||
const isInFlightPRRefresh = prRefreshState?.status === 'in-flight'
|
||||
const isPausedPRRefresh = prRefreshState?.status === 'paused'
|
||||
const isErroredPRRefresh = prRefreshState?.status === 'error'
|
||||
|
||||
const canCreate = hostedReviewCreation?.canCreate
|
||||
const canPushCreate = hostedReviewCreation?.blockedReason === 'needs_push'
|
||||
const canPublishBranch =
|
||||
isPublishingBranch ||
|
||||
(!publishActionHasUncommittedChanges &&
|
||||
shouldShowChecksPanelPublishBranchAction({
|
||||
hostedReviewBlockedReason: hostedReviewCreation?.blockedReason,
|
||||
hasUpstream: publishActionRemoteStatus?.hasUpstream
|
||||
}))
|
||||
const emptyStateCopy = getChecksPanelEmptyStateCopy({
|
||||
operationLabel,
|
||||
prRefreshStatus: prRefreshState?.status,
|
||||
hostedReviewBlockedReason: hostedReviewCreation?.blockedReason,
|
||||
hasUpstream: publishActionRemoteStatus?.hasUpstream
|
||||
})
|
||||
return (
|
||||
<>
|
||||
{repo && (
|
||||
@@ -1158,32 +1505,19 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
/>
|
||||
)}
|
||||
<div className="px-4 py-6">
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{operationInProgress
|
||||
? `${operationLabel} in progress`
|
||||
: isErroredPRRefresh
|
||||
? 'Could not refresh pull request'
|
||||
: isQueuedPRRefresh || isInFlightPRRefresh
|
||||
? 'Checking for pull request'
|
||||
: 'No pull request found'}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
{operationInProgress
|
||||
? 'PR checks will be available after the operation completes'
|
||||
: isErroredPRRefresh
|
||||
? 'GitHub status could not be refreshed. Existing cached data was preserved.'
|
||||
: isQueuedPRRefresh
|
||||
? 'Waiting to refresh GitHub status for this branch'
|
||||
: isInFlightPRRefresh
|
||||
? 'Refreshing GitHub status for this branch'
|
||||
: isPausedPRRefresh
|
||||
? 'GitHub refresh is paused by the current rate-limit budget'
|
||||
: canPushCreate
|
||||
? 'Push your branch before creating a pull request.'
|
||||
: 'Create a pull request to start checks and review.'}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-foreground">{emptyStateCopy.title}</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">{emptyStateCopy.description}</div>
|
||||
{!operationInProgress && (
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{canPublishBranch && (
|
||||
<Button
|
||||
size="xs"
|
||||
disabled={isPublishingBranch || isRemoteOperationActive}
|
||||
onClick={handlePublishBranch}
|
||||
>
|
||||
{isPublishingBranch ? 'Publishing…' : 'Publish Branch'}
|
||||
</Button>
|
||||
)}
|
||||
{(canCreate || canPushCreate) && (
|
||||
<Button
|
||||
size="xs"
|
||||
@@ -1198,7 +1532,7 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline"
|
||||
disabled={emptyRefreshing}
|
||||
disabled={emptyRefreshing || isPublishingBranch || isRemoteOperationActive}
|
||||
onClick={() => {
|
||||
if (!activeWorktreeId) {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
getChecksPanelEmptyStateCopy,
|
||||
shouldShowChecksPanelPublishBranchAction
|
||||
} from './checks-panel-empty-state'
|
||||
|
||||
describe('getChecksPanelEmptyStateCopy', () => {
|
||||
it('shows a local-only branch message instead of a refresh error', () => {
|
||||
expect(
|
||||
getChecksPanelEmptyStateCopy({
|
||||
operationLabel: null,
|
||||
prRefreshStatus: 'error',
|
||||
hostedReviewBlockedReason: 'no_upstream',
|
||||
hasUpstream: false
|
||||
})
|
||||
).toEqual({
|
||||
title: 'Branch not published',
|
||||
description: 'Publish this branch before creating a pull request.'
|
||||
})
|
||||
})
|
||||
|
||||
it('uses remote status as a fallback before eligibility finishes', () => {
|
||||
expect(
|
||||
getChecksPanelEmptyStateCopy({
|
||||
operationLabel: null,
|
||||
prRefreshStatus: 'error',
|
||||
hostedReviewBlockedReason: undefined,
|
||||
hasUpstream: false
|
||||
}).title
|
||||
).toBe('Branch not published')
|
||||
})
|
||||
|
||||
it('uses remote status as a fallback when eligibility has no concrete blocker', () => {
|
||||
expect(
|
||||
getChecksPanelEmptyStateCopy({
|
||||
operationLabel: null,
|
||||
prRefreshStatus: 'error',
|
||||
hostedReviewBlockedReason: null,
|
||||
hasUpstream: false
|
||||
}).title
|
||||
).toBe('Branch not published')
|
||||
})
|
||||
|
||||
it('shows unpushed commits before a refresh error', () => {
|
||||
expect(
|
||||
getChecksPanelEmptyStateCopy({
|
||||
operationLabel: null,
|
||||
prRefreshStatus: 'error',
|
||||
hostedReviewBlockedReason: 'needs_push',
|
||||
hasUpstream: true
|
||||
})
|
||||
).toEqual({
|
||||
title: 'Branch has unpushed commits',
|
||||
description: 'Push your branch before creating a pull request.'
|
||||
})
|
||||
})
|
||||
|
||||
it('shows unpublished branch copy even when PR provider eligibility has another blocker', () => {
|
||||
expect(
|
||||
getChecksPanelEmptyStateCopy({
|
||||
operationLabel: null,
|
||||
prRefreshStatus: 'error',
|
||||
hostedReviewBlockedReason: 'unsupported_provider',
|
||||
hasUpstream: false
|
||||
}).title
|
||||
).toBe('Branch not published')
|
||||
})
|
||||
|
||||
it('keeps the generic refresh error when no local branch action is known', () => {
|
||||
expect(
|
||||
getChecksPanelEmptyStateCopy({
|
||||
operationLabel: null,
|
||||
prRefreshStatus: 'error',
|
||||
hostedReviewBlockedReason: null,
|
||||
hasUpstream: true
|
||||
}).title
|
||||
).toBe('Could not refresh pull request')
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldShowChecksPanelPublishBranchAction', () => {
|
||||
it('shows publish when eligibility reports no upstream', () => {
|
||||
expect(
|
||||
shouldShowChecksPanelPublishBranchAction({
|
||||
hostedReviewBlockedReason: 'no_upstream',
|
||||
hasUpstream: undefined
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('uses remote status even when provider eligibility has a separate blocker', () => {
|
||||
expect(
|
||||
shouldShowChecksPanelPublishBranchAction({
|
||||
hostedReviewBlockedReason: undefined,
|
||||
hasUpstream: false
|
||||
})
|
||||
).toBe(true)
|
||||
expect(
|
||||
shouldShowChecksPanelPublishBranchAction({
|
||||
hostedReviewBlockedReason: 'unsupported_provider',
|
||||
hasUpstream: false
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { HostedReviewCreationBlockedReason } from '../../../../shared/hosted-review'
|
||||
|
||||
type PRRefreshStatus = 'queued' | 'in-flight' | 'paused' | 'error' | 'skipped' | undefined
|
||||
|
||||
type ChecksPanelEmptyStateInput = {
|
||||
operationLabel: string | null
|
||||
prRefreshStatus: PRRefreshStatus
|
||||
hostedReviewBlockedReason: HostedReviewCreationBlockedReason | undefined
|
||||
hasUpstream: boolean | undefined
|
||||
}
|
||||
|
||||
type ChecksPanelEmptyStateCopy = {
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export function getChecksPanelEmptyStateCopy(
|
||||
input: ChecksPanelEmptyStateInput
|
||||
): ChecksPanelEmptyStateCopy {
|
||||
if (input.operationLabel) {
|
||||
return {
|
||||
title: `${input.operationLabel} in progress`,
|
||||
description: 'PR checks will be available after the operation completes'
|
||||
}
|
||||
}
|
||||
|
||||
const blockedReason = input.hostedReviewBlockedReason
|
||||
if (
|
||||
shouldShowChecksPanelPublishBranchAction({
|
||||
hostedReviewBlockedReason: blockedReason,
|
||||
hasUpstream: input.hasUpstream
|
||||
})
|
||||
) {
|
||||
// Why: a local-only branch cannot have GitHub PR status yet; surfacing a
|
||||
// refresh error here makes a normal pre-publish state look broken.
|
||||
return {
|
||||
title: 'Branch not published',
|
||||
description: 'Publish this branch before creating a pull request.'
|
||||
}
|
||||
}
|
||||
|
||||
if (blockedReason === 'needs_push') {
|
||||
return {
|
||||
title: 'Branch has unpushed commits',
|
||||
description: 'Push your branch before creating a pull request.'
|
||||
}
|
||||
}
|
||||
|
||||
switch (input.prRefreshStatus) {
|
||||
case 'error':
|
||||
return {
|
||||
title: 'Could not refresh pull request',
|
||||
description: 'GitHub status could not be refreshed. Existing cached data was preserved.'
|
||||
}
|
||||
case 'queued':
|
||||
return {
|
||||
title: 'Checking for pull request',
|
||||
description: 'Waiting to refresh GitHub status for this branch'
|
||||
}
|
||||
case 'in-flight':
|
||||
return {
|
||||
title: 'Checking for pull request',
|
||||
description: 'Refreshing GitHub status for this branch'
|
||||
}
|
||||
case 'paused':
|
||||
return {
|
||||
title: 'No pull request found',
|
||||
description: 'GitHub refresh is paused by the current rate-limit budget'
|
||||
}
|
||||
default:
|
||||
return {
|
||||
title: 'No pull request found',
|
||||
description: 'Create a pull request to start checks and review.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldShowChecksPanelPublishBranchAction(input: {
|
||||
hostedReviewBlockedReason: HostedReviewCreationBlockedReason | undefined
|
||||
hasUpstream: boolean | undefined
|
||||
}): boolean {
|
||||
const blockedReason = input.hostedReviewBlockedReason
|
||||
return input.hasUpstream === false || blockedReason === 'no_upstream'
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
buildChecksPanelGitStatusContextKey,
|
||||
readChecksPanelPublishActionGitStatus,
|
||||
readChecksPanelGitStatusSnapshot,
|
||||
shouldClearChecksPanelGitStatusSnapshot,
|
||||
shouldCoalesceChecksPanelGitStatusSnapshotRefresh,
|
||||
shouldCommitChecksPanelGitStatusSnapshot,
|
||||
shouldPollChecksPanelRuntimeSshStatus,
|
||||
type ChecksPanelGitStatusSnapshot
|
||||
} from './checks-panel-git-status-snapshot'
|
||||
|
||||
const SNAPSHOT: ChecksPanelGitStatusSnapshot = {
|
||||
contextKey: 'runtime:env-1::repo::worktree::branch',
|
||||
hasUncommittedChanges: true,
|
||||
remoteStatus: {
|
||||
hasUpstream: false,
|
||||
ahead: 0,
|
||||
behind: 0
|
||||
}
|
||||
}
|
||||
|
||||
describe('buildChecksPanelGitStatusContextKey', () => {
|
||||
it('changes when an explicit push target changes', () => {
|
||||
const base = {
|
||||
repoId: 'repo-1',
|
||||
worktreeId: 'worktree-1',
|
||||
worktreePath: 'repo-worktree',
|
||||
branch: 'feature/checks',
|
||||
runtimeEnvironmentId: 'runtime-1',
|
||||
repoConnectionId: 'ssh-1'
|
||||
}
|
||||
|
||||
expect(
|
||||
buildChecksPanelGitStatusContextKey({
|
||||
...base,
|
||||
pushTarget: { remoteName: 'origin', branchName: 'feature/checks' }
|
||||
})
|
||||
).not.toBe(
|
||||
buildChecksPanelGitStatusContextKey({
|
||||
...base,
|
||||
pushTarget: { remoteName: 'fork', branchName: 'feature/checks' }
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('readChecksPanelGitStatusSnapshot', () => {
|
||||
it('returns status inputs for the matching panel context', () => {
|
||||
expect(readChecksPanelGitStatusSnapshot(SNAPSHOT, SNAPSHOT.contextKey)).toEqual({
|
||||
hasUncommittedChanges: true,
|
||||
remoteStatus: {
|
||||
hasUpstream: false,
|
||||
ahead: 0,
|
||||
behind: 0
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('withholds worktree-keyed status after a runtime or SSH context change', () => {
|
||||
expect(
|
||||
readChecksPanelGitStatusSnapshot(SNAPSHOT, 'runtime:env-2::repo::worktree::branch')
|
||||
).toEqual({
|
||||
hasUncommittedChanges: undefined,
|
||||
remoteStatus: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('readChecksPanelPublishActionGitStatus', () => {
|
||||
it('uses the matching panel snapshot before worktree-keyed fallback status', () => {
|
||||
expect(
|
||||
readChecksPanelPublishActionGitStatus({
|
||||
snapshot: SNAPSHOT,
|
||||
contextKey: SNAPSHOT.contextKey,
|
||||
fallbackEntries: [],
|
||||
fallbackRemoteStatus: {
|
||||
hasUpstream: true,
|
||||
ahead: 0,
|
||||
behind: 0
|
||||
}
|
||||
})
|
||||
).toEqual({
|
||||
hasUncommittedChanges: true,
|
||||
remoteStatus: {
|
||||
hasUpstream: false,
|
||||
ahead: 0,
|
||||
behind: 0
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to active worktree status when the panel snapshot is unavailable', () => {
|
||||
expect(
|
||||
readChecksPanelPublishActionGitStatus({
|
||||
snapshot: null,
|
||||
contextKey: SNAPSHOT.contextKey,
|
||||
fallbackEntries: [],
|
||||
fallbackRemoteStatus: {
|
||||
hasUpstream: false,
|
||||
ahead: 0,
|
||||
behind: 0
|
||||
}
|
||||
})
|
||||
).toEqual({
|
||||
hasUncommittedChanges: false,
|
||||
remoteStatus: {
|
||||
hasUpstream: false,
|
||||
ahead: 0,
|
||||
behind: 0
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('does not synthesize publish inputs without fallback upstream status', () => {
|
||||
expect(
|
||||
readChecksPanelPublishActionGitStatus({
|
||||
snapshot: null,
|
||||
contextKey: SNAPSHOT.contextKey,
|
||||
fallbackEntries: [],
|
||||
fallbackRemoteStatus: undefined
|
||||
})
|
||||
).toEqual({
|
||||
hasUncommittedChanges: undefined,
|
||||
remoteStatus: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldCommitChecksPanelGitStatusSnapshot', () => {
|
||||
it('suppresses stale status refresh completions from an older execution boundary', () => {
|
||||
expect(
|
||||
shouldCommitChecksPanelGitStatusSnapshot(
|
||||
'runtime:env-2::repo::worktree::branch',
|
||||
'runtime:env-1::repo::worktree::branch'
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldCoalesceChecksPanelGitStatusSnapshotRefresh', () => {
|
||||
it('coalesces only requests for the same panel context', () => {
|
||||
expect(
|
||||
shouldCoalesceChecksPanelGitStatusSnapshotRefresh(
|
||||
'runtime:env-1::repo::worktree::branch',
|
||||
'runtime:env-1::repo::worktree::branch'
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
shouldCoalesceChecksPanelGitStatusSnapshotRefresh(
|
||||
'runtime:env-1::repo::worktree::branch',
|
||||
'runtime:env-2::repo::worktree::branch'
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldClearChecksPanelGitStatusSnapshot', () => {
|
||||
it('keeps the current snapshot while a same-context refresh is in flight', () => {
|
||||
expect(shouldClearChecksPanelGitStatusSnapshot(SNAPSHOT, SNAPSHOT.contextKey)).toBe(false)
|
||||
})
|
||||
|
||||
it('clears snapshots from another execution boundary', () => {
|
||||
expect(
|
||||
shouldClearChecksPanelGitStatusSnapshot(SNAPSHOT, 'runtime:env-2::repo::worktree::branch')
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldPollChecksPanelRuntimeSshStatus', () => {
|
||||
it('polls while a runtime environment is driving an SSH-backed repo', () => {
|
||||
expect(
|
||||
shouldPollChecksPanelRuntimeSshStatus({
|
||||
isPanelVisible: true,
|
||||
runtimeEnvironmentId: 'runtime-1',
|
||||
repoConnectionId: 'ssh-1'
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('does not poll when the Checks panel is hidden or execution is not runtime-routed SSH', () => {
|
||||
expect(
|
||||
shouldPollChecksPanelRuntimeSshStatus({
|
||||
isPanelVisible: false,
|
||||
runtimeEnvironmentId: 'runtime-1',
|
||||
repoConnectionId: 'ssh-1'
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
shouldPollChecksPanelRuntimeSshStatus({
|
||||
isPanelVisible: true,
|
||||
runtimeEnvironmentId: null,
|
||||
repoConnectionId: 'ssh-1'
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
shouldPollChecksPanelRuntimeSshStatus({
|
||||
isPanelVisible: true,
|
||||
runtimeEnvironmentId: 'runtime-1',
|
||||
repoConnectionId: null
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { GitPushTarget, GitStatusEntry, GitUpstreamStatus } from '../../../../shared/types'
|
||||
|
||||
export type ChecksPanelGitStatusContextInput = {
|
||||
repoId: string | null | undefined
|
||||
worktreeId: string | null | undefined
|
||||
worktreePath: string | null | undefined
|
||||
branch: string
|
||||
runtimeEnvironmentId: string | null
|
||||
repoConnectionId: string | null
|
||||
pushTarget: GitPushTarget | null | undefined
|
||||
}
|
||||
|
||||
export type ChecksPanelGitStatusSnapshot = {
|
||||
contextKey: string
|
||||
hasUncommittedChanges: boolean
|
||||
remoteStatus: GitUpstreamStatus | undefined
|
||||
}
|
||||
|
||||
export type ChecksPanelGitStatusInputs = {
|
||||
hasUncommittedChanges: boolean | undefined
|
||||
remoteStatus: GitUpstreamStatus | undefined
|
||||
}
|
||||
|
||||
export function buildChecksPanelGitStatusContextKey(
|
||||
input: ChecksPanelGitStatusContextInput
|
||||
): string {
|
||||
return JSON.stringify({
|
||||
repoId: input.repoId ?? '',
|
||||
worktreeId: input.worktreeId ?? '',
|
||||
worktreePath: input.worktreePath ?? '',
|
||||
branch: input.branch,
|
||||
runtimeEnvironmentId: input.runtimeEnvironmentId ?? '',
|
||||
repoConnectionId: input.repoConnectionId ?? '',
|
||||
pushTarget: input.pushTarget
|
||||
? {
|
||||
remoteName: input.pushTarget.remoteName,
|
||||
branchName: input.pushTarget.branchName,
|
||||
remoteUrl: input.pushTarget.remoteUrl ?? null,
|
||||
remoteCreated: input.pushTarget.remoteCreated ?? false
|
||||
}
|
||||
: null
|
||||
})
|
||||
}
|
||||
|
||||
export function shouldPollChecksPanelRuntimeSshStatus(input: {
|
||||
isPanelVisible: boolean
|
||||
runtimeEnvironmentId: string | null
|
||||
repoConnectionId: string | null
|
||||
}): boolean {
|
||||
return (
|
||||
input.isPanelVisible && input.runtimeEnvironmentId !== null && input.repoConnectionId !== null
|
||||
)
|
||||
}
|
||||
|
||||
export function shouldCommitChecksPanelGitStatusSnapshot(
|
||||
currentContextKey: string,
|
||||
requestContextKey: string
|
||||
): boolean {
|
||||
return currentContextKey === requestContextKey
|
||||
}
|
||||
|
||||
export function shouldCoalesceChecksPanelGitStatusSnapshotRefresh(
|
||||
inFlightContextKey: string | null,
|
||||
requestContextKey: string
|
||||
): boolean {
|
||||
return inFlightContextKey === requestContextKey
|
||||
}
|
||||
|
||||
export function shouldClearChecksPanelGitStatusSnapshot(
|
||||
snapshot: ChecksPanelGitStatusSnapshot | null,
|
||||
contextKey: string
|
||||
): boolean {
|
||||
return snapshot?.contextKey !== contextKey
|
||||
}
|
||||
|
||||
export function readChecksPanelGitStatusSnapshot(
|
||||
snapshot: ChecksPanelGitStatusSnapshot | null,
|
||||
contextKey: string
|
||||
): ChecksPanelGitStatusInputs {
|
||||
if (!snapshot || snapshot.contextKey !== contextKey) {
|
||||
return {
|
||||
hasUncommittedChanges: undefined,
|
||||
remoteStatus: undefined
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hasUncommittedChanges: snapshot.hasUncommittedChanges,
|
||||
remoteStatus: snapshot.remoteStatus
|
||||
}
|
||||
}
|
||||
|
||||
export function readChecksPanelPublishActionGitStatus(input: {
|
||||
snapshot: ChecksPanelGitStatusSnapshot | null
|
||||
contextKey: string
|
||||
fallbackEntries: GitStatusEntry[] | undefined
|
||||
fallbackRemoteStatus: GitUpstreamStatus | undefined
|
||||
}): ChecksPanelGitStatusInputs {
|
||||
const snapshotInputs = readChecksPanelGitStatusSnapshot(input.snapshot, input.contextKey)
|
||||
if (snapshotInputs.hasUncommittedChanges !== undefined || !input.fallbackRemoteStatus) {
|
||||
return snapshotInputs
|
||||
}
|
||||
|
||||
return {
|
||||
hasUncommittedChanges: (input.fallbackEntries?.length ?? 0) > 0,
|
||||
remoteStatus: input.fallbackRemoteStatus
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user