mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 00:02:29 +00:00
Resolve workspace space row state during render (#3387)
This commit is contained in:
@@ -72,6 +72,9 @@ import {
|
||||
getVisibleDeletableWorkspaceIds,
|
||||
getWorkspaceSpaceGitStatusRefreshCandidates,
|
||||
isWorkspaceSpaceRowReadyToDelete,
|
||||
pruneWorkspaceSpaceSelectedIds,
|
||||
resolveWorkspaceSpaceInspectedWorktreeId,
|
||||
resolveWorkspaceSpaceTreemapZoomWorktreeId,
|
||||
sortWorkspaceSpaceRows,
|
||||
type WorkspaceSpaceSortDirection,
|
||||
type WorkspaceSpaceSortKey
|
||||
@@ -1245,6 +1248,27 @@ export function WorkspaceSpaceManagerPanel(): React.JSX.Element {
|
||||
[onlyDeletable, query, sortDirection, sortKey, sourceRows]
|
||||
)
|
||||
|
||||
const nextInspectedWorktreeId = resolveWorkspaceSpaceInspectedWorktreeId(
|
||||
sourceRows,
|
||||
inspectedWorktreeId
|
||||
)
|
||||
const nextSelectedIds = pruneWorkspaceSpaceSelectedIds(sourceRows, selectedIds)
|
||||
const nextTreemapZoomWorktreeId = resolveWorkspaceSpaceTreemapZoomWorktreeId(
|
||||
sourceRows,
|
||||
treemapZoomWorktreeId
|
||||
)
|
||||
// Why: these ids are local UI state derived from the latest scan rows. Repair
|
||||
// them before commit so stale selections cannot flash after a scan changes.
|
||||
if (inspectedWorktreeId !== nextInspectedWorktreeId) {
|
||||
setInspectedWorktreeId(nextInspectedWorktreeId)
|
||||
}
|
||||
if (nextSelectedIds !== selectedIds) {
|
||||
setSelectedIds(nextSelectedIds)
|
||||
}
|
||||
if (treemapZoomWorktreeId !== nextTreemapZoomWorktreeId) {
|
||||
setTreemapZoomWorktreeId(nextTreemapZoomWorktreeId)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const candidates = getWorkspaceSpaceGitStatusRefreshCandidates(sourceRows)
|
||||
if (candidates.length === 0) {
|
||||
@@ -1272,16 +1296,16 @@ export function WorkspaceSpaceManagerPanel(): React.JSX.Element {
|
||||
}, [refreshWorkspaceGitStatus, sourceRows])
|
||||
|
||||
const inspectedWorktree =
|
||||
rows.find((row) => row.worktreeId === inspectedWorktreeId) ??
|
||||
rows.find((row) => row.worktreeId === nextInspectedWorktreeId) ??
|
||||
rows.find((row) => row.status === 'ok') ??
|
||||
null
|
||||
const zoomedWorktree =
|
||||
sourceRows.find((row) => row.worktreeId === treemapZoomWorktreeId && row.status === 'ok') ??
|
||||
sourceRows.find((row) => row.worktreeId === nextTreemapZoomWorktreeId && row.status === 'ok') ??
|
||||
null
|
||||
const maxSize = getLargestWorkspaceSpaceRowSize(rows)
|
||||
const selectedDeletableIds = useMemo(
|
||||
() => getSelectedDeletableWorkspaceIds(rows, selectedIds, isWorktreeUnavailableForDelete),
|
||||
[isWorktreeUnavailableForDelete, rows, selectedIds]
|
||||
() => getSelectedDeletableWorkspaceIds(rows, nextSelectedIds, isWorktreeUnavailableForDelete),
|
||||
[isWorktreeUnavailableForDelete, nextSelectedIds, rows]
|
||||
)
|
||||
const selectedDeletableIdSet = useMemo(
|
||||
() => new Set(selectedDeletableIds),
|
||||
@@ -1292,8 +1316,8 @@ export function WorkspaceSpaceManagerPanel(): React.JSX.Element {
|
||||
[isWorktreeUnavailableForDelete, rows]
|
||||
)
|
||||
const allVisibleSelected =
|
||||
visibleDeletableIds.length > 0 && visibleDeletableIds.every((id) => selectedIds.has(id))
|
||||
const someVisibleSelected = visibleDeletableIds.some((id) => selectedIds.has(id))
|
||||
visibleDeletableIds.length > 0 && visibleDeletableIds.every((id) => nextSelectedIds.has(id))
|
||||
const someVisibleSelected = visibleDeletableIds.some((id) => nextSelectedIds.has(id))
|
||||
const visibleSelectionState = allVisibleSelected ? true : someVisibleSelected ? 'mixed' : false
|
||||
const isInitialScan = isScanning && !analysis
|
||||
const hasRows = sourceRows.length > 0
|
||||
@@ -1307,34 +1331,6 @@ export function WorkspaceSpaceManagerPanel(): React.JSX.Element {
|
||||
[rows, selectedDeletableIdSet]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!analysis) {
|
||||
setInspectedWorktreeId(null)
|
||||
return
|
||||
}
|
||||
setInspectedWorktreeId((current) =>
|
||||
current && analysis.worktrees.some((worktree) => worktree.worktreeId === current)
|
||||
? current
|
||||
: (analysis.worktrees.find((worktree) => worktree.status === 'ok')?.worktreeId ?? null)
|
||||
)
|
||||
}, [analysis])
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIds((current) => {
|
||||
const valid = new Set(sourceRows.map((row) => row.worktreeId))
|
||||
const next = new Set([...current].filter((id) => valid.has(id)))
|
||||
return next.size === current.size ? current : next
|
||||
})
|
||||
}, [sourceRows])
|
||||
|
||||
useEffect(() => {
|
||||
setTreemapZoomWorktreeId((current) =>
|
||||
current && sourceRows.some((row) => row.worktreeId === current && row.status === 'ok')
|
||||
? current
|
||||
: null
|
||||
)
|
||||
}, [sourceRows])
|
||||
|
||||
const toggleSort = (key: WorkspaceSpaceSortKey): void => {
|
||||
if (sortKey === key) {
|
||||
setSortDirection((current) => (current === 'asc' ? 'desc' : 'asc'))
|
||||
@@ -1692,7 +1688,7 @@ export function WorkspaceSpaceManagerPanel(): React.JSX.Element {
|
||||
key={worktree.worktreeId}
|
||||
worktree={worktree}
|
||||
maxSize={maxSize}
|
||||
selected={selectedIds.has(worktree.worktreeId)}
|
||||
selected={nextSelectedIds.has(worktree.worktreeId)}
|
||||
inspected={inspectedWorktree?.worktreeId === worktree.worktreeId}
|
||||
decisionDetails={
|
||||
decisionDetailsByWorktreeId.get(worktree.worktreeId) ??
|
||||
|
||||
@@ -9,6 +9,9 @@ import {
|
||||
getVisibleDeletableWorkspaceIds,
|
||||
getWorkspaceSpaceGitStatusRefreshCandidates,
|
||||
isWorkspaceSpaceRowReadyToDelete,
|
||||
pruneWorkspaceSpaceSelectedIds,
|
||||
resolveWorkspaceSpaceInspectedWorktreeId,
|
||||
resolveWorkspaceSpaceTreemapZoomWorktreeId,
|
||||
sortWorkspaceSpaceRows
|
||||
} from './workspace-space-presentation'
|
||||
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
|
||||
@@ -239,4 +242,37 @@ describe('workspace space presentation helpers', () => {
|
||||
getWorkspaceSpaceGitStatusRefreshCandidates(rows).map((item) => item.worktreeId)
|
||||
).toEqual(rows.map((item) => item.worktreeId))
|
||||
})
|
||||
|
||||
it('resolves inspected worktree ids from the current scan rows', () => {
|
||||
const rows = [
|
||||
row({ worktreeId: 'errored', status: 'error' }),
|
||||
row({ worktreeId: 'ready', status: 'ok' })
|
||||
]
|
||||
|
||||
expect(resolveWorkspaceSpaceInspectedWorktreeId(rows, 'errored')).toBe('errored')
|
||||
expect(resolveWorkspaceSpaceInspectedWorktreeId(rows, 'missing')).toBe('ready')
|
||||
expect(resolveWorkspaceSpaceInspectedWorktreeId([], 'missing')).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps treemap zoom only for ready current scan rows', () => {
|
||||
const rows = [
|
||||
row({ worktreeId: 'ready', status: 'ok' }),
|
||||
row({ worktreeId: 'errored', status: 'error' })
|
||||
]
|
||||
|
||||
expect(resolveWorkspaceSpaceTreemapZoomWorktreeId(rows, 'ready')).toBe('ready')
|
||||
expect(resolveWorkspaceSpaceTreemapZoomWorktreeId(rows, 'errored')).toBeNull()
|
||||
expect(resolveWorkspaceSpaceTreemapZoomWorktreeId(rows, 'missing')).toBeNull()
|
||||
})
|
||||
|
||||
it('prunes selected workspace ids that are absent from the current scan', () => {
|
||||
const selectedIds = new Set(['ready', 'missing'])
|
||||
const pruned = pruneWorkspaceSpaceSelectedIds([row({ worktreeId: 'ready' })], selectedIds)
|
||||
|
||||
expect([...pruned]).toEqual(['ready'])
|
||||
expect(pruned).not.toBe(selectedIds)
|
||||
|
||||
const unchanged = pruneWorkspaceSpaceSelectedIds([row({ worktreeId: 'ready' })], pruned)
|
||||
expect(unchanged).toBe(pruned)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -273,3 +273,44 @@ export function getVisibleDeletableWorkspaceIds(
|
||||
.filter((row) => row.canDelete && row.status === 'ok' && !isWorktreeDeleting(row.worktreeId))
|
||||
.map((row) => row.worktreeId)
|
||||
}
|
||||
|
||||
export function resolveWorkspaceSpaceInspectedWorktreeId(
|
||||
rows: readonly WorkspaceSpaceWorktree[],
|
||||
currentWorktreeId: string | null
|
||||
): string | null {
|
||||
if (currentWorktreeId && rows.some((row) => row.worktreeId === currentWorktreeId)) {
|
||||
return currentWorktreeId
|
||||
}
|
||||
return rows.find((row) => row.status === 'ok')?.worktreeId ?? null
|
||||
}
|
||||
|
||||
export function resolveWorkspaceSpaceTreemapZoomWorktreeId(
|
||||
rows: readonly WorkspaceSpaceWorktree[],
|
||||
currentWorktreeId: string | null
|
||||
): string | null {
|
||||
return currentWorktreeId &&
|
||||
rows.some((row) => row.worktreeId === currentWorktreeId && row.status === 'ok')
|
||||
? currentWorktreeId
|
||||
: null
|
||||
}
|
||||
|
||||
export function pruneWorkspaceSpaceSelectedIds(
|
||||
rows: readonly WorkspaceSpaceWorktree[],
|
||||
selectedIds: Set<string>
|
||||
): Set<string> {
|
||||
if (selectedIds.size === 0) {
|
||||
return selectedIds
|
||||
}
|
||||
|
||||
const validIds = new Set(rows.map((row) => row.worktreeId))
|
||||
let changed = false
|
||||
const nextIds = new Set<string>()
|
||||
for (const id of selectedIds) {
|
||||
if (validIds.has(id)) {
|
||||
nextIds.add(id)
|
||||
} else {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed ? nextIds : selectedIds
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user