feat(ui): add board refresh and stale-state polling

This commit is contained in:
DhanushSantosh
2026-02-04 10:16:11 +05:30
parent 0b6e84ec6e
commit d4b7a0c57d
6 changed files with 144 additions and 34 deletions

View File

@@ -437,6 +437,63 @@ export function BoardView() {
// Auto mode hook - pass current worktree to get worktree-specific state
// Must be after selectedWorktree is defined
const autoMode = useAutoMode(selectedWorktree);
const refreshBoardState = useCallback(async () => {
if (!currentProject) return;
const projectPath = currentProject.path;
const beforeFeatures = (
queryClient.getQueryData(queryKeys.features.all(projectPath)) as Feature[] | undefined
)?.length;
const beforeWorktrees = (
queryClient.getQueryData(queryKeys.worktrees.all(projectPath)) as
| { worktrees?: unknown[] }
| undefined
)?.worktrees?.length;
const beforeRunningAgents = (
queryClient.getQueryData(queryKeys.runningAgents.all()) as { count?: number } | undefined
)?.count;
const beforeAutoModeRunning = autoMode.isRunning;
try {
await Promise.all([
queryClient.refetchQueries({ queryKey: queryKeys.features.all(projectPath) }),
queryClient.refetchQueries({ queryKey: queryKeys.runningAgents.all() }),
queryClient.refetchQueries({ queryKey: queryKeys.worktrees.all(projectPath) }),
autoMode.refreshStatus(),
]);
const afterFeatures = (
queryClient.getQueryData(queryKeys.features.all(projectPath)) as Feature[] | undefined
)?.length;
const afterWorktrees = (
queryClient.getQueryData(queryKeys.worktrees.all(projectPath)) as
| { worktrees?: unknown[] }
| undefined
)?.worktrees?.length;
const afterRunningAgents = (
queryClient.getQueryData(queryKeys.runningAgents.all()) as { count?: number } | undefined
)?.count;
const afterAutoModeRunning = autoMode.isRunning;
if (
beforeFeatures !== afterFeatures ||
beforeWorktrees !== afterWorktrees ||
beforeRunningAgents !== afterRunningAgents ||
beforeAutoModeRunning !== afterAutoModeRunning
) {
logger.info('[Board] Refresh detected state mismatch', {
features: { before: beforeFeatures, after: afterFeatures },
worktrees: { before: beforeWorktrees, after: afterWorktrees },
runningAgents: { before: beforeRunningAgents, after: afterRunningAgents },
autoModeRunning: { before: beforeAutoModeRunning, after: afterAutoModeRunning },
});
}
} catch (error) {
logger.error('[Board] Failed to refresh board state:', error);
toast.error('Failed to refresh board state');
}
}, [autoMode, currentProject, queryClient]);
// Get runningTasks from the hook (scoped to current project/worktree)
const runningAutoTasks = autoMode.runningTasks;
// Get worktree-specific maxConcurrency from the hook
@@ -1321,6 +1378,7 @@ export function BoardView() {
isCreatingSpec={isCreatingSpec}
creatingSpecProjectPath={creatingSpecProjectPath}
onShowBoardBackground={() => setShowBoardBackgroundModal(true)}
onRefreshBoard={refreshBoardState}
viewMode={viewMode}
onViewModeChange={setViewMode}
/>

View File

@@ -1,7 +1,9 @@
import { useCallback, useState } from 'react';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import { Wand2, GitBranch, ClipboardCheck } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Wand2, GitBranch, ClipboardCheck, RefreshCw } from 'lucide-react';
import { UsagePopover } from '@/components/usage-popover';
import { useAppStore } from '@/store/app-store';
import { useSetupStore } from '@/store/setup-store';
@@ -35,6 +37,7 @@ interface BoardHeaderProps {
creatingSpecProjectPath?: string;
// Board controls props
onShowBoardBackground: () => void;
onRefreshBoard: () => Promise<void>;
// View toggle props
viewMode: ViewMode;
onViewModeChange: (mode: ViewMode) => void;
@@ -60,6 +63,7 @@ export function BoardHeader({
isCreatingSpec,
creatingSpecProjectPath,
onShowBoardBackground,
onRefreshBoard,
viewMode,
onViewModeChange,
}: BoardHeaderProps) {
@@ -110,9 +114,20 @@ export function BoardHeader({
// State for mobile actions panel
const [showActionsPanel, setShowActionsPanel] = useState(false);
const [isRefreshingBoard, setIsRefreshingBoard] = useState(false);
const isTablet = useIsTablet();
const handleRefreshBoard = useCallback(async () => {
if (isRefreshingBoard) return;
setIsRefreshingBoard(true);
try {
await onRefreshBoard();
} finally {
setIsRefreshingBoard(false);
}
}, [isRefreshingBoard, onRefreshBoard]);
return (
<div className="flex items-center justify-between gap-5 p-4 border-b border-border bg-glass backdrop-blur-md">
<div className="flex items-center gap-4">
@@ -127,6 +142,22 @@ export function BoardHeader({
<BoardControls isMounted={isMounted} onShowBoardBackground={onShowBoardBackground} />
</div>
<div className="flex gap-4 items-center">
{isMounted && !isTablet && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
size="icon-sm"
onClick={handleRefreshBoard}
disabled={isRefreshingBoard}
aria-label="Refresh board state from server"
>
<RefreshCw className={isRefreshingBoard ? 'w-4 h-4 animate-spin' : 'w-4 h-4'} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">Refresh board state from server</TooltipContent>
</Tooltip>
)}
{/* Usage Popover - show if either provider is authenticated, only on desktop */}
{isMounted && !isTablet && (showClaudeUsage || showCodexUsage) && <UsagePopover />}