feat(ui): add React Query hooks for data fetching

- Add useFeatures, useFeature, useAgentOutput for feature data
- Add useGitHubIssues, useGitHubPRs, useGitHubValidations, useGitHubIssueComments
- Add useClaudeUsage, useCodexUsage with polling intervals
- Add useRunningAgents, useRunningAgentsCount
- Add useWorktrees, useWorktreeInfo, useWorktreeStatus, useWorktreeDiffs
- Add useGlobalSettings, useProjectSettings, useCredentials
- Add useAvailableModels, useCodexModels, useOpencodeModels
- Add useSessions, useSessionHistory, useSessionQueue
- Add useIdeationPrompts, useIdeas
- Add CLI status queries (claude, cursor, codex, opencode, github)
- Add useCursorPermissionsQuery, useWorkspaceDirectories
- Add usePipelineConfig, useSpecFile, useSpecRegenerationStatus

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Shirone
2026-01-15 16:20:24 +01:00
parent e57549c06e
commit 2bc931a8b0
16 changed files with 1646 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
/**
* Query Hooks Barrel Export
*
* Central export point for all React Query hooks.
* Import from this file for cleaner imports across the app.
*
* @example
* ```tsx
* import { useFeatures, useGitHubIssues, useClaudeUsage } from '@/hooks/queries';
* ```
*/
// Features
export { useFeatures, useFeature, useAgentOutput } from './use-features';
// GitHub
export {
useGitHubIssues,
useGitHubPRs,
useGitHubValidations,
useGitHubRemote,
useGitHubIssueComments,
} from './use-github';
// Usage
export { useClaudeUsage, useCodexUsage } from './use-usage';
// Running Agents
export { useRunningAgents, useRunningAgentsCount } from './use-running-agents';
// Worktrees
export {
useWorktrees,
useWorktreeInfo,
useWorktreeStatus,
useWorktreeDiffs,
useWorktreeBranches,
useWorktreeInitScript,
useAvailableEditors,
} from './use-worktrees';
// Settings
export {
useGlobalSettings,
useProjectSettings,
useSettingsStatus,
useCredentials,
useDiscoveredAgents,
} from './use-settings';
// Models
export {
useAvailableModels,
useCodexModels,
useOpencodeModels,
useOpencodeProviders,
useModelProviders,
} from './use-models';
// CLI Status
export {
useClaudeCliStatus,
useCursorCliStatus,
useCodexCliStatus,
useOpencodeCliStatus,
useGitHubCliStatus,
useApiKeysStatus,
usePlatformInfo,
} from './use-cli-status';
// Ideation
export { useIdeationPrompts, useIdeas, useIdea } from './use-ideation';
// Sessions
export { useSessions, useSessionHistory, useSessionQueue } from './use-sessions';
// Git
export { useGitDiffs } from './use-git';
// Pipeline
export { usePipelineConfig } from './use-pipeline';
// Spec
export { useSpecFile, useSpecRegenerationStatus } from './use-spec';
// Cursor Permissions
export { useCursorPermissionsQuery } from './use-cursor-permissions';
export type { CursorPermissionsData } from './use-cursor-permissions';
// Workspace
export { useWorkspaceDirectories } from './use-workspace';

View File

@@ -0,0 +1,147 @@
/**
* CLI Status Query Hooks
*
* React Query hooks for fetching CLI tool status (Claude, Cursor, Codex, etc.)
*/
import { useQuery } from '@tanstack/react-query';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
/**
* Fetch Claude CLI status
*
* @returns Query result with Claude CLI status
*/
export function useClaudeCliStatus() {
return useQuery({
queryKey: queryKeys.cli.claude(),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.setup.getClaudeStatus();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch Claude status');
}
return result;
},
staleTime: STALE_TIMES.CLI_STATUS,
});
}
/**
* Fetch Cursor CLI status
*
* @returns Query result with Cursor CLI status
*/
export function useCursorCliStatus() {
return useQuery({
queryKey: queryKeys.cli.cursor(),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.setup.getCursorStatus();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch Cursor status');
}
return result;
},
staleTime: STALE_TIMES.CLI_STATUS,
});
}
/**
* Fetch Codex CLI status
*
* @returns Query result with Codex CLI status
*/
export function useCodexCliStatus() {
return useQuery({
queryKey: queryKeys.cli.codex(),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.setup.getCodexStatus();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch Codex status');
}
return result;
},
staleTime: STALE_TIMES.CLI_STATUS,
});
}
/**
* Fetch OpenCode CLI status
*
* @returns Query result with OpenCode CLI status
*/
export function useOpencodeCliStatus() {
return useQuery({
queryKey: queryKeys.cli.opencode(),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.setup.getOpencodeStatus();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch OpenCode status');
}
return result;
},
staleTime: STALE_TIMES.CLI_STATUS,
});
}
/**
* Fetch GitHub CLI status
*
* @returns Query result with GitHub CLI status
*/
export function useGitHubCliStatus() {
return useQuery({
queryKey: queryKeys.cli.github(),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.setup.getGhStatus();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch GitHub CLI status');
}
return result;
},
staleTime: STALE_TIMES.CLI_STATUS,
});
}
/**
* Fetch API keys status
*
* @returns Query result with API keys status
*/
export function useApiKeysStatus() {
return useQuery({
queryKey: queryKeys.cli.apiKeys(),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.setup.getApiKeys();
return result;
},
staleTime: STALE_TIMES.CLI_STATUS,
});
}
/**
* Fetch platform info
*
* @returns Query result with platform info
*/
export function usePlatformInfo() {
return useQuery({
queryKey: queryKeys.cli.platform(),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.setup.getPlatform();
if (!result.success) {
throw new Error('Failed to fetch platform info');
}
return result;
},
staleTime: Infinity, // Platform info never changes
});
}

View File

@@ -0,0 +1,58 @@
/**
* Cursor Permissions Query Hooks
*
* React Query hooks for fetching Cursor CLI permissions.
*/
import { useQuery } from '@tanstack/react-query';
import { getHttpApiClient } from '@/lib/http-api-client';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
import type { CursorPermissionProfile } from '@automaker/types';
export interface CursorPermissionsData {
activeProfile: CursorPermissionProfile | null;
effectivePermissions: { allow: string[]; deny: string[] } | null;
hasProjectConfig: boolean;
availableProfiles: Array<{
id: string;
name: string;
description: string;
permissions: { allow: string[]; deny: string[] };
}>;
}
/**
* Fetch Cursor permissions for a project
*
* @param projectPath - Optional path to the project
* @param enabled - Whether to enable the query
* @returns Query result with permissions data
*
* @example
* ```tsx
* const { data: permissions, isLoading, refetch } = useCursorPermissions(projectPath);
* ```
*/
export function useCursorPermissionsQuery(projectPath?: string, enabled = true) {
return useQuery({
queryKey: queryKeys.cursorPermissions.permissions(projectPath),
queryFn: async (): Promise<CursorPermissionsData> => {
const api = getHttpApiClient();
const result = await api.setup.getCursorPermissions(projectPath);
if (!result.success) {
throw new Error(result.error || 'Failed to load permissions');
}
return {
activeProfile: result.activeProfile || null,
effectivePermissions: result.effectivePermissions || null,
hasProjectConfig: result.hasProjectConfig || false,
availableProfiles: result.availableProfiles || [],
};
},
enabled,
staleTime: STALE_TIMES.SETTINGS,
});
}

View File

@@ -0,0 +1,127 @@
/**
* Features Query Hooks
*
* React Query hooks for fetching and managing features data.
* These hooks replace manual useState/useEffect patterns with
* automatic caching, deduplication, and background refetching.
*/
import { useQuery } from '@tanstack/react-query';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
import type { Feature } from '@/store/app-store';
/**
* Fetch all features for a project
*
* @param projectPath - Path to the project
* @returns Query result with features array
*
* @example
* ```tsx
* const { data: features, isLoading, error } = useFeatures(currentProject?.path);
* ```
*/
export function useFeatures(projectPath: string | undefined) {
return useQuery({
queryKey: queryKeys.features.all(projectPath ?? ''),
queryFn: async (): Promise<Feature[]> => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
const result = await api.features?.getAll(projectPath);
if (!result?.success) {
throw new Error(result?.error || 'Failed to fetch features');
}
return (result.features ?? []) as Feature[];
},
enabled: !!projectPath,
staleTime: STALE_TIMES.FEATURES,
});
}
interface UseFeatureOptions {
enabled?: boolean;
/** Override polling interval (ms). Use false to disable polling. */
pollingInterval?: number | false;
}
/**
* Fetch a single feature by ID
*
* @param projectPath - Path to the project
* @param featureId - ID of the feature to fetch
* @param options - Query options including enabled and polling interval
* @returns Query result with single feature
*/
export function useFeature(
projectPath: string | undefined,
featureId: string | undefined,
options: UseFeatureOptions = {}
) {
const { enabled = true, pollingInterval } = options;
return useQuery({
queryKey: queryKeys.features.single(projectPath ?? '', featureId ?? ''),
queryFn: async (): Promise<Feature | null> => {
if (!projectPath || !featureId) throw new Error('Missing project path or feature ID');
const api = getElectronAPI();
const result = await api.features?.get(projectPath, featureId);
if (!result?.success) {
throw new Error(result?.error || 'Failed to fetch feature');
}
return (result.feature as Feature) ?? null;
},
enabled: !!projectPath && !!featureId && enabled,
staleTime: STALE_TIMES.FEATURES,
refetchInterval: pollingInterval,
});
}
interface UseAgentOutputOptions {
enabled?: boolean;
/** Override polling interval (ms). Use false to disable polling. */
pollingInterval?: number | false;
}
/**
* Fetch agent output for a feature
*
* @param projectPath - Path to the project
* @param featureId - ID of the feature
* @param options - Query options including enabled and polling interval
* @returns Query result with agent output string
*/
export function useAgentOutput(
projectPath: string | undefined,
featureId: string | undefined,
options: UseAgentOutputOptions = {}
) {
const { enabled = true, pollingInterval } = options;
return useQuery({
queryKey: queryKeys.features.agentOutput(projectPath ?? '', featureId ?? ''),
queryFn: async (): Promise<string> => {
if (!projectPath || !featureId) throw new Error('Missing project path or feature ID');
const api = getElectronAPI();
const result = await api.features?.getAgentOutput(projectPath, featureId);
if (!result?.success) {
throw new Error(result?.error || 'Failed to fetch agent output');
}
return result.content ?? '';
},
enabled: !!projectPath && !!featureId && enabled,
staleTime: STALE_TIMES.AGENT_OUTPUT,
// Use provided polling interval or default behavior
refetchInterval:
pollingInterval !== undefined
? pollingInterval
: (query) => {
// Only poll if we have data and it's not empty (indicating active task)
if (query.state.data && query.state.data.length > 0) {
return 5000; // 5 seconds
}
return false;
},
});
}

View File

@@ -0,0 +1,37 @@
/**
* Git Query Hooks
*
* React Query hooks for git operations.
*/
import { useQuery } from '@tanstack/react-query';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
/**
* Fetch git diffs for a project (main project, not worktree)
*
* @param projectPath - Path to the project
* @param enabled - Whether to enable the query
* @returns Query result with files and diff content
*/
export function useGitDiffs(projectPath: string | undefined, enabled = true) {
return useQuery({
queryKey: queryKeys.git.diffs(projectPath ?? ''),
queryFn: async () => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
const result = await api.git.getDiffs(projectPath);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch diffs');
}
return {
files: result.files ?? [],
diff: result.diff ?? '',
};
},
enabled: !!projectPath && enabled,
staleTime: STALE_TIMES.WORKTREES,
});
}

View File

@@ -0,0 +1,184 @@
/**
* GitHub Query Hooks
*
* React Query hooks for fetching GitHub issues, PRs, and validations.
*/
import { useQuery, useInfiniteQuery } from '@tanstack/react-query';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
import type { GitHubIssue, GitHubPR, GitHubComment, IssueValidation } from '@/lib/electron';
interface GitHubIssuesResult {
openIssues: GitHubIssue[];
closedIssues: GitHubIssue[];
}
interface GitHubPRsResult {
openPRs: GitHubPR[];
mergedPRs: GitHubPR[];
}
/**
* Fetch GitHub issues for a project
*
* @param projectPath - Path to the project
* @returns Query result with open and closed issues
*
* @example
* ```tsx
* const { data, isLoading } = useGitHubIssues(currentProject?.path);
* const { openIssues, closedIssues } = data ?? { openIssues: [], closedIssues: [] };
* ```
*/
export function useGitHubIssues(projectPath: string | undefined) {
return useQuery({
queryKey: queryKeys.github.issues(projectPath ?? ''),
queryFn: async (): Promise<GitHubIssuesResult> => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
const result = await api.github.listIssues(projectPath);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch issues');
}
return {
openIssues: result.openIssues ?? [],
closedIssues: result.closedIssues ?? [],
};
},
enabled: !!projectPath,
staleTime: STALE_TIMES.GITHUB,
});
}
/**
* Fetch GitHub PRs for a project
*
* @param projectPath - Path to the project
* @returns Query result with open and merged PRs
*/
export function useGitHubPRs(projectPath: string | undefined) {
return useQuery({
queryKey: queryKeys.github.prs(projectPath ?? ''),
queryFn: async (): Promise<GitHubPRsResult> => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
const result = await api.github.listPRs(projectPath);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch PRs');
}
return {
openPRs: result.openPRs ?? [],
mergedPRs: result.mergedPRs ?? [],
};
},
enabled: !!projectPath,
staleTime: STALE_TIMES.GITHUB,
});
}
/**
* Fetch GitHub validations for a project
*
* @param projectPath - Path to the project
* @param issueNumber - Optional issue number to filter by
* @returns Query result with validations
*/
export function useGitHubValidations(projectPath: string | undefined, issueNumber?: number) {
return useQuery({
queryKey: issueNumber
? queryKeys.github.validation(projectPath ?? '', issueNumber)
: queryKeys.github.validations(projectPath ?? ''),
queryFn: async (): Promise<IssueValidation[]> => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
const result = await api.github.getValidations(projectPath, issueNumber);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch validations');
}
return result.validations ?? [];
},
enabled: !!projectPath,
staleTime: STALE_TIMES.GITHUB,
});
}
/**
* Check GitHub remote for a project
*
* @param projectPath - Path to the project
* @returns Query result with remote info
*/
export function useGitHubRemote(projectPath: string | undefined) {
return useQuery({
queryKey: queryKeys.github.remote(projectPath ?? ''),
queryFn: async () => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
const result = await api.github.checkRemote(projectPath);
if (!result.success) {
throw new Error(result.error || 'Failed to check remote');
}
return {
hasRemote: result.hasRemote ?? false,
owner: result.owner,
repo: result.repo,
url: result.url,
};
},
enabled: !!projectPath,
staleTime: STALE_TIMES.GITHUB,
});
}
/**
* Fetch comments for a GitHub issue with pagination support
*
* Uses useInfiniteQuery for proper "load more" pagination.
*
* @param projectPath - Path to the project
* @param issueNumber - Issue number
* @returns Infinite query result with comments and pagination helpers
*
* @example
* ```tsx
* const {
* data,
* isLoading,
* isFetchingNextPage,
* hasNextPage,
* fetchNextPage,
* refetch,
* } = useGitHubIssueComments(projectPath, issueNumber);
*
* // Get all comments flattened
* const comments = data?.pages.flatMap(page => page.comments) ?? [];
* ```
*/
export function useGitHubIssueComments(
projectPath: string | undefined,
issueNumber: number | undefined
) {
return useInfiniteQuery({
queryKey: queryKeys.github.issueComments(projectPath ?? '', issueNumber ?? 0),
queryFn: async ({ pageParam }: { pageParam: string | undefined }) => {
if (!projectPath || !issueNumber) throw new Error('Missing project path or issue number');
const api = getElectronAPI();
const result = await api.github.getIssueComments(projectPath, issueNumber, pageParam);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch comments');
}
return {
comments: (result.comments ?? []) as GitHubComment[],
totalCount: result.totalCount ?? 0,
hasNextPage: result.hasNextPage ?? false,
endCursor: result.endCursor as string | undefined,
};
},
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => (lastPage.hasNextPage ? lastPage.endCursor : undefined),
enabled: !!projectPath && !!issueNumber,
staleTime: STALE_TIMES.GITHUB,
});
}

View File

@@ -0,0 +1,86 @@
/**
* Ideation Query Hooks
*
* React Query hooks for fetching ideation prompts and ideas.
*/
import { useQuery } from '@tanstack/react-query';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
/**
* Fetch ideation prompts
*
* @returns Query result with prompts and categories
*
* @example
* ```tsx
* const { data, isLoading, error } = useIdeationPrompts();
* const { prompts, categories } = data ?? { prompts: [], categories: [] };
* ```
*/
export function useIdeationPrompts() {
return useQuery({
queryKey: queryKeys.ideation.prompts(),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.ideation?.getPrompts();
if (!result?.success) {
throw new Error(result?.error || 'Failed to fetch prompts');
}
return {
prompts: result.prompts ?? [],
categories: result.categories ?? [],
};
},
staleTime: STALE_TIMES.SETTINGS, // Prompts rarely change
});
}
/**
* Fetch ideas for a project
*
* @param projectPath - Path to the project
* @returns Query result with ideas array
*/
export function useIdeas(projectPath: string | undefined) {
return useQuery({
queryKey: queryKeys.ideation.ideas(projectPath ?? ''),
queryFn: async () => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
const result = await api.ideation?.listIdeas(projectPath);
if (!result?.success) {
throw new Error(result?.error || 'Failed to fetch ideas');
}
return result.ideas ?? [];
},
enabled: !!projectPath,
staleTime: STALE_TIMES.FEATURES,
});
}
/**
* Fetch a single idea by ID
*
* @param projectPath - Path to the project
* @param ideaId - ID of the idea
* @returns Query result with single idea
*/
export function useIdea(projectPath: string | undefined, ideaId: string | undefined) {
return useQuery({
queryKey: queryKeys.ideation.idea(projectPath ?? '', ideaId ?? ''),
queryFn: async () => {
if (!projectPath || !ideaId) throw new Error('Missing project path or idea ID');
const api = getElectronAPI();
const result = await api.ideation?.getIdea(projectPath, ideaId);
if (!result?.success) {
throw new Error(result?.error || 'Failed to fetch idea');
}
return result.idea;
},
enabled: !!projectPath && !!ideaId,
staleTime: STALE_TIMES.FEATURES,
});
}

View File

@@ -0,0 +1,134 @@
/**
* Models Query Hooks
*
* React Query hooks for fetching available AI models.
*/
import { useQuery } from '@tanstack/react-query';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
interface CodexModel {
id: string;
label: string;
description: string;
hasThinking: boolean;
supportsVision: boolean;
tier: 'premium' | 'standard' | 'basic';
isDefault: boolean;
}
interface OpencodeModel {
id: string;
name: string;
modelString: string;
provider: string;
description: string;
supportsTools: boolean;
supportsVision: boolean;
tier: string;
default?: boolean;
}
/**
* Fetch available models
*
* @returns Query result with available models
*/
export function useAvailableModels() {
return useQuery({
queryKey: queryKeys.models.available(),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.model.getAvailable();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch available models');
}
return result.models ?? [];
},
staleTime: STALE_TIMES.MODELS,
});
}
/**
* Fetch Codex models
*
* @param refresh - Force refresh from server
* @returns Query result with Codex models
*/
export function useCodexModels(refresh = false) {
return useQuery({
queryKey: queryKeys.models.codex(),
queryFn: async (): Promise<CodexModel[]> => {
const api = getElectronAPI();
const result = await api.codex.getModels(refresh);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch Codex models');
}
return (result.models ?? []) as CodexModel[];
},
staleTime: STALE_TIMES.MODELS,
});
}
/**
* Fetch OpenCode models
*
* @param refresh - Force refresh from server
* @returns Query result with OpenCode models
*/
export function useOpencodeModels(refresh = false) {
return useQuery({
queryKey: queryKeys.models.opencode(),
queryFn: async (): Promise<OpencodeModel[]> => {
const api = getElectronAPI();
const result = await api.setup.getOpencodeModels(refresh);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch OpenCode models');
}
return (result.models ?? []) as OpencodeModel[];
},
staleTime: STALE_TIMES.MODELS,
});
}
/**
* Fetch OpenCode providers
*
* @returns Query result with OpenCode providers
*/
export function useOpencodeProviders() {
return useQuery({
queryKey: queryKeys.models.opencodeProviders(),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.setup.getOpencodeProviders();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch OpenCode providers');
}
return result.providers ?? [];
},
staleTime: STALE_TIMES.MODELS,
});
}
/**
* Fetch model providers status
*
* @returns Query result with provider status
*/
export function useModelProviders() {
return useQuery({
queryKey: queryKeys.models.providers(),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.model.checkProviders();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch providers');
}
return result.providers ?? {};
},
staleTime: STALE_TIMES.MODELS,
});
}

View File

@@ -0,0 +1,39 @@
/**
* Pipeline Query Hooks
*
* React Query hooks for fetching pipeline configuration.
*/
import { useQuery } from '@tanstack/react-query';
import { getHttpApiClient } from '@/lib/http-api-client';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
import type { PipelineConfig } from '@/store/app-store';
/**
* Fetch pipeline config for a project
*
* @param projectPath - Path to the project
* @returns Query result with pipeline config
*
* @example
* ```tsx
* const { data: pipelineConfig, isLoading } = usePipelineConfig(currentProject?.path);
* ```
*/
export function usePipelineConfig(projectPath: string | undefined) {
return useQuery({
queryKey: queryKeys.pipeline.config(projectPath ?? ''),
queryFn: async (): Promise<PipelineConfig | null> => {
if (!projectPath) throw new Error('No project path');
const api = getHttpApiClient();
const result = await api.pipeline.getConfig(projectPath);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch pipeline config');
}
return result.config ?? null;
},
enabled: !!projectPath,
staleTime: STALE_TIMES.SETTINGS,
});
}

View File

@@ -0,0 +1,61 @@
/**
* Running Agents Query Hook
*
* React Query hook for fetching currently running agents.
* This data is invalidated by WebSocket events when agents start/stop.
*/
import { useQuery } from '@tanstack/react-query';
import { getElectronAPI, type RunningAgent } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
interface RunningAgentsResult {
agents: RunningAgent[];
count: number;
}
/**
* Fetch all currently running agents
*
* @returns Query result with running agents and total count
*
* @example
* ```tsx
* const { data, isLoading } = useRunningAgents();
* const { agents, count } = data ?? { agents: [], count: 0 };
* ```
*/
export function useRunningAgents() {
return useQuery({
queryKey: queryKeys.runningAgents.all(),
queryFn: async (): Promise<RunningAgentsResult> => {
const api = getElectronAPI();
const result = await api.runningAgents.getAll();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch running agents');
}
return {
agents: result.runningAgents ?? [],
count: result.totalCount ?? 0,
};
},
staleTime: STALE_TIMES.RUNNING_AGENTS,
// Note: Don't use refetchInterval here - rely on WebSocket invalidation
// for real-time updates instead of polling
});
}
/**
* Get running agents count
* This is a selector that derives count from the main query
*
* @returns Query result with just the count
*/
export function useRunningAgentsCount() {
const query = useRunningAgents();
return {
...query,
data: query.data?.count ?? 0,
};
}

View File

@@ -0,0 +1,86 @@
/**
* Sessions Query Hooks
*
* React Query hooks for fetching session data.
*/
import { useQuery } from '@tanstack/react-query';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
import type { SessionListItem } from '@/types/electron';
/**
* Fetch all sessions
*
* @param includeArchived - Whether to include archived sessions
* @returns Query result with sessions array
*
* @example
* ```tsx
* const { data: sessions, isLoading } = useSessions(false);
* ```
*/
export function useSessions(includeArchived = false) {
return useQuery({
queryKey: queryKeys.sessions.all(includeArchived),
queryFn: async (): Promise<SessionListItem[]> => {
const api = getElectronAPI();
const result = await api.sessions.list(includeArchived);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch sessions');
}
return result.sessions ?? [];
},
staleTime: STALE_TIMES.SESSIONS,
});
}
/**
* Fetch session history
*
* @param sessionId - ID of the session
* @returns Query result with session messages
*/
export function useSessionHistory(sessionId: string | undefined) {
return useQuery({
queryKey: queryKeys.sessions.history(sessionId ?? ''),
queryFn: async () => {
if (!sessionId) throw new Error('No session ID');
const api = getElectronAPI();
const result = await api.agent.getHistory(sessionId);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch session history');
}
return {
messages: result.messages ?? [],
isRunning: result.isRunning ?? false,
};
},
enabled: !!sessionId,
staleTime: STALE_TIMES.FEATURES, // Session history changes during conversations
});
}
/**
* Fetch session message queue
*
* @param sessionId - ID of the session
* @returns Query result with queued messages
*/
export function useSessionQueue(sessionId: string | undefined) {
return useQuery({
queryKey: queryKeys.sessions.queue(sessionId ?? ''),
queryFn: async () => {
if (!sessionId) throw new Error('No session ID');
const api = getElectronAPI();
const result = await api.agent.queueList(sessionId);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch queue');
}
return result.queue ?? [];
},
enabled: !!sessionId,
staleTime: STALE_TIMES.RUNNING_AGENTS, // Queue changes frequently during use
});
}

View File

@@ -0,0 +1,122 @@
/**
* Settings Query Hooks
*
* React Query hooks for fetching global and project settings.
*/
import { useQuery } from '@tanstack/react-query';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
import type { GlobalSettings, ProjectSettings } from '@automaker/types';
/**
* Fetch global settings
*
* @returns Query result with global settings
*
* @example
* ```tsx
* const { data: settings, isLoading } = useGlobalSettings();
* ```
*/
export function useGlobalSettings() {
return useQuery({
queryKey: queryKeys.settings.global(),
queryFn: async (): Promise<GlobalSettings> => {
const api = getElectronAPI();
const result = await api.settings.getGlobal();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch global settings');
}
return result.settings as GlobalSettings;
},
staleTime: STALE_TIMES.SETTINGS,
});
}
/**
* Fetch project-specific settings
*
* @param projectPath - Path to the project
* @returns Query result with project settings
*/
export function useProjectSettings(projectPath: string | undefined) {
return useQuery({
queryKey: queryKeys.settings.project(projectPath ?? ''),
queryFn: async (): Promise<ProjectSettings> => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
const result = await api.settings.getProject(projectPath);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch project settings');
}
return result.settings as ProjectSettings;
},
enabled: !!projectPath,
staleTime: STALE_TIMES.SETTINGS,
});
}
/**
* Fetch settings status (migration status, etc.)
*
* @returns Query result with settings status
*/
export function useSettingsStatus() {
return useQuery({
queryKey: queryKeys.settings.status(),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.settings.getStatus();
return result;
},
staleTime: STALE_TIMES.SETTINGS,
});
}
/**
* Fetch credentials status (masked API keys)
*
* @returns Query result with credentials info
*/
export function useCredentials() {
return useQuery({
queryKey: queryKeys.settings.credentials(),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.settings.getCredentials();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch credentials');
}
return result.credentials;
},
staleTime: STALE_TIMES.SETTINGS,
});
}
/**
* Discover agents for a project
*
* @param projectPath - Path to the project
* @param sources - Sources to search ('user' | 'project')
* @returns Query result with discovered agents
*/
export function useDiscoveredAgents(
projectPath: string | undefined,
sources?: Array<'user' | 'project'>
) {
return useQuery({
queryKey: queryKeys.settings.agents(projectPath ?? ''),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.settings.discoverAgents(projectPath, sources);
if (!result.success) {
throw new Error(result.error || 'Failed to discover agents');
}
return result.agents ?? [];
},
enabled: !!projectPath,
staleTime: STALE_TIMES.SETTINGS,
});
}

View File

@@ -0,0 +1,103 @@
/**
* Spec Query Hooks
*
* React Query hooks for fetching spec file content and regeneration status.
*/
import { useQuery } from '@tanstack/react-query';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
interface SpecFileResult {
content: string;
exists: boolean;
}
interface SpecRegenerationStatusResult {
isRunning: boolean;
currentPhase?: string;
}
/**
* Fetch spec file content for a project
*
* @param projectPath - Path to the project
* @returns Query result with spec content and existence flag
*
* @example
* ```tsx
* const { data, isLoading } = useSpecFile(currentProject?.path);
* if (data?.exists) {
* console.log(data.content);
* }
* ```
*/
export function useSpecFile(projectPath: string | undefined) {
return useQuery({
queryKey: queryKeys.spec.file(projectPath ?? ''),
queryFn: async (): Promise<SpecFileResult> => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
const result = await api.readFile(`${projectPath}/.automaker/app_spec.txt`);
if (result.success && result.content) {
return {
content: result.content,
exists: true,
};
}
return {
content: '',
exists: false,
};
},
enabled: !!projectPath,
staleTime: STALE_TIMES.SETTINGS,
});
}
/**
* Check spec regeneration status for a project
*
* @param projectPath - Path to the project
* @param enabled - Whether to enable the query (useful during regeneration)
* @returns Query result with regeneration status
*
* @example
* ```tsx
* const { data } = useSpecRegenerationStatus(projectPath, isRegenerating);
* if (data?.isRunning) {
* // Show loading indicator
* }
* ```
*/
export function useSpecRegenerationStatus(projectPath: string | undefined, enabled = true) {
return useQuery({
queryKey: queryKeys.specRegeneration.status(projectPath ?? ''),
queryFn: async (): Promise<SpecRegenerationStatusResult> => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
if (!api.specRegeneration) {
return { isRunning: false };
}
const status = await api.specRegeneration.status(projectPath);
if (status.success) {
return {
isRunning: status.isRunning ?? false,
currentPhase: status.currentPhase,
};
}
return { isRunning: false };
},
enabled: !!projectPath && enabled,
staleTime: 5000, // Check every 5 seconds when active
refetchInterval: enabled ? 5000 : false,
});
}

View File

@@ -0,0 +1,77 @@
/**
* Usage Query Hooks
*
* React Query hooks for fetching Claude and Codex API usage data.
* These hooks include automatic polling for real-time usage updates.
*/
import { useQuery } from '@tanstack/react-query';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
import type { ClaudeUsage, CodexUsage } from '@/store/app-store';
/** Polling interval for usage data (60 seconds) */
const USAGE_POLLING_INTERVAL = 60 * 1000;
/**
* Fetch Claude API usage data
*
* @param enabled - Whether the query should run (default: true)
* @returns Query result with Claude usage data
*
* @example
* ```tsx
* const { data: usage, isLoading } = useClaudeUsage(isPopoverOpen);
* ```
*/
export function useClaudeUsage(enabled = true) {
return useQuery({
queryKey: queryKeys.usage.claude(),
queryFn: async (): Promise<ClaudeUsage> => {
const api = getElectronAPI();
const result = await api.claude.getUsage();
// Check if result is an error response
if ('error' in result) {
throw new Error(result.message || result.error);
}
return result;
},
enabled,
staleTime: STALE_TIMES.USAGE,
refetchInterval: enabled ? USAGE_POLLING_INTERVAL : false,
// Keep previous data while refetching
placeholderData: (previousData) => previousData,
});
}
/**
* Fetch Codex API usage data
*
* @param enabled - Whether the query should run (default: true)
* @returns Query result with Codex usage data
*
* @example
* ```tsx
* const { data: usage, isLoading } = useCodexUsage(isPopoverOpen);
* ```
*/
export function useCodexUsage(enabled = true) {
return useQuery({
queryKey: queryKeys.usage.codex(),
queryFn: async (): Promise<CodexUsage> => {
const api = getElectronAPI();
const result = await api.codex.getUsage();
// Check if result is an error response
if ('error' in result) {
throw new Error(result.message || result.error);
}
return result;
},
enabled,
staleTime: STALE_TIMES.USAGE,
refetchInterval: enabled ? USAGE_POLLING_INTERVAL : false,
// Keep previous data while refetching
placeholderData: (previousData) => previousData,
});
}

View File

@@ -0,0 +1,42 @@
/**
* Workspace Query Hooks
*
* React Query hooks for workspace operations.
*/
import { useQuery } from '@tanstack/react-query';
import { getHttpApiClient } from '@/lib/http-api-client';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
interface WorkspaceDirectory {
name: string;
path: string;
}
/**
* Fetch workspace directories
*
* @param enabled - Whether to enable the query
* @returns Query result with directories
*
* @example
* ```tsx
* const { data: directories, isLoading, error } = useWorkspaceDirectories(open);
* ```
*/
export function useWorkspaceDirectories(enabled = true) {
return useQuery({
queryKey: queryKeys.workspace.directories(),
queryFn: async (): Promise<WorkspaceDirectory[]> => {
const api = getHttpApiClient();
const result = await api.workspace.getDirectories();
if (!result.success) {
throw new Error(result.error || 'Failed to load directories');
}
return result.directories ?? [];
},
enabled,
staleTime: STALE_TIMES.SETTINGS,
});
}

View File

@@ -0,0 +1,252 @@
/**
* Worktrees Query Hooks
*
* React Query hooks for fetching worktree data.
*/
import { useQuery } from '@tanstack/react-query';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
import { STALE_TIMES } from '@/lib/query-client';
interface WorktreeInfo {
path: string;
branch: string;
isMain: boolean;
hasChanges?: boolean;
changedFilesCount?: number;
featureId?: string;
linkedToBranch?: string;
}
interface RemovedWorktree {
path: string;
branch: string;
}
interface WorktreesResult {
worktrees: WorktreeInfo[];
removedWorktrees: RemovedWorktree[];
}
/**
* Fetch all worktrees for a project
*
* @param projectPath - Path to the project
* @param includeDetails - Whether to include detailed info (default: true)
* @returns Query result with worktrees array and removed worktrees
*
* @example
* ```tsx
* const { data, isLoading, refetch } = useWorktrees(currentProject?.path);
* const worktrees = data?.worktrees ?? [];
* ```
*/
export function useWorktrees(projectPath: string | undefined, includeDetails = true) {
return useQuery({
queryKey: queryKeys.worktrees.all(projectPath ?? ''),
queryFn: async (): Promise<WorktreesResult> => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
const result = await api.worktree.listAll(projectPath, includeDetails);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch worktrees');
}
return {
worktrees: result.worktrees ?? [],
removedWorktrees: result.removedWorktrees ?? [],
};
},
enabled: !!projectPath,
staleTime: STALE_TIMES.WORKTREES,
});
}
/**
* Fetch worktree info for a specific feature
*
* @param projectPath - Path to the project
* @param featureId - ID of the feature
* @returns Query result with worktree info
*/
export function useWorktreeInfo(projectPath: string | undefined, featureId: string | undefined) {
return useQuery({
queryKey: queryKeys.worktrees.single(projectPath ?? '', featureId ?? ''),
queryFn: async () => {
if (!projectPath || !featureId) throw new Error('Missing project path or feature ID');
const api = getElectronAPI();
const result = await api.worktree.getInfo(projectPath, featureId);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch worktree info');
}
return result;
},
enabled: !!projectPath && !!featureId,
staleTime: STALE_TIMES.WORKTREES,
});
}
/**
* Fetch worktree status for a specific feature
*
* @param projectPath - Path to the project
* @param featureId - ID of the feature
* @returns Query result with worktree status
*/
export function useWorktreeStatus(projectPath: string | undefined, featureId: string | undefined) {
return useQuery({
queryKey: queryKeys.worktrees.status(projectPath ?? '', featureId ?? ''),
queryFn: async () => {
if (!projectPath || !featureId) throw new Error('Missing project path or feature ID');
const api = getElectronAPI();
const result = await api.worktree.getStatus(projectPath, featureId);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch worktree status');
}
return result;
},
enabled: !!projectPath && !!featureId,
staleTime: STALE_TIMES.WORKTREES,
});
}
/**
* Fetch worktree diffs for a specific feature
*
* @param projectPath - Path to the project
* @param featureId - ID of the feature
* @returns Query result with files and diff content
*/
export function useWorktreeDiffs(projectPath: string | undefined, featureId: string | undefined) {
return useQuery({
queryKey: queryKeys.worktrees.diffs(projectPath ?? '', featureId ?? ''),
queryFn: async () => {
if (!projectPath || !featureId) throw new Error('Missing project path or feature ID');
const api = getElectronAPI();
const result = await api.worktree.getDiffs(projectPath, featureId);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch diffs');
}
return {
files: result.files ?? [],
diff: result.diff ?? '',
};
},
enabled: !!projectPath && !!featureId,
staleTime: STALE_TIMES.WORKTREES,
});
}
interface BranchInfo {
name: string;
isCurrent: boolean;
isRemote?: boolean;
lastCommit?: string;
upstream?: string;
}
interface BranchesResult {
branches: BranchInfo[];
aheadCount: number;
behindCount: number;
isGitRepo: boolean;
hasCommits: boolean;
}
/**
* Fetch available branches for a worktree
*
* @param worktreePath - Path to the worktree
* @param includeRemote - Whether to include remote branches
* @returns Query result with branches, ahead/behind counts, and git repo status
*/
export function useWorktreeBranches(worktreePath: string | undefined, includeRemote = false) {
return useQuery({
queryKey: queryKeys.worktrees.branches(worktreePath ?? ''),
queryFn: async (): Promise<BranchesResult> => {
if (!worktreePath) throw new Error('No worktree path');
const api = getElectronAPI();
const result = await api.worktree.listBranches(worktreePath, includeRemote);
// Handle special git status codes
if (result.code === 'NOT_GIT_REPO') {
return {
branches: [],
aheadCount: 0,
behindCount: 0,
isGitRepo: false,
hasCommits: false,
};
}
if (result.code === 'NO_COMMITS') {
return {
branches: [],
aheadCount: 0,
behindCount: 0,
isGitRepo: true,
hasCommits: false,
};
}
if (!result.success) {
throw new Error(result.error || 'Failed to fetch branches');
}
return {
branches: result.result?.branches ?? [],
aheadCount: result.result?.aheadCount ?? 0,
behindCount: result.result?.behindCount ?? 0,
isGitRepo: true,
hasCommits: true,
};
},
enabled: !!worktreePath,
staleTime: STALE_TIMES.WORKTREES,
});
}
/**
* Fetch init script for a project
*
* @param projectPath - Path to the project
* @returns Query result with init script content
*/
export function useWorktreeInitScript(projectPath: string | undefined) {
return useQuery({
queryKey: queryKeys.worktrees.initScript(projectPath ?? ''),
queryFn: async () => {
if (!projectPath) throw new Error('No project path');
const api = getElectronAPI();
const result = await api.worktree.getInitScript(projectPath);
if (!result.success) {
throw new Error(result.error || 'Failed to fetch init script');
}
return {
exists: result.exists ?? false,
content: result.content ?? '',
};
},
enabled: !!projectPath,
staleTime: STALE_TIMES.SETTINGS,
});
}
/**
* Fetch available editors
*
* @returns Query result with available editors
*/
export function useAvailableEditors() {
return useQuery({
queryKey: queryKeys.worktrees.editors(),
queryFn: async () => {
const api = getElectronAPI();
const result = await api.worktree.getAvailableEditors();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch editors');
}
return result.editors ?? [];
},
staleTime: STALE_TIMES.CLI_STATUS,
});
}