Louis/hooks (#599)

* Update useDevServer to use tanstack (vibe-kanban a5e7cb20)

We are migrating the frontend to use Tanstack query. We should make radical changes in order to improve readability and performance.

See an example in frontend/src/hooks/useRebase.ts

Please plan how to modernise the frontend/src/hooks/useDevServer.ts hook

* Deprecate useExecutionProcesses → Replace with useAttemptExecution (vibe-kanban 5123ff1e)
This commit is contained in:
Louis Knight-Webb
2025-09-01 17:17:48 +01:00
committed by GitHub
parent 13fceaf77e
commit a10d3f0854
6 changed files with 79 additions and 79 deletions

View File

@@ -104,7 +104,9 @@ export function AttemptHeaderCard({
Open in IDE Open in IDE
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={runningDevServer ? stopDevServer : startDevServer} onClick={() =>
runningDevServer ? stopDevServer() : startDevServer()
}
disabled={!selectedAttempt} disabled={!selectedAttempt}
className={runningDevServer ? 'text-destructive' : ''} className={runningDevServer ? 'text-destructive' : ''}
> >

View File

@@ -512,7 +512,9 @@ function CurrentAttempt({
<Button <Button
variant={runningDevServer ? 'destructive' : 'outline'} variant={runningDevServer ? 'destructive' : 'outline'}
size="xs" size="xs"
onClick={runningDevServer ? stopDevServer : startDevServer} onClick={() =>
runningDevServer ? stopDevServer() : startDevServer()
}
disabled={isStartingDevServer || !projectHasDevScript} disabled={isStartingDevServer || !projectHasDevScript}
className="gap-1 flex-1" className="gap-1 flex-1"
> >

View File

@@ -1,4 +1,3 @@
export { useExecutionProcesses } from './useExecutionProcesses';
export { useBranchStatus } from './useBranchStatus'; export { useBranchStatus } from './useBranchStatus';
export { useAttemptExecution } from './useAttemptExecution'; export { useAttemptExecution } from './useAttemptExecution';
export { useOpenInEditor } from './useOpenInEditor'; export { useOpenInEditor } from './useOpenInEditor';

View File

@@ -1,30 +1,39 @@
import { useCallback } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { attemptsApi } from '@/lib/api'; import { attemptsApi, type Result } from '@/lib/api';
import type { CreateGitHubPrRequest } from 'shared/types'; import type { CreateGitHubPrRequest, GitHubServiceError } from 'shared/types';
export function useCreatePR( export function useCreatePR(
attemptId: string | undefined, attemptId: string | undefined,
onSuccess?: (prUrl?: string) => void, onSuccess?: (prUrl?: string) => void,
onError?: (err: unknown) => void onError?: (err: unknown) => void
) { ) {
return useCallback( const queryClient = useQueryClient();
async (prData: CreateGitHubPrRequest) => {
if (!attemptId) return;
try { return useMutation<
const result = await attemptsApi.createPR(attemptId, prData); Result<string, GitHubServiceError>,
Error,
if (result.success) { CreateGitHubPrRequest
onSuccess?.(result.data); >({
return result.data; mutationFn: async (prData: CreateGitHubPrRequest) => {
} else { if (!attemptId)
throw result.error || new Error(result.message); return { success: false, error: undefined, message: 'No attempt ID' };
} return attemptsApi.createPR(attemptId, prData);
} catch (err) { },
console.error('Failed to create PR:', err); onSuccess: (result) => {
onError?.(err); if (result.success) {
queryClient.invalidateQueries({
queryKey: ['branchStatus', attemptId],
});
onSuccess?.(result.data);
} else {
throw (
result.error || new Error(result.message || 'Failed to create PR')
);
} }
}, },
[attemptId, onSuccess, onError] onError: (err) => {
); console.error('Failed to create PR:', err);
onError?.(err);
},
});
} }

View File

@@ -1,4 +1,5 @@
import { useCallback, useMemo, useState } from 'react'; import { useMemo } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { attemptsApi, executionProcessesApi } from '@/lib/api'; import { attemptsApi, executionProcessesApi } from '@/lib/api';
import { useAttemptExecution } from '@/hooks/useAttemptExecution'; import { useAttemptExecution } from '@/hooks/useAttemptExecution';
import type { ExecutionProcess } from 'shared/types'; import type { ExecutionProcess } from 'shared/types';
@@ -14,12 +15,11 @@ export function useDevServer(
attemptId: string | undefined, attemptId: string | undefined,
options?: UseDevServerOptions options?: UseDevServerOptions
) { ) {
const queryClient = useQueryClient();
const { attemptData } = useAttemptExecution(attemptId); const { attemptData } = useAttemptExecution(attemptId);
const [isStarting, setIsStarting] = useState(false);
const [isStopping, setIsStopping] = useState(false);
// Find running dev server process // Find running dev server process
const runningDevServer = useMemo((): ExecutionProcess | undefined => { const runningDevServer = useMemo<ExecutionProcess | undefined>(() => {
return attemptData.processes.find( return attemptData.processes.find(
(process) => (process) =>
process.run_reason === 'devserver' && process.status === 'running' process.run_reason === 'devserver' && process.status === 'running'
@@ -27,7 +27,7 @@ export function useDevServer(
}, [attemptData.processes]); }, [attemptData.processes]);
// Find latest dev server process (for logs viewing) // Find latest dev server process (for logs viewing)
const latestDevServerProcess = useMemo((): ExecutionProcess | undefined => { const latestDevServerProcess = useMemo<ExecutionProcess | undefined>(() => {
return [...attemptData.processes] return [...attemptData.processes]
.filter((process) => process.run_reason === 'devserver') .filter((process) => process.run_reason === 'devserver')
.sort( .sort(
@@ -36,41 +36,56 @@ export function useDevServer(
)[0]; )[0];
}, [attemptData.processes]); }, [attemptData.processes]);
const start = useCallback(async () => { // Start mutation
if (!attemptId) return; const startMutation = useMutation({
mutationKey: ['startDevServer', attemptId],
setIsStarting(true); mutationFn: async () => {
try { if (!attemptId) return;
await attemptsApi.startDevServer(attemptId); await attemptsApi.startDevServer(attemptId);
},
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: ['executionProcesses', attemptId],
});
options?.onStartSuccess?.(); options?.onStartSuccess?.();
} catch (err) { },
onError: (err) => {
console.error('Failed to start dev server:', err); console.error('Failed to start dev server:', err);
options?.onStartError?.(err); options?.onStartError?.(err);
} finally { },
setIsStarting(false); });
}
}, [attemptId, options?.onStartSuccess, options?.onStartError]);
const stop = useCallback(async () => { // Stop mutation
if (!runningDevServer) return; const stopMutation = useMutation({
mutationKey: ['stopDevServer', runningDevServer?.id],
setIsStopping(true); mutationFn: async () => {
try { if (!runningDevServer) return;
await executionProcessesApi.stopExecutionProcess(runningDevServer.id); await executionProcessesApi.stopExecutionProcess(runningDevServer.id);
},
onSuccess: async () => {
await Promise.all([
queryClient.invalidateQueries({
queryKey: ['executionProcesses', attemptId],
}),
runningDevServer
? queryClient.invalidateQueries({
queryKey: ['processDetails', runningDevServer.id],
})
: Promise.resolve(),
]);
options?.onStopSuccess?.(); options?.onStopSuccess?.();
} catch (err) { },
onError: (err) => {
console.error('Failed to stop dev server:', err); console.error('Failed to stop dev server:', err);
options?.onStopError?.(err); options?.onStopError?.(err);
} finally { },
setIsStopping(false); });
}
}, [runningDevServer, options?.onStopSuccess, options?.onStopError]);
return { return {
start, start: startMutation.mutate,
stop, stop: stopMutation.mutate,
isStarting, isStarting: startMutation.isPending,
isStopping, isStopping: stopMutation.isPending,
runningDevServer, runningDevServer,
latestDevServerProcess, latestDevServerProcess,
}; };

View File

@@ -1,27 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { executionProcessesApi } from '@/lib/api';
import type { ExecutionProcess } from 'shared/types';
export function useExecutionProcesses(attemptId?: string) {
const query = useQuery({
queryKey: ['executionProcesses', attemptId],
queryFn: () => executionProcessesApi.getExecutionProcesses(attemptId!),
enabled: !!attemptId,
refetchInterval: () => {
// Always poll every 5 seconds when enabled - we'll control this via enabled
return 5000;
},
select: (data) => ({
processes: data,
isAttemptRunning: data.some(
(process: ExecutionProcess) =>
(process.run_reason === 'codingagent' ||
process.run_reason === 'setupscript' ||
process.run_reason === 'cleanupscript') &&
process.status === 'running'
),
}),
});
return query;
}