diff --git a/apps/ui/package.json b/apps/ui/package.json
index b28ad8c7..200df952 100644
--- a/apps/ui/package.json
+++ b/apps/ui/package.json
@@ -63,7 +63,8 @@
"@radix-ui/react-switch": "1.2.6",
"@radix-ui/react-tabs": "1.1.13",
"@radix-ui/react-tooltip": "1.2.8",
- "@tanstack/react-query": "5.90.12",
+ "@tanstack/react-query": "^5.90.17",
+ "@tanstack/react-query-devtools": "^5.91.2",
"@tanstack/react-router": "1.141.6",
"@uiw/react-codemirror": "4.25.4",
"@xterm/addon-fit": "0.10.0",
diff --git a/apps/ui/src/lib/query-client.ts b/apps/ui/src/lib/query-client.ts
new file mode 100644
index 00000000..82344f2a
--- /dev/null
+++ b/apps/ui/src/lib/query-client.ts
@@ -0,0 +1,138 @@
+/**
+ * React Query Client Configuration
+ *
+ * Central configuration for TanStack React Query.
+ * Provides default options for queries and mutations including
+ * caching, retries, and error handling.
+ */
+
+import { QueryClient } from '@tanstack/react-query';
+import { toast } from 'sonner';
+import { createLogger } from '@automaker/utils/logger';
+import { isConnectionError, handleServerOffline } from './http-api-client';
+
+const logger = createLogger('QueryClient');
+
+/**
+ * Default stale times for different data types
+ */
+export const STALE_TIMES = {
+ /** Features change frequently during auto-mode */
+ FEATURES: 60 * 1000, // 1 minute
+ /** GitHub data is relatively stable */
+ GITHUB: 2 * 60 * 1000, // 2 minutes
+ /** Running agents state changes very frequently */
+ RUNNING_AGENTS: 5 * 1000, // 5 seconds
+ /** Agent output changes during streaming */
+ AGENT_OUTPUT: 5 * 1000, // 5 seconds
+ /** Usage data with polling */
+ USAGE: 30 * 1000, // 30 seconds
+ /** Models rarely change */
+ MODELS: 5 * 60 * 1000, // 5 minutes
+ /** CLI status rarely changes */
+ CLI_STATUS: 5 * 60 * 1000, // 5 minutes
+ /** Settings are relatively stable */
+ SETTINGS: 2 * 60 * 1000, // 2 minutes
+ /** Worktrees change during feature development */
+ WORKTREES: 30 * 1000, // 30 seconds
+ /** Sessions rarely change */
+ SESSIONS: 2 * 60 * 1000, // 2 minutes
+ /** Default for unspecified queries */
+ DEFAULT: 30 * 1000, // 30 seconds
+} as const;
+
+/**
+ * Default garbage collection times (gcTime, formerly cacheTime)
+ */
+export const GC_TIMES = {
+ /** Default garbage collection time */
+ DEFAULT: 5 * 60 * 1000, // 5 minutes
+ /** Extended for expensive queries */
+ EXTENDED: 10 * 60 * 1000, // 10 minutes
+} as const;
+
+/**
+ * Global error handler for queries
+ */
+const handleQueryError = (error: Error) => {
+ logger.error('Query error:', error);
+
+ // Check for connection errors (server offline)
+ if (isConnectionError(error)) {
+ handleServerOffline();
+ return;
+ }
+
+ // Don't toast for auth errors - those are handled by http-api-client
+ if (error.message === 'Unauthorized') {
+ return;
+ }
+};
+
+/**
+ * Global error handler for mutations
+ */
+const handleMutationError = (error: Error) => {
+ logger.error('Mutation error:', error);
+
+ // Check for connection errors
+ if (isConnectionError(error)) {
+ handleServerOffline();
+ return;
+ }
+
+ // Don't toast for auth errors
+ if (error.message === 'Unauthorized') {
+ return;
+ }
+
+ // Show error toast for other errors
+ toast.error('Operation failed', {
+ description: error.message || 'An unexpected error occurred',
+ });
+};
+
+/**
+ * Create and configure the QueryClient singleton
+ */
+export const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: STALE_TIMES.DEFAULT,
+ gcTime: GC_TIMES.DEFAULT,
+ retry: (failureCount, error) => {
+ // Don't retry on auth errors
+ if (error instanceof Error && error.message === 'Unauthorized') {
+ return false;
+ }
+ // Don't retry on connection errors (server offline)
+ if (isConnectionError(error)) {
+ return false;
+ }
+ // Retry up to 2 times for other errors
+ return failureCount < 2;
+ },
+ refetchOnWindowFocus: true,
+ refetchOnReconnect: true,
+ // Don't refetch on mount if data is fresh
+ refetchOnMount: true,
+ },
+ mutations: {
+ onError: handleMutationError,
+ retry: false, // Don't auto-retry mutations
+ },
+ },
+});
+
+/**
+ * Set up global query error handling
+ * This catches errors that aren't handled by individual queries
+ */
+queryClient.getQueryCache().subscribe((event) => {
+ if (event.type === 'updated' && event.query.state.status === 'error') {
+ const error = event.query.state.error;
+ if (error instanceof Error) {
+ handleQueryError(error);
+ }
+ }
+});
diff --git a/apps/ui/src/lib/query-keys.ts b/apps/ui/src/lib/query-keys.ts
new file mode 100644
index 00000000..b0eb7291
--- /dev/null
+++ b/apps/ui/src/lib/query-keys.ts
@@ -0,0 +1,280 @@
+/**
+ * Query Keys Factory
+ *
+ * Centralized query key definitions for React Query.
+ * Following the factory pattern for type-safe, consistent query keys.
+ *
+ * @see https://tkdodo.eu/blog/effective-react-query-keys
+ */
+
+/**
+ * Query keys for all API endpoints
+ *
+ * Structure follows the pattern:
+ * - ['entity'] for listing/global
+ * - ['entity', id] for single item
+ * - ['entity', id, 'sub-resource'] for nested resources
+ */
+export const queryKeys = {
+ // ============================================
+ // Features
+ // ============================================
+ features: {
+ /** All features for a project */
+ all: (projectPath: string) => ['features', projectPath] as const,
+ /** Single feature */
+ single: (projectPath: string, featureId: string) =>
+ ['features', projectPath, featureId] as const,
+ /** Agent output for a feature */
+ agentOutput: (projectPath: string, featureId: string) =>
+ ['features', projectPath, featureId, 'output'] as const,
+ },
+
+ // ============================================
+ // Worktrees
+ // ============================================
+ worktrees: {
+ /** All worktrees for a project */
+ all: (projectPath: string) => ['worktrees', projectPath] as const,
+ /** Single worktree info */
+ single: (projectPath: string, featureId: string) =>
+ ['worktrees', projectPath, featureId] as const,
+ /** Branches for a worktree */
+ branches: (worktreePath: string) => ['worktrees', 'branches', worktreePath] as const,
+ /** Worktree status */
+ status: (projectPath: string, featureId: string) =>
+ ['worktrees', projectPath, featureId, 'status'] as const,
+ /** Worktree diffs */
+ diffs: (projectPath: string, featureId: string) =>
+ ['worktrees', projectPath, featureId, 'diffs'] as const,
+ /** Init script for a project */
+ initScript: (projectPath: string) => ['worktrees', projectPath, 'init-script'] as const,
+ /** Available editors */
+ editors: () => ['worktrees', 'editors'] as const,
+ },
+
+ // ============================================
+ // GitHub
+ // ============================================
+ github: {
+ /** GitHub issues for a project */
+ issues: (projectPath: string) => ['github', 'issues', projectPath] as const,
+ /** GitHub PRs for a project */
+ prs: (projectPath: string) => ['github', 'prs', projectPath] as const,
+ /** GitHub validations for a project */
+ validations: (projectPath: string) => ['github', 'validations', projectPath] as const,
+ /** Single validation */
+ validation: (projectPath: string, issueNumber: number) =>
+ ['github', 'validations', projectPath, issueNumber] as const,
+ /** Issue comments */
+ issueComments: (projectPath: string, issueNumber: number) =>
+ ['github', 'issues', projectPath, issueNumber, 'comments'] as const,
+ /** Remote info */
+ remote: (projectPath: string) => ['github', 'remote', projectPath] as const,
+ },
+
+ // ============================================
+ // Settings
+ // ============================================
+ settings: {
+ /** Global settings */
+ global: () => ['settings', 'global'] as const,
+ /** Project-specific settings */
+ project: (projectPath: string) => ['settings', 'project', projectPath] as const,
+ /** Settings status */
+ status: () => ['settings', 'status'] as const,
+ /** Credentials (API keys) */
+ credentials: () => ['settings', 'credentials'] as const,
+ /** Discovered agents */
+ agents: (projectPath: string) => ['settings', 'agents', projectPath] as const,
+ },
+
+ // ============================================
+ // Usage & Billing
+ // ============================================
+ usage: {
+ /** Claude API usage */
+ claude: () => ['usage', 'claude'] as const,
+ /** Codex API usage */
+ codex: () => ['usage', 'codex'] as const,
+ },
+
+ // ============================================
+ // Models
+ // ============================================
+ models: {
+ /** Available models */
+ available: () => ['models', 'available'] as const,
+ /** Codex models */
+ codex: () => ['models', 'codex'] as const,
+ /** OpenCode models */
+ opencode: () => ['models', 'opencode'] as const,
+ /** OpenCode providers */
+ opencodeProviders: () => ['models', 'opencode', 'providers'] as const,
+ /** Provider status */
+ providers: () => ['models', 'providers'] as const,
+ },
+
+ // ============================================
+ // Sessions
+ // ============================================
+ sessions: {
+ /** All sessions */
+ all: (includeArchived?: boolean) => ['sessions', { includeArchived }] as const,
+ /** Session history */
+ history: (sessionId: string) => ['sessions', sessionId, 'history'] as const,
+ /** Session queue */
+ queue: (sessionId: string) => ['sessions', sessionId, 'queue'] as const,
+ },
+
+ // ============================================
+ // Running Agents
+ // ============================================
+ runningAgents: {
+ /** All running agents */
+ all: () => ['runningAgents'] as const,
+ },
+
+ // ============================================
+ // Auto Mode
+ // ============================================
+ autoMode: {
+ /** Auto mode status */
+ status: (projectPath?: string) => ['autoMode', 'status', projectPath] as const,
+ /** Context exists check */
+ contextExists: (projectPath: string, featureId: string) =>
+ ['autoMode', projectPath, featureId, 'context'] as const,
+ },
+
+ // ============================================
+ // Ideation
+ // ============================================
+ ideation: {
+ /** Ideation prompts */
+ prompts: () => ['ideation', 'prompts'] as const,
+ /** Ideas for a project */
+ ideas: (projectPath: string) => ['ideation', 'ideas', projectPath] as const,
+ /** Single idea */
+ idea: (projectPath: string, ideaId: string) =>
+ ['ideation', 'ideas', projectPath, ideaId] as const,
+ /** Session */
+ session: (projectPath: string, sessionId: string) =>
+ ['ideation', 'session', projectPath, sessionId] as const,
+ },
+
+ // ============================================
+ // CLI Status
+ // ============================================
+ cli: {
+ /** Claude CLI status */
+ claude: () => ['cli', 'claude'] as const,
+ /** Cursor CLI status */
+ cursor: () => ['cli', 'cursor'] as const,
+ /** Codex CLI status */
+ codex: () => ['cli', 'codex'] as const,
+ /** OpenCode CLI status */
+ opencode: () => ['cli', 'opencode'] as const,
+ /** GitHub CLI status */
+ github: () => ['cli', 'github'] as const,
+ /** API keys status */
+ apiKeys: () => ['cli', 'apiKeys'] as const,
+ /** Platform info */
+ platform: () => ['cli', 'platform'] as const,
+ },
+
+ // ============================================
+ // Cursor Permissions
+ // ============================================
+ cursorPermissions: {
+ /** Cursor permissions for a project */
+ permissions: (projectPath?: string) => ['cursorPermissions', projectPath] as const,
+ },
+
+ // ============================================
+ // Workspace
+ // ============================================
+ workspace: {
+ /** Workspace config */
+ config: () => ['workspace', 'config'] as const,
+ /** Workspace directories */
+ directories: () => ['workspace', 'directories'] as const,
+ },
+
+ // ============================================
+ // MCP (Model Context Protocol)
+ // ============================================
+ mcp: {
+ /** MCP server tools */
+ tools: (serverId: string) => ['mcp', 'tools', serverId] as const,
+ },
+
+ // ============================================
+ // Pipeline
+ // ============================================
+ pipeline: {
+ /** Pipeline config for a project */
+ config: (projectPath: string) => ['pipeline', projectPath] as const,
+ },
+
+ // ============================================
+ // Suggestions
+ // ============================================
+ suggestions: {
+ /** Suggestions status */
+ status: () => ['suggestions', 'status'] as const,
+ },
+
+ // ============================================
+ // Spec Regeneration
+ // ============================================
+ specRegeneration: {
+ /** Spec regeneration status */
+ status: (projectPath?: string) => ['specRegeneration', 'status', projectPath] as const,
+ },
+
+ // ============================================
+ // Spec
+ // ============================================
+ spec: {
+ /** Spec file content */
+ file: (projectPath: string) => ['spec', 'file', projectPath] as const,
+ },
+
+ // ============================================
+ // Context
+ // ============================================
+ context: {
+ /** File description */
+ file: (filePath: string) => ['context', 'file', filePath] as const,
+ /** Image description */
+ image: (imagePath: string) => ['context', 'image', imagePath] as const,
+ },
+
+ // ============================================
+ // File System
+ // ============================================
+ fs: {
+ /** Directory listing */
+ readdir: (dirPath: string) => ['fs', 'readdir', dirPath] as const,
+ /** File existence */
+ exists: (filePath: string) => ['fs', 'exists', filePath] as const,
+ /** File stats */
+ stat: (filePath: string) => ['fs', 'stat', filePath] as const,
+ },
+
+ // ============================================
+ // Git
+ // ============================================
+ git: {
+ /** Git diffs for a project */
+ diffs: (projectPath: string) => ['git', 'diffs', projectPath] as const,
+ /** File diff */
+ fileDiff: (projectPath: string, filePath: string) =>
+ ['git', 'diffs', projectPath, filePath] as const,
+ },
+} as const;
+
+/**
+ * Type helper to extract query key types
+ */
+export type QueryKeys = typeof queryKeys;
diff --git a/apps/ui/src/routes/__root.tsx b/apps/ui/src/routes/__root.tsx
index 9472545b..a139dbe9 100644
--- a/apps/ui/src/routes/__root.tsx
+++ b/apps/ui/src/routes/__root.tsx
@@ -1,5 +1,7 @@
import { createRootRoute, Outlet, useLocation, useNavigate } from '@tanstack/react-router';
import { useEffect, useState, useCallback, useDeferredValue, useRef } from 'react';
+import { QueryClientProvider } from '@tanstack/react-query';
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { createLogger } from '@automaker/utils/logger';
import { Sidebar } from '@/components/layout/sidebar';
import { ProjectSwitcher } from '@/components/layout/project-switcher';
@@ -27,6 +29,7 @@ import {
signalMigrationComplete,
performSettingsMigration,
} from '@/hooks/use-settings-migration';
+import { queryClient } from '@/lib/query-client';
import { Toaster } from 'sonner';
import { Menu } from 'lucide-react';
import { ThemeOption, themeOptions } from '@/config/theme-options';
@@ -856,9 +859,12 @@ function RootLayoutContent() {
function RootLayout() {
return (
-
-
-
+
+
+
+
+
+
);
}
diff --git a/package-lock.json b/package-lock.json
index 00e0d253..41e0bfaf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,7 +29,7 @@
},
"apps/server": {
"name": "@automaker/server",
- "version": "0.10.0",
+ "version": "0.11.0",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "0.1.76",
@@ -80,7 +80,7 @@
},
"apps/ui": {
"name": "@automaker/ui",
- "version": "0.10.0",
+ "version": "0.11.0",
"hasInstallScript": true,
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
@@ -108,7 +108,8 @@
"@radix-ui/react-switch": "1.2.6",
"@radix-ui/react-tabs": "1.1.13",
"@radix-ui/react-tooltip": "1.2.8",
- "@tanstack/react-query": "5.90.12",
+ "@tanstack/react-query": "^5.90.17",
+ "@tanstack/react-query-devtools": "^5.91.2",
"@tanstack/react-router": "1.141.6",
"@uiw/react-codemirror": "4.25.4",
"@xterm/addon-fit": "0.10.0",
@@ -1483,7 +1484,7 @@
},
"node_modules/@electron/node-gyp": {
"version": "10.2.0-electron.1",
- "resolved": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2",
+ "resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2",
"integrity": "sha512-4MSBTT8y07YUDqf69/vSh80Hh791epYqGtWHO3zSKhYFwQg+gx9wi1PqbqP6YqC4WMsNxZ5l9oDmnWdK5pfCKQ==",
"dev": true,
"license": "MIT",
@@ -5788,9 +5789,19 @@
}
},
"node_modules/@tanstack/query-core": {
- "version": "5.90.12",
- "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.12.tgz",
- "integrity": "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==",
+ "version": "5.90.17",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.17.tgz",
+ "integrity": "sha512-hDww+RyyYhjhUfoYQ4es6pbgxY7LNiPWxt4l1nJqhByjndxJ7HIjDxTBtfvMr5HwjYavMrd+ids5g4Rfev3lVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/query-devtools": {
+ "version": "5.92.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.92.0.tgz",
+ "integrity": "sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ==",
"license": "MIT",
"funding": {
"type": "github",
@@ -5798,12 +5809,13 @@
}
},
"node_modules/@tanstack/react-query": {
- "version": "5.90.12",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.12.tgz",
- "integrity": "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==",
+ "version": "5.90.17",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.17.tgz",
+ "integrity": "sha512-PGc2u9KLwohDUSchjW9MZqeDQJfJDON7y4W7REdNBgiFKxQy+Pf7eGjiFWEj5xPqKzAeHYdAb62IWI1a9UJyGQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
- "@tanstack/query-core": "5.90.12"
+ "@tanstack/query-core": "5.90.17"
},
"funding": {
"type": "github",
@@ -5813,6 +5825,23 @@
"react": "^18 || ^19"
}
},
+ "node_modules/@tanstack/react-query-devtools": {
+ "version": "5.91.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.91.2.tgz",
+ "integrity": "sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-devtools": "5.92.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "@tanstack/react-query": "^5.90.14",
+ "react": "^18 || ^19"
+ }
+ },
"node_modules/@tanstack/react-router": {
"version": "1.141.6",
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.141.6.tgz",