mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
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:
91
apps/ui/src/hooks/queries/index.ts
Normal file
91
apps/ui/src/hooks/queries/index.ts
Normal 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';
|
||||
147
apps/ui/src/hooks/queries/use-cli-status.ts
Normal file
147
apps/ui/src/hooks/queries/use-cli-status.ts
Normal 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
|
||||
});
|
||||
}
|
||||
58
apps/ui/src/hooks/queries/use-cursor-permissions.ts
Normal file
58
apps/ui/src/hooks/queries/use-cursor-permissions.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
127
apps/ui/src/hooks/queries/use-features.ts
Normal file
127
apps/ui/src/hooks/queries/use-features.ts
Normal 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;
|
||||
},
|
||||
});
|
||||
}
|
||||
37
apps/ui/src/hooks/queries/use-git.ts
Normal file
37
apps/ui/src/hooks/queries/use-git.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
184
apps/ui/src/hooks/queries/use-github.ts
Normal file
184
apps/ui/src/hooks/queries/use-github.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
86
apps/ui/src/hooks/queries/use-ideation.ts
Normal file
86
apps/ui/src/hooks/queries/use-ideation.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
134
apps/ui/src/hooks/queries/use-models.ts
Normal file
134
apps/ui/src/hooks/queries/use-models.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
39
apps/ui/src/hooks/queries/use-pipeline.ts
Normal file
39
apps/ui/src/hooks/queries/use-pipeline.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
61
apps/ui/src/hooks/queries/use-running-agents.ts
Normal file
61
apps/ui/src/hooks/queries/use-running-agents.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
86
apps/ui/src/hooks/queries/use-sessions.ts
Normal file
86
apps/ui/src/hooks/queries/use-sessions.ts
Normal 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
|
||||
});
|
||||
}
|
||||
122
apps/ui/src/hooks/queries/use-settings.ts
Normal file
122
apps/ui/src/hooks/queries/use-settings.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
103
apps/ui/src/hooks/queries/use-spec.ts
Normal file
103
apps/ui/src/hooks/queries/use-spec.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
77
apps/ui/src/hooks/queries/use-usage.ts
Normal file
77
apps/ui/src/hooks/queries/use-usage.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
42
apps/ui/src/hooks/queries/use-workspace.ts
Normal file
42
apps/ui/src/hooks/queries/use-workspace.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
252
apps/ui/src/hooks/queries/use-worktrees.ts
Normal file
252
apps/ui/src/hooks/queries/use-worktrees.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user