mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
fix(ui): improve React Query hooks and fix edge cases
- Update query keys to include all relevant parameters (branches, agents) - Fix use-branches to pass includeRemote parameter to query key - Fix use-settings to include sources in agents query key - Update running-agents-view to use correct query key structure - Update use-spec-loading to properly use spec query hooks - Add missing queryClient invalidation in auto-mode mutations - Add missing cache invalidation in spec mutations after creation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import { createLogger } from '@automaker/utils/logger';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
@@ -119,6 +119,9 @@ export function SessionManager({
|
|||||||
// Use React Query for sessions list - always include archived, filter client-side
|
// Use React Query for sessions list - always include archived, filter client-side
|
||||||
const { data: sessions = [], refetch: refetchSessions } = useSessions(true);
|
const { data: sessions = [], refetch: refetchSessions } = useSessions(true);
|
||||||
|
|
||||||
|
// Ref to track if we've done the initial running sessions check
|
||||||
|
const hasCheckedInitialRef = useRef(false);
|
||||||
|
|
||||||
// Check running state for all sessions
|
// Check running state for all sessions
|
||||||
const checkRunningSessions = useCallback(async (sessionList: SessionListItem[]) => {
|
const checkRunningSessions = useCallback(async (sessionList: SessionListItem[]) => {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
@@ -152,12 +155,13 @@ export function SessionManager({
|
|||||||
}
|
}
|
||||||
}, [queryClient, refetchSessions, checkRunningSessions]);
|
}, [queryClient, refetchSessions, checkRunningSessions]);
|
||||||
|
|
||||||
// Check running state on initial load
|
// Check running state on initial load (runs only once when sessions first load)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sessions.length > 0) {
|
if (sessions.length > 0 && !hasCheckedInitialRef.current) {
|
||||||
|
hasCheckedInitialRef.current = true;
|
||||||
checkRunningSessions(sessions);
|
checkRunningSessions(sessions);
|
||||||
}
|
}
|
||||||
}, [sessions.length > 0]); // Only run when sessions first load
|
}, [sessions, checkRunningSessions]);
|
||||||
|
|
||||||
// Periodically check running state for sessions (useful for detecting when agents finish)
|
// Periodically check running state for sessions (useful for detecting when agents finish)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ export function UsagePopover() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={cn('h-6 w-6', claudeLoading && 'opacity-80')}
|
className={cn('h-6 w-6', claudeLoading && 'opacity-80')}
|
||||||
onClick={() => !claudeLoading && fetchClaudeUsage(false)}
|
onClick={() => !claudeLoading && fetchClaudeUsage()}
|
||||||
>
|
>
|
||||||
<RefreshCw className="w-3.5 h-3.5" />
|
<RefreshCw className="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -411,7 +411,7 @@ export function UsagePopover() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={cn('h-6 w-6', codexLoading && 'opacity-80')}
|
className={cn('h-6 w-6', codexLoading && 'opacity-80')}
|
||||||
onClick={() => !codexLoading && fetchCodexUsage(false)}
|
onClick={() => !codexLoading && fetchCodexUsage()}
|
||||||
>
|
>
|
||||||
<RefreshCw className="w-3.5 h-3.5" />
|
<RefreshCw className="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -22,9 +22,11 @@ export function useBranches() {
|
|||||||
const branches = branchData?.branches ?? [];
|
const branches = branchData?.branches ?? [];
|
||||||
const aheadCount = branchData?.aheadCount ?? 0;
|
const aheadCount = branchData?.aheadCount ?? 0;
|
||||||
const behindCount = branchData?.behindCount ?? 0;
|
const behindCount = branchData?.behindCount ?? 0;
|
||||||
|
// Use conservative defaults (false) until data is confirmed
|
||||||
|
// This prevents the UI from assuming git capabilities before the query completes
|
||||||
const gitRepoStatus: GitRepoStatus = {
|
const gitRepoStatus: GitRepoStatus = {
|
||||||
isGitRepo: branchData?.isGitRepo ?? true,
|
isGitRepo: branchData?.isGitRepo ?? false,
|
||||||
hasCommits: branchData?.hasCommits ?? true,
|
hasCommits: branchData?.hasCommits ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchBranches = useCallback(
|
const fetchBranches = useCallback(
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ export function RunningAgentsView() {
|
|||||||
}, [refetch]);
|
}, [refetch]);
|
||||||
|
|
||||||
const handleStopAgent = useCallback(
|
const handleStopAgent = useCallback(
|
||||||
(featureId: string) => {
|
(featureId: string, projectPath: string) => {
|
||||||
stopFeature.mutate(featureId);
|
stopFeature.mutate({ featureId, projectPath });
|
||||||
},
|
},
|
||||||
[stopFeature]
|
[stopFeature]
|
||||||
);
|
);
|
||||||
@@ -168,7 +168,7 @@ export function RunningAgentsView() {
|
|||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleStopAgent(agent.featureId)}
|
onClick={() => handleStopAgent(agent.featureId, agent.projectPath)}
|
||||||
disabled={stopFeature.isPending}
|
disabled={stopFeature.isPending}
|
||||||
>
|
>
|
||||||
<Square className="h-3.5 w-3.5 mr-1.5" />
|
<Square className="h-3.5 w-3.5 mr-1.5" />
|
||||||
|
|||||||
@@ -27,15 +27,13 @@ export function useSpecLoading() {
|
|||||||
const loadSpec = useCallback(async () => {
|
const loadSpec = useCallback(async () => {
|
||||||
if (!currentProject?.path) return;
|
if (!currentProject?.path) return;
|
||||||
|
|
||||||
// First check if generation is running
|
// Fetch fresh status data to avoid stale cache issues
|
||||||
await queryClient.invalidateQueries({
|
// Using fetchQuery ensures we get the latest data before checking
|
||||||
|
const statusData = await queryClient.fetchQuery<{ isRunning: boolean }>({
|
||||||
queryKey: queryKeys.specRegeneration.status(currentProject.path),
|
queryKey: queryKeys.specRegeneration.status(currentProject.path),
|
||||||
|
staleTime: 0, // Force fresh fetch
|
||||||
});
|
});
|
||||||
|
|
||||||
const statusData = queryClient.getQueryData<{ isRunning: boolean }>(
|
|
||||||
queryKeys.specRegeneration.status(currentProject.path)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (statusData?.isRunning) {
|
if (statusData?.isRunning) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,21 +99,36 @@ export function useResumeFeature(projectPath: string) {
|
|||||||
* Stop a running feature
|
* Stop a running feature
|
||||||
*
|
*
|
||||||
* @returns Mutation for stopping a feature
|
* @returns Mutation for stopping a feature
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* const stopFeature = useStopFeature();
|
||||||
|
* // Simple stop
|
||||||
|
* stopFeature.mutate('feature-id');
|
||||||
|
* // Stop with project path for cache invalidation
|
||||||
|
* stopFeature.mutate({ featureId: 'feature-id', projectPath: '/path/to/project' });
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
export function useStopFeature() {
|
export function useStopFeature() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (featureId: string) => {
|
mutationFn: async (input: string | { featureId: string; projectPath?: string }) => {
|
||||||
|
const featureId = typeof input === 'string' ? input : input.featureId;
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
const result = await api.autoMode.stopFeature(featureId);
|
const result = await api.autoMode.stopFeature(featureId);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || 'Failed to stop feature');
|
throw new Error(result.error || 'Failed to stop feature');
|
||||||
}
|
}
|
||||||
return result;
|
// Return projectPath for use in onSuccess
|
||||||
|
return { ...result, projectPath: typeof input === 'string' ? undefined : input.projectPath };
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: (data) => {
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.runningAgents.all() });
|
queryClient.invalidateQueries({ queryKey: queryKeys.runningAgents.all() });
|
||||||
|
// Also invalidate features cache if projectPath is provided
|
||||||
|
if (data.projectPath) {
|
||||||
|
queryClient.invalidateQueries({ queryKey: queryKeys.features.all(data.projectPath) });
|
||||||
|
}
|
||||||
toast.success('Feature stopped');
|
toast.success('Feature stopped');
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { getElectronAPI, GitHubIssue, GitHubComment } from '@/lib/electron';
|
|||||||
import { queryKeys } from '@/lib/query-keys';
|
import { queryKeys } from '@/lib/query-keys';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import type { LinkedPRInfo, ModelId } from '@automaker/types';
|
import type { LinkedPRInfo, ModelId } from '@automaker/types';
|
||||||
|
import { resolveModelString } from '@automaker/model-resolver';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Input for validating a GitHub issue
|
* Input for validating a GitHub issue
|
||||||
@@ -64,10 +65,13 @@ export function useValidateIssue(projectPath: string) {
|
|||||||
linkedPRs,
|
linkedPRs,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Resolve model alias to canonical model identifier
|
||||||
|
const resolvedModel = model ? resolveModelString(model) : undefined;
|
||||||
|
|
||||||
const result = await api.github.validateIssue(
|
const result = await api.github.validateIssue(
|
||||||
projectPath,
|
projectPath,
|
||||||
validationInput,
|
validationInput,
|
||||||
model,
|
resolvedModel,
|
||||||
thinkingLevel,
|
thinkingLevel,
|
||||||
reasoningEffort
|
reasoningEffort
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -157,6 +157,11 @@ export function useSaveSpec(projectPath: string) {
|
|||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (content: string) => {
|
mutationFn: async (content: string) => {
|
||||||
|
// Guard against empty projectPath to prevent writing to invalid locations
|
||||||
|
if (!projectPath || projectPath.trim() === '') {
|
||||||
|
throw new Error('Invalid project path: cannot save spec without a valid project');
|
||||||
|
}
|
||||||
|
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
|
|
||||||
await api.writeFile(`${projectPath}/.automaker/app_spec.txt`, content);
|
await api.writeFile(`${projectPath}/.automaker/app_spec.txt`, content);
|
||||||
|
|||||||
@@ -107,7 +107,8 @@ export function useDiscoveredAgents(
|
|||||||
sources?: Array<'user' | 'project'>
|
sources?: Array<'user' | 'project'>
|
||||||
) {
|
) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: queryKeys.settings.agents(projectPath ?? ''),
|
// Include sources in query key so different source combinations have separate caches
|
||||||
|
queryKey: queryKeys.settings.agents(projectPath ?? '', sources),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
const result = await api.settings.discoverAgents(projectPath, sources);
|
const result = await api.settings.discoverAgents(projectPath, sources);
|
||||||
|
|||||||
@@ -162,7 +162,8 @@ interface BranchesResult {
|
|||||||
*/
|
*/
|
||||||
export function useWorktreeBranches(worktreePath: string | undefined, includeRemote = false) {
|
export function useWorktreeBranches(worktreePath: string | undefined, includeRemote = false) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: queryKeys.worktrees.branches(worktreePath ?? ''),
|
// Include includeRemote in query key so different configurations have separate caches
|
||||||
|
queryKey: queryKeys.worktrees.branches(worktreePath ?? '', includeRemote),
|
||||||
queryFn: async (): Promise<BranchesResult> => {
|
queryFn: async (): Promise<BranchesResult> => {
|
||||||
if (!worktreePath) throw new Error('No worktree path');
|
if (!worktreePath) throw new Error('No worktree path');
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ export const queryKeys = {
|
|||||||
single: (projectPath: string, featureId: string) =>
|
single: (projectPath: string, featureId: string) =>
|
||||||
['worktrees', projectPath, featureId] as const,
|
['worktrees', projectPath, featureId] as const,
|
||||||
/** Branches for a worktree */
|
/** Branches for a worktree */
|
||||||
branches: (worktreePath: string) => ['worktrees', 'branches', worktreePath] as const,
|
branches: (worktreePath: string, includeRemote = false) =>
|
||||||
|
['worktrees', 'branches', worktreePath, { includeRemote }] as const,
|
||||||
/** Worktree status */
|
/** Worktree status */
|
||||||
status: (projectPath: string, featureId: string) =>
|
status: (projectPath: string, featureId: string) =>
|
||||||
['worktrees', projectPath, featureId, 'status'] as const,
|
['worktrees', projectPath, featureId, 'status'] as const,
|
||||||
@@ -86,7 +87,8 @@ export const queryKeys = {
|
|||||||
/** Credentials (API keys) */
|
/** Credentials (API keys) */
|
||||||
credentials: () => ['settings', 'credentials'] as const,
|
credentials: () => ['settings', 'credentials'] as const,
|
||||||
/** Discovered agents */
|
/** Discovered agents */
|
||||||
agents: (projectPath: string) => ['settings', 'agents', projectPath] as const,
|
agents: (projectPath: string, sources?: Array<'user' | 'project'>) =>
|
||||||
|
['settings', 'agents', projectPath, sources ?? []] as const,
|
||||||
},
|
},
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
Reference in New Issue
Block a user