Unverified 提交 8a38dc75 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Refactor React Query keys to use centralized factory pattern (#2268)

Introduce a type-safe, centralized query key system following TanStack Query best practices: - Add src/lib/queryKeys.ts with hierarchical factory functions - Use `as const` assertions for full type inference - Group keys by feature (apps, chats, versions, etc.) - Support prefix-based invalidation via parent keys Update all 30+ hooks and components to use the new queryKeys: - Replace inline string arrays with factory calls - Replace scattered exports (CHATS_QUERY_KEY, TOKEN_COUNT_QUERY_KEY, etc.) - Consistent pattern: queryKeys.<feature>.<operation>(params) Benefits: - Single source of truth for all query keys - Full autocomplete/IntelliSense support - Type-safe invalidation (catches typos at compile time) - Easier refactoring and key discovery <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Centralized React Query keys behind a typed queryKeys factory and updated 30+ hooks/components to use it. This improves type-safety, enables autocomplete, and makes invalidation and refactors simpler. - **Refactors** - Added src/lib/queryKeys.ts with hierarchical factory functions (as const) grouped by feature. - Replaced inline arrays and scattered constants (e.g., CHATS_QUERY_KEY, TOKEN_COUNT_QUERY_KEY, APP_THEME_QUERY_KEY, SUPABASE_QUERY_KEYS). - Standardized invalidate/remove calls to use parent keys (e.g., queryKeys.chats.all, queryKeys.versions.list({ appId })). - Structured MCP keys (mcp.toolsByServer.all and mcp.toolsByServer.list({ serverIds })) and updated Supabase branches to include organizationSlug. - No behavior changes; safer invalidation and consistent keys across the app. - **Migration** - Use queryKeys.<feature>.<operation>(params object) for all queryKey definitions. - Invalidate broadly via parent keys when needed (e.g., queryKeys.chats.all). - Update Supabase branches calls to pass organizationSlug: queryKeys.supabase.branches({ projectId, organizationSlug }). - Do not add or export per-hook key constants; rely on queryKeys. <sup>Written for commit 2b80e408f077b8ea3141369ca21f62e514852cfd. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces a centralized, typed React Query key factory and applies it across the app for consistency and safer invalidation. > > - Adds `src/lib/queryKeys.ts` with hierarchical, `as const` key factories (e.g., `queryKeys.apps.detail`, `queryKeys.versions.list`) > - Refactors 30+ hooks/components to use factory keys in `useQuery`/`useMutation` and `invalidateQueries`/`removeQueries` > - Replaces scattered constants (e.g., `CHATS_QUERY_KEY`, `TOKEN_COUNT_QUERY_KEY`, `APP_THEME_QUERY_KEY`, Supabase/MCP keys) with `queryKeys` > - Updates IPC-driven UI pieces (`AppUpgrades`, `CapacitorControls`, `ModelPicker`, `ChatInput`, preview panels, Neon, MCP, Supabase, Vercel, etc.) to the new keys > - Documentation: `AGENTS.md` adds an Architecture section with usage guidelines and changes format script to `npm run fmt` > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2b80e408f077b8ea3141369ca21f62e514852cfd. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: 's avatarClaude <noreply@anthropic.com>
上级 69c0259b
...@@ -25,7 +25,7 @@ Otherwise, run the following commands directly: ...@@ -25,7 +25,7 @@ Otherwise, run the following commands directly:
**Formatting** **Formatting**
```sh ```sh
npm run prettier npm run fmt
``` ```
**Linting** **Linting**
...@@ -61,11 +61,39 @@ Note: if you do this, then you will need to re-add the changes and commit again. ...@@ -61,11 +61,39 @@ Note: if you do this, then you will need to re-add the changes and commit again.
3. `src/ipc/ipc_host.ts` registers handlers that live in files under `src/ipc/handlers/` (e.g., `app_handlers.ts`, `chat_stream_handlers.ts`, `settings_handlers.ts`). 3. `src/ipc/ipc_host.ts` registers handlers that live in files under `src/ipc/handlers/` (e.g., `app_handlers.ts`, `chat_stream_handlers.ts`, `settings_handlers.ts`).
4. IPC handlers should `throw new Error("...")` on failure instead of returning `{ success: false }` style payloads. 4. IPC handlers should `throw new Error("...")` on failure instead of returning `{ success: false }` style payloads.
## Architecture
### React Query key factory
All React Query keys must be defined in `src/lib/queryKeys.ts` using the centralized factory pattern. This provides:
- Type-safe query keys with full autocomplete
- Hierarchical structure for easy invalidation (invalidate parent to invalidate children)
- Consistent naming across the codebase
- Single source of truth for all query keys
**Usage:**
```ts
import { queryKeys } from "@/lib/queryKeys";
// In useQuery:
useQuery({
queryKey: queryKeys.apps.detail({ appId }),
queryFn: () => IpcClient.getInstance().getApp(appId),
});
// Invalidating queries:
queryClient.invalidateQueries({ queryKey: queryKeys.apps.all });
```
**Adding new keys:** Add entries to the appropriate domain in `queryKeys.ts`. Follow the existing pattern with `all` for the base key and factory functions using object parameters for parameterized keys.
## React + IPC integration pattern ## React + IPC integration pattern
When creating hooks/components that call IPC handlers: When creating hooks/components that call IPC handlers:
- Wrap reads in `useQuery`, providing a stable `queryKey`, async `queryFn` that calls the relevant `IpcClient` method, and conditionally use `enabled`/`initialData`/`meta` as needed. - Wrap reads in `useQuery`, using keys from `queryKeys` factory (see above), async `queryFn` that calls the relevant `IpcClient` method, and conditionally use `enabled`/`initialData`/`meta` as needed.
- Wrap writes in `useMutation`; validate inputs locally, call the IPC client, and invalidate related queries on success. Use shared utilities (e.g., toast helpers) in `onError`. - Wrap writes in `useMutation`; validate inputs locally, call the IPC client, and invalidate related queries on success. Use shared utilities (e.g., toast helpers) in `onError`.
- Synchronize TanStack Query data with any global state (like Jotai atoms) via `useEffect` only if required. - Synchronize TanStack Query data with any global state (like Jotai atoms) via `useEffect` only if required.
......
...@@ -5,6 +5,7 @@ import { Terminal } from "lucide-react"; ...@@ -5,6 +5,7 @@ import { Terminal } from "lucide-react";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AppUpgrade } from "@/ipc/ipc_types"; import { AppUpgrade } from "@/ipc/ipc_types";
import { queryKeys } from "@/lib/queryKeys";
export function AppUpgrades({ appId }: { appId: number | null }) { export function AppUpgrades({ appId }: { appId: number | null }) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -14,7 +15,7 @@ export function AppUpgrades({ appId }: { appId: number | null }) { ...@@ -14,7 +15,7 @@ export function AppUpgrades({ appId }: { appId: number | null }) {
isLoading, isLoading,
error: queryError, error: queryError,
} = useQuery({ } = useQuery({
queryKey: ["app-upgrades", appId], queryKey: queryKeys.appUpgrades.byApp({ appId }),
queryFn: () => { queryFn: () => {
if (!appId) { if (!appId) {
return Promise.resolve([]); return Promise.resolve([]);
...@@ -40,13 +41,19 @@ export function AppUpgrades({ appId }: { appId: number | null }) { ...@@ -40,13 +41,19 @@ export function AppUpgrades({ appId }: { appId: number | null }) {
}); });
}, },
onSuccess: (_, upgradeId) => { onSuccess: (_, upgradeId) => {
queryClient.invalidateQueries({ queryKey: ["app-upgrades", appId] }); queryClient.invalidateQueries({
queryKey: queryKeys.appUpgrades.byApp({ appId }),
});
if (upgradeId === "capacitor") { if (upgradeId === "capacitor") {
// Capacitor upgrade is done, so we need to invalidate the Capacitor // Capacitor upgrade is done, so we need to invalidate the Capacitor
// query to show the new status. // query to show the new status.
queryClient.invalidateQueries({ queryKey: ["is-capacitor", appId] }); queryClient.invalidateQueries({
queryKey: queryKeys.appUpgrades.isCapacitor({ appId }),
});
} }
queryClient.invalidateQueries({ queryKey: ["versions", appId] }); queryClient.invalidateQueries({
queryKey: queryKeys.versions.list({ appId }),
});
}, },
}); });
......
...@@ -24,6 +24,7 @@ import { ...@@ -24,6 +24,7 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { queryKeys } from "@/lib/queryKeys";
interface CapacitorControlsProps { interface CapacitorControlsProps {
appId: number; appId: number;
...@@ -42,7 +43,7 @@ export function CapacitorControls({ appId }: CapacitorControlsProps) { ...@@ -42,7 +43,7 @@ export function CapacitorControls({ appId }: CapacitorControlsProps) {
// Check if Capacitor is installed // Check if Capacitor is installed
const { data: isCapacitor, isLoading } = useQuery({ const { data: isCapacitor, isLoading } = useQuery({
queryKey: ["is-capacitor", appId], queryKey: queryKeys.appUpgrades.isCapacitor({ appId }),
queryFn: () => IpcClient.getInstance().isCapacitor({ appId }), queryFn: () => IpcClient.getInstance().isCapacitor({ appId }),
enabled: appId !== undefined && appId !== null, enabled: appId !== undefined && appId !== null,
}); });
......
...@@ -28,7 +28,7 @@ import { PriceBadge } from "@/components/PriceBadge"; ...@@ -28,7 +28,7 @@ import { PriceBadge } from "@/components/PriceBadge";
import { TURBO_MODELS } from "@/ipc/shared/language_model_constants"; import { TURBO_MODELS } from "@/ipc/shared/language_model_constants";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { TOKEN_COUNT_QUERY_KEY } from "@/hooks/useCountTokens"; import { queryKeys } from "@/lib/queryKeys";
export function ModelPicker() { export function ModelPicker() {
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
...@@ -37,7 +37,7 @@ export function ModelPicker() { ...@@ -37,7 +37,7 @@ export function ModelPicker() {
updateSettings({ selectedModel: model }); updateSettings({ selectedModel: model });
// Invalidate token count when model changes since different models have different context windows // Invalidate token count when model changes since different models have different context windows
// (technically they have different tokenizers, but we don't keep track of that). // (technically they have different tokenizers, but we don't keep track of that).
queryClient.invalidateQueries({ queryKey: TOKEN_COUNT_QUERY_KEY }); queryClient.invalidateQueries({ queryKey: queryKeys.tokenCount.all });
}; };
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
......
...@@ -36,11 +36,12 @@ import { ContextFilesPicker } from "@/components/ContextFilesPicker"; ...@@ -36,11 +36,12 @@ import { ContextFilesPicker } from "@/components/ContextFilesPicker";
import { FileAttachmentDropdown } from "./FileAttachmentDropdown"; import { FileAttachmentDropdown } from "./FileAttachmentDropdown";
import { CustomThemeDialog } from "@/components/CustomThemeDialog"; import { CustomThemeDialog } from "@/components/CustomThemeDialog";
import { useThemes } from "@/hooks/useThemes"; import { useThemes } from "@/hooks/useThemes";
import { useAppTheme, APP_THEME_QUERY_KEY } from "@/hooks/useAppTheme"; import { useAppTheme } from "@/hooks/useAppTheme";
import { useCustomThemes } from "@/hooks/useCustomThemes"; import { useCustomThemes } from "@/hooks/useCustomThemes";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "@/lib/queryKeys";
interface AuxiliaryActionsMenuProps { interface AuxiliaryActionsMenuProps {
onFileSelect: ( onFileSelect: (
...@@ -108,7 +109,9 @@ export function AuxiliaryActionsMenu({ ...@@ -108,7 +109,9 @@ export function AuxiliaryActionsMenu({
themeId, themeId,
}); });
// Invalidate app theme query to refresh // Invalidate app theme query to refresh
queryClient.invalidateQueries({ queryKey: APP_THEME_QUERY_KEY(appId) }); queryClient.invalidateQueries({
queryKey: queryKeys.appTheme.byApp({ appId }),
});
} else { } else {
// Update default theme in settings (for new apps) // Update default theme in settings (for new apps)
// Store as string for settings (empty string for no theme) // Store as string for settings (empty string for no theme)
...@@ -126,7 +129,7 @@ export function AuxiliaryActionsMenu({ ...@@ -126,7 +129,7 @@ export function AuxiliaryActionsMenu({
if (!open) { if (!open) {
// Refresh custom themes when dialog closes // Refresh custom themes when dialog closes
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["custom-themes"], queryKey: queryKeys.customThemes.all,
}); });
} }
}; };
......
...@@ -80,7 +80,7 @@ import { useChatModeToggle } from "@/hooks/useChatModeToggle"; ...@@ -80,7 +80,7 @@ import { useChatModeToggle } from "@/hooks/useChatModeToggle";
import { VisualEditingChangesDialog } from "@/components/preview_panel/VisualEditingChangesDialog"; import { VisualEditingChangesDialog } from "@/components/preview_panel/VisualEditingChangesDialog";
import { useUserBudgetInfo } from "@/hooks/useUserBudgetInfo"; import { useUserBudgetInfo } from "@/hooks/useUserBudgetInfo";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { TOKEN_COUNT_QUERY_KEY } from "@/hooks/useCountTokens"; import { queryKeys } from "@/lib/queryKeys";
const showTokenBarAtom = atom(false); const showTokenBarAtom = atom(false);
...@@ -102,7 +102,7 @@ export function ChatInput({ chatId }: { chatId?: number }) { ...@@ -102,7 +102,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const toggleShowTokenBar = useCallback(() => { const toggleShowTokenBar = useCallback(() => {
setShowTokenBar((prev) => !prev); setShowTokenBar((prev) => !prev);
queryClient.invalidateQueries({ queryKey: TOKEN_COUNT_QUERY_KEY }); queryClient.invalidateQueries({ queryKey: queryKeys.tokenCount.all });
}, [setShowTokenBar, queryClient]); }, [setShowTokenBar, queryClient]);
const [selectedComponents, setSelectedComponents] = useAtom( const [selectedComponents, setSelectedComponents] = useAtom(
selectedComponentsPreviewAtom, selectedComponentsPreviewAtom,
......
...@@ -24,6 +24,7 @@ import { selectedAppIdAtom } from "@/atoms/appAtoms"; ...@@ -24,6 +24,7 @@ import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { NeonConfigure } from "./NeonConfigure"; import { NeonConfigure } from "./NeonConfigure";
import { queryKeys } from "@/lib/queryKeys";
const EnvironmentVariablesTitle = () => ( const EnvironmentVariablesTitle = () => (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
...@@ -62,7 +63,7 @@ export const ConfigurePanel = () => { ...@@ -62,7 +63,7 @@ export const ConfigurePanel = () => {
isLoading, isLoading,
error, error,
} = useQuery({ } = useQuery({
queryKey: ["app-env-vars", selectedAppId], queryKey: queryKeys.appEnvVars.byApp({ appId: selectedAppId }),
queryFn: async () => { queryFn: async () => {
if (!selectedAppId) return []; if (!selectedAppId) return [];
const ipcClient = IpcClient.getInstance(); const ipcClient = IpcClient.getInstance();
...@@ -83,7 +84,7 @@ export const ConfigurePanel = () => { ...@@ -83,7 +84,7 @@ export const ConfigurePanel = () => {
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["app-env-vars", selectedAppId], queryKey: queryKeys.appEnvVars.byApp({ appId: selectedAppId }),
}); });
showSuccess("Environment variables saved"); showSuccess("Environment variables saved");
}, },
......
...@@ -16,6 +16,7 @@ import { useQueryClient } from "@tanstack/react-query"; ...@@ -16,6 +16,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { useCheckProblems } from "@/hooks/useCheckProblems"; import { useCheckProblems } from "@/hooks/useCheckProblems";
import { getLanguage } from "@/utils/get_language"; import { getLanguage } from "@/utils/get_language";
import { queryKeys } from "@/lib/queryKeys";
interface FileEditorProps { interface FileEditorProps {
appId: number | null; appId: number | null;
...@@ -195,7 +196,9 @@ export const FileEditor = ({ ...@@ -195,7 +196,9 @@ export const FileEditor = ({
filePath, filePath,
currentValueRef.current, currentValueRef.current,
); );
await queryClient.invalidateQueries({ queryKey: ["versions", appId] }); await queryClient.invalidateQueries({
queryKey: queryKeys.versions.list({ appId }),
});
if (settings?.enableAutoFixProblems) { if (settings?.enableAutoFixProblems) {
checkProblems(); checkProblems();
} }
......
...@@ -10,6 +10,7 @@ import { useLoadApp } from "@/hooks/useLoadApp"; ...@@ -10,6 +10,7 @@ import { useLoadApp } from "@/hooks/useLoadApp";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import type { GetNeonProjectResponse, NeonBranch } from "@/ipc/ipc_types"; import type { GetNeonProjectResponse, NeonBranch } from "@/ipc/ipc_types";
import { NeonDisconnectButton } from "@/components/NeonDisconnectButton"; import { NeonDisconnectButton } from "@/components/NeonDisconnectButton";
import { queryKeys } from "@/lib/queryKeys";
const getBranchTypeColor = (type: NeonBranch["type"]) => { const getBranchTypeColor = (type: NeonBranch["type"]) => {
switch (type) { switch (type) {
...@@ -40,7 +41,7 @@ export const NeonConfigure = () => { ...@@ -40,7 +41,7 @@ export const NeonConfigure = () => {
isLoading, isLoading,
error, error,
} = useQuery<GetNeonProjectResponse, Error>({ } = useQuery<GetNeonProjectResponse, Error>({
queryKey: ["neon-project", selectedAppId], queryKey: queryKeys.neon.project({ appId: selectedAppId }),
queryFn: async () => { queryFn: async () => {
if (!selectedAppId) throw new Error("No app selected"); if (!selectedAppId) throw new Error("No app selected");
const ipcClient = IpcClient.getInstance(); const ipcClient = IpcClient.getInstance();
......
...@@ -3,6 +3,7 @@ import { selectedAppIdAtom } from "@/atoms/appAtoms"; ...@@ -3,6 +3,7 @@ import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { selectedChatIdAtom } from "@/atoms/chatAtoms"; import { selectedChatIdAtom } from "@/atoms/chatAtoms";
import { useSecurityReview } from "@/hooks/useSecurityReview"; import { useSecurityReview } from "@/hooks/useSecurityReview";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { queryKeys } from "@/lib/queryKeys";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
...@@ -748,7 +749,7 @@ export const SecurityPanel = () => { ...@@ -748,7 +749,7 @@ export const SecurityPanel = () => {
rulesContent, rulesContent,
); );
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ["versions", selectedAppId], queryKey: queryKeys.versions.list({ appId: selectedAppId }),
}); });
if (warning) { if (warning) {
showWarning(warning); showWarning(warning);
......
...@@ -18,6 +18,7 @@ import { ...@@ -18,6 +18,7 @@ import {
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "@/lib/queryKeys";
interface ModelsSectionProps { interface ModelsSectionProps {
providerId: string; providerId: string;
...@@ -35,10 +36,10 @@ export function ModelsSection({ providerId }: ModelsSectionProps) { ...@@ -35,10 +36,10 @@ export function ModelsSection({ providerId }: ModelsSectionProps) {
const invalidateModels = () => { const invalidateModels = () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["language-models", providerId], queryKey: queryKeys.languageModels.forProvider({ providerId }),
}); });
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["language-models-by-providers"], queryKey: queryKeys.languageModels.byProviders,
}); });
}; };
......
...@@ -7,6 +7,7 @@ import { IpcClient } from "@/ipc/ipc_client"; ...@@ -7,6 +7,7 @@ import { IpcClient } from "@/ipc/ipc_client";
import type { AgentToolName } from "../pro/main/ipc/handlers/local_agent/tool_definitions"; import type { AgentToolName } from "../pro/main/ipc/handlers/local_agent/tool_definitions";
import type { AgentTool } from "@/ipc/ipc_types"; import type { AgentTool } from "@/ipc/ipc_types";
import { AgentToolConsent } from "@/lib/schemas"; import { AgentToolConsent } from "@/lib/schemas";
import { queryKeys } from "@/lib/queryKeys";
// Re-export types for convenience // Re-export types for convenience
export type { AgentToolName, AgentTool }; export type { AgentToolName, AgentTool };
...@@ -15,7 +16,7 @@ export function useAgentTools() { ...@@ -15,7 +16,7 @@ export function useAgentTools() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const toolsQuery = useQuery({ const toolsQuery = useQuery({
queryKey: ["agent-tools"], queryKey: queryKeys.agentTools.all,
queryFn: async () => { queryFn: async () => {
const ipcClient = IpcClient.getInstance(); const ipcClient = IpcClient.getInstance();
return ipcClient.getAgentTools(); return ipcClient.getAgentTools();
...@@ -31,7 +32,7 @@ export function useAgentTools() { ...@@ -31,7 +32,7 @@ export function useAgentTools() {
return ipcClient.setAgentToolConsent(params); return ipcClient.setAgentToolConsent(params);
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["agent-tools"] }); queryClient.invalidateQueries({ queryKey: queryKeys.agentTools.all });
}, },
}); });
......
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { queryKeys } from "@/lib/queryKeys";
export const APP_THEME_QUERY_KEY = (appId: number | undefined) => [
"app-theme",
appId,
];
export function useAppTheme(appId: number | undefined) { export function useAppTheme(appId: number | undefined) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const query = useQuery({ const query = useQuery({
queryKey: APP_THEME_QUERY_KEY(appId), queryKey: queryKeys.appTheme.byApp({ appId }),
queryFn: async (): Promise<string | null> => { queryFn: async (): Promise<string | null> => {
return IpcClient.getInstance().getAppTheme({ appId: appId! }); return IpcClient.getInstance().getAppTheme({ appId: appId! });
}, },
...@@ -18,7 +14,9 @@ export function useAppTheme(appId: number | undefined) { ...@@ -18,7 +14,9 @@ export function useAppTheme(appId: number | undefined) {
}); });
const invalidate = () => { const invalidate = () => {
queryClient.invalidateQueries({ queryKey: APP_THEME_QUERY_KEY(appId) }); queryClient.invalidateQueries({
queryKey: queryKeys.appTheme.byApp({ appId }),
});
}; };
return { return {
......
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import type { ChatSummary } from "@/lib/schemas"; import type { ChatSummary } from "@/lib/schemas";
import { queryKeys } from "@/lib/queryKeys";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
export const CHATS_QUERY_KEY = "chats";
export function useChats(appId: number | null) { export function useChats(appId: number | null) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data, isLoading } = useQuery<ChatSummary[]>({ const { data, isLoading } = useQuery<ChatSummary[]>({
queryKey: [CHATS_QUERY_KEY, appId], queryKey: queryKeys.chats.list({ appId }),
queryFn: async () => { queryFn: async () => {
return IpcClient.getInstance().getChats(appId ?? undefined); return IpcClient.getInstance().getChats(appId ?? undefined);
}, },
...@@ -17,7 +16,7 @@ export function useChats(appId: number | null) { ...@@ -17,7 +16,7 @@ export function useChats(appId: number | null) {
const invalidateChats = () => { const invalidateChats = () => {
// Invalidate all chat queries (any appId) since mutations affect both // Invalidate all chat queries (any appId) since mutations affect both
// app-specific lists and the global list (appId=null) // app-specific lists and the global list (appId=null)
queryClient.invalidateQueries({ queryKey: [CHATS_QUERY_KEY] }); queryClient.invalidateQueries({ queryKey: queryKeys.chats.all });
}; };
return { return {
......
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { queryKeys } from "@/lib/queryKeys";
export const useCheckName = (appName: string) => { export const useCheckName = (appName: string) => {
return useQuery({ return useQuery({
queryKey: ["checkAppName", appName], queryKey: queryKeys.appName.check({ name: appName }),
queryFn: async () => { queryFn: async () => {
const result = await IpcClient.getInstance().checkAppName({ appName }); const result = await IpcClient.getInstance().checkAppName({ appName });
return result; return result;
......
...@@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query"; ...@@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import type { ProblemReport } from "@/ipc/ipc_types"; import type { ProblemReport } from "@/ipc/ipc_types";
import { useSettings } from "./useSettings"; import { useSettings } from "./useSettings";
import { queryKeys } from "@/lib/queryKeys";
export function useCheckProblems(appId: number | null) { export function useCheckProblems(appId: number | null) {
const { settings } = useSettings(); const { settings } = useSettings();
...@@ -11,7 +12,7 @@ export function useCheckProblems(appId: number | null) { ...@@ -11,7 +12,7 @@ export function useCheckProblems(appId: number | null) {
error, error,
refetch: checkProblems, refetch: checkProblems,
} = useQuery<ProblemReport, Error>({ } = useQuery<ProblemReport, Error>({
queryKey: ["problems", appId], queryKey: queryKeys.problems.byApp({ appId }),
queryFn: async (): Promise<ProblemReport> => { queryFn: async (): Promise<ProblemReport> => {
if (!appId) { if (!appId) {
throw new Error("App ID is required"); throw new Error("App ID is required");
......
...@@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; ...@@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { activeCheckoutCounterAtom } from "@/store/appAtoms"; import { activeCheckoutCounterAtom } from "@/store/appAtoms";
import { queryKeys } from "@/lib/queryKeys";
interface CheckoutVersionVariables { interface CheckoutVersionVariables {
appId: number; appId: number;
...@@ -30,10 +31,10 @@ export function useCheckoutVersion() { ...@@ -30,10 +31,10 @@ export function useCheckoutVersion() {
onSuccess: (_, variables) => { onSuccess: (_, variables) => {
// Invalidate queries that depend on the current version/branch // Invalidate queries that depend on the current version/branch
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["currentBranch", variables.appId], queryKey: queryKeys.branches.current({ appId: variables.appId }),
}); });
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["versions", variables.appId], queryKey: queryKeys.versions.list({ appId: variables.appId }),
}); });
}, },
meta: { showErrorToast: true }, meta: { showErrorToast: true },
......
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { showError, showSuccess } from "@/lib/toast"; import { showError, showSuccess } from "@/lib/toast";
import { queryKeys } from "@/lib/queryKeys";
export function useCommitChanges() { export function useCommitChanges() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -19,9 +20,13 @@ export function useCommitChanges() { ...@@ -19,9 +20,13 @@ export function useCommitChanges() {
onSuccess: (_, { appId }) => { onSuccess: (_, { appId }) => {
showSuccess("Changes committed successfully"); showSuccess("Changes committed successfully");
// Invalidate uncommitted files query // Invalidate uncommitted files query
queryClient.invalidateQueries({ queryKey: ["uncommittedFiles", appId] }); queryClient.invalidateQueries({
queryKey: queryKeys.uncommittedFiles.byApp({ appId }),
});
// Also invalidate versions query to update version count // Also invalidate versions query to update version count
queryClient.invalidateQueries({ queryKey: ["versions", appId] }); queryClient.invalidateQueries({
queryKey: queryKeys.versions.list({ appId }),
});
}, },
onError: (error: Error) => { onError: (error: Error) => {
showError(`Failed to commit: ${error.message}`); showError(`Failed to commit: ${error.message}`);
......
...@@ -3,6 +3,7 @@ import { useAtomValue } from "jotai"; ...@@ -3,6 +3,7 @@ import { useAtomValue } from "jotai";
import { selectedAppIdAtom } from "@/atoms/appAtoms"; import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { GlobPath, ContextPathResults } from "@/lib/schemas"; import { GlobPath, ContextPathResults } from "@/lib/schemas";
import { queryKeys } from "@/lib/queryKeys";
export function useContextPaths() { export function useContextPaths() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -13,7 +14,7 @@ export function useContextPaths() { ...@@ -13,7 +14,7 @@ export function useContextPaths() {
isLoading, isLoading,
error, error,
} = useQuery<ContextPathResults, Error>({ } = useQuery<ContextPathResults, Error>({
queryKey: ["context-paths", appId], queryKey: queryKeys.contextPaths.byApp({ appId }),
queryFn: async () => { queryFn: async () => {
if (!appId) if (!appId)
return { return {
...@@ -53,7 +54,9 @@ export function useContextPaths() { ...@@ -53,7 +54,9 @@ export function useContextPaths() {
}); });
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["context-paths", appId] }); queryClient.invalidateQueries({
queryKey: queryKeys.contextPaths.byApp({ appId }),
});
}, },
}); });
......
...@@ -6,8 +6,7 @@ import { ...@@ -6,8 +6,7 @@ import {
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import type { TokenCountResult } from "@/ipc/ipc_types"; import type { TokenCountResult } from "@/ipc/ipc_types";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { queryKeys } from "@/lib/queryKeys";
export const TOKEN_COUNT_QUERY_KEY = ["tokenCount"] as const;
export function useCountTokens(chatId: number | null, input: string = "") { export function useCountTokens(chatId: number | null, input: string = "") {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -35,7 +34,7 @@ export function useCountTokens(chatId: number | null, input: string = "") { ...@@ -35,7 +34,7 @@ export function useCountTokens(chatId: number | null, input: string = "") {
error, error,
refetch, refetch,
} = useQuery<TokenCountResult | null>({ } = useQuery<TokenCountResult | null>({
queryKey: [...TOKEN_COUNT_QUERY_KEY, chatId, debouncedInput], queryKey: queryKeys.tokenCount.forChat({ chatId, input: debouncedInput }),
queryFn: async () => { queryFn: async () => {
if (chatId === null) return null; if (chatId === null) return null;
return IpcClient.getInstance().countTokens({ return IpcClient.getInstance().countTokens({
...@@ -49,7 +48,7 @@ export function useCountTokens(chatId: number | null, input: string = "") { ...@@ -49,7 +48,7 @@ export function useCountTokens(chatId: number | null, input: string = "") {
// For imperative invalidation (e.g., after streaming completes) // For imperative invalidation (e.g., after streaming completes)
const invalidateTokenCount = useCallback(() => { const invalidateTokenCount = useCallback(() => {
queryClient.invalidateQueries({ queryKey: TOKEN_COUNT_QUERY_KEY }); queryClient.invalidateQueries({ queryKey: queryKeys.tokenCount.all });
}, [queryClient]); }, [queryClient]);
return { return {
......
...@@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; ...@@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { showError } from "@/lib/toast"; import { showError } from "@/lib/toast";
import type { CreateAppParams, CreateAppResult } from "@/ipc/ipc_types"; import type { CreateAppParams, CreateAppResult } from "@/ipc/ipc_types";
import { queryKeys } from "@/lib/queryKeys";
export function useCreateApp() { export function useCreateApp() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -17,7 +18,7 @@ export function useCreateApp() { ...@@ -17,7 +18,7 @@ export function useCreateApp() {
}, },
onSuccess: () => { onSuccess: () => {
// Invalidate apps list to trigger refetch // Invalidate apps list to trigger refetch
queryClient.invalidateQueries({ queryKey: ["apps"] }); queryClient.invalidateQueries({ queryKey: queryKeys.apps.all });
}, },
onError: (error) => { onError: (error) => {
showError(error); showError(error);
......
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import type { BranchResult } from "@/ipc/ipc_types"; import type { BranchResult } from "@/ipc/ipc_types";
import { queryKeys } from "@/lib/queryKeys";
export function useCurrentBranch(appId: number | null) { export function useCurrentBranch(appId: number | null) {
const { const {
...@@ -8,7 +9,7 @@ export function useCurrentBranch(appId: number | null) { ...@@ -8,7 +9,7 @@ export function useCurrentBranch(appId: number | null) {
isLoading, isLoading,
refetch: refetchBranchInfo, refetch: refetchBranchInfo,
} = useQuery<BranchResult, Error>({ } = useQuery<BranchResult, Error>({
queryKey: ["currentBranch", appId], queryKey: queryKeys.branches.current({ appId }),
queryFn: async (): Promise<BranchResult> => { queryFn: async (): Promise<BranchResult> => {
if (appId === null) { if (appId === null) {
// This case should ideally be handled by the `enabled` option // This case should ideally be handled by the `enabled` option
......
...@@ -5,6 +5,7 @@ import type { ...@@ -5,6 +5,7 @@ import type {
LanguageModelProvider, LanguageModelProvider,
} from "@/ipc/ipc_types"; } from "@/ipc/ipc_types";
import { showError } from "@/lib/toast"; import { showError } from "@/lib/toast";
import { queryKeys } from "@/lib/queryKeys";
export function useCustomLanguageModelProvider() { export function useCustomLanguageModelProvider() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -33,7 +34,9 @@ export function useCustomLanguageModelProvider() { ...@@ -33,7 +34,9 @@ export function useCustomLanguageModelProvider() {
}, },
onSuccess: () => { onSuccess: () => {
// Invalidate and refetch // Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ["languageModelProviders"] }); queryClient.invalidateQueries({
queryKey: queryKeys.languageModels.providers,
});
}, },
onError: (error) => { onError: (error) => {
showError(error); showError(error);
...@@ -63,7 +66,9 @@ export function useCustomLanguageModelProvider() { ...@@ -63,7 +66,9 @@ export function useCustomLanguageModelProvider() {
}, },
onSuccess: () => { onSuccess: () => {
// Invalidate and refetch // Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ["languageModelProviders"] }); queryClient.invalidateQueries({
queryKey: queryKeys.languageModels.providers,
});
}, },
onError: (error) => { onError: (error) => {
showError(error); showError(error);
...@@ -80,7 +85,9 @@ export function useCustomLanguageModelProvider() { ...@@ -80,7 +85,9 @@ export function useCustomLanguageModelProvider() {
}, },
onSuccess: () => { onSuccess: () => {
// Invalidate and refetch // Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ["languageModelProviders"] }); queryClient.invalidateQueries({
queryKey: queryKeys.languageModels.providers,
});
}, },
onError: (error) => { onError: (error) => {
showError(error); showError(error);
......
...@@ -7,16 +7,14 @@ import type { ...@@ -7,16 +7,14 @@ import type {
GenerateThemePromptParams, GenerateThemePromptParams,
GenerateThemePromptResult, GenerateThemePromptResult,
} from "@/ipc/ipc_types"; } from "@/ipc/ipc_types";
import { queryKeys } from "@/lib/queryKeys";
// Query key for custom themes
export const CUSTOM_THEMES_QUERY_KEY = ["custom-themes"];
/** /**
* Hook to fetch all custom themes. * Hook to fetch all custom themes.
*/ */
export function useCustomThemes() { export function useCustomThemes() {
const query = useQuery({ const query = useQuery({
queryKey: CUSTOM_THEMES_QUERY_KEY, queryKey: queryKeys.customThemes.all,
queryFn: async (): Promise<CustomTheme[]> => { queryFn: async (): Promise<CustomTheme[]> => {
const ipcClient = IpcClient.getInstance(); const ipcClient = IpcClient.getInstance();
return ipcClient.getCustomThemes(); return ipcClient.getCustomThemes();
...@@ -47,7 +45,7 @@ export function useCreateCustomTheme() { ...@@ -47,7 +45,7 @@ export function useCreateCustomTheme() {
onSuccess: () => { onSuccess: () => {
// Invalidate all custom theme queries using prefix matching // Invalidate all custom theme queries using prefix matching
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["custom-themes"], queryKey: queryKeys.customThemes.all,
}); });
}, },
}); });
...@@ -66,7 +64,7 @@ export function useUpdateCustomTheme() { ...@@ -66,7 +64,7 @@ export function useUpdateCustomTheme() {
onSuccess: () => { onSuccess: () => {
// Invalidate all custom theme queries using prefix matching // Invalidate all custom theme queries using prefix matching
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["custom-themes"], queryKey: queryKeys.customThemes.all,
}); });
}, },
}); });
...@@ -83,7 +81,7 @@ export function useDeleteCustomTheme() { ...@@ -83,7 +81,7 @@ export function useDeleteCustomTheme() {
onSuccess: () => { onSuccess: () => {
// Invalidate all custom theme queries using prefix matching // Invalidate all custom theme queries using prefix matching
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["custom-themes"], queryKey: queryKeys.customThemes.all,
}); });
}, },
}); });
......
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { queryKeys } from "@/lib/queryKeys";
interface DeleteCustomModelParams { interface DeleteCustomModelParams {
providerId: string; providerId: string;
...@@ -29,11 +30,13 @@ export function useDeleteCustomModel({ ...@@ -29,11 +30,13 @@ export function useDeleteCustomModel({
onSuccess: (data, params: DeleteCustomModelParams) => { onSuccess: (data, params: DeleteCustomModelParams) => {
// Invalidate queries related to language models for the specific provider // Invalidate queries related to language models for the specific provider
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["language-models", params.providerId], queryKey: queryKeys.languageModels.forProvider({
providerId: params.providerId,
}),
}); });
// Invalidate general model list if needed // Invalidate general model list if needed
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["language-models-by-providers"], queryKey: queryKeys.languageModels.byProviders,
}); });
onSuccess?.(); onSuccess?.();
}, },
......
...@@ -7,13 +7,14 @@ import { ...@@ -7,13 +7,14 @@ import {
VertexProviderSetting, VertexProviderSetting,
AzureProviderSetting, AzureProviderSetting,
} from "@/lib/schemas"; } from "@/lib/schemas";
import { queryKeys } from "@/lib/queryKeys";
export function useLanguageModelProviders() { export function useLanguageModelProviders() {
const ipcClient = IpcClient.getInstance(); const ipcClient = IpcClient.getInstance();
const { settings, envVars } = useSettings(); const { settings, envVars } = useSettings();
const queryResult = useQuery<LanguageModelProvider[], Error>({ const queryResult = useQuery<LanguageModelProvider[], Error>({
queryKey: ["languageModelProviders"], queryKey: queryKeys.languageModels.providers,
queryFn: async () => { queryFn: async () => {
return ipcClient.getLanguageModelProviders(); return ipcClient.getLanguageModelProviders();
}, },
......
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import type { LanguageModel } from "@/ipc/ipc_types"; import type { LanguageModel } from "@/ipc/ipc_types";
import { queryKeys } from "@/lib/queryKeys";
/** /**
* Fetches all available language models grouped by their provider IDs. * Fetches all available language models grouped by their provider IDs.
...@@ -11,7 +12,7 @@ export function useLanguageModelsByProviders() { ...@@ -11,7 +12,7 @@ export function useLanguageModelsByProviders() {
const ipcClient = IpcClient.getInstance(); const ipcClient = IpcClient.getInstance();
return useQuery<Record<string, LanguageModel[]>, Error>({ return useQuery<Record<string, LanguageModel[]>, Error>({
queryKey: ["language-models-by-providers"], queryKey: queryKeys.languageModels.byProviders,
queryFn: async () => { queryFn: async () => {
return ipcClient.getLanguageModelsByProviders(); return ipcClient.getLanguageModelsByProviders();
}, },
......
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import type { LanguageModel } from "@/ipc/ipc_types"; import type { LanguageModel } from "@/ipc/ipc_types";
import { queryKeys } from "@/lib/queryKeys";
/** /**
* Fetches the list of available language models for a specific provider. * Fetches the list of available language models for a specific provider.
...@@ -15,7 +16,9 @@ export function useLanguageModelsForProvider(providerId: string | undefined) { ...@@ -15,7 +16,9 @@ export function useLanguageModelsForProvider(providerId: string | undefined) {
LanguageModel[], LanguageModel[],
Error // Specify Error type for better error handling Error // Specify Error type for better error handling
>({ >({
queryKey: ["language-models", providerId], queryKey: queryKeys.languageModels.forProvider({
providerId: providerId ?? "",
}),
queryFn: async () => { queryFn: async () => {
if (!providerId) { if (!providerId) {
// Avoid calling IPC if providerId is not set // Avoid calling IPC if providerId is not set
......
...@@ -4,6 +4,7 @@ import { IpcClient } from "@/ipc/ipc_client"; ...@@ -4,6 +4,7 @@ import { IpcClient } from "@/ipc/ipc_client";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { currentAppAtom } from "@/atoms/appAtoms"; import { currentAppAtom } from "@/atoms/appAtoms";
import { App } from "@/ipc/ipc_types"; import { App } from "@/ipc/ipc_types";
import { queryKeys } from "@/lib/queryKeys";
export function useLoadApp(appId: number | null) { export function useLoadApp(appId: number | null) {
const [, setApp] = useAtom(currentAppAtom); const [, setApp] = useAtom(currentAppAtom);
...@@ -14,7 +15,7 @@ export function useLoadApp(appId: number | null) { ...@@ -14,7 +15,7 @@ export function useLoadApp(appId: number | null) {
error, error,
refetch: refreshApp, refetch: refreshApp,
} = useQuery<App | null, Error>({ } = useQuery<App | null, Error>({
queryKey: ["app", appId], queryKey: queryKeys.apps.detail({ appId }),
queryFn: async () => { queryFn: async () => {
if (appId === null) { if (appId === null) {
return null; return null;
...@@ -44,5 +45,7 @@ export const invalidateAppQuery = ( ...@@ -44,5 +45,7 @@ export const invalidateAppQuery = (
queryClient: QueryClient, queryClient: QueryClient,
{ appId }: { appId: number | null }, { appId }: { appId: number | null },
) => { ) => {
return queryClient.invalidateQueries({ queryKey: ["app", appId] }); return queryClient.invalidateQueries({
queryKey: queryKeys.apps.detail({ appId }),
});
}; };
...@@ -8,6 +8,7 @@ import type { ...@@ -8,6 +8,7 @@ import type {
McpToolConsent, McpToolConsent,
CreateMcpServer, CreateMcpServer,
} from "@/ipc/ipc_types"; } from "@/ipc/ipc_types";
import { queryKeys } from "@/lib/queryKeys";
export type Transport = "stdio" | "http"; export type Transport = "stdio" | "http";
...@@ -15,7 +16,7 @@ export function useMcp() { ...@@ -15,7 +16,7 @@ export function useMcp() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const serversQuery = useQuery<McpServer[], Error>({ const serversQuery = useQuery<McpServer[], Error>({
queryKey: ["mcp", "servers"], queryKey: queryKeys.mcp.servers,
queryFn: async () => { queryFn: async () => {
const ipc = IpcClient.getInstance(); const ipc = IpcClient.getInstance();
const list = await ipc.listMcpServers(); const list = await ipc.listMcpServers();
...@@ -30,7 +31,7 @@ export function useMcp() { ...@@ -30,7 +31,7 @@ export function useMcp() {
); );
const toolsByServerQuery = useQuery<Record<number, McpTool[]>, Error>({ const toolsByServerQuery = useQuery<Record<number, McpTool[]>, Error>({
queryKey: ["mcp", "tools-by-server", serverIds], queryKey: queryKeys.mcp.toolsByServer.list({ serverIds }),
enabled: serverIds.length > 0, enabled: serverIds.length > 0,
queryFn: async () => { queryFn: async () => {
const ipc = IpcClient.getInstance(); const ipc = IpcClient.getInstance();
...@@ -43,7 +44,7 @@ export function useMcp() { ...@@ -43,7 +44,7 @@ export function useMcp() {
}); });
const consentsQuery = useQuery<McpToolConsent[], Error>({ const consentsQuery = useQuery<McpToolConsent[], Error>({
queryKey: ["mcp", "consents"], queryKey: queryKeys.mcp.consents,
queryFn: async () => { queryFn: async () => {
const ipc = IpcClient.getInstance(); const ipc = IpcClient.getInstance();
const list = await ipc.getMcpToolConsents(); const list = await ipc.getMcpToolConsents();
...@@ -66,9 +67,9 @@ export function useMcp() { ...@@ -66,9 +67,9 @@ export function useMcp() {
return ipc.createMcpServer(params); return ipc.createMcpServer(params);
}, },
onSuccess: async () => { onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["mcp", "servers"] }); await queryClient.invalidateQueries({ queryKey: queryKeys.mcp.servers });
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ["mcp", "tools-by-server"], queryKey: queryKeys.mcp.toolsByServer.all,
}); });
}, },
meta: { showErrorToast: true }, meta: { showErrorToast: true },
...@@ -80,9 +81,9 @@ export function useMcp() { ...@@ -80,9 +81,9 @@ export function useMcp() {
return ipc.updateMcpServer(params); return ipc.updateMcpServer(params);
}, },
onSuccess: async () => { onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["mcp", "servers"] }); await queryClient.invalidateQueries({ queryKey: queryKeys.mcp.servers });
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ["mcp", "tools-by-server"], queryKey: queryKeys.mcp.toolsByServer.all,
}); });
}, },
meta: { showErrorToast: true }, meta: { showErrorToast: true },
...@@ -94,9 +95,9 @@ export function useMcp() { ...@@ -94,9 +95,9 @@ export function useMcp() {
return ipc.deleteMcpServer(id); return ipc.deleteMcpServer(id);
}, },
onSuccess: async () => { onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["mcp", "servers"] }); await queryClient.invalidateQueries({ queryKey: queryKeys.mcp.servers });
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ["mcp", "tools-by-server"], queryKey: queryKeys.mcp.toolsByServer.all,
}); });
}, },
meta: { showErrorToast: true }, meta: { showErrorToast: true },
...@@ -112,7 +113,7 @@ export function useMcp() { ...@@ -112,7 +113,7 @@ export function useMcp() {
return ipc.setMcpToolConsent(params); return ipc.setMcpToolConsent(params);
}, },
onSuccess: async () => { onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["mcp", "consents"] }); await queryClient.invalidateQueries({ queryKey: queryKeys.mcp.consents });
}, },
meta: { showErrorToast: true }, meta: { showErrorToast: true },
}); });
...@@ -137,9 +138,11 @@ export function useMcp() { ...@@ -137,9 +138,11 @@ export function useMcp() {
const refetchAll = async () => { const refetchAll = async () => {
await Promise.all([ await Promise.all([
queryClient.invalidateQueries({ queryKey: ["mcp", "servers"] }), queryClient.invalidateQueries({ queryKey: queryKeys.mcp.servers }),
queryClient.invalidateQueries({ queryKey: ["mcp", "tools-by-server"] }), queryClient.invalidateQueries({
queryClient.invalidateQueries({ queryKey: ["mcp", "consents"] }), queryKey: queryKeys.mcp.toolsByServer.all,
}),
queryClient.invalidateQueries({ queryKey: queryKeys.mcp.consents }),
]); ]);
}; };
......
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { queryKeys } from "@/lib/queryKeys";
export interface PromptItem { export interface PromptItem {
id: number; id: number;
...@@ -13,7 +14,7 @@ export interface PromptItem { ...@@ -13,7 +14,7 @@ export interface PromptItem {
export function usePrompts() { export function usePrompts() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const listQuery = useQuery({ const listQuery = useQuery({
queryKey: ["prompts"], queryKey: queryKeys.prompts.all,
queryFn: async (): Promise<PromptItem[]> => { queryFn: async (): Promise<PromptItem[]> => {
const ipc = IpcClient.getInstance(); const ipc = IpcClient.getInstance();
return ipc.listPrompts(); return ipc.listPrompts();
...@@ -31,7 +32,7 @@ export function usePrompts() { ...@@ -31,7 +32,7 @@ export function usePrompts() {
return ipc.createPrompt(params); return ipc.createPrompt(params);
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["prompts"] }); queryClient.invalidateQueries({ queryKey: queryKeys.prompts.all });
}, },
meta: { meta: {
showErrorToast: true, showErrorToast: true,
...@@ -49,7 +50,7 @@ export function usePrompts() { ...@@ -49,7 +50,7 @@ export function usePrompts() {
return ipc.updatePrompt(params); return ipc.updatePrompt(params);
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["prompts"] }); queryClient.invalidateQueries({ queryKey: queryKeys.prompts.all });
}, },
meta: { meta: {
showErrorToast: true, showErrorToast: true,
...@@ -62,7 +63,7 @@ export function usePrompts() { ...@@ -62,7 +63,7 @@ export function usePrompts() {
return ipc.deletePrompt(id); return ipc.deletePrompt(id);
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["prompts"] }); queryClient.invalidateQueries({ queryKey: queryKeys.prompts.all });
}, },
meta: { meta: {
showErrorToast: true, showErrorToast: true,
......
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import type { ProposalResult } from "@/lib/schemas"; import type { ProposalResult } from "@/lib/schemas";
import { queryKeys } from "@/lib/queryKeys";
export function useProposal(chatId?: number | undefined) { export function useProposal(chatId?: number | undefined) {
const { const {
...@@ -9,7 +10,7 @@ export function useProposal(chatId?: number | undefined) { ...@@ -9,7 +10,7 @@ export function useProposal(chatId?: number | undefined) {
error, error,
refetch: refreshProposal, refetch: refreshProposal,
} = useQuery<ProposalResult | null, Error>({ } = useQuery<ProposalResult | null, Error>({
queryKey: ["proposal", chatId], queryKey: queryKeys.proposals.detail({ chatId }),
queryFn: async (): Promise<ProposalResult | null> => { queryFn: async (): Promise<ProposalResult | null> => {
if (chatId === undefined) { if (chatId === undefined) {
return null; return null;
......
...@@ -3,6 +3,7 @@ import { IpcClient } from "@/ipc/ipc_client"; ...@@ -3,6 +3,7 @@ import { IpcClient } from "@/ipc/ipc_client";
import { showError } from "@/lib/toast"; import { showError } from "@/lib/toast";
import { selectedAppIdAtom } from "@/atoms/appAtoms"; import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { queryKeys } from "@/lib/queryKeys";
interface RenameBranchParams { interface RenameBranchParams {
appId: number; appId: number;
...@@ -30,10 +31,10 @@ export function useRenameBranch() { ...@@ -30,10 +31,10 @@ export function useRenameBranch() {
onSuccess: (_, variables) => { onSuccess: (_, variables) => {
// Invalidate queries that depend on branch information // Invalidate queries that depend on branch information
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["currentBranch", variables.appId], queryKey: queryKeys.branches.current({ appId: variables.appId }),
}); });
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["versions", variables.appId], queryKey: queryKeys.versions.list({ appId: variables.appId }),
}); });
// Potentially show a success message or trigger other actions // Potentially show a success message or trigger other actions
}, },
......
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import type { AppFileSearchResult } from "@/ipc/ipc_types"; import type { AppFileSearchResult } from "@/ipc/ipc_types";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "@/lib/queryKeys";
export function useSearchAppFiles(appId: number | null, query: string) { export function useSearchAppFiles(appId: number | null, query: string) {
const trimmedQuery = query.trim(); const trimmedQuery = query.trim();
const enabled = Boolean(appId != null && trimmedQuery.length > 0); const enabled = Boolean(appId != null && trimmedQuery.length > 0);
const { data, isFetching, isLoading, error } = useQuery({ const { data, isFetching, isLoading, error } = useQuery({
queryKey: ["search-app-files", appId, trimmedQuery], queryKey: queryKeys.files.search({ appId, query: trimmedQuery }),
enabled, enabled,
queryFn: async (): Promise<AppFileSearchResult[]> => { queryFn: async (): Promise<AppFileSearchResult[]> => {
return IpcClient.getInstance().searchAppFiles(appId!, trimmedQuery); return IpcClient.getInstance().searchAppFiles(appId!, trimmedQuery);
......
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { AppSearchResult } from "@/lib/schemas"; import { AppSearchResult } from "@/lib/schemas";
import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { queryKeys } from "@/lib/queryKeys";
export function useSearchApps(query: string) { export function useSearchApps(query: string) {
const enabled = Boolean(query && query.trim().length > 0); const enabled = Boolean(query && query.trim().length > 0);
const { data, isFetching, isLoading } = useQuery({ const { data, isFetching, isLoading } = useQuery({
queryKey: ["search-apps", query], queryKey: queryKeys.apps.search({ query }),
enabled, enabled,
queryFn: async (): Promise<AppSearchResult[]> => { queryFn: async (): Promise<AppSearchResult[]> => {
return IpcClient.getInstance().searchApps(query); return IpcClient.getInstance().searchApps(query);
......
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import type { ChatSearchResult } from "@/lib/schemas"; import type { ChatSearchResult } from "@/lib/schemas";
import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { queryKeys } from "@/lib/queryKeys";
export function useSearchChats(appId: number | null, query: string) { export function useSearchChats(appId: number | null, query: string) {
const enabled = Boolean(appId && query && query.trim().length > 0); const enabled = Boolean(appId && query && query.trim().length > 0);
const { data, isFetching, isLoading } = useQuery({ const { data, isFetching, isLoading } = useQuery({
queryKey: ["search-chats", appId, query], queryKey: queryKeys.chats.search({ appId, query }),
enabled, enabled,
queryFn: async (): Promise<ChatSearchResult[]> => { queryFn: async (): Promise<ChatSearchResult[]> => {
// Non-null assertion safe due to enabled guard // Non-null assertion safe due to enabled guard
......
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { queryKeys } from "@/lib/queryKeys";
export function useSecurityReview(appId: number | null) { export function useSecurityReview(appId: number | null) {
return useQuery({ return useQuery({
queryKey: ["security-review", appId], queryKey: queryKeys.securityReview.byApp({ appId }),
queryFn: async () => { queryFn: async () => {
if (!appId) { if (!appId) {
throw new Error("App ID is required"); throw new Error("App ID is required");
......
...@@ -28,6 +28,7 @@ import { usePostHog } from "posthog-js/react"; ...@@ -28,6 +28,7 @@ import { usePostHog } from "posthog-js/react";
import { useCheckProblems } from "./useCheckProblems"; import { useCheckProblems } from "./useCheckProblems";
import { useSettings } from "./useSettings"; import { useSettings } from "./useSettings";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "@/lib/queryKeys";
export function getRandomNumberId() { export function getRandomNumberId() {
return Math.floor(Math.random() * 1_000_000_000_000_000); return Math.floor(Math.random() * 1_000_000_000_000_000);
...@@ -162,7 +163,9 @@ export function useStreamChat({ ...@@ -162,7 +163,9 @@ export function useStreamChat({
}); });
} }
// Use queryClient directly with the chatId parameter to avoid stale closure issues // Use queryClient directly with the chatId parameter to avoid stale closure issues
queryClient.invalidateQueries({ queryKey: ["proposal", chatId] }); queryClient.invalidateQueries({
queryKey: queryKeys.proposals.detail({ chatId }),
});
refetchUserBudget(); refetchUserBudget();
......
...@@ -12,12 +12,7 @@ import { ...@@ -12,12 +12,7 @@ import {
} from "@/ipc/ipc_types"; } from "@/ipc/ipc_types";
import { useSettings } from "./useSettings"; import { useSettings } from "./useSettings";
import { isSupabaseConnected } from "@/lib/schemas"; import { isSupabaseConnected } from "@/lib/schemas";
import { queryKeys } from "@/lib/queryKeys";
const SUPABASE_QUERY_KEYS = {
organizations: ["supabase", "organizations"] as const,
projects: ["supabase", "projects"] as const,
branches: (projectId: string) => ["supabase", "branches", projectId] as const,
};
export interface UseSupabaseOptions { export interface UseSupabaseOptions {
branchesProjectId?: string | null; branchesProjectId?: string | null;
...@@ -37,7 +32,7 @@ export function useSupabase(options: UseSupabaseOptions = {}) { ...@@ -37,7 +32,7 @@ export function useSupabase(options: UseSupabaseOptions = {}) {
// Query: Load all connected Supabase organizations // Query: Load all connected Supabase organizations
// Only runs when Supabase is connected to avoid unnecessary API calls // Only runs when Supabase is connected to avoid unnecessary API calls
const organizationsQuery = useQuery<SupabaseOrganizationInfo[], Error>({ const organizationsQuery = useQuery<SupabaseOrganizationInfo[], Error>({
queryKey: SUPABASE_QUERY_KEYS.organizations, queryKey: queryKeys.supabase.organizations,
queryFn: async () => { queryFn: async () => {
const ipcClient = IpcClient.getInstance(); const ipcClient = IpcClient.getInstance();
return ipcClient.listSupabaseOrganizations(); return ipcClient.listSupabaseOrganizations();
...@@ -49,7 +44,7 @@ export function useSupabase(options: UseSupabaseOptions = {}) { ...@@ -49,7 +44,7 @@ export function useSupabase(options: UseSupabaseOptions = {}) {
// Query: Load Supabase projects from all connected organizations // Query: Load Supabase projects from all connected organizations
// Only runs when there are connected organizations to avoid unauthorized errors // Only runs when there are connected organizations to avoid unauthorized errors
const projectsQuery = useQuery<SupabaseProject[], Error>({ const projectsQuery = useQuery<SupabaseProject[], Error>({
queryKey: SUPABASE_QUERY_KEYS.projects, queryKey: queryKeys.supabase.projects,
queryFn: async () => { queryFn: async () => {
const ipcClient = IpcClient.getInstance(); const ipcClient = IpcClient.getInstance();
return ipcClient.listAllSupabaseProjects(); return ipcClient.listAllSupabaseProjects();
...@@ -70,9 +65,9 @@ export function useSupabase(options: UseSupabaseOptions = {}) { ...@@ -70,9 +65,9 @@ export function useSupabase(options: UseSupabaseOptions = {}) {
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: SUPABASE_QUERY_KEYS.organizations, queryKey: queryKeys.supabase.organizations,
}); });
queryClient.invalidateQueries({ queryKey: SUPABASE_QUERY_KEYS.projects }); queryClient.invalidateQueries({ queryKey: queryKeys.supabase.projects });
}, },
meta: { showErrorToast: true }, meta: { showErrorToast: true },
}); });
...@@ -101,10 +96,10 @@ export function useSupabase(options: UseSupabaseOptions = {}) { ...@@ -101,10 +96,10 @@ export function useSupabase(options: UseSupabaseOptions = {}) {
// Query: Load branches for a Supabase project // Query: Load branches for a Supabase project
const branchesQuery = useQuery<SupabaseBranch[], Error>({ const branchesQuery = useQuery<SupabaseBranch[], Error>({
queryKey: [ queryKey: queryKeys.supabase.branches({
...SUPABASE_QUERY_KEYS.branches(branchesProjectId ?? ""), projectId: branchesProjectId ?? "",
branchesOrganizationSlug ?? null, organizationSlug: branchesOrganizationSlug ?? null,
], }),
queryFn: async () => { queryFn: async () => {
const ipcClient = IpcClient.getInstance(); const ipcClient = IpcClient.getInstance();
const list = await ipcClient.listSupabaseBranches({ const list = await ipcClient.listSupabaseBranches({
......
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { localTemplatesData, type Template } from "@/shared/templates"; import { localTemplatesData, type Template } from "@/shared/templates";
import { queryKeys } from "@/lib/queryKeys";
export function useTemplates() { export function useTemplates() {
const query = useQuery({ const query = useQuery({
queryKey: ["templates"], queryKey: queryKeys.templates.all,
queryFn: async (): Promise<Template[]> => { queryFn: async (): Promise<Template[]> => {
const ipcClient = IpcClient.getInstance(); const ipcClient = IpcClient.getInstance();
return ipcClient.getTemplates(); return ipcClient.getTemplates();
......
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { themesData, type Theme } from "@/shared/themes"; import { themesData, type Theme } from "@/shared/themes";
import { queryKeys } from "@/lib/queryKeys";
export function useThemes() { export function useThemes() {
const query = useQuery({ const query = useQuery({
queryKey: ["themes"], queryKey: queryKeys.themes.all,
queryFn: async (): Promise<Theme[]> => { queryFn: async (): Promise<Theme[]> => {
const ipcClient = IpcClient.getInstance(); const ipcClient = IpcClient.getInstance();
return ipcClient.getThemes(); return ipcClient.getThemes();
......
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import type { UncommittedFile } from "@/ipc/ipc_types"; import type { UncommittedFile } from "@/ipc/ipc_types";
import { queryKeys } from "@/lib/queryKeys";
export type { UncommittedFile }; export type { UncommittedFile };
...@@ -10,7 +11,7 @@ export function useUncommittedFiles(appId: number | null) { ...@@ -10,7 +11,7 @@ export function useUncommittedFiles(appId: number | null) {
isLoading, isLoading,
refetch: refetchUncommittedFiles, refetch: refetchUncommittedFiles,
} = useQuery<UncommittedFile[], Error>({ } = useQuery<UncommittedFile[], Error>({
queryKey: ["uncommittedFiles", appId], queryKey: queryKeys.uncommittedFiles.byApp({ appId }),
queryFn: async (): Promise<UncommittedFile[]> => { queryFn: async (): Promise<UncommittedFile[]> => {
if (appId === null) { if (appId === null) {
throw new Error("appId is null, cannot fetch uncommitted files."); throw new Error("appId is null, cannot fetch uncommitted files.");
......
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import type { UserBudgetInfo } from "@/ipc/ipc_types"; import type { UserBudgetInfo } from "@/ipc/ipc_types";
import { queryKeys } from "@/lib/queryKeys";
const FIVE_MINUTES_IN_MS = 5 * 60 * 1000; const FIVE_MINUTES_IN_MS = 5 * 60 * 1000;
export function useUserBudgetInfo() { export function useUserBudgetInfo() {
const queryKey = ["userBudgetInfo"];
const { data, isLoading, error, isFetching, refetch } = useQuery< const { data, isLoading, error, isFetching, refetch } = useQuery<
UserBudgetInfo | null, UserBudgetInfo | null,
Error, Error,
UserBudgetInfo | null UserBudgetInfo | null
>({ >({
queryKey: queryKey, queryKey: queryKeys.userBudget.info,
queryFn: async () => { queryFn: async () => {
const ipcClient = IpcClient.getInstance(); const ipcClient = IpcClient.getInstance();
return ipcClient.getUserBudget(); return ipcClient.getUserBudget();
......
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { VercelDeployment } from "@/ipc/ipc_types"; import { VercelDeployment } from "@/ipc/ipc_types";
import { queryKeys } from "@/lib/queryKeys";
export function useVercelDeployments(appId: number) { export function useVercelDeployments(appId: number) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -11,7 +12,7 @@ export function useVercelDeployments(appId: number) { ...@@ -11,7 +12,7 @@ export function useVercelDeployments(appId: number) {
error, error,
refetch, refetch,
} = useQuery<VercelDeployment[], Error>({ } = useQuery<VercelDeployment[], Error>({
queryKey: ["vercel-deployments", appId], queryKey: queryKeys.vercel.deployments({ appId }),
queryFn: async () => { queryFn: async () => {
const ipcClient = IpcClient.getInstance(); const ipcClient = IpcClient.getInstance();
return ipcClient.getVercelDeployments({ appId }); return ipcClient.getVercelDeployments({ appId });
...@@ -26,7 +27,9 @@ export function useVercelDeployments(appId: number) { ...@@ -26,7 +27,9 @@ export function useVercelDeployments(appId: number) {
}, },
onSuccess: () => { onSuccess: () => {
// Clear deployments cache when project is disconnected // Clear deployments cache when project is disconnected
queryClient.removeQueries({ queryKey: ["vercel-deployments", appId] }); queryClient.removeQueries({
queryKey: queryKeys.vercel.deployments({ appId }),
});
}, },
}); });
......
...@@ -6,6 +6,7 @@ import { IpcClient } from "@/ipc/ipc_client"; ...@@ -6,6 +6,7 @@ import { IpcClient } from "@/ipc/ipc_client";
import { chatMessagesByIdAtom, selectedChatIdAtom } from "@/atoms/chatAtoms"; import { chatMessagesByIdAtom, selectedChatIdAtom } from "@/atoms/chatAtoms";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import type { RevertVersionResponse, Version } from "@/ipc/ipc_types"; import type { RevertVersionResponse, Version } from "@/ipc/ipc_types";
import { queryKeys } from "@/lib/queryKeys";
import { toast } from "sonner"; import { toast } from "sonner";
export function useVersions(appId: number | null) { export function useVersions(appId: number | null) {
...@@ -20,7 +21,7 @@ export function useVersions(appId: number | null) { ...@@ -20,7 +21,7 @@ export function useVersions(appId: number | null) {
error, error,
refetch: refreshVersions, refetch: refreshVersions,
} = useQuery<Version[], Error>({ } = useQuery<Version[], Error>({
queryKey: ["versions", appId], queryKey: queryKeys.versions.list({ appId }),
queryFn: async (): Promise<Version[]> => { queryFn: async (): Promise<Version[]> => {
if (appId === null) { if (appId === null) {
return []; return [];
...@@ -71,9 +72,11 @@ export function useVersions(appId: number | null) { ...@@ -71,9 +72,11 @@ export function useVersions(appId: number | null) {
} else if ("warningMessage" in result) { } else if ("warningMessage" in result) {
toast.warning(result.warningMessage); toast.warning(result.warningMessage);
} }
await queryClient.invalidateQueries({ queryKey: ["versions", appId] });
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ["currentBranch", appId], queryKey: queryKeys.versions.list({ appId }),
});
await queryClient.invalidateQueries({
queryKey: queryKeys.branches.current({ appId }),
}); });
if (selectedChatId) { if (selectedChatId) {
const chat = await IpcClient.getInstance().getChat(selectedChatId); const chat = await IpcClient.getInstance().getChat(selectedChatId);
...@@ -84,7 +87,7 @@ export function useVersions(appId: number | null) { ...@@ -84,7 +87,7 @@ export function useVersions(appId: number | null) {
}); });
} }
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ["problems", appId], queryKey: queryKeys.problems.byApp({ appId }),
}); });
}, },
meta: { showErrorToast: true }, meta: { showErrorToast: true },
......
/**
* Centralized React Query key factory.
*
* This pattern provides:
* - Type-safe query keys with full autocomplete
* - Hierarchical structure for easy invalidation (invalidate parent to invalidate children)
* - Consistent naming across the codebase
* - Single source of truth for all query keys
*
* Usage:
* queryKey: queryKeys.apps.detail({ appId })
* queryClient.invalidateQueries({ queryKey: queryKeys.apps.all })
*
* @see https://tkdodo.eu/blog/effective-react-query-keys
*/
export const queryKeys = {
// ─────────────────────────────────────────────────────────────────────────────
// Apps
// ─────────────────────────────────────────────────────────────────────────────
apps: {
all: ["apps"] as const,
detail: ({ appId }: { appId: number | null }) =>
["apps", "detail", appId] as const,
search: ({ query }: { query: string }) =>
["apps", "search", query] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// Chats
// ─────────────────────────────────────────────────────────────────────────────
chats: {
all: ["chats"] as const,
list: ({ appId }: { appId: number | null }) => ["chats", appId] as const,
search: ({ appId, query }: { appId: number | null; query: string }) =>
["chats", "search", appId, query] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// Proposals
// ─────────────────────────────────────────────────────────────────────────────
proposals: {
all: ["proposal"] as const,
detail: ({ chatId }: { chatId: number | undefined }) =>
["proposal", chatId] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// Git / Versions
// ─────────────────────────────────────────────────────────────────────────────
versions: {
all: ["versions"] as const,
list: ({ appId }: { appId: number | null }) => ["versions", appId] as const,
},
branches: {
all: ["currentBranch"] as const,
current: ({ appId }: { appId: number | null }) =>
["currentBranch", appId] as const,
},
uncommittedFiles: {
all: ["uncommittedFiles"] as const,
byApp: ({ appId }: { appId: number | null }) =>
["uncommittedFiles", appId] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// Problems / Diagnostics
// ─────────────────────────────────────────────────────────────────────────────
problems: {
all: ["problems"] as const,
byApp: ({ appId }: { appId: number | null }) =>
["problems", appId] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// Context Paths
// ─────────────────────────────────────────────────────────────────────────────
contextPaths: {
all: ["context-paths"] as const,
byApp: ({ appId }: { appId: number | null }) =>
["context-paths", appId] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// Token Counting
// ─────────────────────────────────────────────────────────────────────────────
tokenCount: {
all: ["tokenCount"] as const,
forChat: ({ chatId, input }: { chatId: number | null; input: string }) =>
["tokenCount", chatId, input] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// Files
// ─────────────────────────────────────────────────────────────────────────────
files: {
search: ({ appId, query }: { appId: number | null; query: string }) =>
["search-app-files", appId, query] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// App Name Check
// ─────────────────────────────────────────────────────────────────────────────
appName: {
check: ({ name }: { name: string }) => ["checkAppName", name] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// Security Review
// ─────────────────────────────────────────────────────────────────────────────
securityReview: {
byApp: ({ appId }: { appId: number | null }) =>
["security-review", appId] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// App Theme
// ─────────────────────────────────────────────────────────────────────────────
appTheme: {
all: ["app-theme"] as const,
byApp: ({ appId }: { appId: number | undefined }) =>
["app-theme", appId] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// Themes (global list)
// ─────────────────────────────────────────────────────────────────────────────
themes: {
all: ["themes"] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// Custom Themes
// ─────────────────────────────────────────────────────────────────────────────
customThemes: {
all: ["custom-themes"] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// Templates
// ─────────────────────────────────────────────────────────────────────────────
templates: {
all: ["templates"] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// Prompts
// ─────────────────────────────────────────────────────────────────────────────
prompts: {
all: ["prompts"] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// Agent Tools
// ─────────────────────────────────────────────────────────────────────────────
agentTools: {
all: ["agent-tools"] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// Language Models / Providers
// ─────────────────────────────────────────────────────────────────────────────
languageModels: {
providers: ["languageModelProviders"] as const,
byProviders: ["language-models-by-providers"] as const,
forProvider: ({ providerId }: { providerId: string }) =>
["language-models", providerId] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// User Budget
// ─────────────────────────────────────────────────────────────────────────────
userBudget: {
info: ["userBudgetInfo"] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// Vercel Deployments
// ─────────────────────────────────────────────────────────────────────────────
vercel: {
all: ["vercel-deployments"] as const,
deployments: ({ appId }: { appId: number }) =>
["vercel-deployments", appId] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// App Upgrades
// ─────────────────────────────────────────────────────────────────────────────
appUpgrades: {
byApp: ({ appId }: { appId: number | null }) =>
["app-upgrades", appId] as const,
isCapacitor: ({ appId }: { appId: number | null }) =>
["is-capacitor", appId] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// MCP (Model Context Protocol)
// ─────────────────────────────────────────────────────────────────────────────
mcp: {
all: ["mcp"] as const,
servers: ["mcp", "servers"] as const,
toolsByServer: {
all: ["mcp", "tools-by-server"] as const,
list: ({ serverIds }: { serverIds: number[] }) =>
["mcp", "tools-by-server", serverIds] as const,
},
consents: ["mcp", "consents"] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// Supabase
// ─────────────────────────────────────────────────────────────────────────────
supabase: {
all: ["supabase"] as const,
organizations: ["supabase", "organizations"] as const,
projects: ["supabase", "projects"] as const,
branches: ({
projectId,
organizationSlug,
}: {
projectId: string;
organizationSlug: string | null;
}) => ["supabase", "branches", projectId, organizationSlug] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// Neon
// ─────────────────────────────────────────────────────────────────────────────
neon: {
project: ({ appId }: { appId: number | null }) =>
["neon-project", appId] as const,
},
// ─────────────────────────────────────────────────────────────────────────────
// App Environment Variables
// ─────────────────────────────────────────────────────────────────────────────
appEnvVars: {
byApp: ({ appId }: { appId: number | null }) =>
["app-env-vars", appId] as const,
},
} as const;
// ─────────────────────────────────────────────────────────────────────────────
// Type helpers for extracting query key types
// ─────────────────────────────────────────────────────────────────────────────
/** Extract the type of a query key from a factory function or constant */
export type QueryKeyOf<T> = T extends readonly unknown[]
? T
: T extends (...args: never[]) => infer R
? R
: never;
/** All possible query keys (useful for typing queryClient operations) */
export type AppQueryKey =
| QueryKeyOf<(typeof queryKeys.apps)[keyof typeof queryKeys.apps]>
| QueryKeyOf<(typeof queryKeys.chats)[keyof typeof queryKeys.chats]>
| QueryKeyOf<(typeof queryKeys.proposals)[keyof typeof queryKeys.proposals]>
| QueryKeyOf<(typeof queryKeys.versions)[keyof typeof queryKeys.versions]>
| QueryKeyOf<(typeof queryKeys.branches)[keyof typeof queryKeys.branches]>
| QueryKeyOf<
(typeof queryKeys.uncommittedFiles)[keyof typeof queryKeys.uncommittedFiles]
>
| QueryKeyOf<(typeof queryKeys.problems)[keyof typeof queryKeys.problems]>
| QueryKeyOf<
(typeof queryKeys.contextPaths)[keyof typeof queryKeys.contextPaths]
>
| QueryKeyOf<(typeof queryKeys.tokenCount)[keyof typeof queryKeys.tokenCount]>
| QueryKeyOf<(typeof queryKeys.files)[keyof typeof queryKeys.files]>
| QueryKeyOf<(typeof queryKeys.appName)[keyof typeof queryKeys.appName]>
| QueryKeyOf<
(typeof queryKeys.securityReview)[keyof typeof queryKeys.securityReview]
>
| QueryKeyOf<(typeof queryKeys.appTheme)[keyof typeof queryKeys.appTheme]>
| QueryKeyOf<(typeof queryKeys.themes)[keyof typeof queryKeys.themes]>
| QueryKeyOf<
(typeof queryKeys.customThemes)[keyof typeof queryKeys.customThemes]
>
| QueryKeyOf<(typeof queryKeys.templates)[keyof typeof queryKeys.templates]>
| QueryKeyOf<(typeof queryKeys.prompts)[keyof typeof queryKeys.prompts]>
| QueryKeyOf<(typeof queryKeys.agentTools)[keyof typeof queryKeys.agentTools]>
| QueryKeyOf<
(typeof queryKeys.languageModels)[keyof typeof queryKeys.languageModels]
>
| QueryKeyOf<(typeof queryKeys.userBudget)[keyof typeof queryKeys.userBudget]>
| QueryKeyOf<(typeof queryKeys.vercel)[keyof typeof queryKeys.vercel]>
| QueryKeyOf<
(typeof queryKeys.appUpgrades)[keyof typeof queryKeys.appUpgrades]
>
| QueryKeyOf<(typeof queryKeys.mcp)[keyof typeof queryKeys.mcp]>
| QueryKeyOf<(typeof queryKeys.supabase)[keyof typeof queryKeys.supabase]>
| QueryKeyOf<(typeof queryKeys.neon)[keyof typeof queryKeys.neon]>
| QueryKeyOf<
(typeof queryKeys.appEnvVars)[keyof typeof queryKeys.appEnvVars]
>;
...@@ -18,6 +18,7 @@ import { ...@@ -18,6 +18,7 @@ import {
pendingAgentConsentsAtom, pendingAgentConsentsAtom,
agentTodosByChatIdAtom, agentTodosByChatIdAtom,
} from "./atoms/chatAtoms"; } from "./atoms/chatAtoms";
import { queryKeys } from "./lib/queryKeys";
// @ts-ignore // @ts-ignore
console.log("Running in mode:", import.meta.env.MODE); console.log("Running in mode:", import.meta.env.MODE);
...@@ -202,7 +203,10 @@ function App() { ...@@ -202,7 +203,10 @@ function App() {
useEffect(() => { useEffect(() => {
const ipc = IpcClient.getInstance(); const ipc = IpcClient.getInstance();
const unsubscribe = ipc.onAgentProblemsUpdate((payload) => { const unsubscribe = ipc.onAgentProblemsUpdate((payload) => {
queryClient.setQueryData(["problems", payload.appId], payload.problems); queryClient.setQueryData(
queryKeys.problems.byApp({ appId: payload.appId }),
payload.problems,
);
}); });
return () => unsubscribe(); return () => unsubscribe();
}, []); }, []);
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论