mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
feat: show ShieldAlert icon for conflict-review tabs (#249)
* feat: show ShieldAlert icon for conflict-review tabs Display a distinct orange ShieldAlert icon for tabs opened in conflict-review mode, differentiating them from regular diff tabs. * fix: clear stale Rebasing/Merging badge on non-active worktrees The git status polling only runs for the active worktree. When a non-active worktree finishes rebasing, its conflict operation was never updated to 'unknown', leaving the badge stuck. Add a lightweight git:conflictOperation IPC (fs-only, no git subprocess) and poll it for non-active worktrees that have a non-unknown operation. Once the operation clears, the worktree drops out of the poll list.
This commit is contained in:
@@ -237,7 +237,7 @@ async function getConflictCompatibilityStatus(
|
||||
// rebase is still in progress but REBASE_HEAD is absent. The rebase-merge/ or
|
||||
// rebase-apply/ directory persists for the entire rebase, so checking it
|
||||
// catches the "rebase in progress, no conflicts on current step" case.
|
||||
async function detectConflictOperation(worktreePath: string): Promise<GitConflictOperation> {
|
||||
export async function detectConflictOperation(worktreePath: string): Promise<GitConflictOperation> {
|
||||
const gitDir = await resolveGitDir(worktreePath)
|
||||
const mergeHead = path.join(gitDir, 'MERGE_HEAD')
|
||||
const rebaseHead = path.join(gitDir, 'REBASE_HEAD')
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { Store } from '../persistence'
|
||||
import type {
|
||||
DirEntry,
|
||||
GitBranchCompareResult,
|
||||
GitConflictOperation,
|
||||
GitDiffResult,
|
||||
GitStatusResult,
|
||||
SearchOptions,
|
||||
@@ -17,6 +18,7 @@ import type {
|
||||
} from '../../shared/types'
|
||||
import {
|
||||
getStatus,
|
||||
detectConflictOperation,
|
||||
getDiff,
|
||||
stageFile,
|
||||
unstageFile,
|
||||
@@ -333,6 +335,17 @@ export function registerFilesystemHandlers(store: Store): void {
|
||||
}
|
||||
)
|
||||
|
||||
// Why: lightweight fs-only check for conflict operation state. Used to poll
|
||||
// non-active worktrees so their "Rebasing"/"Merging" badges clear when the
|
||||
// operation finishes, without running a full `git status`.
|
||||
ipcMain.handle(
|
||||
'git:conflictOperation',
|
||||
async (_event, args: { worktreePath: string }): Promise<GitConflictOperation> => {
|
||||
const worktreePath = await resolveRegisteredWorktreePath(args.worktreePath, store)
|
||||
return detectConflictOperation(worktreePath)
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
'git:diff',
|
||||
async (
|
||||
|
||||
Vendored
+2
@@ -13,6 +13,7 @@ import type {
|
||||
UpdateStatus,
|
||||
DirEntry,
|
||||
GitBranchCompareResult,
|
||||
GitConflictOperation,
|
||||
GitStatusEntry,
|
||||
GitDiffResult,
|
||||
SearchOptions,
|
||||
@@ -144,6 +145,7 @@ type FsApi = {
|
||||
|
||||
type GitApi = {
|
||||
status: (args: { worktreePath: string }) => Promise<GitStatusResult>
|
||||
conflictOperation: (args: { worktreePath: string }) => Promise<GitConflictOperation>
|
||||
diff: (args: {
|
||||
worktreePath: string
|
||||
filePath: string
|
||||
|
||||
@@ -260,6 +260,8 @@ const api = {
|
||||
git: {
|
||||
status: (args: { worktreePath: string }): Promise<unknown> =>
|
||||
ipcRenderer.invoke('git:status', args),
|
||||
conflictOperation: (args: { worktreePath: string }): Promise<unknown> =>
|
||||
ipcRenderer.invoke('git:conflictOperation', args),
|
||||
diff: (args: { worktreePath: string; filePath: string; staged: boolean }): Promise<unknown> =>
|
||||
ipcRenderer.invoke('git:diff', args),
|
||||
branchCompare: (args: { worktreePath: string; baseRef: string }): Promise<unknown> =>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { useAppStore } from '@/store'
|
||||
import type { GitStatusResult } from '../../../../shared/types'
|
||||
import type { GitConflictOperation, GitStatusResult } from '../../../../shared/types'
|
||||
|
||||
const POLL_INTERVAL_MS = 3000
|
||||
|
||||
@@ -8,6 +8,8 @@ export function useGitStatusPolling(): void {
|
||||
const activeWorktreeId = useAppStore((s) => s.activeWorktreeId)
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const setGitStatus = useAppStore((s) => s.setGitStatus)
|
||||
const setConflictOperation = useAppStore((s) => s.setConflictOperation)
|
||||
const conflictOperationByWorktree = useAppStore((s) => s.gitConflictOperationByWorktree)
|
||||
|
||||
const worktreePath = useMemo(() => {
|
||||
if (!activeWorktreeId) {
|
||||
@@ -22,6 +24,27 @@ export function useGitStatusPolling(): void {
|
||||
return null
|
||||
}, [activeWorktreeId, worktreesByRepo])
|
||||
|
||||
// Why: build a list of non-active worktrees that still have a known conflict
|
||||
// operation (merge/rebase/cherry-pick). These need lightweight polling so
|
||||
// their sidebar badges clear when the operation finishes — the full git status
|
||||
// poll only covers the active worktree.
|
||||
const staleConflictWorktrees = useMemo(() => {
|
||||
const result: { id: string; path: string }[] = []
|
||||
for (const [worktreeId, op] of Object.entries(conflictOperationByWorktree)) {
|
||||
if (worktreeId === activeWorktreeId || op === 'unknown') {
|
||||
continue
|
||||
}
|
||||
for (const worktrees of Object.values(worktreesByRepo)) {
|
||||
const wt = worktrees.find((w) => w.id === worktreeId)
|
||||
if (wt) {
|
||||
result.push({ id: wt.id, path: wt.path })
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}, [conflictOperationByWorktree, activeWorktreeId, worktreesByRepo])
|
||||
|
||||
const fetchStatus = useCallback(async () => {
|
||||
if (!activeWorktreeId || !worktreePath) {
|
||||
return
|
||||
@@ -39,4 +62,30 @@ export function useGitStatusPolling(): void {
|
||||
const intervalId = setInterval(() => void fetchStatus(), POLL_INTERVAL_MS)
|
||||
return () => clearInterval(intervalId)
|
||||
}, [fetchStatus])
|
||||
|
||||
// Why: poll conflict operation for non-active worktrees that have a stale
|
||||
// non-unknown operation. This is a lightweight fs-only check (no git status)
|
||||
// so it won't cause performance issues even with many worktrees.
|
||||
useEffect(() => {
|
||||
if (staleConflictWorktrees.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const pollStale = async (): Promise<void> => {
|
||||
for (const { id, path } of staleConflictWorktrees) {
|
||||
try {
|
||||
const op = (await window.api.git.conflictOperation({
|
||||
worktreePath: path
|
||||
})) as GitConflictOperation
|
||||
setConflictOperation(id, op)
|
||||
} catch {
|
||||
// ignore — worktree may have been removed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void pollStale()
|
||||
const intervalId = setInterval(() => void pollStale(), POLL_INTERVAL_MS)
|
||||
return () => clearInterval(intervalId)
|
||||
}, [staleConflictWorktrees, setConflictOperation])
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { X, FileCode, GitCompareArrows, Copy } from 'lucide-react'
|
||||
import { X, FileCode, GitCompareArrows, Copy, ShieldAlert } from 'lucide-react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -49,6 +49,7 @@ export default function EditorFileTab({
|
||||
}
|
||||
|
||||
const isDiff = file.mode === 'diff'
|
||||
const isConflictReview = file.mode === 'conflict-review'
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const [menuPoint, setMenuPoint] = useState({ x: 0, y: 0 })
|
||||
|
||||
@@ -104,7 +105,11 @@ export default function EditorFileTab({
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isDiff ? (
|
||||
{isConflictReview ? (
|
||||
<ShieldAlert
|
||||
className={`w-3.5 h-3.5 mr-1.5 shrink-0 ${isActive ? 'text-orange-400' : 'text-orange-400/70'}`}
|
||||
/>
|
||||
) : isDiff ? (
|
||||
<GitCompareArrows
|
||||
className={`w-3.5 h-3.5 mr-1.5 shrink-0 ${isActive ? 'text-foreground' : 'text-muted-foreground'}`}
|
||||
/>
|
||||
|
||||
@@ -182,6 +182,9 @@ export type EditorSlice = {
|
||||
trackedConflictPathsByWorktree: Record<string, Record<string, GitConflictKind>>
|
||||
trackConflictPath: (worktreeId: string, path: string, conflictKind: GitConflictKind) => void
|
||||
setGitStatus: (worktreeId: string, status: GitStatusResult) => void
|
||||
// Why: lightweight updater for conflict operation only, used to clear stale
|
||||
// "Rebasing"/"Merging" badges on non-active worktrees without a full git status poll.
|
||||
setConflictOperation: (worktreeId: string, operation: GitConflictOperation) => void
|
||||
gitBranchChangesByWorktree: Record<string, GitBranchChangeEntry[]>
|
||||
gitBranchCompareSummaryByWorktree: Record<string, GitBranchCompareSummary | null>
|
||||
gitBranchCompareRequestKeyByWorktree: Record<string, string>
|
||||
@@ -969,6 +972,35 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
||||
: { ...s.trackedConflictPathsByWorktree, [worktreeId]: currentTracked }
|
||||
}
|
||||
}),
|
||||
setConflictOperation: (worktreeId, operation) =>
|
||||
set((s) => {
|
||||
const prev = s.gitConflictOperationByWorktree[worktreeId] ?? 'unknown'
|
||||
if (prev === operation) {
|
||||
return s
|
||||
}
|
||||
// Why: when the operation clears (transitions to 'unknown') on a non-active
|
||||
// worktree, we also need to clear tracked conflict paths — same as the
|
||||
// full setGitStatus handler does for the active worktree.
|
||||
const nextTracked =
|
||||
operation === 'unknown' && prev !== 'unknown'
|
||||
? {}
|
||||
: s.trackedConflictPathsByWorktree[worktreeId]
|
||||
const trackedUnchanged = nextTracked === s.trackedConflictPathsByWorktree[worktreeId]
|
||||
return {
|
||||
gitConflictOperationByWorktree: {
|
||||
...s.gitConflictOperationByWorktree,
|
||||
[worktreeId]: operation
|
||||
},
|
||||
...(trackedUnchanged
|
||||
? {}
|
||||
: {
|
||||
trackedConflictPathsByWorktree: {
|
||||
...s.trackedConflictPathsByWorktree,
|
||||
[worktreeId]: nextTracked
|
||||
}
|
||||
})
|
||||
}
|
||||
}),
|
||||
gitBranchChangesByWorktree: {},
|
||||
gitBranchCompareSummaryByWorktree: {},
|
||||
gitBranchCompareRequestKeyByWorktree: {},
|
||||
|
||||
Reference in New Issue
Block a user